I’m looking forward to a stable version of newforms in the next Django release. The old forms and manipulators system, while decent, was the most painful part of Django to use—it seemed out of place amongst the rest of the framework.
Recently I ran into a situation which is not that out of the ordinary, but which the current manipulator-based admin make it very difficult for me to do.
Background: A “Book” has a main_category field and a categories field, with the former providing the category slug used in the url. The main_category field is a foreign key to the Category model, and categories has a many-to-many relationship with Category.
Objective: In order to make searching one line of code, copy the category chosen in main_category to categories upon save. That simplifies queries for Books based on category to a single books = desired_category.book_set.all() call. (Without the save I would have to OR this with Books.objects.filter(main_category=desired_category)).
So, sounds pretty straightforward to those with any experience in Django—modify the save() function in Books to add whatever is in main_category to categories as follows:
self.categories.add(self.main_category)
super(Book, self).save()
This works perfectly in ipython.
However, when I tested this through Django’s admin interface, it did not work upon save, ie main_category is not copied over.
This is because saving in admin goes through the AutomaticManipulator. Ample pdb usage found the code in question in db/models/manipulators.py. I’ll paste the relevant extracts here, comments enclosed in ### are my annotations.
def save(self, new_data):
### [ snipped ] ###
# First, save the basic object itself.
new_object = self.model(**params)
new_object.save()
### post_save hook kicks in at the end of save() above ###
### [ snipped ] ###
# Save many-to-many objects. Example: Set sites for a poll.
for f in self.opts.many_to_many:
if self.follow.get(f.name, None):
if not f.rel.edit_inline:
if f.rel.raw_id_admin:
new_vals = new_data.get(f.name, ())
else:
new_vals = new_data.getlist(f.name)
# First, clear the existing values.
rel_manager = getattr(new_object, f.name)
### Sacre bleu! what is this? ###
rel_manager.clear()
# Then, set the new values.
for n in new_vals:
rel_manager.add(f.rel.to._default_manager.get(pk=n))
Ok, here’s what happens.
- My overridden save method gets called in
new_object.save() - rel_manager deletes everything that used to be in categories with rel_manager.clear() (found in db/models/fields/related.py
The post_save hook does not help here, since it kicks in at the end of new_object.save().
Essentially, the code for saving many-to-many objects works by deleting what is already there and adding whatever was included in the form. All my overriding of the save() function in Book managed to do was provide one more category to be deleted.
Now I’m not saying this is the wrong thing to do—from the admin’s point of view, the correct thing to do is ignore whatever was in categories before, and only look at what has been selected in the form field. It is the right thing to do within the current application structure, imo.
However, when that application structure limits me from doing something that should be fairly straightforward (adding to a many2many field programmatically in the admin), then maybe there is some room for improvement.
I haven’t had the time to have a comprehensive look at newforms yet, but my brief skimming of related posts on newforms leads me to think that it will eventually be a change near the same magnitude of magic-removal. Just as I am glad I only came across Django post magic-removal, I’m sure future users of Django will be glad they didn’t have to deal with oldforms—such is the improvement newforms will provide. Those interested in getting a headstart can find an excellent intro to newforms here.

