Name
Gantry::Docs::Cookbook - Gantry How Tos
Intro
This document is set up like a cookbook, but all the recipes are fully implemented in Gantry::Samples. The first recipe explains how to run the samples.
You might also be interested in Gantry::Docs::FAQ which answers a different set of questions that are not covered in Gantry::Samples. Bigtop has its own Bigtop::Docs::Cookbook and full documentation suite, see Bigtop::Docs::TOC.
The questions are:
How do I run the samples?
To run the samples, you need sqlite 3 and Gantry. Once those are in place just change to the samples directory of the Gantry distribution and type:
./app.server
The stand alone server will print a list of available URLs like this:
Available urls:
http://localhost:8080/
http://localhost:8080/ajaxrequest
http://localhost:8080/authcookie
http://localhost:8080/authcookie/sqlite
http://localhost:8080/authcookie/sqlite/closed
http://localhost:8080/fileupload
http://localhost:8080/table_perms
http://localhost:8080/table_perms_crud
http://localhost:8080/user
http://localhost:8080/user/group
These locations will be mentioned again in the appropriate sections below.
How do I upload files?
You need three things to upload a file...
A controller method expecting the file, which knows what to do with it.
A form for the user to supply the file info from their browser.
The
file_upload
method supplied by all engines.
Note that there is no special plugin to load. Gantry engines all know how to upload files and are happy to do so at any time.
Gantry::Samples::FileUpload provides an example. It has a single do_ method, do_main
, which uses fileupload.tt
supplied in the samples html/templates
sub directory.
Once the form validates, do_main
says:
my $upload = $self->file_upload( 'file' );
where file is the name of the form field containing the file name. This method returns a hash ref with thses keys:
- unique_key
-
A unique identifier for the file based on the current time and a random number.
- name
-
Base name of user's file.
- suffix => $suffix,
-
File suffix (e.g. txt).
- fullname
-
Name of user's file including suffix.
- size
-
Number of bytes in file.
- mime
-
Mime type of file.
- filehandle
-
The handle from which you read the file.
To see how to catch and store the file, see do_main in Gantry::Samples::FileUpload.
Note that Bigtop does not help with file uploads, since the actual upload is done by a single method call and the details of processing the received file vary too much for a generic scheme.
How do I authenticate users?
How do I authorize users?
How do I authorize based at the row level?
What is a three way join?
A three way join is a many-to-many relationship between two tables. For example, the relationship between authors and books. An author of one book likely has written other books. For each author, there are many books. But, in the other direction a good number of books have multiple authors. For each book there could be many authors.
Generally, you need a special extra table to hold the relationship:
+--------+ +------+
| author | | book |
+--------+ +------+
^ ^
\ /
+-------------------+
| author_book |
+-------------------+
Here the author_book table has only two (or three fields if you give it an id). Both are foriegn keys to the tables on the end of the relationship.
How do I work with a simple three way join?
You need two things for easy use of a three way structure, once your SQL is in place. First, you need your model to understand it. Second, you need an easy way to perform CRUD on the join table rows.
Making and using the model directly
Making the model understand your three way preferences is easy. Start with the two tables on the ends of the relationship as normal. Then add this to the bigtop file:
join_table author_book {
joins author => book;
}
You can do this with kickstart syntax when you make or augment the bigtop file:
bigtop -n BookStore 'author(name)<->book(title,year:int4)'
That will make the two regular tables and the joining table.
The Threeway utils module
Once you have a three way structure, you can use Gantry::Utils::Threeway to manage the tables in the joining table (the one in the middle). You can do this from the controller for the table on either end of the many-to-many relationship, or for both of them.
To show this, I'll pull code from Gantry::Samples::User.
First, use the module:
use Gantry::Utils::Threeway;
Then provide a do_ method. The one in the user example manages group membership for users. In kickstart syntax, the relationship is user<->groups. The full method is:
#-----------------------------------------------------------------
# $self->do_groups( )
#-----------------------------------------------------------------
sub do_groups {
my ( $self, $user_id ) = @_;
my $threeway = Gantry::Utils::Threeway->new( {
self => $self,
primary_id => $user_id,
primary_table => 'user',
join_table => 'user_user_group',
secondary_table => 'user_group',
legend => 'Assign Groups to User'
} );
$threeway->process();
} # END do_groups
All you have to do is construct the three way object and call process on it. This displays a form with a check box for each group. The current memberships are already checked. Clicking in the boxes and submitting the forms updates them.
The keys needed by new
are:
- self
-
The Gantry site object.
- primary_id
-
The name of the foreign key in the joining table that points to this controller.
- primary_table
-
The name of the controller's table.
- join_table
-
The name of the joining table.
- secondary_table => 'user_group',
-
The name of the table on the other end of the many-to-many.
- legend
-
HTML fieldset legend around the form where new joining table rows are created from check box values.
Using the three way manually
If you want to access rows fromthe table on the other end of the many-to-many relationship, use the many_to_many
relationship in the model:
my @groups = $user->user_groups();
That will return an array of groups to which the current user belongs. You can turn that around through a group row:
my @members = $group->users();
If you need the rows from the joining table, use the has_many
relationship from the model:
my @joining_rows = $user->user_user_groups();
Making a three way manuall
By far, the easiest way to create a three way joining structure is with a bigtop join_table
block as shown above. But you can do it yourself. In your author model, add calls like these:
__PACKAGE__->has_many(
author_books => 'Gantry::Samples::Model::author_book',
'author' # your table name
);
__PACKAGE__->many_to_many(
books => 'author_books', # value matches the has many above
'book' # the other table name
);
Then do the same in the book model. Finally, make sure you have a model for the joining rows with a normal foreign key has_many
for each of the end point tables.
How do I make my Gantry app respond to SOAP requests?
There are two types or 'styles' of SOAP requests. Gantry can help with either, but it is better at the document style, so that is what I'll discuss here.
To see a sample of this approach, run the samples app.server in one shell and samples/bin/soap_client in another. Give the client a Farenheit temperature and you should see first a SOAP request packet, which the client will then send to the server. Finally, you should see a SOAP response packet with the temperature in Celcius.
Here's what you need to do to make your own server (and I apologize in advance for the fact that Bigtop can't help much here).
Make a controller. For instance, you could add this to your bigtop file:
controller SOAP {
rel_location GantrySoapService;
skip_test 1;
method do_f2c is stub {
}
}
When you regenerate, you'll need to make some changes. First, the top of the new SOAP.pm looks like this:
package Gantry::Samples::SOAP;
use strict;
use warnings;
use base 'Gantry::Samples';
Replace that with:
package Gantry::Samples::SOAP;
use strict;
use warnings;
use Gantry::Samples qw{
-PluginNamespace=Gantry::Samples::SOAP
SOAP::Doc
};
our @ISA = ( 'Gantry::Samples' );
sub namespace {
return 'Gantry::Samples::SOAP';
}
That will load Gantry::Plugins::SOAP::Doc and apply it to this controller only (due to the namespace). Again, I admit that this is too much work and hope to automate it through bigtop as time permits.
The remainder of the process is specific to your application. Bigtop made this stub:
#-----------------------------------------------------------------
# $self->do_f2c( )
#-----------------------------------------------------------------
sub do_f2c {
my ( $self ) = @_;
}
All we need to do is fill it in.
If all the SOAP request parameters are at the same level in their packet (a fairly common case), you can take advantage of the plugin's automated conversion of the SOAP packet into form parameters. If your SOAP packet is more complex, you'll need to parse the XML with XML::LibXML, XML::Twig, etc. The sample's packets are simple.
Here is the finished routine (less comments offering advice on XML::LibXML):
1 sub do_f2c {
2 my ( $self ) = @_;
3 my $request = $self->get_post_body();
4 my $time = $self->soap_current_time();
5 my $params = $self->get_param_hash(); # easy way
6
7 my $f_temp = $params->{ farenheit };
8 my $celcius = 5.0 * ( $f_temp - 32 ) / 9.0;
9
10 my $ret_struct = [
11 {
12 GantrySoapServiceResponse => [
13 { currentUTCTime => $time },
14 { celcius => $celcius },
15 ]
16 }
17 ];
18
19 $self->soap_namespace_set(
20 'http://usegantry.org/soapservice'
21 );
22
23 return $self->soap_out( $ret_struct, 'internal', 'pretty' );
24 } # END do_f2c
First, you need to call get_post_body
(line 3) to get the full XML packet. This method is exported by each engine. It only works when a plugin has registered consume_post_body
as a pre_init
callback, luckily Gantry::Plugins::SOAP::Doc does that.
If you need to tell your client the UTC time of your response in valid SOAP time format, call soap_current_time
, as I did on line 4.
Since my server's SOAP requests are simple, I can call get_param_hash
on line 5, just as I would to handle form parameters. The input parameter is in the farenheit
key (line 7). A grade school formula does the conversion on line 8.
Lines 10-17 build the structure of the return packet. The top level tag is GantrySoapServiceResponse
. Inside it will be a list of tags (order often matters to DTDs), one for the time, the other for the converted temperature.
To control the namespace of GantrySoapServiceResponse
and its children, I called soap_namespace_set
(line 19).
Finally, line 23 uses soap_out
to send the packet back to the client. It expects:
- return structure
-
See the example above. If you need a empty tag like <empty />, use
{ empty => undef }
- namespace position
-
This must be a string chosen from 'prefix' or 'internal'. The default is prefix. This governs where the namespace is defined, and therefore has a cosmetic effect on the SOAP packet. A prefix namespace is defined in the SOAP Envelope tag where it is given the prefix tns. That prefix appears on all tags in the returned packet.
If you use internal instead, the namespace is defined in the top level tag:
<GantrySoapServiceResponse xmlns="http://usegantry.org/soapservice">
Then the elements in the body of the response have no explicit namespace prefix.
- pretty print
-
If this has a true value, the resulting XML packet will have various whitespace added to it to improve human readability. No whitespace will be added anywhere that would affect parsing the result.