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