From: Simon Cozens Date: Fri, 16 Apr 2004 16:45:28 +0000 (+0000) Subject: Another day, another 1500 words. X-Git-Tag: 2.10~224 X-Git-Url: https://git.decadent.org.uk/gitweb/?a=commitdiff_plain;h=09bbd69d71a0fca2b1c6543d1bc7212a654df44b;p=maypole.git Another day, another 1500 words. git-svn-id: http://svn.maypole.perl.org/Maypole/trunk@138 48953598-375a-da11-a14b-00016c27c3ee --- diff --git a/doc/Flox.pod b/doc/Flox.pod index 3c637cd..f42bc2e 100644 --- a/doc/Flox.pod +++ b/doc/Flox.pod @@ -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 method to give us a C +template variable: + + sub additional_data { + my $r = shift; $r->{template_args}{my} = $r->{user}; + } + +I've called it C rather than C 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 method to actually do +the work. So our C method is just an empty action: + + sub issue :Exported {} + +and the template proceeds as normal: + + [% PROCESS header %] +

Invite a friend

+ +
+ + +Now we use the "Catching errors in a form" recipe from L and +write our form template: + + + + [% IF errors.forename OR errors.surname %] + + + + + [% END %] + + ... + +Now we need to work on the C action. This has to validate the +form parameters, create the invited user, create the row in the C +table, and send an email to the new user asking them to join. + +We'd normally use C 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, 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 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 and we want to turn this into a +C, 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 instead of C. + +Ideally, we'd do this with a Apache redirect, but we want to get that +C 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/I/I>, 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 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 +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 on the URL. What happens? + +XXX + =head2 Friendship Connections =head2 diff --git a/doc/Request.pod b/doc/Request.pod index 1782cd5..bc2b303 100644 --- a/doc/Request.pod +++ b/doc/Request.pod @@ -609,6 +609,82 @@ on an ISBN: The request will carry on as though it were a normal C 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: This is basically what the default C template and +C 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, and we process the same F 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"; + "

"; + ""; classmetadata.colnames.$col; ""; + ": "; + item.to_field(col).as_HTML; + "

"; + IF errors.$col; + ""; errors.$col; ""; + END; + END; + +If we're designing our own templates, instead of using generic ones, we +can make this process a lot simpler. For instance: + +
+ + + [% IF errors.forename OR errors.surname %] + + + + + [% 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. Hence: + + + + +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: + + + + =head3 Uploading files and other data You want the user to be able to upload files to store in the database.
+ First name: + + Last name: +
[% errors.forename %] [% errors.surname %]
+ First name: + + Last name: +
[% errors.forename %] [% errors.surname %]
+ First name: + + Last name: +
+ First name: + + Last name: +