How to update Django modelform without inserting? - django

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 }}.

Related

Django filter the queryset of ModelChoiceField - what did i do wrong?

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

Django modelformset creates new record instead of updating existing one

I have a System that can have one or more Models. I have modeled this relationship in the database with a manytomany field. The code below is for editing the system and its associated methods in a single form.
Adding a new method by filling out its form and pressing submit works only the first time. If I then make a small change and submit again, I get the following message (generated by the code below):
METHODFORMSET.ERRORS: [{}, {'name': [u'Method with this Name already exists.']}]
This is caused by the fact that the name field is unique, but it should have updated, not created a new record, even though I am using the POST data to generate the methodformset instance...
Note that this behaviour only applies to the last appended method instance, not to ones that were already present in the table.
Here is the relevant code, can anyone let me know what I am doing wrong?
def sysedit(request, sys_id):
system = System.objects.get(id=sys_id)
MethodFormSet = modelformset_factory(Method, form=MethodForm)
post = None
if request.POST:
post = request.POST.copy()
if 'add_method' in request.POST:
post['method-TOTAL_FORMS'] = repr(int(
post['method-TOTAL_FORMS'])+ 1)
systemform = SystemForm(data=post, instance=system)
methodformset = MethodFormSet(data=post, prefix='method',
queryset=Method.objects.filter(id__in=system.method.all()))
if methodformset.is_valid():
mfs = methodformset.save()
print 'SAVED-method', mfs
for mf in mfs:
if systemform.is_valid():
sp = systemform.save(mf)
print 'SYSTEM', sp
else:
print 'SYSFORMSET.ERRORS:', systemform.errors
else:
print 'METHODFORMSET.ERRORS:', methodformset.errors
return render_to_response('sysedit.html',
{'systemform': systemform,
'methodformset': methodformset,
'system': system},
context_instance=RequestContext(request))
class System(models.Model):
method = models.ManyToManyField(Method)
...
class Method(models.Model):
name = models.CharField(unique=True)
...
class MethodForm(ModelForm):
class Meta:
model = Method
class SystemForm(ModelForm):
def save(self, new_method=None, commit=True, *args, **kwargs):
m = super(SystemForm, self).save(commit=False, *args, **kwargs)
if new_method:
m.method.add(new_method)
if commit:
m.save()
return m
class Meta:
model = System
exclude = ('method')
[EDIT after Sergzach's answer]:
The problem is not how to deal with the Method with this name already exists error, but to prevent that from occurring in the first place. I think the actual problem may have something to do with the way modelformsets deal with new forms. Somehow it looks like it always tries to create a new instance for the last formset, regardless of whether it already exits.
So if I do not add a new formset after the last one was appended, the modelformset will try to re-create the last one (even though it was just created on the previous submit).
The initial situation is that I have 1 valid Method instance and 1 new unbound instance in the methodformset. I then fill out the form and hit save, which validates both Methods and binds the 2nd one, which is then saved to the table.
So far all is well, but if I then hit save the 2nd time the error occurs. Maybe this has to do with the fact that method-TOTAL_FORMS=2 and method-INITIAL_FORMS=1. Could it be that this causes modelformset to force a create on the 2nd Method?
Can anyone confirm/deny this?
[Edit after a weekend of not looking at the code]:
The problem is caused by the fact that I am saving the forms in the view and after saving, I am sending the original methodformset instance (from before the save) to the template. The problem can be solved by re-instantiating modelformset after the save, using the queryset and NOT the POST data.
So the general rule to prevent errors like this, is either to go to a different page after a save (avoid it altogether), or use the above solution.
Before I post this as THE solution, I need to do more testing.
You can validate each form when saving a formset. I have created a simple example (similar to your code) and it works well for me. It creates new objects if there is no object with a such name otherwise it edits an existing object.
You need a form to edit your model objects:
class EditMethodForm( forms.ModelForm ):
class Meta:
model = Method
exclude = ( 'name', )
Then instead of methodformset.is_valid() you do the next:
for methodform in methodformset:
try:
instance = Method.objects.get( name = request.POST[ 'name' ] )
except Method.DoesNotExist:
methodform.save()
else:
editmethodform = EditMethodForm( request.POST, instance = instance )
if editmethodform.is_valid():
editmethodform.save()
There are some additional features in your code. I show the working principle. Is it enough to understand the solution?
I have solved the problem by re-instantiating modelformset after the save (see edit at the bottom of the question)

ManyToManyField causing Django Model Save Problems

I want to save a Django model instance with a ManyToManyField. When I try to do so with the create() manager, it produces the following error:
Exception Value:'post' is an invalid keyword argument for this function
Here is my model:
class Amenity(models.Model):
post=models.ManyToManyField(Post,blank=True,null=True)
name=models.CharField(max_length=50, choices=AMENITIES)
def __unicode__(self):
return str(self.name)
Here is the relevant part of the view:
if request.POST.get('amenities'):
amens=request.POST['amenities'].split(',')
p=int(post.id)
for a in amens:
Amenity.objects.create(post=p,name=a)
return HttpResponse('success')
I'm trying to save multiple amenities at one time and I'd doing so outside of a modelform because I have design in mind and I didn't want to have to create a custom field in this case.
post.id is returning the correct value here, so that doesn't seem to be the issue.
Thanks!
There are two ways you can solve this:
1) By making another database hit: (which is the safest)
p = Post.objects.get(pk=post.id)
if p:
Amenity.objects.create(post=p, name=a)
else:
...
2) Passing the id to post_id
p = int(post.id)
Amenity.objects.create(post_id=p, name=a)
EDIT:
Ok, got it working on my pc. First of all as it is Many to Many, sounds better to use posts not post as a model field. Well anyway this is how you do it:
post = Post.objects.get(pk=id)
for a in amens:
a = Amenity(name=a)
a.post.add(post) #better if it said a.posts.add(post)
a.save()
You might be able to do it via Amenity.objects.create(posts=[p],name=a) since posts expects a list though I haven't tested it myself - all my ManyToMany use a through since they add additional metadata.
You shouldn't pass post.id to post field, cause post is m2m and django take care of it. Where you set post instance ?

Django Forms and Buttons

I have been working on forms only recently and I am still puzzeld by them.
What I want are standard Forms:
Next Button
Submit Data to Db
Timestamp
Clickable Images with Regions defined where when I click I get to the next page
And
I would like to combine these.
E.g. have a next button + Record the Timestamp.
or
E.g. Click into an Image + Next + Timestamp
If anybody could give me some examples for code that can achieve that or a good online resource on where to get info on that, that would be awesome.
Thanks for the time!!
I'm a little unclear about what you're trying to accomplish, but if you're trying to move data from an HTML form to the database, I'd suggest looking at how to use ModelForms. In a nutshell, you create a model class, like this:
class MyModel(models.Model):
field1 = models.CharField(max_length=50)
Then you create a ModelForm class that references that model:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
You can render an instance of MyModelForm in a view function. Inside of a POST request in that view, you bind the POST data to the form, validate it, and call save() on it to commit it to the database:
if request.method == 'POST':
form = MyModelForm(request.POST)
if form.is_valid():
model_instance = form.save()
This really isn't a question, I'm not exactly sure what you're trying to accomplish.
If you want to use Django forms, start here, or here.
I assume the stuff you mention about a timestamp should probably be an auto_now field in a model. Take a look at this.
The stuff you mention about buttons and click-able images is really just HTML and has nothing to do with Django. I would try Google for that.

Building a formset dynamically

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?