From f4516347d4bc5e14921e7c5e62d7143478c9b800 Mon Sep 17 00:00:00 2001
From: Chris Oden <wodenx@gmail.com>
Date: Tue, 24 Jan 2012 21:44:25 -0500
Subject: [PATCH 1/2] Issue #1380772: Update product display via ajax when add-to-cart form data changes (e.g. attributes).

---
 uc_attribute/uc_attribute.module     |  132 ++++++++++++++++++++++++++++------
 uc_order/uc_order.order_pane.inc     |    8 ++
 uc_product/uc_product.admin.inc      |    6 ++
 uc_product/uc_product.module         |   56 ++++++++++++++-
 uc_product_kit/uc_product_kit.module |   24 ++++++-
 5 files changed, 197 insertions(+), 29 deletions(-)

diff --git a/uc_attribute/uc_attribute.module b/uc_attribute/uc_attribute.module
index c2118b5..074b22f 100644
--- a/uc_attribute/uc_attribute.module
+++ b/uc_attribute/uc_attribute.module
@@ -291,7 +291,7 @@ function uc_attribute_form_uc_product_settings_form_alter(&$form, &$form_state)
       'adjustment' => t('Display price adjustment'),
       'total' => t('Display total price'),
     ),
-    '#description' => t('Determines how price variations are displayed to the customer. Prices may be displayed directly next to each attribute option in the attribute selection form either as a total price for the product with that option or as an adjustment (+ or -) showing how that option affects the product base price. However, the total price will not be displayed if a product has more than one attribute that can affect the price.'),
+    '#description' => t('Determines how price variations are displayed to the customer. Prices may be displayed directly next to each attribute option in the attribute selection form either as a total price for the product with that option or as an adjustment (+ or -) showing how that option affects the product base price. Note that the price will always be displayed as an adjustment for attributes that can have multiple options (using checkboxes).'),
   );
 }
 
@@ -318,7 +318,9 @@ function uc_attribute_module_implements_alter(&$implementations, $hook) {
  */
 function uc_attribute_uc_form_alter(&$form, &$form_state, $form_id) {
   if (strpos($form_id, 'add_to_cart_form') || $form_id == 'uc_order_add_product_form') {
+    $use_ajax = strpos($form_id, 'add_to_cart_form') && variable_get('uc_product_update_node_view', FALSE);
     $node =& $form['node']['#value'];
+    $id = $form_id . '-' . $node->nid . '-attributes';
     // If the node has a product list, add attributes to them.
     if (isset($form['products']) || isset($form['sub_products'])) {
       if (isset($form['products'])) {
@@ -328,7 +330,8 @@ function uc_attribute_uc_form_alter(&$form, &$form_state, $form_id) {
         $element = &$form['sub_products'];
       }
       foreach (element_children($element) as $key) {
-        $element[$key]['attributes'] = _uc_attribute_alter_form(node_load($key));
+        $element[$key]['attributes'] = _uc_attribute_alter_form($id . '-' . $key, $node->products[$key], $use_ajax);
+
         if (is_array($element[$key]['attributes'])) {
           $element[$key]['attributes']['#tree'] = TRUE;
           $element[$key]['#type'] = 'fieldset';
@@ -337,7 +340,7 @@ function uc_attribute_uc_form_alter(&$form, &$form_state, $form_id) {
     }
     // If not, add attributes to the node.
     else {
-      $form['attributes'] = _uc_attribute_alter_form($node);
+      $form['attributes'] = _uc_attribute_alter_form($id, $node, $use_ajax);
 
       if (is_array($form['attributes'])) {
         $form['attributes']['#tree'] = TRUE;
@@ -1228,30 +1231,82 @@ function _uc_cart_product_get_options($item) {
 }
 
 /**
+ * Ajax callback for attribute selection form elements.
+ */
+function uc_attribute_option_ajax($form, $form_state) {
+  $parents = $form_state['triggering_element']['#array_parents'];
+  $wrapper = '#' . $form_state['triggering_element']['#ajax']['wrapper'];
+  while ($key = array_pop($parents)) {
+    if ($key == 'attributes') {
+      array_push($parents, $key);
+      $element = drupal_array_get_nested_value($form, $parents);
+      $commands[] = ajax_command_replace($wrapper, drupal_render($element));
+      break;
+    }
+  }
+  if (strpos($form['#form_id'], 'add_to_cart_form') !== FALSE) {
+    $commands = array_merge($commands, uc_product_view_ajax_commands($form_state, array('display_price', 'weight', 'cost')));
+  }
+  $commands[] = ajax_command_prepend($wrapper, theme('status_messages'));
+  return array('#type' => 'ajax', '#commands' => $commands);
+}
+
+/**
  * Helper function for uc_attribute_form_alter().
+ * @param $id
+ *   The unique id to use to wrap these form elements.
+ * @param &$product
+ *   The product node for which the attribute form elements are to be attached.
+ * @param $use_ajax
+ *   TRUE to add ajax to the form.  Note that ajax may be added even if this is FALSE,
+ *   if there are multiple attributes and one or more of them is set to display total price.
  *
  * @see theme_uc_attribute_add_to_cart()
+ * @see uc_attribute_option_ajax()
  */
-function _uc_attribute_alter_form($product) {
-  // Prevent infinite recursion because we call node_view() below to get the
-  // display prices.
-  $skip_flag = array('display_only' => TRUE);
-
+function _uc_attribute_alter_form($id, &$product, $use_ajax) {
   // If the product doesn't have attributes, return the form as it is.
-  if (!isset($product->attributes)) {
-    $product->attributes = uc_product_get_attributes($product->nid);
-  }
-  if (!is_array($product->attributes) || empty($product->attributes)) {
+  if (empty($product->attributes) || !is_array($product->attributes)) {
     return NULL;
   }
 
   $nid = $product->nid;
-
-  // Load all product attributes for the given nid.
-  $priced_attributes = uc_attribute_priced_attributes($nid);
   $attributes = $product->attributes;
+  $priced_attributes = uc_attribute_priced_attributes($nid);
+
+  // If the form is being built for the first time, populate attributes with their default values.
+  if (!isset($product->data['attributes'])) {
+    $values = array();
+    foreach($priced_attributes as $aid) {
+      if (!$attributes[$aid]->required && ($attributes[$aid]->display == 1 || $attributes[$aid]->display == 2)) {
+        $values[$aid] = $attributes[$aid]->default_option;
+      }
+    }
+    if (!empty($values)) {
+      $data = $product->data;
+      $data['attributes'] = $values;
+      if (isset($product->qty)) {
+        // Preserve the quantity (for product-kit sub-products).
+        $qty = $product->qty;
+      }
+      $product = uc_product_load_variant($product->nid, $data);
+      if (isset($qty)) {
+        $product->qty = $qty;
+      }
+    }
+  }
+
+  if (empty($product->data) || !is_array($product->data)) {
+    $product->data = array();
+  }
 
-  $form_attributes = array();
+  // Initialize the form element.
+  $form_attributes = array(
+    '#theme' => 'uc_attribute_add_to_cart',
+    '#id' => $id,
+  );
+
+  $price_format = variable_get('uc_attribute_option_price_format', 'adjustment');
 
   // Loop through each product attribute and generate its form element.
   foreach ($attributes as $attribute) {
@@ -1260,16 +1315,42 @@ function _uc_attribute_alter_form($product) {
     foreach ($attribute->options as $option) {
       $display_price = '';
       if (in_array($attribute->aid, $priced_attributes)) {
-        $variant = node_view(uc_product_load_variant($product->nid, $skip_flag + array('attributes' => array($attribute->aid => $option->oid))), 'teaser');
-        switch (variable_get('uc_attribute_option_price_format', 'adjustment')) {
+        $data = array('display_only' => TRUE) + $product->data;
+        if (empty($data['attributes'])) {
+          $data['attributes'] = array();
+        }
+        switch ($price_format) {
           case 'total':
-            if (count($priced_attributes) == 1 && $attribute->display != 3) {
+            // Only display total price for non-checkbox options.
+            // !TODO Fix attribute option total price display for product kits.
+            if ($attribute->display != 3 && !isset($product->data['kit_id'])) {
+              $use_ajax = $use_ajax || (count($priced_attributes) > 1);
+              $data['attributes'] = array($attribute->aid => $option->oid) + $data['attributes'];
+              $variant = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
               $display_price = uc_currency_format($variant['display_price']['#value']);
               break;
             }
           case 'adjustment':
-            $base = node_view(uc_product_load_variant($product->nid, $skip_flag), 'teaser');
-            $adjustment = $variant['display_price']['#value'] - $base['display_price']['#value'];
+            if ($attribute->display == 3 || !$use_ajax) {
+              // For checkboxes, or if the node totals are not being updated,
+              // we compare this attribute against base price.
+              if (empty($base)) { // only build the base once.
+                unset($data['attributes']);
+                $base = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
+              }
+              $data['attributes'] = array($attribute->aid => $option->oid);
+              $variant = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
+              $adjustment = $variant['display_price']['#value'] - $base['display_price']['#value'];
+            }
+            else {
+              // Otherwise we compare against current total price.
+              if (empty($selected)) {
+                $selected = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
+              }
+              $data['attributes'] = array($attribute->aid => $option->oid) + $data['attributes'];
+              $variant = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
+              $adjustment = $variant['display_price']['#value'] - $selected['display_price']['#value'];
+            }
             if ($adjustment != 0) {
               $display_price = $adjustment > 0 ? '+' : '-';
               $display_price .= uc_currency_format(abs($adjustment));
@@ -1310,6 +1391,12 @@ function _uc_attribute_alter_form($product) {
         '#default_value' => $attribute->default_option,
         '#options' => $options,
       );
+      if ($use_ajax) {
+        $form_attributes[$attribute->aid]['#ajax'] = array(
+          'callback' => 'uc_attribute_option_ajax',
+          'wrapper' => $id,
+        );
+      }
     }
     elseif ($attribute->display > 0) {
       $form_attributes[$attribute->aid] = array(
@@ -1334,9 +1421,8 @@ function _uc_attribute_alter_form($product) {
 
     $form_attributes[$attribute->aid]['#description'] = filter_xss($attribute->description);
     $form_attributes[$attribute->aid]['#required'] = $attribute->required;
-
-    $form_attributes['#theme'] = 'uc_attribute_add_to_cart';
   }
+
   return $form_attributes;
 }
 
diff --git a/uc_order/uc_order.order_pane.inc b/uc_order/uc_order.order_pane.inc
index 65fb194..04f7d3e 100644
--- a/uc_order/uc_order.order_pane.inc
+++ b/uc_order/uc_order.order_pane.inc
@@ -316,6 +316,14 @@ function uc_order_product_select_form_validate($form, &$form_state) {
  * @ingroup forms
  */
 function uc_order_add_product_form($form, &$form_state, $order, $node) {
+  $data = array();
+  if (isset($form_state['values']['product_controls']['qty'])) {
+    $data += module_invoke_all('uc_add_to_cart_data', $form_state['values']['product_controls']);
+  }
+  if (!empty($node->data) && is_array($node->data)) {
+    $data += $node->data;
+  }
+  $node = uc_product_load_variant(intval($form_state['values']['product_controls']['nid']), $data);
   $form['title'] = array(
     '#markup' => '<h3>' . check_plain($node->title) . '</h3>',
   );
diff --git a/uc_product/uc_product.admin.inc b/uc_product/uc_product.admin.inc
index 3386e7e..cce8dbd 100644
--- a/uc_product/uc_product.admin.inc
+++ b/uc_product/uc_product.admin.inc
@@ -84,6 +84,12 @@ function uc_product_settings_form($form, &$form_state) {
       '#title' => t('Display an optional quantity field in the <em>Add to Cart</em> form.'),
       '#default_value' => variable_get('uc_product_add_to_cart_qty', FALSE),
     );
+    $form['product']['uc_product_update_node_view'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Update product display based on customer selections'),
+      '#default_value' => variable_get('uc_product_update_node_view', FALSE),
+			'#description' => t('Check this box to dynamically update the display of product information such as display-price or weight based on customer input on the add-to-cart form (e.g. selecting a particular attribute option).'),
+    );
   }
 
   foreach (module_invoke_all('uc_product_feature') as $feature) {
diff --git a/uc_product/uc_product.module b/uc_product/uc_product.module
index 946f307..30656f9 100644
--- a/uc_product/uc_product.module
+++ b/uc_product/uc_product.module
@@ -709,6 +709,40 @@ function uc_product_delete(&$node) {
 }
 
 /**
+ * Returns a list of ajax commands which can be used to replace portions of a product view
+ * based on user input to the add-to-cart form.
+ *
+ * If a module adds an input field to the add-to-cart form which affects some aspect of a product
+ * (e.g. display price or weight), it should attach an #ajax callback to that form element, and use
+ * this function in the callback to build updated content for the affected fields based on user input.
+ *
+ * @param $form_state
+ *   The current form state.  This must contain a 'variant' entry in the 'storage' array which represents
+ *   the product as configured by user input data. In most cases, this is provided automatically by
+ *   uc_product_add_to_cart_form_validate().
+ *
+ * @param $keys
+ *   An array of keys in the built product content which should be replaced (e.g. 'display_price').
+ *
+ * @return
+ *   An array of ajax commands to replace each of the display fields with it's updated value.
+ */
+function uc_product_view_ajax_commands($form_state, $keys) {
+  $commands = array();
+  if (variable_get('uc_product_update_node_view', FALSE) && !empty($form_state['storage']['variant'])) {
+    $node_div = '#node-' . $form_state['storage']['variant']->nid;
+    $build = node_view($form_state['storage']['variant']);
+    foreach ($keys as $key) {
+      if (isset($build[$key])) {
+        $id = $node_div . ' .' . str_replace('_', '-', $key);
+        $commands[] = ajax_command_replace($id, drupal_render($build[$key]));
+      }
+    }
+  }
+  return $commands;
+}
+
+/**
  * Implements hook_view().
  */
 function uc_product_view($node, $view_mode) {
@@ -717,6 +751,15 @@ function uc_product_view($node, $view_mode) {
   // be sure not to alter twice -- cf. entity_prepare_view().
   $variant = empty($node->variant) ? _uc_product_get_variant($node) : $node;
 
+  // Build the add_to_cart form, and use the updated variant based on data provided by the form
+  // (e.g. attribute default options).
+  if (module_exists('uc_cart') && empty($variant->data['display_only'])) {
+    $add_to_cart_form = drupal_get_form('uc_product_add_to_cart_form_' . $variant->nid, $variant);
+    if (variable_get('uc_product_update_node_view', FALSE)) {
+      $variant = $add_to_cart_form['node']['#value'];
+    }
+  }
+
   $node->content['display_price'] = array(
     '#theme' => 'uc_product_price',
     '#value' => $variant->price,
@@ -778,11 +821,11 @@ function uc_product_view($node, $view_mode) {
     '#view_mode' => $view_mode,
   );
 
-  if (module_exists('uc_cart') && isset($variant->nid) && empty($variant->data['display_only'])) {
+  if (isset($add_to_cart_form)) {
     $node->content['add_to_cart'] = array(
       '#theme' => 'uc_product_add_to_cart',
       '#view_mode' => $view_mode,
-      '#form' => drupal_get_form('uc_product_add_to_cart_form_' . $variant->nid, $variant),
+      '#form' => $add_to_cart_form,
     );
   }
 
@@ -1205,7 +1248,7 @@ function uc_product_add_to_cart_form($form, &$form_state, $node) {
 
   $form['node'] = array(
     '#type' => 'value',
-    '#value' => $node,
+    '#value' => isset($form_state['storage']['variant']) ? $form_state['storage']['variant'] : $node,
   );
 
   uc_form_alter($form, $form_state, __FUNCTION__);
@@ -1214,6 +1257,13 @@ function uc_product_add_to_cart_form($form, &$form_state, $node) {
 }
 
 /**
+ * Form validation handler for uc_product_add_to_cart_form().
+ */
+function uc_product_add_to_cart_form_validate($form, &$form_state) {
+  $form_state['storage']['variant'] = uc_product_load_variant($form_state['values']['nid'], module_invoke_all('uc_add_to_cart_data', $form_state['values']));
+}
+
+/**
  * Form submission handler for uc_product_add_to_cart_form().
  *
  * @see uc_product_add_to_cart_form()
diff --git a/uc_product_kit/uc_product_kit.module b/uc_product_kit/uc_product_kit.module
index c3ab6ec..63831f7 100644
--- a/uc_product_kit/uc_product_kit.module
+++ b/uc_product_kit/uc_product_kit.module
@@ -642,6 +642,13 @@ function uc_product_kit_view($node, $view_mode) {
   // flag to be sure not to alter twice.
   $variant = empty($node->variant) ? uc_product_load_variant($node->nid) : $node;
 
+  if (module_exists('uc_cart') && empty($variant->data['display_only'])) {
+    $add_to_cart_form = drupal_get_form('uc_product_kit_add_to_cart_form_' . $variant->nid, clone $variant);
+    if (variable_get('uc_product_update_node_view', FALSE)) {
+      $variant = $add_to_cart_form['node']['#value'];
+    }
+  }
+
   // Calculate the display price.
   $display_price = 0;
   $suffixes = array();
@@ -729,11 +736,11 @@ function uc_product_kit_view($node, $view_mode) {
     }
   }
 
-  if (module_exists('uc_cart') && isset($variant->nid) && empty($variant->data['display_only'])) {
+  if (isset($add_to_cart_form)) {
     $node->content['add_to_cart'] = array(
       '#theme' => 'uc_product_kit_add_to_cart',
       '#view_mode' => $view_mode,
-      '#form' => drupal_get_form('uc_product_kit_add_to_cart_form_' . $variant->nid, $variant),
+      '#form' => $add_to_cart_form,
     );
   }
 
@@ -782,7 +789,7 @@ function uc_product_kit_add_to_cart_form($form, &$form_state, $node) {
 
   $form['node'] = array(
     '#type' => 'value',
-    '#value' => $node,
+    '#value' => isset($form_state['storage']['variant']) ? $form_state['storage']['variant'] : $node,
   );
 
   uc_form_alter($form, $form_state, __FUNCTION__);
@@ -790,6 +797,17 @@ function uc_product_kit_add_to_cart_form($form, &$form_state, $node) {
   return $form;
 }
 
+function uc_product_kit_add_to_cart_form_validate($form, &$form_state) {
+  uc_product_add_to_cart_form_validate($form, $form_state);
+  foreach ($form_state['storage']['variant']->products as &$product) {
+    $data = module_invoke_all('uc_add_to_cart_data', $form_state['values']['products'][$product->nid]);
+    $data += $product->data;
+    $qty = $product->qty;
+    $product = uc_product_load_variant($product->nid, $data);
+    $product->qty = $qty;
+  }
+}
+
 /**
  * Adds each product kit's component to the cart in the correct quantities.
  *
-- 
1.7.4.1


From dcd226e7977146f357d806124ce5e89054a35ec8 Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Tue, 7 Aug 2012 00:40:18 +0100
Subject: [PATCH 2/2] Followup to issue #1380772: Coding standards and documentation fixes.

---
 uc_attribute/uc_attribute.module     |    8 +++++---
 uc_product/uc_product.module         |   25 +++++++++++++------------
 uc_product_kit/uc_product_kit.module |    7 +++++++
 3 files changed, 25 insertions(+), 15 deletions(-)

diff --git a/uc_attribute/uc_attribute.module b/uc_attribute/uc_attribute.module
index 074b22f..9a01bb5 100644
--- a/uc_attribute/uc_attribute.module
+++ b/uc_attribute/uc_attribute.module
@@ -1253,13 +1253,15 @@ function uc_attribute_option_ajax($form, $form_state) {
 
 /**
  * Helper function for uc_attribute_form_alter().
+ *
  * @param $id
  *   The unique id to use to wrap these form elements.
  * @param &$product
  *   The product node for which the attribute form elements are to be attached.
  * @param $use_ajax
- *   TRUE to add ajax to the form.  Note that ajax may be added even if this is FALSE,
- *   if there are multiple attributes and one or more of them is set to display total price.
+ *   TRUE to add ajax to the form. Note that ajax may be added even if this is
+ *   FALSE, if there are multiple attributes and one or more of them is set to
+ *   display total price.
  *
  * @see theme_uc_attribute_add_to_cart()
  * @see uc_attribute_option_ajax()
@@ -1277,7 +1279,7 @@ function _uc_attribute_alter_form($id, &$product, $use_ajax) {
   // If the form is being built for the first time, populate attributes with their default values.
   if (!isset($product->data['attributes'])) {
     $values = array();
-    foreach($priced_attributes as $aid) {
+    foreach ($priced_attributes as $aid) {
       if (!$attributes[$aid]->required && ($attributes[$aid]->display == 1 || $attributes[$aid]->display == 2)) {
         $values[$aid] = $attributes[$aid]->default_option;
       }
diff --git a/uc_product/uc_product.module b/uc_product/uc_product.module
index 30656f9..424d549 100644
--- a/uc_product/uc_product.module
+++ b/uc_product/uc_product.module
@@ -709,23 +709,24 @@ function uc_product_delete(&$node) {
 }
 
 /**
- * Returns a list of ajax commands which can be used to replace portions of a product view
- * based on user input to the add-to-cart form.
+ * Dynamically replaces parts of a product view based on form input.
  *
- * If a module adds an input field to the add-to-cart form which affects some aspect of a product
- * (e.g. display price or weight), it should attach an #ajax callback to that form element, and use
- * this function in the callback to build updated content for the affected fields based on user input.
+ * If a module adds an input field to the add-to-cart form which affects some
+ * aspect of a product (e.g. display price or weight), it should attach an
+ * #ajax callback to that form element, and use this function in the callback
+ * to build updated content for the affected fields.
  *
  * @param $form_state
- *   The current form state.  This must contain a 'variant' entry in the 'storage' array which represents
- *   the product as configured by user input data. In most cases, this is provided automatically by
+ *   The current form state.  This must contain a 'variant' entry in the
+ *   'storage' array which represents the product as configured by user input
+ *   data. In most cases, this is provided automatically by
  *   uc_product_add_to_cart_form_validate().
- *
  * @param $keys
- *   An array of keys in the built product content which should be replaced (e.g. 'display_price').
+ *   An array of keys in the built product content which should be replaced
+ *   (e.g. 'display_price').
  *
  * @return
- *   An array of ajax commands to replace each of the display fields with it's updated value.
+ *   An array of Ajax commands.
  */
 function uc_product_view_ajax_commands($form_state, $keys) {
   $commands = array();
@@ -751,8 +752,8 @@ function uc_product_view($node, $view_mode) {
   // be sure not to alter twice -- cf. entity_prepare_view().
   $variant = empty($node->variant) ? _uc_product_get_variant($node) : $node;
 
-  // Build the add_to_cart form, and use the updated variant based on data provided by the form
-  // (e.g. attribute default options).
+  // Build the 'add to cart' form, and use the updated variant based on data
+  // provided by the form (e.g. attribute default options).
   if (module_exists('uc_cart') && empty($variant->data['display_only'])) {
     $add_to_cart_form = drupal_get_form('uc_product_add_to_cart_form_' . $variant->nid, $variant);
     if (variable_get('uc_product_update_node_view', FALSE)) {
diff --git a/uc_product_kit/uc_product_kit.module b/uc_product_kit/uc_product_kit.module
index 63831f7..22a6083 100644
--- a/uc_product_kit/uc_product_kit.module
+++ b/uc_product_kit/uc_product_kit.module
@@ -755,6 +755,7 @@ function uc_product_kit_view($node, $view_mode) {
  * uc_attribute_form_alter() hooks into this form to add attributes to each
  * element in $form['products'].
  *
+ * @see uc_product_kit_add_to_cart_form_validate()
  * @see uc_product_kit_add_to_cart_form_submit()
  * @ingroup forms
  */
@@ -797,6 +798,12 @@ function uc_product_kit_add_to_cart_form($form, &$form_state, $node) {
   return $form;
 }
 
+/**
+ * Form validation handler for uc_product_add_to_cart_form().
+ *
+ * @see uc_product_kit_add_to_cart_form()
+ * @see uc_product_add_to_cart_form_validate()
+ */
 function uc_product_kit_add_to_cart_form_validate($form, &$form_state) {
   uc_product_add_to_cart_form_validate($form, $form_state);
   foreach ($form_state['storage']['variant']->products as &$product) {
-- 
1.7.4.1

