Web programming and a different "type" problem

Dave Cole djc at object-craft.com.au
Wed Apr 30 07:48:19 EDT 2003


>>>>> "Ian" == Ian Bicking <ianb at colorstudy.com> writes:

Ian> On Tue, 2003-04-29 at 19:12, Dave Cole wrote:
>> So what Albatross does is allow (require) you to specify when a
>> field should return a list.  If you build a form which contains
>> multiple inputs with the same name ("radio" is an obvious
>> exception) then it will raise an exception if you do not use the
>> "list" attribute on all of the instances of the field in the form.

Ian> How do you specify that you expect a list?  Does it happen at
Ian> runtime, when you fetch the value from the request with an
Ian> I'm-expecting-a-list method?  Or is there some more declarative
Ian> schema involved?

You tell the template interpreter that you expect a list of values to
be returned for a particular input field.  This information is
included in the form as a hidden field.  When the browser request form
that form is submitted the values from the request are handled
according to the information from the hidden field.

If you do not specify that a field should return a list of values and
your template includes multiple fields with the same name then the
template interpreter will raise an exception.

>> In our experience you can get subtle and annoying bugs in your
>> application as a result of incorrect handling of fields which
>> sometimes return multiple values.

Ian> Indeed, but I've usually found that the bugs were from form
Ian> generation, not from form processing.

That is true, The bug is in the form generation.  Where it causes
problems though is in the processing of requests from that form.
Albatross forces you to declare (in the form) that you expect multiple
values in response to a form.

Ian> It might be better if the error is signaled earlier and in a more
Ian> pleasant way, but IMHO throwing away duplicate values when they
Ian> aren't expected just covers up bugs.

Albatross reports the problem immediately as an exception.  Since it
is could be a programming error to build forms with duplicate input
field names, the exception is the right thing to do.  The way to avoid
the exception is to tell the template interpreter (via the "list"
attribute in the <al-input> tag) that you really did want multiple
values for the field.

Ian> But I don't think that's what you're doing in this case (though
Ian> it sounds like jonpy does this, for instance)...?

I haven't looked at jonpy yet.

>> When the </al-form> tag is processed, the record of fields is
>> encoded into a hidden field called __albform__.  The field value is
>> MD5 signed by combining the encoded value with a server side secret
>> and the sign is combined with the __albform__ value.

Ian> Again out of curiosity -- what exactly goes into the __albform__
Ian> value?  I.e., what information are you saving there?

The important thing to realise here is that Albatross does not force
you to do much of anything in any single way.  If you use an execution
context with inherits from the NameRecorderMixin then you get the
following (from the next release of Albatross - FILE is a new type).

The actual source from Albatross is included with commentary.

class NameRecorderMixin:

    # These are the three different types that we expect an input
    # field to have.  For each field in a form we record the name and
    # type of that field.

    NORMAL, LIST, FILE = range(3)

    def __init__(self):

        # The NameRecorderMixin is used in execution contexts.  This
        # means that this constructor is called when the application
        # object receives a new browser request.  The execution
        # context is destroyed once request processing is complete.

        self.__elem_names = {}

    def form_open(self):

        # This is called by the template interpreter when an <al-form>
        # open tag is processed.  We just reset the field type
        # information.

        self.__elem_names = {}
        self.__need_multipart_enc = 0

    def form_close(self):

        # This is called by the template interpreter when an
        # </al-form> tag is processed, but before the </form> tag is
        # written.  We create an extra hidden field in the form which
        # contains the field type information.

        # First we pickle the field type information.

        text = cPickle.dumps(self.__elem_names, 1)

        # Then we ask the application to sign the pickle.  Currently
        # Albatross provides the PickleSignMixin which MD5 signs a
        # secret and the pickle and returns the MD5 sign prepended to
        # the pickle (without the secret of course).

        text = self.app.pickle_sign(text)

        # If zlib is available we then compress and bas64 encode the
        # signed pickle.

        if have_zlib:
            text = zlib.compress(text)
        text = base64.encodestring(text)

        # Finally the hidden field is written to the output.

        self.write_content('<input type="hidden" name="__albform__" value="')
        self.write_content(text)
        self.write_content('">\n')

        # We reset the form record and tell the template interpreter
        # whether or not any type="file" input fields were included in
        # the form.
        #
        # This is done because the template interpreter generated the
        # content of the <form> tag inside a "content trap".  The
        # execution context maintains a stack of content traps.  All
        # content written between the call to push_content_trap() and
        # pop_content_trap() is returned as a string to the caller of
        # pop_content_trap() as a string.  This allows the <al-form>
        # tag to delay the output of the <form> tag until after all of
        # the form content has been written.  At the end of the form
        # the <al-form> tag automatically includes the
        # enctype="multipart/form-data" attribute if any file input
        # fields were present.

        self.__elem_names = {}
        return self.__need_multipart_enc

    def input_add(self, itype, name, unused_value = None, return_list = 0):

        # This is called by the template interpreter for each
        # <al-input> tag encountered.  The type of each field is
        # recorded and checked for consistency.

        if itype == 'file':
            self.__need_multipart_enc = 1
            self.__elem_names[name] = self.FILE
        elif self.__elem_names.has_key(name):
            prev_multiples = self.__elem_names[name]
            implicit_multi = itype in ('radio', 'submit', 'image')
            if not return_list and not implicit_multi:
                raise ApplicationError(
                    'al-input "%s" not defined as "list"' % name)
            if return_list and implicit_multi:
                raise ApplicationError(
                    'al-input "%s" should not be defined as "list"' % name)
            if prev_multiples == 0 and return_list != 0:
                raise ApplicationError(
                    'al-input "%s" initially not defined as "list"' % name)
            if return_list == 0 and prev_multiples != 0:
                raise ApplicationError(
                    'al-input "%s" initially defined as "list"' % name)
        else:
            if return_list:
                self.__elem_names[name] = self.LIST
            else:
                self.__elem_names[name] = self.NORMAL

    def merge_request(self):

        # This is called when the application wants to merge the
        # browser request into the execution context namespace.  The
        # Request object has already been attached to the execution
        # context in the request member.
        #
        # Try to get the hidden field out of the request.  If it is
        # not present then assume the request was GET rather than
        # POST.  Fall back to default behaviour and just merge all
        # request values into namespace.

        if not self.request.has_field('__albform__'):
            for name in self.request.field_names():
                value = self.request.field_value(name)
                self.set_value(name, value)
            return

        # Found hidden field - reverse the decoding process.

        text = self.request.field_value('__albform__')
        text = base64.decodestring(text)
        if have_zlib:
            text = zlib.decompress(text)

        # Ask the application to check and remove the sign on the
        # pickle.  If the sign does not match we get back an empty
        # string.  At this point we ignore the request (refuse to
        # merge it).

        text = self.app.pickle_unsign(text)
        if not text:
            return

        # All is good - merge the raw browser request into the
        # execution context according to the type information we
        # recorded.

        elem_names = cPickle.loads(text)
        for name, mode in elem_names.items():
            if mode == self.FILE:
                value = self.request.field_file(name)
            elif self.request.has_field(name):
                value = self.request.field_value(name)
            else:
                x_name = '%s.x' % name
                y_name = '%s.y' % name
                if self.request.has_field(x_name) \
                   and self.request.has_field(y_name):
                    value = Point(int(self.request.field_value(x_name)),
                                  int(self.request.field_value(y_name)))
                else:
                    value = None
            if mode == self.LIST:
                if not value:
                    value = []
                elif type(value) is not type([]):
                    value = [value]
            self.set_value(name, value)

Having explained the above, it is not set in stone.  You can implement
your own scheme and have your execution context class inherit from
that mixin class rather than the NameRecorderMixin.

There is an alternate mixin provided called the StubRecorderMixin.  It
is shown in full below.

class StubRecorderMixin:
    def form_open(self):
        pass

    def form_close(self):
        pass

    def input_add(self, itype, name, value = None, return_list = 0):
        pass

    def merge_request(self):
        for name in self.request.field_names():
            value = self.request.field_value(name)
            self.set_value(name, value)

We do have plans for other recording schemes.

- Dave

-- 
http://www.object-craft.com.au






More information about the Python-list mailing list