implement access to the cart for the new template tagging
authorTony Cook <tony@develop-help.com>
Fri, 16 Nov 2012 05:05:30 +0000 (16:05 +1100)
committerTony Cook <tony@develop-help.com>
Fri, 16 Nov 2012 05:05:30 +0000 (16:05 +1100)
MANIFEST
site/cgi-bin/bse.cfg
site/cgi-bin/modules/BSE/Cart.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Util/DynamicTags.pm
site/cgi-bin/modules/BSE/Util/Format.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Util/Tags.pm
site/cgi-bin/modules/BSE/Variables.pm
site/templates/cart_base.tmpl

index aaf68ca..55fffaa 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -67,6 +67,7 @@ site/cgi-bin/modules/BSE/Cache.pm
 site/cgi-bin/modules/BSE/Cache/Cache.pm
 site/cgi-bin/modules/BSE/Cache/CHI.pm
 site/cgi-bin/modules/BSE/Cache/Memcached.pm
+site/cgi-bin/modules/BSE/Cart.pm
 site/cgi-bin/modules/BSE/Cfg.pm
 site/cgi-bin/modules/BSE/CfgInfo.pm
 site/cgi-bin/modules/BSE/CGI.pm
@@ -274,6 +275,7 @@ site/cgi-bin/modules/BSE/UserReg.pm
 site/cgi-bin/modules/BSE/Util/ContentType.pm
 site/cgi-bin/modules/BSE/Util/DynamicTags.pm
 site/cgi-bin/modules/BSE/Util/DynSort.pm
+site/cgi-bin/modules/BSE/Util/Format.pm
 site/cgi-bin/modules/BSE/Util/HTML.pm
 site/cgi-bin/modules/BSE/Util/Iterate.pm
 site/cgi-bin/modules/BSE/Util/Prereq.pm
index dbc7f98..f1ce876 100644 (file)
@@ -474,3 +474,7 @@ bse_audit_log_clean=1
 1=home page
 2=more home page
 5=sidebar
+
+[number money]
+divisor=100
+places=2
diff --git a/site/cgi-bin/modules/BSE/Cart.pm b/site/cgi-bin/modules/BSE/Cart.pm
new file mode 100644 (file)
index 0000000..d802dd9
--- /dev/null
@@ -0,0 +1,267 @@
+package BSE::Cart;
+use strict;
+use Scalar::Util;
+
+our $VERSION = "1.000";
+
+=head1 NAME
+
+BSE::Cart - abstraction for the BSE cart.
+
+=head1 SYNOPSIS
+
+  use BSE::Cart;
+  my $cart = BSE::Cart->new($req);
+
+  my $items = $cart->items;
+  my $products = $cart->products;
+
+=head1 DESCRIPTION
+
+This class provides a simple abstraction for access to the BSE
+shopping cart.
+
+This is intended for use in templates, but may be expanded further.
+
+=head1 METHODS
+
+=over
+
+=item new()
+
+Create a new cart object based on the session.
+
+=cut
+
+sub new {
+  my ($class, $req) = @_;
+
+  my $self = bless
+    {
+     products => {},
+     req => $req,
+    }, $class;
+  Scalar::Util::weaken($self->{req});
+  my $items = $req->session->{cart} || [];
+  my $myself = $self;
+  Scalar::Util::weaken($myself);
+  my $index = 0;
+  $self->{items} =
+    [
+     map
+     {
+       my $myself = $self;
+       my $myindex = $index++;
+       my %item = %$_;
+       my $id = $item{productId};
+       my $options = $item{options};
+       $item{product} = sub { $myself->_product($id) };
+       $item{extended} = $item{price} * $item{units};
+       $item{link} = sub { $myself->_product_link($id) };
+       $item{option_list} = sub { $myself->_option_list($myindex) };
+       $item{option_text} = sub { $myself->_option_text($myindex) };
+
+       my $session_id = $item{session_id};
+       $item{session} = sub { $myself->_item_session($session_id) };
+
+       \%item;
+     } @$items
+    ];
+
+  return $self;
+}
+
+=item items()
+
+Return an array reference of cart items.
+
+=cut
+
+sub items {
+  return $_[0]{items};
+}
+
+=item products().
+
+Return an array reference of products in the cart, corresponding to
+the array reference returned by items().
+
+=cut
+
+sub products {
+  my $self = shift;
+  return [ map $self->_product($_->{productId}), @{$self->items} ];
+}
+
+=item total_cost
+
+Return the total cost of the items in the cart.
+
+=cut
+
+sub total_cost {
+  my ($self) = @_;
+
+  my $total_cost = 0;
+  for my $item (@{$self->items}) {
+    $total_cost += $item->{extended};
+  }
+
+  return $total_cost;
+}
+
+=item total_units
+
+Return the total number of units in the cart.
+
+=cut
+
+sub total_units {
+  my ($self) = @_;
+
+  my $total_units = 0;
+  for my $item (@{$self->items}) {
+    $total_units += $item->{units};
+  }
+
+  return $total_units;
+}
+
+=item total
+
+Total of items in the cart and any custom extras.
+
+=cut
+
+=back
+
+=head2 Item Members
+
+=over
+
+=item product
+
+Returns the product for that line item.
+
+=cut
+
+sub _product {
+  my ($self, $id) = @_;
+
+  my $product = $self->{products}{$id};
+  unless ($product) {
+    require Products;
+    $product = Products->getByPkey($id)
+      or die "No product $id\n";
+    # FIXME
+    if ($product->generator ne "Generate::Product") {
+      require BSE::TB::Seminars;
+      $product = BSE::TB::Seminars->getByPkey($id)
+       or die "Not a product, not a seminar $id\n";
+    }
+
+    $self->{products}{$id} = $product;
+  }
+  return $product;
+
+}
+
+=item extended
+
+The extended price for the item.
+
+=item link
+
+A link to the product.
+
+=cut
+
+sub _product_link {
+  my ($self, $id) = @_;
+
+  my $product = $self->_product($id);
+  my $link = $product->link;
+  unless ($link =~ /^\w+:/) {
+    $link = BSE::Cfg->single->entryErr("site", "url") . $link;
+  }
+
+  return $link;
+}
+
+=item option_list
+
+Return a list of options for the item, each with:
+
+=over
+
+=item *
+
+id, name - the identifier for the option
+
+=item *
+
+value - the value of the option.
+
+=item *
+
+desc - the description of the option
+
+=item *
+
+display - display of the option value
+
+=back
+
+=cut
+
+sub _option_list {
+  my ($self, $index) = @_;
+
+  my $item = $self->items()->[$index];
+  my $product = $self->_product($item->{productId});
+
+  return [ $product->option_descs(BSE::Cfg->single, $item->{options}) ];
+}
+
+=item option_text
+
+Display text for options for the item.
+
+=cut
+
+sub _option_text {
+  my ($self, $index) = @_;
+
+  my $options = $self->_option_list($index);
+  return join(", ", map "$_->{desc}: $_->{display}", @$options);
+}
+
+=item session
+
+The session object of the seminar session
+
+=cut
+
+sub _item_session {
+  my ($self, $id) = @_;
+
+  $id or return;
+  my $session = $self->{sessions}{$id};
+  unless ($session) {
+    require BSE::TB::SeminarSessions;
+    $session = BSE::TB::SeminarSessions->getByPkey($id);
+    $self->{sessions}{$id} = $session;
+  }
+
+  return $session;
+}
+
+1;
+
+=back
+
+=head1 AUTHOR
+
+Tony Cook <tony@develop-help.com>
+
+=cut
index b5c41ae..15a6707 100644 (file)
@@ -5,8 +5,9 @@ use BSE::Util::HTML;
 use base 'BSE::ThumbLow';
 use base 'BSE::TagFormats';
 use BSE::CfgInfo qw(custom_class);
+use BSE::Cart;
 
-our $VERSION = "1.025";
+our $VERSION = "1.026";
 
 =head1 NAME
 
@@ -57,6 +58,7 @@ sub tags {
   my ($self) = @_;
   
   my $req = $self->{req};
+
   return
     (
      BSE::Util::Tags->common($req),
diff --git a/site/cgi-bin/modules/BSE/Util/Format.pm b/site/cgi-bin/modules/BSE/Util/Format.pm
new file mode 100644 (file)
index 0000000..f1293ed
--- /dev/null
@@ -0,0 +1,37 @@
+package BSE::Util::Format;
+use strict;
+
+our $VERSION = "1.000";
+
+sub bse_number {
+  my ($format, $value, $cfg) = @_;
+
+  $cfg ||= BSE::Cfg->single;
+  my $section = "number $format";
+  my $comma_sep = $cfg->entry($section, "comma", ",");
+  $comma_sep =~ s/^"(.*)"$/$1/;
+  $comma_sep =~ /\w/ and return "* comma cannot be a word character *";
+  my $comma_limit = $cfg->entry($section, "comma_limit", 1000);
+  my $commify = $cfg->entry($section, "commify", 1);
+  my $dec_sep = $cfg->entry($section, "decimal", ".");
+  my $div = $cfg->entry($section, "divisor", 1)
+    or return "* divisor must be non-zero *";
+  my $places = $cfg->entry($section, "places", -1);
+
+  my $div_value = $value / $div;
+  my $formatted = $places < 0 ? $div_value : sprintf("%.*f", $places, $div_value);
+
+  my ($int, $frac) = split /\./, $formatted;
+  if ($commify && $int >= $comma_limit) {
+    1 while $int =~ s/([0-9])([0-9][0-9][0-9]\b)/$1$comma_sep$2/;
+  }
+
+  if (defined $frac) {
+    return $int . $dec_sep . $frac;
+  }
+  else {
+    return $int;
+  }
+}
+
+1;
index 9bf23af..d2cc1e3 100644 (file)
@@ -8,7 +8,7 @@ use vars qw(@EXPORT_OK @ISA);
 @ISA = qw(Exporter);
 require Exporter;
 
-our $VERSION = "1.022";
+our $VERSION = "1.023";
 
 sub _get_parms {
   my ($acts, $args) = @_;
@@ -1308,32 +1308,9 @@ sub tag_number {
   my ($format, $value) = 
     DevHelp::Tags->get_parms($args, $acts, $templater);
   $format or return "* no number format *";
-  my $section = "number $format";
-  my $cfg = BSE::Cfg->single;
-  my $comma_sep = $cfg->entry($section, "comma", ",");
-  $comma_sep =~ s/^"(.*)"$/$1/;
-  $comma_sep =~ /\w/ and return "* comma cannot be a word character *";
-  my $comma_limit = $cfg->entry($section, "comma_limit", 1000);
-  my $commify = $cfg->entry($section, "commify", 1);
-  my $dec_sep = $cfg->entry($section, "decimal", ".");
-  my $div = $cfg->entry($section, "divisor", 1)
-    or return "* divisor must be non-zero *";
-  my $places = $cfg->entry($section, "places", -1);
-
-  my $div_value = $value / $div;
-  my $formatted = $places < 0 ? $div_value : sprintf("%.*f", $places, $div_value);
-
-  my ($int, $frac) = split /\./, $formatted;
-  if ($commify && $int >= $comma_limit) {
-    1 while $int =~ s/([0-9])([0-9][0-9][0-9]\b)/$1$comma_sep$2/;
-  }
 
-  if (defined $frac) {
-    return $int . $dec_sep . $frac;
-  }
-  else {
-    return $int;
-  }
+  require BSE::Util::Format;
+  return BSE::Util::Format::bse_number($format, $value);
 }
 
 =item mail_tags()
index f60e230..39b72fb 100644 (file)
@@ -4,7 +4,7 @@ use Scalar::Util qw(blessed);
 use BSE::TB::Site;
 use BSE::Util::HTML;
 
-our $VERSION = "1.006";
+our $VERSION = "1.007";
 
 sub _base_variables {
   my ($self, %opts) = @_;
@@ -26,6 +26,12 @@ sub _base_variables {
        return escape_html(Data::Dumper::Dumper(shift));
      },
      categorize_tags => \&_categorize_tags,
+     date => \&_date_format,
+     now => \&date_now,
+     number => sub {
+       require BSE::Util::Format;
+       return BSE::Util::Format::bse_number(@_);
+     },
     );
 }
 
@@ -43,10 +49,16 @@ sub dyn_variables {
 
   my $req = $opts{request} or die "No request parameter";
   my $cgi = $req->cgi;
+  my $cart;
   return
     +{
       $self->_base_variables(%opts),
       paged => sub { return _paged($cgi, @_) },
+      cart => sub {
+       require BSE::Cart;
+       $cart ||= BSE::Cart->new($req);
+       return $cart;
+      },
      };
 }
 
@@ -202,6 +214,29 @@ sub _variable_class {
   }
 }
 
+# format an SQL format date
+sub _date_format {
+  my ($format, $date) = @_;
+
+  my ($year, $month, $day, $hour, $min, $sec) = 
+    $date =~ /(\d+)\D+(\d+)\D+(\d+)(?:\D+(\d+)\D+(\d+)\D+(\d+))?/;
+  $hour = $min = $sec = 0 unless defined $sec;
+  $year -= 1900;
+  --$month;
+  # passing the isdst as 0 seems to provide a more accurate result than
+  # -1 on glibc.
+  require DevHelp::Date;
+  return DevHelp::Date::dh_strftime($format, $sec, $min, $hour, $day, $month, $year, -1, -1, -1);
+}
+
+sub _date_now {
+  my ($fmt) = @_;
+
+  $fmt ||= "%d-%b-%Y";
+  require DevHelp::Date;
+  return DevHelp::Date::dh_strftime($fmt, localtime);
+}
+
 1;
 
 =head1 NAME
@@ -266,6 +301,14 @@ a name (of the category) and a list of tags in that category.
 
 The article and product collections.
 
+=item date(format, when)
+
+Format an SQL date/time.
+
+=item now(format)
+
+Format the current date/time.
+
 =back
 
 =head1 DYNAMIC ONLY VARIABLES
@@ -389,6 +432,10 @@ ppname - the name of the items per page parameter
 
 =back
 
+=item bse.cart
+
+The contents of the cart.  See L<BSE::Cart> for details.
+
 =back
 
 =head1 AUTHOR
index c9020b0..a9006d0 100644 (file)
             <td height="18"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Price:</b></font>&nbsp;</td>
             <td height="18">&nbsp; </td>
           </tr>
-          <:ifCount:>
-          <:iterator begin items:> 
+         <:-.set items = assert_dynamic ? bse.cart.items : 0 -:>
+          <:.if items.size -:>
+           <:.for item in items -:>
+             <:.set options = item.option_list -:>
+             <:.set session = item.session -:>
           <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
-            <td width="100%" align="left"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:item link:>"><:item 
-              description:> <:if Options:>(<:iterator begin options:><:option desc:>: 
-              <:option label:><:iterator separator options:>, <:iterator end options:>)<:or 
-              Options:><:eif Options:></a><:ifItem session_id:>(session at <:location description:> <:date "%H:%M %d/%m/%Y" session when_at:>)<:or:><:eif:></font></td>
+            <td width="100%" align="left"> &nbsp;<font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:= item.link | html:>"><:= item.product.description | html :> <:.if options.size:>(<:.for option in options:><:= loop.index ? ", " : "" :><:= option.desc | html:>: 
+              <:= option.display |html :><:.end for:>)<:.end if -:></a><:.if item.session_id:>(session at <:= session.location.description | html:> <:= bse.date("%H:%M %d/%m/%Y", session.when_at) -:>)<:.end if:></font></td>
             <td nowrap align="center"> 
-              <input type="text" name="quantity_<:index:>" size="2" value="<:item units:>">
+              <input type="text" name="quantity_<:= loop.index :>" size="2" value="<:= item.units :>">
             </td>
-            <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:money item price:></b></font></td>
+            <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:= bse.number("money", item.price) | html :></b></font></td>
             <td nowrap> 
-              <input type="submit" name="delete_<:index:>" value="Remove">
+              <input type="submit" name="delete_<:= loop.index :>" value="Remove">
             </td>
           </tr>
-          <:iterator end items:>
-          <:or:>
+           <:.end for -:>
+          <:.else -:>
           <tr valign="middle" align="center" bgcolor="#FFFFFF"> 
             <td width="100%" height="20" align="center" colspan="4"><font face="Verdana, Arial, Helvetica, sans-serif" size="2">You have no items in your shopping cart!</font></td>
            </tr>
-          <:eif:>
+          <:.end if -:>
         </table>
       </td>
     </tr>