MB200 week calculation bug

一个月前,我的 MB200 显示有系统更新可用,我就升级了一下。升级了以后很失望,firmware version 仍是 1.5,看来 motorola 不打算让 MB200 用上 android 1.6 以上版本了,有空了我还得自行升级。

用了一个月,我发现了 MB200 现有的版本 (Blur version 1.3.39) 居然有个日期计算上的 bug。比如今天是星期天,它会认为是星期一。这样,我设定的闹钟,闹周一到周五,结果它闹的是周日到周四;10月的最后一个星期天的凌晨从夏令时切换到冬令时,结果它提前一天星期六就让我用上冬令时了,搞得我周六一早起来看时间后总觉得怪怪的。

如果我来升级,我就上 android 2.2。

Double rainbow is nothing special

Double Rainbow over Devon
Double Rainbow over Devon

This week I read a news on dailymail After seven years and 2,000 photographs… a double delight as rainbow lights up Devon

By Daily Mail Reporter
Last updated at 9:26 AM on 27th October 2010

A photographer who has taken pictures of the view from his office window every day for seven years has finally struck gold by snapping this stunning double rainbow.

Andy Kyle, 53, captured the weather phenomenon as it appeared to create a bridge over the river at Dartmouth in Devon.

Andy, who runs a website called The View From The Dartmouth Office, has been posting pictures of the vista each day for the past seven years.

He said he had taken more than 2,000 images of the view before he snapped the beautiful double rainbow.
Howzat? Sometimes a dim secondary rainbow is seen outside the primary rainbow. Secondary rainbows are caused by a double reflection of sunlight inside the raindrops

Howzat? The ‘bridge’ over the river at Dartmouth in Devon. Sometimes a dim secondary rainbow is seen outside the primary rainbow. They are caused by a double reflection of sunlight inside the raindrops

Sometimes a fainter ‘secondary rainbow’ can be seen outside the main rainbow.

Secondary rainbows are caused by a double reflection of sunlight inside the raindrops, and appear at an angle of 50–53°.

If you look closely you can see the colours of the secondary rainbow are inverted, with blue on the outside and red on the inside.

The secondary rainbow is fainter than the primary because more light escapes from two reflections than one and because the rainbow itself is spread over a greater area of the sky.

The effect is quite ghostly, and makes for a stunning photograph, although it makes looking for the pot of gold slightly more tricky.

I did not think about it too much. But today when I was taking my friend Erin to Heathrow Terminal 3, I saw double rainbow just above the airport. It was a pity I had no hands free to take a photo to compete because I was busy driving my car.

When I am back home, I search for “double rainbow” photos, I find it is nothing special but reported many times in many places. If I head up a bit often, I may see double rainbow again very soon.

Magento promotional free shipping does not apply to tablerate

Magento 自有的 shipping calculation 已足够强大,如果谁还说不够用,我就会说是经营上的问题。

最近我被要求做这么一件事,在一个 storeview 下,对部分商品不论金额大小,一律 free shipping。一听之下我觉得可以用 cart salesrule free shipping action 来实现,实际尝试后却发现,cart salesrule free shipping 不能对 tablerate 起作用。而 tablerate 是 frontend 唯一一个 shipping method,被设置强制选择,顾客不会被问到 choose shipping method。在这个逻辑下,如果我启用 free shipping method,checkout 就多问一个问题,老板说这不好。而 flatrate 另有他用(backend 接单时对特殊地区额外征收运费),也无法拿它来实现部分商品 free shipping。

这件事最后用一个普通 discount 抵消 shipping charge 的方式迂回解决,但解决得很别扭。我认为

  • shipping rates 是一种全局设置,就算能,也不应该为某些商品创建例外的;
  • 如果想为某些商品创建例外,那么再想一下为什么要创建例外,比如这些商品针对是一群特殊用户,如果 cross selling 对特殊用户不起作用,那就专门为他们建一个 store 或 website;
  • 新建 store 或 website 的成本与销售预期收益相比,是否值得?如果不值得,再反思一下是否值得为这些商品创建例外;
  • 自己写一个 shipping module 肯定可以实现任何稀奇古怪的 shipping rules,但写程序不是为了照顾 business 复杂化的需要。

Place Magento model, resource, collection class at your will

Most Magento model, resource, collection are placed this way:

  • Model is located at Model folder
  • Resource is located at Model/Mysql4
  • Collection is located at Model/Mysql4/(ClassName)

What if you want organise them in a different way? Say how place all of 3 classes under one folder?

I enclose a complete a working sample.

1. Model class


class Qian_Msdk_Model_MediaGallery_Mg extends Mage_Core_Model_Abstract
{
public function _construct()
{
parent::_construct();
$this->_init('msdk_mg/resource');
}
}

2. Resource class


class Qian_Msdk_Model_MediaGallery_Resource extends Mage_Core_Model_Mysql4_Abstract
{
public function _construct()
{
$this->_init('msdk_mg/product_attribute_media_gallery', 'value_id');
}
}

3. Collection class


class Qian_Msdk_Model_MediaGallery_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{
public function _construct()
{
parent::_construct();
$this->_init('msdk_mg/mg');
}
}

4. config.xml


<?xml version="1.0" encoding="UTF-8"?>
<config>
<global>
<models>
<msdk_mg>
<class>Qian_Msdk_Model_MediaGallery</class>
<resourceModel>msdk_mg_mysql4</resourceModel>
</msdk_mg>
<msdk_mg_mysql4>
<class>Qian_Msdk_Model_MediaGallery</class>
<entities>
<product_attribute_media_gallery>
<table>catalog_product_entity_media_gallery</table>
</product_attribute_media_gallery>
<product_attribute_media_gallery_value>
<table>catalog_product_entity_media_gallery_value</table>
</product_attribute_media_gallery_value>
</entities>
</msdk_mg_mysql4>
</models>
</global>
</config>

this vs $(this) in jQuery

I don’t know why, but this is how it happens.

When I make a jQuery plugin, the skeleton is

(function($) {
$.fn.myplugin = function() {
return this;
};
})(jQuery);

And if I need an each operation inside plugin, the skeleton can be

(function($) {
$.fn.myplugin = function() {
//outside each, this.width() can work
return this.each(function() {
//inside each, only $(this).width() works
});
};
})(jQuery);

The interesting thing is inside each, I must use “$(this)” to run jQuery functions, but outside each, I can also use “this”.

I don’t know why now, but I think I will know some day.

How to read Magento product custom options correctly?

Magento product custom options 的存储结构比较“绕”,我绕了好长时间才解开。

假设 $product 有两个 custom options,一个是 textarea input type,另一个是 multiple select input type:

先用 $product->getOptions(),得到是包含两个 custom options 的集合;
然后 foreach 一下,得到每个 $option。对第一个 textarea 类型的,处理起来相对简单,用 $option->getData(‘title’) 查看 title,以此类推。对第二个 multiple select 类型的,用 $option->getData() 只能查看概况,对于 select 里的每个 option (对应 <select><option>…</option></select>),还得用 $option->getValues(),然后再次 foreach 才能得到 select option。

够复杂吧。

How to pager array items in Magento

The Mage class Mage_Page_Block_Html_Pager is handy to pager collection items, but it is designed to work with a collection derived from Mage_Core_Model_Mysql4_Collection_Abstract.

Now I run into a situation to pager arbitrary array items. The array items are pre-built and not loaded via a resource model, so the pager lost a clue how to control which items to display. (Which makes me think – Magento is powerful, but not so powerful without the help of mysql – Magento collection functionality rely heavily on mysql operation – it is a bit off topic.)

So I need to tell the pager how to load the array after array items are loaded already. The following is my solution class.

<?php
class Qian_Msdk_Model_PagerableCollection extends Varien_Data_Collection {

/***
* load only current page items
*/
public function load($printQuery = false, $logQuery = false) {
if ($this->isLoaded()) {
return $this;
}
$this->loadForPager();
$this->_setIsLoaded();
return $this;
}

public function loadForPager() {
if (!$this->getPageSize()) {  //not pagerized
return $this;
}
$items = array();
$currentPage = $this->getCurPage();
$i = 0;
foreach ($this->_items as $item) {
if ($i < ($currentPage-1) * $this->getPageSize()) {
$i++;
}
elseif ($i >= $currentPage * $this->getPageSize()) {
break;
}
else {
$items[] = $item;
$i++;
}
}
$this->_items = $items;
return $this;
}

/***
* get the size of collection before pagerize
*/
public function getSize()
{
if (is_null($this->_totalRecords)) {
$this->_totalRecords = count($this->_items);
}
return intval($this->_totalRecords);
}

}

A universal recursion method for Magento

Magento 历遍 category 时用的 Tree/Node 模型,我看了一知半解。当我把 CMS page 整理成树状,想对所有 pages 如同 category 历遍操作时,我抛开 Tree/Node,就写了两个简单的方法来套用(感谢 php 5.3 让这方法成为可能)。

public function recurseSubpages($function, &$params = null) {
$this->_recurse($this, $function, 0, $params);
}

protected function _recurse($page, $function, $level, &$params = null) {
$function($page, $level, $params);
foreach ($page->getSubpageCollection() as $subpage) {
$this->_recurse($subpage, $function, $level + 1, $params);
}
}

若要 CMS page 历遍 someOperation(),则只要临时定义一个 $function,然后调用 $page->recurseSubpages(),具体代码是

$function = function($page, $level, &$params) {
$page->someOperation();
};
Mage::getModel('cms/page')->recurseSubpages($function);

$params 是用来回传参数的,如果不需要回传,可以省略。

我认为这样让复杂的递归变得很简单,可是在想做一张 html 格式的 CMS page sitemap (用<ul><li>…</li></ul>层次嵌套体现 CMS page 的层次,如同 Magento top navigation 那段代码。顺便说一下,我认为 Magento category sitemap 的代码不够好,它把树形结构线性化了,不够 semantic)时,遇到困难。当时我觉得要在 _recurse() 里再次调用 _recurse() 之前和之后有条件地插入一些操作,不是简单定义一个 $function 可以做到的。

所以第一版的 CMS page sitemap 绕开了我自己的 page recurse 模型独自写了很长一段代码,大意如下:

public function prepareSitemap() {
if ($id = Mage::getSingleton('cms/page')->getId()) {
$page = Mage::getModel('cpfp/page')->load($id);
}
else {
$page = Mage::getModel('cpfp/page');
}
$this->_output = self::_recurseSitemap($page, 0);
return $this;
}

protected function _recurseSitemap($page, $level) {
$output = '';
$hasId = ($page->getId())?true:false;
$title = Mage::helper('cpfp')->htmlEscape($page->getTitle());

foreach ($page->getSubpageCollection() as $subpage) {
$output .= self::_recurseSitemap($subpage, $level+1);
}

if ($hasId) {
if ('xhtml' == $this->_outputFormat) {
$pageContent = "<a href='{$this->getUrl($page->getIdentifier())}'>" . $title . '</a>';
if ($output) {
$output = "<li>$pageContent<ul>$output</ul></li>";
}
else {
$output = "<li>$pageContent</li>";
}
}
elseif ('text' == $this->_outputFormat) {
$pageContent = str_repeat("\t", $level) . $title . "\n";
$output = $pageContent . $output;
}
else {
//do nothing
}
}
else { //!$hasId
//do nothing
}

if ($level == 0 && $output && 'xhtml' == $this->_outputFormat) { //final wrap here
$output = '<ul>' . $output . '</ul>';
}
return $output;
}

以上代码是放在一个 Block 类里,该 Block 没有使用 template 直接输出了 html。Magento top navigation 也是由 Mage_Catalog_Block_Navigation 直接输出了 html,所以当时我想,输出递归结果也只能是这样了。

过了一段时间,突然有了灵感,其实还有更好的方法输出递归结果!具体步骤是:

  1. 定义这么一个 $function,把当前 $level 信息存储到当前 $page 里,把 $page 压入 collection 通过 $params 返回;
  2. 调用 recurseSubpages($function, $params) 历遍 CMS pages,就得到了树状顺序、包含层次信息的 collection;
  3. 在 template 里 foreach $collection 的元素时,比对前一个元素和后一个元素和当前元素的 level
    • 如果 $levelFormer = $levelCurrent,而且 $levelCurrent = $levelLatter,输出<li>当前元素</li>
    • 如果 $levelFormer = $levelCurrent,而且 $levelCurrent < $levelLatter,输出<li>当前元素
    • 如果 $levelFormer = $levelCurrent,而且 $levelCurrent > $levelLatter,输出<li>当前元素</li></ul></li>
    • 如果 $levelFormer < $levelCurrent,而且 $levelCurrent = $levelLatter,输出<ul><li>当前元素</li>
    • 如果 $levelFormer < $levelCurrent,而且 $levelCurrent < $levelLatter,输出<ul><li>当前元素
    • 如果 $levelFormer < $levelCurrent,而且 $levelCurrent > $levelLatter,输出<ul><li>当前元素</li></ul></li>
    • 如果 $levelFormer > $levelCurrent,而且 $levelCurrent = $levelLatter,输出<li>当前元素</li>
    • 如果 $levelFormer > $levelCurrent,而且 $levelCurrent < $levelLatter,输出<li>当前元素
    • 如果 $levelFormer > $levelCurrent,而且 $levelCurrent > $levelLatter,输出<li>当前元素</li></ul></li>

一一列出 template 的 9 种情况只为理清思路,实际代码并不需要分 9 种情况处理。不管怎样,我把历遍和表现相分离了,相比 Mage_Catalog_Block_Navigation 的代码更体现 MVC 的精髓。

讲了这么多,如果不摆完整的例子出来,估计谁也看不明白。那就请关注一下 Cpfp (Cms Page Foot Print)这个模块吧,在 Cpfp 模块中,我实现了 CMS page 的层次化,附送 CMS Page Sitemap,并修改了 CMS page breadcrumb 以充分体现页面的层次。有人用 category page 去构筑需要层次的内容,有人用 blog 模块去管理层次内容,有人干脆整合 wordpress 去管理内容。我的 Cpfp 只是用另一种方法去实现内容的层次,出发点是在内容表现方式上,CMS page 比 category 要强大,我在不修改 Magento 原有 cms_page 数据表的前提下实现层次化,是非常“绿色”、非常“轻量级”的模块。

Magento extension project: Grouped DIY Product

这是我用了 Magento 后站在需求分析的角度写得相对完整的文档,以前都直接 get hands dirty 了。

What is the motive?

We want to sell Christmas selections of products. Usually each unit product is of very low value, but the total value of selection is significant. If we pre-define the selection as a product, we can only create a limited number of selection products. Actually, we do not know what our customers want, so we leave the choice of selections to customers.

Why not use Simple Product?

As mentioned above, we do not think our pre-defined products will meet customers’ demand. Even we work hard to create hundreds of products, there are still millions of possible combinations left out.

Why not use Grouped Product?

Grouped Product provides a facility to add multiple products to cart in one go, but indeed products are added separately. Looking at the sales_flat_quote_item table after adding a Grouped Product to cart, I find each item does not have a parent item. Grouped Product is not fit for us because we have comprehensive promotion rules. Shopping cart takes more than 10 seconds to load when 10 items in cart. Customers’ shopping experience is even worse if they mix order Christmas selection with normal products.

There are also some limitations of Grouped Product.

  1. It requires Simple Products before grouping. Our Christmas products are nothing new but dispersed pieces of normal products. I have spent hours and hours in eliminating duplicated information between related products. I think it is extremely bad to go back to the old route duplicating images, descriptions, specifications, prices, inventory quantity and location from normal products to Christmas Simple Products.
  2. The price of Grouped Product can not be set, which means we do not have a control on selling price as a whole. Although we can still use cart sales rule for Christmas selection, it even slows down everything.

Why not use Configurable Product?

Configurable Product offers configuration over shared attributes. It is difficult to offer configuration over product choice, and NOT possible to associate products of different attribute sets into one Configurable Product. Configurable Product also require price difference being set up, which is not synchronised when associated product price changes. Its typical usage is to sell a style of garment offering XL, L, M, S, XS configuration (no price difference).

Why not use Bundle Product?

Bundle Product offer add-ons. Each add-on product quantity is pre-defined. Customers can only say yes or no to add-ons, but can not change the quantities. Its typical usage is selling PC with all-in-one printer, anti-virus software, insurance.

What we want to achieve?

  1. Define where unit product come from (from existing normal products) without creating another Simple Product. For example, we are selling 500 Santa-on-ice bags as a normal product. If we now extract 1 Santa-on-ice bag and add to Christmas selection, we just re-use all product information from the normal product.
  2. Define the selection freely and easily. Here is the list of fields for defining a product.
    • SKU of normal product
    • Piece Price (overwriting the price of normal product)
    • Piece Quantity (overwriting the Bunch Quantity of normal product)
    • Product name (We may want to describe it differently from the normal product name)
  3. Define an overall price (overwriting the total of Piece Price) to serve as lightweight promotion before adding to cart.
  4. If customers would like to know more about selection product information, they can click and go to normal product page.
  5. Allow drag and drop product thumbnails to give fun shopping experience. The progress of making choices is similar to a sub-shopping-cart remembering what have been selected before adding to cart.

What we do not want to achieve?

  1. No stock control on unit products.
  2. No price relations between unit product and normal product.
  3. No GUI customised for creating Grouped DIY Product in admin end. In the time scale, I am focusing on improving shopping experience in customer end.

Detailed approach

I call it Grouped DIY Product as terminology must be very clear – it is not Magento’s native Grouped Product. The new product type is built on top of Simple Product because Simple Product works better in the admin end. However, how order is assembled is closest to Grouped Product. Grouped DIY Product lives in the best of both worlds.

Counter down for Magento CMS blocks

我做了一个 Magento CMS static blocks 里用的倒计时 block。虽然功能比较简单,但还是比较智能的,所以还是想自我表彰一下。这不是独立的模块,我也没打算就拿这么单一的功能去生成一个模块,类似的功能我都会合并在一个 Msdk (Magento SDK) 模块里。


<?php
/***
* Usage:
*
* Example 1:
* {{block type="msdk/date_countdown" deadline="2010-10-31" before="Halloween in %d days!!" after="Happy Halloween!"}}
*
* Example 2:
* {{block type="msdk/date_countdown" deadline="2010-10-31 19:35:00" before="Lucky draw in %i minute!!" after="" smart_plural="true"}}
*
* Obviously, "deadline" value is required. And it should be fed in ISO 8601 format.
*
* "before" and "after" are the message strings to be used before and after deadline respectively.
* If any of them is not set, it uses "%h" as default.
* If you want the message string be empty, you must set it to "", explicitly. (Not setting it will result in "%h")
* You can use "%d" for days, "%m" for months, ... in the message strings. For a complete list of usable symbols, visit
* http://www.php.net/manual/en/dateinterval.format.php
*
* "smart_plural" is a switch you can turn it on using smart_plural="true" (It is default to false).
* When it is on, it looks for singular words in the message strings, and turn them into plurals when appropriate.
* This feature is in experiment stage, so it may not be so smart.
*/
class Qian_Msdk_Block_Date_Countdown extends Mage_Core_Block_Template {

//unix timestamp
protected $_deadline;

protected $_before = '%h'; //default to hour diff

protected $_after = '%h'; //default to hour diff

protected $_smartPlural = false; //default to false;

//DateInterval
protected $_interval; //to store value used by different methods, avoid passing value

protected function _toHtml() {
$now = Mage::getModel('core/date')->date();
$dateNow = date_create($now);
$dateDeadline = date_create($this->_deadline);
$interval = $dateNow->diff($dateDeadline);
if ($interval->format('%R%s') > 0) {  //before deadline
$display = $this->_before;
}
else { //after deadline
$display = $this->_after;
}

$this->_interval = $interval;
$display = $this->_pluralize($display);
return $interval->format($display);
}

public function setDeadline($strDeadline) {
$this->_deadline = Mage::getModel('core/date')->date($strDeadline);
return $this;
}

public function setBefore($before) {
$this->_before = $before;
return $this;
}

public function setAfter($after) {
$this->_after = $after;
return $this;
}

public function setSmartPlural($smartPlural) {
if ($smartPlural == 'true' || $smartPlural == 'yes' || $smartPlural == 1) {
$this->_smartPlural = true;
}
else {
$this->_smartPlural = false;
}
return $this;
}

protected function _pluralize($str) {
if (!$this->_smartPlural) {  //smart plural not turned on, no further processing
return $str;
}
$pattern = '/(.*)(%[YyMmDdaHhIiSs])(\s+)([a-zA-Z]+)([^a-zA-Z]|$)(.*)/Ui';
return preg_replace_callback($pattern, "self::_pluralizeMatches", $str);
}

protected function _pluralizeMatches($matches) {
return $matches[1] .
Mage::helper('msdk/plural')->english($this->_interval->format($matches[2]), $matches[4]) .
$matches[5] . $matches[6];
}
}