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
23       normally operates, so we have a choice as to whether we're going to
24       completely 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 mock-
95       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:
160       "$module->definition->DesktopSrc" returns a path name for an "ascx"
161       component file. All we need to do is convert those files from ASP to
162       the Template Toolkit, and have Maypole process each component for each
163       module, right?
164
165   Components and templates
166       Dead right, but it was here that I got too clever. I guess it was the
167       word "component" that set me off. I thought that since the page was
168       made up of a large number of different modules, all requiring their own
169       set of objects, I should use a separate Maypole sub-request for each
170       one, as shown in the "Component-based pages" recipe in the Request
171       Cookbook.
172
173       So this is what I did. I created a method in "Portal::Module" that
174       would set the template to the appropriate "ascx" file:
175
176           sub view_desktop :Exported {
177               my ($self, $r) = @_;
178               $r->template($r->objects->[0]->definition->DesktopSrc);
179           }
180
181       and changed the "pane" macro to fire off a sub-request for each module:
182
183           [% MACRO pane(panename) BLOCK;
184               FOR module = tab.modules("pane", panename);
185                   SET path = "/module/view_desktop/" _ module.id;
186                   request.component(path);
187               END;
188           END; %]
189
190       This did the right thing, and a call to "/module/view_desktop/12" would
191       look up the "Html Document" module definition, find the "DesktopSrc" to
192       be DesktopModules/HtmlModule.ascx, and process module 12 with that
193       template. Once I had converted HtmlModule.ascx to be a Template Toolkit
194       file (and we'll look at the conversion of the templates in a second) it
195       would display nicely on my portal.
196
197       Except it was all very slow; we were firing off a large number of
198       Maypole requests in series, so that each template could get at the
199       objects it needed. Requests were taking 5 seconds.
200
201       That's when it dawned on me that these templates don't actually need
202       different objects at all. The only object of interest that
203       "/module/view_desktop" is passing in is a "module" object, and each
204       template figures everything out by accessor calls on that. But we
205       already have a "module" object, in our "FOR" loop - we're using it to
206       make the component call, after all! Why not just "PROCESS" each
207       template inside the loop directly?
208
209           [% MACRO pane(panename) BLOCK;
210               FOR module = tab.modules("pane", panename);
211                   SET src = module.definition.DesktopSrc;
212                   TRY;
213                       PROCESS $src;
214                   CATCH DEFAULT;
215                       "Bah, template $src broke on me!";
216                   END;
217               END;
218           END; %]
219
220       This worked somewhat better, and took request times from 5 seconds down
221       to acceptable sub-second levels again. I could take the "view_desktop"
222       method out again; fewer lines of code to maintain is always good. Now
223       all that remained to do for the view side of the portal was to convert
224       our ASP templates over to something sensible.
225
226   ASP to Template Toolkit
227       They're all much of a muchness, these templating languages. Some of
228       them, though, are just a wee bit more verbose than others. For
229       instance, the banner template which appears in the header consists of
230       104 lines of ASP code; most of those are to create the navigation bar
231       of tabs that we can view. Now I admit that we're slightly cheating at
232       the moment since we don't have the concept of a logged-in user and so
233       we don't distinguish between the tabs that anyone can see and those
234       than only an admin can see, but we'll come back to it later. Still, 104
235       lines, eh?
236
237       The actual tab list is presented here: (reformated slightly for sanity)
238
239           <tr>
240               <td>
241                   <asp:datalist id="tabs" cssclass="OtherTabsBg"
242        repeatdirection="horizontal" ItemStyle-Height="25"
243        SelectedItemStyle-CssClass="TabBg" ItemStyle-BorderWidth="1"
244        EnableViewState="false" runat="server">
245                       <ItemTemplate>
246                           &nbsp;<a href='<%= Request.ApplicationPath %>/
247        DesktopDefault.aspx?tabindex=<%# Container.ItemIndex %>&tabid=
248        <%# ((TabStripDetails) Container.DataItem).TabId %>' class="OtherTabs">
249        <%# ((TabStripDetails) Container.DataItem).TabName %></a>&nbsp;
250                       </ItemTemplate>
251                       <SelectedItemTemplate>
252                           &nbsp;<span class="SelectedTab">
253        <%# ((TabStripDetails) Container.DataItem).TabName %></span>&nbsp;
254                       </SelectedItemTemplate>
255                   </asp:datalist>
256               </td>
257           </tr>
258
259       But it has to be built up in some 22 lines of C# code which creates and
260       populates an array and then binds it to a template parameter. See those
261       "<%#" and "<%=" tags? They're the equivalent of our Template Toolkit
262       "[% %]" tags. "Request.ApplicationPath"? That's our "base" template
263       argument.
264
265       In our version we ask the portal what tabs it has, and display the list
266       directly, displaying the currently selected tab differently:
267
268           <table id="Banner_tabs" class="OtherTabsBg" cellspacing="0" border="0">
269               <tr>
270           [% FOR a_tab = portal.tabs %]
271               [% IF a_tab.id == tab.id %]
272                   <td class="TabBg" height="25">
273                       &nbsp;<span class="SelectedTab">[%tab.name%]</span>&nbsp;
274               [% ELSE %]
275                   <td height="25">
276                       &nbsp;<a href='[%base%]DesktopDefault.aspx?tabid=[%a_tab.id%]'
277                       class="OtherTabs">[%a_tab.name%]</a>&nbsp;
278               [% END %]
279                   </td>
280           [% END %]
281               </tr>
282           </table>
283
284       This is the way the world should be. But wait, where have we pulled
285       this "portal" variable from? We need to tell the "Portal" class to put
286       the default portal into the template arguments:
287
288           sub additional_data {
289               shift->{template_args}{portal} = Portal::Portal->retrieve(2);
290           }
291
292       Translating all the other ASP.NET components is a similar exercise in
293       drudgery; on the whole, there was precisely nothing interesting about
294       them at all - we merely converted a particularly verbose templating
295       language (and if I never see "asp:BoundColumn" again, it'll be no loss)
296       into a rather more sophisticated one.
297
298       The simplest component, HtmlModule.ascx, asks a module for its
299       associated "htmltexts", and then displays the "DesktopHtml" for each of
300       them in a table.  This was 40 lines of ASP.NET, including more odious
301       C# to make the SQL calls and retrieve the "htmltexts". But we can do
302       all that retrieval by magic, so our HtmlModule.ascx looks like this:
303
304           [% PROCESS module_title %]
305           <portal:title EditText="Edit" EditUrl="~/DesktopModules/EditHtml.aspx" />
306           <table id="t1" cellspacing="0" cellpadding="0">
307               <tr valign="top">
308                   <td id="HtmlHolder">
309                   [% FOR html = module.htmltexts; html.DesktopHtml; END %]
310                   </td>
311               </tr>
312           </table>
313
314       Now I admit that we've cheated here and kept that "portal:title" tag
315       until we know what to do with it - it's obvious that we should turn it
316       into a link to edit the HTML of this module if we're allowed to.
317
318       The next simplest one actually did provide a slight challenge;
319       ImageModule.ascx took the height, width and image source properties of
320       an image from the module's "settings" table, and displayed an "IMG" tag
321       with the appropriate values. This is only slightly difficult because we
322       have to arrange the array of "module.settings" into a hash of
323       "key_name" => "setting" pairs. Frankly, I can't be bothered to do this
324       in the template, so we'll add it into the "template_args" again. This
325       time "additional_data" looks like:
326
327           sub additional_data {
328               my $r = shift;
329               shift->template_args->{portal} = Portal::Portal->retrieve(2);
330               if ($r->objects->[0]->isa("Portal::Module")) {
331                   $r->template_args->{module_settings} =
332                       { map { $_->key_name, $_->setting }
333                         $r->objects->[0]->settings };
334               }
335           }
336
337       And the ImageModule.ascx drops from the 30-odd lines of ASP into:
338
339           [% PROCESS module_title; %]
340           <img id="Image1" border="0" src="[% module_settings.src %]"
341             width="[% module_settings.width %]"
342             height="[% module_settings.height %]" />
343           <br>
344
345       Our portal is taking shape; after a few more templates have been
346       translated, we now have a complete replica of the front page of the
347       portal and all its tabs. It's fast, it's been developed rapidly, and
348       it's less than 50 lines of Perl code so far. But it's not finished yet.
349
350   Adding users
351       ...
352
353   Links
354       Contents, Next That's all folks! Time to start coding ..., Previous
355       Flox
356
357
358
359perl v5.38.0                      2023-07-20        Maypole::Manual::BuySpy(3)
Impressum