14 сентября 2010 г.

Отладка обратных вызовов (callback), Python, Pydev

Я пишу сайты на Django. Обычно разработка ведётся на локальной машине, а не на сервере :) И иногда приходится отлаживать коллбэки каких-то сервисов, например, IPN у PayPal. Проблема в том, что локальная машина недоступна из интернета и коллбэк просто не дойдёт до пункта назначения. Для решения этой проблемы есть несколько решений.

Удалённая отладка

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

Всё что нужно уже идёт вместе с Pydev, а на их сайте есть хорошая инструкция как настроить удалённую отладку. Схема такая:

  1. На локальной машине (там где запущен Eclipse/Aptana с Pydev) запускаем remote debugger server с помощью кнопочки start the pydev server. У меня эта кнопочка находится в перспективе Debug:

    После запуска сервера во вкладке Debug появится запущенный сервер, а во вкладку Console добавится строчка что сервер запущен.

  2. На удалённой машине нужно вставить в код, который нужно отладить, такую строчку: import pydevd;pydevd.settrace();

    Эта строчка импортирует модуль, который идёт вместе с Pydev, а вызов settrace() остановит выполнение скрипта, предоставляя нам возможность удалённой отладки. Модуль pydevd, вместе с содержимым папки <eclipse path>\plugins\org.python.pydev.debug_x.x.x.xxxxxxxxxx\pysrc, нужно разместить там, где Python сможет найти эти файлы (в папках из переменной окружения PATH, или добавить туда свою папку);

  3. Пока мы имеем никак не связанные две машины - локальную и удалённую. Что бы они смогли обмениваться данными, настроим ssh туннелинг. pydevd на удалённой машине по умолчанию будет слать данные (после вызова settrace()) в порт 5678 (соответственно, remote debugger server слушает тот же порт). Добавим новый порт в настройках PuTTY:

Всё готово. Я настраивал удалённую отладку для работы с Django приложением, поэтому проверим её на такои примере - надо отладить код вьюхи index, которая выполняется, когда пользователь заходит на корень сайта.

  1. Добавляю строчку:

    def index(request):
        import pydevd;pydevd.settrace()
        a = 1 + 2
        b = a * 2
        ...
        return direct_to_template(request, ...)
    
  2. Перезапускаю сервер, выполняющий Django приложение (Apache):

    service httpd restart

  3. Захожу браузером в корень сайта - выполнение кода останавливается, а Aptana на локальной машине позволяет отлаживать код.

Вот хорошее описание как работает settrace():

After passing through the pydevd.settrace() it should stop in any breakpoints you have defined. You can even call pydevd.settrace(suspend=False) if you don't want it to stop at the settrace call, only on defined breakpoints. There is a catch though, by default it'll only trace the current thread, if you want it to trace other threads in this call, you have to call settrace(trace_only_current_thread=False).

Т.е. лучше поместить вызов settrace() куда-нибудь в django.wsgi в код запуска приложения.

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

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

Перенаправление портов

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

Один из вариантов - настроить форвардинг портов ssh. Внешний сервис будет посылать коллбэк нашему серверу на специальный порт, а этот порт с помощью ssh будет перенаправляться на локальную машину с запущенным под отладчиком приложением.

  1. Для начала нужно разрешить демону sshd принимать запросы с удалённых адресов (по-умолчанию он принимает запросы только с localhost). Правим файл /etc/ssh/sshd_config, раскомментируем/добавляем строчку GatewayPorts yes:

    mcedit /etc/ssh/sshd_config

    Перезапустим sshd:

    service sshd restart

  2. Затем настраиваем PuTTY что бы перенаправлял порт удалённой машины (пусть будет 5679) на порт локальной машины (у меня django devserver работает на 9999):

  3. Запускаем PuTTY, запускаем django dev server под отладчиком, и заходим браузером по адресу http://<server.domain>:5679/ и пользуемся нормальной отладкой с бряками и прочим.

Оба способа требуют хорошего подключения к серверу. У меня PuTTY часто терял подключение и даже вылетал. Не очень приятно, но думаю для отладки пары коллбэков хватит :)

PS. Есть ещё как минимум один вариант - для Opera Unite есть приложение Web Proxy, которое делает почти тоже самое что и перенаправление портов, только сервер будет оперовский и без PuTTY.

21 июля 2010 г.

Компилирование Python пакетов

Недавно обновил Python до версии 2.7. Это правильно, потому что эта ветка будет поддерживаться долго-долго, параллельно с веткой 3.x, плюс она имеет некоторые клёвые фичи ветки 3.x (бэкпорт).

Но тут возникла проблема. Не все пакеты имеют скомпилированные установщики для Windows под Python 2.7. А установить такие пакеты средствами Python, такими, как pip, easy_install или setup.py install, не выходит, т.к. таким пакетам нужно откомпилировать кое-какой C/C++ код. А компилятора у меня нет. Значит, надо установить и настроить! :)

Можно пойти тремя путями:

  • Установить MS Visual Studio. Если я правильно понял, это не очень удобный вариант;
  • Установить Cygwin. Он создаёт Linux окружение в Windows. Тоже не очень хороший вариант, т.к. он будет компилировать код, который будет работать в этом Linux-окружении. В отличие от MinGW. К тому же у меня не получилось его завести после n времени;
  • Установить MinGW. Этот проект позволяет компилировать код в родной для Windows формат. Получающимся файлам не нужно никакое Linux окружение. Этот вариант и будем использовать.

Установка MinGW очень простая - заходим на сайт, качаем установщик, отмечаем нужные компоненты - c++ компилятор, fortran компилятор, mingw make, он скачает их из интернета.

После установки нужно добавить папку MinGW\bin\ в переменную окружения PATH. После установки MinGW у нас есть необходимые инструменты для компилирования C/C++/Fortran кода.

Теперь нужно настроить distutils для работы с этими инструментами. Создаём файл Python27\Lib\distutils\distutils.cfg и вставляем туда такой текст:

[build]
compiler = mingw32

Готово! Проверяем на пакете numpy. Скачиваем исходники (.tar.gz), распаковываем, выполняем:

cd numpy-1.4.1
setup.py install

Всё должно пройти хорошо и мы должны получить скомпилированный и установленный пакет numpy.

Кроме установки, setup.py может выполнять много других команд. Например, такая команда создаст установщик для Windows:

setup.py bdist_wininst

Получится файл numpy-1.4.1.win32-py2.7.exe:

Альтернативный способ, без редактирования файла distutils.cfg - указать компилятор в параметрах командной строки:

setup.py install build -c mingw32

Но не все пакеты могут быть так легко установлены. Некоторым нужно разрешить зависимости. Например, matplotlib как минимум не компилируется без freetype и без его исходников. Но это уже другая история.

Ссылки:

4 июня 2010 г.

22 марта 2010 г.

demalexx.org

А потому что дорого свой сервак. Да, blogger не умеет markdown, для этого я взял js реализацию Showdown, пишу текст на markdown, вставляю html в blogger. Имею real-time предпросмотр, удобнее чем в blogger.

Да, blogger не умеет картинки. Для этого есть Dropbox. Картинка вот например: Два типа людей

Это было весело, да...

18 декабря 2009 г.

Переезд

Наверное уже можно :)

http://demalexx.org

21 ноября 2009 г.

Сервисы Google и Python

Наткнулся в журнале Хакер на статью про использование сервисов Гугла в Питоне. Мощная штука! :) Можно работать, например, с блоггером, почтой, таблицами, документами и т.д.

Начать нужно с Google Data Protocol - страницы протокола, который используется для работы с API гугловых сервисов. Там есть библиотечки для разных языков.

Хотя, это API какое-то странное и не очевидное... Вот простой пример, который печатает в консоль все посты дефолтного блога пользователя demalexx@gmail.com.
# -*- coding:utf-8 -*-

'''This script prints all blog posts from blogger.com'''

from gdata import service
import gdata
import atom

blogger_service = service.GDataService('demalexx@gmail.com', 'password')
blogger_service.source = ''
blogger_service.service = 'blogger'
blogger_service.account_type = 'GOOGLE'
blogger_service.server = 'www.blogger.com'
blogger_service.ProgrammaticLogin()

query = service.Query()
query.feed = '/feeds/default/blogs'
feed = blogger_service.Get(query.ToUri())

blog_id = feed.entry[0].GetSelfLink().href.split("/")[-1]

feed = blogger_service.GetFeed('/feeds/' + blog_id + '/posts/default')

print feed.title.text
for entry in feed.entry:
    print "\t" + entry.title.text.decode('utf-8')
Вывод:
D:\Projects\python\google>get_blogger_posts.py
race1
    Сервисы Google и Python
    PAP Affiliate и eAccelerator
    SSH - аутентификация ключом
...

20 ноября 2009 г.

PAP Affiliate и eAccelerator

На сайте стоит eAccelerator и потом я поставил PAP Affiliate. С самого начала этот пап работал очень медленно. Настолько медленно, что оставлял после себя процессы httpd, работающие по несколько часов и потребляющие все ресурсы процессора. Единственный выход был в перезагрузке Апача, но это спасало не надолго.

Интересна реакция саппорта. Они просили проверить настройки электропочты. Типа проблема в этом. Хотя письма как раз отправлялись. Попробовал я поменять эти настройки - в настроках PAP сделал отправку писем не функцией mail(), а используя SMTP сервер - ничего лучше не стало, конечно.

Как временная мера - установил лимит на выполнение скриптов из папки PAP в 30 секунд :)

В общем в итоге всё оказалось просто. PAP не работает с eAccelerator. Вот страница у них на сайте, где говорится что они знакомы с проблемой, и ничего не собираются с ней делать: http://support.qualityunit.com/knowledgebase/post-affiliate-pro/troubleshooting/eaccelerator-module-installed-at-server.html.

Что бы решить проблему нужно выключить eAccelerator для скриптов из папки PAP. Для этого я создал файл /var/www/vhosts/centurysupplements.com/conf/vhost.conf (конфиг Апача для виртуального хоста), и написал туда такой текст:
<Directory /var/www/vhosts/centurysupplements.com/httpdocs/affiliate>
    php_admin_value eaccelerator.enable 0
</Directory>
Т.е. выключить eAccelerator для папки /var/www/vhosts/centurysupplements.com/httpdocs/affiliate (там установлен PAP).

Теперь PAP просто летает, а eAccelerator работает для всего остального сайта.

PS. Удалил eAccelerator, заменил его APC. Он, говорят лучше, плюс от создателей PHP. И PAP с ним работает.

15 ноября 2009 г.

SSH - аутентификация ключом

Вот здесь хорошая документация, первоисточник - http://the.earth.li/~sgtatham/putty/0.58/htmldoc/Chapter8.html.

В SSH можно авторизоваться либо паролем, либо ключом. Авторизация ключом более секурная, но настраивается на 5 минут дольше авторизации паролем (которая просто уже есть) :) А ещё, используя авторизацию ключом, можно избавиться от ввода пароля, правда это будет уже не так секурно.

Суть в том, что генерируется пара ключей - приватный и публичный. Приватный ключ находится только у пользователя, и используется для шифрования сигнатуры (кража приватного ключа означает то же самое, что кража пароля). Расшифровать сигнатуру можно публичным ключом, который нужно поместить на сервер. При аутентицикации Putty генерирует сигнатуру, которая шифруется приватным ключом, и отправляет её серверу. Сервер, используя публичный ключ, расшифровывает сигнатуру и проверяет её. Т.е. авторизация пройдёт только в том случае, если сигнатура зашифрована приватным ключом пользователя. Это более секурно, потому что даже получив доступ к серверу, злоумышленник не узнает приватного ключа (который хранится у пользователя), в отличие от пароля (который хранится на сервере). Правда для этого очевидно нужно отключить авторизацию по паролю на сервере...

По-хорошему приватный ключ тоже следует зашифровать. Но тогда при каждм соединении по SSH нужно будет вводить пароль для рассшифровки приватного ключа. Если же приватный ключ не шифровать, и указать в Putty имя пользователя для входа - вводить пароли не нужно будет вообще. А только следить что бы приватный ключ никто не стырил :)

Для генерирования ключей нужно взять программку PuttyGen. С её помощью сгенерировать пару ключей. Публичный ключ нужно поместить в файл .ssh/authorized_keys2 в домашней папке на сервере. Каждая строка файла - отдельный публичный ключ. Приватный ключ нужно сохранить куда-то на компьютере.

В Putty во вкладке SSH->Auth нужно указать приватный ключ в поле Private key file for authentication. Что бы Putty автоматически использовала имя пользователя для входа, его нужно прописать в поле Auto-login username, во вкладке Connection->Data.

Готово. Теперь при соединении аутенификация будет происходить по ключу, не нужно вводить ни логин, ни пароль. Удобно :)

HeidiSQL через SSH

Оказывается, настроить HeidiSQL (фронтэнд к MySql) для работы с удалённой базой через SSH оказалось не просто, а очень просто :) На удивление такой вариант работает гораздо быстрее чем phpMyAdmin, и потребляет совсем немного трафика. А ещё это более производительный вариант - серверу не нужно обрабатывать множество http запросов от phpMyAdmin. В общем, теперь можно забыть phpMyAdmin как страшный сон, и очень удобно работать с локальными полноценными приложениями типа HeidiSQL и другими.

Итак, что бы это заработало, нужно настроить туннелирование в Putty и настроить соединение в HeidiSQL.

Настройка Putty

Во вкладке SSH->Tunnels нужно добавить новый туннель, где Source это локальный порт, который будет связан с портом на сервере (который слушает MySQL). Порт 3306 занят локальным MySQL, поэтому укажем порт 3307. Destination это имя хоста и порт MySQL на сервере (по-умолчанию 3306), указываем localhost:3306. Всё, теперь все запросы на локальный порт 3307 будут перенаправлены на порт 3306 на сервере.

Настройка HeidiSQL

Создаём новое соединение, указываем хост localhost, и порт 3307. Ещё нужно указать логин/пароль для доступа к базам. Готово.

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"