Adding variable numbers of related objects in a form

77 views
Skip to first unread message

exdnc

unread,
Oct 24, 2006, 11:19:49 AM10/24/06
to Django users
Hi all,

I'm trying to build a form that lets the user add any number of steps
to an existing activity. I've got an activity model and a related
steps model, with an activity having 0 or more steps. The
functionality I want is to have an "Add more steps" button on my
add/edit form, and to have this button add more steps. Eventually I'll
learn javascript and the 'add more' button will just add more fields to
the page. It looks to me like Django can easily edit existing related
objects, and can easily create num_in_admin related objects for a new
activity. However the framework doesn't seem to be able to create more
than num_in_admin related objects without some code to add the extra
fields to the manipulator and to add those extra fields to the
FormWrappers _inline_collections.

Is there a 'proper' way to get the framework to add more than
num_in_admin related objects to both the Add/ChangeManipulator and to
the FormWrapper? Thanks!
-- Micah

Malcolm Tredinnick

unread,
Oct 24, 2006, 9:17:32 PM10/24/06
to django...@googlegroups.com

The short answer to your question is "no": the num_in_admin setting
controls the default number of form entry fields in that case.

To solve your problem in a neat way, you might want to have a look in
django/forms/__init__.py at the InlineObjectCollection class and try to
exploit that in a custom manipulator. This isn't documented at all, so
you'll have to read the source code and do a bit of experimenting, but
it should be possible to get something that works.

Regards,
Malcolm


Michael Radziej

unread,
Oct 25, 2006, 9:01:09 AM10/25/06
to django...@googlegroups.com
Malcolm Tredinnick schrieb:

OK--I wanted to wait whether Malcolms comes up with a better idea ;-)

I can provide you with a skeleton, but it's not that easy, and
I'm not such a good writer. If you try it, we could summarize the
results into a wiki page.

I have gone that path (and I really hope that the reworked forms
will make this easier. It was all trial and error.) I removed
everything specific from my working code, so there might be
trivial typing errors, but the idea worked. But you need to
follow my instructions not blindly, try to understand what I mean.

First, you need a class that represents the collection of inlined
objects, RelatedWHATEVER (replace WHATEVER with the inline model
name and ... with what fits). It's needed for all the FormWrapper
stuff to work and allows {{ XXX.1.fieldname }} in the template.
(Of course you should replace XXX with a better name.)

----------------------------

class RelatedWHATEVER(object):
class DummyField(object):
"""
Fakes a database field
"""
def __init__(self, field_names):
self.field_names = field_names

def get_manipulator_field_names(self, dummy):
return [self.field_names]

def __init__(self, related_objs):
self.related_objs = related_objs
self.related_fields = ["fieldname1", ...]
self.name = 'XXX' # qualifier in forms
self.opts = models.WHATEVER._meta

def extract_data(self, data):
"""Values for inline model data. It doesn't hurt to
return everything, though.
"""
return data

def get_list(self, original_mailbox):
"""Returns inline objects and None for empty lines to
insert"""
return self.related_objs + [None]

def editable_fields(self):
"""Needs to return kind of placebos for all database
fields."""
return [self.DummyField(name)
for name in self.related_fields]


-------------------------

Then you need custom manipulators (I hope I haven't lost you
now). I usually derive them from Manipulator and not from the
automatic manipulators, these seem to get into my way and try to
validate fields I don't use etc. Your custom manipulator needs to
do a little extra stuff.

Some imports:

from itertools import izip, chain, repeat, count
from django.utils.datastructures import DotExpandedDict

In __init__:
self.rel_WHATEVER = RelatedWHATEVER(related_objs)
# (Or [] in the AddManipulator)

for i in range(related_objs + 1):
prefix = "XXX.%d." % i
self.fields.extend([
... # add fields with names like "%s.fieldname" % prefix"
])

You need the following function for FormWrapper etc.
def get_related_objects(self):
return [self.rel_WHATEVER]

The ChangeManipulator needs to flatten the related objects too:

def flatten_data(self):
data = { ... } # data for your master object
for i, obj in enumerate(self.rel_WHATEVER.related_objs):
prefix = 'XXX.%d.' % i
... # add data['%s.fieldname' % prefix]
# for all fields of the related object obj
return data


Save also needs to save your related data. I found it's best
to move this into a separate method, so my save() looks like this:

... # save your master object, then
self.save_related(...)
...

And this is save_related(self, data, ...):

dot_data = DotExpandedDict(data)['XXX']
# go through all related rows, put data in rel_data
# and the corresponding mailrule object into rule,
filling in None when
# we run out of existing related objs.
for rel_data, obj in izip(
(dot_data[str(i)] for i in range(len(dot_data))),
chain(self.rel_WHATEVER.related_objs, repeat(None))):
# you have the data for one related obj in rel_data
... # save it

All validators for your related fields will be called the normal
way, but if you need to have special validation on the related
data, you need to tweak get_validation_errors. This is optional!

def get_validation_errors(self ,data):
errors = super(..., self).get_validation_errors(data)
dot_data = DotExpandedDict(data)['XXX']
for rel_data, obj in izip(
(dot_data[str(i)] for i in range(len(dot_data))),
chain(self.rel_WHATEVER.related_objs, repeat(None))):
... # do your validation with rel_data and obj.
...


That's it. It probably won't work immediately, just ask when you
have trouble that you can't solve on your own.

Michael


--
noris network AG - Deutschherrnstraße 15-19 - D-90429 Nürnberg -
Tel +49 911 9352-0 - Fax +49 911 9352-100

http://www.noris.de - The IT-Outsourcing Company

Reply all
Reply to author
Forward
0 new messages