Django manyToManyField initial value based on another object using Formwizard - django

I have the model below:
class DrawingRevision(models.Model):
revision = models.CharField(max_length = 10)
previous_revision = models.ForeignKey('self', blank=True, null=True)
drawing = models.ForeignKey(Drawing)
part = models.ManyToManyField(Part, blank=True, null=True)
I have a formWizard which allows the user to select previous_revision. I would like to be able to have the list of parts from previous_revision already selected when the user gets to the last page of the formWizard. My thought is to do this in the ModelForm for DrawingRevision, but I'm not really sure how to go about that.
class DrawingRevisionForm(forms.ModelForm):
class Meta:
model = DrawingRevision
exclude = ('drawing','revision', 'previous_revision',)
def __init__(self, *args, **kwargs):
self(DrawingRevisoinForm, self).__init__(*args, **kwargs)
self.fields['revision_date'].widget = widgets.AdminDateWidget()
Thanks for your help!
EDIT:
I've made some progress with this. In my formwizard class, I have the following process_step method defined. It gets called just before I need to display the manytomany field with the initial values set. Below, 'd' is the list of parts from the previous drawing and nextform should be the form to be displayed (although type(self.form_list[1]) tells me its a declarativefieldsmetaclass). What I need to do is get the list of parts (d) to be the initial values for the next pages parts field.
def process_step(self, request, form, step):
if step == 1:
d = DrawingRevision.objects.filter(id__exact=request.POST['0-prerev'])[0].part.all()
nextform = self.form_list[1]
EDIT 2:
I'm making a little more progress with this. I think that I need to set the initial member of the DrawingRevision form to something like:
nextform.initial = {'part': [DrawingRevision.objects.filter(id__exact=request.POST['0-prerev'])[0].part.all()], }
probably in parse_params(). But when I do this, the initial values are not selected on the last form, but rather everything is deselected.

I added this in parse_params() and it worked!:
if request.method == 'POST' and current_step == 1:
form = self.get_form(current_step, request.POST)
if form.is_valid():
self.initial[(current_step + 1)] = {'part': DrawingRevision.objects.filter(id__exact=request.POST['0-prerev'])[0].part.all(), }
I hope somebody finds this useful.

Related

Using field values from a ForeignKeyed object for ModelChoiceFields

My friends and I play a spreadsheet-based sports picking game which is very tedious to make changes to. I've wanted to learn Django for a while so I've been working on creating it as a webapp. Here are the models I'm working with:
class Sheet(models.Model):
user = models.ForeignKey(User)
... other stuff
class Game(models.Model):
home_team = models.CharField(max_length=100, default='---')
away_team = models.CharField(max_length=100, default='---')
... other stuff
class Pick(models.Model):
sheet = models.ForeignKey(Sheet)
game = models.ForeignKey(Game)
HOME = 'H'
AWAY = 'A'
PICK_TEAM_CHOICES = (
(HOME, 'Home'),
(AWAY, 'Away'),
)
pick_team = models.CharField(max_length=4,
choices=PICK_TEAM_CHOICES,
default=HOME)
... other stuff
Right now, I'm trying to nail down a simple way to display the following form with information from a foreign keyed model instead of the pick_team default choices. The game is hidden because it's paired with the generated PickForm via use of the initial functionality in the view.
class PickForm(ModelForm):
class Meta:
model = Pick
widgets = {'game': forms.HiddenInput()}
fields = ['sheet','game','amount','pick_type','pick_team']
def __init__(self, *args, **kwargs):
game = kwargs['initial']['game']
super(PickForm, self).__init__(*args, **kwargs)
self.fields['pick_team']=forms.ModelChoiceField([game.away_team,game.home_team])
From what I can tell, the ModelChoiceField expects a queryset- so when I provide a list or a tuple, I get a 'list' object has no attribute 'iterator' error. Knowing this now, how can I display the Game fields home_team and away_team in the pick_team dropdown on the template? Currently it defaults to 'Home' and 'Away'.
I know this is a common question at the core- how to display ForeignKeyed information in a dropdown, however all the answers I've found are fine with providing a queryset to ModelChoiceField, because they're typically trying to list a field from every object (or some filtered subset). In this case, I only want to list 2 fields, and only from one object.
I tried returning a queryset consisting of the Game object already present in kwargs, but it just displays the game's str() method in the dropdown, and attempting to refine the queryset with the relevant field names isn't working either.
EDIT: I realized that actually using the home_team and away_team values from the Game object would require extra processing on saving the Pick, or possibly be harder than that. Is there any way to do this sort of aliasing in the template alone? Similar to how with choice fields I can use get_pick_team_display to show a nicer looking display value ('Home', 'Away') instead of the vague 'H' or 'A'.
EDIT2: View code
class GameDetail(DetailView):
#model = Game
template_name = 'app/games.html'
context_object_name = 'game_detail'
def get_object(self):
game = get_object_or_404(...object filtering)
return game
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
pick = Pick.objects.get(game=context['game_detail'],
....other stuff)
context['pickform'] = PickForm(initial={'game':context['game_detail'],
.... other stuff
except Pick.DoesNotExist:
#pick = none
context['pickform'] = PickForm(initial={'game':context['game_detail'],
})
return context
def post(self, request, *args, **kwargs):
form = PickForm(request.POST)
if form.is_valid():
....process form
This is a quickfix approach. In your __init__, instead of reassigning pick_team field, just redefine its options as follows:
self.fields['pick_team'].choices = (
('H', game.home_team),
('A', game.away_team),
)

Django: Different form field widget per form in formset

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

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

Django custom update model form - display selected field of related model rather than foreign key id.

My question is: is there a way to create custom model form that will use a specified field from a related model rather than the related model's id?
To clarify, if I have the following two models:
class ModelOne(models.Model):
id = models.AutoField(primary_key = True)
name = models.CharField(unique = True, blank = False, null = False)
class ModelTwo(models.Model):
id = models.AutoField(primary_key = True)
parent = models.ForeignKey(ModelOne, blank = False, null = False)
attribute_1 = models.CharField(blank = False, null = False)
attribute_2 = models.IntegerField(blank = False, null = False)
Now if I create an UpdateView on ModelTwo using a ModelForm then the parent field will be pre-filled with the corresponding id from ModelOne. However I want it to display the name attribute of ModelOne and then on form submission parse the unique name (of ModelOne) to the corresponding instance of ModelOne. The reason I want to do it this way, is that I believe it is far more intuitive from a users perspective to deal with the name of ModelOne (when updating a ModelTwo instance) rather than its "id".
Any suggestions of how I can do this?
Firstly, try defining the unicode method on ModelOne. It might not apply to the solution, but it's worth having - it will drive the text values in a form Select widget...
def __unicode__(self):
'''Warning: be careful not to use related objects here,
could cause unwanted DB hits when debugging/logging
'''
return self.name
If that's not sufficient, something like this might work (it is adapted from a form I have that updates the user's name attached to a profile)...
class M2Form(forms.ModelForm):
m1_name = forms.CharField()
class Meta:
model = ModelTwo
def save(self, *args, **kw):
# Update your name field here, something like
if self.cleaned_data.get('m1_name'):
self.instance.parent = ModelOne.objects.get(name=self.cleaned_data.get('m1_name'))
return super(M2Form, self).save(*args, **kw)
This is untested, and you'll likely need to adapt this to validate that the name exists and make sure the original parent field doesn't appear on the form. With any luck, the first answer covers what I think your question is.
Using Rog's answer as a starting point and delving through some of Django's internals I eventually came to a working solution. Given my level of Django knowledge, I imagine there is a better way of doing this; so if you have another method please add it.
So based on the above two models, I created the following form class:
class CustomForm(forms.ModelForm):
parent = models.CharField(label='Name')
class Meta:
model = ModelTwo
exclude = ['parent']
def __init__(self,*args,**kwargs):
# The line of code below is the one that I was looking for. It pre-populates
# the "parent" field of the form with the "name" attribute of the related
# ModelOne instance.
kwargs['initial']['parent'] = kwargs['instance'].parent.name
super(CustomForm,self).__init__(*args,**kwargs)
# The next line is for convenience and orders the form fields in our desired
# order. I got this tip from:
# http://stackoverflow.com/questions/913589/django-forms-inheritance-and-order-of-form-fields
self.fields.keyOrder = ['parent','attribute_1','attribute_2']
def save(self, *args, **kwargs):
if self.cleaned_data.get('parent'):
# This section of code is important because we need to convert back from the
# unique 'name' attribute of ModelOne to the corresponding instance so that
# ModelTwo can be saved. Thanks goes to Rog for this section of code.
self.instance.parent = ModelOne.objects.get(name=self.cleaned_data.get('parent'))
return super(CustomForm, self).save(*args, **kwargs)

Django "Enter a list of values" form error when rendering a ManyToManyField as a Textarea

I'm trying to learn Django and I've ran into some confusing points. I'm currently having trouble creating a movie using a form. The idea of the form is to give the user any field he'd like to fill out. Any field that the user fills out will be updated in its respective sql table (empty fields will be ignored). But, the form keeps giving me the error "Enter a list of values" when I submit the form. To address this, I thought stuffing the data from the form into a list and then returning that list would solve this.
The first idea was to override the clean() in my ModelForm. However, because the form fails the is_valid() check in my views, the cleaned_data variable in clean() doesn't contain anything. Next, I tried to override the to_python(). However, to_python() doesn't seem to be called.
If I put __metaclass__ = models.SubfieldBase in the respective model, I receive the runtime error
"TypeError: Error when calling the
metaclass bases
metaclass conflict: the metaclass of a derived class must be a
(non-strict) subclass of the
metaclasses of all its bases"
My approach doesn't seem to work. I'm not sure how to get around the 'Enter a list of values" error! Any advice?
Here is the relevant code (updated):
models.py
""" Idea:
A movie consists of many equipments, actors, and lighting techniques. It also has a rank for the particular movie, as well as a title.
A Theater consists of many movies.
A nation consists of many theaters.
"""
from django.db import models
from django.contrib.auth.models import User
class EquipmentModel(models.Model):
equip = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class ActorModel(models.Model):
actor = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class LightModel(models.Model):
light = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class MovieModel(models.Model):
# __metaclass__ = models.SubfieldBase
rank = models.DecimalField(max_digits=5000, decimal_places=3)
title = models.CharField(max_length=20)
equipments = models.ManyToManyField(EquipmentModel, blank=True, null=True)
actors = models.ManyToManyField(ActorModel, blank=True, null=True)
lights = models.ManyToManyField(LightModel, blank=True, null=True)
class TheaterModel(models.Model):
movies = models.ForeignKey(MovieModel)
class NationModel(models.Model):
theaters = models.ForeignKey(TheaterModel)
=====================================
forms.py
"""
These Modelforms tie in the models from models.py
Users will be able to write to any of the fields in MovieModel when creating a movie.
Users may leave any field blank (empty fields should be ignored, ie: no updates to database).
"""
from django import forms
from models import MovieModel
from django.forms.widgets import Textarea
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
self.fields["equipments"].widget = Textarea()
self.fields["lights"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
print 'cleaning actors'
return [data]
class Meta:
model = MovieModel
=============================================
views.py
""" This will display the form used to create a MovieModel """
from django.shortcuts import render_to_response
from django.template import RequestContext
from forms import MovieModelForm
def add_movie(request):
if request.method == "POST":
form = MovieModelForm(request.POST)
if form.is_valid():
new_moviemodel = form.save()
return HttpResponseRedirect('/data/')
else:
form = MovieModelForm()
return render_to_response('add_movie_form.html', {form:form,}, context_instance=RequestContext(request))
The probable problem is that the list of values provided in the text area can not be normalized into a list of Models.
See the ModelMultipleChoiceField documentation.
The field is expecting a list of valid IDs, but is probably receiving a list of text values, which django has no way of converting to the actual model instances. The to_python will be failing within the form field, not within the form itself. Therefore, the values never even reach the form.
Is there something wrong with using the built in ModelMultipleChoiceField? It will provide the easiest approach, but will require your users to scan a list of available actors (I'm using the actors field as the example here).
Before I show an example of how I'd attempt to do what you want, I must ask; how do you want to handle actors that have been entered that don't yet exist in your database? You can either create them if they exist, or you can fail. You need to make a decision on this.
# only showing the actor example, you can use something like this for other fields too
class MovieModelForm(forms.ModelForm):
actors_list = fields.CharField(required=False, widget=forms.Textarea())
class Meta:
model = MovieModel
exclude = ('actors',)
def clean_actors_list(self):
data = self.cleaned_data
actors_list = data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(','):
try:
actor = Actor.objects.get(actor=actor_name)
except Actor.DoesNotExist:
if FAIL_ON_NOT_EXIST: # decide if you want this behaviour or to create it
raise forms.ValidationError('Actor %s does not exist' % actor_name)
else: # create it if it doesnt exist
Actor(actor=actor_name).save()
return actors_list
def save(self, commit=True):
mminstance = super(MovieModelForm, self).save(commit=commit)
actors_list = self.cleaned_data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(","):
actor = Actor.objects.get(actor=actor_name)
mminstance.actors.add(actor)
mminstance.save()
return mminstance
The above is all untested code, but something approaching this should work if you really want to use a Textarea for a ModelMultipleChoiceField. If you do go down this route, and you discover errors in my code above, please either edit my answer, or provide a comment so I can. Good luck.
Edit:
The other option is to create a field that understands a comma separated list of values, but behaves in a similar way to ModelMultipleChoiceField. Looking at the source code for ModelMultipleChoiceField, it inhertis from ModelChoiceField, which DOES allow you to define which value on the model is used to normalize.
## removed code because it's no longer relevant. See Last Edit ##
Edit:
Wow, I really should have checked the django trac to see if this was already fixed. It is. See the following ticket for information. Essentially, they've done the same thing I have. They've made ModelMutipleChoiceField respect the to_field_name argument. This is only applicable for django 1.3!
The problem is, the regular ModelMultipleChoiceField will see the comma separated string, and fail because it isn't a List or Tuple. So, our job becomes a little more difficult, because we have to change the string to a list or tuple, before the regular clean method can run.
class ModelCommaSeparatedChoiceField(ModelMultipleChoiceField):
widget = Textarea
def clean(self, value):
if value is not None:
value = [item.strip() for item in value.split(",")] # remove padding
return super(ModelCommaSeparatedChoiceField, self).clean(value)
So, now your form should look like this:
class MovieModelForm(forms.ModelForm):
actors = ModelCommaSeparatedChoiceField(
required=False,
queryset=Actor.objects.filter(),
to_field_name='actor')
equipments = ModelCommaSeparatedChoiceField(
required=False,
queryset=Equipment.objects.filter(),
to_field_name='equip')
lights = ModelCommaSeparatedChoiceField(
required=False,
queryset=Light.objects.filter(),
to_field_name='light')
class Meta:
model = MovieModel
to_python AFAIK is a method for fields, not forms.
clean() occurs after individual field cleaning, so your ModelMultipleChoiceFields clean() methods are raising validation errors and thus cleaned_data does not contain anything.
You haven't provided examples for what kind of data is being input, but the answer lies in form field cleaning.
http://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-a-specific-field-attribute
You need to write validation specific to that field that either returns the correct data in the format your field is expecting, or raises a ValidationError so your view can re-render the form with error messages.
update: You're probably missing the ModelForm __init__ -- see if that fixes it.
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
# validate incoming data. Convert the raw incoming string
# to a list of ids this field is expecting.
# if invalid, raise forms.ValidationError("Error MSG")
return data.split(',') # just an example if data was '1,3,4'