1Maypole::Manual::BuySpyU(s3e)r Contributed Perl DocumentaMtaiyopnole::Manual::BuySpy(3)
2
3
4

NAME

6       Maypole::Manual::BugSpy - The Maypole iBuySpy Portal
7

DESCRIPTION

9       I think it's good fun to compare Maypole against other frameworks, so
10       here's how to build the ASP.NET tutorial site in Maypole.
11
12       We begin with a lengthy process of planning and investigating the
13       sources. Of prime interest is the database schema and the initial data,
14       which we convert to a MySQL database. Converting MS SQL to MySQL is not
15       fun.  I shall spare you the gore. Especially the bit where the default
16       insert IDs didn't match up between the tables.
17
18       The "ibsportal" database has a number of tables which describe how the
19       portal should look, and some tables which describe the data that should
20       appear on it. The portal is defined in terms of a set of modules; each
21       module takes some data from somewhere, and specifies a template to be
22       used to format the data. This is quite different from how Maypole nor‐
23       mally operates, so we have a choice as to whether we're going to com‐
24       pletely copy this design, or use a more "natural" implementation in
25       terms of having the portal display defined as a template itself, with
26       all the modules specified right there in Template Toolkit code rather
27       than picked up from the database. This would be much faster, since you
28       get one shot of rendering instead of having to process each module's
29       template independently. The thing is, I feel like showing off precisely
30       how flexible Maypole is, so we'll do it the hard way.
31
32       The first thing we need to do is get the database into some sort of
33       useful shape, and work out the relationships between the tables. This
34       of course requires half a day of playing with GraphViz, Omnigraffle and
35       mysql, but ended up with something like this:
36
37       This leads naturally to the following driver code:
38
39           package Portal;
40           use Maypole::Application;
41           Portal->setup("dbi:mysql:ibsportal");
42           use Class::DBI::Loader::Relationship;
43           Portal->config->loader->relationship($_) for (
44               "A module has a definition",  "A module has settings",
45               "A tab has modules",          "A portal has tabs",
46               "A role has a portal",        "A definition has a portal",
47               "A module has announcements", "A module has contacts",
48               "A module has discussions",   "A module has events",
49               "A module has htmltexts",     "A module has links",
50               "A module has documents",
51               "A user has roles via userrole"
52           );
53           1;
54
55       As you can see, a portal is made up of a number of different tabs; the
56       tabs contain modules, but they're separated into different panes, so a
57       module knows whether it belongs on the left pane, the right pane or the
58       center. A module also knows where it appears in the pane.
59
60       We'll begin by mocking up the portal view in plain text, like so:
61
62           use Portal;
63           my $portal = Portal::Portal->retrieve(2);
64           for my $tab ($portal->tabs) {
65               print $tab,"\n";
66               for my $pane (qw(LeftPane ContentPane RightPane)) {
67                   print "\t$pane:\n";
68                   for (sort { $a->module_order <=> $b->module_order }
69                       $tab->modules(pane => $pane)) {
70                       print "\t\t$_:\t", $_->definition,"\n";
71                   }
72               }
73               print "\n";
74           }
75
76       This dumps out the tabs of our portal, along with the modules in each
77       tab and their types; this lets us check that we've got the database set
78       up properly. If we have, it should produce something like this:
79
80           Home
81                   LeftPane:
82                           Quick link:     Quicklink
83                   ContentPane:
84                           Welcome to the IBuySpy Portal:  Html Document
85                           News and Features:      announcement
86                           Upcoming event: event
87                   RightPane:
88                           This Week's Special:    Html Document
89                           Top Movers:     XML/XSL
90
91           ...
92
93       Now we want to get the front page up; for the moment, we'll just have
94       it display the module names and their definitions like our text
95       mock-up, and we'll flesh out the actual modules later.
96
97       But before we do that, we'll write a front-end URL handler method, to
98       allow us to ape the ASP file names. Why do we want to make a Maypole
99       site look like it's running ".aspx" files? Because we can! - and
100       because I want to show we don't necessarily have to follow the Maypole
101       tradition of having our URLs look like "/table/action/id/arguments".
102
103           our %pages = (
104               "DesktopDefault.aspx" => { action => "view", table => "tab" },
105               "MobileDefault.aspx"  => { action => "view_mobile", table => "tab" },
106           );
107
108           sub parse_path {
109               my $self = shift;
110               $self->path("DesktopDefault.aspx") unless $self->path;
111               return $self->SUPER::parse_path if not exists $pages{$self->path};
112               my $page = $pages{$self->path} ;
113               $self->action($page->{action});
114               $self->table($page->{table});
115               my %query = $self->ar->args;
116               $self->args( [ $query{tabid} ⎪⎪ $query{ItemID} ⎪⎪ 1] );
117           }
118
119           1;
120
121       Here we're overriding the "parse_path" method which takes the "path"
122       slot from the request and populates the "table", "action" and "args"
123       slots. If the user has asked for a page we don't know about, we ask the
124       usual Maypole path handling method to give it a try; this will become
125       important later on. We turn the default page, "DesktopDefault.aspx",
126       into the equivalent of "/tab/view/1" unless another "tabid" or "ItemID"
127       is given in the query parameters; this allows us to use the
128       ASP.NET-style "DesktopDefault.aspx?tabid=3" to select a tab.
129
130       Now we have to create our "tab/view" template; the majority of this is
131       copied from the DesktopDefault.aspx source, but our panes look like
132       this:
133
134           <td id="LeftPane" Width="170">
135               [% pane("LeftPane") %]
136           </td>
137           <td width="1">
138           </td>
139           <td id="ContentPane" Width="*">
140               [% pane("ContentPane") %]
141           </td>
142           <td id="RightPane" Width="230">
143               [% pane("RightPane") %]
144           </td>
145           <td width="10">
146               &nbsp;
147          </td>
148
149       The "pane" macro has to be the Template Toolkit analogue of the Perl
150       code we used for our mock-up:
151
152           [% MACRO pane(panename) BLOCK;
153               FOR module = tab.modules("pane", panename);
154                   "<P>"; module; " - "; module.definition; "</P>";
155               END;
156           END;
157
158       Now, the way that the iBuySpy portal works is that each module has a
159       definition, and each definition contains a path to a template: "$mod‐
160       ule->definition->DesktopSrc" returns a path name for an "ascx" compo‐
161       nent file. All we need to do is convert those files from ASP to the
162       Template Toolkit, and have Maypole process each component for each mod‐
163       ule, right?
164
165       Components and templates
166
167       Dead right, but it was here that I got too clever. I guess it was the
168       word "component" that set me off. I thought that since the page was
169       made up of a large number of different modules, all requiring their own
170       set of objects, I should use a separate Maypole sub-request for each
171       one, as shown in the "Component-based pages" recipe in the Request
172       Cookbook.
173
174       So this is what I did. I created a method in "Portal::Module" that
175       would set the template to the appropriate "ascx" file:
176
177           sub view_desktop :Exported {
178               my ($self, $r) = @_;
179               $r->template($r->objects->[0]->definition->DesktopSrc);
180           }
181
182       and changed the "pane" macro to fire off a sub-request for each module:
183
184           [% MACRO pane(panename) BLOCK;
185               FOR module = tab.modules("pane", panename);
186                   SET path = "/module/view_desktop/" _ module.id;
187                   request.component(path);
188               END;
189           END; %]
190
191       This did the right thing, and a call to "/module/view_desktop/12" would
192       look up the "Html Document" module definition, find the "DesktopSrc" to
193       be DesktopModules/HtmlModule.ascx, and process module 12 with that tem‐
194       plate. Once I had converted HtmlModule.ascx to be a Template Toolkit
195       file (and we'll look at the conversion of the templates in a second) it
196       would display nicely on my portal.
197
198       Except it was all very slow; we were firing off a large number of May‐
199       pole requests in series, so that each template could get at the objects
200       it needed. Requests were taking 5 seconds.
201
202       That's when it dawned on me that these templates don't actually need
203       different objects at all. The only object of interest that "/mod‐
204       ule/view_desktop" is passing in is a "module" object, and each template
205       figures everything out by accessor calls on that. But we already have a
206       "module" object, in our "FOR" loop - we're using it to make the compo‐
207       nent call, after all! Why not just "PROCESS" each template inside the
208       loop directly?
209
210           [% MACRO pane(panename) BLOCK;
211               FOR module = tab.modules("pane", panename);
212                   SET src = module.definition.DesktopSrc;
213                   TRY;
214                       PROCESS $src;
215                   CATCH DEFAULT;
216                       "Bah, template $src broke on me!";
217                   END;
218               END;
219           END; %]
220
221       This worked somewhat better, and took request times from 5 seconds down
222       to acceptable sub-second levels again. I could take the "view_desktop"
223       method out again; fewer lines of code to maintain is always good. Now
224       all that remained to do for the view side of the portal was to convert
225       our ASP templates over to something sensible.
226
227       ASP to Template Toolkit
228
229       They're all much of a muchness, these templating languages. Some of
230       them, though, are just a wee bit more verbose than others. For
231       instance, the banner template which appears in the header consists of
232       104 lines of ASP code; most of those are to create the navigation bar
233       of tabs that we can view. Now I admit that we're slightly cheating at
234       the moment since we don't have the concept of a logged-in user and so
235       we don't distinguish between the tabs that anyone can see and those
236       than only an admin can see, but we'll come back to it later. Still, 104
237       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       "<%#" and "<%=" tags? They're the equivalent of our Template Toolkit
264       "[% %]" tags. "Request.ApplicationPath"? That's our "base" template
265       argument.
266
267       In our version we ask the portal what tabs it has, and display the list
268       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
287       this "portal" variable from? We need to tell the "Portal" class to put
288       the 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
295       drudgery; on the whole, there was precisely nothing interesting about
296       them at all - we merely converted a particularly verbose templating
297       language (and if I never see "asp:BoundColumn" again, it'll be no loss)
298       into a rather more sophisticated one.
299
300       The simplest component, HtmlModule.ascx, asks a module for its associ‐
301       ated "htmltexts", and then displays the "DesktopHtml" for each of them
302       in a table.  This was 40 lines of ASP.NET, including more odious C# to
303       make the SQL calls and retrieve the "htmltexts". But we can do all that
304       retrieval by magic, so our 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 "portal:title" tag
317       until we know what to do with it - it's obvious that we should turn it
318       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; Image‐
321       Module.ascx took the height, width and image source properties of an
322       image from the module's "settings" table, and displayed an "IMG" tag
323       with the appropriate values. This is only slightly difficult because we
324       have to arrange the array of "module.settings" into a hash of
325       "key_name" => "setting" pairs. Frankly, I can't be bothered to do this
326       in the template, so we'll add it into the "template_args" again. This
327       time "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 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 trans‐
348       lated, we now have a complete replica of the front page of the portal
349       and all its tabs. It's fast, it's been developed rapidly, and it's less
350       than 50 lines of Perl code so far. But it's not finished yet.
351
352       Adding users
353
354       ...
355
356       Links
357
358       Contents, Next That's all folks! Time to start coding ..., Previous
359       Flox
360
361
362
363perl v5.8.8                       2005-11-23        Maypole::Manual::BuySpy(3)
Impressum