I initially wrote code to build a form dynamically, based on data from the DB, similar to what I described in my previous SO post.
As SO user Daniel Roseman points out, he would use a formset for this, and now I've come to the realization that he must be completely right. :)
My approach works, basically, but I can't seem to get validation across the entire form to be working properly (I believe it's possible, but it's getting quite complex, and there has to be a smarter way of doing it => Formsets!).
So now my question is: How can I build a formset dynamically? Not in an AJAX way, I want each form's label to be populated with an FK value (team) from the DB.
As I have a need for passing parameters to the form, I've used this technique from a previous SO post.
With the former approach, my view code is (form code in previous link):
def render_form(request):
teams = Team.objects.filter(game=game)
form_collection = []
for team in teams:
f = SuggestionForm(request.POST or None, team=team, user=request.user)
form_collection.append(f)
Now I want to do something like:
def render_form(request):
teams = Team.objects.filter(game=game)
from django.utils.functional import curry
from django.forms.formsets import formset_factory
formset = formset_factory(SuggestionForm)
for team in teams:
formset.form.append(staticmethod(curry(SuggestionForm, request.POST or None, team=team, user=request.user)))
But the append bit doesn't work. What's the proper way of doing this?
Thanks!
Thanks for the recognition of my unvarying rightness...
Probably what you need here is a model formset, which will automatically build itself from a queryset you pass in:
from django.forms.models import modelformset_factory
def render_form(request):
teams = Team.objects.filter(game=game)
formset = modelformset_factory(form=SuggestionForm, queryset=teams)
As for the dynamic parameter, which I'm guessing is user, I've previously used the closure solution, but the curry method should still work:
formset.form = staticmethod(curry(SuggestionForm, user=request.user))
Edit after comment Thanks for the clarification. I think I understand what you're trying to do. I wonder if an inline formset might work better? If you started off with a Game object pre-populated with eight related Team objects, an inline formset would give you eight pre-existing forms.
my_game = Game.objects.create(params=whatever)
for i in range(1, 9):
team = Team.objects.create(game=my_game, name="team_%s" % i
formset = inlinemodelformset_factory(Game, Team, form=SuggestionForm)
fs = formset(instance=my_game)
Does that work?
Related
Thanks in advance for reading this. I can't wrap my head around it and it's getting quite frustrating by now.
We have the following registration form:
class RegistrationForm(forms.ModelForm):
class Meta:
model = Register
fields = ('name', 'company_name')
def clean(self):
if is not self.cleaned_data.get('card').is_available():
raise forms.ValidationError(_('Error'))
The Register model includes a card linked to a Card model. This includes is_available() which functionally works.
Our flow is:
The end user selects the card which lists all registrations for it.
They click the 'Add registration'-button which brings them to cards/{PK}/add.
The Add registration-button is a generic.View. In post(self, request, pk) I have the following code:
form = RegistrationForm(request.POST)
But how do I pass it the contents of Card.objects.get(pk=pk) to it?
I tried:
data = request.POST.copy()
data['card'] = pk
form = RegistrationForm(data)
But I think because card is not included in fields it gets lost somewhere, which makes sense from a sanitize-all-input-point of view, but I would very much like to add the card dynamically, in this case.
Any ideas?
So, just use CreateView and study how it does things using the linked site.
There is no need to use generic.View as it's the basic of basics. You only want to implement all this logic using generic.View to get more familiar with the way things work or if you need some very special form handling.
The short version would be:
from django.views import generic
from myapp.forms import RegistrationForm
class CardCreateView(generic.CreateView):
form_class = RegistrationForm
ModelForm has a save method. The correct way to solve this is to use it with commit=False, that will return an object that hasn’t yet been saved to the database. Then you can alter that object before finally saving it.
This is explained here in the docs
So this is what your code should look like:
form = RegistrationForm(request.POST)
form.save(commit=False)
form.card = Card.objects.get(pk=pk)
form.save_m2m()
save_m2m should be used if your model has many-to-many relationships with other models. In my case, it was a OneToOne, so I used save() instead.
If you use a CreateView instead of the generic View, the snippet above should go into your overridden form_valid method
I know that many questions exist about this same topic, but i am confused on one point.
My intent is to show two ModelChoiceFields on the form, but not directly tie them to the Game model.
I have the following:
forms.py
class AddGame(forms.ModelForm):
won_lag = forms.ChoiceField(choices=[('1','Home') , ('2', 'Away') ])
home_team = forms.ModelChoiceField(queryset=Player.objects.all())
away_team = forms.ModelChoiceField(queryset=Player.objects.all())
class Meta:
model = Game
fields = ('match', 'match_sequence')
Views.py
def game_add(request, match_id):
game = Game()
try:
match = Match.objects.get(id=match_id)
except Match.DoesNotExist:
# we have no object! do something
pass
game.match = match
# get form
form = AddGame(request.POST or None, instance=game)
form.fields['home_team'].queryset = Player.objects.filter(team=match.home_team )
# handle post-back (new or existing; on success nav to game list)
if request.method == 'POST':
if form.is_valid():
form.save()
# redirect to list of games for the specified match
return HttpResponseRedirect(reverse('nine.views.list_games'))
...
Where i am confused is when setting the queryset filter.
First i tried:
form.home_team.queryset = Player.objects.filter(team=match.home_team )
but i got this error
AttributeError at /nine/games/new/1
'AddGame' object has no attribute 'home_team'
...
so i changed it to the following: (after reading other posts)
form.fields['home_team'].queryset = Player.objects.filter(team=match.home_team )
and now it works fine.
So my question is, what is the difference between the two lines? Why did the second one work and not the first? I am sure it is a newbie (i am one) question, but i am baffled.
Any help would be appreciated.
Django Forms are metaclasses:
>>> type(AddGame)
<class 'django.forms.forms.DeclarativeFieldsMetaclass'>
They basically create a form instance according to the information given in its definition. This means, you won't get exactly what you see when you define the AddGame form. When you instantiate it, the metaclass will return the proper instance with the fields provided:
>>> type(AddGame())
<class 'your_app.forms.AddGame'>
So, with the instance, you can access the fields by simply doing form.field. In fact, it is a bit more complicated than that. There are two types of fields you can access. With form['field'] you'll be accessing a BoundField. Which is used for output and raw_input.
By doing form.fields['fields'] you'll be then accessing to a field that python can understand. This is because if the from already got any input, there's where validation and data conversion take places (in fact, those are the fields used for this, the general process of validation is a bit more complicated).
I hope this might clear a little the issue for you but as you may see, the whole form's API is really big and complicated. Is very simple for end-users but it has a lot of programming behind the curtains :)
Reading the links provides will help clear your doubts and will improve your knowledge about this very useful topic and Django in general.
Good luck!
UPDATE: By the way, if you want to learn more about Python's Metaclasses, this is a hell of an answer about the topic.
In you views.py, you have this line:
form = AddGame(request.POST or None, instance=game)
So form is a Form object of class AddGame (Side note: you should change the name to AddGameForm to avoid confusion).
Since home_team is a field in AddGame class, it's not an attribute in form object. That's why you can't access it via form.home_team.
However, Django Form API provides fields attribute to any form object, which is a dict contains all form fields. That's why you can access form.fields['home_team'].
And finally since home_team is a ModelChoiceField, it can contain a queryset attribute, that's why you can access form.fields['home_team'].queryset
I am having a problem with my Django code inserting new rows instead of updating existing ones. Many people on here have asked for help with a similar problem, but generally they forget about setting instance as in form = SomeModelForm(request.POST or None, instance=some_model_instance). I seem to be doing that right, but it still doesn't save over existing data.
One other poster had the exact same issue (see Django form INSERTs when I want it to UPDATE). It looks like the code given to him to fix the problem resolved the issue, but no one explained why it worked when he asked!
VIEW
if form_type == "edit_event":
# we need to load all the data based on the event_id
event_id = request.POST['event_id'] or None
event_to_edit = mapDataManager.getEvent(event_id)
if event_to_edit and mapDataManager.userCreatedEvent(request.user, event_to_edit):
# this user actually created this event
# let them edit it
event_form = EventModelForm(request.POST or None, instance=event_to_edit)
location_form = LocationModelForm(request.POST or None, instance=event_to_edit.location)
list_of_event_scheds = EventSchedule.objects.filter(pk=event_id).values()
event_sched_formset = EventScheduleModelFormSet(request.POST or None, initial=list_of_event_scheds)
if event_form and location_form and event_sched_formset:
if event_form.is_valid() and location_form.is_valid() and event_sched_formset.is_valid():
# update event
mapDataManager.updateEvent(event_form, location_form, event_sched_formset)
# redirect
return HttpResponseRedirect(reverse('map'))
else:
# set feedback to show edit event form
feedback = "edit_event"
updateEvent()
def updateEvent(self, event_form, location_form, event_sched_forms):
event = event_form.save(commit=False)
location = location_form.save()
event.location = location
event.save()
for event_sched_form in event_sched_forms: # loop through formset
event_sched = event_sched_form.save(commit=False)
event_sched.event = event
event_sched.save()
Can anyone help me out? Am I missing something really basic?
EDIT: Just to clarify, NONE of my forms were updating, all were inserting. Based on Daniel's suggestion I think I need to use formsets to link all three modelforms that are being edited.
For some strange reason, you're correctly passing instance to the main forms, but initial to EventScheduleModelFormSet, which is the immediate cause of the problem.
However, since these models obviously have a foreign key relationship, it sounds like you should be using a inline model formset:
from django.forms.models import inlineformset_factory
eventschedule_formset_class = inlineformset_factory(Event, EventSchedule)
event_sched_formset = eventschedule_formset_class(request.POST or None,
instance=event_to_edit)
Now there is no need for that updateEvent logic - you can just save event_sched_formset and it will correctly reference the event.
you are trying to save the EventScheduleModelFormSet form? there is no instance set only initial data so you are creating a new object every time. or am I missing something?
I was able to get the inline model formset working as per Daniel's suggestion with help from this blog post: http://charlesleifer.com/blog/djangos-inlineformsetfactory-and-you/
Just as an FYI, one crucial point I was missing is that the formset's instance refers to the parent model. Also, if you're displaying the formset manually in your template, don't forget to include {{ formset.id }}.
I have the following model and it's form:
class Project(models.Model)
class ProjectForm(forms.ModelForm)
class Meta:
Model = Project
So it's easy to create a form by instantiating:
form = ProjectForm()
But in my case, I have several models aside from "Projects", and I don't know in advance for which of these models I will need to create the form.
So I would like to create the form from the ContentType instance of the Project model.
In other words, I'm looking for something that looks like:
myproject = Project()
form = createform(myproject.ContentType)
Presumably you have a certain limited selection of models that might be used. The simplest way is just to create form classes for each of them, then choose the one you need from a dictionary:
MODEL_FORMS = {
MyModel: MyModelForm,
MyOtherModel: MyOtherModelForm
}
my_form_class = MODEL_FORMS[my_project.content_type]
my_form = my_form_class()
Unfortunately, this was the best I could find - but a combination of get_model and form_for_model should do the trick. You'll need to use get_model to load up the model type you want to work on, and then form_for_model to get a form for that model.
Edit: Daniel's solution is a much better one if you know what models you're dealing with.
Thank you to both of you, this helps a lot !
I will go with Daniel's solution as I have a limited number of models.
I think maybe I will need to add model_class() to "my_project.content_type.model_class()" in order to get the model class (to be checked) ?
Just for the record, I had managed to make something work with model formset factories :
from django.forms.models import modelformset_factory
ProjectFormSet = modelformset_factory(my_project.content_type.model_class())
my_form = ProjectFormSet()
but this form would of course not get all the customisations made in my model forms... so that was not a good solution.
For my project I need many "workflow" forms. I explain myself:
The user selects a value in the first field, validates the form and new fields appear depending on the first field value. Then, depending on the others fields, new fields can appear...
How can I implement that in a generic way ?
I think the solution you are looking for is django form wizard
Basically you define separate forms for different pages and customize the next ones based on input in previous screens, at the end, you get all form's data together.
Specifically look at the process step advanced option on the form wizard.
FormWizard.process_step()
"""
Hook for modifying the wizard's internal state, given a fully validated Form object. The Form is guaranteed to have clean, valid data.
This method should not modify any of that data. Rather, it might want to set self.extra_context or dynamically alter self.form_list, based on previously submitted forms.
Note that this method is called every time a page is rendered for all submitted steps.
The function signature:
"""
def process_step(self, request, form, step):
# ...
If you need to only modify the dropdown values based on other dropdowns within the same form, you should have a look at the implemented dajaxproject
I think it depends on the scale of the problem.
You could write some generic JavaScript that shows and hides the form fields (then in the form itself you apply these css classes). This would work well for a relatively small number showing and hiding fields.
If you want to go further than that you will need to think about developing dynamic forms in Django. I would suggest you don't modify the ['field'] in the class like Ghislain suggested. There is a good post here about dynamic forms and it shows you a few approaches.
I would imagine that a good solution might be combining the dynamic forms in the post above with the django FormWizard. The FormWizard will take you through various different Forms and then allow you to save the overall data at the end.
It had a few gotchas though as you can't easily go back a step without loosing the data of the step your on. Also displaying all the forms will require a bit of a customization of the FormWizard. Some of the API isn't documented or considered public (so be wary of it changing in future versions of Django) but if you look at the source you can extend and override parts of the form wizard fairly easily to do what you need.
Finally a simpler FormWizard approach would be to have say 5 static forms and then customize the form selection in the wizard and change what forms are next and only show the relevant forms. This again would work well but it depends how much the forms change on previous choices.
Hope that helps, ask any questions if have any!
It sounds like you want an AJAXy type solution. Checkout the Taconite plugin for jQuery. I use this for populating pulldowns, etc. on forms. Works very nicely.
As for being "generic" ... you might have standard methods on your container classes that return lists of children and then have a template fragmen t that knows how to format that in some 'standard' way.
Ok, I've found a solution that does not use ajax at all and seems nice enough to me :
Create as many forms as needed and make them subclass each other. Put an Integer Hidden Field into the first one :
class Form1(forms.Form):
_nextstep = forms.IntegerField(initial = 0, widget = forms.HiddenInput())
foo11 = forms.IntegerField(label = u'First field of the first form')
foo12 = forms.IntegerField(label = u'Second field of the first form')
class Form2(Form1):
foo21 = forms.CharField(label = u'First field of the second form')
class Form3(Form2):
foo31 = forms.ChoiceField([],
label=u'A choice field which choices will be completed\
depending on the previous forms')
foo32 = forms.IntegerField(label = u'A last one')
# You can alter your fields depending on the data.
# Example follows for the foo31 choice field
def __init__(self, *args, **kwargs):
if self.data and self.data.has_key('foo12'):
self.fields['foo31'].choices = ['make','a','nice','list',
'and you can','use your models']
Ok, that was for the forms now here is the view :
def myview(request):
errors = []
# define the forms used :
steps = [Form1,Form2,Form3]
if request.method != 'POST':
# The first call will use the first form :
form = steps[0]()
else:
step = 0
if request.POST.has_key('_nextstep'):
step = int(request.POST['_nextstep'])
# Fetch the form class corresponding to this step
# and instantiate the form
klass = steps[step]
form = klass(request.POST)
if form.is_valid():
# If the form is valid, increment the step
# and use the new class to create the form
# that will be displayed
data = form.cleaned_data
data['_nextstep'] = min(step + 1, len(steps) - 1)
klass = steps[data['_nextstep']]
form = klass(data)
else:
errors.append(form.errors)
return render_to_response(
'template.html',
{'form':form,'errors':errors},
context_instance = RequestContext(request))
The only problem I saw is that if you use {{form}} in your template, it calls form.errors and so automagically validates the new form (Form2 for example) with the data of the previous one (Form1). So what I do is iterate over the items in the form and only use {{item.id}}, {{item.label}} and {{item}}. As I've already fetched the errors of the previous form in the view and passed this to the template, I add a div to display them on top of the page.