add some support for customizing product options
authorTony Cook <tony@develop-help.com>
Sat, 18 Apr 2020 03:09:51 +0000 (13:09 +1000)
committerTony Cook <tony@develop-help.com>
Sat, 18 Apr 2020 03:09:51 +0000 (13:09 +1000)
MANIFEST
site/cgi-bin/modules/BSE/Cart.pm
site/cgi-bin/modules/BSE/Edit/Product.pm
site/cgi-bin/modules/BSE/PubSub.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/Product.pm
site/cgi-bin/modules/BSE/UI/Shop.pm
site/templates/admin/edit_prodopts.tmpl
site/templates/admin/prodopt_edit.tmpl
site/templates/cart_base.tmpl

index efc039b..97df6f8 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -84,6 +84,7 @@ site/cgi-bin/modules/BSE/Coupon/Percent.pm
 site/cgi-bin/modules/BSE/Coupon/ProductPercent.pm
 site/cgi-bin/modules/BSE/Custom.pm
 site/cgi-bin/modules/BSE/CustomBase.pm
+site/cgi-bin/modules/BSE/CustomData.pm
 site/cgi-bin/modules/BSE/DB.pm
 site/cgi-bin/modules/BSE/DB/Mysql.pm
 site/cgi-bin/modules/BSE/DummyArticle.pm
@@ -151,6 +152,7 @@ site/cgi-bin/modules/BSE/Password/Plain.pm
 site/cgi-bin/modules/BSE/Passwords.pm
 site/cgi-bin/modules/BSE/PayPal.pm
 site/cgi-bin/modules/BSE/Permissions.pm
+site/cgi-bin/modules/BSE/PubSub.pm
 site/cgi-bin/modules/BSE/Regen.pm
 site/cgi-bin/modules/BSE/Report.pm
 site/cgi-bin/modules/BSE/Request.pm
index 5923e95..9a69209 100644 (file)
@@ -1,8 +1,9 @@
 package BSE::Cart;
 use strict;
 use Scalar::Util;
+use BSE::PubSub;
 
-our $VERSION = "1.015";
+our $VERSION = "1.016";
 
 =head1 NAME
 
@@ -894,6 +895,14 @@ sub price {
 
   unless (defined $self->{calc_price}) {
     $self->{calc_price} = $self->product->price(user => $self->{cart}{req}->siteuser);
+
+    BSE::PubSub->customize(
+      cart_price => {
+       cartitem => $self,
+       cart => $self->{cart},
+       price => \($self->{calc_price}),
+       request => $self->{cart}{req},
+      });
   }
 
   return $self->{calc_price};
index e668ffb..e85fc05 100644 (file)
@@ -8,9 +8,10 @@ use BSE::Util::Iterate;
 use BSE::Util::HTML;
 use BSE::CfgInfo 'product_options';
 use BSE::Util::Tags qw(tag_hash tag_article);
+use BSE::PubSub;
 use constant PRODUCT_CUSTOM_FIELDS_CFG => "product custom fields";
 
-our $VERSION = "1.016";
+our $VERSION = "1.017";
 
 =head1 NAME
 
@@ -300,6 +301,7 @@ sub low_edit_tags {
   my $price_tier;
   my %prices;
   $req->set_variable(product => $article);
+  BSE::PubSub->customize(product_edit_variables => { req => $req, product => $article, errors => \$errors });
   return 
     (
      product => [ \&tag_article, $article, $cfg ],
@@ -739,6 +741,14 @@ sub req_add_option {
   }
   $req->validate(fields => \%work_option_fields,
                 errors => \%errors);
+  BSE::PubSub->customize(
+    product_option_add_validate =>
+      {
+       req => $req,
+       errors => \%errors,
+       product => $article,
+       fields => \%work_option_fields,
+       });
   keys %errors
     and return $self->_service_error($req, $article, $articles, undef, 
                                     \%errors);
@@ -776,6 +786,15 @@ sub req_add_option {
     $option->save;
   }
 
+  BSE::PubSub->customize(
+    product_option_add =>
+      {
+       req => $req,
+       product => $article,
+       option => $option,
+       values => \%value_keys
+       });
+
   $req->is_ajax
     and return $req->json_content
       (
@@ -831,8 +850,17 @@ sub _common_option {
   keys %errors
     and return $self->_service_error($req, $article, $articles, undef, \%errors);
 
+  if ($template =~ /edit/) {
+    BSE::PubSub->customize(
+      product_edit_option_edit => {
+       req => $req,
+       product => $article,
+       option => $option
+       });
+  }
   $req->set_variable(option => $option);
   $req->messages($errors);
+
   my $it = BSE::Util::Iterate->new;
   my %acts;
   %acts =
@@ -881,6 +909,7 @@ sub req_edit_option {
   $req->user_can(bse_edit_prodopt_edit => $article)
     or return $self->_service_error($req, $article, $articles, "Insufficient product access to edit options");
 
+
   return $self->_common_option('admin/prodopt_edit', $req, $article, 
                               $articles, $msg, $errors);
 }
@@ -983,6 +1012,14 @@ sub req_save_option {
     and return $self->_service_error($req, $article, $articles, undef, \%errors, 'FIELD', "req_edit_option");
   $req->validate(fields => \%option_name,
                 errors => \%errors);
+  BSE::PubSub->customize(
+    product_option_edit_validate =>
+      {
+       req => $req,
+       errors => \%errors,
+       product => $article,
+       option => $option,
+       });
   my @values = $option->values;
   my %fields = map {; "value$_->{id}" => \%option_value } @values;
   $req->validate(fields => \%fields,
@@ -994,7 +1031,6 @@ sub req_save_option {
       or $errors{default_value} = "Unknown value selected as default";
   }
 
-  $DB::single = 1;
   my @new_values;
   my $index = 1;
   while ($index < 10 && defined $cgi->param("newvalue$index")) {
@@ -1002,7 +1038,7 @@ sub req_save_option {
     my $value = $cgi->param($field);
     $req->validate(fields => { $field => \%option_value },
                   errors => \%errors);
-    push @new_values, $value;
+    push @new_values, [ $field, $value ];
 
     ++$index;
   }
@@ -1028,14 +1064,24 @@ sub req_save_option {
     }
   }
   my $order = @values ? $values[-1]->display_order : time;
-  for my $value (@new_values) {
-    BSE::TB::ProductOptionValues->make
+  my %newvalues;
+  for my $new (@new_values) {
+    my ($name, $value) = @$new;
+    $newvalues{$name} = BSE::TB::ProductOptionValues->make
        (
         product_option_id => $option->id,
         value => $value,
         display_order => ++$order,
        );
   }
+  BSE::PubSub->customize(
+    product_option_edit_save =>
+      {
+       req => $req,
+       product => $article,
+       option => $option,
+       newvalues => \%newvalues,
+       });
 
   $req->is_ajax
     and return $req->json_content
diff --git a/site/cgi-bin/modules/BSE/PubSub.pm b/site/cgi-bin/modules/BSE/PubSub.pm
new file mode 100644 (file)
index 0000000..194d409
--- /dev/null
@@ -0,0 +1,220 @@
+package BSE::PubSub;
+use strict;
+use BSE::CfgInfo qw(load_class);
+
+our $VERSION = "1.000";
+
+my %subscribers;
+
+my %customizers;
+
+sub _class_desc {
+  my ($class) = @_;
+
+  ref($class) ? "Object of " . ref($class) . " type" : $class;
+}
+
+sub _load_handlers {
+  my ($name, $cache) = @_;
+
+  $cache->{_} and return;
+
+  my $cfg = BSE::Cfg->single;
+
+  my @entries = $cfg->entries($name);
+  while (@entries) {
+    my ($key, $val) = splice @entries, 0, 2;
+    $key =~ s/\d+$//;
+    $key =~ s/-.*//;
+    push @{$cache->{$key}}, [ split /,/, $val ];
+  }
+  $cache->{_} = 1;
+
+  # preloads
+  if ($cache->{preload}) {
+    for my $handler (@{$cache->{preload}}) {
+      my ($class, $method) = @$handler;
+
+      if (ref $class || eval { load_class($class); 1 }) {
+       if (!eval { $class->$method(); 1 }) {
+         _log_error("pubsub::preload", "Call to method $method of $class for preload threw an exception",
+                    { class => $class, method => $method, message => "preload", error => $@ });
+       }
+      }
+      else {
+         _log_error("pubsub::preload", "Loading $class for preload threw an exception",
+                    { class => $class, method => $method, message => "preload", error => $@ });
+      }
+    }
+  }
+}
+
+sub _sub_info {
+  my ($code) = @_;
+
+  ref $code eq "CODE"
+    or return +{ type => ref($code) };
+
+  my $info = eval {
+    require B;
+    my $cv = B::svref_2object($code);
+    +{
+      file => scalar($cv->FILE),
+      stash => scalar($cv->STASH->NAME),
+     };
+  };
+
+  $info ||= { error => $@ };
+
+  return $info;
+}
+
+sub _log_error {
+  my ($comp, $msg, $dump) = @_;
+
+  require BSE::TB::AuditLog;
+  BSE::TB::AuditLog->log
+      (
+       component => $comp,
+       msg => $msg,
+       dump => $dump,
+       actor => "S",
+       level => "error",
+       );
+}
+
+sub _publish {
+  my ($self, $section, $hash, $comp, $message, $param) = @_;
+
+  _load_handlers($section => $hash);
+  $hash->{$message}
+    or return;
+
+  for my $handler (@{$hash->{$message}}) {
+    if (ref($handler) eq "CODE") {
+      eval { $handler->($param); 1 }
+       or _log_error("pubsub::publish", "Internal handler for $message threw an exception", _sub_info($handler));
+    }
+    elsif (ref($handler) eq "ARRAY") {
+      my ($class, $method) = @$handler;
+      my $cls = _class_desc($class);
+      if (ref $class || eval { load_class($class); 1 }) {
+       if (!eval { $class->$method($param); 1 }) {
+         _log_error($comp, "Call to method $method of $class for message $message threw an exception",
+                    { class => _class_desc($class), method => $method, message => $message, error => $@ });
+       }
+      }
+      else {
+         _log_error($comp, "Loading $class for message $message threw an exception",
+                    { class => _class_desc($class), method => $method, message => $message, error => $@ });
+      }
+    }
+  }
+
+  return;
+}
+
+sub publish {
+  my ($self, $message, $param) = @_;
+
+  $self->_publish(subscribers => \%subscribers, "pubsub::publish", $message, $param);
+}
+
+sub customize {
+  my ($self, $message, $param) = @_;
+
+  $self->_publish(customizers => \%customizers, "pubsub::customize", $message, $param);
+
+  if ($customizers{"*"}) {
+    for my $class (@{$customizers{"*"}}) {
+      if ($class->can($message)) {
+       if (!eval { $class->$message($param); 1 }) {
+         _log_error("pubsub::customize", "Call to method $message for " . _class_desc($class) . " threw an exception", { class => _class_desc($class), method => $message, message => $message, error => $@,  preload => 1 });
+       }
+      }
+    }
+  }
+}
+
+sub handle {
+  my ($class, $message, $handler) = @_;
+
+  $customizers{$message} = $handler;
+}
+
+sub subscribe {
+  my ($class, $message, $handler) = @_;
+
+  $subscribers{$message} = $handler;
+}
+
+1;
+
+=head1 NAME
+
+BSE::Notify - BSE's publish/subcribe system.
+
+=head1 SYNOPSIS
+
+  use BSE::PubSub;
+
+  BSE::PubSub->publish(message_name => \%parameters);
+
+  BSE::PubSub->subscribe(message_name => \&code);
+  BSE::PubSub->subscribe(message_name => [ $class_object, $method ]);
+
+  BSE::PubSub->customize(custom_type => \%parameters);
+
+  BSE::PubSub->handle(custom_type => \&code);
+  BSE::PubSub->handle(custom_type => [ $class_object, $method ]);
+  BSE::PubSub->handle("*" => $class_object);
+
+  # in a config file somewhere
+  [subscribers]
+  messagename=classname,methodname,reserved
+
+  [customizers]
+  custom_type=classname,methodname,reserved
+
+=head1 DESCRIPTION
+
+A less messy, more extensible version of custom classes.
+
+Advantages over the custom class interface:
+
+=over
+
+=item *
+
+we're not passing a long list of positional arguments.
+
+=item *
+
+multiple handlers are better defined.
+
+=back
+
+The two methods are distinguished based on their purpose.
+
+=over
+
+=item *
+
+C<publish> - sends off a notification, no response is expected.
+Under some circumstances the message might be retained by the receiver
+or passed onto another system.
+
+=item *
+
+C<customize> - requests customization of a data structure.  This is
+intended for in-process use only.
+
+=back
+
+The return value of either method is unspecified and may change.
+
+Typically handlers for messages will be configured in the
+configuration file, but other parts of the system may request to
+handle specific messages (or customizations), but this will be rare.
+
+=cut
index efa485d..9a47e98 100644 (file)
@@ -103,12 +103,14 @@ sub _get_prod_options {
   for my $opt (@all_options) {
     my @opt_values = $opt->values;
     my %opt_values = map { $_->id => $_->value } @opt_values;
+    my %opt_valueobjs = map { $_->id => $_ } @opt_values;
     my $result_opt = 
       {
        id => $opt->key,
        name => $opt->key,
        desc => $opt->name,
        value => $values[$index],
+       valueobj => $opt_valueobjs{$values[$index]},
        type => $opt->type,
        labels => \%opt_values,
        default => $opt->default_value,
index e03e7dd..5bfc98e 100644 (file)
@@ -18,7 +18,7 @@ use BSE::Countries qw(bse_country_code);
 use BSE::Util::Secure qw(make_secret);
 use BSE::Template;
 
-our $VERSION = "1.051";
+our $VERSION = "1.052";
 
 =head1 NAME
 
@@ -1125,6 +1125,7 @@ sub req_payment {
     $order_values->{paidFor} = 0;
     
     my @items = $class->_build_items($req);
+    my @cartitems = $cart->items;
     @products = $cart->products;
     
     if ($session->{order_work}) {
@@ -1183,7 +1184,7 @@ sub req_payment {
        my @option_descs = $product->option_descs($cfg, $item->{options});
        my $display_order = 1;
        for my $option (@option_descs) {
-         BSE::TB::OrderItemOptions->make
+         my $optionitem = BSE::TB::OrderItemOptions->make
              (
               order_item_id => $dbitem->{id},
               original_id => $option->{id},
@@ -1192,9 +1193,18 @@ sub req_payment {
               display => $option->{display},
               display_order => $display_order++,
              );
+         BSE::PubSub->customize(
+           order_item_option =>
+             {
+               cartitem => $cartitems[$row_num],
+               cartoption => $option->{valueobj},
+               cart => $cart,
+               orderitem => $dbitem,
+               orderitemoption => $optionitem,
+             });
        }
       }
-      
+
       my $sub = $product->subscription;
       if ($sub) {
        $subscribing_to{$sub->{text_id}} = $sub;
@@ -1815,7 +1825,13 @@ sub _build_items {
        $work{product_discount} = 0;
        $work{product_discount_units} = 0;
       }
-      
+      BSE::PubSub->customize(
+       order_build_item => {
+         cartitem => $item,
+         cart => $cart,
+         orderitem => \%work,
+        });
+
       push @newcart, \%work;
     }
   }
index 1534935..6de8a66 100644 (file)
 <span id="prodoptvaluemove<:= loop.current.id :>"><:.call "make_arrows", down_url: downurl, up_url: upurl :></span>
 <:.end define :>
 
+<:.define addform_value_entry:>
+<div class="valueentry<:= index mod 2 == 0 ? "" : " odd":>">
+  <span><input type="text" name="value<:= index :>" value="<:= cgi.param("value" _ index) :>" maxlength="255" class="editor_field" title="Enter some values here" /></span>
+  <span><input type="radio" name="default" value="value<:= index :>"></span>
+  <span><:.call "error_img", field: "value" _ index:></span>
+</div>
+<:.end define:>
+<:.define addform_value_head:>
+<div class="valueentry"><span>Values</span><span>Default</span></div>
+<:.end define:>
+<:.define value_entry_menu :>
+<:.if request.user_can("bse_edit_prodopt_edit", article):>
+<div class="valueentrymenu">
+<a href="<:script:>?id=<:= product.id:>&amp;value_id=<:= dboptionvalue.id:>&amp;a_edit_option_value=1">Edit</a>
+<a href="<:script:>?id=<:= product.id:>&amp;value_id=<:= dboptionvalue.id:>&amp;a_confdel_option_value=1">Delete</a>
+<:.call "dboptionvalue_move":>
+<:.end if :>
+</div>
+<:.end define :>
+<:.define value_entry:>
+<div id="valentry<:= dboptionvalue.id:>" class="valueentry<:= loop.index mod 2 == 1 ? " odd" : "":>">
+<span id="prodoptvalue<:= dboptionvalue.id:>"><:= dboptionvalue.value:></span>
+<:.if dboptionvalue.id == dboption.default_value:>(default)<:.end if:>
+<:.call "value_entry_menu":>
+</div>
+<:.end define:>
+<:include admin/edit_prodopt_custom.tmpl optional:>
+
 <:.set dboptions = [ product.db_options ] :>
 <:.if dboptions.size:>
 <h2>Product options</h2>
 <div id="vallist<:= dboption.id:>" class="prodoptvalues">
 <:.for dboptionvalue in [ dboption.values ] :>
 <:.set dbovloop = loop :>
-<div id="valentry<:= dboptionvalue.id:>" class="valueentry<:= dbovloop.index mod 2 == 1 ? " odd" : "":>"><span id="prodoptvalue<:= dboptionvalue.id:>"><:= dboptionvalue.value:></span>
-<:.if dboptionvalue.id == dboption.default_value:>(default)<:.end if:>
-<:.if request.user_can("bse_edit_prodopt_edit", article):>
-<div class="valueentrymenu">
-<a href="<:script:>?id=<:= product.id:>&amp;value_id=<:= dboptionvalue.id:>&amp;a_edit_option_value=1">Edit</a>
-<a href="<:script:>?id=<:= product.id:>&amp;value_id=<:= dboptionvalue.id:>&amp;a_confdel_option_value=1">Delete</a>
-<:.call "dboptionvalue_move", loop:dbovloop:>
-</div>
-<:.end if :>
-</div>
+<:.call "value_entry", "loop":dbovloop:>
 <:.end for :>
 </div>
 <:.if request.user_can("bse_edit_prodopt_edit", article) :>
 </div>
 <:.end if:>
 <:.if request.user_can("bse_edit_prodopt_add", article) :>
-<:.define addform_value_entry:>
-<div class="valueentry<:= index mod 2 == 0 ? "" : " odd":>"><span><input type="text" name="value<:= index :>" value="<:= cgi.param("value" _ index) :>" maxlength="255" class="editor_field" title="Enter some values here" /></span><span><input type="radio" name="default" value="value<:= index :>"></span><span><:.call "error_img", field: "value" _ index:></span></div>
-<:.end define:>
-<:.define addform_value_head:>
-<div class="valueentry"><span>Values</span><span>Default</span></div>
-<:.end define:>
-<:include admin/edit_prodopt_custom.tmpl optional:>
 <div id="addoptionform" class="prodopt">
 <form action="<:script:>" method="post">
 <:csrfp admin_add_option hidden:>
index ed84d6a..ccd5121 100644 (file)
@@ -1,5 +1,23 @@
 <:wrap admin/base.tmpl title => "Edit Product Option", menuitem=>"none", showtitle=>"1", js => "admin_editprodopt.js" :>
 <:include admin/product_menu.tmpl:>
+<:.define value_head :>
+<tr>
+  <td colspan="2"></td>
+  <th>Default<:.call "error_img", field:"default_value":></th>
+</tr>
+<:.end define:>
+<:.define value_entry:>
+<tr>
+  <th>Value:</th>
+  <td><input type="text" name="value<:= value.id:>" value="<:= cgi.param("save_enabled") ? cgi.param("value" _ value.id) : value.value:>" /><:.call "error_img", field:"value" _ value.id:></td>
+  <td class="check"><input type="radio" name="default_value" value="<:= value.id:>" <:.if value.id == option.default_value:>checked="checked"<:.end if:> /></td>
+</tr>
+<:.end define:>
+<:-.define newvalue_entry -:>
+<div><label for="newvalue<:= index:>">Value:</label>
+<input type="text" name="newvalue<:= index:>" value="<:=cgi.param("newvalue" _ index) :>:>" /><:.call "error_img", field:"newvalue" _ index :></div>
+<:.end define-:>
+<:include admin/prodopt_edit_custom.tmpl optional:>
 <form action="<:script:>" method="post">
 <input type="hidden" name="id" value="<:=article.id:>" />
 <input type="hidden" name="option_id" value="<:=option.id:>" />
   <th>Values:</th>
 <td id="product_option_values">
 <table class="editform">
-<tr>
-  <td colspan="2"></td>
-  <th>Default<:.call "error_img", field:"default_value":></th>
-</tr>
+<:-.call "value_head" -:>
 <:.for value in [ option.values ] :>
-<tr>
-  <th>Value:</th>
-  <td><input type="text" name="value<:= value.id:>" value="<:= cgi.param("save_enabled") ? cgi.param("value" _ value.id) : value.value:>" /><:.call "error_img", field:"value" _ value.id:></td>
-  <td class="check"><input type="radio" name="default_value" value="<:= value.id:>" <:.if value.id == option.default_value:>checked="checked"<:.end if:> /></td>
-</tr>
+<:-.call "value_entry", "value": value -:>
 <:.end for:>
 </table>
 <:.if cgi.param("newvaluecount"):>
 <:.for i in [ 1 .. cgi.param("newvaluecount") ] :>
-<div><label for="newvalue<:= i:>">Value:</label>
-<input type="text" name="newvalue<:= i:>" value="<:=cgi.param("newvalue" _ i) :>:>" /><:.call "error_img", field:"newvalue" _ i :></div>
+<:.call "newvalue_entry", "index":i :>
 <:.end for :>
 <:.end if :>
 </td>
index b4a1553..28f64b1 100644 (file)
@@ -143,7 +143,7 @@ Unknown coupon code
       </td>
       <td><img src="/images/store/left_bottom_corner_line.gif" width="26" height="31"></td>
       <td align="center" bgcolor="#FFFFFF" height="100%" NOWRAP> <font size="3" face="Verdana, Arial, Helvetica, sans-serif"> 
-        <b>$<:money total:></b></font></td>
+        <b>$<:= bse.number("money", cart.total) :></b></font></td>
       <td><img src="/images/store/right_bottom_corner_line.gif" width="26" height="31"></td>
     </tr>
     <tr>