]> git.decadent.org.uk Git - maypole.git/blob - lib/Maypole/Manual/BuySpy.pod
fix manual so search.cpan.org indexes it properly (it uses the NAME section for cross...
[maypole.git] / lib / Maypole / Manual / BuySpy.pod
1 =head1 NAME
2
3 Maypole::Manual::BugSpy - The Maypole iBuySpy Portal
4
5 =head1 DESCRIPTION
6
7 I think it's good fun to compare Maypole against other frameworks,
8 so here's how to build the ASP.NET tutorial site in Maypole.
9
10 We begin with a lengthy process of planning and investigating the
11 sources. Of prime interest is the database schema and the initial data,
12 which we convert to a MySQL database. Converting MS SQL to MySQL is not fun.
13 I shall spare you the gore. Especially the bit where the default insert IDs
14 didn't match up between the tables.
15
16 The C<ibsportal> database has a number of tables which describe how the
17 portal should look, and some tables which describe the data that should
18 appear on it. The portal is defined in terms of a set of modules; each
19 module takes some data from somewhere, and specifies a template to be
20 used to format the data. This is quite different from how Maypole
21 normally operates, so we have a choice as to whether we're going to
22 completely copy this design, or use a more "natural" implementation in 
23 terms of having the portal display defined as a template itself, with
24 all the modules specified right there in Template Toolkit code rather
25 than picked up from the database. This would be much faster, since you
26 get one shot of rendering instead of having to process each module's
27 template independently. The thing is, I feel like showing off
28 precisely how flexible Maypole is, so we'll do it the hard way.
29
30 The first thing we need to do is get the database into some sort of
31 useful shape, and work out the relationships between the tables. This of
32 course requires half a day of playing with GraphViz, Omnigraffle and
33 mysql, but ended up with something like this:
34
35 =for html
36 <img src="ibs-schema.png">
37
38 This leads naturally to the following driver code:
39
40     package Portal;
41     use Maypole::Application;
42     Portal->setup("dbi:mysql:ibsportal");
43     use Class::DBI::Loader::Relationship;
44     Portal->config->loader->relationship($_) for (
45         "A module has a definition",  "A module has settings",
46         "A tab has modules",          "A portal has tabs",
47         "A role has a portal",        "A definition has a portal",
48         "A module has announcements", "A module has contacts",
49         "A module has discussions",   "A module has events",
50         "A module has htmltexts",     "A module has links",
51         "A module has documents",
52         "A user has roles via userrole"
53     );
54     1;
55
56 As you can see, a portal is made up of a number of different tabs;
57 the tabs contain modules, but they're separated into different panes,
58 so a module knows whether it belongs on the left pane, the right pane
59 or the center. A module also knows where it appears in the pane.
60
61 We'll begin by mocking up the portal view in plain text, like so:
62
63     use Portal;
64     my $portal = Portal::Portal->retrieve(2);
65     for my $tab ($portal->tabs) {
66         print $tab,"\n";
67         for my $pane (qw(LeftPane ContentPane RightPane)) {
68             print "\t$pane:\n";
69             for (sort { $a->module_order <=> $b->module_order }
70                 $tab->modules(pane => $pane)) {
71                 print "\t\t$_:\t", $_->definition,"\n";
72             }
73         }
74         print "\n";
75     }
76
77 This dumps out the tabs of our portal, along with the modules in each
78 tab and their types; this lets us check that we've got the database
79 set up properly. If we have, it should produce something like this:
80
81     Home
82             LeftPane:
83                     Quick link:     Quicklink
84             ContentPane:
85                     Welcome to the IBuySpy Portal:  Html Document
86                     News and Features:      announcement
87                     Upcoming event: event
88             RightPane:
89                     This Week's Special:    Html Document
90                     Top Movers:     XML/XSL
91
92     ...
93
94 Now we want to get the front page up; for the moment, we'll just have it
95 display the module names and their definitions like our text mock-up,
96 and we'll flesh out the actual modules later.
97
98 But before we do that, we'll write a front-end URL handler method, to
99 allow us to ape the ASP file names. Why do we want to make a Maypole
100 site look like it's running C<.aspx> files? Because we can! - and
101 because I want to show we don't necessarily B<have> to follow the
102 Maypole tradition of having our URLs look like
103 C</I<table>/I<action>/I<id>/I<arguments>>. 
104
105     our %pages = (
106         "DesktopDefault.aspx" => { action => "view", table => "tab" },
107         "MobileDefault.aspx"  => { action => "view_mobile", table => "tab" },
108     );
109
110     sub parse_path {
111         my $self = shift;
112         $self->path("DesktopDefault.aspx") unless $self->path;
113         return $self->SUPER::parse_path if not exists $pages{$self->path};
114         my $page = $pages{$self->path} ;
115         $self->action($page->{action});
116         $self->table($page->{table});
117         my %query = $self->ar->args;
118         $self->args( [ $query{tabid} || $query{ItemID} || 1] );
119     }
120
121     1;
122
123 Here we're overriding the C<parse_path> method which takes the C<path>
124 slot from the request and populates the C<table>, C<action> and
125 C<args> slots. If the user has asked for a page we don't know
126 about, we ask the usual Maypole path handling method to give it a try;
127 this will become important later on. We turn the default page,
128 C<DesktopDefault.aspx>, into the equivalent of C</tab/view/1> unless
129 another C<tabid> or C<ItemID> is given in the query parameters; this allows us
130 to use the ASP.NET-style C<DesktopDefault.aspx?tabid=3> to select a tab.
131
132 Now we have to create our C<tab/view> template; the majority of
133 this is copied from the F<DesktopDefault.aspx> source, but our panes
134 look like this:
135
136     <td id="LeftPane" Width="170">
137         [% pane("LeftPane") %]
138     </td>
139     <td width="1">
140     </td>
141     <td id="ContentPane" Width="*">
142         [% pane("ContentPane") %]
143     </td>
144     <td id="RightPane" Width="230">
145         [% pane("RightPane") %]
146     </td>
147     <td width="10">
148         &nbsp;
149    </td>
150
151 The C<pane> macro has to be the Template Toolkit analogue of the Perl code
152 we used for our mock-up:
153
154     [% MACRO pane(panename) BLOCK;
155         FOR module = tab.modules("pane", panename);
156             "<P>"; module; " - "; module.definition; "</P>";
157         END;
158     END;
159
160 Now, the way that the iBuySpy portal works is that each module has a
161 definition, and each definition contains a path to a template:
162 C<$module-E<gt>definition-E<gt>DesktopSrc> returns a path name for an C<ascx>
163 component file. All we need to do is convert those files from ASP to the
164 Template Toolkit, and have Maypole process each component for each module,
165 right?
166
167 =head2 Components and templates
168
169 Dead right, but it was here that I got too clever. I guess it was the word
170 "component" that set me off. I thought that since the page was made up of a
171 large number of different modules, all requiring their own set of objects, I
172 should use a separate Maypole sub-request for each one, as shown in the
173 "Component-based pages" recipe in the
174 L<Request Cookbook|Maypole::Manual::Request>.
175
176 So this is what I did. I created a method in C<Portal::Module> that would
177 set the template to the appropriate C<ascx> file:
178
179     sub view_desktop :Exported {
180         my ($self, $r) = @_;
181         $r->template($r->objects->[0]->definition->DesktopSrc);
182     }
183
184 and changed the C<pane> macro to fire off a sub-request for each module:
185
186     [% MACRO pane(panename) BLOCK;
187         FOR module = tab.modules("pane", panename);
188             SET path = "/module/view_desktop/" _ module.id;
189             request.component(path);
190         END;
191     END; %]
192
193 This did the right thing, and a call to C</module/view_desktop/12> would
194 look up the C<Html Document> module definition, find the C<DesktopSrc> to
195 be F<DesktopModules/HtmlModule.ascx>, and process module 12 with that
196 template. Once I had converted F<HtmlModule.ascx> to be a Template Toolkit
197 file (and we'll look at the conversion of the templates in a second) it
198 would display nicely on my portal.
199
200 Except it was all very slow; we were firing off a large number of Maypole
201 requests in series, so that each template could get at the objects it
202 needed. Requests were taking 5 seconds.
203
204 That's when it dawned on me that these templates don't actually need different
205 objects at all. The only object of interest that C</module/view_desktop> is
206 passing in is a C<module> object, and each template figures everything out by
207 accessor calls on that. But we already have a C<module> object, in our C<FOR>
208 loop - we're using it to make the component call, after all! Why not just
209 C<PROCESS> each template inside the loop directly?
210
211     [% MACRO pane(panename) BLOCK;
212         FOR module = tab.modules("pane", panename);
213             SET src = module.definition.DesktopSrc;
214             TRY;
215                 PROCESS $src;
216             CATCH DEFAULT;
217                 "Bah, template $src broke on me!";
218             END;
219         END;
220     END; %]
221
222 This worked somewhat better, and took request times from 5 seconds down
223 to acceptable sub-second levels again. I could take the C<view_desktop>
224 method out again; fewer lines of code to maintain is always good. Now
225 all that remained to do for the view side of the portal was to convert
226 our ASP templates over to something sensible.
227
228 =head2 ASP to Template Toolkit
229
230 They're all much of a muchness, these templating languages. Some of
231 them, though, are just a wee bit more verbose than others. For instance,
232 the banner template which appears in the header consists of 104 lines
233 of ASP code; most of those are to create the navigation bar of tabs
234 that we can view. Now I admit that we're slightly cheating at the moment
235 since we don't have the concept of a logged-in user and so we don't
236 distinguish between the tabs that anyone can see and those than only an
237 admin can see, but we'll come back to it later. Still, 104 lines, eh?
238
239 The actual tab list is presented here: (reformated slightly for sanity)
240
241     <tr>
242         <td>
243             <asp:datalist id="tabs" cssclass="OtherTabsBg" 
244  repeatdirection="horizontal" ItemStyle-Height="25" 
245  SelectedItemStyle-CssClass="TabBg" ItemStyle-BorderWidth="1" 
246  EnableViewState="false" runat="server">
247                 <ItemTemplate>
248                     &nbsp;<a href='<%= Request.ApplicationPath %>/
249  DesktopDefault.aspx?tabindex=<%# Container.ItemIndex %>&tabid=
250  <%# ((TabStripDetails) Container.DataItem).TabId %>' class="OtherTabs">
251  <%# ((TabStripDetails) Container.DataItem).TabName %></a>&nbsp;
252                 </ItemTemplate>
253                 <SelectedItemTemplate>
254                     &nbsp;<span class="SelectedTab">
255  <%# ((TabStripDetails) Container.DataItem).TabName %></span>&nbsp;
256                 </SelectedItemTemplate>
257             </asp:datalist>
258         </td>
259     </tr>
260
261 But it has to be built up in some 22 lines of C# code which creates and
262 populates an array and then binds it to a template parameter. See those
263 C<E<lt>%#> and C<E<lt>%=> tags? They're the equivalent of our Template
264 Toolkit C<[% %]> tags. C<Request.ApplicationPath>? That's our C<base>
265 template argument. 
266
267 In our version we ask the portal what tabs it has, and display the
268 list directly, displaying the currently selected tab differently:
269
270     <table id="Banner_tabs" class="OtherTabsBg" cellspacing="0" border="0">
271         <tr>
272     [% FOR a_tab = portal.tabs %]
273         [% IF a_tab.id == tab.id %]
274             <td class="TabBg" height="25">
275                 &nbsp;<span class="SelectedTab">[%tab.name%]</span>&nbsp;
276         [% ELSE %]
277             <td height="25">
278                 &nbsp;<a href='[%base%]DesktopDefault.aspx?tabid=[%a_tab.id%]' 
279                 class="OtherTabs">[%a_tab.name%]</a>&nbsp;
280         [% END %]
281             </td>
282     [% END %]
283         </tr>
284     </table>
285
286 This is the way the world should be. But wait, where have we pulled this
287 C<portal> variable from? We need to tell the C<Portal> class to put the
288 default portal into the template arguments:
289
290     sub additional_data {
291         shift->{template_args}{portal} = Portal::Portal->retrieve(2);
292     }
293
294 Translating all the other ASP.NET components is a similar exercise in drudgery;
295 on the whole, there was precisely nothing interesting about them at all - we
296 merely converted a particularly verbose templating language (and if I never see
297 C<asp:BoundColumn> again, it'll be no loss) into a rather more sophisticated
298 one.
299
300 The simplest component, F<HtmlModule.ascx>, asks a module for its associated
301 C<htmltexts>, and then displays the C<DesktopHtml> for each of them in a table.
302 This was 40 lines of ASP.NET, including more odious C# to make the SQL calls
303 and retrieve the C<htmltexts>. But we can do all that retrieval by magic, so
304 our F<HtmlModule.ascx> looks like this:
305
306     [% PROCESS module_title %]
307     <portal:title EditText="Edit" EditUrl="~/DesktopModules/EditHtml.aspx" />
308     <table id="t1" cellspacing="0" cellpadding="0">
309         <tr valign="top">
310             <td id="HtmlHolder">
311             [% FOR html = module.htmltexts; html.DesktopHtml; END %]
312             </td>
313         </tr>
314     </table>
315
316 Now I admit that we've cheated here and kept that C<portal:title> tag
317 until we know what to do with it - it's obvious that we should turn
318 it into a link to edit the HTML of this module if we're allowed to.
319
320 The next simplest one actually did provide a slight challenge;
321 F<ImageModule.ascx> took the height, width and image source properties
322 of an image from the module's C<settings> table, and displayed an C<IMG>
323 tag with the appropriate values. This is only slightly difficult because
324 we have to arrange the array of C<module.settings> into a hash of
325 C<key_name> => C<setting> pairs. Frankly, I can't be bothered to do this
326 in the template, so we'll add it into the C<template_args> again. This
327 time C<additional_data> looks like:
328
329     sub additional_data {
330         my $r = shift;
331         shift->template_args->{portal} = Portal::Portal->retrieve(2);
332         if ($r->objects->[0]->isa("Portal::Module")) {
333             $r->template_args->{module_settings} =
334                 { map { $_->key_name, $_->setting } 
335                   $r->objects->[0]->settings };
336         }
337     }
338
339 And the F<ImageModule.ascx> drops from the 30-odd lines of ASP into:
340
341     [% PROCESS module_title; %]
342     <img id="Image1" border="0" src="[% module_settings.src %]" 
343       width="[% module_settings.width %]" 
344       height="[% module_settings.height %]" />
345     <br>
346
347 Our portal is taking shape; after a few more templates have been translated,
348 we now have a complete replica of the front page of the portal and all its
349 tabs. It's fast, it's been developed rapidly, and it's less than 50 lines
350 of Perl code so far. But it's not finished yet.
351
352 =head2 Adding users
353
354 ...
355
356 =head2 Links
357
358 L<Contents|Maypole::Manual>,
359 Next B<That's all folks! Time to start coding ...>,
360 Previous L<Flox|Maypole::Manual::Flox>
361