]> git.decadent.org.uk Git - maypole.git/commitdiff
A request thing that makes me feel stupid, and the current state of buyspy.
authorSimon Cozens <simon@simon-cozens.org>
Wed, 14 Apr 2004 17:17:11 +0000 (17:17 +0000)
committerSimon Cozens <simon@simon-cozens.org>
Wed, 14 Apr 2004 17:17:11 +0000 (17:17 +0000)
git-svn-id: http://svn.maypole.perl.org/Maypole/trunk@134 48953598-375a-da11-a14b-00016c27c3ee

doc/BuySpy.pod [new file with mode: 0644]
doc/Request.pod

diff --git a/doc/BuySpy.pod b/doc/BuySpy.pod
new file mode 100644 (file)
index 0000000..ead5e20
--- /dev/null
@@ -0,0 +1,339 @@
+=head1 The Maypole iBuySpy Portal
+
+I think it's good fun to compare Maypole
+
+We begin with a length process of planning and investigating the
+sources. Of prime interest is the database schema and the initial data,
+which we convert to a Mysql database. Converting MS SQL to Mysql is not fun.
+I shall spare you the gore. Especially the bit where the default insert IDs
+didn't match up between the tables.
+
+The C<ibsportal> database has a number of tables which describe how the
+portal should look, and some tables which describe the data that should
+appear on it. The portal is defined in terms of a set of modules; each
+module takes some data from somewhere, and specifies a template to be
+used to format the data. This is quite different from how Maypole
+normally operates, so we have a choice as to whether we're going to
+completely copy this design, or use a more "natural" implementation in 
+terms of having the portal display defined as a template itself, with
+all the modules specified right there in Template Toolkit code rather
+than picked up from the database. This would be much faster, since you
+get one shot of rendering instead of having to process each module's
+template independently. The thing is, I feel like showing off
+precisely how flexible Maypole is, so we'll do it the hard way.
+
+The first thing we need to do is get the database into some sort of
+useful shape, and work out the relationships between the tables. This of
+course requires half a day of playing with GraphViz, Omnigraffle and
+mysql, but ended up with something like this:
+
+=for html
+<img src="ibs-schema.png">
+
+This leads naturally to the following driver code:
+
+    package Portal;
+    use base 'Apache::MVC';
+    Portal->setup("dbi:mysql:ibsportal");
+    use Class::DBI::Loader::Relationship;
+    Portal->config->{loader}->relationship($_) for (
+        "A module has a definition",  "A module has settings",
+        "A tab has modules",          "A portal has tabs",
+        "A role has a portal",        "A definition has a portal",
+        "A module has announcements", "A module has contacts",
+        "A module has discussions",   "A module has events",
+        "A module has htmltexts",     "A module has links",
+        "A module has documents",
+        "A user has roles via userrole"
+    );
+    1;
+
+As you can see, a portal is made up of a number of different tabs;
+the tabs contain modules, but they're separated into different panes,
+so a module knows whether it belongs on the left pane, the right pane
+or the center. A module also knows where it appears in the pane.
+
+We'll begin by mocking up the portal view in plain text, like so:
+
+    use Portal;
+    my $portal = Portal::Portal->retrieve(2);
+    for my $tab ($portal->tabs) {
+        print $tab,"\n";
+        for my $pane (qw(LeftPane ContentPane RightPane)) {
+            print "\t$pane:\n";
+            for (sort { $a->module_order <=> $b->module_order }
+                $tab->modules(pane => $pane)) {
+                print "\t\t$_:\t", $_->definition,"\n";
+            }
+        }
+        print "\n";
+    }
+
+This dumps out the tabs of our portal, along with the modules in each
+tab and their types; this lets us check that we've got the database
+set up properly. If we have, it should produce something like this:
+
+    Home
+            LeftPane:
+                    Quick link:     Quicklink
+            ContentPane:
+                    Welcome to the IBuySpy Portal:  Html Document
+                    News and Features:      announcement
+                    Upcoming event: event
+            RightPane:
+                    This Week's Special:    Html Document
+                    Top Movers:     XML/XSL
+
+    ...
+
+Now we want to get the front page up; for the moment, we'll just have it
+display the module names and their definitions like our text mock-up,
+and we'll flesh out the actual modules later.
+
+But before we do that, we'll write a front-end URL handler method, to
+allow us to ape the ASP file names. Why do we want to make a Maypole
+site look like it's running C<.aspx> files? Because we can! - and
+because I want to show we don't necessarily B<have> to follow the
+Maypole tradition of having our URLs look like
+C</I<table>/I<action>/I<id>/I<arguments>>. 
+
+    our %pages = (
+        "DesktopDefault.aspx" => { action => "view", table => "tab" },
+        "MobileDefault.aspx"  => { action => "view_mobile", table => "tab" },
+    );
+
+    sub parse_path {
+        my $self = shift;
+        $self->{path} ||= "DesktopDefault.aspx";
+        return $self->SUPER::parse_path if not exists $pages{$self->{path}};
+        my $page = $pages{$self->{path}} ;
+        $self->{action} = $page->{action};
+        $self->{table} = $page->{table};
+        my %query = $self->{ar}->args;
+        $self->{args} = [ $query{tabid} || $query{ItemID} || 1];
+    }
+
+    1;
+
+Here we're overriding the C<parse_path> method which takes the C<path>
+slot from the request and populates the C<table>, C<action> and
+C<arguments> slots. If the user has asked for a page we don't know
+about, we ask the usual Maypole path handling method to give it a try;
+this will become important later on. We turn the default page,
+C<DesktopDefault.aspx>, into the equivalent of C</tab/view/1> unless
+another C<tabid> or C<ItemID> is given in the query parameters; this allows us
+to use the ASP.NET-style C<DesktopDefault.aspx?tabid=3> to select a tab.
+
+Now we have to create our C<tab/view> template; the majority of
+this is copied from the F<DesktopDefault.aspx> source, but our panes
+look like this:
+
+    <td id="LeftPane" Width="170">
+        [% pane("LeftPane") %]
+    </td>
+    <td width="1">
+    </td>
+    <td id="ContentPane" Width="*">
+        [% pane("ContentPane") %]
+    </td>
+    <td id="RightPane" Width="230">
+        [% pane("RightPane") %]
+    </td>
+    <td width="10">
+        &nbsp;
+   </td>
+
+The C<pane> macro has to be the Template Toolkit analogue of the Perl code
+we used for our mock-up:
+
+    [% MACRO pane(panename) BLOCK;
+        FOR module = tab.modules("pane", panename);
+            "<P>"; module; " - "; module.definition; "</P>";
+        END;
+    END;
+
+Now, the way that the iBuySpy portal works is that each module has a
+definition, and each definition contains a path to a template:
+C<$module-E<gt>definition-E<gt>DesktopSrc> returns a path name for an C<ascx>
+component file. All we need to do is convert those files from ASP to the
+Template Toolkit, and have Maypole process each component for each module,
+right?
+
+=head2 Components and templates
+
+Dead right, but it was here that I got too clever. I guess it was the word
+"component" that set me off. I thought that since the page was made up of a
+large number of different modules, all requiring their own set of objects, I
+should use a seperate Maypole sub-request for each one, as shown in the
+"Component-based pages" recipe in L<Request.pod>.
+
+So this is what I did. I created a method in C<Portal::Module> that would
+set the template to the appropriate C<ascx> file:
+
+    sub view_desktop :Exported {
+        my ($self, $r) = @_;
+        $r->{template} = $r->objects->[0]->definition->DesktopSrc;
+    }
+
+and changed the C<pane> macro to fire off a sub-request for each module:
+
+    [% MACRO pane(panename) BLOCK;
+        FOR module = tab.modules("pane", panename);
+            SET path = "/module/view_desktop/" _ module.id;
+            request.component(path);
+        END;
+    END; %]
+
+This did the right thing, and a call to C</module/view_desktop/12> would
+look up the C<Html Document> module definition, find the C<DesktopSrc> to
+be F<DesktopModules/HtmlModule.ascx>, and process module 12 with that
+template. Once I had converted F<HtmlModule.ascx> to be a Template Toolkit
+file (and we'll look at the conversion of the templates in a second) it
+would display nicely on my portal.
+
+Except it was all very slow; we were firing off a large number of Maypole
+requests in series, so that each template could get at the objects it
+needed. Requests were taking 5 seconds.
+
+That's when it dawned on me that these templates don't actually need different
+objects at all. The only object of interest that C</module/view_desktop> is
+passing in is a C<module> object, and each template figures everything out by
+accessor calls on that. But we already have a C<module> object, in our C<FOR>
+loop - we're using it to make the component call, after all! Why not just
+C<PROCESS> each template inside the loop directly?
+
+    [% MACRO pane(panename) BLOCK;
+        FOR module = tab.modules("pane", panename);
+            SET src = module.definition.DesktopSrc;
+            TRY;
+                PROCESS $src;
+            CATCH DEFAULT;
+                "Bah, template $src broke on me!";
+            END;
+        END;
+    END; %]
+
+This worked somewhat better, and took request times from 5 seconds down
+to acceptable sub-second levels again. I could take the C<view_desktop>
+method out again; fewer lines of code to maintain is always good. Now
+all that remained to do for the view side of the portal was to convert
+our ASP templates over to something sensible.
+
+=head2 ASP to Template Toolkit
+
+They're all much of a muchness, these templating languages. Some of
+them, though, are just a wee bit more verbose than others. For instance,
+the banner template which appears in the header consists of 104 lines
+of ASP code; most of those are to create the navigation bar of tabs
+that we can view. Now I admit that we're slightly cheating at the moment
+since we don't have the concept of a logged-in user and so we don't
+distinguish between the tabs that anyone can see and those than only an
+admin can see, but we'll come back to it later. Still, 104 lines, eh?
+
+The actual tab list is presented here:
+
+    <tr>
+        <td>
+            <asp:datalist id="tabs" cssclass="OtherTabsBg" repeatdirection="horizontal" ItemStyle-Height="25" SelectedItemStyle-CssClass="TabBg" ItemStyle-BorderWidth="1" EnableViewState="false" runat="server">
+                <ItemTemplate>
+                    &nbsp;<a href='<%= Request.ApplicationPath %>/DesktopDefault.aspx?tabindex=<%# Container.ItemIndex %>&tabid=<%# ((TabStripDetails) Container.DataItem).TabId %>' class="OtherTabs"><%# ((TabStripDetails) Container.DataItem).TabName %></a>&nbsp;
+                </ItemTemplate>
+                <SelectedItemTemplate>
+                    &nbsp;<span class="SelectedTab"><%# ((TabStripDetails) Container.DataItem).TabName %></span>&nbsp;
+                </SelectedItemTemplate>
+            </asp:datalist>
+        </td>
+    </tr>
+
+But it has to be built up in some 22 lines of C# code which creates and
+populates an array and then binds it to a template parameter. See those
+C<E<lt>%#> and C<E<lt>%=> tags? They're the equivalent of our Template
+Toolkit C<[% %]> tags. C<Request.ApplicationPath>? That's our C<base>
+template argument. 
+
+In our version we ask the portal what tabs it has, and display the
+list directly, displaying the currently selected tab differently:
+
+    <table id="Banner_tabs" class="OtherTabsBg" cellspacing="0" border="0">
+        <tr>
+    [% FOR a_tab = portal.tabs %]
+        [% IF a_tab.id == tab.id %]
+            <td class="TabBg" height="25">
+                &nbsp;<span class="SelectedTab">[%tab.name%]</span>&nbsp;
+        [% ELSE %]
+            <td height="25">
+                &nbsp;<a href='[%base%]DesktopDefault.aspx?tabid=[%a_tab.id%]' class="OtherTabs">[%a_tab.name%]</a>&nbsp;
+        [% END %]
+            </td>
+    [% END %]
+        </tr>
+    </table>
+
+This is the way the world should be. But wait, where have we pulled this
+C<portal> variable from? We need to tell the C<Portal> class to put the
+default portal into the template arguments:
+
+    sub additional_data {
+        shift->{template_args}{portal} = Portal::Portal->retrieve(2);
+    }
+
+Translating all the other ASP.NET components is a similar exercise in drudgery;
+on the whole, there was precisely nothing interesting about them at all - we
+merely converted a particularly verbose templating language (and if I never see
+C<asp:BoundColumn> again, it'll be no loss) into a rather more sophisticated
+one.
+
+The simplest component, F<HtmlModule.ascx>, asks a module for its associated
+C<htmltexts>, and then displays the C<DesktopHtml> for each of them in a table.
+This was 40 lines of ASP.NET, including more odious C# to make the SQL calls
+and retrieve the C<htmltexts>. But we can do all that retrieval by magic, so
+our F<HtmlModule.ascx> looks like this:
+
+    [% PROCESS module_title %]
+    <portal:title EditText="Edit" EditUrl="~/DesktopModules/EditHtml.aspx" />
+    <table id="t1" cellspacing="0" cellpadding="0">
+        <tr valign="top">
+            <td id="HtmlHolder">
+            [% FOR html = module.htmltexts; html.DesktopHtml; END %]
+            </td>
+        </tr>
+    </table>
+
+Now I admit that we've cheated here and kept that C<portal:title> tag
+until we know what to do with it - it's obvious that we should turn
+it into a link to edit the HTML of this module if we're allowed to.
+
+The next simplest one actually did provide a slight challenge;
+F<ImageModule.ascx> took the height, width and image source properties
+of an image from the module's C<settings> table, and displayed an C<IMG>
+tag with the appropriate values. This is only slightly difficult because
+we have to arrange the array of C<module.settings> into a hash of
+C<key_name> => C<setting> pairs. Frankly, I can't be bothered to do this
+in the template, so we'll add it into the C<template_args> again. This
+time C<addition_data> looks like:
+
+    sub additional_data {
+        my $r = shift;
+        shift->{template_args}{portal} = Portal::Portal->retrieve(2);
+        if ($r->{objects}->[0]->isa("Portal::Module")) {
+            $r->{template_args}{module_settings} =
+                { map { $_->key_name, $_->setting } 
+                  $r->{objects}->[0]->settings };
+        }
+    }
+
+And the F<ImageModule.ascx> drops from the 30-odd lines of ASP into:
+
+    [% PROCESS module_title; %]
+    <img id="Image1" border="0" src="[% module_settings.src %]" 
+      width="[% module_settings.width %]" 
+      height="[% module_settings.height %]" />
+    <br>
+
+Our portal is taking shape; after a few more templates have been translated,
+we now have a complete replica of the front page of the portal and all its
+tabs. It's fast, it's been developed rapidly, and it's less than 50 lines
+of Perl code so far. But it's not finished yet.
+
+=head2 Adding users
+
index 96f7c552ae954e378d696f61bfaf507654ed2211..1505d361d9a014b2f0c058e4e9c3f64b7676fb0e 100644 (file)
@@ -219,6 +219,9 @@ C</links/list_comp> will be placed in the C<links> DIV. Naturally, you're
 responsible for exporting actions and creating templates which return 
 fragments of HTML suitable for inserting into the appropriate locations.
 
+Alternatively, if you've already got all the objects you need, you can
+probably just C<[% PROCESS %]> the templates directly.
+
 =head3 Bailing out with an error
 
 Maypole's error handling sucks. Something really bad has happened to the