22 октября 2009 г.

Magento, products sitemap

На сайте есть обычные продукты, которые можно продавать, и есть специальная категория Glossary, в которой находятся определения - такие продукты нельзя купить :) Т.е. список определений сделан через категорию, это оказалось достаточно удобно.

Проблема в том, что автогенерируемая страница с картой сайта показывает все активные продукты, в том числе и из категории Glossary. Вот этого и надо избежать. Да, и ещё нужно отсортировать продукты по алфавиту.

Сначала я попробовал сделать категорию Glossary неактивной. Ничего не вышло, Sitemap по-прежнему отображал все продукты. Вариант "выключить" все продукты категории Glossary - вообще не вариант :) Т.е. нигде в настройках в админике, и в xml, нельзя задать ни список категорий для исключения, ни порядка сортировки (может я плохо искал?).

Значит надо делать это вручную. Можно либо поменять ядрёные файлы (оставив нетронутыми шаблонные файлы) - блок catalog/seo_sitemap_product, изменить его так, что бы он исключал категорию Glossary и сортировал продукты. Понятно, почему это плохой вариант, и мы так делать не будем :) Вместо этого мы будем менять только шаблонные файлы. Вообще решение этой проблемы добавит немало безнес-логики в шаблоны. Не очень хорошо, но с этим можно жить :) Зато без переделки ядрёных файлов.

Шаблон странички Sitemap находится в файле template/catalog/seo/sitemap.phtml. Существующий код берёт коллекцию всех продуктов (полученную в соответствующем блоке) и выводит все её элементы.

Мы добавим новый массив, который будет содержать продукты, теже объекты, что и коллекция. Но этот массив не будет содержать "продуктов" категории Glossary. После формирования массива вызовем функцию usort и отсортируем массив по названиям продуктов.

Что бы узнать, в каких группах находится текущий продукт, нужно вызвать метод getCategoryIds - он вернёт массив id категорий. Только нужно не забывать, что один и тот же шаблон template/catalog/seo/sitemap.phtml используется для вывода как продуктов, так и категорий. Если вызвать getCategoryIds у объекта-категории, он вернёт null. Нужно специально обработать этот случай.

Итак, вот код, формирующий новый массив без "продуктов" категории Glossary, и упорядоченный по именам объектов:
<?php $_items = $this->getCollection(); ?>
<?php
    // Original collection is not ordered. To sort items by name
    // we'll use array, then we'll sort it by name. Array will
    // contain the same objects as original collection
    $items = array();

    // Create our array, excluding Glossary category (id == 63)
    foreach ($_items as $_item) {
        if (is_array($_item->getCategoryIds()) && in_array(63, $_item->getCategoryIds()))
            continue;
        $items[] = $_item;
    }

    function cmp($a, $b) {
        return strcmp($a->getName(), $b->getName());
    }

    usort($items, cmp);
?>

Остальной код, по выводу продуктов, остаётся нетронутым, кроме перебора не коллекции $_items, а нового массива $items.

Сразу же после просмотра результата стал заметен баг - хотя продукты из категории Glossary теперь не отображаются, кол-во продуктов выводимое сразу перед списком продуктов (130 Item(s)) осталось прежним.

Поправить это оказалось чуть посложнее. Код, выводящий эту информацию, - шаблон template/page/html/pager.html. И используется он в catalog.xml вот так:
<catalog_seo_sitemap>
    ...
    <reference name="content">
        <block type="page/template_container" name="seo.sitemap.container" template="catalog/seo/sitemap/container.phtml">
            <block type="page/template_links" name="seo.sitemap.links" as="links" template="page/template/links.phtml"/>
            
            <!-- Magento adds block with template "page/html/pager.phtml" -->
            <block type="page/html_pager" name="seo.sitemap.pager.top" as="pager_top" template="page/html/pager.phtml"/>
            
            ...

Код использует исходную коллекцию, содержащую все продукты, её размер соответственно больше чем нужно. Выход - написать свой шаблон, который подкорректирует кол-во продуктов, выводимое на экран.

Создаим этот шаблон - template/catalog/seo/sitemap/mypager.phtml.

Сначала посчитаем продукты, исключая продукты из категории Glossary:
<?php // Calc products that don't belong to Glossary category (63) ?>
<?php // We already hide glossary "products" from sitemap page, now we need ?>
<?php // to correct products count ?>
<?php $productsCountWithoutGlossary = 0; ?>
<?php foreach ($this->getCollection() as $_i) { ?>
<?php     if (is_array($_i->getCategoryIds()) && in_array(63, $_i->getCategoryIds())) continue; ?>
<?php     $productsCountWithoutGlossary++; ?>
<?php } ?>

Т.к. Sitemap отображает вообще всё что есть на одной странице (это сделано уже давно), то и весь код пейджера, рисующий номера страниц, не нужен - оставим только тот код, который выводит кол-во элементов на одной странице:
<table class="pager" cellspacing="0">
    <tr>
        <td>
            <strong><?php echo $this->__('%s Item(s)', $productsCountWithoutGlossary) ?></strong>
        </td>
    </tr>
</table>

Ещё нужно сказать Magento что мы хотим использовать наш шаблон в файле catalog.xml:
<catalog_seo_sitemap>
    ...
    <reference name="content">
        <block type="page/template_container" name="seo.sitemap.container" template="catalog/seo/sitemap/container.phtml">
            <block type="page/template_links" name="seo.sitemap.links" as="links" template="page/template/links.phtml"/>
            
            <!-- Replace default template with ours "catalog/seo/sitemap/mypager.phtml" -->
            <block type="page/html_pager" name="seo.sitemap.pager.top" as="pager_top" template="catalog/seo/sitemap/mypager.phtml"/>
            
            ...

Последнее - кол-во продуктов выводится не только вверху, но и внизу. Для коррекции выводимого кол-ва так же просто поменяем шаблон в файле catalog.xml:
<catalog_seo_sitemap>
    ...
    <reference name="content">
        <block type="page/template_container" name="seo.sitemap.container" template="catalog/seo/sitemap/container.phtml">
            <block type="page/html_pager" name="seo.sitemap.pager.bottom" as="pager_bottom" template="catalog/seo/sitemap/mypager.phtml"/>
    ...

Вот, готово. Теперь Products Sitemap исключает продукты категории Glossary, и выводит правильное кол-во продуктов. Страница Category Sitemap не падает и выводит все категории :)

Изменённые файлы:
  • app/design/frontend/default/sunnyD/layout/catalog.xml
  • app/design/frontend/default/sunnyD/template/catalog/seo/sitemap.phtml
  • app/design/frontend/default/sunnyD/template/catalog/seo/sitemap/mypager.phtml

20 октября 2009 г.

Magento, права

Я уже описывал как делать модули, имеющий интерфейс в админке (и пункт меню там же), но этот модуль никак не работал с разрешениями (permissions). Как оно действует по-умолчанию я не знаю - либо показывает меню всем, либо показывает только администратору.

Сейчас я добавил в config.xml модуля немного "кода", так, что теперь можно выбрать, может ли пользователь работать с пунктом меню или нет. Как-то так :) Информации по этому очень мало...
<?xml version="1.0"?>
<config>
    ...
    <adminhtml>
        <menu>
            <catalog module="catalog">
                <children>
                    <blogrewrite translate="title" module="blogrewrite">
                        <title>Blog Rewrite</title>
                        <action>blogrewrite/adminhtml_blogrewrite</action>
                    </blogrewrite>
                </children>
            </catalog>
        </menu>

        <acl>
            <resources>
                <admin>
                    <children>
                        <catalog>
                            <children>
                                <blogrewrite>
                                    <title>Blog Rewrites</title>
                                </blogrewrite>
                            </children>
                        </catalog>
                    </children>
                </admin>
            </resources>
        </acl>
    </adminhtml>
</config>

Я так понимаю, что т.к. мой модуль находится внутри пункта меню верхнего уровня Catalog, то нужно добавить его. А называется модуль (или пункт меню?) blogrewrite.

Как бы то ни было, теперь право просматривать страницу модуля BlogRewrites можно назначить в Admin - System - Permissions - Roles:
permissions.jpg
А на будущее надо бы по-лучше поизучать как работают права в Magento.

Magento, перемещение блоков на странице Shopping Cart

Вот схемка как надо сделать :)
checkout-cart-before.png
Т.е. переместить блоки снизу влево:
checkout-cart-after.png
Первое, что приходит в голову - в checkout.xml переместить блоки из одного места другое. К сожалению, всё не так просто - эти два блока так же имеют немного кода в файле template/checkout/cart.phtml, и блоки, объявленные в xml вызываются как раз из этого файла:
<!-- Discount Code block -->
<div class="shopping-cart-collaterals">
    <div class="col2-set">
        <?php echo $this->getChildHtml('crosssell') ?>
        <div class="col-2">
            <!-- Load block by name "coupon" -->
            <?php echo $this->getChildHtml('coupon') ?>
            <?php if (!$this->getIsVirtual()): echo $this->getChildHtml('shipping'); endif; ?>
        </div>
    </div>
</div>

<!-- Totals block -->
<div class="shopping-cart-totals">
    <?php echo $this->getChildHtml('totals'); ?>
    <?php if(!$this->hasError()): ?>
    <ul class="checkout-types">
        <?php echo $this->getChildHtml('methods') ?>
    </ul>
    <?php endif; ?>
</div>

Пришлось выдёргивать этот код и переносить его в свой новый файл - пришлось создать новый шаблон, на основе 2columns-left.phtml. Почему не использовать уже готовый файл - во-первых, ширина левой колонки в стандартном файле маловата для размещения блока Totals. Во-вторых, в этот файл нужно поместить код для отображения блоков слева (перемещённый из файла cart.phtml).

В общем, я сделал новый файл 2columns-left-checkout.phtml на основе 2columns-left.phtml, и назначил его странице Shopping Cart. Вот что там было изменено:
<?php echo $this->getChildHtml('breadcrumbs') ?>
<!-- start left -->
<div class="col-left side-col" style="width: 250px;">
    <!-- Show left column, take content from xml -->
    <?php echo $this->getChildHtml('left') ?>
    <!-- Make some html for Totals block -->
    <div class="box">
        <!-- Following code took from cart.phtml -->
        <!-- Block should be non-float and have 0 margin -->
        <div class="shopping-cart-totals" style="float: none; margin: 0;">
            <?php echo $this->getChildHtml('totals'); ?>
            <?php if(!$this->hasError()): ?>
            <ul class="checkout-types">
                <?php echo $this->getChildHtml('methods') ?>
            </ul>
            <?php endif; ?>
        </div>
    </div>
</div>
<!-- Set main block (content) width to 613px
     (default is a little bit more, therefore layout problems) -->
<div id="main" class="col-main" style="width: 613px;">
<!-- start global messages -->
    <?php echo $this->getChildHtml('global_messages') ?>
<!-- end global messages -->
<!-- start content -->
    <?php echo $this->getChildHtml('content') ?>
<!-- end content -->
</div>

Итак, мы добавили вывод блока Totals после вывода левой колонки. А так же немного модифицировали стили блоков (см. комментарии). Левая колонка выводит Discount Coupons, это задаётся полностью в checkout.xml. Вот как блок Discount Coupons переносится в левую колонку:
<checkout_cart_index>
    ...
    <remove name="right"/>
    <!-- We should not remove left column, comment that line -->
    <!--<remove name="left"/>-->
    <reference name="left">
        <action method="unsetChildren"/>
        <block type="checkout/cart_coupon" name="checkout.cart.coupon" as="coupon" template="checkout/cart/coupon.phtml"/>
    </reference>
    ...

Убираем удаление левой колонки, убираем всех лишних детей из левой колонки, добавляем блок с шаблоном блока Discount Coupon.

С перемещением блока получился небольшой затык. Хотя блоки, которые вызываются из шаблона (totals, methods), я скопировал из reference name="content" в reference name="left", всё отображалось совсем не как надо. А потом я сообразил, что в шаболоне эти блоки вызываются не "изнутри" блока left/content, а из "корня". После того как я переместил эти блоки в reference name="root" всё наконец заработало. Вот этот кусочек:
<reference name="root">
    <block type="checkout/cart_totals" name="checkout.cart.totals" as="totals" template="checkout/cart/totals.phtml"/>
    <block type="core/text_list" name="checkout.cart.methods" as="methods">
        <block type="checkout/onepage_link" name="checkout.cart.methods.onepage" template="checkout/onepage/link.phtml"/>
        <block type="checkout/multishipping_link" name="checkout.cart.methods.multishipping" template="checkout/multishipping/link.phtml"/>
    </block>
</reference>

В основном всё, блоки переместились снизу налево и отображают то, что и должны. Остались мелочи - удалить выводы блоков снизу (можно просто закомментировать вывод блоков прямо в cart.phtml), и поправить css, т.к. текущие правила расчитаны на то, что эти блоки будут внутри блоков с определёнными классами. Вот добавленный в boxes.css код:
/* Copy some styles from Discount Codes, it was in the middle of
   page, now it's on the left side */
.col-left .discount-codes h4 {
    background-image:url(../images/icon_asterick.gif);  padding-left:23px;
    min-height:16px;
    color:#e26703;
    background-repeat:no-repeat;
    background-position:0 0;
    padding:1px 0 1px 21px;
    text-transform:uppercase;
}

.col-left .discount-codes.box {
    margin-bottom:18px;
    padding:12px 15px;
    border:1px solid #D0CBC1;
    background:url(../images/base_mini_head_bg.gif) repeat-x #fff;
}

Ну и последнее - убираем вывод блока с классом shopping-cart-collaterals. Этот блок содержит не только Discount Coupons, но и кое-что ещё. Но его можно убрать целиком, потому что в текущей конфигурации весь остальной вывод выключен (в админке) - выводится только Discount Coupons.

Вот код, который убирает всё ненужное снизу страницы:
<?php /*
<div class="shopping-cart-collaterals">
    <div class="col2-set">
        <?php echo $this->getChildHtml('crosssell') ?>
        <div class="col-2">
            <?php echo $this->getChildHtml('coupon') ?>
            <?php if (!$this->getIsVirtual()): echo $this->getChildHtml('shipping'); endif; ?>
        </div>
    </div>
</div>

<div class="shopping-cart-totals">
    <?php echo $this->getChildHtml('totals'); ?>
    <?php if(!$this->hasError()): ?>
    <ul class="checkout-types">
        <?php echo $this->getChildHtml('methods') ?>
    </ul>
    <?php endif; ?>
</div>
*/ ?>

Просто комментируем это дело :)

Вот так за пару часов блоки переехали снизу в левую колонку.

Изменённые файлы:
  • app/design/frontend/default/sunnyD/layout/checkout.xml
  • app/design/frontend/default/sunnyD/template/checkout/cart.phtml
  • app/design/frontend/default/sunnyD/template/page/2columns-left-cart.phtml
  • skin/frontend/default/sunnyD/css/boxes.css

16 октября 2009 г.

Дополнительный контент на Checkout странице, левая колонка

В прошлый раз я писал как добавить свой контент в правую колонку на странице чекаута. Оказалось что это не совсем то, что нужно :) А нужно добавить левую колонку и уже туда поместить картинки. Вот так:
checkout-left.png
Это оказалось даже проще чем добавить контент в правую колонку. Только есть один нюанс - когда мы добавим новую колонку, размер средней колонки (с основным контентом) уменьшится. А в этой средней колонке есть элементы, которые расчитаны на строго определённый размер основной колонки, и размеры там задаются в пикселях. Это выпадающий список с адресами, поле ввода адреса-1 и адреса-2, и поля логина/пароля, если чекаут делается из под гостя.

Если не изменить размеры этих элементов, они выедут за свою родительскую колонку, и будет некрасиво. Ну и вообще, колонка рассчитана быть размером 650px вроде, поэтому в любом случае с уменьшением размера элементы внутри поедут. Это будет выглядеть немного не так, как задумывалось, но в целом неплохо. В частности, форма ввода адреса при нормальном размере отображается как бы в двух колонках - поля ввода есть и слева и справа. При уменьшенном размере все поля располагаются друг под другом.

Итак, для начала нужно поменять шаблон страницы чекаута на 3columns.phtml. Старый шаблон 2columns-right-checkout.phtml больше не нужен, его можно удалить.

Здесь же, недалеко от изменения шаблона, добавим наш шаблон с картинками в левую колонку:
<checkout_onepage_index>
    ...
    <!-- Mage_Checkout -->
    <!--<remove name="left"/>-->

    <reference name="root">
        <action method="setTemplate"><template>page/3columns.phtml</template></action>
    </reference>
    <reference name="left">
        <action method="unsetChildren" />
        <block type="core/template" name="images" template="callouts/checkout.phtml"/>
    </reference>
    ...

Здесь сначала мы убираем удаление левой колонки remove name="left". Потом устанавливаем для чекаута шаблон 3columns.phtml. Затем модифицируем блок с названием left - удаляем все добавленные в левую колонку блоки, добавленные где-то до нас - action method="unsetChildren". Если этого не сделать, в левой колонке будут отображаться разные блоки, вроде списка категорий, списка тэгов, и т.д. И, наконец, добавляем наш шаблон block type="core/template"...

Основная задача решена - страница чекаута превратилась в трёхколоночную, и левая колонка отображает наш шаблон с картинками.

Теперь нужно поменять размеры некоторых элементов.

Для начала поменяем размеры выпадающих списков с адресами. Эти элементы можно легко достать по их id - это billing-address-select и shipping-address-select. Добавим новый стиль в boxes.css:
#billing-address-select, #shipping-address-select {
    width: 420px;
}

Дальше нужно поменять размеры полей для ввода адреса на страницах Billing Information и Shipping Information. Эти поля возникают если в списке выбора адреса выбрать New Address.

Эти поля тоже имеют id, но они какие-то странные - shipping:street1, т.е. содержат двоеточие. У меня не получилось выбрать такие элементы с помощью css селектора #shipping:street1, поэтому пришлось добавить атрибут style прямо в эти самые тэги. Они находятся в файлах template/checkout/onepage/billing.phtml и template/checkout/onepage/shipping.phtml. Просто добавляем код style="width: 250px;" к input элементам, всего их 4 штуки.

Теперь - формы ввода логина/пароля, если пользователь делает чекаут как гость. Хотя эти поля можно вытянуть по id, я не стал этого делать, потому что точно такая же форма, такие же поля с такими же id, могут использоваться на другой форме, например, на форме логина пользователя. Поэтому я опять добавил тэг style прямо в тэг. Эта форма находится в файле template/checkout/onapage/login.phtml, всего два поля нужно обновить.

Например, тэг для пароля будет иметь такой вид:
<input type="password" class="input-text required-entry" id="login-password" name="login[password]" style="width: 200px;" />

И последнее - форма добавления комментария на последнем шаге. Она совсем чуть-чуть вылезает за разрешённые границы, но этого достаточно, что бы в IE всё поехало конкретно.

Файл template/checkout/onepage/agreements.phtml содержит этот элемент. Немного уменьшаем ширину, прописанную в атрибуте style, до 420px:
<ol class="checkout-agreements">
    <li>
        <label for="biebersdorfCustomerOrderComment"><?php echo Mage::helper('biebersdorfcustomerordercomment')->__('Your Comment for this Order') ?></label>
        <br/><textarea name="biebersdorfCustomerOrderComment" id="biebersdorfCustomerOrderComment" style="width: 420px; height: 100px;"></textarea>
    </li>
</ol>

Всё готово, теперь чекаут содержит левую колонку с нашими картинками, и никакие поля не вылезают за рамки родительских блоков.

Модифицированные файлы:
  • app/design/frontend/default/sunnyD/template/checkout/onepage/billing.phtml
  • app/design/frontend/default/sunnyD/template/checkout/onepage/shipping.phtml
  • app/design/frontend/default/sunnyD/template/checkout/onepage/login.phtml
  • app/design/frontend/default/sunnyD/template/checkout/onepage/agreemenst.phtml
  • app/design/frontend/default/sunnyD/layout/checkout.xml

15 октября 2009 г.

Дополнительный контент на странице Checkout

Нужно добавить две картинки на страницу чекаута, как на этой картинке:
checkout.png
Это оказалось не так просто, как казалось :)

Для начала сделаем небольшой файл-шаблончик с нужными картинками. Это файл app/design/frontend/default/sunnyD/template/callouts/checkout.phtml:
<div class="box">
    <script src="https://siteseal.thawte.com/cgi/server/thawte_seal_generator.exe"></script>
</div>

<div class="box">
    <img src="<?php echo $this->getSkinUrl('images/free-shipping.gif') ?>" alt="<?php echo __('Free Shipping') ?>" />
</div>

Первая картинка загружается динамически, для чего используется Javascript.

Первая попытка

Почему бы просто не добавить новый блок в checkout.xml? Для этого можно использовать встроенный блок core/template. Он очень удобен для вывода простых шаблонов без логики, ему нужно передать всего один параметр - шаблон, который надо вывести. И, т.к. шаблон простого чекаута состоит из контента и правого блока (шаблон 2columns-right.phtml), добавить наш новый блок к правой части страницы, как-то так:
<checkout_onepage_index>
    ...
    <reference name="right">
        ...
        <block type="core/template" name="images" template="callouts/checkout.phtml"/>
        ...
Это даже работает, и на первый взгляд мы имеем именно то что надо - справа появляются наши картинки из шаблона checkout.phtml.

Но на второй взгляд, когда нажать кнопку Continue что бы перейти к следующему шагу чекаута, правая часть страницы целиком перезагружается. html для правой страницы приходит в Ajax запросе. Этот html больше не содержит наших картинок.

Вторая попытка

Ладно, значит нужно поправить возвращаемый html, что бы он содержал наши картинки.

Для этого надо поправить checkout.xml, секцию checkout_onepage_progress. Нужно добавить наш блок, как мы это делали в секции checkout_onepage_index. Но что это? Наших картинок всё-равно нет!

В общем в итоге выяснилось, что к блоку нужно добавить параметр output и присвоить ему значение toHtml:
<checkout_onepage_progress>
    ...
    <block type="core/template" name="images" output="toHtml" template="callouts/checkout.phtml"/>
    ...

Да что же это? Теперь после кнопки Continue видно только одну картинку, которая загружается через тэг img. Второй картинки, загружаемой через Javascript, не видно. Более того, хотя js код пришёл в ответе от сервера, он пропал из DOM дерева. Какой-то фильтр?

В общем, попытка номер два тоже провалилась.

Успех

Ладно, если не получается показывать обе картинки модифицируя код правой части, возвращаемый с помощью Ajax запроса, попробуем сделать так, что бы шаблон оставался неизменным на протяжении всех шагов чекаута.

Javascript, работающий со страницей чекаута находится в файле opcheckout.js. Похоже, что код, который обновляет правую часть страницы, выглядит так:
...
reloadProgressBlock: function(){
    var updater = new Ajax.Updater($$('.col-right')[0], this.progressUrl, {method: 'get', onFailure: this.ajaxFailure.bind(this)});
},
...
Т.е. он берёт первый элемент с классом col-right и устанавиливает ему новый html, полученный с сервера.

А вот где элемент с этим классом объявлен - файл template/page/2columns-right.phtml:
...
<!-- start right -->
<div class="col-right side-col">
    <?php echo $this->getChildHtml('right') ?>&nbsp;
</div>
<!-- end right -->
...
Не очень хорошо... Что бы мы ни поместили в правую часть страницы с помощью xml и reference name="right", либо прямо в данный шаблон, это всё будет внутри дива с классом col-right и, в итоге, будет затёрто на последующих шагах чекаута.

Выходит, единственный выход - сделать свой шаблон, основанный на 2columns-right.phtml. А в этом шаблоне как-то поместить наш шаблон с картинками "вне" правой части, что бы он не затёрся новыми данными.

Итак, для начала создадим новый шаблон 2columns-right-checkout.phtml, и укажем в checkout.xml что использовать нужно именно его.

Было:
<checkout_onepage_index>
    ...
    <reference name="root">
        <action method="setTemplate"><template>page/2columns-right.phtml</template></action>
Стало:
<checkout_onepage_index>
    ...
    <reference name="root">
        <action method="setTemplate"><template>page/2columns-right-checkout.phtml</template></action>
Далее модифицируем новый шаблон 2columns-right.checkout.phtml в части вывода правой части:
<!-- start right -->
<div class="side-col" style="float: right;">
    <div class="col-right side-col" <?php echo $this->getChildHtml('right') ?></div>

    <div class="col-right side-col">
        <?php echo $this->getChildHtml('images') ?>
    </div>
</div>
<!-- end right -->
Смысл такой. Создаём новый div-обёртку, которая будет содержать правую колонку. Мы не можем назначит ему класс col-right, потому что тогда контент этого дива затрётся. Но мы можем оставить ему класс side-col (который просто установливает ширину колонки, сейчас 195px). И добавляем свойство float: right что бы наш див-обёртка распологался справа.

Затем добавляем внутрь этого дива-обёртки сначала исходную правую часть getChildHtml('right'), затем наш шаблон с картинками getChildHtml('images').

Теперь оба дива могут иметь оригинальные классы side-col и col-right. При переходе между шагами будет обновляться содержимое первого блока с классом col-right, а второй блок с нашими картинками останется нетронутым на протяжении всего чекаута. Что и требовалось :)

И ещё - что бы код getChildHtml('images') отработал и отобразил наш блок core/template с шаблоном с картинками, нужно его добавить в checkout.xml внутри reference name="root". И ещё добавить свойство as, равное images. Это images и есть то имя, по которому getChildHtml() найдёт наш блок. Получаем:
<checkout_onepage_index>
    ...
    <reference name="root">
        ...
        <block type="core/template" name="images" as="images" template="callouts/checkout.phtml"/>
        ...
Вот, всего несколько часов работы и страничка чекаута содержит наш допольнительный шаблон. Всё в рамках приличия, ядрёные файлы не модифицированы, все изменения только в папке с пользовательской темой.

Magento, список категорий

В теме Modern есть список категорий, отображаемый сверху. Если навести на категорию - выскочит список подкатегорий. Красивый и удобный список.
top-links.png
Только есть проблема с этим списком - он показывает все активные категории. Нет возможности через админку/xml убрать из этого списка некоторые категории, например, категорию Glossary. Вообще-то Glossary уже скрывается, а сейчас нужно скрыть категорию Out of Stock.

Но можно скрывать категории с помощью css, и воспользоваться для этого особенностью этого списка - для каждой категории генерируется уникальный класс, совпадающий с названием категории. Т.о. для того, что бы "убрать" категорию из этого списка, нужно просто добавить новый класс в какой-либо css файл.

Структура списка имеет вид:
<ul id="nav">
    <li class="level0 nav-category-name">...</li>
</ul>
Категория Out of Stock будет иметь класс nav-out-of-stock. Добавим новый стиль в файл skin/frontend/default/sunnyD/css/menu.css:
/* Hide Out Of Stock category */
#nav li.level0.nav-out-of-stock {
    display: none;
};
Это правило будет применено к элеменам li с классами nav-out-of-stock и level0, которые являются дочерними по отношению к элементу с id равным nav. Это и есть категория Out of Stock.

14 октября 2009 г.

Windows Seven - Windows XP

Перешёл с семёрки на XP. Наконец-то, и я этому рад! :)

Установленное ПО

После переустановки винды нужно переустановить вагон с тележкой разного софта... Утомительное же это занятие...

Drivers (Wireless, Video), WinRar, foobar2000, Miranda, Skype, Opera, Firefox (+plugins), Total Commander, Notepad++, Video Codecs (+player Media Player Classic), .Net Framework, Java, Flash Player, Pdf viewer, Djv viewer, Python (+modules: setup-tools, MySQL, mechanize, django, djang-debug-toolbar), Php, MySQL, HeidiSQL, Apache Web Server, Aptana, NuSphere PhpEd, oDesk Team, F.lux, uTorrent, John's Background Switcher, Dropbox, Lingvo.

Это только то, что сейчас установлено, что-то может забыл.

А ещё нужно настроить эти программы как удобно, а некоторые - подружить (Php с сервером, отладчик от PhpEd'а с Php, и т.д.). Хорошо что некоторые программы легко переносятся со всеми настройками :)

Что стало лучше

Всё стало работать быстро! Я много читал отзывов, в которых в основном говорилось что семёрка работает как минимум не медленнее хрюши. Всякие Fetch/Prefetch механизмы, кеши-шмеши... А толку?

Сейчас я на каждом действии замечаю разницу между семёркой и XP. Попасть в Панель управления - в семёрке 5 секунд, в XP - 1 секунда. Запустить приложение, в семёрке - от секунды, в XP - от меньше чем за секунду (иногда мгновенно, чего на семёрке не наблюдал никогда).

Ещё в семёрке иногда заедал курсор, если например попытаться развернуть программу, свёрнутую пару часов назад. Т.е. всё тормозило секунд 5-10-20-30, было невозможно работать, пока ОС не сделает свои дела по развёртыванию проги... Чаще чем курсор заедала музыка... Странно зависала миранда секунд на 5 во время создания окошка с чатом.

В общем, в семёрке всё работало с жуткими задержками на каждое действие, в XP всё работает моментально, по сравнению с семёркой :)

Чего будет не хватать

Superbar, наследник Taskbar, - очень удобная штука!

Интерфейс Aero. Это когда всё как будто сделано из стекла, всё полупрозрачное и размазанное :) Полупрозрачный Superbar это красиво :)

Шрифты. Шрифты в семёрке - загляденье! К сожалению, в XP это не так.

Драйвера. Семёрка нашла все устройства на моём Dell Inspiron 1525 "из коробки".

Что будет с радостью забыто

Тормоза, но об этом уже было :)

Несовместимости с некоторыми программами. Т.е. как бы самих несовместимых программ очень мало... Но foobar2000, например, мог работать только в режиме совместимости (я использую старую версию, потому что в новых версиях нет нужного мне плагина), а для того, что бы запустить такую программу, нужно будет ответить Yes на вопрос UAC. Неприятная мелочь.

Сюда же можно отнести неумение некоторых программ работать не из под администратора. Например, Behold, программа для работы с ТВ-тюнером, сохраняет настройки в папке куда она установлена. Поэтому сохранить сделанные в программе изменения настроек можно либо запустив программу из под администратора, либо дать нужные права на папку установки, либо поставить в папку у которой не будет запретов. Неприятная мелочь.

Некоторые другие программы так же хранят настройки в папке установки, что приводит к таким же последствиям (foobar2000, Miranda, ...).

Итог

Windows Seven - хорошая операционка. Всё что было сказано выше относится к публичной бета-версии. Всё было стабильно, красиво, даже приятно, если отбросить странные тормоза :) Её смело можно ставить через пару лет, плюс желательно на новое железо.

Общие впечатления такие, что семёрка добавляет некоторые приятные мелочи. Не больше...

7 октября 2009 г.

https и warning

В интернет-магазине есть страница, которая должна передаваться по защищённому соединению, т.е. по https. Если эта страница делает хотя бы один http запрос, Firefox ругается, что "content is partially encrypted", или что-то такое. Я проверил, что бы все запросы были только по https. И у меня на Win 7 никаких предупреждений не было. Но у других людей, на линуксе, на маке, и даже на той же семёрке, Firefox ругался.

Что за странное дело, для проверки я использовал плагины Firebug (вкладку Net), и Tamper Data, для отслеживания всех запросов, и все они были по https!

Потом я запустил Explorer (восьмой), и он сразу показал окошечко, мол, "на этой секурной странице есть несекурные запросы, выполнять только секурные запросы?". Ещё IE позволил посмотреть несекурные запросы. И тогда ошибка раскрылась.

В коде страницы был вызов одного и тоже js скрипта, но один вызов был по https, другой по http (именно в таком порядке). Видимо, FF не загружал повторно один и тот же скрипт. Сначала он загружал скрипт по https, что и было видно в Firebug'е. А со второй загрузкой, по http он, вёл себя "undefined" - у меня он не выдавал предупреждений о несекурности страницы, у других людей выдавал.

Вот так, не следует больше одного раза загружать на страницу одни и те же данные :) И, как ни странно, не стоит забывать про IE, он сэкономил мне много времени.

6 октября 2009 г.

Wordpress, "Read the rest of this entry"

Если в тексте поста в Wordpress есть специальный тэг <!--more-->, то при выводе списка постов Wordpress выводит только текст до этого тэга, а сам тэг заменяет на ссылку на пост, с текстом Read the rest of this entry. Эта ссылка содержит указатель на якорь, т.е. на то место, где находится тэг more. Зачем-то понадобилось убрать из этой ссылки указание на этот якорь. Т.е. было <a href="/blog/2009/10/my-blog-post/#more-123">My Blog Post</a>, надо <a href="/blog/2009/10/my-blog-post/">My Blog Post</a>. Ну, надо - так надо.

Содержимое поста, и ссылка Read the rest (если нужно) выводится функцией the_content. Включить/выключить добавление якоря в ссылку нельзя через настройки. Поэтому остаётся единственный вариант - получить строку контента и удалить якорь.

Функция the_content сразу выводит содержимое. Но есть аналогичная функция get_the_content, которая возвращает контент, а не выводит. Проблема в том, что результат этой функции не содержит переносов строк, т.е. текст идёт сплошным потоком. Я подсмотрел как работает the_content - она применяет фильтр the_content перед выводом. Т.е. нам надо сделать тоже самое.

Итого, получаем код, который удаляет из ссылки якорь:
<?php $content = get_the_content('Read the rest of this entry &raquo;'); ?>
<?php $content = preg_replace('/\/#more-\d+/', '/', $content); ?>
<?php $content = apply_filters('the_content', $content); ?>
<?php print $content;  ?>

Деньги на банковской карте

После создания графика для баланса в телефоне, захотел сделать график баланса на банковской карте ВТБ24 :) Для этого можно использовать Телебанк.

Научить питон-робота работать с сайтом Телебанка было чуть сложнее чем с сайтом ИССА. Вот почему.

Форма логина перед отправкой данных вызывает JS код, который делает очень простую проверку введённых данных (просто что они есть), и выставляет скрытому полю js значение 1 (по-умолчанию - 0). Если серверу приходит форма где js == 0, он говорит что в браузере нет JavaScript и не хочет работать дальше. Хотя JS там выполняет далеко не главную задачи и без него вполне можно работать.

Немного поразбиравшись с mechanize, я научил робота выставлять в поле js значение 1:
browser.select_form(name = 'FormLogin')
browser['TextBoxName'] = LOGIN
browser['TextBoxPassword'] = PASSWORD

# There is `js` hidden readonly param, by default it's 0, and site
# says that JS is disabled. So we pretend that we have JS by setting
# `js`= 1. But first delete readonly flag 
browser.find_control('js').readonly = False
browser['js'] = '1'
На этом трудности кончились :)

Добавил новый скрипт во встроенный планировщик, теперь у меня два красивых графика :)


# -*- coding:utf-8 -*-

from mechanize import Browser
from time import strftime
import re
import os, sys

LOGIN_URL = 'https://www.telebank.ru/WebNew/Login.aspx'
ACCOUNT_URL = 'https://www.telebank.ru/WebNew/Accounts/Accounts.aspx'
DATA_FILE = os.path.dirname(sys.argv[0]) + '/history.txt'
LOGIN = 'Телебанк логин'
PASSWORD = 'Телебанк пароль'

browser = Browser()

# Open page with login form
#print('Opening login page...')
browser.open(LOGIN_URL)

# Fill in form fields - login/password
#print('Filling in login form...')
browser.select_form(name = 'FormLogin')
browser['TextBoxName'] = LOGIN
browser['TextBoxPassword'] = PASSWORD

# There is `js` hidden readonly param, by default it's 0, and site
# says that JS is disabled. So we pretend that we have JS by setting
# `js`= 1. But first delete readonly flag 
browser.find_control('js').readonly = False
browser['js'] = '1'

# Submit form
#print('Submitting form...')
r = browser.submit()
page = r.read().decode('cp1251')

# Check that site gave us "You logged in, but please wait"
try:
    page.index('Вход в систему выполнен')
except:
    print("! Login failed")
    exit(1)

# Open accounts URL
#print('Opening accounts page...')
r = browser.open(ACCOUNT_URL)
page = r.read().decode('cp1251')

match = re.search(ur'>(?P<money>\d+(\.|,)\d+)<', page, re.IGNORECASE)
if match:
    money = float(match.group('money').replace(',', '.'))
    f = open(DATA_FILE, 'a')
    f.write('%s, %.2f\n' % (strftime('%d.%m.%Y %H:%M'), + money))
    f.close()
    
    print('Card balance: %.2f rub' % money)
else:
    print("! Can't find balance info")
    print page

3 октября 2009 г.

Python backup, часть 3

Третья версия скрипта-бэкапа на питоне :) На этот раз архив с файлами для бэкапа не создаётся. Вместо этого файлы с одного места (исходные папки) перемещаются в другое место (папки с бэкапами) как есть. Для реальной работы, когда в день может поменяться всего пара десятков файлов, это гораздо выгоднее. DropBox будет загружать лишь эти несколько файлов, вместо громадного архива.

Вообще-то мне кажется что что-то такое уже должно существовать, тем более для линукса :) С другой стороны бэкап был написан не потому что такого ещё нет, а что бы лучше узнать питон.

Теперь алгоритм такой. Берём исходную папку ("лево"), создаём папку в выходной папке ("право"), копируем слева-направо только изменившиеся папки/файлы, удалённые слева папки/файлы удаляем и справа. При этом справа будет создана папка, отображающая полный путь до папки слева.

Например, исходная папка d:\projects\python, папка назначения - d:\dropbox\backup, тогда реальная папка назначения будет такой - d:\dropbox\backup\d_\projects\python.

Всё остальное осталось от предыдущих версий - файлы-списки toBackup.lst, ignore.lst, extra.lst.

Скриншотик:
Free Image Hosting at www.ImageShack.us

Скрипт:
import sys
import os
import os.path
import tarfile
import time
import gzip
import copy
import shutil

Verbose = False

ignoreList = []

copiedFiles = 0
skippedFiles = 0
createdFolders = 0
deletedFolders = 0
deletedFiles = 0
errors = 0

def removeDir(path):
    '''
    Remove dir and all subdirs with all subfiles
    '''
    for root, dirs, files in os.walk(path, False):
        for f in files:
            os.unlink(os.path.join(root, f))
            
        for d in dirs:
            os.rmdir(os.path.join(root, d))
            
    os.rmdir(path) 
   
def copyFile(fromFilename, toFilename):
    '''
    Function will copy file only if toFilename is different than fromFilename.
    Size and modification date checked.
    
    Returns True if file been realy copied,
    return False if there was no real copy (files seems the same)
    '''
    copy = True
    if os.path.isfile(toFilename):
        fromStat = os.stat(fromFilename)
        toStat = os.stat(toFilename)

        copy = (fromStat.st_size != toStat.st_size) or (str(fromStat.st_mtime) != str(toStat.st_mtime))        

    # Copy file with stat info, including e.g. modification date
    if copy:
        shutil.copy2(fromFilename, toFilename)
        
    return copy

def createFolderStruct(path):
    '''
    Create struct (tree node) for folder tree
    '''
    return {'full_path': path,
            'files': [],
            'dirs': {}}

def makeDirTree(path, data):
    '''
    Creates folder tree in form of special struct
    '''
    for i in os.listdir(path):
        newPath = os.path.join(path, i)

        # Ignore some folders from proccessing
        if (i.lower() in ignoreList) or (newPath.lower() in ignoreList):
            continue
        
        if os.path.isfile(newPath):
            data['files'].append(i)
        elif os.path.isdir(newPath):
            data['dirs'][i] = createFolderStruct(newPath) 
            
            makeDirTree(newPath, data['dirs'][i])
            
def copyFolder(srcDirs, outDirs):
    '''
    Copy folder from "left" to "right", given folders trees srcDirs and outDirs.
    Right folder will be complete copy of left folder with minimum number of file operations
    (don't copy file if it's already exists and hasn't been modified)
    '''
    
    # Use global counters
    global copiedFiles
    global skippedFiles
    global createdFolders
    global deletedFolders
    global deletedFiles
    global errors    
    
    # Loop throught all src folders, s - current folder name
    for s in srcDirs['dirs']:
        # If src folder isn't exist in output folder
        if s not in outDirs['dirs']:
            # If there is file in output folder with name as folder in src folder - delete that file
            if s in outDirs['files']:
                # Delete file s
                fullFilename = os.path.join(outDirs['full_path'], s)
                os.unlink(fullFilename)
                if Verbose:
                    print('D %s' % fullFilename)
                deletedFiles += 1                                        
                
            outDirs['dirs'][s] = createFolderStruct(os.path.join(outDirs['full_path'], s))
            
            # Create output folder with src folder name                
            fullPath = os.path.join(outDirs['full_path'], s)
            os.mkdir(fullPath)
            if Verbose:
                print('M [%s]' % fullPath)
            createdFolders += 1

        # Go deep inside current folder
        copyFolder(srcDirs['dirs'][s], outDirs['dirs'][s])
    
    # Remove folders from right, that don't exist on left
    for o in outDirs['dirs']:
        if o not in srcDirs['dirs']:
            fullPath = os.path.join(outDirs['dirs'][o]['full_path'])
            removeDir(fullPath)
            if Verbose:
                print('D [%s]' % fullPath)
            deletedFolders += 1                

    # Remove files from right, that don't exist on left
    for o in outDirs['files']:
        if o not in srcDirs['files']:
            fullFilename = os.path.join(outDirs['full_path'], o)
            os.unlink(fullFilename)
            if Verbose:
                print('D %s' % fullFilename)
            deletedFiles += 1            

    # Copy files from left to right
    for s in srcDirs['files']:
        fullFrom = os.path.join(srcDirs['full_path'], s)
        fullTo = os.path.join(outDirs['full_path'], s)
        
        copied = copyFile(fullFrom, fullTo)
        if copied: 
            c = 'C'
            copiedFiles += 1 
        else: 
            c = '-'
            skippedFiles += 1
            
        if Verbose and copied:
            print('%s %s -> %s' % (c, fullFrom, fullTo)) 

if __name__ == "__main__":
    if (len(sys.argv) < 2):
        print("Usage: backup.py <output folder> [--verbose]")
        print('E.g.: "backup.py d:\\dropbox\\my dropbox\\backup\\"')
        exit(1)

    RootFoldersFile = "tobackup.lst"
    IgnoreFoldersFile = "ignore.lst"
    ExtraFile = "extra.lst"
    OutputFolder = sys.argv[1]
    
    try:
        Verbose = (sys.argv[2] == '--verbose')
    except:
        pass

    # Only mandatory file is RootFoldersFile, check that it exists
    if (not os.path.isfile(RootFoldersFile)):
        print("! %s doesn't exist" % RootFoldersFile)
        exit(1)

    # Output folder should exist, or we should be able to create it
    if not os.path.isdir(OutputFolder):
        try:
            os.makedirs(OutputFolder)
        except:
            print("! Can't create output folder %s" % OutputFolder)
            exit(1)

    # Read root folders from file, skip empty lines
    rootFolders = [i.strip() for i in open(RootFoldersFile, "r").readlines()]
    if '' in rootFolders:
        rootFolders.remove('')

    # Read list of folders that need to be ignored (if specified)
    if (os.path.isfile(IgnoreFoldersFile)):
        ignoreList = [i.strip().lower() for i in open(IgnoreFoldersFile, "r").readlines()]
        if '' in ignoreList:
            ignoreList.remove('')

    # Create folders trees for each input folder
    for rootFolder in rootFolders:
        inputTree = createFolderStruct(rootFolder)
        makeDirTree(rootFolder, inputTree)

        # File will be placed under OutputFolder plus full path to src folder
        # E.g. src folder d:\projects\python will be copied to folder q:\backup\d_\projects\python
        #                 ^^^^^^^^^^^^^^^^^^                                    ^^^^^^^^^^^^^^^^^^            
        outFolder = inputTree['full_path']
        outFolder = outFolder.replace(':', '_')
        outFolder = os.path.join(OutputFolder, outFolder)

        print('[%s] -> [%s]...' % (rootFolder, outFolder))
            
        if not os.path.isdir(outFolder):
            try:
                os.makedirs(outFolder)
                print('M [%s]' % outFolder)
            except:
                print("! Can't create output folder [%s]" % outFolder)
                print("! Skip input folder %s" % rootFolder)
                errors += 1
                continue
        
        # Make folder tree for real output folder
        outputTree = createFolderStruct(outFolder)        
        makeDirTree(outFolder, outputTree)
        
        # Copy input folder to output folder
        copyFolder(inputTree, outputTree)
        
    # Read list of extra files
    if (os.path.isfile(ExtraFile)):
        extraList = [i.strip().lower() for i in open(ExtraFile, "r").readlines()]
        if '' in extraList:
            extraList.remove('')
        
        print('Extra files...')
        
        for file in extraList:
            outFileDir = os.path.join(OutputFolder, os.path.dirname(file).replace(':', '_'))
            if not os.path.isdir(outFileDir):
                try:
                    os.makedirs(outFileDir)
                except:
                    print("! Can't create output folder [%s]. Skip extra file %s" % (outFileDir, file))
                    errors += 1
                    continue
        
            fullTo = os.path.join(outFileDir, os.path.basename(file))
        
            copied = copyFile(file, fullTo)
            if copied: 
                c = 'C'
                copiedFiles += 1 
            else: 
                c = '-'
                skippedFiles += 1
                
            if Verbose and copied:
                print('%s %s -> %s' % (c, file, fullTo))
            
    print('==============================')
    print('Copied file(s):    %d' % copiedFiles)
    print('Skipped file(s):   %d' % skippedFiles)
    print('Created folder(s): %d' % createdFolders)
    print('Deleted folder(s): %d' % deletedFolders)
    print('Deleted file(s):   %d' % deletedFiles)
    print('Errors:            %d' % errors)
    print('==============================')    
    print("Done"

2 октября 2009 г.

Баланс в телефоне

Решил реализовать такую интересную и простую задачку - следить за балансом на телефоне, что бы был красивый график. Я использую БВК, у них есть ИССА - Интернет Служба Сервиса Абонента, через которую можно узнать состояние своего счёта.

Задача разбивается на две подзадачи - посмотреть состояние счёта в интернете, сохраняя эти данные куда-нибудь; построить график на основе этих данных.

Получение состояния счёта

Я написал небольшой скрипт на питоне, который логинится в ИССА и узнаёт состояние счёта. Далее он записывает эти данные, вместе с текущей датой/временем, в текстовый файл. Для работы с интернетом используется mechanize, этот веб-клиент, написанный на питоне, поддерживает куки, что необходимо для логина.

mechanize не входит в стандартную поставку питона, его нужно установить:
easy_install.py mechanize

Поиск баланса ведётся с помощью регулярных выражений по строке "Баланс вашего лицевого счёта равен xx руб.".
# -*- coding:utf-8 -*-

from mechanize import Browser
from time import strftime
import re
import os, sys

LOGIN_URL = 'http://issa.bwc.ru/'
ACCOUNT_URL = 'http://issa.bwc.ru/cgi-bin/cgi.exe?function=is_account'
DATA_FILE = os.path.dirname(sys.argv[0]) + '/history.txt'
PHONE_NUMBER = <phone number>
PASSWORD = <issa password>

browser = Browser()

browser.open(LOGIN_URL)
browser.select_form(name = 'num')
browser['mobnum'] = PHONE_NUMBER
browser['Password'] = PASSWORD
browser.submit()
r = browser.open(ACCOUNT_URL)

page = r.read().decode('cp1251')

match = re.search(ur'Баланс вашего лицевого счета равен (?P<money1>\d+).(?P<money2>\d)', page, re.IGNORECASE)
if match:
    money = int(match.group('money1')) + float(match.group('money2')) / 10 
    f = open(DATA_FILE, 'a')
    f.write('%s, %.1f\n' % (strftime('%d.%m.%Y %H:%M'), + money))
    f.close()
    
    print('Phone balance: %.1f rub' % money)
else:
    print page
Я добавил задание в стандартный планировщик Windows, который запускает этот скрипт каждые два часа.

График

Для рисования графика я взял библиотеку от Google google.visualization.AnnotatedTimeLine. Ей нужно передать данные, она сама нарисует красивый график, используя Flash. Для разбора файла-лога с балансами я написал скрипт на PHP. Страница index.php, которая ответственна за вывод графика, имеет такой вид:

<?php
    define('HISTORY_FILE', 'd:\projects\python\issa.bwc\history.txt');

    $history = array();
    $hist = explode("\n", trim(file_get_contents(HISTORY_FILE)));
    foreach ($hist as $_h) {
        $tmp = split('[. ,:]', $_h);

        if (!$tmp[0])
            continue;

        $history[] = array('day' => $tmp[0],
                           'month' => $tmp[1],
                           'year' => $tmp[2],
                           'hours' => $tmp[3],
                           'minutes' => $tmp[4],
                           'value' => intval($tmp[6]) + intval($tmp[7]) / 10);
    }
?>

<script type='text/javascript' src='http://www.google.com/jsapi'></script>
<script type='text/javascript'>
  google.load('visualization', '1', {'packages':['annotatedtimeline']});
  google.setOnLoadCallback(drawChart);
  function drawChart() {
    var data = new google.visualization.DataTable();
    data.addColumn('datetime', 'Date');
    data.addColumn('number', 'Phone balance');
    data.addRows([
    <?php foreach ($history as $_h) { ?>
      [new Date(<?php print $_h['year']; ?>, <?php print $_h['month']; ?>, <?php print $_h['day']; ?>, <?php print $_h['hours']; ?>, <?php print $_h['minutes']; ?>), <?php print $_h['value']; ?>]<?php if ($_h != end($history)) { ?>,<?php } ?>

    <?php } ?>
    ]);

    var chart = new google.visualization.AnnotatedTimeLine(document.getElementById('chart_div'));
    chart.draw(data, {displayAnnotations: true});
  }
</script>

<body>
    <div id='chart_div' style='width: 100%; height: 100%;'></div>
</body>

В результате работы скрипта получаем такую картинку. Этот скрипт работает всего пол дня, поэтому изменения совсем незаметные :)

Заключение

Практическая ценность такой игрушки весьма сомнительна :) Потом, когда у меня появится свой VDS :) это нужно будет переместить туда.

1 октября 2009 г.

ЧПУ для тэгов в Magento

Задача - сделать ЧПУ адреса для тэгов, т.е. из /tag/product/list/tagId/253/ сделать /tag/weight-loss. Думаю другого способа, кроме реврайтов, нет, поэтому будем создавать по реврайту на тэг. А т.к. модуль, создающий реврайты, у нас уже есть (Blogrewrite, он делает реврайты для блоговых адресов), то вместо создания нового модуля воспользуемся существующим. В будущем надо бы сделать отдельный модуль, если можно - даже перехватывать создание нового тэга и автоматически делать реврайт. Но сейчас лениво :)

Контроллер

Итак, добавляем новую кнопку на страницу модуля Blogrewrite, которая будет создавать/обновлять реврайты для тэгов. Соответственно, добавляем новое действие в контроллер Blogrewrite:
<?php

class Mage_Blogrewrite_Adminhtml_BlogrewriteController extends Mage_Adminhtml_Controller_Action {
    ...
    public function maketagrewritesAction() {
    ...
    }
}
?>
Действие очень простое - нужно перебрать все тэги и на каждый создать реврайт. Для этого будем использовать модель tag/tag:
// Load all approved tags
$model = Mage::getModel('tag/tag');
$collection = $model->getResourceCollection()
    ->addStatusFilter($model->getApprovedStatus())
    ->addStoreFilter(Mage::app()->getStore()->getId())
    ->load();
Получили коллекцию тэгов, утверждённых, и относящихся к текущему магазину. Дальше перебираем эту коллекцию и обновляем реврайты:
foreach ($collection as $_tag) {
    $tagId = $_tag->getId();

    $idPath = "tag/$tagId";
    $requestPath = 'tag/' . Slugify($_tag->getName());
    $targetPath = "tag/product/list/tagId/$tagId";

    $this->_makeRewrite($idPath, $requestPath, $targetPath);
}
Наверное я плохо искал, но я не увидел как сделать slug адреса с помощью Magento. Поэтому я написал свою функцию, которую вынес в index.php. Она нам ещё пригодится для шаблонов.
function Slugify($url) {
    // remove all non-alphanumeric characters except for spaces and hyphens
    $res = preg_replace('/[^a-z0-9- ]/', '', strtolower($url));

    // remove double spaces and substitute the spaces with hyphens
    $res = str_replace(' ', '-', str_replace('  ', ' ', $res));

    return $res;
}
Готово. Теперь что бы создать реврайты для тэгов нужно зайти в админку, на странцу модуля Blogrewrite, и нажать кнопку Make Tag Rewrites.

Шаблоны

Теперь адреса вида /tag/weight-loss обработаются нормально, только вот они пока нигде не используются. Нужно поправить шаблоны, которые выводят тэги. Это файлы cloud.phtml, list.phtml и popular.phtml в папке app\design\frontend\default\sunnyD\template\tag\. Вывод адреса тэга имеет такой вид:
<a href="<?php echo $_tag->getTaggedProductsUrl() ?>"><?php echo $this->htmlEscape($_tag->getName()) ?></a>
Нам нужно заменить вызов функции getTaggedProductsUrl на свой код:
<a href="<?php print trim(Mage::getBaseUrl(), '/') . '/tag/' . Slugify($_tag->getName()) ?>"><?php echo $this->htmlEscape($_tag->getName()) ?></a>
Готово! :) Реврайты созданы и работают, шаблоны выводят ЧПУ.