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