1Maypole::Manual::BuySpyU(s3e)r Contributed Perl DocumentaMtaiyopnole::Manual::BuySpy(3)
2
3
4
6 Maypole::Manual::BugSpy - The Maypole iBuySpy Portal
7
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
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 <a href='<%= Request.ApplicationPath %>/
249 DesktopDefault.aspx?tabindex=<%# Container.ItemIndex %>&tabid=
250 <%# ((TabStripDetails) Container.DataItem).TabId %>' class="OtherTabs">
251 <%# ((TabStripDetails) Container.DataItem).TabName %></a>
252 </ItemTemplate>
253 <SelectedItemTemplate>
254 <span class="SelectedTab">
255 <%# ((TabStripDetails) Container.DataItem).TabName %></span>
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 <span class="SelectedTab">[%tab.name%]</span>
276 [% ELSE %]
277 <td height="25">
278 <a href='[%base%]DesktopDefault.aspx?tabid=[%a_tab.id%]'
279 class="OtherTabs">[%a_tab.name%]</a>
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)