Archive for June 2008

Dumb redirects interact poorly with forms

2008-06-23-Mon

The current design for an improved role editor is still in prototype, but this is what it looks like:

Selecting a role to delete

The implementation of role removal is problematic, which I will be illustrating.

Upon pressing the Remove button, this confirmation form is unhidden using JavaScript:

Confirming delete role

After pressing OK, the form is submitted back onto the same page. The user is redirected to another page (admin?action=remove_role) if the role is still in use:

Reassign roles before removing

Otherwise the user gets a reloaded copy of the original page (admin?action=edit_roles):

Successfully deleted role

There are a number of problems with this implementation:

  • On the server, the pages for “action=edit_roles” and “action=remove_roles” are in separate files. Looking at remove_roles, it is not immediately obvious that it needs to process form data from edit_roles.
  • When the edit_roles page redirects to the remove_roles page, all the form data is lost; the browser issues a fresh request to remove_roles. The remove_roles page gets the role in question through the URL, e.g. action=remove_roles&role=viewer. This is not scalable to a large number of fields that must be preserved in a redirect.
  • If the role to be removed is still in use by some memberships, then after the user hits the confirmational OK button, he gets the reassignment page but the role is not yet removed. At the cost of extra complexity, this can be resolved by embedding the “which roles can be removed immediately?” information on the edit_roles page, so that JavaScript knows when the confirmation should be suppressed.
  • The DrProject web interface typically shows acknowledgement messages for administrative commands. So if I make the role removal subform submit to remove_roles, then it would redirect back to edit_roles if the role was immediately removable. But the page would also need to pass the name of the deleted role back to edit_roles so that it can display the acknowledgement message.

One way to eliminate the redirects is to have all the possibly displayed forms put into one page, having the server cue the appropriate form on demand. This idea suffers from excessive complexity.

Another approach is to use AJAX techniques so that the browser does not move between pages at all.

Mixing Web 1.0 techniques and Web 2.0 techniques can result in more problems than using either set of techniques exclusively.

Advertisements

HTML ought to have nested forms

2008-06-23-Mon

So far, the redesign of the role editor has been done like this:

Role editor subforms

The selection box on the left is a list of all the existing roles. It is an necessary piece of information that needs to be submitted for the copy, rename, and remove operations.

The four panels on the right are initially hidden (using CSS). When one of the four buttons (add, copy, rename, remove) is pressed, its corresponding panel becomes visible.

The code for the form(s) can be designed in at least three ways, each with its own serious problems.

Option 0: The left side select is common to three operations, so one form should enclose all the input elements shown in the screenshot. Note that the form would contain multiple (i.e. four) submit buttons. This is not a problem because when the user clicks an OK button, the hidden value of the button is sent, so the server can distinguish which action the user intended to take.

The problem here is when the user presses enter in one of the text input fields. This also submits the form. But because there are multiple buttons and none of them are specifically pressed, the web browser pretends that an arbitrary (e.g. the first) button is pressed. So if a user was trying to copy a role and presses enter in the copy panel’s text field, then the server thinks the user wants to add a role and fails the operation because no name was entered for the add operation.

Option 1: Put each of the four panels in its own form. Use JavaScript to copy the select’s state onto a hidden field in each form. Already, this smells like a hack. This is the approach that is currently implemented.

Problems with this idea include possible desynchronization between the select and hidden fields, and mandating the use of JavaScript.

Option 2: Instead of having four panels, just have one. Use JavaScript DOM manipulation to change the panel’s contents appropriately for each action. Use a hidden field (updated by JavaScript) inform the server which action the user intended to perform.

Again, this mandates the use of JavaScript, and DOM manipulation is more difficult than declaring the forms in HTML.

The ideal solution would be for HTML to support nested forms. (If you look at the authoritative page on HTML forms and squint at the DTD snippet hard enough, you’ll see that form elements are forbidden to nest.) In this case, everything (the select and the 4 panels) would be in a form, and each panel would be in its own subform. When a user presses enter in one of the text fields, its enclosing subform is submitted, along with all the fields in the parent form – but not the fields in sibling forms.

My misinterpretation of Python decorators

2008-06-22-Sun

DrProject is written in Python, but I had only a little experience with Python when I first started. So I had to learn many things along the way, sometimes in the hard way.

Today’s topic is the semantics of decorators in Python (PEP 318). I used to think I knew how they worked, but I was wrong. Let’s start with a simple example:

def decorate(func):
    def decorated(self):
        print "Before"
        func(self)
        print "After"
    return decorated

class Foo(object):
    @decorate
    def bar(self):
        print "Hello, world!"

This is equivalent to the following code in the old style (I won’t repeat decorate because it remains unchanged):

class Foo(object):
    def bar(self):
        print "Hello, world!"
    bar = decorate(bar)

In either case (they’re equivalent), if we create a Foo and invoke bar, we get:

Before
Hello, world!
After

So far so good? This is the kind of code I found in DrProject:

def action(name, template=None, stylesheets=None):
    def _decorate(func):
        def _execute(controller, req):
            (...)
        (...)
        return _execute
    (...)
    return _decorate

class AdminController(Controller):
    @action('list_projects',
        template='admin_list_projects.html',
        stylesheets=['admin.css'])
    def _process_list_projects(self, req, order='name', desc=''):
        (... omitted ...)

Somehow, I naïvely expected the decorator invocation to be equivalent to something like _process_list_projects = action(_process_list_projects, 'list_projects', template='admin_list_projects.html', stylesheets=['admin.css']), with the function as the zeroth argument. But that would be impossible, because action only takes 3 arguments, and has no provisions for extra arguments – so Python would err loudly if action is called with 4 arguments.

The key to understanding this situation lies in PEP 318, but the code itself is very suggestive too. Essentially, _process_list_projects is decorated by the function returned by action('list_projects', template=..., stylesheets=...). (See how action returns a nested _decorate function?) So overall, the explicit form of this decoration would be as follows:

class AdminController(Controller):
    def _process_list_projects(self, req, order='name', desc=''):
        (... omitted ...)
    _process_list_projects = action('list_projects',
        template='admin_list_projects.html',
        stylesheets=['admin.css'])(_process_list_projects)

Notice the double call, i.e. action(...)(...).

To recap, a decorator usage like

@decorate
def foo():
    (...)

expands to the form

def foo():
    (...)
foo = decorate(foo)

where decorate is an arbitrary expression that evaluates to a decorator, which can be a simple function variable or the result of a function maker (e.g. make_decorator(1,2,3)).

I have nothing against the decorator syntax; in fact, I think it makes a lot of sense now that I understand it better. I just don’t want anyone else to repeat the same naïve misinterpretation.

A simplified overview of HTML forms

2008-06-22-Sun

A form is an element that can be added to an HTML page. It consists of named fields, with each field having a value. Through forms, a user can send information back to a web server.

Suppose we have a simple HTML form on the page alpha.php:

<form action="beta.php" method="post">
    <p>
        Enter your first name:
        <input type="text" name="firstname"/>
    </p>
    <p>
        Enter your last name:
        <input type="text" name="lastname"/>
    </p>
    <p><input type="submit"/></p>
</form>

This is what it looks like:

A simple HTML form

When the user John Smith comes along and submits this form, his web browser issues an HTTP POST request to the page beta.php with the following extra information: firstname = John, lastname = Smith. It should be apparent that the field names “firstname” and “lastname” are taken from the two input elements in the form.

For the technically inclined, the POST request looks like this in the literal Hypertext Transfer Protocol (unimportant details have been abridged):

POST /beta.php HTTP/1.1
Host: www.myserver.com
User-Agent: Mozilla Firefox 3.0
Accept: text/html
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

firstname=John&lastname=Smith

As a preview for the next article, note that on most web server systems, alpha.php and beta.php would be in different files. And each page would be responsible for both handling the potential incoming POST data and generating the outgoing HTML page. But even though beta.php processes data from alpha.php, it does not refer to alpha.php – so it be hard to track down where beta.php is receiving POST data from. Also note that it is legal for a form to post back onto its own URL (in this case, change the form to action="alpha.php").

Miscellaneous humour pages

2008-06-17-Tue

Well, it seems like at least one coworker thoroughly enjoyed the link to a “Shoot Yourself in the Foot” joke page. So I’m going to post more humour pages that I can think of, to lighten the mood around here. (As if it wasn’t light enough already.)

The “shoot yourself in the foot” comes in many variants, including but not limited to:

Operating systems as airlines:

If operating systems were like cars:

Finding the height of a building using a barometer:

All odd numbers are prime:

More reading books

2008-06-16-Mon

These are the books that I’ve been reading lately:

Default behaviour of comparisons in Python

2008-06-09-Mon

Browsing DrProject’s code base a moment ago, I came across this piece of code in drproject.project.Project: (I abbreviated the long expression a bit)

    def get_members(self):
        members = [m.user for m in Membership.query.filter_by(...)]
        members.sort()
        return members

I was curious about what the sort actually did. So, members is a list of drproject.project.User objects. The class User extends Elixir’s Entity class. Neither of these classes appeared to have overridden the comparison operators. EntityMeta didn’t seem to do anything either. And the sort method was being called with no comparator function passed in. What on earth is going on?

The Python language reference page on comparisons was not very helpful. So I had to make some guesses. Since User objects are database rows, maybe they are sorted by their primary keys? But the aforementioned lack of code suggested against this possibility. Based on my experience with object-oriented languages, the only other thing I could think of was the IDs of objects — their memory addresses. It was a good candidate, since IDs are comparable for all objects.

A quick experiment in a Python session gives evidence for this behaviour:

In [1]: import random
In [2]: randcmp = lambda x, y: random.choice([-1, 0, +1])

In [3]: things = [object() for i in range(10)]
In [4]: things  # The IDs are in ascending order merely by coincidence.
Out[4]:
[<object object at 0xb7d6d598>,
 <object object at 0xb7d6d5a0>,
 <object object at 0xb7d6d5a8>,
 <object object at 0xb7d6d5b0>,
 <object object at 0xb7d6d5b8>,
 <object object at 0xb7d6d5c0>,
 <object object at 0xb7d6d5c8>,
 <object object at 0xb7d6d5d0>,
 <object object at 0xb7d6d5d8>,
 <object object at 0xb7d6d5e0>]

In [5]: things.sort(randcmp)
In [6]: things  # Now the IDs are in random order.
Out[6]:
[<object object at 0xb7d6d5c8>,
 <object object at 0xb7d6d5a8>,
 <object object at 0xb7d6d5d0>,
 <object object at 0xb7d6d598>,
 <object object at 0xb7d6d5a0>,
 <object object at 0xb7d6d5b0>,
 <object object at 0xb7d6d5d8>,
 <object object at 0xb7d6d5b8>,
 <object object at 0xb7d6d5c0>,
 <object object at 0xb7d6d5e0>]

In [7]: things.sort()
In [8]: things  # My gosh, we have ascending IDs again!
Out[8]:
[<object object at 0xb7d6d598>,
 <object object at 0xb7d6d5a0>,
 <object object at 0xb7d6d5a8>,
 <object object at 0xb7d6d5b0>,
 <object object at 0xb7d6d5b8>,
 <object object at 0xb7d6d5c0>,
 <object object at 0xb7d6d5c8>,
 <object object at 0xb7d6d5d0>,
 <object object at 0xb7d6d5d8>,
 <object object at 0xb7d6d5e0>]

Is this behaviour unintuitive, and does it let you shoot yourself in the foot easily? You be the judge.

And going back to the original DrProject code, the sort indeed sorts by IDs, which is probably not very useful.