storborg / pyramid_uniform

Form handling for Pyramid

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add support for enumerated sub-forms

storborg opened this issue · comments

This could look something like:

${renderer.text('name', obj.name)}

% for fi, item in renderer.enumerate('items', obj.items):
  ${fi.text('qty', item.qty)}
% endfor

Also, the yielded object (fi) should just be another FormRenderer instance.

More notes on behavior:

  1. When called with just a collection prefix, e.g. renderer.enumerate('items'), yield only sub-forms, over the existing data that is already present in form.data. E.g. if no params have been passed, don't yield anything.
  2. When called with one or more positional arguments, assume they are sequences, and zip them into the yielded result. E.g. renderer.enumerate('items', items_a, items_b) should yield 3-tuples, where the first element is a sub-form, and elements 2 and 3 are item_a and item_b respectively.

Here are some examples:

% for fi in renderer.enumerate('new_items'):
  ${fi.hidden('id')}
  ${fi.text('qty')}
% endfor

% for fi, item in renderer.enumerate('items', obj.items):
  ${fi.hidden('id', item.id)}
  ${fi.text('qty', item.qty)}
% endfor

% for fi, item_a, item_b in renderer.enumerate('items', items_a, items_b):
  ${fi.hidden('id', item_a.id)}
  ${fi.text('alt_qty', item_b.qty)}
% endfor

Usage of this would likely have a consistent glaring bug in the form of collections which may change server-side in between requests. For example, let's imagine a form that gets rendered using a collection of objects:

A, B, C, D

A different request then modifies the collection so that is now:

A, L, B, C, D

The user fills out the form and submits it, but there is a validation error with item C. The enumerated form helper zips together the items and parameters and presents the validation error:

A, L, [B], C, D

Because the index of item C has changed, the validation error (and, indeed, all of the submitted-but-not-persisted data) is rendered on the wrong element.

I'm not sure if it is possible to solve this without providing the form generation helper some sort of hook into the identity of the object, to check for collection order changes.

Here's an example of what this could look like, but it suffers from the problem above:

diff --git a/pyramid_uniform/__init__.py b/pyramid_uniform/__init__.py
index 0c1fd13..ccc20c2 100644
--- a/pyramid_uniform/__init__.py
+++ b/pyramid_uniform/__init__.py
@@ -1,7 +1,10 @@
 import logging

 import six
+
 from formencode import Invalid
+from formencode.variabledecode import variable_decode
+
 from webhelpers2.html import tags
 from webhelpers2.html.builder import HTML
 from pyramid.httpexceptions import HTTPBadRequest
@@ -376,6 +379,23 @@ class Renderer(object):
         else:
             return name

+    def enumerate(self, collection_name, *args):
+        # First make a list of the keys corresponding to collection name
+        decoded = variable_decode(self.data)
+        collection_data = decoded.get(collection_name, [])
+
+        for ii, els in enumerate(izip_longest(collection_data, *args)):
+            sub_name_prefix = '%s%s-%d.' % (self.name_prefix,
+                                            collection_name, ii)
+            subrenderer = Renderer(data=self.data,
+                                   errors=self.errors,
+                                   name_prefix=sub_name_prefix,
+                                   id_prefix=self.id_prefix)
+            if args:
+                yield subrenderer, els[1:]
+            else:
+                yield subrenderer
+

 class FormRenderer(Renderer):
     """

One possible solution might be to never support enumerating over a zipped combination of submitted data and an object collection: instead, only support one or the other.

To supplement this, in addition to collection iteration, offer a method to create a subform which is strictly key-referenced, and not indexed.

Some examples:

Here's a collection that would be client-originated only:

% for subrenderer in renderer.enumerate('new_items'):
  ${subrenderer.text('name')}
  ${subrenderer.text('qty')}
% endfor

Here's a collection that would be server-originated only, and keyed instead of indexed:

% for item in obj.items:
   <% subrenderer = renderer.sub('item_%d.' % item.id) %>
  ${subrenderer.hidden('id', item.id)}
  ${subrenderer.text('name', item.name)}
  ${subrenderer.text('qty', item.qty)}
% endfor

There could be a helper for the server-originated case as well, but it's less straightforward, because it requires knowing how to get the object's identity.

A helper might look like:

% for subrenderer, item in renderer.enumerate_server(obj.items, identityfunc=lambda item: item.id):
  ${subrenderer.hidden('id', item.id)}
  ${subrenderer.text('name', item.name)}
  ${subrenderer.text('qty', item.qty)}
% endfor

The identity function approach, using dict keys instead of indexes, has the issue that it's impossible to tell a FormEncode schema how to parse that: FormEncode does not allow dict-based sub-schemas to be changed dynamically.

Actually, upon more thought, the form generation doesn't have to use the post-FormEncode version of submitted data, it can use the direct params. So, as long as the form enumeration generates consistent indices between multiple requests (using the identityfunc) a list-based subschema can be used.