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
- app/code/community/Shoptimally/Analytics/Helper/FeatureEvents.php +68 -0
- app/code/community/Shoptimally/Analytics/Helper/UserEvents.php +264 -0
- app/code/community/Shoptimally/Analytics/Helper/Utils.php +63 -0
- app/code/community/Shoptimally/Analytics/Model/Observer.php +106 -0
- app/code/community/Shoptimally/Analytics/etc/config.xml +52 -0
- app/code/community/Shoptimally/Analytics/readme.md +4 -0
- app/code/community/Shoptimally/CatalogSync/Block/ProductsRenderer.php +95 -0
- app/code/community/Shoptimally/CatalogSync/Helper/InterestingList.php +204 -0
- app/code/community/Shoptimally/CatalogSync/Helper/TimeBased.php +152 -0
- app/code/community/Shoptimally/CatalogSync/Helper/UpdateItemsHtmls.php +62 -0
- app/code/community/Shoptimally/CatalogSync/Helper/Utils.php +109 -0
- app/code/community/Shoptimally/CatalogSync/Model/Cron.php +95 -0
- app/code/community/Shoptimally/CatalogSync/Model/Observer.php +91 -0
- app/code/community/Shoptimally/CatalogSync/etc/config.xml +90 -0
- app/code/community/Shoptimally/CatalogSync/readme.md +2 -0
- app/code/community/Shoptimally/Core/Block/Injectjs.php +113 -0
- app/code/community/Shoptimally/Core/Helper/BlockUtils.php +34 -0
- app/code/community/Shoptimally/Core/Helper/ClientData.php +86 -0
- app/code/community/Shoptimally/Core/Helper/Config.php +196 -0
- app/code/community/Shoptimally/Core/Helper/Cookie.php +65 -0
- app/code/community/Shoptimally/Core/Helper/Data.php +14 -0
- app/code/community/Shoptimally/Core/Helper/FeatureBase.php +290 -0
- app/code/community/Shoptimally/Core/Helper/HandleFatals.php +29 -0
- app/code/community/Shoptimally/Core/Helper/Log.php +202 -0
- app/code/community/Shoptimally/Core/Helper/ObjectUtils.php +49 -0
- app/code/community/Shoptimally/Core/Helper/PageInfo.php +157 -0
- app/code/community/Shoptimally/Core/Helper/ProductsUtils.php +351 -0
- app/code/community/Shoptimally/Core/Helper/RemoteConfig.php +195 -0
- app/code/community/Shoptimally/Core/Helper/Server.php +175 -0
- app/code/community/Shoptimally/Core/Helper/Storage.php +233 -0
- app/code/community/Shoptimally/Core/Helper/UrlUtils.php +112 -0
- app/code/community/Shoptimally/Core/Model/Cron.php +41 -0
- app/code/community/Shoptimally/Core/Model/Observer.php +65 -0
- app/code/community/Shoptimally/Core/controllers/DebugDataController.php +381 -0
- app/code/community/Shoptimally/Core/etc/adminhtml.xml +26 -0
- app/code/community/Shoptimally/Core/etc/config.xml +89 -0
- app/code/community/Shoptimally/Core/etc/system.xml +57 -0
- app/code/community/Shoptimally/Core/readme.md +7 -0
- app/code/community/Shoptimally/FeaturedItems/Helper/Data.php +6 -0
- app/code/community/Shoptimally/FeaturedItems/Helper/Main.php +252 -0
- app/code/community/Shoptimally/FeaturedItems/Model/Observer.php +36 -0
- app/code/community/Shoptimally/FeaturedItems/etc/config.xml +58 -0
- app/code/community/Shoptimally/FeaturedItems/readme.md +5 -0
- app/code/community/Shoptimally/FeaturedItemsAjax/Helper/Data.php +6 -0
- app/code/community/Shoptimally/FeaturedItemsAjax/controllers/AjaxController.php +35 -0
- app/code/community/Shoptimally/FeaturedItemsAjax/etc/config.xml +45 -0
- app/code/community/Shoptimally/FeaturedItemsAjax/readme.md +7 -0
- app/code/community/Shoptimally/FullSort/Block/ProductPlaceholders.php +63 -0
- app/code/community/Shoptimally/FullSort/Helper/Data.php +6 -0
- app/code/community/Shoptimally/FullSort/controllers/AjaxController.php +35 -0
- app/code/community/Shoptimally/FullSort/etc/config.xml +51 -0
- app/code/community/Shoptimally/FullSort/readme.md +8 -0
- app/code/community/Shoptimally/RelatedProducts/Helper/Data.php +6 -0
- app/code/community/Shoptimally/RelatedProducts/Helper/Main.php +126 -0
- app/code/community/Shoptimally/RelatedProducts/Model/Observer.php +61 -0
- app/code/community/Shoptimally/RelatedProducts/etc/config.xml +69 -0
- app/code/community/Shoptimally/RelatedProducts/readme.md +4 -0
- app/code/community/Shoptimally/UpsaleCoupons/Block/Coupons.php +49 -0
- app/code/community/Shoptimally/UpsaleCoupons/Helper/Data.php +6 -0
- app/code/community/Shoptimally/UpsaleCoupons/Helper/Main.php +140 -0
- app/code/community/Shoptimally/UpsaleCoupons/etc/config.xml +41 -0
- app/code/community/Shoptimally/UpsaleCoupons/readme.md +4 -0
- app/design/frontend/base/default/layout/shoptimally/core.xml +20 -0
- app/design/frontend/base/default/layout/shoptimally/full_sort.xml +8 -0
- app/design/frontend/base/default/layout/shoptimally/upsale_coupons.xml +13 -0
- app/design/frontend/base/default/template/shoptimally/fullsort_placeholders.phtml +3 -0
- app/design/frontend/base/default/template/shoptimally/injectjs.phtml +18 -0
- app/design/frontend/base/default/template/shoptimally/injectjs_extras.phtml +6 -0
- app/design/frontend/base/default/template/shoptimally/injectjs_extras_buttom.phtml +6 -0
- app/design/frontend/base/default/template/shoptimally/upsale_coupons.phtml +6 -0
- app/design/frontend/base/default/template/shoptimally/upsale_coupons_header.phtml +3 -0
- app/etc/modules/Shoptimally_Analytics.xml +10 -0
- app/etc/modules/Shoptimally_CatalogSync.xml +10 -0
- app/etc/modules/Shoptimally_Core.xml +10 -0
- app/etc/modules/Shoptimally_FeaturedItems.xml +10 -0
- app/etc/modules/Shoptimally_FeaturedItemsAjax.xml +10 -0
- app/etc/modules/Shoptimally_FullSort.xml +10 -0
- app/etc/modules/Shoptimally_RelatedProducts.xml +10 -0
- app/etc/modules/Shoptimally_UpsaleCoupons.xml +10 -0
- 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.
|
11 |
+
* Shoptimally will increase the number of customers that purchase at your store.
|
12 |
+
* Shoptimally automatically measures and chooses the right features for your site.</description>
|
13 |
+
<notes>Included features:
|
14 |
+
1. Core
|
15 |
+
2. Analytics
|
16 |
+
3. Catalog Sync
|
17 |
+
4. Featured Items
|
18 |
+
5. Featured Items Ajax
|
19 |
+
6. Upsale Coupons
|
20 |
+
7. Related Products
|
21 |
+
8. Interesting Products Sync
|
22 |
+
9. Htmls update
|
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>
|