]> git.decadent.org.uk Git - maypole.git/commitdiff
Another day, another 1500 words.
authorSimon Cozens <simon@simon-cozens.org>
Fri, 16 Apr 2004 16:45:28 +0000 (16:45 +0000)
committerSimon Cozens <simon@simon-cozens.org>
Fri, 16 Apr 2004 16:45:28 +0000 (16:45 +0000)
git-svn-id: http://svn.maypole.perl.org/Maypole/trunk@138 48953598-375a-da11-a14b-00016c27c3ee

doc/Flox.pod
doc/Request.pod

index 3c637cd6c0b52d4efa87461c382a8045c625e75c..f42bc2e3b73e9549d408bde29368be1f311b3c0a 100644 (file)
@@ -152,6 +152,17 @@ little more detail about how user handling is done.
 
 XXX
 
+We also want to be able to refer to the current user from the templates,
+so we use the overridable C<additional_data> method to give us a C<my>
+template variable:
+
+    sub additional_data { 
+        my $r = shift; $r->{template_args}{my} = $r->{user}; 
+    }
+
+I've called it C<my> rather than C<me> because we it lets us check 
+C<[% my.name %]>, and so on.
+
 =head2 Viewing a user
 
 The first page that a user will see after logging in will be their own
@@ -297,6 +308,185 @@ other people involved, and registering connections between users.
 
 =head2 Invitations
 
+We need to do two things to invitations working: first provide a way to
+issue an invitation, and then provide a way to accept it. Since what
+we're doing in issuing an invitation is essentially creating a new
+one, we'll use our usual practice of having a page to display the form
+to offer an invitation, and then use a C<do_edit> method to actually do
+the work. So our C<issue> method is just an empty action:
+
+    sub issue :Exported {}
+
+and the template proceeds as normal:
+
+    [% PROCESS header %]
+    <h2> Invite a friend </h2>
+
+    <FORM ACTION="[%base%]/invitation/do_edit/" METHOD="post">
+    <TABLE>
+
+Now we use the "Catching errors in a form" recipe from L<Request.pod> and
+write our form template:
+
+    <TR><TD>
+    First name: <INPUT TYPE="text" NAME="forename"
+    VALUE="[%request.params.forename%]">
+    </TD>
+    <TD>
+    Last name: <INPUT TYPE="text" NAME="surname"
+    VALUE="[%request.params.surname%]"> 
+    </TD></TR>
+    [% IF errors.forename OR errors.surname %]
+        <TR>
+        <TD><SPAN class="error">[% errors.forename %]</SPAN> </TD>
+        <TD><SPAN class="error">[% errors.surname %]</SPAN> </TD>
+        </TR>
+    [% END %]
+    <TR>
+    ...
+
+Now we need to work on the C<do_edit> action. This has to validate the
+form parameters, create the invited user, create the row in the C<invitation>
+table, and send an email to the new user asking them to join.
+
+We'd normally use C<create_from_cgi> to do the first two stages, but this time
+we handle the untainting manually, because there are a surprising number of
+things we need to check before we actually do the create. So here's the
+untainting of the parameters:
+
+    my ($self, $r) = @_;
+    my $h = CGI::Untaint->new(%{$r->{params}});
+    my (%errors, %ex);
+    for (qw( email forename surname )) {
+        $ex{$_} = $h->extract(
+                "-as_".($_ eq "email" ? "email" : "printable") => $_
+        ) or $errors{$_} = $h->error;
+    }
+
+Next, we do the usual dance of throwing the user back at the form in case
+of errors:
+
+    if (keys %errors) {
+        $r->{template_args}{message} = "There was something wrong with that...";
+        $r->{template_args}{errors} = \%errors;
+        $r->{template} = "issue";
+        return;
+    }
+
+We've introduced a new template variable here, C<message>, which we'll use
+to display any important messages to the user.
+
+The first check we need to do is whether or not we already have a user
+with that email address. If we have, and they're a real user, then we
+abort the invite progress and instead redirect them to viewing that user's 
+profile.
+
+    my ($user) = Flox::User->search({ email => $ex{email} });
+    if ($user) {
+        if ($user->status eq "real") {
+            $r->{template_args}{message} =
+                "That user already seems to exist on Flox. ".
+                "Is this the one you meant?";
+
+            $self->redirect_to_user($r,$user);
+        } 
+
+Where C<redirect_to_user> looks like this:
+
+    sub redirect_to_user {
+        my ($self, $r, $user) = @_;
+        $r->{objects} = [ $user ];
+        $r->{template} = "view";
+        $r->{model_class} = "Flox::User"; # Naughty.
+    }
+
+This is, as the comment quite rightly points out, naughty. We're currently
+doing a C</invitation/do_edit/> and we want to turn this into a
+C</user/view/xxx>, changing the table, template and arguments all at once.
+To do this, we have to change the Maypole request object's idea of the model
+class, since this determines where to look for the template: if we didn't,
+we'd end up with C<invitation/view> instead of C<user/view>.
+
+Ideally, we'd do this with a Apache redirect, but we want to get that
+C<message> in there as well, so this will have to do. This isn't good practice;
+we put it into a subroutine so that we can fix it up if we find a better way
+to do it.
+
+Anyway, this is what we should do if a user already exists on the system
+and has accepted an invite already. What if we're trying to invite a user but
+someone else has invited them first and they haven't replied yet?
+
+         } else {
+            # Put it back to the form
+            $r->{template_args}{message} =
+            "That user has already been invited; ".
+            "please wait for them to accept";
+            $r->{template} = "issue";
+        }
+        return;
+    }
+
+Race conditions suck.
+
+Okay. Now we know that the user doesn't exist, and so can create the new 
+one:
+
+    my $new_user = Flox::User->create({
+        email => $ex{email},
+        first_name => $ex{forename},
+        last_name  => $ex{surname},
+        status => "invitee"
+    });
+
+We want to give the invitee a URL that they can go to in order to
+accept the invite. Now we don't just want the IDs of our invites to
+be sequential, since someone could get one invite, and then guess the
+rest of the invite codes. We provide a relatively secure MD5 hash as
+the invite ID:
+
+    my $random = md5_hex(time.(0+{}).$$.rand);
+
+For additional security, we're going to have the URL in the form
+C</invitation/accept/I<id>/I<from_id>/I<to_id>>, encoding the user ids
+of the two users. Now we can send email to the invitee to ask them to
+visit that URL:
+
+    my $newid = $new_user->id;
+    my $myid  = $r->{user}->id;
+    _send_mail(to => $ex{email}, url => "$random/$myid/$newid", 
+               user => $r->{user});
+
+I'm not going to show the C<_send_mail> routine, since it's boring.
+We haven't actually created the C<Invitation> object yet, so let's
+do that now.
+
+    Flox::Invitation->create({
+        id => $random,
+        issuer => $r->{user},
+        recipient => $new_user,
+        expires => Time::Piece->new(time + LIFETIME)->datetime
+    });
+
+You can also imagine a daily cron job that cleans up the C<Invitation>
+table looking for invitations that ever got replied to within their
+lifetime:
+
+   ($_->expires > localtime && $_->delete)
+       for Flox::Invitation->retrieve_all;
+
+Notice that we don't check whether the ID is already used. We could, but,
+you know, if MD5 sums start colliding, we have much bigger problems on
+our hands.
+
+Anyway, now we've got the invitation created, we can go back to whence we
+came: viewing the original user:
+
+    $self->redirect_to_user($r, $r->{user});
+
+Now our invitee has an email, and goes B<click> on the URL. What happens?
+
+XXX
+
 =head2 Friendship Connections
 
 =head2 
index 1782cd5df5b23c5b1c5600aade6e0e0e8d8fabfb..bc2b303555fedc51257a4e446fefc499def7aba8 100644 (file)
@@ -609,6 +609,82 @@ on an ISBN:
 The request will carry on as though it were a normal C<do_edit> POST, but
 with the additional fields we have provided.
 
+=head3 Catching errors in a form
+
+A user has submitted erroneous input to an edit/create form. You want to
+send him back to the form with errors displayed against the erroneous
+fields, but have the other fields maintain the values that the user
+submitted.
+
+B<Solution>: This is basically what the default C<edit> template and
+C<do_edit> method conspire to do, but it's worth highlighting again how
+they work. 
+
+If there are any errors, these are placed in a hash, with each error
+keyed to the erroneous field. The hash is put into the template as
+C<errors>, and we process the same F<edit> template again:
+
+        $r->{template_args}{errors} = \%errors;
+        $r->{template} = "edit";
+
+This throws us back to the form, and so the form's template should take
+note of the errors, like so:
+
+     FOR col = classmetadata.columns;
+        NEXT IF col == "id";
+        "<P>";
+        "<B>"; classmetadata.colnames.$col; "</B>";
+        ": ";
+            item.to_field(col).as_HTML;
+        "</P>";
+        IF errors.$col;
+            "<FONT COLOR=\"#ff0000\">"; errors.$col; "</FONT>";
+        END;
+    END;
+
+If we're designing our own templates, instead of using generic ones, we
+can make this process a lot simpler. For instance:
+
+    <TR><TD>
+    First name: <INPUT TYPE="text" NAME="forename">
+    </TD>
+    <TD>
+    Last name: <INPUT TYPE="text" NAME="surname">
+    </TD></TR>
+
+    [% IF errors.forename OR errors.surname %]
+        <TR>
+        <TD><SPAN class="error">[% errors.forename %]</SPAN> </TD>
+        <TD><SPAN class="error">[% errors.surname %]</SPAN> </TD>
+        </TR>
+    [% END %]
+
+The next thing we want to do is to put the originally-submitted values
+back into the form. We can do this relatively easily because Maypole
+passes the Maypole request object to the form, and the POST parameters
+are going to be stored in a hash as C<request.params>. Hence:
+
+    <TR><TD>
+    First name: <INPUT TYPE="text" NAME="forename"
+    VALUE="[%request.params.forename%]">
+    </TD>
+    <TD>
+    Last name: <INPUT TYPE="text" NAME="surname"
+    VALUE="[%request.params.surname%]"> 
+    </TD></TR>
+
+Finally, we might want to only re-fill a field if it is not erroneous, so
+that we don't get the same bad input resubmitted. This is easy enough:
+
+    <TR><TD>
+    First name: <INPUT TYPE="text" NAME="forename"
+    VALUE="[%request.params.forename UNLESS errors.forename%]">
+    </TD>
+    <TD>
+    Last name: <INPUT TYPE="text" NAME="surname"
+    VALUE="[%request.params.surname UNLESS errors.surname%]"> 
+    </TD></TR>
+
 =head3 Uploading files and other data
 
 You want the user to be able to upload files to store in the database.