Shoptimally - Version 1.1.03

Version Notes

Included features:
1. Core
2. Analytics
3. Catalog Sync
4. Featured Items
5. Featured Items Ajax
6. Upsale Coupons
7. Related Products
8. Interesting Products Sync
9. Htmls update
10. Features analytics

Download this release

Release Info

Developer Ronen Ness
Extension Shoptimally
Version 1.1.03
Comparing to
See all releases


Version 1.1.03

Files changed (80) hide show
  1. app/code/community/Shoptimally/Analytics/Helper/FeatureEvents.php +68 -0
  2. app/code/community/Shoptimally/Analytics/Helper/UserEvents.php +264 -0
  3. app/code/community/Shoptimally/Analytics/Helper/Utils.php +63 -0
  4. app/code/community/Shoptimally/Analytics/Model/Observer.php +106 -0
  5. app/code/community/Shoptimally/Analytics/etc/config.xml +52 -0
  6. app/code/community/Shoptimally/Analytics/readme.md +4 -0
  7. app/code/community/Shoptimally/CatalogSync/Block/ProductsRenderer.php +95 -0
  8. app/code/community/Shoptimally/CatalogSync/Helper/InterestingList.php +204 -0
  9. app/code/community/Shoptimally/CatalogSync/Helper/TimeBased.php +152 -0
  10. app/code/community/Shoptimally/CatalogSync/Helper/UpdateItemsHtmls.php +62 -0
  11. app/code/community/Shoptimally/CatalogSync/Helper/Utils.php +109 -0
  12. app/code/community/Shoptimally/CatalogSync/Model/Cron.php +95 -0
  13. app/code/community/Shoptimally/CatalogSync/Model/Observer.php +91 -0
  14. app/code/community/Shoptimally/CatalogSync/etc/config.xml +90 -0
  15. app/code/community/Shoptimally/CatalogSync/readme.md +2 -0
  16. app/code/community/Shoptimally/Core/Block/Injectjs.php +113 -0
  17. app/code/community/Shoptimally/Core/Helper/BlockUtils.php +34 -0
  18. app/code/community/Shoptimally/Core/Helper/ClientData.php +86 -0
  19. app/code/community/Shoptimally/Core/Helper/Config.php +196 -0
  20. app/code/community/Shoptimally/Core/Helper/Cookie.php +65 -0
  21. app/code/community/Shoptimally/Core/Helper/Data.php +14 -0
  22. app/code/community/Shoptimally/Core/Helper/FeatureBase.php +290 -0
  23. app/code/community/Shoptimally/Core/Helper/HandleFatals.php +29 -0
  24. app/code/community/Shoptimally/Core/Helper/Log.php +202 -0
  25. app/code/community/Shoptimally/Core/Helper/ObjectUtils.php +49 -0
  26. app/code/community/Shoptimally/Core/Helper/PageInfo.php +157 -0
  27. app/code/community/Shoptimally/Core/Helper/ProductsUtils.php +351 -0
  28. app/code/community/Shoptimally/Core/Helper/RemoteConfig.php +195 -0
  29. app/code/community/Shoptimally/Core/Helper/Server.php +175 -0
  30. app/code/community/Shoptimally/Core/Helper/Storage.php +233 -0
  31. app/code/community/Shoptimally/Core/Helper/UrlUtils.php +112 -0
  32. app/code/community/Shoptimally/Core/Model/Cron.php +41 -0
  33. app/code/community/Shoptimally/Core/Model/Observer.php +65 -0
  34. app/code/community/Shoptimally/Core/controllers/DebugDataController.php +381 -0
  35. app/code/community/Shoptimally/Core/etc/adminhtml.xml +26 -0
  36. app/code/community/Shoptimally/Core/etc/config.xml +89 -0
  37. app/code/community/Shoptimally/Core/etc/system.xml +57 -0
  38. app/code/community/Shoptimally/Core/readme.md +7 -0
  39. app/code/community/Shoptimally/FeaturedItems/Helper/Data.php +6 -0
  40. app/code/community/Shoptimally/FeaturedItems/Helper/Main.php +252 -0
  41. app/code/community/Shoptimally/FeaturedItems/Model/Observer.php +36 -0
  42. app/code/community/Shoptimally/FeaturedItems/etc/config.xml +58 -0
  43. app/code/community/Shoptimally/FeaturedItems/readme.md +5 -0
  44. app/code/community/Shoptimally/FeaturedItemsAjax/Helper/Data.php +6 -0
  45. app/code/community/Shoptimally/FeaturedItemsAjax/controllers/AjaxController.php +35 -0
  46. app/code/community/Shoptimally/FeaturedItemsAjax/etc/config.xml +45 -0
  47. app/code/community/Shoptimally/FeaturedItemsAjax/readme.md +7 -0
  48. app/code/community/Shoptimally/FullSort/Block/ProductPlaceholders.php +63 -0
  49. app/code/community/Shoptimally/FullSort/Helper/Data.php +6 -0
  50. app/code/community/Shoptimally/FullSort/controllers/AjaxController.php +35 -0
  51. app/code/community/Shoptimally/FullSort/etc/config.xml +51 -0
  52. app/code/community/Shoptimally/FullSort/readme.md +8 -0
  53. app/code/community/Shoptimally/RelatedProducts/Helper/Data.php +6 -0
  54. app/code/community/Shoptimally/RelatedProducts/Helper/Main.php +126 -0
  55. app/code/community/Shoptimally/RelatedProducts/Model/Observer.php +61 -0
  56. app/code/community/Shoptimally/RelatedProducts/etc/config.xml +69 -0
  57. app/code/community/Shoptimally/RelatedProducts/readme.md +4 -0
  58. app/code/community/Shoptimally/UpsaleCoupons/Block/Coupons.php +49 -0
  59. app/code/community/Shoptimally/UpsaleCoupons/Helper/Data.php +6 -0
  60. app/code/community/Shoptimally/UpsaleCoupons/Helper/Main.php +140 -0
  61. app/code/community/Shoptimally/UpsaleCoupons/etc/config.xml +41 -0
  62. app/code/community/Shoptimally/UpsaleCoupons/readme.md +4 -0
  63. app/design/frontend/base/default/layout/shoptimally/core.xml +20 -0
  64. app/design/frontend/base/default/layout/shoptimally/full_sort.xml +8 -0
  65. app/design/frontend/base/default/layout/shoptimally/upsale_coupons.xml +13 -0
  66. app/design/frontend/base/default/template/shoptimally/fullsort_placeholders.phtml +3 -0
  67. app/design/frontend/base/default/template/shoptimally/injectjs.phtml +18 -0
  68. app/design/frontend/base/default/template/shoptimally/injectjs_extras.phtml +6 -0
  69. app/design/frontend/base/default/template/shoptimally/injectjs_extras_buttom.phtml +6 -0
  70. app/design/frontend/base/default/template/shoptimally/upsale_coupons.phtml +6 -0
  71. app/design/frontend/base/default/template/shoptimally/upsale_coupons_header.phtml +3 -0
  72. app/etc/modules/Shoptimally_Analytics.xml +10 -0
  73. app/etc/modules/Shoptimally_CatalogSync.xml +10 -0
  74. app/etc/modules/Shoptimally_Core.xml +10 -0
  75. app/etc/modules/Shoptimally_FeaturedItems.xml +10 -0
  76. app/etc/modules/Shoptimally_FeaturedItemsAjax.xml +10 -0
  77. app/etc/modules/Shoptimally_FullSort.xml +10 -0
  78. app/etc/modules/Shoptimally_RelatedProducts.xml +10 -0
  79. app/etc/modules/Shoptimally_UpsaleCoupons.xml +10 -0
  80. package.xml +30 -0
app/code/community/Shoptimally/Analytics/Helper/FeatureEvents.php ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Analytics
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Feature analytics related events - this is when feature successfully runs, rejected, failed etc.
11
+ */
12
+ class Shoptimally_Analytics_Helper_FeatureEvents extends Mage_Core_Helper_Abstract
13
+ {
14
+ // analytic utils
15
+ protected $_utils = null;
16
+
17
+ // possible features to report on
18
+ const FEATURE_FEATURED_ITEMS = "FeaturedItems";
19
+ const FEATURE_UPSALE_COUPON = "UpsaleCoupons";
20
+ const FEATURE_RELATED_PRODUCTS = "RelatedProducts";
21
+
22
+ // possible statuses we can report
23
+ const STATUS_OK = "ok"; // everything went ok.
24
+ const STATUS_REJECTED = "rejected"; // client chose to reject the results from server and will not show them.
25
+ const STATUS_TIMEOUT = "timeout"; // timeout occured.
26
+ const STATUS_ERROR = "error"; // error occured.
27
+
28
+ /**
29
+ * load the currently existing events in queue
30
+ */
31
+ public function __construct()
32
+ {
33
+ // load the analytic utils
34
+ $this->_utils = Mage::helper('shoptimally_analytics/utils');
35
+ }
36
+
37
+ /*
38
+ * send feature-related analytics.
39
+ * note: this does not send immediately to server, it write it into a cookie and our client-side javascript
40
+ * will send it to shoptimally server in an async way.
41
+ *
42
+ * these reports are crutial to keep track on features performance and make sure they are efficient and
43
+ * do a good job in increasing conversion.
44
+ *
45
+ * @param $featureName - feature name, case sensitive. see FEATURE_XXX for options.
46
+ * @param $status - feature status, see STATUS_XXX for options.
47
+ * @extra - optional, any extra data we want to add (dictionary).
48
+ * */
49
+ public function report($featureName, $status, $extra=array()) {
50
+
51
+ try
52
+ {
53
+ // set data to send to event
54
+ $data = array(
55
+ "feature_name" => $featureName,
56
+ "code" => $status,
57
+ "extra" => $extra
58
+ );
59
+
60
+ // send the event
61
+ $this->_utils->addEvent("feature_analytics", $data);
62
+ }
63
+ catch (Exception $e)
64
+ {
65
+ Mage::helper('shoptimally_core/log')->warn("Failed to send feature analytics!", $e);
66
+ }
67
+ }
68
+ }
app/code/community/Shoptimally/Analytics/Helper/UserEvents.php ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Analytics
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * User-events related analytics. This handle things like add-to-cart, remove-from-cart, checkout-complete.
11
+ */
12
+ class Shoptimally_Analytics_Helper_UserEvents extends Mage_Core_Helper_Abstract
13
+ {
14
+ // all event types
15
+ const EVENT_ADD_TO_CART = "add_to_cart";
16
+ const EVENT_REMOVE_FROM_CART = "remove_from_cart";
17
+ const EVENT_CHECKOUT_COMPLETE = "checkout_complete";
18
+
19
+ // current events queue
20
+ protected $_utils = null;
21
+
22
+ /**
23
+ * load the currently existing events in queue
24
+ */
25
+ public function __construct()
26
+ {
27
+ // load the analytic utils
28
+ $this->_utils = Mage::helper('shoptimally_analytics/utils');
29
+ }
30
+
31
+ /**
32
+ * report checkout event
33
+ * */
34
+ public function reportCheckout($cart)
35
+ {
36
+ $this->addUserEvent(self::EVENT_CHECKOUT_COMPLETE, $cart);
37
+ }
38
+
39
+ /**
40
+ * get unique id from quota item
41
+ * @return item id
42
+ */
43
+ public function getIdFromItem($item)
44
+ {
45
+ return $item->getProduct()->getId();
46
+ }
47
+
48
+ /**
49
+ * get quantity from quota item
50
+ * @return quantity
51
+ */
52
+ public function getItemQuantity($item)
53
+ {
54
+ return $item->getData()['qty'];
55
+ }
56
+
57
+ /*
58
+ * return parent product id from quote item
59
+ * */
60
+ private function getItemParentProductId($item)
61
+ {
62
+ if ($item->getParentItem())
63
+ {
64
+ return $item->getParentItem()->getProduct()->getId();
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * get all cart items from current cart and convert to our desired format of [{unique_id, quantity}, ]
71
+ * @param $cartItems - all the items in cart you want to process. if null (default) will take from current checkout cart.
72
+ * @return list of items from cart. every item in list is a dictionary with 'unique_id' and 'quantity'.
73
+ */
74
+ public function getCartItemsConverted($cartItems=null)
75
+ {
76
+ // if not provided, get cart items from cart
77
+ if (is_null($cartItems))
78
+ {
79
+ $cartItems = Mage::getModel('checkout/cart')->getQuote()->getAllItems();
80
+ }
81
+
82
+ // convert to our format
83
+ $cartData = array();
84
+ foreach ($cartItems as $item) {
85
+
86
+ // get id and quantity
87
+ $id = $this->getIdFromItem($item);
88
+ $quantity = $this->getItemQuantity($item);
89
+ $parentId = $this->getItemParentProductId($item);
90
+ $parentQuoteId = $item->getParentItemId();
91
+
92
+ // get current item data
93
+ $newItem = array (
94
+ 'unique_id' => $id,
95
+ 'quantity' => $quantity,
96
+ 'parent_id' => $parentId,
97
+ 'parent_quote_id' => $parentQuoteId,
98
+ 'quote_id' => $item->getId(),
99
+ 'unit_price' => $item->getProduct()->getFinalPrice(),
100
+ );
101
+
102
+ // iterate over cart data we already have, and if this item already exist add to its quantity.
103
+ // you might wonder how this might happen? answer is this:
104
+ // 1. user add item, lets say a baby diaper.
105
+ // 2. user tries to add a bundle of baby diaper + toy.
106
+ // 3. however, in diaper + toy the toy is out of stock, so it only adds the diaper.
107
+ // 4. because its not really bundle, the parent id is null. however, magento still identify
108
+ // the two items as different items and store their quantity separately.
109
+ $wasAddedToExisting = false;
110
+ foreach ($cartData as $index => $prevItem)
111
+ {
112
+ if ($this->isSameCartItem($newItem, $prevItem))
113
+ {
114
+ $prevItem["quantity"] += $newItem["quantity"];
115
+ $cartData[$index] = $prevItem;
116
+ $wasAddedToExisting = true;
117
+ break;
118
+ }
119
+ }
120
+
121
+ // add current data to cart data
122
+ if (!$wasAddedToExisting)
123
+ {
124
+ array_push($cartData, $newItem);
125
+ }
126
+ }
127
+
128
+ // return result
129
+ return $cartData;
130
+ }
131
+
132
+ /**
133
+ * save current cart to cookie, so the js client will be able to access it and we can get it later.
134
+ * @param $cart - current cart data
135
+ */
136
+ public function writeCartToCookie($cart)
137
+ {
138
+ // get cookie utils
139
+ $cookies = Mage::helper('shoptimally_core/cookie');
140
+ $cookies->setCookie("shoptimally_curr_cart", $cart, true);
141
+ }
142
+
143
+ /**
144
+ * return cart from stored cookie
145
+ */
146
+ public function getCartFromCookie()
147
+ {
148
+ // get previous cart data
149
+ $cookies = Mage::helper('shoptimally_core/cookie');
150
+ return $cookies->getCookie("shoptimally_curr_cart", true, array());
151
+ }
152
+
153
+ /**
154
+ * This function gets two entries in the cart struct and return true if its the same item.
155
+ * see "getCartItemsConverted()" for more info about cart format
156
+ * */
157
+ private function isSameCartItem($a, $b)
158
+ {
159
+ return ($a['quote_id'] === $b['quote_id']);
160
+ }
161
+
162
+ /**
163
+ * this function gets old cart and new cart, compare them, and send corresponding add_item and
164
+ * remove_item events.
165
+ * */
166
+ public function compareCartsAndSendEvents($prevCart, $newCart)
167
+ {
168
+
169
+ // first iterate over previous cart, to send "remove item" events
170
+ foreach ($prevCart as $prevItem)
171
+ {
172
+ // get id and old quantity of the item
173
+ $currId = $prevItem['unique_id'];
174
+ $oldQuantity = $prevItem['quantity'];
175
+
176
+ // skip items with parents, because we already operate on the parents themselves
177
+ if (!is_null($prevItem["parent_id"])) {continue;}
178
+
179
+ // get new quantity for current item
180
+ $newQuantity = 0;
181
+ foreach ($newCart as $newItem)
182
+ {
183
+ if ($this->isSameCartItem($prevItem, $newItem))
184
+ {
185
+ $newQuantity = $newItem['quantity'];
186
+ break;
187
+ }
188
+ }
189
+
190
+ // if quantity decreased, send remove item events
191
+ if ($newQuantity < $oldQuantity)
192
+ {
193
+ // first add the data for the item itself
194
+ $data = $prevItem;
195
+ $data['quantity'] = $oldQuantity - $newQuantity;
196
+
197
+ // now convert to list and add all children items as well
198
+ $data = array($data);
199
+ foreach ($prevCart as $childItem)
200
+ {
201
+ if ($childItem["parent_quote_id"] === $prevItem["quote_id"])
202
+ {
203
+ array_push($data, $childItem);
204
+ }
205
+ }
206
+ $this->addUserEvent(self::EVENT_REMOVE_FROM_CART, $data);
207
+ }
208
+ }
209
+
210
+ // now iterate over new cart to send "add item" events
211
+ foreach ($newCart as $newItem)
212
+ {
213
+ // get id and new quantity of the item
214
+ $currId = $newItem['unique_id'];
215
+ $newQuantity = $newItem['quantity'];
216
+
217
+ // skip items with parents, because we already operate on the parents themselves
218
+ if (!is_null($newItem["parent_id"])) {continue;}
219
+
220
+ // get old quantity for current item
221
+ $oldQuantity = 0;
222
+ foreach ($prevCart as $prevItem)
223
+ {
224
+ if ($this->isSameCartItem($prevItem, $newItem))
225
+ {
226
+ $oldQuantity = $prevItem['quantity'];
227
+ break;
228
+ }
229
+ }
230
+
231
+ // if quantity decreased, send remove item events
232
+ if ($newQuantity > $oldQuantity)
233
+ {
234
+ // first add the data for the item itself
235
+ $data = $newItem;
236
+ $data['quantity'] = $newQuantity - $oldQuantity;
237
+
238
+ // now convert to list and add all children items as well
239
+ $data = array($data);
240
+ foreach ($newCart as $childItem)
241
+ {
242
+ if ($childItem["parent_quote_id"] === $newItem["quote_id"])
243
+ {
244
+ array_push($data, $childItem);
245
+ }
246
+ }
247
+ $this->addUserEvent(self::EVENT_ADD_TO_CART, $data);
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * add event for Shoptimally to send.
254
+ * for example, when magento detect add-to-cart event, we will use this
255
+ * function to pass the data to the Shoptimally client js.
256
+ *
257
+ * @param $type - srting, event type
258
+ * @param $data - data to send with the event
259
+ */
260
+ private function addUserEvent($type, $data)
261
+ {
262
+ $this->_utils->addEvent($type, $data);
263
+ }
264
+ }
app/code/community/Shoptimally/Analytics/Helper/Utils.php ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Analytics
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Misc analytic-related utils.
11
+ * This just wraps the functionality of sending event to Shoptimally via the user.
12
+ */
13
+ class Shoptimally_Analytics_Helper_Utils extends Mage_Core_Helper_Abstract
14
+ {
15
+ // current events queue
16
+ protected $_events = null;
17
+
18
+ /**
19
+ * load the currently existing events in queue
20
+ */
21
+ public function __construct()
22
+ {
23
+ // get events pending to be pushed to shoptimally
24
+ $cookies = Mage::helper('shoptimally_core/cookie');
25
+ $this->_events = $cookies->getCookie("shoptimally_events_queue", true, array());
26
+ }
27
+
28
+ /**
29
+ * add event for Shoptimally to send.
30
+ * for example, when magento detect add-to-cart event, we will use this
31
+ * function to pass the data to the Shoptimally client js.
32
+ *
33
+ * @param $type - srting, event type
34
+ * @param $data - data to send with the event
35
+ */
36
+ public function addEvent($type, $data)
37
+ {
38
+ // get source url
39
+ try
40
+ {
41
+ $srcUrl = Mage::helper('shoptimally_core/urlUtils')->getActualCurrentUrl();
42
+ }
43
+ catch (Exception $e)
44
+ {
45
+ $srcUrl = null;
46
+ }
47
+
48
+ // set data to push
49
+ $to_push = array(
50
+ 'type' => $type,
51
+ 'data' => $data,
52
+ 'src_url' => $srcUrl
53
+ );
54
+
55
+ // for debug
56
+ Mage::helper('shoptimally_core/log')->debug("New event to send " . $type . ": ", $to_push);
57
+
58
+ // add new event and re-set the cookie
59
+ array_push($this->_events, $to_push);
60
+ $cookies = Mage::helper('shoptimally_core/cookie');
61
+ $cookies->setCookie("shoptimally_events_queue", $this->_events, true);
62
+ }
63
+ }
app/code/community/Shoptimally/Analytics/Model/Observer.php ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Analytics
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * The observer to listen to different events we want to collect analytics about.
11
+ */
12
+ class Shoptimally_Analytics_Model_Observer
13
+ {
14
+
15
+ /**
16
+ * handle cart save
17
+ * write event to shoptimally
18
+ */
19
+ public function onCartSave(Varien_Event_Observer $obs)
20
+ {
21
+ try
22
+ {
23
+ if (!Mage::helper('shoptimally_core/config')->getIsEnabled())
24
+ {
25
+ return $this;
26
+ }
27
+
28
+ // get utils
29
+ $userEvents = Mage::helper('shoptimally_analytics/userEvents');
30
+
31
+ // get all cart items
32
+ $newCartData = $userEvents->getCartItemsConverted();
33
+
34
+ // get previous cart and compare with new cart to send events
35
+ $prevCart = $userEvents->getCartFromCookie();
36
+ $userEvents->compareCartsAndSendEvents($prevCart, $newCartData);
37
+
38
+ // set cookie with new cart data
39
+ $userEvents->writeCartToCookie($newCartData);
40
+
41
+ }
42
+ catch (Exception $e)
43
+ {
44
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
45
+ }
46
+
47
+ return $this;
48
+ }
49
+
50
+ /**
51
+ * handle successful checkout
52
+ * write event to shoptimally
53
+ */
54
+ public function onCheckoutComplete(Varien_Event_Observer $obs)
55
+ {
56
+ try
57
+ {
58
+ if (!Mage::helper('shoptimally_core/config')->getIsEnabled())
59
+ {
60
+ return $this;
61
+ }
62
+
63
+ // get utils
64
+ $userEvents = Mage::helper('shoptimally_analytics/userEvents');
65
+ $objUtils = Mage::helper('shoptimally_core/objectUtils');
66
+
67
+ // get current order
68
+ $order = new Mage_Sales_Model_Order();
69
+ $incrementId = Mage::getSingleton('checkout/session')->getLastRealOrderId();
70
+ $order->loadByIncrementId($incrementId);
71
+
72
+ // this if is important because if its null it will create fatal that we will not catch.
73
+ if (is_null($order))
74
+ {
75
+ Mage::helper('shoptimally_core/log')->warn("Failed to get order data from observer!");
76
+ return $this;
77
+ }
78
+
79
+ // get order data
80
+ $orderData = $order->getData();
81
+
82
+ // get cart and clear our cart cookie.
83
+ // its important to clean cart cookie now because after the checkout magento will empty the cart and save, and if we still
84
+ // have items in our shoptimally cart cookie we will think there was an item-removed events and send false "remove items".
85
+ $currCartData = $userEvents->getCartFromCookie();
86
+ $userEvents->writeCartToCookie(array());
87
+
88
+ // get all checkout totals (prices, tax, shipping, etc.)
89
+ $keys = array("grand_total", "subtotal", "shipping_amount", "tax_amount");
90
+ $orderData = $objUtils->array_extract($orderData, $keys, -1);
91
+
92
+ // add checkout event
93
+ $data = array(
94
+ "cart_items" => $currCartData,
95
+ "order_data" => $orderData,
96
+ );
97
+ $userEvents->reportCheckout($data);
98
+ }
99
+ catch (Exception $e)
100
+ {
101
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
102
+ }
103
+
104
+ return $this;
105
+ }
106
+ }
app/code/community/Shoptimally/Analytics/etc/config.xml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+
4
+ <!-- general module config -->
5
+ <modules>
6
+ <Shoptimally_Analytics>
7
+ <version>1.0.0</version>
8
+ </Shoptimally_Analytics>
9
+ </modules>
10
+
11
+ <!-- basic settings - models, helpers, block dirs etc.. -->
12
+ <global>
13
+ <helpers>
14
+ <shoptimally_analytics>
15
+ <class>Shoptimally_Analytics_Helper</class>
16
+ </shoptimally_analytics>
17
+ </helpers>
18
+ <models>
19
+ <shoptimally_analytics>
20
+ <class>Shoptimally_Analytics_Model</class>
21
+ </shoptimally_analytics>
22
+ </models>
23
+ </global>
24
+
25
+ <!-- frontend and events config -->
26
+ <frontend>
27
+ <events>
28
+
29
+ <!-- event: cart save -->
30
+ <checkout_cart_save_after>
31
+ <observers>
32
+ <shoptimally_analytics_cart_save_before>
33
+ <class>shoptimally_analytics/observer</class>
34
+ <method>onCartSave</method>
35
+ </shoptimally_analytics_cart_save_before>
36
+ </observers>
37
+ </checkout_cart_save_after>
38
+
39
+ <!-- event: after successful checkout -->
40
+ <checkout_onepage_controller_success_action>
41
+ <observers>
42
+ <shoptimally_analytics_success_checkout>
43
+ <class>shoptimally_analytics/observer</class>
44
+ <method>onCheckoutComplete</method>
45
+ </shoptimally_analytics_success_checkout>
46
+ </observers>
47
+ </checkout_onepage_controller_success_action>
48
+
49
+ </events>
50
+ </frontend>
51
+
52
+ </config>
app/code/community/Shoptimally/Analytics/readme.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ This module responsible to listen to important events (add to cart, removed from cart, checkout, etc.) and transfer this data to the client javascript (via cookie), so the client can send analytics to server.
2
+ In other words, this module responsible to collect users data.
3
+
4
+ Note: to see the corresponding javascript code for this module (for client side), take a look at "adapters/for_platforms/magento/" inside the client_side project (not in this dir tree).
app/code/community/Shoptimally/CatalogSync/Block/ProductsRenderer.php ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This block gets a list of product ids and render the products without toolbar and other nonesense.
11
+ * Note however that weather its a single product or multiple products, they will always be wrapped inside
12
+ * the products grid and have classes like "item first" etc, as if its items grid on page.
13
+ */
14
+ class Shoptimally_CatalogSync_Block_ProductsRenderer extends Mage_Catalog_Block_Product_List
15
+ {
16
+ protected $_productIds = null;
17
+
18
+ /**
19
+ * set the list of product ids to render
20
+ **/
21
+ public function setProductsList($productIds)
22
+ {
23
+ $this->_productIds = $productIds;
24
+ return $this;
25
+ }
26
+
27
+
28
+ /**
29
+ * Retrieve loaded category collection
30
+ *
31
+ * @return Mage_Eav_Model_Entity_Collection_Abstract
32
+ **/
33
+ protected function _getProductCollection()
34
+ {
35
+ $collection = Mage::getModel('catalog/product')->getCollection()
36
+ ->addAttributeToFilter('entity_id', array('in' => $this->_productIds))
37
+ ->addAttributeToSelect('*')
38
+ ->load();
39
+ return $collection;
40
+ }
41
+
42
+ /**
43
+ * We override this function so we won't dispatch the catalog_block_product_list_collection event.
44
+ * Note: we must add the toolbar as child because it is used internally to determine how to display
45
+ * the products. but we still need to not render it somehow.
46
+ */
47
+ protected function _beforeToHtml()
48
+ {
49
+ $toolbar = $this->getToolbarBlock();
50
+
51
+ // called prepare sortable parameters
52
+ $collection = $this->_getProductCollection();
53
+
54
+ // use sortable parameters
55
+ if ($orders = $this->getAvailableOrders()) {
56
+ $toolbar->setAvailableOrders($orders);
57
+ }
58
+ if ($sort = $this->getSortBy()) {
59
+ $toolbar->setDefaultOrder($sort);
60
+ }
61
+ if ($dir = $this->getDefaultDirection()) {
62
+ $toolbar->setDefaultDirection($dir);
63
+ }
64
+ if ($modes = $this->getModes()) {
65
+ $toolbar->setModes($modes);
66
+ }
67
+
68
+ // set collection to toolbar and apply sort
69
+ $toolbar->setCollection($collection);
70
+ $this->setChild('toolbar', $toolbar);
71
+
72
+ // call the base _beforeToHtml(), while skipping the Mage_Catalog_Block_Product_List::beforeToHtml()
73
+ return Mage_Catalog_Block_Product_Abstract::_beforeToHtml();
74
+ }
75
+
76
+ /**
77
+ * Retrieve additional blocks html
78
+ *
79
+ * @return string
80
+ */
81
+ public function getAdditionalHtml()
82
+ {
83
+ return "";
84
+ }
85
+
86
+ /**
87
+ * Retrieve list toolbar HTML
88
+ *
89
+ * @return string
90
+ */
91
+ public function getToolbarHtml()
92
+ {
93
+ return "";
94
+ }
95
+ }
app/code/community/Shoptimally/CatalogSync/Helper/InterestingList.php ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * All the functionality to send catalog items based on interesting list / product views
11
+ */
12
+ class Shoptimally_CatalogSync_Helper_InterestingList extends Mage_Core_Helper_Abstract
13
+ {
14
+
15
+ /**
16
+ * add list of interesting products we want to give priority to next time we send
17
+ * catalog sync.
18
+ * @param $products - list of products that were viewed.
19
+ *
20
+ * */
21
+ public function addProductsToInterestingList($products)
22
+ {
23
+ // get interesting list config
24
+ $remoteConfig = Mage::helper('shoptimally_core/remoteConfig');
25
+ $interestingListConfig = $remoteConfig->get("catalog_sync_interesting_list");
26
+
27
+ // get storage helper
28
+ $storage = Mage::helper('shoptimally_core/storage');
29
+
30
+ // get sent history, current interesting list and calc how many room we got left
31
+ try {
32
+ $sentHistory = $storage->get("interesting_list_sent_history", array(), true);
33
+ $currList = $storage->get("current_interesting_list", array(), true);
34
+ }
35
+ // on exception zero the lists
36
+ catch (Exception $e) {
37
+ $log->warn("Had exception while updating interesting list, deleted both lists.", $e);
38
+ $currList = array();
39
+ $sentHistory = array();
40
+ }
41
+
42
+ $roomLeft = $interestingListConfig["max_products_count"] - count($currList);
43
+
44
+ // if list is full skip this whole function
45
+ if ($roomLeft <= 0)
46
+ {
47
+ return;
48
+ }
49
+
50
+ // just in case..
51
+ if (is_null($currList)) {$currList = array();}
52
+ if (is_null($sentHistory)) {$sentHistory = array();}
53
+
54
+ // get ttl, eg how old a product must be to push it into the interesting list
55
+ $ttl = $interestingListConfig["products_ttl"];
56
+ $currTime = time ();
57
+
58
+ // iterate over the products collection and create a list with only the ids that were not
59
+ // update in the last X hours (configurable)
60
+ $productsToPush = array();
61
+ foreach ($products as $product)
62
+ {
63
+ // if no more room in interesting list, skip
64
+ if ($roomLeft <= 0)
65
+ {
66
+ break;
67
+ }
68
+
69
+ // get id
70
+ $id = $product->getId();
71
+
72
+ // if already appear in the interesting list, skip
73
+ if (in_array($id, $currList))
74
+ {
75
+ continue;
76
+ }
77
+
78
+ // if was recently sent, skip
79
+ if (array_key_exists($id, $sentHistory))
80
+ {
81
+ continue;
82
+ }
83
+
84
+ // if got here all conditions are met and we add this item to the interesting list!
85
+ array_push($currList, $id);
86
+ $roomLeft--;
87
+ }
88
+
89
+ // write the updated list to storage
90
+ $storage->set("current_interesting_list", $currList, true);
91
+ }
92
+
93
+
94
+ /**
95
+ * Called every X minutes to update the Shoptimally server with the latest catalog.
96
+ * This function uses the "interesting products" list generated by most viewed products.
97
+ */
98
+ public function sendInterestingListToServer()
99
+ {
100
+ // get some core utilities
101
+ $log = Mage::helper('shoptimally_core/log');
102
+ $storage = Mage::helper('shoptimally_core/storage');
103
+ $remoteConfig = Mage::helper('shoptimally_core/remoteConfig');
104
+ $productUtils = Mage::helper('shoptimally_core/productsUtils');
105
+
106
+ // get interesting list config
107
+ $interestingListConfig = $remoteConfig->get("catalog_sync_interesting_list");
108
+
109
+ // if disabled skip
110
+ if (!$interestingListConfig["enable"])
111
+ {
112
+ return;
113
+ }
114
+
115
+ // get current iteration
116
+ $currIteration = $storage->get("interesting_list_iteration", 0, true);
117
+
118
+ // get history list and curr list
119
+ try {
120
+ $sentHistory = $storage->get("interesting_list_sent_history", array(), true);
121
+ $currList = $storage->get("current_interesting_list", array(), true);
122
+ }
123
+ // on exception zero the lists
124
+ catch (Exception $e) {
125
+ $log->warn("Had exception while reading interesting list, deleted both lists.", $e);
126
+ $storage->set("interesting_list_sent_history", array(), true);
127
+ $storage->set("current_interesting_list", array(), true);
128
+ }
129
+
130
+ $toSendCount = $interestingListConfig["max_products_send_count"];
131
+
132
+ // if emtpy skip
133
+ if (empty($currList))
134
+ {
135
+ $log->log("Update Catalog from interesting list: No interesting products to update..");
136
+ $idsToSend = array();
137
+ }
138
+ else
139
+ {
140
+ // get how many items to send
141
+ $idsToSend = array_slice($currList, 0, $toSendCount);
142
+
143
+ // set the htmls update queue
144
+ $productIds = $storage->set("htmls_interesting_list", $idsToSend, true);
145
+
146
+ // log report
147
+ $log->log("Update Catalog from interesting list (iteration " . $currIteration . "): ", $idsToSend);
148
+
149
+ // iterate over the interesting list
150
+ $collection = Mage::getModel('catalog/product')->getCollection()
151
+ ->addAttributeToFilter('entity_id', array('in' => $idsToSend))
152
+ ->addAttributeToSelect('*')
153
+ ->load();
154
+
155
+ // do the update
156
+ $utils = Mage::helper('shoptimally_catalogsync/utils');
157
+ $updatedCount = $utils->sendUpdateToServer(null, $collection);
158
+
159
+ $log->log("Done, updated " . $updatedCount . " products!");
160
+ }
161
+
162
+ // get ttl and current time
163
+ $ttl = $interestingListConfig["products_ttl"];
164
+
165
+ // iterate over history list and remove items that are too old
166
+ $removedCount = 0;
167
+ foreach ($sentHistory as $id => $updateTime)
168
+ {
169
+ // if too old remove
170
+ if ($updateTime > $ttl || $updateTime === $currIteration)
171
+ {
172
+ $removedCount++;
173
+ unset($sentHistory[$id]);
174
+ }
175
+ }
176
+ if ($removedCount > 0)
177
+ {
178
+ $log->debug("Removed " . $removedCount . " products from history list because they were too old.");
179
+ }
180
+
181
+ // add new items to history list
182
+ if (!empty($idsToSend))
183
+ {
184
+ $log->debug("Adding " . count($idsToSend) . " items to history list.");
185
+ foreach ($idsToSend as $id)
186
+ {
187
+ $sentHistory[$id] = $currIteration;
188
+ }
189
+ }
190
+
191
+ // increase iteration and save
192
+ if (++$currIteration > $ttl) {$currIteration = 0;}
193
+ $storage->set("interesting_list_iteration", $currIteration, true);
194
+
195
+ // save updated history list
196
+ $storage->set("interesting_list_sent_history", $sentHistory, true);
197
+
198
+ // cut the items from the curr list
199
+ $log->debug("Removed " . $toSendCount . " products from interesting products list.");
200
+ $currList = array_slice($currList, $toSendCount);
201
+ $storage->set("current_interesting_list", $currList, true);
202
+
203
+ }
204
+ }
app/code/community/Shoptimally/CatalogSync/Helper/TimeBased.php ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This class responsible for the timely-based catalog sync updates, eg sending random / by order items every
11
+ * hour or so.
12
+ */
13
+ class Shoptimally_CatalogSync_Helper_TimeBased extends Mage_Core_Helper_Abstract
14
+ {
15
+ // how many products to update every update batch
16
+ const DEFAULT_UPDATE_PRODUCTS_PAGE_SIZE = 200;
17
+
18
+ /**
19
+ * Called every X minutes to update the Shoptimally server with the latest catalog.
20
+ * This method try to crawl categories by order so that every item has const update cycles.
21
+ * This is the old way that will be removed soon.
22
+ */
23
+ public function doTimelyCatalogUpdate()
24
+ {
25
+
26
+ // get some core utilities
27
+ $log = Mage::helper('shoptimally_core/log');
28
+ $storage = Mage::helper('shoptimally_core/storage');
29
+ $remoteStorage = Mage::helper('shoptimally_core/remoteConfig');
30
+
31
+ // get last category and page to continue update from there
32
+ $startCategory = $storage->get("update_last_category");
33
+ $startPage = $storage->get("update_last_page");
34
+
35
+ // if first run
36
+ if (is_null($startCategory) || empty($startCategory))
37
+ {
38
+ $startCategory = 0;
39
+ $startPage = 0;
40
+ }
41
+
42
+ // get how many products to send to server, either from remote config or default
43
+ $pageSize = $remoteStorage->get("catalog_sync_products_batch_size");
44
+ if (is_null($pageSize)) {$pageSize = self::DEFAULT_UPDATE_PRODUCTS_PAGE_SIZE;}
45
+
46
+ // get all categories
47
+ $categoriesCollection = Mage::getModel('catalog/category')
48
+ ->getCollection()
49
+ ->addAttributeToSelect('name');
50
+
51
+ // convert to array of categories
52
+ $categories = array();
53
+ $indx = 0;
54
+ foreach($categoriesCollection as $category)
55
+ {
56
+ array_push($categories, $category);
57
+ if ($indx++ > $startCategory) break;
58
+ }
59
+
60
+ // if category overflows, set back to 0
61
+ if ($startCategory >= count($categories))
62
+ {
63
+ $log->log("Update Catalog: finished cycle, restarting from category 0.");
64
+ $startCategory = 0;
65
+ }
66
+
67
+ // get current category
68
+ $category = $categories[$startCategory];
69
+
70
+ // get products in category based on page size and current page
71
+ $productCollection = $category->getProductCollection()
72
+ ->setPageSize($pageSize)->setCurPage($startPage);
73
+
74
+
75
+ // get total items count in category
76
+ $totalItemsInCategory = $productCollection->getSize();
77
+
78
+ // for debug purposes
79
+ $storage->set("last_category_name", $category->getName());
80
+
81
+ // log report
82
+ $log->log("Update Catalog: start update [category = '" . $category->getName() . "', page = " . $startPage . ", Category progress: " . ($pageSize * $startPage) . "/" . $totalItemsInCategory . "]");
83
+
84
+ // increase page index (will be saved at the end)
85
+ $startPage++;
86
+
87
+ // select the attributes we want to get
88
+ $productCollection->addAttributeToSelect('*');
89
+
90
+ // do the update
91
+ $utils = Mage::helper('shoptimally_catalogsync/utils');
92
+ $productsCount = $utils->sendUpdateToServer($category, $productCollection);
93
+
94
+ $log->log("Update Catalog: finished update [sent " . $productsCount . " items]");
95
+
96
+ // check if we finished this cateogry
97
+ if ($pageSize * $startPage > $totalItemsInCategory)
98
+ {
99
+ $log->debug("Update Catalog: finished category '" . $category->getName() . "', move to next category.");
100
+ $startCategory++;
101
+ $startPage = 0;
102
+ }
103
+
104
+ // store new page index and category
105
+ $storage->set("update_last_category", $startCategory);
106
+ $storage->set("update_last_page", $startPage);
107
+
108
+ }
109
+
110
+
111
+ /**
112
+ * Called every X minutes to update the Shoptimally server with the latest catalog.
113
+ * This function takes random objects from any categories. This is the new method.
114
+ */
115
+ public function doTimelyCatalogUpdateRandom()
116
+ {
117
+ // get some core utilities
118
+ $log = Mage::helper('shoptimally_core/log');
119
+ $storage = Mage::helper('shoptimally_core/storage');
120
+ $remoteStorage = Mage::helper('shoptimally_core/remoteConfig');
121
+
122
+ // get how many products to send to server, either from remote config or default
123
+ $pageSize = $remoteStorage->get("catalog_sync_products_batch_size");
124
+ if (is_null($pageSize) || $pageSize == 0) {$pageSize = self::DEFAULT_UPDATE_PRODUCTS_PAGE_SIZE;}
125
+
126
+ $collection = Mage::getModel('catalog/product')->getCollection();
127
+ $total_products_in_store = $collection->getSize();
128
+
129
+ // calc max page for random
130
+ $maxPage = floor(($total_products_in_store / $pageSize) + 0.5);
131
+
132
+ // log
133
+ $log->debug("Update Catalog: prepare to send " . $pageSize . " products to shoptimally (out of total " . $total_products_in_store . " products, " . $maxPage . " pages)");
134
+
135
+ // random page index
136
+ $pageIndex = rand(0, $maxPage);
137
+ $productCollection = Mage::getModel('catalog/product')->getCollection()
138
+ ->setPageSize($pageSize)->setCurPage($pageIndex)
139
+ ->addAttributeToSelect('*')->load();
140
+
141
+ // log report
142
+ $log->log("Update Catalog: send item indexes (not == ids) " . ($pageIndex*$pageSize) . "-" . ($pageIndex*$pageSize+$pageSize) . " [page: " . $pageIndex . "].");
143
+
144
+ // do the update
145
+ $utils = Mage::helper('shoptimally_catalogsync/utils');
146
+ $actualProductsCount = $utils->sendUpdateToServer(null, $productCollection);
147
+
148
+ // log report
149
+ $log->log("Update Catalog: update done. actually sent: " . $actualProductsCount . " products.");
150
+
151
+ }
152
+ }
app/code/community/Shoptimally/CatalogSync/Helper/UpdateItemsHtmls.php ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This class responsible to send products htmls to Shoptimally server, for ajax-based renderings.
11
+ */
12
+ class Shoptimally_CatalogSync_Helper_UpdateItemsHtmls extends Mage_Core_Helper_Abstract
13
+ {
14
+
15
+ /**
16
+ * Send item htmls to server
17
+ */
18
+ public function sendProductsHtmlsToServer()
19
+ {
20
+ // get some core utilities
21
+ $log = Mage::helper('shoptimally_core/log');
22
+ $storage = Mage::helper('shoptimally_core/storage');
23
+ $remoteConfig = Mage::helper('shoptimally_core/remoteConfig');
24
+ $interestingListConfig = $remoteConfig->get("catalog_sync_interesting_list");
25
+
26
+ // get current interesting list
27
+ $productIds = $storage->get("htmls_interesting_list", array(), true);
28
+
29
+ // zero the htmls interesting list
30
+ $storage->set("htmls_interesting_list", array(), true);
31
+
32
+ // slice just the ids we want to send based on interesting products list settings
33
+ $toSendCount = $interestingListConfig["max_products_send_count"];
34
+ $productIds = array_slice($productIds, 0, $toSendCount);
35
+
36
+ // create a special block to render the products html
37
+ $block = Mage::app()->getLayout()->createBlock('shoptimally_catalogsync/productsRenderer')
38
+ ->setTemplate('catalog/product/list.phtml');
39
+
40
+ // log report
41
+ $log->debug("Update Products Html: Send html of " . count($productIds) . " products from interesting list.");
42
+
43
+ // prepare data to send - dictionary with product_id => html
44
+ $data = array();
45
+ foreach ($productIds as $id)
46
+ {
47
+ $block->setProductsList(array($id));
48
+ $data[$id] = $block->toHtml();
49
+ }
50
+
51
+ // send products htmls
52
+ $server = Mage::helper('shoptimally_core/server');
53
+ $response = $server->sendRequest("sites/update_products_html/", array("items" => $data), 30);
54
+
55
+ // report errors
56
+ if (is_null($response) || $response->isError()) {
57
+ Mage::helper('shoptimally_core/log')->warn(
58
+ "Failed to update server with products htmls!",
59
+ $response);
60
+ }
61
+ }
62
+ }
app/code/community/Shoptimally/CatalogSync/Helper/Utils.php ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Utility functions for CatalogSync, main functionality is to send products data to server
11
+ */
12
+ class Shoptimally_CatalogSync_Helper_Utils extends Mage_Core_Helper_Abstract
13
+ {
14
+
15
+ /**
16
+ * send update to server about list of products
17
+ *
18
+ * @param $category - related category (category that is the source of this update)
19
+ * note: if $category is not provided (eg null), will update ALL
20
+ * source categories for this product
21
+ * @param $products - list/collection of products or to update server about
22
+ * @return - number of items successfuly updated (0 if failed to update at all)
23
+ */
24
+ public function sendUpdateToServer($category, $productCollection)
25
+ {
26
+ // get some core utilities
27
+ $log = Mage::helper('shoptimally_core/log');
28
+ $urlUtils = Mage::helper('shoptimally_core/urlUtils');
29
+ $productsUtils = Mage::helper('shoptimally_core/productsUtils');
30
+ $server = Mage::helper('shoptimally_core/server');
31
+
32
+ // get site currency
33
+ $currencyCode = Mage::app()->getStore()->getCurrentCurrencyCode();
34
+
35
+ // get category url
36
+ if (is_null($category))
37
+ {
38
+ $categoryUrl = "";
39
+ }
40
+ else
41
+ {
42
+ $categoryUrl = $urlUtils->toRelative($category->getUrl());
43
+ }
44
+
45
+ // prepare data to send items in current category and page
46
+ $dataToSend = array(
47
+ "normalized_source_url" => $categoryUrl,
48
+ "items" => array(),
49
+ );
50
+
51
+ // for logging at the end
52
+ $productsCount = 0;
53
+
54
+ // Now you can loop through your collection
55
+ foreach($productCollection as $product) {
56
+
57
+ // increase products count (for logging at the end)
58
+ $productsCount++;
59
+
60
+ // get product data to send
61
+ $productData = $productsUtils->getProductFullData($product);
62
+
63
+ // if didn't get a specific source category, get all source categories urls
64
+ if (is_null($category))
65
+ {
66
+ // get product categories to get all categories urls
67
+ $categoriesUrls = array();
68
+ $cats = $product->getCategoryIds();
69
+ foreach ($cats as $category_id) {
70
+ $_cat = Mage::getModel('catalog/category')->load($category_id);
71
+ array_push($categoriesUrls, $urlUtils->toRelative($_cat->getUrl()));
72
+ }
73
+
74
+ // add special all-categories update field
75
+ $productData['all_src_urls'] = $categoriesUrls;
76
+ }
77
+
78
+ // add currency
79
+ $productData['currency'] = $currencyCode;
80
+
81
+ // push into data to send to server
82
+ array_push($dataToSend["items"], $productData);
83
+ }
84
+
85
+ // log report
86
+ if (is_null($category))
87
+ {
88
+ $log->debug("Update Catalog: send " . $productsCount . " products.");
89
+ }
90
+ else
91
+ {
92
+ $log->debug("Update Catalog: send " . $productsCount . " products from category '" . $category->getName() . "'.");
93
+ }
94
+
95
+ // send update to server
96
+ $response = $server->sendRequest("sites/update_products/", $dataToSend, 240);
97
+
98
+ // handle errors from server
99
+ if (is_null($response) || $response->isError()) {
100
+ Mage::helper('shoptimally_core/log')->warn(
101
+ "Failed to update server with catalog data!",
102
+ $response);
103
+ return 0;
104
+ }
105
+
106
+ // success!
107
+ return $productsCount;
108
+ }
109
+ }
app/code/community/Shoptimally/CatalogSync/Model/Cron.php ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This cron job update Shoptimally server about changed products every X hours.
11
+ * These low rate updates are just to make sure that if some product updates somehow
12
+ * slipped away (for example due to temporary network failure), those products will still be
13
+ * updated, eventually. even if the admin doesn't save them again.
14
+ */
15
+ class Shoptimally_CatalogSync_Model_Cron
16
+ {
17
+ /**
18
+ * called every X minutes to update the Shoptimally server with the latest catalog
19
+ */
20
+ public function updateCatalog()
21
+ {
22
+ try
23
+ {
24
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled())
25
+ {
26
+ // get which method to use
27
+ $method = Mage::helper('shoptimally_core/remoteConfig')->get("catalog_sync_timely_method");
28
+
29
+ switch ($method)
30
+ {
31
+ // the old-school incremental method
32
+ case "incremental":
33
+ Mage::helper('shoptimally_catalogsync/timeBased')->doTimelyCatalogUpdate();
34
+ break;
35
+
36
+ // the new random-based catalog sync
37
+ case "random":
38
+ Mage::helper('shoptimally_catalogsync/timeBased')->doTimelyCatalogUpdateRandom();
39
+ break;
40
+
41
+ // disabled
42
+ case "none":
43
+ Mage::helper('shoptimally_core/log')->log("Time-based did not run because catalog sync is currently disabled (method=none).");
44
+ break;
45
+
46
+ // invalid value
47
+ default:
48
+ Mage::helper('shoptimally_core/log')->warn("Invalid time-base catalog sync method! value: " . $method);
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ catch (Exception $e)
54
+ {
55
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception while updating items to server!", $e);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * called every X minutes to update products html to the Shoptimally server
61
+ * */
62
+ public function updateProductsHtmls()
63
+ {
64
+ try
65
+ {
66
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled() &&
67
+ Mage::helper('shoptimally_core/remoteConfig')->get("update_items_html"))
68
+ {
69
+ Mage::helper('shoptimally_catalogsync/updateItemsHtmls')->sendProductsHtmlsToServer();
70
+ }
71
+ }
72
+ catch (Exception $e)
73
+ {
74
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception while updating item htmls to server!", $e);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * called every X minutes to update the Shoptimally server with the latest catalog based on interesting list
80
+ */
81
+ public function updateCatalogInterestingList()
82
+ {
83
+ try
84
+ {
85
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled())
86
+ {
87
+ Mage::helper('shoptimally_catalogsync/interestingList')->sendInterestingListToServer();
88
+ }
89
+ }
90
+ catch (Exception $e)
91
+ {
92
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception while updating items to server from interesting list!", $e);
93
+ }
94
+ }
95
+ }
app/code/community/Shoptimally/CatalogSync/Model/Observer.php ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This observer listen to events like products save and attribute updates, to tell
11
+ * Shoptimally about that and keep us in sync.
12
+ */
13
+ class Shoptimally_CatalogSync_Model_Observer
14
+ {
15
+
16
+ /**
17
+ * called when products list is loaded to update the "interesting products" list we want to update.
18
+ * */
19
+ public function onProductsListLoaded(Varien_Event_Observer $observer)
20
+ {
21
+ try
22
+ {
23
+ // make sure shoptimally is enabled
24
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled())
25
+ {
26
+ // get remote config
27
+ $remoteConfig = Mage::helper('shoptimally_core/remoteConfig');
28
+
29
+ // make sure this feature is enabled
30
+ $interestingListConfig = $remoteConfig->get("catalog_sync_interesting_list");
31
+ if ($interestingListConfig["enable"])
32
+ {
33
+ // randomly choose if we are going to add to interesting list at this point or not
34
+ $chance = $interestingListConfig["frequency"];
35
+ $roll = rand(0, 100);
36
+
37
+ // if we should run at this time:
38
+ if ($chance >= $roll)
39
+ {
40
+ $collection = $observer->getCollection();
41
+ Mage::helper('shoptimally_catalogsync/interestingList')->addProductsToInterestingList($collection);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ catch (Exception $e)
47
+ {
48
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
49
+ }
50
+
51
+ return $this;
52
+ }
53
+
54
+ /**
55
+ * called whenever a product is updated
56
+ */
57
+ public function onProductUpdate(Varien_Event_Observer $obs)
58
+ {
59
+ try
60
+ {
61
+ if (!Mage::helper('shoptimally_core/config')->getIsEnabled())
62
+ {
63
+ return $this;
64
+ }
65
+
66
+ // get updated product
67
+ $product = $obs->getProduct();
68
+
69
+ // to make sure we won't mess things up for older versions
70
+ if (is_null($product))
71
+ {
72
+ $log = Mage::helper('shoptimally_core/log');
73
+ $log->warn("Product was saved but could not send update because failed to get the product from observer!");
74
+ return $this;
75
+ }
76
+
77
+ // get log helper
78
+ $log = Mage::helper('shoptimally_core/log');
79
+ $log->debug("Product '" . $product->getName() . "' was updated.");
80
+
81
+ // do the update
82
+ $productCollection = array($product);
83
+ $productsCount = Mage::helper('shoptimally_catalogsync/utils')->sendUpdateToServer(null, $productCollection);
84
+ }
85
+ catch (Exception $e)
86
+ {
87
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
88
+ }
89
+ return $this;
90
+ }
91
+ }
app/code/community/Shoptimally/CatalogSync/etc/config.xml ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+
4
+ <!-- general module config -->
5
+ <modules>
6
+ <Shoptimally_CatalogSync>
7
+ <version>1.0.0</version>
8
+ </Shoptimally_CatalogSync>
9
+ </modules>
10
+
11
+ <!-- basic settings - models, helpers, block dirs etc.. -->
12
+ <global>
13
+
14
+ <helpers>
15
+ <shoptimally_catalogsync>
16
+ <class>Shoptimally_CatalogSync_Helper</class>
17
+ </shoptimally_catalogsync>
18
+ </helpers>
19
+
20
+ <models>
21
+ <shoptimally_catalogsync>
22
+ <class>Shoptimally_CatalogSync_Model</class>
23
+ </shoptimally_catalogsync>
24
+ </models>
25
+
26
+ <blocks>
27
+ <shoptimally_catalogsync>
28
+ <class>Shoptimally_CatalogSync_Block</class>
29
+ </shoptimally_catalogsync>
30
+ </blocks>
31
+
32
+ </global>
33
+
34
+ <!-- cronjob to update products to the server -->
35
+ <crontab>
36
+ <jobs>
37
+ <!-- time-based catalog sync -->
38
+ <shoptimally_catalogsync_time_based>
39
+ <schedule><cron_expr>0,15,30,45 * * * *</cron_expr></schedule>
40
+ <run><model>shoptimally_catalogsync/cron::updateCatalog</model></run>
41
+ </shoptimally_catalogsync_time_based>
42
+
43
+ <!-- interesting list catalog sync -->
44
+ <shoptimally_catalogsync_interesting_list>
45
+ <schedule><cron_expr>0,10,20,30,40,50 * * * *</cron_expr></schedule>
46
+ <run><model>shoptimally_catalogsync/cron::updateCatalogInterestingList</model></run>
47
+ </shoptimally_catalogsync_interesting_list>
48
+
49
+ <!-- update products htmls -->
50
+ <shoptimally_catalogsync_update_htmls>
51
+ <schedule><cron_expr>9,19,29,39,49,59 * * * *</cron_expr></schedule>
52
+ <run><model>shoptimally_catalogsync/cron::updateProductsHtmls</model></run>
53
+ </shoptimally_catalogsync_update_htmls>
54
+ </jobs>
55
+ </crontab>
56
+
57
+ <!-- frontend and events config -->
58
+ <adminhtml>
59
+ <events>
60
+
61
+ <!-- event: product save, to update shoptimally about new item data -->
62
+ <catalog_product_save_after>
63
+ <observers>
64
+ <shoptimally_catalogsync>
65
+ <type>model</type>
66
+ <class>shoptimally_catalogsync/observer</class>
67
+ <method>onProductUpdate</method>
68
+ </shoptimally_catalogsync>
69
+ </observers>
70
+ </catalog_product_save_after>
71
+
72
+ </events>
73
+ </adminhtml>
74
+
75
+ <!-- event: products list loaded, to add them to interesting products list -->
76
+ <frontend>
77
+ <events>
78
+ <catalog_block_product_list_collection>
79
+ <observers>
80
+ <shoptimally_catalogsync>
81
+ <type>model</type>
82
+ <class>shoptimally_catalogsync/observer</class>
83
+ <method>onProductsListLoaded</method>
84
+ </shoptimally_catalogsync>
85
+ </observers>
86
+ </catalog_block_product_list_collection>
87
+ </events>
88
+ </frontend>
89
+
90
+ </config>
app/code/community/Shoptimally/CatalogSync/readme.md ADDED
@@ -0,0 +1,2 @@
 
 
1
+ This module responsible to sync the site catalog and products data with Shoptimally.
2
+ It listen to events like product save and attribute update, and in addition have a cronjob to slowly update catalog over time.
app/code/community/Shoptimally/Core/Block/Injectjs.php ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This block inject the shoptimally client js tag into header.
11
+ */
12
+ class Shoptimally_Core_Block_Injectjs extends Mage_Core_Block_Template
13
+ {
14
+ // return if shoptimally is currently enabled on this site
15
+ public function isEnabled()
16
+ {
17
+ return Mage::helper('shoptimally_core/config')->getIsEnabled();
18
+ }
19
+
20
+ // get shoptimally version
21
+ public function getVersion()
22
+ {
23
+ return Mage::helper('shoptimally_core/config')->getVersion();
24
+ }
25
+
26
+ // get the full url to the shoptimally js for this site
27
+ public function getShoptimallyJsUrl()
28
+ {
29
+ try
30
+ {
31
+ return Mage::helper('shoptimally_core/config')->getJsUrl();
32
+ }
33
+ catch (Exception $e)
34
+ {
35
+ return "error";
36
+ }
37
+ }
38
+
39
+ // get api key
40
+ public function getApiKey()
41
+ {
42
+ try
43
+ {
44
+ return Mage::helper('shoptimally_core/config')->getApiKey();
45
+ }
46
+ catch (Exception $e)
47
+ {
48
+ return "error";
49
+ }
50
+ }
51
+
52
+ // get either "async" or empty string, to enable/disable async js mode
53
+ public function getShoptimallyAsyncMode()
54
+ {
55
+ try
56
+ {
57
+ if (Mage::helper('shoptimally_core/config')->shouldLoadJsAsync()) {
58
+ return "async";
59
+ }
60
+ return "";
61
+ }
62
+ catch (Exception $e)
63
+ {
64
+ return "async data-sh-error=''";
65
+ }
66
+ }
67
+
68
+ // return some extra hinters and metadata we provide for Shoptimally javascript code
69
+ // reutnr json object
70
+ public function getPageInfo()
71
+ {
72
+ try
73
+ {
74
+ // get basic info + host prefix
75
+ $ret = Mage::helper('shoptimally_core/pageInfo')->getBasicInfo();
76
+ $ret["host_prefix"] = Mage::helper('shoptimally_core/remoteConfig')->get("host_urls_prefix");
77
+
78
+ // stringify and return result
79
+ return Mage::helper('core')->jsonEncode($ret);
80
+ }
81
+ catch (Exception $e)
82
+ {
83
+ return "null";
84
+ }
85
+ }
86
+
87
+ // get product ids on current page (or empty list if none or failed)
88
+ public function getProductIds()
89
+ {
90
+ try
91
+ {
92
+ $ret = Mage::helper('shoptimally_core/pageInfo')->getProductIds();
93
+ return Mage::helper('core')->jsonEncode($ret);
94
+ }
95
+ catch (Exception $e)
96
+ {
97
+ return "[]";
98
+ }
99
+ }
100
+
101
+ // get the shoptimally server url
102
+ public function getServerUrl()
103
+ {
104
+ try
105
+ {
106
+ return "//" . Mage::helper('shoptimally_core/config')->getServerUrl();
107
+ }
108
+ catch (Exception $e)
109
+ {
110
+ return "error";
111
+ }
112
+ }
113
+ }
app/code/community/Shoptimally/Core/Helper/BlockUtils.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This helper provide block-related utilities.
11
+ */
12
+ class Shoptimally_Core_Helper_BlockUtils extends Mage_Core_Helper_Abstract
13
+ {
14
+
15
+ // keep track on the current block being rendered
16
+ protected $_curr_block = null;
17
+
18
+ /**
19
+ * get current block rendered
20
+ */
21
+ public function getCurrentBlock()
22
+ {
23
+ return $this->_curr_block;
24
+ }
25
+
26
+ /**
27
+ * called whenever a block is rendered to set the current block variable
28
+ */
29
+ public function onBlockRender(Varien_Event_Observer $observer)
30
+ {
31
+ // Get block instance from event
32
+ $this->_curr_block = $observer->getBlock();
33
+ }
34
+ }
app/code/community/Shoptimally/Core/Helper/ClientData.php ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This helper read the client cookie to get data from it, including things like user id, features list, etc..
11
+ */
12
+ class Shoptimally_Core_Helper_ClientData extends Mage_Core_Helper_Abstract
13
+ {
14
+ /**
15
+ * Will store the content of the shoptimally_user cookie
16
+ * @var array
17
+ */
18
+ protected $_userCookie;
19
+
20
+ /**
21
+ * init the user data.
22
+ */
23
+ public function __construct()
24
+ {
25
+ $this->init();
26
+ }
27
+
28
+ /**
29
+ * init the client data.
30
+ */
31
+ protected function init()
32
+ {
33
+ $this->_userCookie = Mage::helper('shoptimally_core/cookie')->getCookie("shoptimally_user", true);
34
+ if (is_null($this->_userCookie)) {$this->_userCookie = array();}
35
+ $this->decodePart('active_features_cache');
36
+ }
37
+
38
+ /**
39
+ * get all user data for debug
40
+ * */
41
+ public function _getDataDebug()
42
+ {
43
+ return $this->_userCookie;
44
+ }
45
+
46
+ /**
47
+ * decode sub-keys in the user cookie that are stringified inside the cookie.
48
+ * eg: {some_field: "{a: 1, b: 2}"} instead of {some_field: {a: 1, b: 2}}
49
+ */
50
+ private function decodePart($name)
51
+ {
52
+ if (array_key_exists ($name, $this->_userCookie))
53
+ {
54
+ $decoder = Mage::helper('core');
55
+ $this->_userCookie[$name] = $decoder->jsonDecode($this->_userCookie[$name]);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * get features list from the cookie.
61
+ * these features are result of the Shoptimally server config + AB test
62
+ */
63
+ public function getEnabledFeatures()
64
+ {
65
+ try
66
+ {
67
+ return $this->_userCookie['active_features_cache']['list'];
68
+ }
69
+ catch (Exception $e)
70
+ {
71
+ return array();
72
+ }
73
+ }
74
+
75
+ /**
76
+ * get user id
77
+ */
78
+ public function getUserId()
79
+ {
80
+ if (array_key_exists("id", $this->_userCookie))
81
+ {
82
+ return $this->_userCookie["id"];
83
+ }
84
+ return null;
85
+ }
86
+ }
app/code/community/Shoptimally/Core/Helper/Config.php ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Provide access to global config and other Shoptimally general consts and settings.
11
+ */
12
+ class Shoptimally_Core_Helper_Config extends Mage_Core_Helper_Abstract
13
+ {
14
+ // caching if shoptimally is currently enabled
15
+ protected $_isEnabled = false;
16
+
17
+ // caching the remote config helper
18
+ protected $_remoteConfig;
19
+
20
+ // caching shoptimally domain
21
+ protected $_shoptimallyDomain;
22
+
23
+ // current version
24
+ const SHOPTIMALLY_VERSION = "1.1.03";
25
+
26
+ /**
27
+ * init the config helper.
28
+ */
29
+ public function __construct()
30
+ {
31
+ $this->init();
32
+ }
33
+
34
+ /**
35
+ * init some config related stuff
36
+ */
37
+ protected function init()
38
+ {
39
+ // get remote config
40
+ $this->_remoteConfig = Mage::helper('shoptimally_core/remoteConfig');
41
+
42
+ // calculate enabled status
43
+ $this->_isEnabled = ($this->getGeneralSetting('ShoptimallyEnabled')) &&
44
+ (strlen($this->getApiKey()) > 0) &&
45
+ $this->_remoteConfig->get("enabled");
46
+
47
+ // init shoptimally domain
48
+ $this->_shoptimallyDomain = $this->_remoteConfig->get('shoptimally_domain');
49
+ if (is_null($this->_shoptimallyDomain) ||
50
+ strlen($this->_shoptimallyDomain) == 0)
51
+ {
52
+ $this->_shoptimallyDomain = "api1.shoptimally.com";
53
+ }
54
+ }
55
+
56
+ /**
57
+ * get current version
58
+ */
59
+ public function getVersion()
60
+ {
61
+ //return (string) Mage::getConfig()->getNode()->modules->Shoptimally_Core->version;
62
+ return self::SHOPTIMALLY_VERSION;
63
+ }
64
+
65
+ /**
66
+ * Return general setting val by name
67
+ * see system.xml for more info.
68
+ *
69
+ * @param $config_name is the config name relative to 'Shoptimally/GeneralSettings/'
70
+ */
71
+ public function getGeneralSetting($config_name)
72
+ {
73
+ return Mage::getStoreConfig('Shoptimally/GeneralSettings/' . $config_name, Mage::app()->getStore());
74
+ }
75
+
76
+ /**
77
+ * Return debug-related config.
78
+ *
79
+ * @param $configName is the debug config name.
80
+ * @param $default is value to return if debug config doesn't exist.
81
+ */
82
+ protected function getDebugSetting($configName, $default=false)
83
+ {
84
+ // get debug settings and if exist return it
85
+ $debugSettings = $this->_remoteConfig->get('debug', array());
86
+ if (array_key_exists($configName, $debugSettings))
87
+ {
88
+ return $debugSettings[$configName];
89
+ }
90
+
91
+ // return default value
92
+ return $default;
93
+ }
94
+
95
+ /**
96
+ * return the config dictionary for a specific feature
97
+ *
98
+ * @param $featureName - the unique identifier of the feature (for example, "FeaturedItems").
99
+ * @param $default - default value to return if not found.
100
+ */
101
+ public function getFeatureConfig($featureName, $default=array())
102
+ {
103
+ return $this->_remoteConfig->get('feature_' . $featureName, $default);
104
+ }
105
+
106
+ /**
107
+ * return if logging is enabled
108
+ * @param $level is which level of log to test (0 = fatal, 1=warning, 2 = log, 3 = debug)
109
+ */
110
+ public function isLogEnabled($level=2)
111
+ {
112
+ return $this->getDebugSetting("enable_log") == true &&
113
+ $this->getDebugSetting("log_level") >= $level;
114
+ }
115
+
116
+ /**
117
+ * Return if a given feature is enabled
118
+ *
119
+ * @param $featureName - string, feature name / identifier
120
+ * @return bool
121
+ */
122
+ public function isFeatureEnabled($featureName)
123
+ {
124
+ // first make sure shoptimally is generally enabled
125
+ if (!$this->getIsEnabled())
126
+ {
127
+ return false;
128
+ }
129
+
130
+ // get user data
131
+ $userData = Mage::helper('shoptimally_core/clientData');
132
+
133
+ // make sure we have valid user id
134
+ $userId = $userData->getUserId();
135
+ if (empty($userId))
136
+ {
137
+ return false;
138
+ }
139
+
140
+ // now check from features list from the user cookie
141
+ // this will give us the input from the server + the AB testing
142
+ return in_array($featureName, $userData->getEnabledFeatures());
143
+ }
144
+
145
+ /**
146
+ * Return the site api key
147
+ *
148
+ * @return string
149
+ */
150
+ public function getApiKey()
151
+ {
152
+ return $this->getGeneralSetting('ApiKey');
153
+ }
154
+
155
+ /**
156
+ * Return if Shoptimally currently enabled
157
+ *
158
+ * @return boolean
159
+ */
160
+ public function getIsEnabled()
161
+ {
162
+ return $this->_isEnabled;
163
+ }
164
+
165
+ /**
166
+ * Return the Shoptimally server url
167
+ *
168
+ * @return string
169
+ */
170
+ public function getServerUrl()
171
+ {
172
+ return $this->_shoptimallyDomain;
173
+ }
174
+
175
+ /**
176
+ * Return if the shoptimally js file should be loaded in async mode or not
177
+ *
178
+ * @return boolean
179
+ */
180
+ public function shouldLoadJsAsync()
181
+ {
182
+ return (!$this->_remoteConfig->get("javascript_synced"));
183
+ }
184
+
185
+ /**
186
+ * Return the client-js cdn url, for this specific site (based on configured url & api key)
187
+ *
188
+ * @return string
189
+ */
190
+ public function getJsUrl()
191
+ {
192
+ // get javascript from url and replace the <api-key> tag with our api key
193
+ $url = $this->_remoteConfig->get("javascript_url");
194
+ return str_replace("<api-key>", $this->getApiKey(), $url);
195
+ }
196
+ }
app/code/community/Shoptimally/Core/Helper/Cookie.php ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Helper functions to handle cookies
11
+ */
12
+ class Shoptimally_Core_Helper_Cookie extends Mage_Core_Helper_Abstract
13
+ {
14
+
15
+ /**
16
+ * get cookie value
17
+ * @param $name is cookie name to read
18
+ * @param $jsonDecode if true will also decode cookie as json
19
+ * @param $default default to return if cookie not found
20
+ * @return cookie as string or array, depends if you requested json decode
21
+ */
22
+ public function getCookie($name, $jsonDecode=true, $default=null)
23
+ {
24
+ // read cookie
25
+ $ret = Mage::getModel('core/cookie')->get($name);
26
+
27
+ // if cookie not found (eg null), return
28
+ if (is_null($ret) || strlen($ret) == 0){
29
+ return $default;
30
+ }
31
+
32
+ // decode if needed
33
+ if ($jsonDecode) {
34
+ try {
35
+ $ret = Mage::helper('core')->jsonDecode($ret);
36
+ }
37
+ catch (Exception $e) {
38
+ Mage::helper('shoptimally_core/log')->warn("Exception while parsing cookie '" . $name . "'!", $ret);
39
+ return null;
40
+ }
41
+ }
42
+
43
+ // return value
44
+ return $ret;
45
+ }
46
+
47
+ /**
48
+ * set cookie value
49
+ * @param $name is cookie name to read
50
+ * @param $value is the value to set
51
+ * @param $jsonEncode if true will encode value as json before setting the cookie
52
+ */
53
+ public function setCookie($name, $value, $jsonEncode=true)
54
+ {
55
+
56
+ // if requested, encode as json
57
+ if ($jsonEncode) {
58
+ $value = Mage::helper('core')->jsonEncode($value);
59
+ }
60
+
61
+ // set cookie
62
+ // getModel('core/cookie')->set($name, $value, $period, $path, $domain, $secure, $httponly);
63
+ return Mage::getModel('core/cookie')->set($name, $value, time()+86400, '/', NULL, false, false);
64
+ }
65
+ }
app/code/community/Shoptimally/Core/Helper/Data.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Required for admin config
11
+ */
12
+ class Shoptimally_Core_Helper_Data extends Mage_Core_Helper_Abstract
13
+ {
14
+ }
app/code/community/Shoptimally/Core/Helper/FeatureBase.php ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * The basic structure for a feature implementation.
11
+ * All Shoptimally features are implemented as helpers (even block-based features - they just wrap a helper),
12
+ * that inherit from this base class.
13
+ *
14
+ * This helps us reuse feature functionality and have a well-defined feature structure.
15
+ *
16
+ * What to do when inheriting from this class:
17
+ *
18
+ * 1. override 'const NAME' with feature name (case sensitive).
19
+ * 2. override '_runFeatureImp()' and put main logic inside.
20
+ * 2. for logging and config use the helpers '$this->_log' and '$this->_config'.
21
+ * 3. for ajax requests use '$this->sendAjax()'.
22
+ * 4. don't try-catch stuff, its already handled. you can crash freely.
23
+ * 5. if you choose to reject server's answer and not show the feature, report it using 'reportRejected()'
24
+ * 6. if you have any magento error that's not an exception but should be reported, use 'reportError()'.
25
+ * 7. at the end of the implementation if all goes well, report to server by using 'reportSuccess()'.
26
+ */
27
+ class Shoptimally_Core_Helper_FeatureBase extends Mage_Core_Helper_Abstract
28
+ {
29
+ // override this const with the feature name.
30
+ // this must match the feature name as defined on Shoptimally server etc.
31
+ const NAME = "FeatureName";
32
+
33
+ // will hold the required helpers that are loaded by default for all features
34
+ protected $_analytics = null;
35
+ protected $_config = null;
36
+ protected $_server = null;
37
+ protected $_log = null;
38
+
39
+ // to make sure a state was reported
40
+ private $_was_reported = false;
41
+
42
+ /**
43
+ * init all the required helpers
44
+ */
45
+ public function __construct()
46
+ {
47
+ // init all helpers
48
+ $this->_analytics = Mage::helper('shoptimally_analytics/featureEvents');
49
+ $this->_config = Mage::helper('shoptimally_core/config');
50
+ $this->_server = Mage::helper('shoptimally_core/server');
51
+ $this->_log = Mage::helper('shoptimally_core/log');
52
+
53
+ // generate unique feature request id (required for feature analytics)
54
+ try {
55
+ $this->_feature_event_id = $this->_generateFeatureEventId();
56
+ } catch(Exception $e) {
57
+ $this->_feature_event_id = "error";
58
+ }
59
+
60
+ // get feature-specific configuration
61
+ $this->_featureConfig = $this->_config->getFeatureConfig($this->getName());
62
+ }
63
+
64
+ /**
65
+ * generate a random string used as feature event id for analytics.
66
+ * */
67
+ private function _generateFeatureEventId($length=24) {
68
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
69
+ $charactersLength = strlen($characters);
70
+ $randomString = '';
71
+ for ($i = 0; $i < $length; $i++) {
72
+ $randomString .= $characters[rand(0, $charactersLength - 1)];
73
+ }
74
+ return $randomString;
75
+ }
76
+
77
+ /**
78
+ * get feature config (from the 'feature_<FeatureName>' section in the remote config file)
79
+ * @param name - config name.
80
+ * @param default - default to return if undefined.
81
+ * */
82
+ protected function getFeatureConfig($name, $default=null)
83
+ {
84
+ if (isset($this->_featureConfig[$name]))
85
+ {
86
+ return $this->_featureConfig[$name];
87
+ }
88
+ return $default;
89
+ }
90
+
91
+ /**
92
+ * return the name of this feature
93
+ * */
94
+ public function getName()
95
+ {
96
+ return static::NAME;
97
+ }
98
+
99
+ /**
100
+ * send request to Shoptimally server and return response.
101
+ * - if failed, will report feature error and return null.
102
+ * - if timedout, will report feature timeout and return null.
103
+ *
104
+ * this function expect to get response in the Shoptimally feature response format, eg a dictionary
105
+ * with metadata and the actual result in "result".
106
+ * so this function returns the part that is inside the "result", but if key is not present will just
107
+ * return the http response (to support old APIs or ajax to urls that are not feature actions).
108
+ * */
109
+ protected function sendAjax($url, $data, $timeout=1)
110
+ {
111
+ // add feature event id to data
112
+ if (!is_null($data))
113
+ {
114
+ $data["feature_event_id"] = $this->_feature_event_id;
115
+ }
116
+
117
+ // send the request
118
+ $response = $this->_server->sendRequest($url, $data, $timeout);
119
+
120
+ // if exception happened:
121
+ if (is_null($response) || $response->isError()) {
122
+
123
+ // report about a timeout
124
+ if ($this->_server->getLastErrorMessage() == "Unable to read response, or response is empty")
125
+ {
126
+ $this->reportTimeout();
127
+ }
128
+ // report about other errors
129
+ else
130
+ {
131
+ $this->reportError("Error in ajax! url: '" . $url . "'. Error: " . $this->_server->getLastErrorMessage());
132
+ }
133
+
134
+ // return null
135
+ return null;
136
+ }
137
+
138
+ // no error, time to return response!
139
+
140
+ // if there's a "result" key in response, return the result (it means its a valid feature response)
141
+ if (array_key_exists("_result", $response))
142
+ {
143
+ return $response["_result"];
144
+ }
145
+ // if no result key, just return the whole response
146
+ else
147
+ {
148
+ return $response;
149
+ }
150
+ }
151
+
152
+ private function _doActualReport($status, $extraData=null)
153
+ {
154
+ // default extra data
155
+ if (is_null($extraData)) {$extraData = array();}
156
+
157
+ // add feature event id to extra data
158
+ $extraData['feature_event_id'] = $this->_feature_event_id;
159
+
160
+ // add event to send
161
+ $analytics = $this->_analytics;
162
+ $this->_analytics->report($this->getName(), $status, $extraData);
163
+
164
+ // set that was reported successfully
165
+ $this->_was_reported = true;
166
+ }
167
+
168
+ /**
169
+ * report failure of this feature (call this on error and exceptions)
170
+ * @param $msg - fail message.
171
+ * @param $extraData - any extra data to add to the report.
172
+ * */
173
+ protected function reportError($msg)
174
+ {
175
+ // report warning to log and shoptimally analytics
176
+ $this->_log->warn("'" . $this->getName() . "' Failed to run. reason: " . $msg);
177
+ $analytics = $this->_analytics;
178
+ $this->_doActualReport($analytics::STATUS_ERROR);
179
+ }
180
+
181
+ /**
182
+ * report failure of this feature due to timeout.
183
+ * */
184
+ protected function reportTimeout()
185
+ {
186
+ // report warning to log and shoptimally analytics
187
+ $this->_log->warn("'" . $this->getName() . "' got timeout!");
188
+ $analytics = $this->_analytics;
189
+ $this->_doActualReport($analytics::STATUS_TIMEOUT);
190
+ }
191
+
192
+ /**
193
+ * report rejected - when we got answer from server and everything was ok, but we chose not to show it
194
+ * at this time. for example, this happens if we get too few items to show in featured items.
195
+ * */
196
+ protected function reportRejected()
197
+ {
198
+ // report warning to log and shoptimally analytics
199
+ $analytics = $this->_analytics;
200
+ $this->_doActualReport($analytics::STATUS_REJECTED);
201
+ }
202
+
203
+ /**
204
+ * report success - when this feature was successfully displayed and worked.
205
+ * @param $extraData - any extra data to add to the report.
206
+ * */
207
+ protected function reportSuccess($extraData=null)
208
+ {
209
+ // report warning to log and shoptimally analytics
210
+ $analytics = $this->_analytics;
211
+ $this->_doActualReport($analytics::STATUS_OK, $extraData);
212
+ }
213
+
214
+ /**
215
+ * report success + products replacement. This is to report success and update the original-items and
216
+ * acutllay-replaced-to items lists.
217
+ * @param $originalItems - collection of original items.
218
+ * @param $resultItems - collection of actual result items.
219
+ * */
220
+ protected function reportSuccessReplacement($originalItems=null, $resultItems=null)
221
+ {
222
+ $data = array();
223
+ if (!is_null($originalItems)) {$data["original_items"] = $originalItems->getAllIds();}
224
+ if (!is_null($resultItems)) {$data["result_items"] = $resultItems->getAllIds();}
225
+ $this->reportSuccess($data);
226
+ }
227
+
228
+ /**
229
+ * return if this feature is currently enabled
230
+ */
231
+ public function isEnabled()
232
+ {
233
+ $config = $this->_config;
234
+ return (($config->isFeatureEnabled($this->getName())) &&
235
+ ($config->getIsEnabled()));
236
+ }
237
+
238
+ /**
239
+ * this function will be called if feature is disabled.
240
+ * this might be required for features that are blocks, and we want to put
241
+ * a default placeholder or an empty block when disabled
242
+ * */
243
+ protected function _runIfDisabled()
244
+ {
245
+ }
246
+
247
+ /**
248
+ * run the feature.
249
+ * @param $data - any required data for the execution of the feature (optional)
250
+ * @return true if run with no errors, false if didn't run (disabled) or had an exception or problem.
251
+ * */
252
+ public function runFeature($data=null)
253
+ {
254
+ try
255
+ {
256
+ // if enabled, execute feature
257
+ if ($this->isEnabled())
258
+ {
259
+ $this->_runFeatureImp($data);
260
+ return true;
261
+ }
262
+ // if not enabled:
263
+ else
264
+ {
265
+ $this->_runIfDisabled();
266
+ return false;
267
+ }
268
+ }
269
+ catch(Exception $e)
270
+ {
271
+ $this->reportError($e->getMessage());
272
+ return false;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * this is the internal feature impelemnt function.
278
+ * every feature should impelemnt all the main logic in here.
279
+ *
280
+ * Remember - when this function runs everything is already wrapped in try-catch, that also report
281
+ * to features analytics. so don't try-catch things inside, and remember to use reportError() on problems
282
+ * that are not exception, and reportRejected() if deciding this feature should not show anything at this time.
283
+ *
284
+ * also, remember to call reportSuccess() at the end!
285
+ * */
286
+ protected function _runFeatureImp($data)
287
+ {
288
+ $this->_log->warn("'" . $this->getName() . "' main function not implemented!");
289
+ }
290
+ }
app/code/community/Shoptimally/Core/Helper/HandleFatals.php ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // this code snippet catches fatal errors and log them right before
4
+ // NOTE!!! this code doesn't catch just Shoptimally fatals, it caches ANY fatal.
5
+ // because of that we report it as general fatal exception and not as shoptimally log.
6
+ // in addition if you see this report in action it doesn't necessarily means Shoptimally has a problem.
7
+ // it might be another module.
8
+ function ShoptimallyLogFatalErrors()
9
+ {
10
+ $error = error_get_last();
11
+ if (!is_null($error))
12
+ {
13
+ // skip this fatal as its built-in in magento (its actually a notice but called a lot when
14
+ // working localhost and if in dev mode this might generate fake fatals. so we don't want to
15
+ // spam tests).
16
+ if (strpos ($error["message"], "vsprintf(): Too few arguments") === 0)
17
+ {
18
+ return;
19
+ }
20
+
21
+ // report fatal
22
+ Mage::helper('shoptimally_core/log')->fatal("warning", $error);
23
+ }
24
+ }
25
+ register_shutdown_function("ShoptimallyLogFatalErrors");
26
+
27
+ class Shoptimally_Core_Helper_HandleFatals extends Mage_Core_Helper_Abstract
28
+ {
29
+ }
app/code/community/Shoptimally/Core/Helper/Log.php ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Wrap magento log so we can easily disable/enable all logs from Shoptimally.
11
+ */
12
+ class Shoptimally_Core_Helper_Log extends Mage_Core_Helper_Abstract
13
+ {
14
+ // how many characters of last logs to keep
15
+ const LAST_LOGS_CHAR_COUNT = 5000;
16
+ const LOGS_CACHE_SEPERATOR = "---|---";
17
+
18
+ // severity level for different type of logs
19
+ const SEVERITY_FATAL = 0;
20
+ const SEVERITY_WARN = 1;
21
+ const SEVERITY_LOG = 2;
22
+ const SEVERITY_DEBUG = 3;
23
+
24
+ /*
25
+ * return last part of last logs (LAST_LOGS_CHAR_COUNT characters of log)
26
+ * */
27
+ public function getLastLogs()
28
+ {
29
+ $ret = Mage::helper('shoptimally_core/storage')->get("last_logs");
30
+ return explode(self::LOGS_CACHE_SEPERATOR, $ret);
31
+ }
32
+
33
+
34
+ /*
35
+ * get the class name of the caller.
36
+ * */
37
+ private function getCallingClass()
38
+ {
39
+
40
+ //get the trace
41
+ $trace = debug_backtrace();
42
+
43
+ // Get the class that is asking for who awoke it
44
+ // note: 3 is because: caller -> log.debug/log/warn() -> log._writeLog() -> log.formatReport()
45
+ $class = $trace[3]['class'];
46
+
47
+ // +1 to i cos we have to account for calling this function
48
+ for ( $i=1; $i<count( $trace ); $i++ ) {
49
+ if ( isset( $trace[$i] ) ) // is it set?
50
+ if ( $class != $trace[$i]['class'] ) // is it a different class
51
+ return $trace[$i]['class'];
52
+ }
53
+ }
54
+
55
+
56
+ /**
57
+ * debug logs will only appear in debug mode (system.log)
58
+ * @param $text is text to log
59
+ * @param $data is optional object to dump right after log
60
+ */
61
+ public function debug($text, $data=null)
62
+ {
63
+ // tbd this will be a good place to check if in debug mode before calling to log.
64
+ $this->_writeLog($text, $data, self::SEVERITY_DEBUG);
65
+ }
66
+
67
+ /**
68
+ * warnings logs will always appear in system.log
69
+ * @param $text is text to log
70
+ * @param $data is optional object to dump right after log
71
+ */
72
+ public function warn($text, $data=null)
73
+ {
74
+ // tbd this will be a good place to check if in debug mode before calling to log.
75
+ $this->_writeLog($text, $data, self::SEVERITY_WARN);
76
+ }
77
+
78
+ /**
79
+ * warnings logs will always appear in system.log
80
+ * @param $text is text to log
81
+ * @param $data is optional object to dump right after log
82
+ */
83
+ public function log($text, $data=null)
84
+ {
85
+ // tbd this will be a good place to check if in debug mode before calling to log.
86
+ $this->_writeLog($text, $data, self::SEVERITY_LOG);
87
+ }
88
+
89
+ /**
90
+ * actually do the log write.
91
+ * @param $text is text to log
92
+ * @param $data is optional object to dump right after log
93
+ * @param $severity - log sevirity level
94
+ */
95
+ protected function _writeLog($text, $data, $severity)
96
+ {
97
+ // if log disabled skip
98
+ if (!Mage::helper('shoptimally_core/config')->isLogEnabled($severity))
99
+ {
100
+ return;
101
+ }
102
+
103
+ // format text before writing it
104
+ $severityNames = array("fatal", "warning", "log", "debug");
105
+ $text = $this->formatReport($text, $data, $severityNames[$severity]);
106
+
107
+ // write to log
108
+ Mage::log($text);
109
+
110
+ // add to cache of logs
111
+ $this->writeToCachedLog($text);
112
+ }
113
+
114
+ /**
115
+ * write log to our cached last logs
116
+ * */
117
+ protected function writeToCachedLog($text)
118
+ {
119
+ $logsHistory = Mage::helper('shoptimally_core/storage')->get("last_logs");
120
+ $logsHistory = $text . self::LOGS_CACHE_SEPERATOR . $logsHistory;
121
+ $logsHistory = substr($logsHistory, 0, self::LAST_LOGS_CHAR_COUNT);
122
+ Mage::helper('shoptimally_core/storage')->set("last_logs", $logsHistory);
123
+ }
124
+
125
+ /**
126
+ * get text and data, add prefix etc and format the string for the actual report.
127
+ * $text - text to send
128
+ * $data - attached data object
129
+ * $logType - debug / log / warn / ...
130
+ * */
131
+ protected function formatReport($text, $data, $logType)
132
+ {
133
+ // get class name
134
+ $className = $this->getCallingClass();
135
+
136
+ // if no class name set "shoptimally"
137
+ if (is_null($className) || strlen($className) == 0)
138
+ {
139
+ $className = "global";
140
+ }
141
+ // if got class name shorten it a bit
142
+ else
143
+ {
144
+ $className = str_replace("_Helper", "", $className);
145
+ $className = str_replace("_Model", "", $className);
146
+ $className = str_replace("Shoptimally_", "", $className);
147
+ }
148
+
149
+ // get date
150
+ $date = date("m-d H:i:s");
151
+
152
+ // add prefix to report
153
+ $text = "[Shoptimally-" . $logType . "][" . $className . "] " . $date . " >> " . $text;
154
+
155
+ // add data if exist
156
+ if (!is_null($data))
157
+ {
158
+ // if data is exception get its message
159
+ if (is_subclass_of($data, 'Exception') || method_exists($data, "getMessage"))
160
+ {
161
+ $text = $text . " -- " . $data->getMessage();
162
+ }
163
+ else
164
+ {
165
+ $text = $text . "\r\n" . Mage::helper('core')->jsonEncode($data);
166
+ }
167
+ }
168
+
169
+ // return result
170
+ return $text;
171
+ }
172
+
173
+ /**
174
+ * report fatal log.
175
+ * note: this will be reported to Shoptimally log only.
176
+ *
177
+ * @param $text is text to log
178
+ * @param $data is optional object to dump right after log
179
+ *
180
+ * */
181
+ public function fatal($text, $data=null)
182
+ {
183
+ // if log disabled skip
184
+ if (!Mage::helper('shoptimally_core/config')->isLogEnabled(self::SEVERITY_FATAL))
185
+ {
186
+ return;
187
+ }
188
+
189
+ // add prefix
190
+ $text = date("m-d H:i:s") . " " . $text;
191
+
192
+ // add data if exist
193
+ if (!empty($data))
194
+ {
195
+ $text = $text . " " . Mage::helper('core')->jsonEncode($data);
196
+ }
197
+
198
+ // format text and add to cached log
199
+ Mage::log($text);
200
+ $this->writeToCachedLog($text);
201
+ }
202
+ }
app/code/community/Shoptimally/Core/Helper/ObjectUtils.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Utilities and helper functions to work with arrays and other objects
11
+ */
12
+ class Shoptimally_Core_Helper_ObjectUtils extends Mage_Core_Helper_Abstract
13
+ {
14
+ /**
15
+ * get from array with default value (if key doesn't exist will return default).
16
+ * this should never cause exception or fatal.
17
+ * @param $array - array to get from.
18
+ * @param $key - key to search and get.
19
+ * @param $default - value to return if not found, default to null.
20
+ * @return - either value from array, or default if not found
21
+ * */
22
+ public function array_get($array, $key, $default=null)
23
+ {
24
+ if (array_key_exists($key, $array)) {
25
+ return $array[$key];
26
+ }
27
+ return $default;
28
+ }
29
+
30
+ /**
31
+ * extract one array from another, based on list of keys.
32
+ * for example, if you have one array with keys (a,b,c,d) and you want
33
+ * to extract only keys and values of a, b, this function helps you do that.
34
+ * in addition it support default values for keys that doesn't exist.
35
+ * @param $srcArray - array to extract from.
36
+ * @param $keys - array of keys to extract.
37
+ * @param $default - value to use if key not found.
38
+ * @return - extracted array.
39
+ * */
40
+ public function array_extract($srcArray, $keys, $default=null)
41
+ {
42
+ $ret = array();
43
+ foreach ($keys as $key)
44
+ {
45
+ $ret[$key] = $this->array_get($srcArray, $key, $default);
46
+ }
47
+ return $ret;
48
+ }
49
+ }
app/code/community/Shoptimally/Core/Helper/PageInfo.php ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Return information about current page (only works when called from user
11
+ * requests, not from cronjobs or global events).
12
+ */
13
+ class Shoptimally_Core_Helper_PageInfo extends Mage_Core_Helper_Abstract
14
+ {
15
+ // contain the ids of all products on current page
16
+ // this is filled by _setProductIdsOnPage(), which is called from the observer
17
+ protected $_productIdsOnPage = array();
18
+
19
+ // set the product ids on current page
20
+ // should be called only from the observer when products list is loaded
21
+ public function _setProductIdsOnPage($prodctIds)
22
+ {
23
+ $this->_productIdsOnPage = $prodctIds;
24
+ }
25
+
26
+ /**
27
+ * return a list with all the product ids on current page
28
+ * */
29
+ public function getProductIds()
30
+ {
31
+ return $this->_productIdsOnPage;
32
+ }
33
+
34
+
35
+ /**
36
+ * return the main product on this page (its instance)
37
+ *
38
+ * @return product instance.
39
+ * */
40
+ public function getMainProduct()
41
+ {
42
+ return Mage::registry('current_product');
43
+ }
44
+
45
+ /**
46
+ * return the main product on this page (for example when viewing a specific item), or null
47
+ * if this page does not feature one main product.
48
+ *
49
+ * @return product id.
50
+ * */
51
+ public function getMainProductId()
52
+ {
53
+ $prod = $this->getMainProduct();
54
+ if (is_null($prod)) {return null;}
55
+ return $prod->getId();
56
+ }
57
+
58
+ /**
59
+ * return a dictionary with all the page basic info
60
+ * contains: category, index (if category view), and type
61
+ */
62
+ public function getBasicInfo()
63
+ {
64
+ // return data
65
+ $ret = array(
66
+ "category" => $this->getCategory(),
67
+ "type" => $this->getPageType(),
68
+ "index" => $this->getIndex(),
69
+ "main_product_id" => $this->getMainProductId(),
70
+ // product ids comes in the buttom block because its not yet loaded in header.
71
+ //"products" => $this->getProductIds(),
72
+ );
73
+
74
+ // split category to name and id
75
+ if (!empty($ret["category"]))
76
+ {
77
+ $ret["category_id"] = $ret["category"]->getId();
78
+ $ret["category"] = $ret["category"]->getName();
79
+ }
80
+ return $ret;
81
+ }
82
+
83
+ /**
84
+ * get current page category (or null if not exist for this page)
85
+ */
86
+ public function getCategory()
87
+ {
88
+ return Mage::registry('current_category');
89
+ }
90
+
91
+ /**
92
+ * return general data
93
+ */
94
+ public function getData()
95
+ {
96
+ return array(
97
+ "controller_name" => Mage::app()->getRequest()->getControllerName(),
98
+ "action_name" => Mage::app()->getRequest()->getActionName(),
99
+ "route_name" => Mage::app()->getRequest()->getRouteName(),
100
+ "module_name" => Mage::app()->getRequest()->getModuleName(),
101
+ );
102
+ }
103
+
104
+ /**
105
+ * if browsing category pages, return the index of the current page
106
+ */
107
+ public function getIndex()
108
+ {
109
+ return Mage::getBlockSingleton('page/html_pager')->getCurrentPage();
110
+ }
111
+
112
+ /**
113
+ * return the type of the current page.
114
+ *
115
+ * @return string: "cms" / "cms_home" / "product" / "category" / "cart"
116
+ */
117
+ public function getPageType()
118
+ {
119
+
120
+ $product = Mage::registry('current_product');
121
+ $category = Mage::registry('current_category');
122
+
123
+ if ($product && $product->getId()) {
124
+ // The current page is a product page.
125
+ // If you only want the main product detail page, also check for
126
+ // Mage::app()->getFrontController()->getAction()->getFullActionName() == 'catalog_product_view'
127
+ // Be aware that a current_product and a current_category can be set at the same time.
128
+ // In that case the visitor is viewing a product in a category.
129
+ return "product";
130
+
131
+ } elseif ($category && $category->getId()) {
132
+ // The current page is a category page
133
+ // If you only want the category list page, also check for
134
+ // Mage::app()->getFrontController()->getAction()->getFullActionName() == 'catalog_category_view'
135
+ return "category";
136
+ }
137
+
138
+ // Check for cart page
139
+ if (Mage::app()->getFrontController()->getAction()->getFullActionName() == 'checkout_cart_index') {
140
+ return "cart";
141
+ }
142
+
143
+ // Check if it's a CMS page:
144
+ $page = Mage::getSingleton('cms/page');
145
+ if ($page->getId()) {
146
+ // The current page is a CMS page
147
+
148
+ if ($page->getIdentifier() == Mage::getStoreConfig('web/default/cms_home_page')) {
149
+ return "cms_home";
150
+ }
151
+ return "cms";
152
+ }
153
+
154
+ // unknown type
155
+ return "unknown";
156
+ }
157
+ }
app/code/community/Shoptimally/Core/Helper/ProductsUtils.php ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * General products and products-collection utilities.
11
+ */
12
+ class Shoptimally_Core_Helper_ProductsUtils extends Mage_Core_Helper_Abstract
13
+ {
14
+ /**
15
+ * insert a list of products into the begining of a given collection
16
+ *
17
+ * @param $collection - collection to insert products into.
18
+ * @param $newProducts - products to insert.
19
+ *
20
+ * Note! if product already exist in collection it will not push it twice,
21
+ * it will just move it to the begining of the list
22
+ */
23
+ public function insertProducts($collection, $newProducts)
24
+ {
25
+ // store all previous products in $oldProducts
26
+ $oldProducts = $collection->getItems();
27
+
28
+ // remove all previous products
29
+ // note: using clear() raise exception
30
+ foreach ($collection as $key => $item) {
31
+ $collection->removeItemByKey($key);
32
+ }
33
+
34
+ // insert the new products
35
+ foreach ($newProducts as $product) {
36
+ $collection->addItem($product);
37
+ }
38
+
39
+ // re-add the original products after the new products
40
+ foreach ($oldProducts as $oldProduct) {
41
+
42
+ // make sure product don't exist in new products list
43
+ $skip = false;
44
+ foreach ($newProducts as $newProduct)
45
+ {
46
+ if ($newProduct->getId() == $oldProduct->getId())
47
+ {
48
+ $skip = true;
49
+ break;
50
+ }
51
+ }
52
+ if ($skip) continue;
53
+
54
+ // add the old product back into collection
55
+ $collection->addItem($oldProduct);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * totally replace all the products in the given collection with $newProducts
61
+ *
62
+ * @param $collection - collection to replace products.
63
+ * @param $newProducts - products to insert.
64
+ *
65
+ */
66
+ public function replaceProducts($collection, $newProducts)
67
+ {
68
+ // remove all previous products
69
+ // note: using clear() raise exception
70
+ foreach ($collection as $key => $item) {
71
+ $collection->removeItemByKey($key);
72
+ }
73
+
74
+ // insert the new products
75
+ foreach ($newProducts as $product) {
76
+ $collection->addItem($product);
77
+ }
78
+
79
+ }
80
+
81
+ /**
82
+ * get product instance from id
83
+ *
84
+ * @param $id - product id to get
85
+ * @return product instance
86
+ */
87
+ public function getProduct($id)
88
+ {
89
+ $product = Mage::getModel('catalog/product');
90
+ $product->load($id);
91
+ return $product;
92
+ }
93
+
94
+ /**
95
+ * get a list of products from a list of ids
96
+ *
97
+ * @param $idsList - list of ids to get.
98
+ * @param $loadAll - if true will also select * and call collection->load();
99
+ * @return collection of products.
100
+ * note: if one or more ids do not exist, they will not be included in the returned list.
101
+ */
102
+ public function getProducts($idsList, $loadAll=true)
103
+ {
104
+ // get collection
105
+ $ret = Mage::getModel('catalog/product')->getCollection()
106
+ ->addAttributeToFilter('entity_id', array('in' => $idsList));
107
+
108
+ // load all attributes
109
+ if ($loadAll)
110
+ {
111
+ $ret = $ret->addAttributeToSelect('*')->load();
112
+ }
113
+
114
+ // return collection
115
+ return $ret;
116
+ }
117
+
118
+ /**
119
+ * get a list of all associated product ids for given product.
120
+ * associated ids can be children for configurable product, products in bundle, etc..
121
+ *
122
+ * @return either a list with associated ids, or null if not relevant.
123
+ * */
124
+ private function getAssociatedIds($product)
125
+ {
126
+ // get accociated products if grouped or configurable product
127
+ $associatedProductIds = array();
128
+ switch ($product->getTypeId())
129
+ {
130
+ // get associated products for grouped product
131
+ case "grouped":
132
+ $associated = $product->getTypeInstance(true)->getAssociatedProducts($product);
133
+ if (!is_null($associated) && $associated)
134
+ {
135
+ foreach ($associated as $associate)
136
+ {
137
+ array_push($associatedProductIds, $associate->getId());
138
+ }
139
+ }
140
+ break;
141
+
142
+ // get products in bundle
143
+ case "bundle":
144
+ $associatedProductIds = $product->getTypeInstance(true)->getOptionsIds($product);
145
+ break;
146
+
147
+ // get children for configurable
148
+ case "configurable":
149
+ $associated = Mage::getModel('catalog/product_type_configurable')->getUsedProducts(null, $product);
150
+ if (!is_null($associated) && $associated)
151
+ {
152
+ foreach ($associated as $associate)
153
+ {
154
+ array_push($associatedProductIds, $associate->getId());
155
+ }
156
+ }
157
+ break;
158
+
159
+ // unrelevant type
160
+ default:
161
+ return null;
162
+ }
163
+
164
+ // return the associated ids
165
+ return $associatedProductIds;
166
+ }
167
+
168
+ /**
169
+ * return a dictionary with all the interesting info of a product
170
+ */
171
+ public function getProductFullData($product)
172
+ {
173
+ // get url utils helper
174
+ $urlUtils = Mage::helper('shoptimally_core/urlUtils');
175
+
176
+ // this is to get parent product ids later
177
+ $product->loadParentProductIds();
178
+
179
+ // get product type and associated ids
180
+ $productTypeId = $product->getTypeId();
181
+ $associatedProductIds = $this->getAssociatedIds($product);
182
+
183
+ // all basic fields we want to get
184
+ // key is the field name in output array
185
+ // value is array of (funcName, defaultValue, exceptionValue)
186
+ $fields = array(
187
+ 'unique_id' => array('getId', "invalid id", "ERROR"),
188
+ 'item_name' => array('getName', "", "ERROR"),
189
+ 'url' => array('getProductUrl', "", "ERROR"),
190
+ 'price' => array('getFinalPrice', -1, -2),
191
+ 'parent_ids' => array('getParentProductIds', array(), "ERROR"),
192
+ 'category_ids' => array('getCategoryIds', array(), "ERROR"),
193
+ 'original_price' => array('getPrice', -1, -2),
194
+ 'special_price' => array('getSpecialPrice', -1, -2),
195
+ 'weight' => array('getWeight', 0, 0),
196
+ 'sku' => array('getSku', null, "ERROR"),
197
+ 'is_in_stock' => array('isInStock', false, false),
198
+ 'created_at' => array('getCreatedAt', null, "ERROR"),
199
+ 'updated_at' => array('getUpdatedAt', null, "ERROR"),
200
+ 'image_url' => array('getImageUrl', "", "ERROR"),
201
+ 'short_description' => array('getShortDescription', "", "ERROR"),
202
+ 'status' => array('getStatus', 2, "ERROR"),
203
+ );
204
+
205
+ // get all fields
206
+ $ret = array();
207
+ foreach ($fields as $fieldName => $fieldData)
208
+ {
209
+ $ret[$fieldName] = $this->_getProdValSafe($product, $fieldData[0], $fieldData[1], $fieldData[2]);
210
+ }
211
+
212
+ // some extra field processing
213
+ $ret['url'] = $urlUtils->toRelative($ret['url']);
214
+ $ret['price'] = intval($ret['price']);
215
+ $ret['special_price'] = intval($ret['special_price']);
216
+ $ret['original_price'] = intval($ret['original_price']);
217
+ $ret['price'] = intval($ret['price']);
218
+ $ret['associated_products'] = $associatedProductIds;
219
+ $ret['product_type'] = $productTypeId;
220
+
221
+ // determine if product is visible in store
222
+ try {
223
+ $ret['is_in_store'] = $this->isProductVisible($product);
224
+ }
225
+ catch (Exception $e) {
226
+ $ret['is_in_store'] = false;
227
+ }
228
+
229
+ // get stock item data
230
+ try
231
+ {
232
+ $stockItem = $product->getStockItem();
233
+ if (!is_null($stockItem))
234
+ {
235
+ $ret['is_stock_item_in_stock'] = $stockItem->getIsInStock();
236
+ }
237
+ }
238
+ catch (Exception $e) {$ret['is_stock_item_in_stock'] = "ERROR";}
239
+
240
+ // get the extra fields from remote config
241
+ $extraFields = Mage::helper('shoptimally_core/remoteConfig')->get("extra_product_fields", array());
242
+ foreach ($extraFields as $extra)
243
+ {
244
+ $ret[$extra] = $this->getProductAttr($product, $extra);
245
+ }
246
+
247
+ return $ret;
248
+ }
249
+
250
+ /*
251
+ * This code requires some explaination:
252
+ * Basically we have a safe method to get product attribute while testing if they exist first.
253
+ * However, what about Magento built-ins?
254
+ * For example, the function getImageUrl() can throw exception if there are no images.
255
+ * there are hundreds of special cases, depending on the state of the product and what it has, and I
256
+ * don't trust Magento to not raise exceptions on things instead of returning null.
257
+ * so this function wraps up getting product data from function in a safe way.
258
+ * @param $product - product instance.
259
+ * @param $function - the name of the function (string) to get.
260
+ * @param $defaultValue - optional value if getting null.
261
+ * @return - either the value, or $onException value if had an exception.
262
+ * */
263
+ protected function _getProdValSafe($product, $function, $defaultValue=null, $onException="[error]")
264
+ {
265
+ try
266
+ {
267
+ $ret = $product->{$function}();
268
+ if (is_null($ret))
269
+ {
270
+ return $defaultValue;
271
+ }
272
+ return $ret;
273
+ }
274
+ catch (Exception $e)
275
+ {
276
+ return $onException;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * get a single attribute from a product as text.
282
+ * @param $product - the product to get attribute from.
283
+ * @param $attr - the attribute to get.
284
+ * @param $default - return value if non existence
285
+ * */
286
+ public function getProductAttr($product, $attr, $default=null)
287
+ {
288
+ // get attribute if existing
289
+ $attribute = $product->getResource()->getAttribute($attr);
290
+ if ($attribute)
291
+ {
292
+ return $attribute->getFrontend()->getValue($product);
293
+ }
294
+
295
+ // return default
296
+ return $default;
297
+ }
298
+
299
+ /**
300
+ * get all cart items
301
+ * @return list of items from cart (as received from magento)
302
+ * these are quota items, not products.
303
+ */
304
+ public function getCartItems()
305
+ {
306
+ // note: getAllVisibleItems() return only the added items without parents
307
+ // getAllItems() return ALL quota items.
308
+ return Mage::getModel('checkout/cart')->getQuote()->getAllItems();
309
+ }
310
+
311
+ /**
312
+ * get the total price of the cart, with tax and discounts etc included.
313
+ */
314
+ public function getCartTotal()
315
+ {
316
+ $quote = Mage::getModel('checkout/session')->getQuote();
317
+ $quoteData = $quote->getData();
318
+ if (array_key_exists('grand_total', $quoteData))
319
+ {
320
+ return $quoteData['grand_total'];
321
+ }
322
+ else
323
+ {
324
+ return 0;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * get all attributes of a product.
330
+ * you can later use it like this:
331
+ *
332
+ * $attributes = $prodUtils->getAllAttributes($product));
333
+ * foreach ($attributes as $attribute) {
334
+ * _log($attribute->getName() . " = " . $attribute->getFrontend()->getValue($product));
335
+ * }
336
+ */
337
+ public function getAllAttributes($product)
338
+ {
339
+ return $product->getAttributes();
340
+ }
341
+
342
+ /**
343
+ * return true only if product is truely visible to customers, eg in catalog
344
+ * or search, in stock, is valid etc.
345
+ */
346
+ public function isProductVisible($product)
347
+ {
348
+ return $product->isVisibleInCatalog() && $product->isVisibleInSiteVisibility() &&
349
+ $product->isInStock();
350
+ }
351
+ }
app/code/community/Shoptimally/Core/Helper/RemoteConfig.php ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Remote config is a remote config file that updates automatically once every X minutes, and its goal
11
+ * is to let Shoptimally control some of the settings remotely, without bothering the shop owner.
12
+ */
13
+ class Shoptimally_Core_Helper_RemoteConfig extends Mage_Core_Helper_Abstract
14
+ {
15
+ // will hold global and local configs once loaded
16
+ protected $_config = array();
17
+
18
+ // Shoptimally cdn domain
19
+ //const CDN_DOMAIN = "cdn.shoptimally.com";
20
+ const CDN_DOMAIN = "s3-eu-west-1.amazonaws.com/shoptimally-ire";
21
+
22
+ /**
23
+ * load config from storage
24
+ */
25
+ public function __construct()
26
+ {
27
+ // first set empty config
28
+ $this->_config = array();
29
+
30
+ // now try to load config from storage
31
+ $this->loadConfig("local");
32
+ $this->loadConfig("global");
33
+
34
+ // in case a config was not loaded
35
+ if (is_null($this->_config["local"])) {$this->_config["local"] = array();}
36
+ if (is_null($this->_config["global"])) {$this->_config["global"] = array();}
37
+ }
38
+
39
+ /*
40
+ * for debug purposes, get all config dict
41
+ * */
42
+ public function _getAll()
43
+ {
44
+ return $this->_config;
45
+ }
46
+
47
+ /**
48
+ * get config value.
49
+ * @param $key - the config key.
50
+ * @param $default - returned if value does not exist
51
+ */
52
+ public function get($key, $default=null)
53
+ {
54
+ // if local config exist for this key, return it
55
+ if (array_key_exists ($key, $this->_config['local']))
56
+ {
57
+ return $this->_config['local'][$key];
58
+ }
59
+
60
+ // else, return from the global config
61
+ if (array_key_exists ($key, $this->_config['global']))
62
+ {
63
+ return $this->_config['global'][$key];
64
+ }
65
+
66
+ // value doesn't exist in global OR local? return $default
67
+ return $default;
68
+ }
69
+
70
+ /**
71
+ * attempt to load config from local storage
72
+ */
73
+ protected function loadConfig($configType)
74
+ {
75
+ // get storage helper
76
+ $storage = Mage::helper('shoptimally_core/storage');
77
+
78
+ // get from storage and try to parse and set
79
+ try
80
+ {
81
+ $newConfig = $storage->get($configType . "_config", "[]");
82
+ $this->_config[$configType] = Mage::helper('core')->jsonDecode($newConfig);
83
+ }
84
+ catch (Exception $e)
85
+ {
86
+ Mage::log("Shoptimally: Invalid format in '" . $configType . "' config file from storage!", $newConfig);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Update from the remote config file from the cdn.
92
+ * This is called every X minutes by the cronejob.
93
+ * @param $callback - optional function to call when done / fail.
94
+ * function get ($type, $succeed, $reason) as params:
95
+ * $type - local / global (it will be called twice).
96
+ * $succeed - was this config type loaded successfully (bool).
97
+ * $reason - if failed, reason.
98
+ */
99
+ public function updateFromCdn($callback=null)
100
+ {
101
+ // get global config
102
+ $this->fetchConfigFile("global", $callback);
103
+
104
+ // get local config
105
+ $this->fetchConfigFile("local", $callback);
106
+ }
107
+
108
+ /**
109
+ * fetch and prase config file.
110
+ * Note: if failed to get config from CDN we just continue with last config.
111
+ * maybe in the future we would like to implement some mechanism that after X fails we turn oursevels disabled,
112
+ * but currently we don't need it.
113
+ * @param $configType - either "global" or "local".
114
+ * @param $callback - optional function to call when done / fail. see updateFromCdn() docs for more info.
115
+ */
116
+ protected function fetchConfigFile($configType, $callback=null)
117
+ {
118
+ // get required helpers
119
+ $log = Mage::helper('shoptimally_core/log');
120
+ $storage = Mage::helper('shoptimally_core/storage');
121
+ $server = Mage::helper('shoptimally_core/server');
122
+ $cdn = self::CDN_DOMAIN;
123
+
124
+ // to remove annoying "if (!is_null($callback)) {...}" all over the place
125
+ if (is_null($callback))
126
+ {
127
+ $callback = function($type, $succeed, $errMsg) {};
128
+ }
129
+
130
+ try {
131
+
132
+ // get url based on type of file
133
+ switch ($configType)
134
+ {
135
+ case "global":
136
+ $url = "http://{$cdn}/global_config.txt";
137
+ break;
138
+
139
+ case "local":
140
+ $key = Mage::helper('shoptimally_core/config')->getApiKey();
141
+ $key = str_replace('-', "", $key);
142
+ $url = "http://{$cdn}/sites/{$key}/config.txt";
143
+ break;
144
+ }
145
+
146
+ // fetch the file
147
+ $response = $server->http($url, "GET", null, 15);
148
+
149
+ // make sure no errors occured
150
+ if (is_null($response) || $response->isError())
151
+ {
152
+ $log->warn("Failed to get '" . $configType . "' config file!");
153
+ $callback($configType, false, $server->getLastErrorMessage());
154
+ return;
155
+ }
156
+
157
+ // parse and set the config file
158
+ try
159
+ {
160
+ $newConfig = $response->getBody();
161
+ $this->_config[$configType] = Mage::helper('core')->jsonDecode($newConfig);
162
+ }
163
+ catch (Exception $e)
164
+ {
165
+ $log->warn("Invalid format in '" . $configType . "' config file!", $newConfig);
166
+ $callback($configType, false, "Invalid format / corrupted file!");
167
+ return;
168
+ }
169
+
170
+ try
171
+ {
172
+ // set in persistent storage
173
+ $storage->set($configType . "_config", $newConfig);
174
+
175
+ // add timestamp
176
+ $storage->set($configType . "_config_last_update", date("Y-m-d H:i:s"));
177
+ }
178
+ catch (Exception $e)
179
+ {
180
+ $log->warn("Failed to set remote config in storage!", $e);
181
+ $callback($configType, false, "Failed to write config to storage! Error: " . $e->getMessage());
182
+ return;
183
+ }
184
+
185
+ // success
186
+ $callback($configType, true, "");
187
+
188
+ }
189
+ catch (Exception $e)
190
+ {
191
+ $log->warn("Unexpected exception while fetching config file!", $e);
192
+ $callback($configType, false, "Unexpected exception: " . $e->getMessage());
193
+ }
194
+ }
195
+ }
app/code/community/Shoptimally/Core/Helper/Server.php ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Wrap communicating with Shoptimally server (via ajax requests)
11
+ */
12
+ class Shoptimally_Core_Helper_Server extends Mage_Core_Helper_Abstract
13
+ {
14
+
15
+ /**
16
+ * The site API key
17
+ * @var string
18
+ */
19
+ protected $_apiKey = '';
20
+
21
+ /**
22
+ * Shoptimally API url
23
+ * @var string
24
+ */
25
+ protected $_serverUrl = '';
26
+
27
+ /**
28
+ * Shoptimally user id
29
+ * @var string
30
+ */
31
+ protected $_userId = '';
32
+
33
+ // holds the exception (if happened) we got from last http request.
34
+ public $lastError = null;
35
+
36
+ // hold the response we got from last http request.
37
+ public $lastResponse = null;
38
+
39
+ // the method we use when sending messages to server
40
+ const DEFAULT_METHOD = Varien_Http_Client::POST;
41
+
42
+ /**
43
+ * init the network helper.
44
+ */
45
+ public function __construct()
46
+ {
47
+ $this->init();
48
+ }
49
+
50
+ /**
51
+ * init the api key, user id, and shoptimally URL.
52
+ */
53
+ protected function init()
54
+ {
55
+ $config = Mage::helper('shoptimally_core/config');
56
+ $this->_apiKey = $config->getApiKey();
57
+ $serverUrl = $config->getServerUrl();
58
+ $this->_serverUrl = "http://{$serverUrl}/";
59
+ $this->_userId = Mage::helper('shoptimally_core/clientData')->getUserId();
60
+ }
61
+
62
+ /**
63
+ * Send ajax request to Shoptimally server, with api key and all the basic data
64
+ * built-in. Use this function to communicate with Shoptimally's web API.
65
+ *
66
+ * @param $url - relative api url to send to (eg "user/events/")
67
+ * @param $data - optional data to send.
68
+ * @param $timeout - optional request timeout in seconds. if null will use default.
69
+ * @return - http response, or null if had unexpected exception.
70
+ *
71
+ * note! best way to check for errors after calling this function is to do:
72
+ * if (is_null($response) || $response->isError()) { ... }
73
+ */
74
+ public function sendRequest($url, $data=array(), $timeout=1)
75
+ {
76
+ // get full url
77
+ // note: $this->_serverUrl should end with trailing slash /
78
+ $fullUrl = "{$this->_serverUrl}{$url}";
79
+
80
+ // set api key and user id to message data
81
+ $data['api_key'] = $this->_apiKey;
82
+ $data['user_id'] = $this->_userId;
83
+
84
+ // send the request
85
+ return $this->http($fullUrl, self::DEFAULT_METHOD, $data, $timeout);
86
+ }
87
+
88
+ /**
89
+ * Send http request to anywhere.
90
+ *
91
+ * @param $url - full url to send to.
92
+ * @param $method - http method (GET / POST)
93
+ * @param $data - optional data to send (default to null).
94
+ * @param $timeout - request timeout in seconds. if null will use default.
95
+ * @return - http response, or null if had unexpected exception.
96
+ *
97
+ * To communicate with Shoptimally API don't use this function, use sendRequest() instead.
98
+ *
99
+ * note! best way to check for errors after calling this function is to do:
100
+ * if (is_null($response) || $response->isError()) { ... }
101
+ */
102
+ public function http($url, $method, $data=null, $timeout=1)
103
+ {
104
+ // reset last error and last response
105
+ $this->lastError = null;
106
+ $this->lastResponse = null;
107
+
108
+ // create request with url and method
109
+ $request = new Varien_Http_Client();
110
+ $request->setUri($url);
111
+ $request->setMethod($method);
112
+
113
+ // set data (if provided)
114
+ if (!is_null($data))
115
+ {
116
+ // set content type header
117
+ $request->setHeaders(array('Content-Type' => 'application/json'));
118
+
119
+ // set post data
120
+ $request->setRawData(Mage::helper('core')->jsonEncode($data), 'application/json');
121
+ }
122
+
123
+ // set timeout
124
+ $request->setConfig(array('timeout' => $timeout));
125
+
126
+ // send the request
127
+ // note: if request got a response, doesn't matter the return code we will get a valid response object
128
+ // with code, and no exception. so if we get server 500 for example, no report will occur here and we'll get
129
+ // a return object and not null.
130
+ //
131
+ // if we get a timeout, we will get a Zend_Http_Client_Exception with message "Unable to read response, or response is empty"
132
+ try
133
+ {
134
+ $response = $request->request($method);
135
+ $this->lastResponse = $response;
136
+ }
137
+ catch (Exception $e)
138
+ {
139
+ $this->reportNetworkError($url, $method, $e);
140
+ return null;
141
+ }
142
+
143
+ // return the response
144
+ return $response;
145
+ }
146
+
147
+ // return last error message or null if no errors.
148
+ // this checks if there was exception and return its message, and if not, check if we got error code
149
+ // from server and return error code instead. if all well return null.
150
+ // note: this is valid for the lifetime of this instance only.
151
+ public function getLastErrorMessage()
152
+ {
153
+ // if got exception:
154
+ if (!is_null($this->lastError))
155
+ {
156
+ return $this->lastError->getMessage();
157
+ }
158
+
159
+ // if got response but its error
160
+ if (!is_null($this->lastResponse) && $this->lastResponse->isError())
161
+ {
162
+ return "Got error code from server - " . $this->lastResponse->getStatus() . ".";
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ // report an error
169
+ private function reportNetworkError($url, $method, $error)
170
+ {
171
+ Mage::helper('shoptimally_core/log')->warn("Exception while sending http request!",
172
+ array("exception" => $error->getMessage(), "url" => $url, "method" => $method));
173
+ $this->lastError = $error;
174
+ }
175
+ }
app/code/community/Shoptimally/Core/Helper/Storage.php ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Provide a simple key-value persistent(-ish) storage.
11
+ * NOTE!!! DON'T USE LOGS HERE.
12
+ * Shoptimally Logs relay on this class, calling a log from inside set() or get() will result in stack overflow.
13
+ */
14
+ class Shoptimally_Core_Helper_Storage extends Mage_Core_Helper_Abstract
15
+ {
16
+
17
+ // since we have cache AND config, we have a problem deleting keys when cache is enabled.
18
+ // if we delete the cache key when cache is enabled in magento, when we next read it we will
19
+ // still get the config value from config cache. even if we delete the config itself, we don't
20
+ // touch the config cache. so the solution is instead of deleting we set this special value which
21
+ // mark for us that the value had been deleted, in on next cache clear it will actually take effect.
22
+ const DELETED_VALUE = '-@$__deleted_key__$@-';
23
+
24
+ // will hold the cache manager
25
+ protected $_cache = null;
26
+
27
+ // will hold the config manager
28
+ protected $_config = null;
29
+
30
+ // array of currently active keys, eg keys we get / set.
31
+ // this is for debug purposes.
32
+ protected $_activeKeys = null;
33
+
34
+ // keys prefix for cache and config storage
35
+ const KEY_PREFIX = "shoptimally/";
36
+
37
+ // max data we allow to save
38
+ // this is a protection mechanism because if you try to set cache larger than this magento won't
39
+ // say anything, but will just cut the string in the middle. so we want to send a warning and don't save.
40
+ const MAX_DATA_LEN = 65500;
41
+
42
+ /**
43
+ * init the storage helper.
44
+ */
45
+ public function __construct()
46
+ {
47
+ // get cache and config managers
48
+ $this->_cache = Mage::app()->getCache();
49
+ $this->_config = new Mage_Core_Model_Config();
50
+
51
+ // get active keys list
52
+ $this->_activeKeys = $this->get("active_keys", array(), true, false);
53
+ }
54
+
55
+ // return all the active keys, eg things that shoptimally tried to set/get
56
+ // note: the keys here are without the Shoptimally namespace prefix.
57
+ public function getActiveKeys()
58
+ {
59
+ return $this->_activeKeys;
60
+ }
61
+
62
+ // add a key to the list of active keys
63
+ private function _addToActiveKeys($key)
64
+ {
65
+ // if already in list return
66
+ if (in_array($key, $this->_activeKeys))
67
+ {
68
+ return;
69
+ }
70
+
71
+ // add to list and set it
72
+ array_push($this->_activeKeys, $key);
73
+ $this->set("active_keys", $this->_activeKeys, true, false);
74
+ }
75
+
76
+ // remove a key from the list of active keys
77
+ private function _removeFromActiveKeys($key)
78
+ {
79
+ // if not in list return
80
+ if (!in_array($key, $this->_activeKeys))
81
+ {
82
+ return;
83
+ }
84
+
85
+ // remove to list and set it
86
+ if(($listKey = array_search($key, $this->_activeKeys)) !== false) {
87
+ unset($this->_activeKeys[$listKey]);
88
+ }
89
+ $this->set("active_keys", $this->_activeKeys, true, false);
90
+ }
91
+
92
+ /**
93
+ * set from cache only
94
+ * */
95
+ public function setCache($key, $value)
96
+ {
97
+ $key = self::KEY_PREFIX . $key;
98
+ $keySettings = array(Mage_Core_Model_Config::CACHE_TAG, 'SHOPTIMALLY_STORAGE');
99
+ $this->_cache->save($value, $key, $keySettings, false);
100
+ }
101
+
102
+ /**
103
+ * get from cache only
104
+ * */
105
+ public function getCache($key, $default=null)
106
+ {
107
+ $key = self::KEY_PREFIX . $key;
108
+ $ret = $this->_cache->load($key);
109
+ if ($ret === false)
110
+ {
111
+ return $default;
112
+ }
113
+ return $ret;
114
+ }
115
+
116
+ /**
117
+ * set a config value into config.
118
+ * this is a simple key-value persistent storage.
119
+ * @param $jsonEncode if true, will json-encode value before returning it.
120
+ * Use this option for objects!!!
121
+ * @param $addToActiveKeys if true (default) will add these values to the list of active keys
122
+ */
123
+ public function set($key, $value, $jsonEncode=false, $addToActiveKeys=true)
124
+ {
125
+ // add to list of active keys
126
+ if ($addToActiveKeys)
127
+ {
128
+ $this->_addToActiveKeys($key);
129
+ }
130
+
131
+ // convert to full key name (added shoptimally name to avoid collision with other stuff)
132
+ $key = self::KEY_PREFIX . $key;
133
+
134
+ // do json encoding
135
+ if ($jsonEncode)
136
+ {
137
+ $value = Mage::helper('core')->jsonEncode($value);
138
+ }
139
+
140
+ // make sure value len is valid
141
+ if (strlen($value) > self::MAX_DATA_LEN)
142
+ {
143
+ Mage::log("Shoptimally notice: tried to set a value too big: '" . $key . "'.");
144
+ return false;
145
+ }
146
+
147
+ // put value in cache
148
+ $this->_setCacheVal($key, $value);
149
+
150
+ // also store in config cache (for persistency)
151
+ $this->_config->saveConfig($key, $value);
152
+ return true;
153
+ }
154
+
155
+ /**
156
+ * get a config value from config
157
+ * this is a simple key-value persistent storage
158
+ * @param $default will be returned if value doesn't exist
159
+ * @param $jsonDecode if true, will json-decode value before returning it.
160
+ * Use this option for objects!!!
161
+ * @param $addToActiveKeys if true (default) will add these values to the list of active keys
162
+ */
163
+ public function get($key, $default=null, $jsonDecode=false, $addToActiveKeys=true)
164
+ {
165
+ // add to list of active keys
166
+ if ($addToActiveKeys)
167
+ {
168
+ $this->_addToActiveKeys($key);
169
+ }
170
+
171
+ // convert to full key name (added shoptimally name to avoid collision with other stuff)
172
+ $key = self::KEY_PREFIX . $key;
173
+
174
+ // try to get from cache
175
+ $val = $this->_cache->load($key);
176
+
177
+ // this special val is set when value is deleted.
178
+ // read delete key docs for more info.
179
+ if ($val === self::DELETED_VALUE) {return $default;}
180
+
181
+ // not found in cache? (cache returns false when item does not exist)
182
+ if ($val === false)
183
+ {
184
+ // try to fetch from config
185
+ $val = Mage::getStoreConfig($key);
186
+
187
+ // not found in config? return default
188
+ if (is_null($val)) {return $default;}
189
+ }
190
+
191
+ // do json decoding
192
+ if ($jsonDecode)
193
+ {
194
+ $val = Mage::helper('core')->jsonDecode($val);
195
+ }
196
+
197
+ // return value
198
+ return $val;
199
+ }
200
+
201
+ /**
202
+ * delete a key from storage
203
+ * @param $removeFromActiveKeys if true (default) will remove the key from the list of active keys
204
+ */
205
+ public function delete($key, $removeFromActiveKeys=true)
206
+ {
207
+ // add to list of active keys
208
+ if ($removeFromActiveKeys)
209
+ {
210
+ $this->_removeFromActiveKeys($key);
211
+ }
212
+
213
+ // set value to empty string because annoyingly sometimes magento don't delete values (scoping reasons)
214
+ $this->set($key, "", false, false);
215
+
216
+ // convert to full key name (added shoptimally name to avoid collision with other stuff)
217
+ $key = self::KEY_PREFIX . $key;
218
+
219
+ // remove from cache and config
220
+ //$this->_cache->remove($key);
221
+ $this->_setCacheVal($key, self::DELETED_VALUE);
222
+ $this->_config->deleteConfig ($key);
223
+ }
224
+
225
+ /**
226
+ * set a cahce value (string)
227
+ * */
228
+ private function _setCacheVal($key, $value)
229
+ {
230
+ $keySettings = array(Mage_Core_Model_Config::CACHE_TAG, 'SHOPTIMALLY_STORAGE');
231
+ $this->_cache->save($value, $key, $keySettings, false);
232
+ }
233
+ }
app/code/community/Shoptimally/Core/Helper/UrlUtils.php ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Provide URL manipulations and utilities.
11
+ */
12
+ class Shoptimally_Core_Helper_UrlUtils extends Mage_Core_Helper_Abstract
13
+ {
14
+ /**
15
+ * convert absolute url to relative url
16
+ * @param $url is absolute url to convert
17
+ * @return relative url, without domain
18
+ */
19
+ public function toRelative($url)
20
+ {
21
+ $url = Mage::getSingleton('core/url')->parseUrl($url);
22
+ return $url->getPath();
23
+ }
24
+
25
+ /**
26
+ * get *relative* current url
27
+ */
28
+ public function getCurrentUrl()
29
+ {
30
+ return $this->toRelative(Mage::helper('core/url')->getCurrentUrl());
31
+ }
32
+
33
+ /**
34
+ * get *relative* previous url
35
+ * note: assuming previous url was inside our domain
36
+ */
37
+ public function getPreviousUrl()
38
+ {
39
+ return $this->toRelative(Mage::getSingleton('core/session')->getLastUrl());
40
+ }
41
+
42
+ /**
43
+ * get url and return if its a visible url, eg an actual page users can browse.
44
+ * note: some urls are just magento internal tricks or transitions, these pages are not visible.
45
+ * for example, the cart page is visible. however, when you do checkout, it switch to something like:
46
+ * "/cart/checkout/processid?=3852908593032...." which immediately switch forward to next page.
47
+ * that middle-url during the checkout process is not a visible page.
48
+ */
49
+ protected function isVisibleUrl($url)
50
+ {
51
+ // make a list of invalid urls
52
+ $invalidUrls = array(
53
+ '/checkout/cart/add/',
54
+ '/checkout/cart/remove/',
55
+ '/checkout/cart/updatePost/',
56
+ '/checkout/cart/index/',
57
+ '/cart/checkout/processid?='
58
+ );
59
+
60
+ // check if url is inalid
61
+ foreach($invalidUrls as $badUrl)
62
+ {
63
+ if (strpos($url, $badUrl) !== false) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // if got here means its a valid, visible page
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * get meaningful current relative url.
74
+ * what this means? when you add item to cart, for example, magento have some middle url
75
+ * to active the event. something like: "cart/add-item?id=53989038..." etc.
76
+ * sometimes we want to get the REAL url we got from, and not the internal url.
77
+ * this function helps us get it. will either return current url, or if its an internal magento
78
+ * trick will return previous url instead.
79
+ */
80
+ public function getActualCurrentUrl()
81
+ {
82
+ // first try to get current url
83
+ $ret = $this->getCurrentUrl();
84
+
85
+ // if its an invisible url fix it
86
+ try
87
+ {
88
+ // if its not magento built-in url, return it
89
+ if ($this->isVisibleUrl($ret))
90
+ {
91
+ return $ret;
92
+ }
93
+
94
+ // if its one of the checkout pages, its a special case.
95
+ // there are lots of special pages there, sometimes event more then one hop.
96
+ // so if url has "/checkout/cart/" just remove everything after the cart.
97
+ if (strpos($ret, "/checkout/cart/") !== false) {
98
+ $ret = explode("/cart/", $ret);
99
+ return $ret[0] . '/cart/';
100
+ }
101
+
102
+ // if got here it means curr url is magento built-in, so we return last url instead
103
+ return $this->getPreviousUrl();
104
+ }
105
+ catch (Exception $e)
106
+ {
107
+ Mage::helper('shoptimally_core/log')->warn("Error while trying to get actual URL.", $e);
108
+ return $ret;
109
+ }
110
+ }
111
+
112
+ }
app/code/community/Shoptimally/Core/Model/Cron.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\CatalogSync
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This cron job update do some basic Shoptimally timely events.
11
+ * Most important functionality is to update the remote-config file.
12
+ */
13
+ class Shoptimally_Core_Model_Cron
14
+ {
15
+ /**
16
+ * called every minute to update the remote config file
17
+ */
18
+ public function updateRemoteConfig()
19
+ {
20
+ try
21
+ {
22
+ // get api key and make sure its defined. if not, skip.
23
+ $config = Mage::helper('shoptimally_core/config');
24
+ $apiKey = $config->getApiKey();
25
+ $enabled = $config->getGeneralSetting('ShoptimallyEnabled');
26
+ if ($enabled == false || is_null($apiKey) || strlen($apiKey) == 0)
27
+ {
28
+ return;
29
+ }
30
+
31
+ Mage::helper('shoptimally_core/storage')->set("last_cron_run", date("Y-m-d H:i:s"), true);
32
+
33
+ // get remote config from cdn.
34
+ Mage::helper('shoptimally_core/remoteConfig')->updateFromCdn();
35
+ }
36
+ catch (Exception $e)
37
+ {
38
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in updating remote config!", $e);
39
+ }
40
+ }
41
+ }
app/code/community/Shoptimally/Core/Model/Observer.php ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This observer listen to some events required for Shoptimally init process
11
+ * and internal state.
12
+ */
13
+ class Shoptimally_Core_Model_Observer
14
+ {
15
+ /**
16
+ * handle event when a block is about to render
17
+ * this function is responsible to trigger the 'BlocksInjecter' helper, so we can
18
+ * inject customized blocks.
19
+ */
20
+ public function onBlockAbstractToHtmlBefore(Varien_Event_Observer $obs)
21
+ {
22
+ try
23
+ {
24
+ // if shoptimally is disabled, skip
25
+ if (!Mage::helper('shoptimally_core/config')->getIsEnabled())
26
+ {
27
+ return;
28
+ }
29
+
30
+ // call the block utils event
31
+ Mage::helper('shoptimally_core/blockUtils')->onBlockRender($obs);
32
+ }
33
+ catch (Exception $e)
34
+ {
35
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
36
+ }
37
+
38
+ return $this;
39
+ }
40
+
41
+ /**
42
+ * called when products list is loaded to get products ids
43
+ * */
44
+ public function onProductsListLoaded(Varien_Event_Observer $observer)
45
+ {
46
+ try
47
+ {
48
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled())
49
+ {
50
+ $collection = $observer->getCollection();
51
+ $ids = array();
52
+ foreach($collection as $product) {
53
+ array_push($ids, $product->getId());
54
+ }
55
+ Mage::helper('shoptimally_core/pageInfo')->_setProductIdsOnPage($ids);
56
+ }
57
+ }
58
+ catch (Exception $e)
59
+ {
60
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
61
+ }
62
+
63
+ return $this;
64
+ }
65
+ }
app/code/community/Shoptimally/Core/controllers/DebugDataController.php ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\Core
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Controller for our debug data page. We use this controller to debug Shoptimally.
11
+ * URL FOR DEBUG DATA: /shoptimally_debug_data/DebugData/dump
12
+ * URL TO SHOW STORAGE VALUE: /shoptimally_debug_data/DebugData/storage/key/<storage_key>
13
+ * URL TO DELETE STORAGE VALUE: /shoptimally_debug_data/DebugData/delete/key/<storage_key>
14
+ * URL TO FORCE-UPDATE REMOTE CONFIG: /shoptimally_debug_data/DebugData/updateconfig
15
+ *
16
+ */
17
+ class Shoptimally_Core_DebugDataController extends Mage_Core_Controller_Front_Action
18
+ {
19
+ // tr's total count
20
+ // this is to do tr background colors
21
+ private $trCount = 0;
22
+
23
+ // dump a storage value by key
24
+ public function storageAction()
25
+ {
26
+ $storage = Mage::helper('shoptimally_core/storage');
27
+ $key = $this->getRequest()->getParam('key');
28
+ echo $storage->get($key, "KEY DOES NOT EXIST.", false, false);
29
+ }
30
+
31
+ // delete storage value by key
32
+ public function deleteAction()
33
+ {
34
+ $storage = Mage::helper('shoptimally_core/storage');
35
+ $key = $this->getRequest()->getParam('key');
36
+ $storage->delete($key);
37
+ echo "Deleted key " . $key;
38
+ }
39
+
40
+ // force-update configuration from cdn
41
+ public function updateconfigAction()
42
+ {
43
+ try
44
+ {
45
+ Mage::helper('shoptimally_core/remoteConfig')->updateFromCdn(function($type, $success, $reason)
46
+ {
47
+ if ($success)
48
+ {
49
+ echo "Update " . $type . " done successfully." . "<br />";
50
+ }
51
+ else
52
+ {
53
+ echo "Update " . $type . " Failed! reason: " . $reason . "<br />";
54
+ }
55
+ });
56
+ echo "Done!";
57
+ }
58
+ catch (Exception $e)
59
+ {
60
+ echo $e;
61
+ }
62
+
63
+ echo "<hr />";
64
+ $remote = Mage::helper('shoptimally_core/remoteConfig');
65
+ $allRemote = $remote->_getAll();
66
+ echo "<h1>LOCAL CONFIG:</h1>";
67
+ echo htmlspecialchars(json_encode ($allRemote['local']));
68
+ echo "<h1>GLOBAL CONFIG:</h1>";
69
+ echo htmlspecialchars(json_encode ($allRemote['global']));
70
+ }
71
+
72
+ // dump Shoptimally debug data
73
+ public function dumpAction()
74
+ {
75
+ $remote = Mage::helper('shoptimally_core/remoteConfig');
76
+ if( !empty( $debugConfig['hide_debug_page'] ) && $debugConfig['hide_debug_page'] === true )
77
+ {
78
+ return;
79
+ }
80
+
81
+ // line break tag
82
+ $lb = "<br />";
83
+
84
+ // open table
85
+ echo "<table style='padding-right:40px;'>";
86
+
87
+ // get some helpers
88
+ $config = Mage::helper('shoptimally_core/config');
89
+ $storage = Mage::helper('shoptimally_core/storage');
90
+ $user = Mage::helper('shoptimally_core/clientData');
91
+ $log = Mage::helper('shoptimally_core/log');
92
+
93
+ $currTime = time();
94
+
95
+ // print version and general config
96
+ try
97
+ {
98
+ $this->printTitle("CONFIG");
99
+ $this->printData("time now", date("Y-m-d H:i:s"));
100
+ $this->printData("timestamp now", $currTime);
101
+ $this->printData("version", $config->getVersion());
102
+ $this->printData("mversion", Mage::getVersion());
103
+ $this->printData("enabled", $config->getIsEnabled(), "bool");
104
+ $this->printData("enabled in admin panel", $config->getGeneralSetting('ShoptimallyEnabled'), "bool");
105
+ $this->printData("log enabled", $config->isLogEnabled(0), "bool");
106
+ $this->printData("log critical (0)", $config->isLogEnabled(0), "bool");
107
+ $this->printData("log warning (1)", $config->isLogEnabled(1), "bool");
108
+ $this->printData("log normal (2)", $config->isLogEnabled(2), "bool");
109
+ $this->printData("log debug (3)", $config->isLogEnabled(3), "bool");
110
+ $this->printData("api key", $config->getApiKey());
111
+ $this->printData("remote config url", Shoptimally_Core_Helper_RemoteConfig::CDN_DOMAIN);
112
+ $this->printData("server url", $config->getServerUrl());
113
+ $this->printData("async js", $config->shouldLoadJsAsync());
114
+ $this->printData("js url", $config->getJsUrl());
115
+ $this->printData("last cron run", $storage->get("last_cron_run", "-", true));
116
+
117
+ $cacheId = $storage->getCache("cache_id");
118
+ if (is_null($cacheId))
119
+ {
120
+ $cacheId = "Not found, cache was cleared since last visit on this page.";
121
+ $storage->setCache("cache_id", date("Y-m-d H:i:s"));
122
+ }
123
+ $this->printData("cache last known clear", $cacheId);
124
+ }
125
+ catch (Exception $e)
126
+ {
127
+ echo "<h1>ERROR IN SECTION 'CONFIG'!</h1>";
128
+ echo $e;
129
+ }
130
+
131
+ // print user related data
132
+ try
133
+ {
134
+ $userEventsUtils = Mage::helper('shoptimally_analytics/userEvents');
135
+ $this->printTitle("USER");
136
+ $this->printData("user id", $user->getUserId());
137
+ $this->printData("user data", $user->_getDataDebug(), "object_pretty");
138
+ $this->printData("current cart", $userEventsUtils->getCartItemsConverted(), "object_pretty");
139
+ }
140
+ catch (Exception $e)
141
+ {
142
+ echo "<h1>ERROR IN SECTION 'USER'!</h1>";
143
+ echo $e;
144
+ }
145
+
146
+ // storage debug
147
+ try
148
+ {
149
+ $this->printTitle("STORAGE");
150
+ $keys = $storage->getActiveKeys();
151
+ foreach ($keys as $storageKey)
152
+ {
153
+ $this->printData($storageKey, "storage/key/" . $storageKey, "link");
154
+ }
155
+ }
156
+ catch (Exception $e)
157
+ {
158
+ echo "<h1>ERROR IN SECTION 'STORAGE'!</h1>";
159
+ echo $e;
160
+ }
161
+
162
+ try
163
+ {
164
+ // catalog sync data
165
+ $this->printTitle("CATALOG SYNC - OLD");
166
+ $isUsed = $remote->get("catalog_sync_timely_method") === "incremental";
167
+ $this->printData("is used", $isUsed, "bool");
168
+ if ($isUsed)
169
+ {
170
+ $this->printData("catalog sync method", $remote->get("catalog_sync_timely_method"));
171
+ $startCategory = $storage->get("update_last_category");
172
+ $startPage = $storage->get("update_last_page");
173
+ $categoryName = $storage->get("last_category_name");
174
+ $this->printData("curr category", $startCategory);
175
+ $this->printData("curr page", $startPage);
176
+ $this->printData("category name", $categoryName);
177
+ }
178
+
179
+ $this->printTitle("CATALOG SYNC - NEW");
180
+
181
+ // interesting list data
182
+ // first get config
183
+ $interestingListConfig = $remote->get("catalog_sync_interesting_list", array(), true);
184
+
185
+ // get history list history and calc ttl for products
186
+ $historyList = $storage->get("interesting_list_sent_history", array(), true);
187
+
188
+ // print interesting list stuff
189
+ $this->printData("use interesting list", $remote->get("catalog_sync_interesting_list")["enable"], "bool");
190
+ $this->printData("next interesting list iter", $storage->get("interesting_list_iteration", 0) . "/" . $interestingListConfig["products_ttl"]);
191
+ $this->printData("curr interesting list", $storage->get("current_interesting_list", array(), true), "array");
192
+ $this->printData("history list", $historyList, "array");
193
+ $this->printData("interesting list settings", $interestingListConfig, "object_pretty");
194
+
195
+ $this->printTitle("CATALOG SYNC - HTMLS");
196
+ $this->printData("send items html", $remote->get("update_items_html"), "bool");
197
+ $this->printData("curr interesting htmls list", $storage->get("htmls_interesting_list", array(), true), "array");
198
+
199
+ }
200
+ catch (Exception $e)
201
+ {
202
+ echo "<h1>ERROR IN SECTION 'CATALOG SYNC'!</h1>";
203
+ echo $e;
204
+ }
205
+
206
+ // print logs
207
+ try
208
+ {
209
+ // last log data
210
+ $this->printTitle("SHOPTIMALLY LOG");
211
+ $lastLogs = $log->getLastLogs();
212
+ $index = 0;
213
+ foreach ($lastLogs as $entry)
214
+ {
215
+ // add color to logs
216
+ $color = "black";
217
+ try
218
+ {
219
+ // normal logs
220
+ if (strpos($entry, "[Shoptimally-log]") !== false)
221
+ {
222
+ $color = "black";
223
+ }
224
+ // debug logs
225
+ else if (strpos($entry, "[Shoptimally-debug]") !== false)
226
+ {
227
+ $color = "#888";
228
+ }
229
+ // warnings (this is usually catched exceptions and weird stuff)
230
+ else if (strpos($entry, "[Shoptimally-warning]") !== false)
231
+ {
232
+ $color = "orange";
233
+ }
234
+ // fatals
235
+ else if (strpos($entry, " warning {") !== false)
236
+ {
237
+ $color = "red";
238
+ }
239
+ $entry = "<font color='" . $color . "'>" . $entry . "</font>";
240
+ }
241
+ catch (Exception $e)
242
+ {
243
+ }
244
+
245
+ // print log line
246
+ $this->printData($index, $entry);
247
+ $index++;
248
+ }
249
+ }
250
+ catch (Exception $e)
251
+ {
252
+ echo "<h1>ERROR IN SECTION 'SHOPTIMALLY LOG'!</h1>";
253
+ echo $e;
254
+ }
255
+
256
+
257
+ // actions we can do with remote config
258
+ try
259
+ {
260
+ $this->printTitle("REMOTE CONFIG ACTIONS");
261
+ $this->printData("Update remote config", "../updateconfig", "link");
262
+ }
263
+ catch (Exception $e)
264
+ {
265
+ echo "<h1>ERROR IN SECTION 'REMOTE CONFIG ACTIONS'!</h1>";
266
+ echo $e;
267
+ }
268
+
269
+ // print remote config local
270
+ try
271
+ {
272
+
273
+ $allRemote = $remote->_getAll();
274
+ $local = $allRemote["local"];
275
+
276
+ // start with local config
277
+ $this->printTitle("REMOTE CONFIG LOCAL");
278
+ $this->printData("last update", $storage->get("local_config_last_update"));
279
+ foreach ($local as $key => $value)
280
+ {
281
+ $this->printData($key, $value, "recursive");
282
+ }
283
+ }
284
+ catch (Exception $e)
285
+ {
286
+ echo "<h1>ERROR IN SECTION 'REMOTE CONFIG LOCAL'!</h1>";
287
+ echo $e;
288
+ }
289
+
290
+ // print remote config global
291
+ try
292
+ {
293
+ $allRemote = $remote->_getAll();
294
+ $global = $allRemote["global"];
295
+
296
+ $this->printTitle("REMOTE CONFIG GLOBAL");
297
+ $this->printData("last update", $storage->get("global_config_last_update"));
298
+ foreach ($global as $key => $value)
299
+ {
300
+ $this->printData($key, $value, "recursive");
301
+ }
302
+ }
303
+ catch (Exception $e)
304
+ {
305
+ echo "<h1>ERROR IN SECTION 'REMOTE CONFIG GLOBAL'!</h1>";
306
+ echo $e;
307
+ }
308
+
309
+ // close table
310
+ echo "</table>";
311
+ }
312
+
313
+ // echo title line
314
+ protected function printTitle($name)
315
+ {
316
+ $name = "<font color='blue'>" . $name . "</font>";
317
+ $this->printData(" .", " .");
318
+ $this->printData($name, "---");
319
+ $this->printData(" .", " .");
320
+ }
321
+
322
+ // echo data
323
+ // name / value is obvious
324
+ // type is for special parsing (like booleans or recursive objects)
325
+ protected function printData($name, $value, $type=null)
326
+ {
327
+ // open row
328
+ $trBack = $this->trCount++ % 2 == 0 ? "#eef" : "#def";
329
+ echo "\r\n<tr style='background:".$trBack."'>";
330
+
331
+ // convert value based on type if provided
332
+ switch ($type)
333
+ {
334
+ case "bool":
335
+ if ($value == true) $value = "true";
336
+ if ($value == false) $value = "false";
337
+ break;
338
+
339
+ case "object":
340
+ $value = Mage::helper('core')->jsonEncode($value);
341
+ break;
342
+
343
+ case "object_pretty":
344
+ $value = "<pre>" . json_encode($value, JSON_PRETTY_PRINT) . "</pre>";
345
+ break;
346
+
347
+ case "array":
348
+ $value = "array<" . count($value) . ">::" . Mage::helper('core')->jsonEncode($value);
349
+ break;
350
+
351
+ case "link":
352
+ $value = "<a href='" . $value . "' target='_blank'>" . $value . "</a>";
353
+ break;
354
+
355
+ case "number":
356
+ if ($value === 0) {$value = "0";}
357
+ break;
358
+
359
+ case "recursive":
360
+ if (is_array($value))
361
+ {
362
+ echo "\r\n<td style=\"padding-right:40px\">".$name."</td>";
363
+ echo "\r\n<td style=\"padding-right:40px\">...</td>";
364
+ echo "\r\n</tr>";
365
+ foreach ($value as $key => $value_2)
366
+ {
367
+ $this->printData($name."/".$key."/", $value_2, "recursive");
368
+ }
369
+ return;
370
+ }
371
+ break;
372
+ }
373
+
374
+ // print name and value
375
+ echo "\r\n<td style=\"padding-right:40px\">".$name."</td>";
376
+ echo "\r\n<td style=\"padding-right:40px\">".$value."</td>";
377
+
378
+ // close row
379
+ echo "\r\n</tr>";
380
+ }
381
+ }
app/code/community/Shoptimally/Core/etc/adminhtml.xml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <acl>
4
+ <resources>
5
+ <all>
6
+ <title>Allow Everything</title>
7
+ </all>
8
+ <admin>
9
+ <children>
10
+ <system>
11
+ <children>
12
+ <config>
13
+ <children>
14
+ <Shoptimally>
15
+ <title>Shoptimally</title>
16
+ <sort_order>100</sort_order>
17
+ </Shoptimally>
18
+ </children>
19
+ </config>
20
+ </children>
21
+ </system>
22
+ </children>
23
+ </admin>
24
+ </resources>
25
+ </acl>
26
+ </config>
app/code/community/Shoptimally/Core/etc/config.xml ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <config>
3
+
4
+ <modules>
5
+ <Shoptimally_Core>
6
+ <version>1.0.0</version>
7
+ </Shoptimally_Core>
8
+ </modules>
9
+
10
+ <!-- define classes -->
11
+ <global>
12
+
13
+ <models>
14
+ <shoptimally_core>
15
+ <class>Shoptimally_Core_Model</class>
16
+ </shoptimally_core>
17
+ </models>
18
+
19
+ <helpers>
20
+ <shoptimally_core>
21
+ <class>Shoptimally_Core_Helper</class>
22
+ </shoptimally_core>
23
+ </helpers>
24
+
25
+ <blocks>
26
+ <shoptimally_core>
27
+ <class>Shoptimally_Core_Block</class>
28
+ </shoptimally_core>
29
+ </blocks>
30
+
31
+ </global>
32
+
33
+ <!-- cronjob to update remote config -->
34
+ <crontab>
35
+ <jobs>
36
+ <shoptimally_core>
37
+ <schedule><cron_expr>* * * * *</cron_expr></schedule>
38
+ <run><model>shoptimally_core/cron::updateRemoteConfig</model></run>
39
+ </shoptimally_core>
40
+ </jobs>
41
+ </crontab>
42
+
43
+ <!-- layout update to inject the shoptimally header -->
44
+ <frontend>
45
+ <layout>
46
+ <updates>
47
+ <shoptimally_core>
48
+ <file>shoptimally/core.xml</file>
49
+ </shoptimally_core>
50
+ </updates>
51
+ </layout>
52
+
53
+ <!-- shoptimally debug data page -->
54
+ <routers>
55
+ <shoptimally_core_debuge_data>
56
+ <use>standard</use>
57
+ <args>
58
+ <module>Shoptimally_Core</module>
59
+ <frontName>shoptimally_debug_data</frontName>
60
+ </args>
61
+ </shoptimally_core_debuge_data>
62
+ </routers>
63
+
64
+ <!-- events to init some internal data -->
65
+ <events>
66
+ <core_block_abstract_to_html_before>
67
+ <observers>
68
+ <shoptimally_core>
69
+ <type>model</type>
70
+ <class>shoptimally_core/observer</class>
71
+ <method>onBlockAbstractToHtmlBefore</method>
72
+ </shoptimally_core>
73
+ </observers>
74
+ </core_block_abstract_to_html_before>
75
+
76
+ <catalog_block_product_list_collection>
77
+ <observers>
78
+ <shoptimally_core>
79
+ <type>model</type>
80
+ <class>shoptimally_core/observer</class>
81
+ <method>onProductsListLoaded</method>
82
+ </shoptimally_core>
83
+ </observers>
84
+ </catalog_block_product_list_collection>
85
+ </events>
86
+
87
+ </frontend>
88
+
89
+ </config>
app/code/community/Shoptimally/Core/etc/system.xml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <tabs>
4
+ <Shoptimally translate="label" module="shoptimally_core">
5
+ <label>Shoptimally</label>
6
+ <sort_order>100</sort_order>
7
+ <class>Shoptimally</class>
8
+ </Shoptimally>
9
+ </tabs>
10
+ <sections>
11
+ <Shoptimally translate="label" module="shoptimally_core">
12
+ <label>General</label>
13
+ <tab>Shoptimally</tab>
14
+ <frontend_type>text</frontend_type>
15
+ <sort_order>100</sort_order>
16
+ <show_in_default>1</show_in_default>
17
+ <show_in_website>1</show_in_website>
18
+ <show_in_store>1</show_in_store>
19
+ <groups>
20
+ <GeneralSettings translate="label">
21
+ <label>General Settings</label>
22
+ <comment>Shoptimally main settings. Here you can set the API key and enable/disable Shoptimally.</comment>
23
+ <frontend_type>text</frontend_type>
24
+ <sort_order>100</sort_order>
25
+ <show_in_default>1</show_in_default>
26
+ <show_in_website>1</show_in_website>
27
+ <show_in_store>1</show_in_store>
28
+ <fields>
29
+ <ShoptimallyEnabled translate="label">
30
+ <label>Shoptimally Enabled</label>
31
+ <frontend_type>select</frontend_type>
32
+ <source_model>adminhtml/system_config_source_yesno</source_model>
33
+ <sort_order>100</sort_order>
34
+ <show_in_default>1</show_in_default>
35
+ <show_in_website>1</show_in_website>
36
+ <show_in_store>1</show_in_store>
37
+ <comment>Use this setting to enable/disable Shoptimally.</comment>
38
+ </ShoptimallyEnabled>
39
+ <ApiKey translate="label">
40
+ <label>API Key</label>
41
+ <frontend_type>text</frontend_type>
42
+ <sort_order>200</sort_order>
43
+ <show_in_default>1</show_in_default>
44
+ <show_in_website>1</show_in_website>
45
+ <show_in_store>1</show_in_store>
46
+ <comment>The API key as provided by Shoptimally.</comment>
47
+ </ApiKey>
48
+ </fields>
49
+ </GeneralSettings>
50
+ </groups>
51
+ </Shoptimally>
52
+ </sections>
53
+ </config>
54
+
55
+
56
+
57
+
app/code/community/Shoptimally/Core/readme.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ This is the "Core" Shoptimally module, which does three things:
2
+
3
+ - Inject the client javascript into the site (note: uses templates and layout files).
4
+ - Add general Shoptimally settings into the admin panel (API key, enable/disable, etc.)
5
+ - Provide general helpers with basic functionality that is relevant to all future modules.
6
+
7
+ Note: all other modules rely on the existence of this module to work.
app/code/community/Shoptimally/FeaturedItems/Helper/Data.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ class Shoptimally_FeaturedItems_Helper_Data extends Mage_Core_Helper_Abstract
5
+ {
6
+ }
app/code/community/Shoptimally/FeaturedItems/Helper/Main.php ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\FeaturedItems
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This helper provide the main functionality of the 'featured items' feature.
11
+ */
12
+ class Shoptimally_FeaturedItems_Helper_Main extends Shoptimally_Core_Helper_FeatureBase
13
+ {
14
+ // define feature name (see Shoptimally_Core_Helper_FeatureBase for more info)
15
+ const NAME = "FeaturedItems";
16
+
17
+ // default number of products required in collection to initiate this feature.
18
+ // this value is used if remote config don't have this option.
19
+ const DEFAULT_MIN_PRODUCTS_TO_OPERATE_ON = 8;
20
+
21
+ /**
22
+ * return the minimum amount of required products to run this feature.
23
+ * for example, if 8 and current page only have 5 products, featured items will not run.
24
+ */
25
+ private function getMinProductsRequiredToRun()
26
+ {
27
+ return $this->getFeatureConfig("min_products_to_operate_on", self::DEFAULT_MIN_PRODUCTS_TO_OPERATE_ON);
28
+ }
29
+
30
+ /**
31
+ * return if this feature should work on current page
32
+ */
33
+ private function shouldWorkOnThisPage($productsCollection)
34
+ {
35
+ // check if there are enough products in this collection
36
+ $minProducts = $this->getMinProductsRequiredToRun();
37
+ if($productsCollection->count() < $minProducts)
38
+ {
39
+ return;
40
+ }
41
+
42
+ // now return if enabled and page is a legal category page
43
+ return Mage::helper('shoptimally_core/pageInfo')->getPageType() == "category";
44
+ }
45
+
46
+ /**
47
+ * get featured items we need to push for current user, page, etc.
48
+ * @param $category - category to get items for.
49
+ * @param $index - page index
50
+ * @return - list of product ids.
51
+ */
52
+ private function getFeaturedItemsFromServer($category, $index)
53
+ {
54
+ // get server communication helper
55
+ $server = $this->_server;
56
+
57
+ # get category url
58
+ $urlUtils = Mage::helper('shoptimally_core/urlUtils');
59
+ $categoryUrl = $urlUtils->toRelative($category->getUrl());
60
+
61
+ // get category id
62
+ $category = Mage::registry('current_category');
63
+ if (!is_null($category)) {
64
+ $category = $category->getId();
65
+ }
66
+
67
+ // send request to server
68
+ $data = array(
69
+ "page_data" => array(
70
+ "index" => $index,
71
+ "base_url" => $categoryUrl,
72
+ "category_id" => $category,
73
+ )
74
+ );
75
+ $response = $this->sendAjax("features/featured_items/get", $data, 1);
76
+
77
+ // if exception happened skip
78
+ if (is_null($response) || $response->isError()) {
79
+ return null;
80
+ }
81
+
82
+ // get and return ids list from response
83
+ $idsList = Mage::helper('core')->jsonDecode($response->getBody());
84
+ return $idsList;
85
+ }
86
+
87
+ /**
88
+ * this function get the list of product ids we are about to push, and prepare it. its ultimate goal is to
89
+ * make sure the product rows are nicely aligned.
90
+ * if for example every row of products have 4 items, and we are about to push 3, we will "break"
91
+ * the last line. so we want to avoid it. also keep in mind that some of the new products we are about to push
92
+ * already exist in original collection, so those product just move up and not added twice.
93
+ * so this function does the following:
94
+ *
95
+ * 1. if there are not enough new items to push that *doesn't already exist*, we remove all the unique new
96
+ * items from the ids list. this means in this case we will only promote existing products on the page
97
+ * and won't add new ones.
98
+ * 2. if there are more than needed unique new items, remove the extras, so we'll add the right amount.
99
+ *
100
+ * @param $newProductIds - list of products ids we want to push.
101
+ * @param $productsCollection - collection to push into.
102
+ * @return - new list of product ids.
103
+ */
104
+ private function fixNewProductsList($newProductIds, $productsCollection)
105
+ {
106
+ // first calculate how many items we want to push based on the items count in original list
107
+ $collectionCount = count($productsCollection);
108
+ if (($collectionCount % 3 == 0) && ($collectionCount % 4 != 0)) {
109
+ $requiredItemsCount = 3;
110
+ }
111
+ else {
112
+ $requiredItemsCount = 4;
113
+ }
114
+
115
+
116
+ // iterate over the new products ids and generate two lists:
117
+ // 1. new items that don't exist in original collection.
118
+ // 2. new items that already exist in collection.
119
+ $uniqueIds = array();
120
+ $existingIds = array();
121
+ foreach($newProductIds as $id)
122
+ {
123
+ // check if current id is unique or already appear
124
+ $unique = true;
125
+ foreach($productsCollection as $existingProduct)
126
+ {
127
+ if ($id == $existingProduct->getId())
128
+ {
129
+ $unique = false;
130
+ break;
131
+ }
132
+ }
133
+
134
+ // push to either the unique ids or the existing ids
135
+ if ($unique) {
136
+ array_push($uniqueIds, $id);
137
+ }
138
+ else {
139
+ array_push($existingIds, $id);
140
+ }
141
+ }
142
+
143
+ // now since we always return the existing items because we want to promote them,
144
+ // we will iterate over unique items (if have enough) and add them to $existingIds.
145
+ if (count($uniqueIds) >= $requiredItemsCount)
146
+ {
147
+ $insertedCount = 0;
148
+ foreach($uniqueIds as $id)
149
+ {
150
+ array_push($existingIds, $id);
151
+ if (++$insertedCount >= $requiredItemsCount) {break;}
152
+ }
153
+ }
154
+
155
+ // return the existing ids with the right amount of new unique ids
156
+ return $existingIds;
157
+ }
158
+
159
+ /**
160
+ * this function do most of the logic:
161
+ * 1. check if should work on this page at all or not, if feature is enabled, etc.
162
+ * 2. get some page info etc, and request items from server.
163
+ * 3. fix featured items to prevent duplications etc.
164
+ *
165
+ * @param $productsCollection - the loaded products list to push the featured items into.
166
+ * @return list of product ids to push as featured items.
167
+ */
168
+ private function getFeaturedItemsIds($productsCollection)
169
+ {
170
+
171
+
172
+ // get current page category
173
+ $pageInfo = Mage::helper('shoptimally_core/pageInfo');
174
+ $pageCategory = $pageInfo->getCategory();
175
+
176
+ // no category on this page? weird, report and return
177
+ if (empty($pageCategory))
178
+ {
179
+ $this->_log->warn("FeaturedItems could not get page category object!",
180
+ Mage::helper('shoptimally_core/urlUtils')->getCurrentUrl());
181
+ return;
182
+ }
183
+
184
+
185
+ // get list of items to push (ids)
186
+ $newProductIds = $this->getFeaturedItemsFromServer($pageCategory, $pageInfo->getIndex());
187
+
188
+ // if don't have anything to show or got exception from getItemsIds(), stop here
189
+ if (is_null($newProductIds) || empty($newProductIds))
190
+ {
191
+ return;
192
+ }
193
+
194
+ // fix the new products list, read function docs for more info
195
+ $newProductIds = $this->fixNewProductsList($newProductIds, $productsCollection);
196
+ return $newProductIds;
197
+ }
198
+
199
+
200
+ /**
201
+ * push the featured items into the products collection.
202
+ * this should be called from the observer, after the products list was loaded.
203
+ */
204
+ private function pushFeaturedItems($productsCollection)
205
+ {
206
+
207
+ }
208
+
209
+ /**
210
+ * run this feature.
211
+ * @param $productsCollection - the loaded products list to push the featured items into.
212
+ */
213
+ protected function _runFeatureImp($productsCollection)
214
+ {
215
+ // make sure we should work on this page
216
+ if (!$this->shouldWorkOnThisPage($productsCollection))
217
+ {
218
+ return;
219
+ }
220
+
221
+ // get current block being rendered and make sure its "catalog/product_list"
222
+ // this prevents us from working on things like "recently viewed" and other special blocks.
223
+ $block = Mage::helper('shoptimally_core/blockUtils')->getCurrentBlock();
224
+ if (empty($block) || $block->getType() != 'catalog/product_list')
225
+ {
226
+ return;
227
+ }
228
+
229
+ // get ids to push
230
+ $newProductIds = $this->getFeaturedItemsIds($productsCollection);
231
+
232
+ // test again after fixed new products list if there's nothing new to show
233
+ if (empty($newProductIds))
234
+ {
235
+ $this->reportRejected();
236
+ return;
237
+ }
238
+
239
+ // get products utils helper
240
+ $productUtils = Mage::helper('shoptimally_core/productsUtils');
241
+
242
+ // convert to a list of products instances
243
+ $newProducts = $productUtils->getProducts($newProductIds);
244
+
245
+ // get original collection for statistics
246
+ $originalList = clone $productsCollection;
247
+
248
+ // add featured items to collection
249
+ $productUtils->insertProducts($productsCollection, $newProducts);
250
+ $this->reportSuccessReplacement($originalList, $productsCollection);
251
+ }
252
+ }
app/code/community/Shoptimally/FeaturedItems/Model/Observer.php ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\FeaturedItems
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Observer to liten when products are loaded, to inject the featured items into the collection
11
+ * before showing them.
12
+ */
13
+ class Shoptimally_FeaturedItems_Model_Observer
14
+ {
15
+ /**
16
+ * called after products list is loaded.
17
+ * in here we will run the feature main logic.
18
+ */
19
+ public function onProductsCollectionLoaded(Varien_Event_Observer $observer)
20
+ {
21
+ try
22
+ {
23
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled())
24
+ {
25
+ // get the main helper class and run this feature
26
+ Mage::helper('shoptimally_featureditems/main')->runFeature($observer->getCollection());
27
+ }
28
+ }
29
+ catch (Exception $e)
30
+ {
31
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
32
+ }
33
+
34
+ return $this;
35
+ }
36
+ }
app/code/community/Shoptimally/FeaturedItems/etc/config.xml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+
4
+ <modules>
5
+ <Shoptimally_FeaturedItems>
6
+ <version>0.1.0</version>
7
+ </Shoptimally_FeaturedItems>
8
+ </modules>
9
+
10
+ <global>
11
+
12
+ <helpers>
13
+ <shoptimally_featureditems>
14
+ <class>Shoptimally_FeaturedItems_Helper</class>
15
+ </shoptimally_featureditems>
16
+ </helpers>
17
+
18
+ <blocks>
19
+ <shoptimally_featureditems>
20
+ <class>Shoptimally_FeaturedItems_Block</class>
21
+ </shoptimally_featureditems>
22
+ </blocks>
23
+
24
+ <models>
25
+ <shoptimally_featureditems>
26
+ <class>Shoptimally_FeaturedItems_Model</class>
27
+ </shoptimally_featureditems>
28
+ </models>
29
+
30
+ </global>
31
+
32
+
33
+ <frontend>
34
+
35
+ <layout>
36
+ <updates>
37
+ <shoptimally_featureditems>
38
+ <file>shoptimally_featureditems.xml</file>
39
+ </shoptimally_featureditems>
40
+ </updates>
41
+ </layout>
42
+
43
+ <events>
44
+ <catalog_block_product_list_collection>
45
+ <observers>
46
+ <shoptimally_featureditems>
47
+ <type>model</type>
48
+ <class>shoptimally_featureditems/observer</class>
49
+ <method>onProductsCollectionLoaded</method>
50
+ </shoptimally_featureditems>
51
+ </observers>
52
+ </catalog_block_product_list_collection>
53
+ </events>
54
+
55
+ </frontend>
56
+
57
+
58
+ </config>
app/code/community/Shoptimally/FeaturedItems/readme.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ This module implement the "Featured Items" feature.
2
+
3
+ Featured Items is a feature that show the right items for the right client while browsing categories.
4
+ This feature works by listening to the event of loading category products, and injecting products into that collection that we get from our Shoptimally server.
5
+ There's also logic of prevent duplications and promoting existing products on page. The result is that the actual collection of products sent to render includes the featured items.
app/code/community/Shoptimally/FeaturedItemsAjax/Helper/Data.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ class Shoptimally_FeaturedItemsAjax_Helper_Data extends Mage_Core_Helper_Abstract
5
+ {
6
+ }
app/code/community/Shoptimally/FeaturedItemsAjax/controllers/AjaxController.php ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\FeaturedItemsAjax
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Controller for our ajax api, to query products dynamically before showing them as featured items.
11
+ * URL: // shoptimally_featureditemsajax/ajax/getproduct/ids/<product-ids-list>
12
+ *
13
+ * This feature is kind of unique in a sense that its 99% in javascript and barely have any server code.
14
+ * All the feature analytics etc are from javascript.
15
+ */
16
+ class Shoptimally_FeaturedItemsAjax_AjaxController extends Mage_Core_Controller_Front_Action
17
+ {
18
+ // return rendered product html from product id(s) (in get request)
19
+ // usage example: /shoptimally_featureditemsajax/Ajax/getProduct/ids/4
20
+ // or: /shoptimally_featureditemsajax/Ajax/getProduct/ids/ids/4,2,6
21
+ public function getProductAction()
22
+ {
23
+ // get product ids from get params
24
+ $productIds = $this->getRequest()->getParam('ids');
25
+ $productIds = explode("," , $productIds);
26
+
27
+ // create a special block to render the products html
28
+ $block = $this->getLayout()->createBlock('shoptimally_catalogsync/productsRenderer')
29
+ ->setTemplate('catalog/product/list.phtml')
30
+ ->setProductsList($productIds);
31
+
32
+ // convert to html and return
33
+ echo $block->toHtml();
34
+ }
35
+ }
app/code/community/Shoptimally/FeaturedItemsAjax/etc/config.xml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+
4
+ <modules>
5
+ <Shoptimally_FeaturedItemsAjax>
6
+ <version>0.1.0</version>
7
+ </Shoptimally_FeaturedItemsAjax>
8
+ </modules>
9
+
10
+ <global>
11
+
12
+ <helpers>
13
+ <shoptimally_featureditemsajax>
14
+ <class>Shoptimally_FeaturedItemsAjax_Helper</class>
15
+ </shoptimally_featureditemsajax>
16
+ </helpers>
17
+
18
+ <blocks>
19
+ <shoptimally_featureditemsajax>
20
+ <class>Shoptimally_FeaturedItemsAjax_Block</class>
21
+ </shoptimally_featureditemsajax>
22
+ </blocks>
23
+
24
+ <models>
25
+ <shoptimally_featureditemsajax>
26
+ <class>Shoptimally_FeaturedItemsAjax_Model</class>
27
+ </shoptimally_featureditemsajax>
28
+ </models>
29
+
30
+ </global>
31
+
32
+
33
+ <frontend>
34
+ <routers>
35
+ <shoptimally_featureditemsajax>
36
+ <use>standard</use>
37
+ <args>
38
+ <module>Shoptimally_FeaturedItemsAjax</module>
39
+ <frontName>shoptimally_featureditemsajax</frontName>
40
+ </args>
41
+ </shoptimally_featureditemsajax>
42
+ </routers>
43
+ </frontend>
44
+
45
+ </config>
app/code/community/Shoptimally/FeaturedItemsAjax/readme.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ This module is just like the "Featured Items" feature, but designed to work with cached pages via ajax.
2
+
3
+ It works like this:
4
+ 1. We inject page data from magento to the JavaScript client.
5
+ 2. The JavaScript query featured items ids from our Shoptimally server when page is loaded.
6
+ 3. We then use the ids and get the html used to render those products from an API we open in magento (a special route that gets product id and return html).
7
+ 4. The JavaScript inject the featured items into the page.
app/code/community/Shoptimally/FullSort/Block/ProductPlaceholders.php ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\FullSort
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This block inject the products placeholder grid used until full-sort products are loaded.
11
+ */
12
+ class Shoptimally_FullSort_Block_ProductPlaceholders extends Mage_Catalog_Block_Product_List
13
+ {
14
+
15
+ /**
16
+ * Retrieve loaded category collection
17
+ *
18
+ * @return Mage_Eav_Model_Entity_Collection_Abstract
19
+ **/
20
+ protected function _getProductCollection()
21
+ {
22
+ $placeholdersIds = array(404,405,406,407);
23
+ $collection = Mage::getModel('catalog/product')->getCollection()
24
+ ->addAttributeToFilter('entity_id', array('in' => $placeholdersIds))
25
+ ->addAttributeToSelect('*')
26
+ ->load();
27
+ return $collection;
28
+ }
29
+
30
+ /**
31
+ * We override this function so we won't dispatch the catalog_block_product_list_collection event.
32
+ * Note: we must add the toolbar as child because it is used internally to determine how to display
33
+ * the products. but we still need to not render it somehow.
34
+ */
35
+ protected function _beforeToHtml()
36
+ {
37
+ $toolbar = $this->getToolbarBlock();
38
+
39
+ // called prepare sortable parameters
40
+ $collection = $this->_getProductCollection();
41
+
42
+ // use sortable parameters
43
+ if ($orders = $this->getAvailableOrders()) {
44
+ $toolbar->setAvailableOrders($orders);
45
+ }
46
+ if ($sort = $this->getSortBy()) {
47
+ $toolbar->setDefaultOrder($sort);
48
+ }
49
+ if ($dir = $this->getDefaultDirection()) {
50
+ $toolbar->setDefaultDirection($dir);
51
+ }
52
+ if ($modes = $this->getModes()) {
53
+ $toolbar->setModes($modes);
54
+ }
55
+
56
+ // set collection to toolbar and apply sort
57
+ $toolbar->setCollection($collection);
58
+ $this->setChild('toolbar', $toolbar);
59
+
60
+ // call the base _beforeToHtml(), while skipping the Mage_Catalog_Block_Product_List::beforeToHtml()
61
+ return Mage_Catalog_Block_Product_Abstract::_beforeToHtml();
62
+ }
63
+ }
app/code/community/Shoptimally/FullSort/Helper/Data.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ class Shoptimally_FeaturedItemsAjax_Helper_Data extends Mage_Core_Helper_Abstract
5
+ {
6
+ }
app/code/community/Shoptimally/FullSort/controllers/AjaxController.php ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\FullSort
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Controller for our ajax api, to query products dynamically before showing them as featured items.
11
+ * URL: // shoptimally_fullsort/ajax/getproduct/ids/<product-ids-list>
12
+ *
13
+ * This feature is kind of unique in a sense that its 99% in javascript and barely have any server code.
14
+ * All the feature analytics etc are from javascript.
15
+ */
16
+ class Shoptimally_FullSort_AjaxController extends Mage_Core_Controller_Front_Action
17
+ {
18
+ // return rendered product html from product id(s) (in get request)
19
+ // usage example: /shoptimally_featureditemsajax/Ajax/getProduct/ids/4
20
+ // or: /shoptimally_featureditemsajax/Ajax/getProduct/ids/ids/4,2,6
21
+ public function getProductAction()
22
+ {
23
+ // get product ids from get params
24
+ $productIds = $this->getRequest()->getParam('ids');
25
+ $productIds = explode("," , $productIds);
26
+
27
+ // create a special block to render the products html
28
+ $block = $this->getLayout()->createBlock('shoptimally_catalogsync/productsRenderer')
29
+ ->setTemplate('catalog/product/list.phtml')
30
+ ->setProductsList($productIds);
31
+
32
+ // convert to html and return
33
+ echo $block->toHtml();
34
+ }
35
+ }
app/code/community/Shoptimally/FullSort/etc/config.xml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+
4
+ <modules>
5
+ <Shoptimally_FullSort>
6
+ <version>0.1.0</version>
7
+ </Shoptimally_FullSort>
8
+ </modules>
9
+
10
+ <global>
11
+
12
+ <helpers>
13
+ <shoptimally_fullsort>
14
+ <class>Shoptimally_FullSort_Helper</class>
15
+ </shoptimally_fullsort>
16
+ </helpers>
17
+
18
+ <blocks>
19
+ <shoptimally_fullsort>
20
+ <class>Shoptimally_FullSort_Block</class>
21
+ </shoptimally_fullsort>
22
+ </blocks>
23
+
24
+ <models>
25
+ <shoptimally_fullsort>
26
+ <class>Shoptimally_FullSort_Model</class>
27
+ </shoptimally_fullsort>
28
+ </models>
29
+
30
+ </global>
31
+
32
+ <frontend>
33
+ <layout>
34
+ <updates>
35
+ <shoptimally_fullsort>
36
+ <file>shoptimally/full_sort.xml</file>
37
+ </shoptimally_fullsort>
38
+ </updates>
39
+ </layout>
40
+ <routers>
41
+ <shoptimally_fullsort>
42
+ <use>standard</use>
43
+ <args>
44
+ <module>Shoptimally_FullSort</module>
45
+ <frontName>shoptimally_fullsort</frontName>
46
+ </args>
47
+ </shoptimally_fullsort>
48
+ </routers>
49
+ </frontend>
50
+
51
+ </config>
app/code/community/Shoptimally/FullSort/readme.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ Full page sort is a feature that actually change all the products on page, eg replace the original products with alternative ones.
2
+
3
+ It works like this:
4
+ 1. We inject another products grid right above the original products grid (so original grid still exist). All the products in the full-sort grid are placeholders with "loading..." graphics.
5
+ 2. We inject a javascript snippet that hide the original grid (before page load so no flickering), but also add a timer to show it again in case Shoptimally times out.
6
+ 3. When Shoptimally JavaScript loads and feature runs, it will start replacing the products placeholders with real products from Shoptimally.
7
+ 3. 1. Shoptimally will send product ids, and the js will use a special URL this module creates to convert them to htmls.
8
+ 4. After feature runs the timer to show the original products will be disabled, so the original products will never be shown.
app/code/community/Shoptimally/RelatedProducts/Helper/Data.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ class Shoptimally_RelatedProducts_Helper_Data extends Mage_Core_Helper_Abstract
5
+ {
6
+ }
app/code/community/Shoptimally/RelatedProducts/Helper/Main.php ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\RelatedProducts
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This helper provide the main functionality of the 'Related Products' feature.
11
+ */
12
+ class Shoptimally_RelatedProducts_Helper_Main extends Shoptimally_Core_Helper_FeatureBase
13
+ {
14
+ // define feature name (see Shoptimally_Core_Helper_FeatureBase for more info)
15
+ const NAME = "RelatedProducts";
16
+
17
+ // how many related products to get by default
18
+ const DEFAULT_RELATED_PRODUCTS_COUNT = 4;
19
+
20
+ // this is because after we reset the products collection this event is called again.
21
+ // so we use this var to make sure we are only called once per http request.
22
+ protected $alreadyGot = false;
23
+
24
+ /**
25
+ * return how many related products we want to show (ideally)
26
+ */
27
+ private function getMaxRelatedProductsCount()
28
+ {
29
+ return $this->getFeatureConfig("products_to_show_count", self::DEFAULT_RELATED_PRODUCTS_COUNT);
30
+ }
31
+
32
+ /**
33
+ * return if this feature should work on current page
34
+ */
35
+ private function shouldWorkOnThisPage()
36
+ {
37
+ // now return if enabled and page is a legal category page
38
+ return Mage::helper('shoptimally_core/pageInfo')->getPageType() == "product";
39
+ }
40
+
41
+ /**
42
+ * get related items for this product from Shoptimally server
43
+ * @param $productId - product id we want related items for
44
+ * @return - list of product ids.
45
+ */
46
+ private function getRelatedProductsFromServer($productId)
47
+ {
48
+
49
+ // send request to server
50
+ $response = $this->sendAjax("features/related_items/get", array("product" => $productId), 1);
51
+
52
+ // if exception or error skip
53
+ if (is_null($response) || $response->isError()) {
54
+ return null;
55
+ }
56
+
57
+ // get and return ids list from response
58
+ $idsList = Mage::helper('core')->jsonDecode($response->getBody());
59
+ return $idsList;
60
+ }
61
+
62
+ /**
63
+ * push the Related Products instead of the original related products collection.
64
+ * this should be called from the observer, after the related products list was loaded.
65
+ *
66
+ * @param $relatedProductsCollection - the related products list we want to replace.
67
+ */
68
+ protected function _runFeatureImp($relatedProductsCollection)
69
+ {
70
+ // if already called this request, skip
71
+ if ($this->alreadyGot)
72
+ {
73
+ return;
74
+ }
75
+
76
+ // make sure feature is enabled and should work on this page
77
+ if (!$this->shouldWorkOnThisPage())
78
+ {
79
+ return;
80
+ }
81
+
82
+ // get current block being rendered and make sure its the related products block
83
+ $block = Mage::helper('shoptimally_core/blockUtils')->getCurrentBlock();
84
+ if (empty($block) || $block->getType() != 'catalog/product_list_related')
85
+ {
86
+ return;
87
+ }
88
+
89
+ // this prevents endless recursive updates
90
+ $this->alreadyGot = true;
91
+
92
+ // get the main product id on this page
93
+ $productId = Mage::helper('shoptimally_core/pageInfo')->getMainProductId();
94
+ if (is_null($productId))
95
+ {
96
+ $this->reportError("Failed to get main product id!");
97
+ return;
98
+ }
99
+
100
+ // get list of items to push (ids) from server
101
+ $newProductIds = $this->getRelatedProductsFromServer($productId);
102
+
103
+ // if didn't get anything to show (or exception) stop here
104
+ if (is_null($newProductIds)) {return;}
105
+ if (empty($newProductIds))
106
+ {
107
+ $this->reportRejected();
108
+ return;
109
+ }
110
+
111
+ // get the ids of the original items for feature event
112
+ $originalRelated = clone $relatedProductsCollection;
113
+
114
+ // get the desired amount of related products to show (this will slice the list if we got more than desired)
115
+ $amount = $this->getMaxRelatedProductsCount();
116
+ $newProductIds = array_slice($newProductIds, 0, $amount);
117
+
118
+ // convert to a list of products instances
119
+ $productUtils = Mage::helper('shoptimally_core/productsUtils');
120
+ $newProducts = $productUtils->getProducts($newProductIds);
121
+
122
+ // replace the original related products collection with out related products.
123
+ $productUtils->replaceProducts($relatedProductsCollection, $newProducts);
124
+ $this->reportSuccessReplacement($originalRelated, $newProducts);
125
+ }
126
+ }
app/code/community/Shoptimally/RelatedProducts/Model/Observer.php ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\RelatedProducts
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * Observer to liten when related products are loaded, to replace them with our related products
11
+ * before showing them.
12
+ */
13
+ class Shoptimally_RelatedProducts_Model_Observer
14
+ {
15
+ /**
16
+ * called after related products list is loaded.
17
+ * in here we will run the feature main logic.
18
+ * */
19
+ public function onProductsCollectionLoaded(Varien_Event_Observer $observer)
20
+ {
21
+ try
22
+ {
23
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled())
24
+ {
25
+ // get the main helper class and run this feature
26
+ Mage::helper('shoptimally_relatedproducts/main')->runFeature($observer->getCollection());
27
+ }
28
+ }
29
+ catch (Exception $e)
30
+ {
31
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
32
+ }
33
+
34
+ return $this;
35
+ }
36
+
37
+ /**
38
+ * called when block is loaded, we check if its a related items block and push items inside.
39
+ * */
40
+ public function onBlockLoaded(Varien_Event_Observer $observer)
41
+ {
42
+ try
43
+ {
44
+ if (Mage::helper('shoptimally_core/config')->getIsEnabled())
45
+ {
46
+ // get block from event and make sure its the related products block
47
+ $block = $observer->getBlock();
48
+ if (get_class($block) == "Mage_Catalog_Block_Product_List_Related")
49
+ {
50
+ // TBD fix empty collection here
51
+ }
52
+ }
53
+ }
54
+ catch (Exception $e)
55
+ {
56
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception in observer!", $e);
57
+ }
58
+
59
+ return $this;
60
+ }
61
+ }
app/code/community/Shoptimally/RelatedProducts/etc/config.xml ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+
4
+ <modules>
5
+ <Shoptimally_RelatedProducts>
6
+ <version>0.1.0</version>
7
+ </Shoptimally_RelatedProducts>
8
+ </modules>
9
+
10
+ <global>
11
+
12
+ <helpers>
13
+ <shoptimally_relatedproducts>
14
+ <class>Shoptimally_RelatedProducts_Helper</class>
15
+ </shoptimally_relatedproducts>
16
+ </helpers>
17
+
18
+ <blocks>
19
+ <shoptimally_relatedproducts>
20
+ <class>Shoptimally_RelatedProducts_Block</class>
21
+ </shoptimally_relatedproducts>
22
+ </blocks>
23
+
24
+ <models>
25
+ <shoptimally_relatedproducts>
26
+ <class>Shoptimally_RelatedProducts_Model</class>
27
+ </shoptimally_relatedproducts>
28
+ </models>
29
+
30
+ </global>
31
+
32
+
33
+ <frontend>
34
+
35
+ <layout>
36
+ <updates>
37
+ <shoptimally_relatedproducts>
38
+ <file>shoptimally_relatedproducts.xml</file>
39
+ </shoptimally_relatedproducts>
40
+ </updates>
41
+ </layout>
42
+
43
+ <events>
44
+ <catalog_product_collection_load_after>
45
+ <observers>
46
+ <shoptimally_relatedproducts>
47
+ <type>model</type>
48
+ <class>shoptimally_relatedproducts/observer</class>
49
+ <method>onProductsCollectionLoaded</method>
50
+ </shoptimally_relatedproducts>
51
+ </observers>
52
+ </catalog_product_collection_load_after>
53
+
54
+ <core_layout_block_create_after>
55
+ <observers>
56
+ <shoptimally_relatedproducts>
57
+ <type>model</type>
58
+ <class>shoptimally_relatedproducts/observer</class>
59
+ <method>onBlockLoaded</method>
60
+ </shoptimally_relatedproducts>
61
+ </observers>
62
+ </core_layout_block_create_after>
63
+
64
+ </events>
65
+
66
+ </frontend>
67
+
68
+
69
+ </config>
app/code/community/Shoptimally/RelatedProducts/readme.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ This module implement the "Related Products" feature.
2
+
3
+ This feature replace the related products for specific products based on our ML logic.
4
+ It works by listening to the event that loads the related products list and replace them with product ids we get from our server.
app/code/community/Shoptimally/UpsaleCoupons/Block/Coupons.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\UpsaleCoupons
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This block inject the upsale coupons into the cart page
11
+ */
12
+ class Shoptimally_UpsaleCoupons_Block_Coupons extends Mage_Core_Block_Template
13
+ {
14
+ // will hold the coupon main helper, which implements the feature
15
+ protected $_main = null;
16
+
17
+ // init helpers and get coupon data
18
+ public function __construct()
19
+ {
20
+ try
21
+ {
22
+ $this->_main = Mage::helper('shoptimally_upsalecoupons/main');
23
+ $this->_main->runFeature();
24
+ }
25
+ catch (Exception $e)
26
+ {
27
+ Mage::helper('shoptimally_core/log')->warn("Unexpected exception while getting upsale coupon!", $e);
28
+ }
29
+ }
30
+
31
+ // return if coupons are enabled (this is used by the phtml)
32
+ public function isEnabled()
33
+ {
34
+ // note: don't use $this->_main->.. as it might not exist when this func is called.
35
+ return Mage::helper('shoptimally_upsalecoupons/main')->isEnabled();
36
+ }
37
+
38
+ // get the html part to put in the page header
39
+ public function getHeaderHtml()
40
+ {
41
+ return $this->_main->getHeaderHtml();
42
+ }
43
+
44
+ // get coupon html
45
+ public function getHtml()
46
+ {
47
+ return $this->_main->getHtml();
48
+ }
49
+ }
app/code/community/Shoptimally/UpsaleCoupons/Helper/Data.php ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ class Shoptimally_UpsaleCoupons_Helper_Data extends Mage_Core_Helper_Abstract
5
+ {
6
+ }
app/code/community/Shoptimally/UpsaleCoupons/Helper/Main.php ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ Mage::helper('shoptimally_core/handleFatals');
3
+
4
+ /**
5
+ * @package Shoptimally\UpsaleCoupons
6
+ * @version 1.0
7
+ * @author Shoptimally, Inc.
8
+ * @copyright Copyright � 2015 Shoptimally, Inc.
9
+ *
10
+ * This helper provide the main functionality of the 'Upsale Coupons' feature.
11
+ */
12
+ class Shoptimally_UpsaleCoupons_Helper_Main extends Shoptimally_Core_Helper_FeatureBase
13
+ {
14
+ // define feature name (see Shoptimally_Core_Helper_FeatureBase for more info)
15
+ const NAME = "UpsaleCoupons";
16
+
17
+ // will hold coupon data after feature execution.
18
+ private $_couponData = null;
19
+
20
+ // will hold the final coupon html after finish execution
21
+ protected $_html = "";
22
+ protected $_headerHtml = "";
23
+
24
+ /**
25
+ * if disabled set false coupon
26
+ * */
27
+ protected function _runIfDisabled()
28
+ {
29
+ $this->_couponData = array("have_coupon" => false);
30
+ }
31
+
32
+ /**
33
+ * push the Related Products instead of the original related products collection.
34
+ * this should be called from the observer, after the related products list was loaded.
35
+ *
36
+ * @param $relatedProductsCollection - the related products list we want to replace.
37
+ */
38
+ protected function _runFeatureImp($relatedProductsCollection)
39
+ {
40
+ // set default disabled coupon
41
+ $this->_runIfDisabled();
42
+
43
+ // get some required helpers
44
+ $prodUtils = Mage::helper('shoptimally_core/productsUtils');
45
+ $cookies = Mage::helper('shoptimally_core/cookie');
46
+ $remoteConfig = Mage::helper('shoptimally_core/remoteConfig');
47
+
48
+ // check if there's a previous coupon cookie
49
+ $lastCoupon = $cookies->getCookie("shoptimally_last_coupon", true);
50
+
51
+ // get all product ids in cart
52
+ $cartProducts = array();
53
+ $cartItems = $prodUtils->getCartItems();
54
+ foreach( $cartItems as $item )
55
+ {
56
+ $data = array(
57
+ "id" => $item->getProductId(),
58
+ "amount" => $item->getQty(),
59
+ "tax" => $item->getTaxAmount(),
60
+ );
61
+ array_push($cartProducts, $data);
62
+ }
63
+
64
+ // get total price
65
+ $grandTotal = $prodUtils->getCartTotal();
66
+
67
+ // prepare data to send to get_coupons
68
+ $data = array(
69
+ "total_price" => $grandTotal,
70
+ "cart_items" => $cartProducts,
71
+ "previous_coupon" => $lastCoupon,
72
+ );
73
+
74
+ // send request to server
75
+ $response = $this->sendAjax("features/upsale_coupons/get", $data, 2);
76
+
77
+ // if exception or error skip
78
+ if (is_null($response) || $response->isError()) {
79
+ return null;
80
+ }
81
+
82
+ // parse coupon data from response body
83
+ $this->_couponData = Mage::helper('core')->jsonDecode($response->getBody());
84
+
85
+ // if coupon enabled get the html template we wan't to use from the response
86
+ if ($this->_couponData["have_coupon"])
87
+ {
88
+ // get feature config
89
+ $couponSettings = $remoteConfig->get("feature_UpsaleCoupons");
90
+
91
+ // get html code for this coupon
92
+ $this->_html = $couponSettings[$this->_couponData["coupon_template"]];
93
+ $this->_headerHtml = $couponSettings["header_html"];
94
+ }
95
+ // if don't have coupon report rejected
96
+ else
97
+ {
98
+ $this->reportRejected();
99
+ }
100
+
101
+ // store coupon data in cookie
102
+ $cookies->setCookie("shoptimally_last_coupon", $this->_couponData, true);
103
+ }
104
+
105
+ // get coupon html, with all coupon data injected into html snippet
106
+ public function getHtml()
107
+ {
108
+ try
109
+ {
110
+ // if don't have a valid coupon, return empty html
111
+ if (!$this->_couponData["have_coupon"])
112
+ {
113
+ return "";
114
+ }
115
+
116
+ // get html and replace template parts
117
+ $html = $this->_html;
118
+
119
+ // do all textual replacements based on coupon data
120
+ foreach ($this->_couponData["replacements"] as $key => $value)
121
+ {
122
+ $html = str_replace($key, $value, $html);
123
+ }
124
+
125
+ // report success and return the html
126
+ $this->reportSuccess();
127
+ return $html;
128
+ }
129
+ catch(Exception $e)
130
+ {
131
+ $this->reportError("Exception while converting to html: " . $e->getMessage());
132
+ }
133
+ }
134
+
135
+ // return the coupon header html
136
+ public function getHeaderHtml()
137
+ {
138
+ return $this->_headerHtml;
139
+ }
140
+ }
app/code/community/Shoptimally/UpsaleCoupons/etc/config.xml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+
4
+ <modules>
5
+ <Shoptimally_UpsaleCoupons>
6
+ <version>0.1.0</version>
7
+ </Shoptimally_UpsaleCoupons>
8
+ </modules>
9
+
10
+ <global>
11
+ <helpers>
12
+ <shoptimally_upsalecoupons>
13
+ <class>Shoptimally_UpsaleCoupons_Helper</class>
14
+ </shoptimally_upsalecoupons>
15
+ </helpers>
16
+
17
+ <blocks>
18
+ <shoptimally_upsalecoupons>
19
+ <class>Shoptimally_UpsaleCoupons_Block</class>
20
+ </shoptimally_upsalecoupons>
21
+ </blocks>
22
+
23
+ <models>
24
+ <shoptimally_upsalecoupons>
25
+ <class>Shoptimally_UpsaleCoupons_Model</class>
26
+ </shoptimally_upsalecoupons>
27
+ </models>
28
+
29
+ </global>
30
+
31
+ <frontend>
32
+ <layout>
33
+ <updates>
34
+ <shoptimally_upsalecoupons>
35
+ <file>shoptimally/upsale_coupons.xml</file>
36
+ </shoptimally_upsalecoupons>
37
+ </updates>
38
+ </layout>
39
+ </frontend>
40
+
41
+ </config>
app/code/community/Shoptimally/UpsaleCoupons/readme.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ This module implement the "Upsale Coupons" feature.
2
+
3
+ Upsale Coupons is a feature that shows smart coupons to promote upsale while in cart page.
4
+ It works by adding a block to the cart page that loads the best coupon from Shoptimally server when user enters the cart.
app/design/frontend/base/default/layout/shoptimally/core.xml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <layout version="0.1.0">
3
+ <default>
4
+ <!-- inject the shoptimally header into head (this includes the js etc) -->
5
+ <reference name="head">
6
+ <block type="shoptimally_core/injectjs" name="shoptimally_js" as="shoptimally_js" template="shoptimally/injectjs.phtml" />
7
+ </reference>
8
+
9
+ <!-- inject basic page data into head (category name, page index, etc) -->
10
+ <reference name="head">
11
+ <block type="shoptimally_core/injectjs" after="-" name="shoptimally_js_extra" as="shoptimally_js_extra" template="shoptimally/injectjs_extras.phtml" />
12
+ </reference>
13
+
14
+ <!-- inject extended page data into the end of body, like product ids on page.
15
+ this data must come last because its it contain things that are not yet loaded while rendering header blocks -->
16
+ <reference name="before_body_end">
17
+ <block type="shoptimally_core/injectjs" after="-" name="shoptimally_js_extra_buttom" as="shoptimally_js_extra_buttom" template="shoptimally/injectjs_extras_buttom.phtml" />
18
+ </reference>
19
+ </default>
20
+ </layout>
app/design/frontend/base/default/layout/shoptimally/full_sort.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <layout version="0.1.0">
3
+ <shoptimally_fullsort>
4
+ <reference name="content">
5
+ <block type="shoptimally_fullsort/productPlaceholders" name="shoptimally_fullsort_holders" template="shoptimally/fullsort_placeholders.phtml"></block>
6
+ </reference>
7
+ </shoptimally_fullsort>
8
+ </layout>
app/design/frontend/base/default/layout/shoptimally/upsale_coupons.xml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <layout version="0.1.0">
3
+
4
+ <!-- the coupon itself -->
5
+ <checkout_cart_index>
6
+
7
+ <reference name="checkout.cart.form.before">
8
+ <block type="shoptimally_upsalecoupons/coupons" name="shoptimally_coupons" as="shoptimally_coupons" template="shoptimally/upsale_coupons.phtml" />
9
+ </reference>
10
+
11
+ </checkout_cart_index>
12
+
13
+ </layout>
app/design/frontend/base/default/template/shoptimally/fullsort_placeholders.phtml ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <div class="shoptimally-fullsort">
2
+ <h1>HELLO WORLD.</h1>
3
+ </div> <div style="clear: both;"></div>
app/design/frontend/base/default/template/shoptimally/injectjs.phtml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php if( $this->isEnabled() ): ?>
2
+
3
+ <!-- Shoptimally <?php echo $this->getVersion(); ?> -->
4
+ <script type="text/javascript">
5
+ var SHOPTIMALLY_ALT_DOMAIN = "<?php echo $this->getServerUrl(); ?>/";
6
+ var SHOPTIMALLY_API_KEY = "<?php echo $this->getApiKey(); ?>";
7
+ var SHOPTIMALLY_PLATFORM = "Magento";
8
+ </script>
9
+ <script type="text/javascript" id="shoptimally-src" <?php echo $this->getShoptimallyAsyncMode(); ?> src="<?php echo $this->getShoptimallyJsUrl(); ?>"></script>
10
+ <!-- / Shoptimally Javascript -->
11
+
12
+ <?php else: ?>
13
+
14
+ <!-- Note: Shoptimally (<?php echo $this->getVersion(); ?>) is temporarily disabled. -->
15
+ <!-- To enable Shoptimally, please seek the enable/disable option under 'Shoptimally' tab in the Magento admin panel. -->
16
+ <!-- for more info: http://shoptimally.com/ -->
17
+
18
+ <?php endif; ?>
app/design/frontend/base/default/template/shoptimally/injectjs_extras.phtml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php if( $this->isEnabled() ): ?>
2
+ <!-- Shoptimally Javascript extra page data -->
3
+ <script type="text/javascript">
4
+ var SHOPTIMALLY_PAGE_INFO = <?php echo $this->getPageInfo(); ?>;
5
+ </script>
6
+ <?php endif; ?>
app/design/frontend/base/default/template/shoptimally/injectjs_extras_buttom.phtml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php if( $this->isEnabled() ): ?>
2
+ <!-- Shoptimally Javascript extra page data - buttom -->
3
+ <script type="text/javascript">
4
+ SHOPTIMALLY_PAGE_INFO["products"] = <?php echo $this->getProductIds(); ?>;
5
+ </script>
6
+ <?php endif; ?>
app/design/frontend/base/default/template/shoptimally/upsale_coupons.phtml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ <?php if( $this->isEnabled() ): ?>
2
+ <?php echo $this->getHeaderHtml(); ?>
3
+ <div class="shoptimally-coupon">
4
+ <?php echo $this->getHtml(); ?>
5
+ </div> <div style="clear: both;"></div>
6
+ <?php endif; ?>
app/design/frontend/base/default/template/shoptimally/upsale_coupons_header.phtml ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <?php if( $this->isEnabled() ): ?>
2
+ <?php echo $this->getHeaderHtml(); ?>
3
+ <?php endif; ?>
app/etc/modules/Shoptimally_Analytics.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_Analytics>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_Analytics>
9
+ </modules>
10
+ </config>
app/etc/modules/Shoptimally_CatalogSync.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_CatalogSync>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_CatalogSync>
9
+ </modules>
10
+ </config>
app/etc/modules/Shoptimally_Core.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_Core>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_Core>
9
+ </modules>
10
+ </config>
app/etc/modules/Shoptimally_FeaturedItems.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_FeaturedItems>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_FeaturedItems>
9
+ </modules>
10
+ </config>
app/etc/modules/Shoptimally_FeaturedItemsAjax.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_FeaturedItemsAjax>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_FeaturedItemsAjax>
9
+ </modules>
10
+ </config>
app/etc/modules/Shoptimally_FullSort.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_FullSort>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_FullSort>
9
+ </modules>
10
+ </config>
app/etc/modules/Shoptimally_RelatedProducts.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_RelatedProducts>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_RelatedProducts>
9
+ </modules>
10
+ </config>
app/etc/modules/Shoptimally_UpsaleCoupons.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <Shoptimally_UpsaleCoupons>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ <version>0.1.0</version>
8
+ </Shoptimally_UpsaleCoupons>
9
+ </modules>
10
+ </config>
package.xml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <package>
3
+ <name>Shoptimally</name>
4
+ <version>1.1.03</version>
5
+ <stability>stable</stability>
6
+ <license uri="http://www.shoptimally.com/wp-content/uploads/2016/03/Shoptimally-Terms-Of-Use.pdf">Custom license</license>
7
+ <channel>community</channel>
8
+ <extends/>
9
+ <summary>Personalize your e-commerce site to match each customer's needs.</summary>
10
+ <description>* Shoptimally shows the right product to the right customer.&#xD;
11
+ * Shoptimally will increase the number of customers that purchase at your store.&#xD;
12
+ * Shoptimally automatically measures and chooses the right features for your site.</description>
13
+ <notes>Included features:&#xD;
14
+ 1. Core&#xD;
15
+ 2. Analytics&#xD;
16
+ 3. Catalog Sync&#xD;
17
+ 4. Featured Items&#xD;
18
+ 5. Featured Items Ajax&#xD;
19
+ 6. Upsale Coupons&#xD;
20
+ 7. Related Products&#xD;
21
+ 8. Interesting Products Sync&#xD;
22
+ 9. Htmls update&#xD;
23
+ 10. Features analytics</notes>
24
+ <authors><author><name>Ronen Ness</name><user>ronenness</user><email>ronen@shoptimally.com</email></author><author><name>Yoav Cafri</name><user>yoavcafri</user><email>yoav@shoptimally.com</email></author><author><name>Omer Nevo</name><user>omernevo</user><email>omer@shoptimally.com</email></author></authors>
25
+ <date>2016-03-07</date>
26
+ <time>14:25:17</time>
27
+ <contents><target name="magecommunity"><dir name="Shoptimally"><dir name="Analytics"><dir name="Helper"><file name="FeatureEvents.php" hash="5281825d73b74e9a18f7eb7b643f23c2"/><file name="UserEvents.php" hash="93607bae6fe7152732f9b746bbe9d910"/><file name="Utils.php" hash="ac9d767abd3b5528840b6b39570f6243"/></dir><dir name="Model"><file name="Observer.php" hash="e35c34d2412868ffd2e985bbdc90545d"/></dir><dir name="etc"><file name="config.xml" hash="7219e8800c455fd5b295ccf646ea6f34"/></dir><file name="readme.md" hash="d411cd45f4cdc507b152e8f84d2fafac"/></dir><dir name="CatalogSync"><dir name="Block"><file name="ProductsRenderer.php" hash="c78f8226256ab1838c3490aa8f852c2a"/></dir><dir name="Helper"><file name="InterestingList.php" hash="c86325968eb3a2cdf55666795bc12063"/><file name="TimeBased.php" hash="a6e2ab604079daa2a532c7bb40c8a919"/><file name="UpdateItemsHtmls.php" hash="2ab1592f6d77639d26848f983efcc840"/><file name="Utils.php" hash="238cf6327b0fdda29c7dab3a273adcce"/></dir><dir name="Model"><file name="Cron.php" hash="4ab95afe2e9e77f7cfe0de94cfdf5f75"/><file name="Observer.php" hash="d3a33e2f90852709aff2411da40c659e"/></dir><dir name="etc"><file name="config.xml" hash="f41a1456f70e90dfbc928e6df1e86199"/></dir><file name="readme.md" hash="ba5baa0302817b0a0b7ae6073e96f19c"/></dir><dir name="Core"><dir name="Block"><file name="Injectjs.php" hash="fdee65b9825627e1073860e2f3a3b44f"/></dir><dir name="Helper"><file name="BlockUtils.php" hash="f0e514f2ce2841b9728d9162434f4edb"/><file name="ClientData.php" hash="5229cc290b9d50e45c8cca48d67dd2af"/><file name="Config.php" hash="b799c2bf0a2896cc3ef340a83d2ce634"/><file name="Cookie.php" hash="b3864cd99048c1748ae1c82cce7e9337"/><file name="Data.php" hash="aec86f09e82ba149e7965433c0ddb2f1"/><file name="FeatureBase.php" hash="117bdca8cd423ef95da862a1a5d65cd8"/><file name="HandleFatals.php" hash="671e3efb4bd501bd69ac1ddd8014a999"/><file name="Log.php" hash="073fbdc932f338780555cf668a0491a0"/><file name="ObjectUtils.php" hash="b1a8cfc2b4bd7504e8a849072d7a327e"/><file name="PageInfo.php" hash="2b9db64894551009572f735f36bef1a7"/><file name="ProductsUtils.php" hash="ecc1167f91d61096f0e79be18ff7257a"/><file name="RemoteConfig.php" hash="0e4ef8cfa9eef2ceeb54fd9b604531b6"/><file name="Server.php" hash="4cb490d9bc80255a49e614ffffd1a8a5"/><file name="Storage.php" hash="cae8d8f118bf50c2df21bf2e39f65210"/><file name="UrlUtils.php" hash="81784ac8bb67aa3abe736f09ec2219c6"/></dir><dir name="Model"><file name="Cron.php" hash="d9eb246b45b9ffd5a72a2a90fcdf5da4"/><file name="Observer.php" hash="36146a1ae0905d9c5671838b59b06897"/></dir><dir name="controllers"><file name="DebugDataController.php" hash="5d59dd1ec4a3e2852f5462f8220ad67d"/></dir><dir name="etc"><file name="adminhtml.xml" hash="2970f7708270930c1cde66968b8a3e16"/><file name="config.xml" hash="a2b82f55853472bfe0d18c08d5a0b642"/><file name="system.xml" hash="2c57df84eef0ca8a3d27d946dbf41a77"/></dir><file name="readme.md" hash="0ee5d07c441ae719d958a77dc69e3eb9"/></dir><dir name="FeaturedItems"><dir name="Helper"><file name="Data.php" hash="5ceb27f0084fce7595e6a92d276fba08"/><file name="Main.php" hash="e05ace01c2298d3256325bd63b08900c"/></dir><dir name="Model"><file name="Observer.php" hash="95e4dc2e20d7ca914d5afe1999994e85"/></dir><dir name="etc"><file name="config.xml" hash="99b045752f80cc5cc09b8a2ebcd3c0be"/></dir><file name="readme.md" hash="451d5e135b12d57e53d9c1520fb29ee1"/></dir><dir name="FeaturedItemsAjax"><dir name="Helper"><file name="Data.php" hash="d64ea9ee4041cb48d9548b3d9a94f88f"/></dir><dir name="controllers"><file name="AjaxController.php" hash="a320209cba61b52c8fe962026ee10a06"/></dir><dir name="etc"><file name="config.xml" hash="bcd4deadf04ea949059fc556a321ce93"/></dir><file name="readme.md" hash="10b08dfc9a7d05d4cd9a30782e4fc931"/></dir><dir name="FullSort"><dir name="Block"><file name="ProductPlaceholders.php" hash="f714d15b8939e5480590af37900b24ac"/></dir><dir name="Helper"><file name="Data.php" hash="74b4f6ad22814907b4ce11aa7fa36ae4"/></dir><dir name="controllers"><file name="AjaxController.php" hash="9fdf4f09b3843ffa62b194fae31ca0a1"/></dir><dir name="etc"><file name="config.xml" hash="41d4c77c97455cf0a2a39bc4f67d2e00"/></dir><file name="readme.md" hash="be6d43d0c03294dfc1074750ce8d668f"/></dir><dir name="RelatedProducts"><dir name="Helper"><file name="Data.php" hash="55bba3fde756ad6cab9810469cca75a8"/><file name="Main.php" hash="a9a124ac6ef091828ac7e3cb5cbeef81"/></dir><dir name="Model"><file name="Observer.php" hash="fa1ad70a07b5f2b03ba7fc1e6073e277"/></dir><dir name="etc"><file name="config.xml" hash="3a89c90662fc7d4c2df174fa358fe7fd"/></dir><file name="readme.md" hash="dc6dd9072db87daf830ad96055903fd4"/></dir><dir name="UpsaleCoupons"><dir name="Block"><file name="Coupons.php" hash="1fd40dc2329b33b95ce37a549c219c72"/></dir><dir name="Helper"><file name="Data.php" hash="18bbb86307910707b702976eac7e495e"/><file name="Main.php" hash="ede426c04599fb426f61e70ca948aac4"/></dir><dir name="etc"><file name="config.xml" hash="8b66809745d750d3aadb592941ad6e69"/></dir><file name="readme.md" hash="edffe43009362fdb2c8deeb60752a5a6"/></dir></dir></target><target name="magedesign"><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><dir name="shoptimally"><file name="core.xml" hash="cdaa2452904bb32b5f50a182db1a2dc8"/><file name="full_sort.xml" hash="2ae66b05544ac9ef4e66f635e71d4a15"/><file name="upsale_coupons.xml" hash="f6931ee1aab64d2cbd60139d3729033d"/></dir></dir><dir name="template"><dir name="shoptimally"><file name="fullsort_placeholders.phtml" hash="6bd053d9e0192bd266f8352a22c10130"/><file name="injectjs.phtml" hash="40a2251735f9dffabadafc32726a43c6"/><file name="injectjs_extras.phtml" hash="528f9827e943f682cd484aca45fc62ee"/><file name="injectjs_extras_buttom.phtml" hash="033f8beb5ccc11a48641bdf7729287cf"/><file name="upsale_coupons.phtml" hash="100cbdd3a25ec068dd6dad2fd4311bf4"/><file name="upsale_coupons_header.phtml" hash="657d91cf143a45e7d9828539870c5018"/></dir></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="Shoptimally_Analytics.xml" hash="1761e7eed1fd972f28d629691fe32fa1"/><file name="Shoptimally_CatalogSync.xml" hash="c9d99d86f724ba77939407c627a462c5"/><file name="Shoptimally_Core.xml" hash="7e532b559d253787b136b366e40069c9"/><file name="Shoptimally_FeaturedItems.xml" hash="9991a991b358176e9e52abf1d33dce23"/><file name="Shoptimally_FeaturedItemsAjax.xml" hash="c9c86ad6c8060c70d9723063fa1a4dc5"/><file name="Shoptimally_FullSort.xml" hash="8d3a8eaec1be9af16eb42e74290bb45c"/><file name="Shoptimally_RelatedProducts.xml" hash="ed6e8cda84821b95294530c85f207044"/><file name="Shoptimally_UpsaleCoupons.xml" hash="1fde232d404822aa5b08d8e10637ee83"/></dir></target></contents>
28
+ <compatible/>
29
+ <dependencies><required><php><min>4.5.0</min><max>6.0.0</max></php><package><name>Mage_Core_Modules</name><channel>community</channel><min>1.5.0.0</min><max>1.9.9.9</max></package></required></dependencies>
30
+ </package>