10.26.2010

Populate fake data in django

Задача - нагенерить N рандомных слов, которые хоть чуть-чуть похожи на настоящие слова (то есть подобие слогов). Халява на пайтоне:

>>> def gen_rand_strings(num):
...     import random
...     allchars = [chr(i) for i in range(97, 97+26)]
...     vowels = ['a', 'e', 'i', 'o', 'u', 'y']
...     consonants = [i for i in allchars if i not in vowels]
...     ret = []
...     for i in range(0, num):
...         s = ''
...         rr = random.randint(2, 6)
...         for j in range(1, rr):
...             s += random.choice(consonants)
...             s += random.choice(vowels)
...         ret.append(s)
...     return ret
>>> gen_rand_strings(20)
['vydohefy', 'hete', 'comegihu', 'sarabo', 'tyde', 'ro', 'mojuletabu', 'ruridaviqy', 'my', 'tafaluhy', 'teleniwu', 'xixy', 'cyky', 'hypakelo', 'kicuba', 'jekiqovazu', 'botaveci', 'byru', 'dolizojaca']

Зачем это понадобилось, да вот захотелось набить БД фейковыми данными :) примерно так (по проекту homebudget):

>>> from homebudget.purchases import models
>>> models.PurchaseTag.objects.count()
0
>>> new_tags = gen_rand_strings(20)
>>> for tag in new_tags:
...     ntag = models.PurchaseTag.objects.get_or_create(value=tag, norm_value=tag, owner=usernew)
...     ntag[0].save()
>>> models.PurchaseTag.objects.count()
20
>>>
>>> from datetime import date, timedelta
>>> last = date.today()
>>> dt = date(year=2010,month=05,day=1)
>>> oneday = timedelta(days=1)
>>> while (dt < last):
...     rr = random.randint(3, 10)
...     for i in range(0, rr):
...          tag = models.PurchaseTag.objects.get(value=random.choice(new_tags))
...          price = random.randint(1, 100)
...          purch = gen_rand_string(2)[0]
...          p = models.Purchase(name=purch,quantity=1.,price_for_one=price,price_total=price, purchase_date=dt, owner=usernew)
...          p.save()
...          p.tags.add(tag)
...          p.save()
...     dt += oneday
База забита фейковыми покупками за полгода, к каждой покупке привязан один из 20 новых тагов. Теперь можно спокойно тестить выборки для построения статистики. Халява как она есть.

10.18.2010

хроники django или homebudget part 7

После долго молчания, возвращаемся к нашим баранам.


Во первых, что обязательно нужно описать, так это передвижение с google-code на launchpad. Предпосылки:

  1. во-первых плюсы DVCS системы:
    1. банальные мелочи, у меня есть четыре машины с которых я колбашу код. Естественно, я один, про фитчи думаю один, изменения архаичные, некоторые нужно сразу в главную ветку, другие - только в бранчи. В свн-бранчи все оформлять тупо лень (т.к. о них нужно помнить). По этому колбашу в trunk-е. И синхронизировать 4 машины ой какая проблема - типа вчера уснул и часть не закомитил, а изменения были эпические. Работа встала на других машинах. В распределенной системе - каждая локальная копия по-сути бранч. Работотать на нескольких машинах в разы проще.
    2. смерджить можно все что угодно, без последствий. В свн-е же для мерджа нужные крепкие нервы и холодные разум.
    3. можно без проблем откатиться до любой версии. Кто как, но я это люблю.
  2. во-вторых плюсы launchpad-а:
    1. bazaar собственно.
    2. Хостинг многих опен-сурс проектов, которые я использую (хотя бы убунта и терминатор)
    3. всякие бонусы типа отображения публичного gpg ключа и активности пользователь.
    4. есть публичная страничка на которой отображены все проекты пользователя. В гугль-коде такую не нашел. Мне это критично.
  3. давно хотел провести глобальные изменения архитектуры
    1. есть мой хостинг (пока не рассекреченный), крутится ngnix, заведен джанго-проект. В нем крутится homebudget (как не трудно догадаться). Иногда мне нужно добавлять новые приложения для личных целей (скорее не целей, а идей и экспериментов). Заводить новый процесс под это ой как не хочется, да и не логично. А в старой структуре свн-а был заведен репозиторий прямо на проект, и лежащие внутри приложения. Новые приложения хранятся (или не хранятся вовсе) в разных местах. Ну вот и логичнее всего мне вести репозитории по приложениям, а не по проекту. Так что от репозиторя для проекта было решено отказаться в пользу локального репозиторя. А приложения - в народ.
    2. Еще один плюс приложений - манипулировать гораздо проще. Развертываешь в отдельной папке такую-то ревизию, ставишь на нее симлинк, а старый затираешь. Круто, да круто.
    3. Хотя нужно сказать структура проект-приложение не совсем гибкая. Это не удобно, тупо не удобно. Нужно больше ветвления. Типа переиспользуемые под-приложения.

Как это делалось, да просто как можно догадаться.

  1. изменил все межпроектные линки на использование глобальных "homebudget.purchases" и "homebudget.tagsfield"
  2. вынес маппинг урлов из проектого urls.py в соответствующие файлы приложений. В проектном urls.py теперь стоит просто include bla-bla-bla.
  3. разместил статик контент и темплейты по директориям проектов (т.е. было "homebudget/content/purchases/css", стало "homebudget/purchases/media/css" и тд)
  4. в новый проект добавляется директория типа "static-content", в нее ставятся симлинки на директории "homebudget/purchases/media". MEDIA_ROOT смотрит на директорию "static-content". В итоге: из проекта до его статик файлов можно достучаться по пути "/site-media/purchases/css/" (работает MEDIA_URL, имя проекта, и путь по симлинке).
  5. темплейтные директории прописываются явно в settings.py в TEMPLATES_DIR (типа "/homebudget/purchases/templates/"). Соответствующий код для взятия темплейтов был подправлен.
  6. ну и проекты были добавлены в PYTHONPATH, типа "export PYTHONATH=${PYTHONPATH}:/path/to/project" где-то внутри "~/.bashrc"

Собственно ссылки:


Посты по теме:

8.27.2010

FrOSCon 2010

Итак, пора уже представить миру мой отчет о том как проходила конференция FrOSCon 2010 (Free and Open Source Software Conference). Состоялась она 21-22 августа, в Германии, в "городке" Sankt-Augustin, в здании местного IT-технического университета.


почему "в городке" - насколько я понял, из объяснений друзей, участников конфы, и карты метро-трамваев - Санкт-Агустин это не совсем город, не совсем деревня, не совсем район города, но официально город. Изначально я приехал в Бонн (население около 300 тыс), но этого города не было в объявлении поезда, так как в 30 км от него находится Кельн где проживает около 3 млн. Бонн состоит из районов - давным давно они были деревнями, потом срослись и сохранили свои названия в названиях районов. Совсем рядом к Бону, ну полностью прилегают, без какой-то границы, еще штук 7-8 аналогичных городов, в числе которых и Санкт-Агустин, и Кельн.

Нужно сказать что конференция проходила пятый раз и выросла с маленькой тусовки в 10-20 человек в конференцию, объединяющую более одной тысячи человек. Организаторы, в бывшем студенты сего местного университета в один день сказали что-то типа "we will go build our own conference, with blackjack and hookers", договорились и сделали. Полный опен-сурс, полный кайф. Что у них получилось? Не маленькая локальная тусовка гиков, а действительно международная тусовка гиков. Хотя на конференции сильно чувствуется преобладание немецкого языка (большинство народу все-таки местные), но как водится в it-community "да че этот английский язык?! там все слова взяты из си-плюс-плюса!" - большинство нормально говорят по английски, и могу свободно изъясняться на двух языках. Как говорят, в этом году было предложение провести полностью конференцию (все доклады) на английском языке, но встала проблема, не все докладчики могли полностью свободно изъясняться на английском, такие дела. Но радует (как для человека без знания немецкого) что около 25% докладов были на английском. Этого вполне достаточно. Если есть вопросы - на них докладчик может ответить и на ломаном английском, но без микрофона.


Где это было. В августе в университетах не проводятся занятия, но все равно там находится туча народу, кто-то делает свой проект, кто-то просто тусуется. И понятно почему. Здание универа просто восхитительное, все очень и очень модерновое, все укомплектовано под завязку, в аудиториях очень сильно приятно находиться. Все семинарные комнаты оборудованы компами, за каждым местом студента. Вокруг универа какие-то архитектурные сооружения (типа гигантского магнита). Внутри куча работающих кафе. Пусть наши универы (вроде триждыпроклятого и родного НГУ) поперхнуться слюной. Организаторы полностью снимают это здание на выходные. Фойе и коридоры отводятся под стенды контор, лекционные аудитории - под доклады, семинарские аудитории - под проекты, столовая - под столовую, внутренний двор - под опен-эйр-пати.


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


Как проводилось. Достаточно скромно, вход для обычных людей на 2 дня - 5 евро, для коммерческих контор - 100 евро, для волонтеров - бесплатно. Вот вам и сила опен-сурса. Сколько нужно мозгоебаний и затрат для того что бы попасть на конфу мсфт? во-во. Стендов было много. bsd, debian, ubuntu, gentoo, centos - и все им смежные... gnome, kde, qt... postgresql, firebird, mysql... openoffice... zope, и че-то там еще для python-а... xen и че-то еще для виртуальщиков... куча прессы типа open-source-press где можно было в тридешево купить книжки (только на немецком)... какие-то проекты типа linux для школы, ноутбук каждому, космическая программа германии... был и гугль, и был (мне не известный) tarent... Всего не упомнил, но большинство охватил.


Кстати что не понравилось - стенд гугля - натянули на стенде три майки с лого, поставили на стол пару макбуков, посадили за стол 3-4 невменяемых человека, и дали им пару занимательных головоломок - стремно это как-то выглядело, но очереди к их стенду были всегда. А вот tarent зажег, действительно было на что посмотреть, загончик с радиоуправляемыми роботами, у которого постоянно крутились дети, куча-куча маленьких стендов, куча-куча людей, какие-то конкурсы.

Каждый час в лекционных были доклады. Об докладах читать ниже. Каждый час одновременно 5 докладов, кроме keynotes раз в день (который для всех). Далее я опишу те доклады на которых мне посчастливилось присутствовать и мое резюме по ним.

  1. Free and Open Source Software in the Developing World Jon "maddog" Hall это было в качестве keynotes, человек-директор Linux International, такой тихинький свиду дедушка, отжег по полной. Полная ненависть к Windows, много умных вещей про развитие стран третьего мира, про коммерческий и опен-сурсный код. Очень профессиональный доклад. Куча инфы, перемешка шуток, очень хорошие ораторские способности. Основные идеи: странам третьего мира навязывается использование проприетарного софта, в большинстве случаев которому имеются полностью свободные аналоги, все происходит от удалености и незнания, на это тратится уйма денег.
  2. Agile in a Year Thomas Ferris Nicolaisen - доклад не про опен-сурс, а про технолигии которые доступны всем, про очень удобные методы разработки, про то как одна компания из Бонна внедряла Agile процессы на практике. Замечательный доклад, как мне показалось, очень много практических вещей было освещено, что было сложно что было легко, зачем и почему. Тема конечно же очень общирная, но тут все было представлено на одном конкретном примере. Допустим "... таким образом для выполнения основных фитч нашей программы мы выбрали систему канбан, и использовали ее напротяжении месяца". Говорилось про то, как в данной конторе пришли к идее ежедневных stand-up meetting, про переселения девелоперов, про функции менеджеров, про то что agile-master бесполезный человек и тп. Очень интересно. Многим компаниям хотя бы до таких мыслей расти и расти, а тут люди не боятся и экспериментируют. Не понравилось то что пример очень уж узкий вышел - компания состоит около 10-15 человек. Что понравилось: графическое представление презинтации, сразу же на все изображение выводятся все слайды, логические переходы между ними обозначены стрелочками, когда разговор переходит на какой-то слайд - тогда камера "приближается"/"перемещается" к нужному участку карты. Очень удобно понимать логику всего доклада, да и запоминается намного лучше, что и отокуда.
  3. Inside the Cassandra distributed database Jonathan Ellis - доклад про не-реляционную БД cassandra. Я все время знал про существование таких бд, про то как их применять, и зачем они нужны, но у меня никогда не было причины задействовать такую БД. Это используется в facebook, в amazon s3, в twitter, в digg и т.д. Там где очень много данных, где данные однотипные и всегда растут в колличестве. После прослушивания доклада я наконец-то проникся идеей, это круто. Любое возможное представление, любые масштабы. Да сложно проектировать и программировать. Но если рассматривать такой тип БД как средство оптимизации - то есть уже полностью есть модель sql обычной базы, все отлажено, выставлены все нужные индексы и тп., но падает производительность на простых запросах, базу нужно перетаскивать на несколько машин и т.д. - тогда вполне реально перетащить базу на cassandra, переделать все sql запросы - и будет простое чудо. Докладчик рассказывал про внутреннее устройство, как происходит запись и чтение, что такое "ключ" в данной бд, как строится дата-кластер, какие возможности есть. Все очень понятно. Не понравилось то что в зале возникло много откровенно тупых вопросов по устройствам дата-кластеров, возможно это сложно для обычных людей. Мне очень понравилось.
  4. Be lazy, make automation Fabrizio Manfredi - говорилось про то как построить свой собственный "умный" дом. Идея в основном такая: существуют девайсы которые можно купить (типа розетка с lan/wlan интерфейсом), поставить их в свой дом для освещения и т.п., потом настроить домашний софт на влючение/выключение по рассписанию (по cron-у), или на показания других датчиков (типа датчиков движения). Входишь в комнату - свет включается, выходишь - выключается. Нужно разогреть баню пока едешь в общественном транспорте? - подключаешься к домашнему серверу или отправляешь смс-ку. Кондиционер сам выключается когда открыты окна. Согласитесь, это круто. Докладчик объяснял как это все делается, на каких протоколах базируется система, как лучше строить архитектуру (от базового - провода или wifi, до сложного - распределенной системы), какие опен-сурсные проекты реализуют доступ к протоколам. Да это очень прикольно, но дорого, одна розетка стоит 10-20 евро, так как в ней стоит как минимум один чип. Плюс, вопрос секурити вообще не ставится (тупо твой сосед может выключить тебе свет). В общем я сильно проникся, но пока это мне не по карману, да и строить такое нужно только в собственном жилье.
  5. Antifeatures, Benjamin Mako Hill - тоже keynotes, тоже очень знаменитая персона (бог debian-а и gnu, как-никак), тоже очень интересно. Что сразу заметилось - очень быстрый и очень понятный английский. Человек тоже замечательный оратор, где нужно вставит конфузную шутку на предмет Windows и тп. Основные идеи: каким образом строится стоимость софта, за что реально приходится платить, какие минусы (не баги) может приносить использование софта, ну и собственно идеи GNU. Один из первых примеров - Windows NT Workstation и Windows NT Server - отличались в цене на 800 баксов, а фактически - одной записью в реестре. За что платится разница? во-во, наводит на мысли.
  6. MySQL Plugins - what are they? Sergei Golubchik - рассказ про еще один форк mysql, про то как в достаточно большой проект была впилена сисмета плагинов и расширений. Вообще говоря - ничего особенного по части проги я не услышал, все очень привычно. Ничего не скажу как это смотрится со стороны знатоков mysql, к коим я не отношусь. Запомнилось одно - английский от русского человека слышно за версту, это точно.
  7. HiPHoP for PHP - докладчик - человек из комманды facebook, у них сейчас много опен-сурсных проектов. Что такое php я сам себе слабо представляю, попал на доклад случайно, но он мне понравился, я достаточно хорошо туда попал. Есть какая-то магия в том что делает facebook, во-первых если им не нравится php - они его исправляют и не стесняются, во-вторых - у них очень даже рационализаторский подход. Так как пхп - интерпретируемый язык и на фейсбуке сидят миллионы, то давайте перенесем код пхп на цпп. Все, на. Другими словами - сделаем свой интерпритатор пхп который будет рождать бинарный код. Вот и вся суть проекта. Да, есть проблемы со скоростью работы, удобством и т.п. Но это работает в разы быстрее и стабильнее! Я проникся.
    Интересно заметить когда в зале возникла куча вопросов по поводу скорости, и компиляции докладчик просто свернул презинтацию, открыл консоль и показал что и как делается. Да реально, вся аудитория засекала ~20 секунд. Вот еще одна фишка it-конференций, можно продемострировать все за пару секунд.
  8. GNOME 3 Hendrik Richter - тут мало чего сказать, так как доклад был на немецком, да и решают только картинки. Могу сказать три вещи: это будет скоро, много добавится графических наворотов, они уже думают об GnomeOS.
  9. 1, Qt, Profit! Daniel Molkentin - честно говоря этот доклад был расчитан на людей которые скажут "ой, а что такой qt?", по-этому особых эмоций у меня не возникло. В большей части было показано как обычное десктоп приложение портируется на symbian, "то что можно его отлаживать" ну и все. Жидко как-то, не уровень qt, это всяко.

Еще был Social Event - огромная пьянка на открытом воздухе, я там был и мне понравилось. Особо нужно сказать как она проводилась. Есть собственная валюта конференции "Bon" - 1 Bon = 1 euro, пиво и еду можно покупать только за Bon-ы. Маленький стаканчик пива - 1 Bon, Большой стакан - 2 Bon + 1 Bon (fant), Мясо (ааа!) - 3 Bon. Вот там монжно было бухнуть много пива и заобщаться с любым человеком. Были награждения проектов (которые я собственно даже не видел), было файер-шоу, был большой торт... у, там было круто!!


Это все что я смог написать про это событие, многое еще крутится в голове, но изложить будет сложно. Выводы - таких конференций очень не хватает в России, очень не хватает конференций такого уровня (типа тут ставятся вопросы - что делать если у тебя дата-кластер на 1000 машин, а не банальное - как установить линукс новичку), просто не хватает аналогичных по духу событий. А организаторам FrOSCon - отдельное спасибо, все было просто круто!!

8.07.2010

хроники django или homebudget part 6

Давно не писал в хроники, а проект уже ушел далеко, так что сначала догоню версию блога до версии репозитория :) Сегодня будет html-css-javascript, делаем календарик для просмотра месяца.

  1. Итак, у нас есть master page следующего содержания. Из отличий с предыдущим его описанием - добавил блок head, через который дочерние страницы могут добавлять скрипты и стили в html код страницы.
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
        <link rel="stylesheet" href="content/css/main.css" />
        <title>Homebudget site: {% block title %} manage yourself {% endblock %}</title>
    {% block head %}{% endblock %}
    </head>
    <body>
    <div id="content">
    {% block content %}
    {% endblock %}
    </div>
    </body>
    </html>
    
  2. Еще есть убогий month view который выводит обычный список день-сумма.
    {% extends "purchases/master.html" %}
    
    {% block title %} monthly view {% endblock %}
    
    {% block content %}
    <p>Date: {{view_date}} </p>
    {% if day_price_dict %}
        <ul>
            {% for key,value in day_price_dict.items|dictsort:"0" %}
                <li>
                    {{key}} 
                    <b>{{value}}</b> 
                    <a href='/purchases/view/{{key.year}}/{{key.month}}/{{key.day}}/'> Edit </a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No purchases yet</p>
    {% endif %}
    <p>
        Total: <b>{{month_total}}</b>
    </p>
    
    <a href='/purchases/add/'>Add</a>
    {% endblock %}
    
  3. Я хочу отобразить календарик. Самое простое решение - тупо таблица, ряды - недели, столбцы - дни недели. Но как сказал знакомый верстальщик "деревня, таблицы - это прошлый век, нужно использовать div-ную верстку". В чем она заключается, как я понял для себя, это выделение всех элементов в блоки, и задание их относительного расположения через стили. В итоге было подсмотрено решение в одном проекте.
  4. В темплейте month-view заключаем список день-сумма в div с id-шником dashboard
    <div id="dashboard">
    <ul>
    ...
    </ul>
    </div>
    
  5. Создаем файлик dashboard.css в content/css, добавляем его в monthview темплейт.
    {% block head %}
    <link rel="stylesheet" href="content/css/dashboard.css" />
    {% endblock %}
    
  6. Теперь все происходит в dashboard.css, сначала выставим фиксированную ширину для контейнера dashboard и уберем дефолтные стили списка с ul.
    #dashboard {
        width: 900px;
        display: inline;
        position: relative;
        float: left;
    }
    #dashboard ul {
        list-style: none outside none;
    }
    
  7. Элементу списка (можно сказать элементу календаря) выставляем фиксированный размер - высоту и ширину. Размер вычисляем по формуле "ширина dashboard-а" разделить на 7, минус пара пикселей. Логика простая - в неделе 7 дней, по этому в контейнер должно помещаться не более 7 элементов. А плюс-минус пиксели, это нужно для учета бордеров элементов. А так же выставим float: left, этот магический параметр определяет с какой стороны будут располагаться соседние элементы. То есть для left следующий элемент выстроится слева от текущего. Если располагать некоторым абстрактным мышлением то можно понять к чему все это приведет. Правильно, к календарю.
    #dashboard li {
        float: left;
        position: relative;
        width: 120px;
        height: 120px;
        text-align: center;
    }
    
  8. Можно посмотреть, да выглядит похоже, но не айс, так как дни не располагаются в соответствии с днями недели. Потому что в возвращаемом списке из вьюхи нет дней соседних месяцов, которые довершали недели. Не проблема добавить ;) Нужно в цикл обхода дат начинать от начала недели (не важно текущего или предыдущего месяца), ну и завершать аналогично. Дополнительно в возвращаемый словарь добавим значение что это за день: день текущего месяца, неактивного месяца, выходной, сегодня.
    def month_view(request, year, month):
        """
        List all day in month, with daily sum
        """
        try:
            view_date = datetime.date(year=int(year), month=int(month), day=1)
        except:
            raise Http404
        from django.db.models import Q
        plist = Purchase.objects.filter(Q(purchase_date__year=view_date.year) & Q(purchase_date__month=view_date.month)).order_by('purchase_date')
    
        y, m, d = view_date.timetuple()[:3]
        dt_next = datetime.date(year=y+((m+1)/12), month=(m+1)%12, day=d)
        day_price_dict = {}
    
        first_calend = view_date
        while first_calend.weekday() != 0:
            first_calend -= datetime.timedelta(days=1)
        dt = first_calend
        while dt < view_date:
            day_price_dict[dt] = (Decimal('0.0'), 'prev_month')
            dt += datetime.timedelta(days=1)
    
        month_total = Decimal('0.0')
        while (dt < dt_next):
            purchases_day = plist.filter(purchase_date__day=dt.day)
            day_total = Decimal('0.0')
            for it in purchases_day:
                day_total += it.price_total
            month_total += day_total
            weekend = dt == datetime.date.today() and 'today' or dt.weekday() >= 5 and 'weekend' or 'day'
            day_price_dict[dt] = (day_total, weekend)
            dt += datetime.timedelta(days=1)
    
        last_calend = dt
        while last_calend.weekday() != 0:
            day_price_dict[last_calend] = (Decimal('0.0'), 'prev_month')
            last_calend += datetime.timedelta(days=1)
    
        return render_to_response('purchases/month_view.html', locals())
    
  9. Теперь сделаем нормальное отображение дня (ячейки календаря). Для этого сначала выставим классы для элементов. sum для суммы дня, edit для линки перехода на день, date для числа календаря. Для самой ячейки нужно добавить класс дня, полученный в предыдущем пункте.
    <div id="dashboard">
        <ul>
            {% for key,value in day_price_dict.items|dictsort:"0" %}
                <li class="{{value.1}}">
                    <span class="date">{{key.day|stringformat:"02i"}}</span>
                    {% ifnotequal value.1 'other_month' %}
                    <span class="sum">{{value.0}}</span> 
                    {% endifnotequal %}
                    <a class="edit" href='/purchases/view/{{key.year}}/{{key.month}}/{{key.day}}/'>Edit</a>
                </li>
            {% endfor %}
        </ul>
    </div>
    
    Потом обозначим для них стили отображения внутри ячейки. Выставим top-left-right-bottom, для этого понадобится position:absolute.
    #dashboard .date {
        font-size: 60px;
        position: absolute;
        left: 10px;
        top: 10px;
    }
    #dashboard .sum {
        font-weight: bold;
        bottom: 5px;
        right: 5px;
        position: absolute;
        font-size: 20px;
    }
    #dashboard .edit {
        position: absolute;
        right: 5px;
        top: 5px;
    }
    
  10. Теперь мы займемся дизигнерством. То есть разукрасим календарь. Так как необходимые навыки отсутствуют напрочь, и вообще мы дальтоники, то спиздим. Открыл для себе клевый сайт на котором можно выбрать комбинацию сочетающихся цветов. Мой выбор такой. В соотвествии этому выставим цвета для всех элементов.
    #dashboard li {
        background: none repeat scroll 0 0 #D5E1DD;
        border: 1px solid white;
        font-color: white;
        cursor: pointer;
    }
    #dashboard .prev_month {
        background: none repeat scroll 0 0 #747e80;
    }
    #dashboard .weekend {
        background: none repeat scroll 0 0 #F7F3E8;
    }
    #dashboard .today {
        background: none repeat scroll 0 0 #F2583E;
    }
    #dashboard .hover {
        background: none repeat scroll 0 0 #77BED2;
    }
    #dashboard .date {
        color: white;
    }
    #dashboard .sum {
        color: #2B3E42;
    }
    
  11. Ну и последние штрихи. При наведении на день курсора будем менять цвет ячейки. Для этого понадобится клиентский javascript. Будем использовать мега рулезную библиотеку jQuery. Есть несколько способов ее подключить.
    • Самый простой и стандартный. Скачать себе jquery, и добавить в статические ресурсы сайта. Использовать можно следующим образом (добавляем в head):
      <script src="content/common/js/jquery-1.4.2.js"></script>
      
    • Использовать гугл-апи. Способ матер и говорят для оптимизации кеширования само оно, но для себя я отметил следующий минус - для локальной разработки он тормозит сильно. Да и кол-во запросов увеличивается. В общем я от него отказался в пользу первого. Использовать можно следующим образом (добавляем в head):
      <script src="http://www.google.com/jsapi" type="text/javascript"></script>
      <script type="text/javascript">
          google.load("jquery", "1.4.2");
      </script>
      
    • Что-нить еще, типа подключить с какого-нить хостинга.
  12. Заполучив jQuery можно решить задачу с изменением цвета под курсором.
    <script type="text/javascript">
    $(document).ready(function(){
        $('#dashboard li').bind("mouseenter", function() {
            var pthis = $(this)
            pthis.addClass('hover')
        });
        $('#dashboard li').bind("mouseleave", function() {
            var pthis = $(this)
            pthis.removeClass('hover')
        });
    });
    </script>
    

Посты по теме:

7.24.2010

compiz headtracking

Моя цитата из твиттера: "собрал эту игрушку, забавно и бесполезно, но кайфа - вагон! хочу большой монитор! http://www.youtube.com/watch?v=kTNG1GN4VV8"

Итак вдохновившись... Теперь о том как она ставилась

  1. Хочу предупредить: у меня ubuntu 10.04, тачка домашне-девелоперская, много на ней чего стоит, так что кой-чего может и не оказаться в вашей системе. Не стоит бояться, как только проблема - сразу же внимательно читаем ошибку и ищем недостающий пакет. В общем, стоит начать с того что - обновляем систему. Главная цель - получить compiz-fusion 0.8+
  2. Очень бодро качаем сам headtracking плагин, и смотрим на то как он не собирается.
    mkdir projects/headtrack
    cd projects/headtrack/
    git clone --depth 1 git://anongit.compiz-fusion.org/users/klange/headtracking
    cd headtracking/
    make
  3. Говорит что-то про bcop, быстро находим в репозитории
    apt-cache search compiz-fusion-bcop
    sudo apt-get install compiz-fusion-bcop
  4. Совершенно аналогичным методом разрешаем остальные зависимости.
    sudo apt-get install compiz-dev libcompizconfig0-dev libtool libtool highgui libhighgui-dev
    
  5. Примерно где-то здесь начнется проблема с opencv. ВАЖНО: не стоит искать в репозитории libcv - хотя она там и есть, но увы не заработает, очень старая сборка, проверено. Это была моя большая ошибка, не повторяйте ее.
  6. Собираем руками последнюю версию opencv из репозитория:
    mkdir projects/opencv
    cd projects/opencv
    svn co https://opencvlibrary.svn.sourceforge.net/svnroot/opencvlibrary/tags/latest_tested_snapshot
    cd latest_tested_snapshot/opencv
    mkdir release
    cd release/
    cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D BUILD_PYTHON_SUPPORT=ON ..
    make
    
  7. После того как вы сварили и попили чаю собралась opencv, устанавливаем ее в систему.
    sudo make install
    sudo vim /etc/ld.so.conf.d/opencv.conf
    sudo ldconfig -v
    
  8. Теперь финально билдим headtracking и устанавливаем его в систему:
    cd projects/headtracking
    make clean
    make
    sudo make install
    
  9. Теперь идем в настройки compiz-а, жмем клавишу extraz, там headtracking. Включаем его. Окошки видоизменились. Нажимаем галку - General/Webcam head track. Теперь если веб-камера включилась и изображение поплыло в какую-то сторону, то все правильно сделано. Можно наслаждаться. Если нет и камера не включилась - что-то значит у вас не по фен-шею или чего еще, типа генетики. А скорее всего тупо web-камера не совместима с video4linux, чего и советую проверять :)
  10. Когда у вас заболит шея (как у меня), можно поинтересоваться как это все работает. Для ленивых хинт:
    cd projects/opencv/latest_tested_snapshot/opencv/samples/c/
    chmod +x build_all.sh 
    ./facedetect
    

6.27.2010

хроники django или homebudget part 5

За посленее время много чего натворил в проекте, попытаюсь все изложить.

  • довел до ума систему тегов
  • добавил South к проекту для удобной работы
  • переделал логику добавления/редактирования покупок
  • сделал красивое отображение месяца

Так судя по всему, первые два пункта получились достаточно обширными, так что сегодня только про них. HTML и javascript останутся на завтра.

South - удобный процесс миграции БД

  1. для начала я обновил себе django до последней дев-сборки (в репозиториях убунты все-то лежит 1.1).
    sudo apt-get remove python-django
    cd ~/devtools
    svn co http://code.djangoproject.com/svn/django/trunk/ django
    cd django
    python setup.py install
  2. стоит проверить что django проекты не потеряли работоспособность (:
  3. Устанавливаем south (для этого нужен mercurial)
    cd ~/devtools
    hg clone http://bitbucket.org/andrewgodwin/south/
    cd south
    sudo python setup.py install
  4. Добавляем south в installed_app (в settings.py)
    INSTALLED_APPS = (
         ...
         'south',
    )
  5. для параноиков - backup/restore существующей бд.
    • Backup:
      sudo -u databaseuser pg_dump homebudget > dump_db
    • Restore:
      sudo -u postres psql homebudget2 < dump_db
  6. создаем таблицы для south в проекте (это последний раз когда мы сделали syncdb(: )
    ./manage.py syncdb
  7. теперь если бд пустая (т.е. таблицы для проектов не созданы), добавляем проекты в installed_apps и выполняем
    ./manage.py schemamigration purchases --initial
    ./manage.py migrate purchases
    
  8. если данные в ней уже были, то: создаем начальную точку миграции
    ./manage.py schemamigration purchases --initial
    
    исправляем скрипт создания (комментим все строки с созданием таблиц)
    purchases/migration/0001_initial.py
    
    накатываем "псевдо" изменения
    ./manage.py migrate purchases
    
    теперь закоменченные строки можна убрать - для истории
  9. основной сценарий работы c South
    • делаем изменения в модели
    • создаем автоматическую точку миграции
      ./manage.py schemamigration purchases --auto
      
    • проверяем сгенеренный код в purchases/migration/XXXX_randomname.py
    • если что-то не так - правим код в методах backward и forward
    • в объекте orm находится замечательная django orm :)
      allpurchases = orm.Purchase.objects.all()
      
    • накатывание изменений на БД
      ./manage.py migrate purchases
    • посмотреть все миграции (звездочками помечены те которые успешно прошли)
      ./manage.py migrate purchases --list
    • если хочется смигрировать только данные
      ./manage.py datamigration purchases my_super_migration_of_data
    • или можно даже вот так
      ./manage.py schemamigration purchases --empty my_super_migration_of_data

Таги

  1. как уже упоминалось ранее таги были добавлены самопальные. Работают примерно так - в модели Purchase есть поле ManyToMany к модели Tag. Дефолтная реализация ManyToMany откидывается. В форму насильно добавляется новое CharField, переопределяются методы загрузки и сохранения формы, в которых как раз идут операции с полем m2m и CharField. Все делается в ручную. Это не никак не нравилось, так как чтобы переиспользовать эти таги пришлось бы прибегать к copy-paste, что не есть хорошо. В общем решил продолжить поиски компонент:
    • django-tagging - жутко наворочанный пакет, вроде бы есть все что нужно, все очень кастомно. но того как оно сделано я вряд ли скоро пойму. много-много черной магии для выполнения простых операций. много, очень много sql-я (без orm-а). Сразу же был откинут.
    • django-taggit проект поменьше чем предыдущий. но черная магия опять таки присутствует. в некоторых моментах я тупо не разобрался.
    • tagsfield от Ивана Сагалаева. Вот оно то что нужно. Идея проста - наследуемся от ManyToManyField и заменяем дефолтный widget на собственный. Кода ну очень мало, все предельно понятно. Выбрал я его. Конечно же понадобились изменения.
  2. первым делом копируем к себе tagsfield, добавляем его в installed_apps
  3. изначально проект работает так: в html записывается список всех тагов и список всех тагов объекта, через javascript все это управляется - юзер может добавлять и убирать теги обекта, может добавлять новые. Когда форма сабмитится список активных тагов сохраняется в поле m2m.
  4. для моего подхода (de.li.cious style таги) было сделано следующее
  5. поле MultipleHiddenInput было заменено на TextInput. Поясняю - при сабмите приходил список значений hidden-input-ов, а мне нужно только одно text-input поле.
    class TagsFormField(forms.Field):
        text_widget = forms.TextInput
  6. собственно загрузка списка тагов (метод render):
    'value_tags': " ".join(value_tags)
  7. из модели убрал поле created и переименовал модель в BaseTag (зачем - ниже)
    class BaseTag(models.Model):
        value = models.CharField(max_length=50)
        norm_value = models.CharField(max_length=50, editable=False)
  8. сохранение:
    def save_form_data(self, instance, data):
        splitted = data.strip().split(' ')
        tags = [self._get_tag(value) for value in splitted]
        setattr(instance, self.attname, tags)
  9. шаблон виджета:
    <div id="{{ id }}" class="tagsfield">
    <input type="text" name="{{ name }}" value="{{ value_tags }}" />
    </div>
  10. удалил больше не нужные файлы и зачистил js и css файлы. Еще были изменения, но все незначительные, так что лучше их смотреть в репозитории.
  11. теперь об одном ограничении оригинального кода - все таги хранятся в одной таблице. То есть если навешать таги на два класса - таги у них будут общие. Плохо. Для решения использовал еще один уровень абстракции, в приложении Purchases создал новый класс PurchaseTag который является наследником BaseTag. Теперь в бд будет две таблицы - одна для BaseTag из приложения TagField и одна для PurchaseTag из приложения Purchase. Таги для них не пересекаются :) Ну и собственно TagField одинаково хорошо работает для PurchaseTag класса. Тем более в последствии можно кастомизировать PurchaseTag и добавить к нему дополнительные поля. В файле purchases/models.py (старый класс Tag уже затерт)
    from homebudget.tagsfield import fields
    from homebudget.tagsfield.models import BaseTag
    class Purchase(models.Model):
        ... 
        tags = fields.TagsField('PurchaseTag')
    
    class PurchaseTag(BaseTag):
        pass
    
    class PurchaseItemForm(ModelForm):
        class Meta:
            model = Purchase
            exclude = ('purchase_date',)
    
  12. Ну и в коде везде где обращались к Tag теперь обращаемся к PurchaseTag, ну и из мелочи - поле name изменилось на value.
  13. Как проходила миграция данных (замена старых таго на новые). У меня база уже забита. Собственно для этого и прикручивался South (см. выше)
  14. Сначала была таблица для приложения TagField (чтобы не было никаких проблем)
  15. Потом было добавлена модель PurchaseTag в проект Purchases, в модель Purchase было добавлено поле ntags = fields.TagsField('PurchaseTag'), старое поле tags = models.ManyToManyField('Tag') не удаляя.
  16. Первая попытка миграции. Не работает (!!) и South отправляет на страницы http://south.aeracode.org/wiki/MyFieldsDontWork http://south.aeracode.org/docs/customfields.html#extending-introspection. Типа дописывайте сами себе правила на собственные поля. Ну ведь использовано поле отнаследованное от m2m, значит и вести оно должно себя также.. Значит спиздим определение ManyToManyField из кода south-а
    rules = [
    (
        (TagsField,),
        [],
        {
            "to": ["rel.to", {}],
            "symmetrical": ["rel.symmetrical", {"default": True}],
            "related_name": ["rel.related_name", {"default": None}],
            "db_table": ["db_table", {"default": None}],
            "through": ["rel.through", {"ignore_if_auto_through": True}],
        },
    ),
    ]
    from south.modelsinspector import add_introspection_rules
    add_introspection_rules(rules, ["^homebudget\.tagsfield\."])
    Поставим это прямо возле определения TagsField-а.
  17. Теперь создание точки миграции должно пройти успешно. Теперь у нас есть два поля тагов в модели. Делаем миграцию данных. Для каждого старого тега добавляем новый, и повторяем все соотношения. В общем выглядит это так:
    class Migration(DataMigration):
        def forwards(self, orm):
            plist = orm.Purchase.objects.all()
            for p in plist:
                old_tags = p.tags.all()
                for i in old_tags:
                    norm_value = i.name.lower()
                    tag, created = orm.PurchaseTag.objects.get_or_create(
                        norm_value = norm_value,
                        defaults = {'value': i.name}
                    )
                    p.ntags.add(tag)
                    p.tags.remove(i)
                p.save()
            old_tags = orm.Tag.objects.all()
            for i in old_tags:
                i.delete()
        def backwards(self, orm):
            "Write your backwards methods here."
            plist = orm.Purchase.objects.all()
            for p in plist:
                old_tags = p.ntags.all()
                for i in old_tags:
                    tag, created = orm.Tag.objects.get_or_create(
                        name = i.value,
                    )
                    p.tags.add(tag)
                    p.ntags.remove(i)
                p.save()
            old_tags = orm.PurchaseTag.objects.all()
            for i in old_tags:
                i.delete()
  18. Теперь удаляем модель Tag (старую) и поле tags. Апдейтим базу через South.
  19. Все хорошо, только поле называется ntags... Переименовываем поле. Смотрим в скрипт сгенеренный South-ом - не порядок, он пытается удалить нашу таблицу и создать новую. Исправляем его ошибку и накатываем изменения
    class Migration(SchemaMigration):
        def forwards(self, orm):
            db.rename_table('purchases_purchase_ntags', 'purchases_purchase_tags')
    
        def backwards(self, orm):
            db.rename_table('purchases_purchase_tags', 'purchases_purchase_ntags')
  20. Теперь все окей :)

Посты по теме:

6.14.2010

хроники django или homebudget part 4

Выдалось время и появилось настроение. Чего-то нового сваял :)
  1. Первой целью стояло добавление тагов. Очень полезная вещь для организации однотипных данных. Посмотрел что предлагает интернет на этот вопрос. Есть проект django-tagging, типа все готовое прямо из коробки под джангу. Не понравился по нескольким причинам - я не понял как он работает (какие-то мега-архи приемы используются для управления тагами, типа прямых запросов к бд), куча не нужных мне наворотов, находится он бете и имеет баг лист, да и народ от него не в восторге. Вообще вопрос построения тагов теоретически выглядит довольно простым - так что было решено отказаться и самому написать простенькое решение, для целей самообучения имхо лучше. Когда понадобится облако тагов - я подумаю в сторону перехода на сей стандартный плагин. Мое требование такое - чтобы было одно поле на форме, в котором записаны все таги итема через пробел, при сохранении это поле split-ится по пробелам, и к итему добавляются/убираются теги. Как узналось в процессе - это de.li.cious style таги.
  2. Отправной точкой является модель тага
    class Tag(models.Model):
        name = models.CharField('keyword, bookmark or term', max_length=100)
    
    и отношение many-to-many к модели purchase
    class Purchase(models.Model):
        ...
        tags = models.ManyToManyField('Tag')
    
  3. Обновляем схему базы. У нас появляется новая таблица Тагов, и таблица отношения таг-покупка (по foreign-ключам из тагов и покупок). Нужно заметить что таблица PurchaseItem не меняется, значит все правильно.
  4. Таким образом получилась следующая штука, которую лучше всего опробовать на shell-е:
    from homebudget.purchases.models import Purchase, Tag
    t = Tag(name='beer')
    t.save()
    p = Purchase.objects.filter(name=u'туборг')[0]
    p.tags.add(t)
    p.save()
    [i.name for i in p.tags.all()]
    [i.name for i in t.purchases_set.all()]
    
    Нужно заметить, что пока объект Purchase не материализован (не сохранен) в базе, у него нет поля tags. И наоборот, пока Tag не создан в базе у него нет поля purchases_set.
  5. Если сейчас оставить все как есть и посмотреть что находится на вьюхах добавления и редактирования, то может стать плохо. Так как я использовал ModelForm для генерации html-формы для Purchase - для всех полей подставилась стандартная реализация. И для поля ManyToManyField она тоже есть, и выглядит прям отвратительно (тупой селект). Минусы - совершенно не подходит под мое представление, и добавить новый тег - нужна отдельная форма. Так что нах стандарты!
  6. В PurchaseItemForm добавляем новое собственное поле, и удаляем из представление оригинальное поле tag модели PurchaseItem
    from django.forms import CharField
    class PurchaseItemForm(ModelForm):
        ...
        tags_inner = CharField()
        ...
        class Meta:
            model = Purchase
            exclude = ('tags',)
    
  7. Правильное поле есть, но оно никак не обрабатывается. Сначала мы его заполним данными. Как мне кажется - лучше всего это сделать через override функции __init__ формы. Сначала вызываем родной __init__ (в котором как мы сделали - уже нет оригинального поля tags), потом заполняем собственное поле tags_inner тагами, разделенными пробелами. Проверка на null-ы нужна как раз для создания и редактирования (пока объект Purchase не создан, у него нет поля tags).
    class PurchaseItemForm(ModelForm):
        ...
        def __init__(self, *args, **kwargs):
            super(PurchaseItemForm, self).__init__(*args, **kwargs)
            if kwargs.has_key('instance'):
                inst = kwargs['instance']
                if inst is not None and inst.pk is not None:
                    self.fields["tags_inner"].initial = " ".join([i.name for i in inst.tags.all()])
    
  8. Теперь сохранение. Я выбрал решение через override функции save (которая вызывается при сохранении данных формы из вьюхи). Сначала - обязательно вызываем базовый метод (чтобы объект Purchase наверняка сохранился в базе), потом - вытаскиваем из cleaned_data значения поля tags_inner. Далее понятные операции - split по пробелам, получаем все таги буквенно, потом создаем новые, удаляем несуществующие, и сохраняем финально.
    def save(self, *args, **kwargs):
            inst = super(PurchaseItemForm, self).save(*args, **kwargs)
            tags = self.cleaned_data["tags_inner"].strip().split(' ')
            for tag in tags:
                if len(Tag.objects.filter(name=tag))==0:
                    new_tag = Tag(name=tag)
                    new_tag.save()
                if len(inst.tags.filter(name=tag))==0:
                    inst.tags.add(Tag.objects.get(name=tag))
            for tag in inst.tags.all():
                if tag.name not in tags:
                    inst.tags.remove(tag)
            inst.save()
    
  9. Ну вроде бы с реализацией все - можно пробовать. Какие минусы сейчас есть: отсутствует валидация, отсутствует зачистка тагов к которым не привязан не один итем, поле tags теперь обязательное для заполнения, решение деревянное и не гибкое. Запишу в todo :) А пока нужно наполнить тагам базу.
  10. После того как база заполнена можно сделать вьюху для просмотра статистики по тагам. Во views.py добавляем purchases_by_tag
    def purchases_by_tag(request, tag_name):
        try:
            tag = Tag.objects.get(name=tag_name)
        except:
            raise Http404
        total = Decimal('0.0')
        for item in tag.purchase_set.all():
            total += item.price_total
        return render_to_response('purchases/tag_view.html', {
            'total': total,
            'tag': tag,
        })
    
    Добавляем новый темплейт tag_view.html
    <p>Tag: {{tag.name}}</p>
    <ul>
        {% for item in tag.purchase_set.all %}
            <li>
                {{item.name}}
                <b>{{item.price_total}}</b>
                {{item.purchase_date}}
                <a href='/purchases/edit/{{item.pk}}/'>Edit</a>
            </li>
        {% endfor %}
    </ul>
    <p>Count: {{tag.purchase_set.count}}</p>
    <p>Total: {{total}}</p>
    
    Добавляем новое правило урлов в urls.py:
    urlpatterns = patterns(
        ...
        (r'^purchases/tags/(?P\w+)/$', 'purchases_by_tag'),
    )
    
    Теперь по ссылке http://127.0.0.1:8000/purchases/tags/beer/ можно узнать когда, и на какую в сумму мы выпили пива :)
  11. Можно навести красивости, типа в daily view добавить в каждую строчку таги связанные с покупкой, типа
    Tags:
       {% for tag in p.tags.all %}
       <span>{{tag.name}}</span>
       {% endfor %}
    
  12. Для реализации тагов мне в один момент понадобился дебаг, использовал для этого модуль logging, очень удобный надо сказать, вот так его заставить работать (output идет в консоль локального сервера, что архи удобно):
    import logging
    logging.basicConfig(
        level = logging.DEBUG,
        format = '%(asctime)s %(levelname)s %(message)s',
    )
    
    а вот так легко использовать:
    logging.debug(p.tags)
    
  13. Еще по мелочам, из области рефакторинга, наткнулся на мега функцию locals - она возвращает все локальные переменные из области видимости, в виде словаря 'имя переменной'-'значение'. Это позволило значительно сократить код на render_to_response, до:
    return render_to_response('purchases/month_view.html', {
        'view_date': view_date,
        'dict': day_price,
        'total': month_total,
    })
    
    после:
    return render_to_response('purchases/month_view.html', locals())
    
    И это в каждой вьюхе, очень удобно. Конечно же пришлось поправить все темплейты и вьюхи на предмет именования переменных, но так все таки намного лучше.
  14. На последок добавил Master Page - по крайней мере так оно обзывается в ASP.NET. Нужно это для двух вещей - чтобы все страницы имели общую часть - в мастере, и чтобы меньше писать кода. После коротких поисков нашел что это в джанге реализуется через наследование темплейтов читать тут. В общем по простому: добавил master.html
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
        <link rel="stylesheet" href="content/css/main.css" />
        <title>Homebudget site: {% block title %} manage yourself {% endblock %}</title>
    {% block head %}{% endblock %}
    </head>
    
    <body>
    
    <div id="content">
    {% block content %}
    {% endblock %}
    </div>
    
    </body>
    </html>
    
    Теперь он будет у меня основным шаблоном. Любой другой шаблон может его расширять и дополнять. Все элементы block - можно использовать в дочернем шаблоне. Пока я полностью не разобрался, но то что есть сейчас меня устраивает. Вот например новый шаблон tag_view.html
    {% extends "purchases/master.html" %}
    
    {% block title %} view purchases by tag {% endblock%}
    
    {% block content %}
    <p>Tag: {{tag.name}}</p>
    <ul>
        {% for item in tag.purchase_set.all %}
            <li>
                {{item.name}}
                <b>{{item.price_total}}</b>
                {{item.purchase_date}}
                <a href='/purchases/edit/{{item.pk}}/'>Edit</a>
            </li>
        {% endfor %}
    </ul>
    <p>Count: {{tag.purchase_set.count}}</p>
    <p>Total: {{total}}</p>
    {% endblock %}
    
  15. Кстати говоря, в мастере используется ссылка на css-документ, он должен лежать в папке обозначенной в конфигурации как MEDIA_ROOT, и для этого пути должно быть правило в урл-реврайтере... Для чего оно нужно? чтобы в можно было вынести весь статический контент в отдельный сайт, необремененный обработчиками джанги и тп. В settings.py:
    MEDIA_ROOT = '/home/username/projects/homebudget/content/'
    ...
    MEDIA_URL = '/content/'
    
    И в urls.py
    from django.conf import settings
    if settings.LOCAL_DEVELOPMENT:
        urlpatterns += patterns("django.views",
            url(r"%s(?P.*)/$" % settings.MEDIA_URL[1:], "static.serve", { "document_root": settings.MEDIA_ROOT, })
        )
    
    Переменная LOCAL_DEVELOPMENT заведена на будующее.

Посты по теме:

5.16.2010

хроники django или homebudget part 3

Этот мини проект обзавелся public репозиторием на google code. Выбрал я его из-за встроенного баг-трека и вики. По большому счету лучше подошел бы launchpad, но с гуглокодом я уже работал и более-менее там освоился. Из минусов - репозиторий svn, а у меня локально все ведется в bzr. Ну это не беда, пройденный этап ;)

Из последних приготовлений кода к первому коммиту - были убраны все пароли и пути из settings.py, убирал примерно следующим образом

  1. Создал файл в домашней директории .homebudget-settings со следующим содержанием:
    DATABASE_NAME = 'homebudget'
    DATABASE_USER = 'username'
    DATABASE_PASSWORD = 'password'
    DATABASE_HOST = 'localhost'
    DATABASE_PORT = '1234'
    
    MEDIA_ROOT = '/home/username/projects/homebudget/content/'
    
    SECRET_KEY = '-thisismysecretkey!'
    TEMPLATE_DIRS = (
        '/home/username/projects/homebudget/templates',
    )
    LOCAL_DEVELOPMENT = True
    
  2. В файле settings.py потер значения одноименных переменных, примерно так
    DATABASE_NAME = ''
    DATABASE_USER = ''
    DATABASE_PASSWORD = ''
    DATABASE_HOST = ''
    DATABASE_PORT = ''
    
  3. В конец файла settings.py была добавлены следующие строчки:
    from os.path import expanduser
    execfile(expanduser('~/.homebudget-settings'))
    
  4. Проект готов к выкладыванию на публику :)

Посты по теме:

5.10.2010

хроники django или homebudget part 2

Итак, как обстоят дела: после первого рывка я скурпулезено заполнял базу через существующие странички. Иногда когда скрипели зубы - добавлял функциональность. А так могу сказать - сейчас меня более-менее устраивает, все работает как и должно, есть куда развивать систему, следующие шаги ясны. Собственно, вот продолжение хроник, информация восстановлена по change-ам. Все измения фискируются в source-control'е в качестве которого я успешно использую bazaar.

  1. Добавляем просмотр отдельного дня. То есть это выборка из базы по конкретному дню, ссылки на редактирование отдельных записей (покупок), собственно ценники, и сумма за день. Как - добавляем новый метод в контроллер, добавляем отдельную вьюху для html-я, добавляем правило для урлов. all right, let's get started
  2. Чтобы было понятно - начинаем с правила для урлов. хотим чтобы запрос для странички дня выглядел типа blah-blah-blah/view/day/2010/04/30 ну типа никаких идиотских GET параметров. В urls.py добавляем следующее правило:
    (r'^purchases/view/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', 'day_view')
    
  3. Новый метод контроллера (в файле purchases/views.py), логика такая - добавленное в предыдущем пункте правило добавит необходимые параметры день/месяц/год, мы из них делаем нормальный объект datetime (если не получается - типа 68 января - то 404), потом забираем из базы все записи Purchase с этой датой, берем сумму по ценам и выводим все во вьюху.
    def day_view(request, year, month, day):
        try:
            dt = datetime.date(year=int(year), month=int(month), day=int(day))
        except:
            raise Http404
        plist = Purchase.objects.filter(purchase_date = dt)
        price_sum = Decimal('0.0')
        for it in plist:
            price_sum += it.price_total
    
        return render_to_response('purchases/day_view.html', {
            'date': dt,
            'list': plist,
            'price_sum': price_sum,
        })
    
  4. Создаем новый темплейт вьюхи templates/purchases/day_view.html с примерно следующим содержимым. Логика такая - пробегаем по списку значений как на главной странице, выводим ссылки на изменение и тд, выводим отдельно заранее посчитанный тотал.
    <p>Date: {{date}} </p>
    {% if list %}
        <ul>
            {% for p in list %}
                <li>
                    {{p.name}} 
                    <b>{{p.price_total}}</b> 
                    <a href='/purchases/edit/{{p.pk}}/'>Edit</a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No purchases yet</p>
    {% endif %}
    <p>
        Total: <b>{{price_sum}}</b>
    </p>
    
    <a href='/purchases/add/'>Add</a>
    
  5. Проверяем на работоспособность. Должно работать :)
  6. Теперь делаем тоже самое но для месяца. Тупо по аналогии.
  7. Добавляем аналогично просмотру дня правило для просмотра месяца (доступ по ссылке типа blah-blah-blah/view/day/2010/04/):
    (r'^purchases/view/(?P<year>\d{4})/(?P<month>\d{1,2})/$', 'month_view')
    
  8. В методе контроллера имеем сразу же несколько проблем. Первая - нужно сделать запрос на совпадение и года, и месяца одновременно (в случае с просмотром дня - все было просто - тупо совпадение даты). Лечим следующим методом:
    from django.db.models import Q
    plist = Purchase.objects.filter(Q(purchase_date__year=dt.year) & Q(purchase_date__month=dt.month))
    
  9. Вторая проблема - нет в стандартной библиотеке пайтона функций типа добавить месяц. Можно есть - добавления отрезка времени, но так как в месяцах непостоянное кол-во дней такой способ выглядит очень корявым. Какой по-проще способ нашел - по дате составить новую с месяцем+1.
    # dt - дата
    y, m, d = dt.timetuple()[:3]
    dt_next = datetime.date(year=y+((m+1)/12), month=(m+1)%12, day=d)
    
  10. Третья проблема - как передать данные во вьюху. Я выбрал способ через словарь (dictionary) - в котором date => purchase_item.
  11. Вот получившийся код. Логика такая - получаем год/месяц - строим дату как первое число месяца. Выбираем из базы все записи purchase за этот месяц. Потом прибавляя по дню пока не прийдем к первому числу следующего месяца, попутно собирая суммы за каждый день и сумму за весь месяц.
    def month_view(request, year, month):
        try:
            dt = datetime.date(year=int(year), month=int(month), day=1)
        except:
            raise Http404
        from django.db.models import Q
        plist = Purchase.objects.filter(Q(purchase_date__year=dt.year) & Q(purchase_date__month=dt.month)).order_by('purchase_date')
    
        y, m, d = dt.timetuple()[:3]
        dt_next = datetime.date(year=y+((m+1)/12), month=(m+1)%12, day=d)
        day_price = {}
        month_total = Decimal('0.0')
        while (dt < dt_next):
            purchases_day = plist.filter(purchase_date__day=dt.day)
            day_total = Decimal('0.0')
            for it in purchases_day:
                day_total += it.price_total
            month_total += day_total
            day_price[dt] = day_total
            dt += datetime.timedelta(days=1)
    
        return render_to_response('purchases/month_view.html', {
                'date': datetime.date(year=int(year),month=int(month),day=1),
                'dict': day_price,
                'total': month_total,
            })
    
  12. Создаем новый темплейт templates/purchases/month_view.html
    <p>Date: {{date}} </p>
    {% if dict %}
        <ul>
            {% for key,value in dict.items %}
                <li>
                    {{key}} 
                    <b>{{value}}</b> 
                    <a href='/purchases/view/{{key.year}}/{{key.month}}/{{key.day}}/'>Edit</a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No purchases yet</p>
    {% endif %}
    <p>
        Total: <b>{{total}}</b>
    </p>
    
    <a href='/purchases/add/'>Add</a>
    
  13. Проверяем. Опа, не работает - все дни вперемешку, хотя и выставлено для запроса order_by. А тут начинаются темные, не хорошо документированные особенности джанги по сортировке словаря (я об этот момент чуть голову не сломал). Есть миллион решений - типа сделать буферный список из отсортированных ключей и его передавать с моделью, и тп. Но вот чтобы применить к for-у для словаря сортировку прямо в темплейте - нашел только один:
    for key,value in dict.items|dictsort:"0"
    
  14. Сделаем так чтобы после Edit-а или Add-а происходил редирект на просмотр использованного дня.
  15. Метод Add-а:
    def purchase_add(request):
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST)
            if form.is_valid():
                form.save()
                purchase_date = form.cleaned_data["purchase_date"]
                return HttpResponseRedirect('/purchases/view/%s/%s/%s/' % purchase_date.timetuple()[:3]) 
        else:
            form = PurchaseItemForm()
    
        return render_to_response('purchases/purchase_add.html', {
            'form': form
        })
    
  16. Метод edit-а:
    def purchase_edit(request, purchase_id):
        try:
            item = Purchase.objects.get(pk=purchase_id)
        except Purchase.DoesNotExist:
            raise Http404
    
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST, instance=item)
            form.save()
            purchase_date = form.cleaned_data["purchase_date"]
            return HttpResponseRedirect('/purchases/view/%s/%s/%s/' % purchase_date.timetuple()[:3])
        
        form = PurchaseItemForm(instance=item)
        return render_to_response('purchases/purchase_edit.html', {
            'form': form,
            'id' : purchase_id
        })
    
  17. Делаем добавление покупки на конкретный день (а не только на сегодня) - те если хотим добавить покупку за вчера со страницы вчерашнего дня - то поле дата должно быть корректно подставлено на вчера. Другими словами blah-blah-blah/purchases/add/ - должно добавлять покупку на сегодня, а blah-blah-blah/purchases/add/2010/04/30 - должно подставлять 2010.04.30
  18. В urls.py добавляем следующий словарь для определения сегодня:
    import datetime
    year, month, day = datetime.datetime.today().timetuple()[:3]
    today_dict = {'year':year, 'month':month, 'day':day}
    
    И следующее правила взамен старого add-а:
        (r'^purchases/add/$', 'purchase_add', today_dict),
        (r'^purchases/add/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', 'purchase_add'),
    
  19. Метод add должен теперь выглядеть так:
    def purchase_add(request, year, month, day):
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST)
            if form.is_valid():
                form.save()
                purchase_date = form.cleaned_data["purchase_date"]
                return HttpResponseRedirect('/purchases/view/%s/%s/%s/' % purchase_date.timetuple()[:3]) 
        else:
            try:
                dt = datetime.date(year=int(year), month=int(month), day=int(day))
            except:
                dt = datetime.today() 
            purchaseitem = Purchase()
            purchaseitem.purchase_date = dt
            form = PurchaseItemForm(instance=purchaseitem)
    
        return render_to_response('purchases/purchase_add.html', {
            'form': form
        })
    
  20. Ну и собственно линка внутри templates/purchases/day_view.html
    <a href='/purchases/add/{{date.year}}/{{date.month}}/{{date.day}}/'>Add</a>
    

Посты по теме:

4.04.2010

хроники django или homebudget part 1

В общем решил вплотную изучить django фреймворк. Есть опыт работы с asp.net mvc фреймворком, есть знания питона. Почему бы и нет?) Как изучить новую технологию максимально эффективно? считаю что нужно сразу же делать какое-нить реальное приложение. Типа учебная тревога.

Здесь буду публиковать хроники моих изысканий, не хочется чтобы они пропали зря. Не в коем случае не претендую на самое оптимальное решение и описание. Вообще происходящее будет много дублировать офф tutorial и примеры из документации с http://docs.djangoproject.com/, в этом нет ничего странного - я по этому и учусь ;)

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

  1. инсталируем django (надеюсь питон уже есть)
    sudo apt-get install python-django
    
  2. инсталируем psycopg2 модуль к питону для работы с бд
    sudo apt-get install python-psycopg2
    
  3. инсталируем postgre
    sudo apt-get install postgresql
    
  4. проверям что все установилось и демон запусчен:
    ps -A | grep postgres
    
  5. запускаем консоль постгреса
    sudo -u postgres psql postgres
    
  6. меняем пользователю postgres пароль
    \password postgres
    
  7. выходим из консоли
    \q
    
  8. создаем базу
    sudo -u postgres createdb homebudget
    
  9. создаем новый django проект
    django-admin.py startproject homebudget
    
  10. проверям что все работает
    cd homebudget
    ./manage.py runserver
    
  11. в браузере переходим на линку
    http://127.0.0.1:8000/
    
  12. редактируем settings.py
    DATABASE_ENGINE = 'postgresql_psycopg2'
    DATABASE_NAME = 'homebudget'
    DATABASE_USER = 'postgres'
    DATABASE_PASSWORD = 'наш пароль'
    DATABASE_HOST = 'localhost'
    DATABASE_PORT = '5432'
    
  13. запускаем синхронизацию проекта с базой (нужно чтобы работали installed_applications из настроек проекта)
    ./manage.py syncdb
    
  14. на вопрос You just installed Django's auth system, which means you don't have any superusers defined. отвечаем да и вводим данные
  15. стартуем новое приложение в проекте
    ./manage.py startapp purchases
    
  16. редактируем models.py
    from django.db import models
     
    class Purchase(models.Model):
        name = models.CharField('identifier of item', max_length=200)
        quantity = models.FloatField('quantity of purchase')
        price_for_one = models.DecimalField('price for one item', max_digits=10, decimal_places=2)
        price_total = models.DecimalField('price for all', max_digits=10, decimal_places=2)
    
  17. добавляем приложение в installed_apps, в файле settings.py добавляем в список
    'homebudget.purchases'
    
  18. синхронизируем базу с моделями
    ./manage.py syncdb
    
  19. забиваем базу тестовыми данными (можно через pgadmin), заходим в консоль джанги
    ./manage.py shell
    
  20. там вбиваем
    from homebudget.purchases.models import Purchase
    p = Purchase(name='beer',quantity=3, price_for_one=48, price_total=144)
    p.save()
    quit()
    
  21. добавляем новое правило для url rewriter-а в файле urls.py
    (r'^purchases/$', 'homebudget.purchases.views.index')
    
  22. создаем новое view, редактируем файл purchases/views.py
    from django.template import Context, loader
    from homebudget.purchases.models import Purchase
    from django.http import HttpResponse
    
    def index(request):
        all_list = Purchase.objects.all()
        t = loader.get_template('purchases/index.html')
        c = Context({
            'all_list': all_list,
        })
        return HttpResponse(t.render(c))
    
  23. создадим папку templates в каталоге проекта, и подпапку purchases
    mkdir templates
    mkdir templates/purchases
    
  24. добавляем папку templates в проектные шаблоны, правим файл settings.py (путь абсолютный!)
    TEMPLATE_DIRS = (
        '/home/username/projects/homebudget/templates',
    )
    
  25. создадим файл templates/purchasesindex.html
    {% if all_list %}
        <ul>
            {% for p in all_list %}
                <li>{{p.name}} <b>{{p.price_total}}</b></li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No purchases yet</p>
    {% endif %}
    
  26. запускаем сервер, в браузере переходим на страницу. ништяк? ;)
    http://127.0.0.1:8000/purchases/
    
  27. добавляем новое поле к модели (purchases/models.py)
        purchase_date = models.DateField('date of buying')
    
  28. так как джанга не поддерживает миграцию бд, делаем руками
    ./manage.py sql purchases
    
  29. получаем sql код, лезем в postgre и правим в нем таблицу
    ALTER TABLE purchases_purchase ADD COLUMN purchase_date date;
    UPDATE purchases_purchase SET purchase_date=now();
    ALTER TABLE purchases_purchase ALTER COLUMN purchase_date SET NOT NULL;
    
  30. создаем add форму
  31. в файл models.py добавляем следующий класс
    from django import forms
    
    class PurchaseItemForm(forms.Form):
        name = forms.CharField(max_length=200)
        quantity = forms.FloatField()
        price_for_one = forms.DecimalField(decimal_places=2)
        price_total = forms.DecimalField(max_digits=10, decimal_places=2)
        purchase_date = forms.DateField()
    
  32. в файл views.py добавляем новую вьюхую
    from homebudget.purchases.models import PurchaseItemForm
    def purchase_add(request):
        form = PurchaseItemForm()
        t = loader.get_template('purchases/purchase_add.html')
        c = Context({
            'form': form
        })
        return HttpResponse(t.render(c))
    
  33. создаем новый файл templates/purchases/purchase_add.html
    <form action="/purchases/add/" method="post">
    {{ form.as_p }}
    <input type="submit" value="Save" />
    </form>
    
  34. в файл urls.py прописываем новый маппинг паттерн
    (r'^purchases/add/$', 'homebudget.purchases.views.purchase_add'),
    
  35. запускам дев сервер, в браузере переходим на страницу
    http://127.0.0.1:8000/purchases/add/
    
  36. теперь добавляем сохранение данных, это будет происходить на той же самой странице по post запросу
  37. в файл views.py добавляем обработку post запроса и сохранение
    from django.http import HttpResponse, HttpResponseRedirect
    def purchase_add(request):
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST)
            if form.is_valid():
                name = form.cleaned_data['name']
                quantity = form.cleaned_data['quantity']
                price_for_one = form.cleaned_data['price_for_one']
                price_total = form.cleaned_data['price_total']
                purchase_date = form.cleaned_data['purchase_date']
                item = Purchase(name=name, quantity=quantity, price_for_one=price_for_one, price_total=price_total, purchase_date=purchase_date)
                item.save()
                return HttpResponseRedirect('/purchases/') 
        else:
            form = PurchaseItemForm()
        t = loader.get_template('purchases/purchase_add.html')
        c = Context({
            'form': form
        })
        return HttpResponse(t.render(c))
    
  38. проверям в браузере, в случае удачного сохранения будет переход на список всех
  39. займемся совершенствованием
  40. в файл views.py добавляем
    from django.shortcuts import render_to_response
    
  41. ужасные loader.get_template, Context, return HttpResponse(..) заменяем на простую конструкцию
        return render_to_response('purchases/purchase_add.html', {
            'form': form
        })
    
  42. в файле models.py вставляем ModelForm взамен идиотского дублирующего кода из модели
    class PurchaseItemForm(forms.ModelForm):
        class Meta:
            model = Purchase
    
  43. да и после этого во вьюхе не нужно перечислять все поля.. теперь будет так:
    def purchase_add(request):
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST)
            if form.is_valid():
                form.save()
                return HttpResponseRedirect('/purchases/') 
        else:
            form = PurchaseItemForm()
    
        return render_to_response('purchases/purchase_add.html', {
            'form': form
        })
    
  44. в файле urls.py тоже изменения:
    urlpatterns = patterns(
        'homebudget.purchases.views',
        (r'^purchases/$', 'index'),
        (r'^purchases/add/$', 'purchase_add'),
    )
    
  45. запускаем дев сервер и все перепроверяем, что работает как до рефакторинга
  46. добавляем edit для элемента, по быстрому
  47. в файл views.py
    def purchase_edit(request, purchase_id):
        try:
            item = Purchase.objects.get(pk=purchase_id)
        except Purchase.DoesNotExist:
            raise Http404
    
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST, instance=item)
            form.save()
            return HttpResponseRedirect('/purchases/')
    
        form = PurchaseItemForm(instance=item)
        return render_to_response('purchases/purchase_edit.html', {
            'form': form,
            'id' : purchase_id
        })
    
  48. создадим файл templates/purchases/purchase_edit.html
    <form action="/purchases/edit/{{ id }}/" method="post">
    {{ form.as_p }}
    <input type="submit" value="Save" />
    </form>
    
  49. в файл index.html добавим линки для редактирования и добавления нового элемента:
    {% if all_list %}
        <ul>
            {% for p in all_list %}
                <li>
                    {{p.name}}
                    <b>{{p.price_total}}</b>
                    {{p.purchase_date}}
                    <a href='/purchases/edit/{{p.pk}}/'>Edit</a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No purchases yet</p>
    {% endif %}
    
    <a href='/purchases/add/'>Add</a>
    
  50. в файл urls.py
        (r'^purchases/edit/(?P<purchase_id>\d+)/$', 'purchase_edit'),
    
  51. запускаем дев сервер и наслаждаемся

сейчас инструментария хватает чтобы забивать и просматривать базу, но это все очень не удобно, так что продолжение следует

Посты по теме:

2.27.2010

This is a debug hell routine... 500 page views per working day by one developer - it's possible! P.S. fucking IE6

1.17.2010

bzr+svn friends?

Уже говорил про мою проблему с bzr-svn плагином. Так вот после умозаключений и тестов, я пришел к оптимальному для меня решению - удалить bzr-svn плагин, пользоваться bzr и svn поотдельности. В существующем checkout от svn я проинициализировал bazaar shared репозиторий (bzr init-repo) прямо в папке svn-резозитория. В svn-е я поставил игнор файлов .bzr, а в Bazaar поставил игнор файлов .svn и почти усе. Потом добавил в bazaar все файлы из svn-овской папки (bzr add). Все локальные изменения теперь я бренчую через bzr, туда же постоянно комитаю свои собственные маленькие изменения. Когда фитча локально оттестирована, я сливаю ее в основной bazaar репозиторий. И только потом идет коммит в обычный svn-репозиторий. С апдейтами из svn, благодаря тому что базарный репозиторий mirror - проблем не возникает. Да, я понимаю что комманд стало чуток больше, но прелесть все равно есть. Мое локальное счастье наступило.

1.13.2010

bzr+svn

Очень хотел использовать Bazaar для работы с svn, но увы и ах bzr-svn needs to access the root to e.g. find out what the repository layout is что не позволяет использовать его на закрытых репозиториях. Практически выглядит так bzr checkout svn+https://svn.myserver.com/myproject/src src bzr: ERROR: A Subversion remote access command failed: Server sent unexpected return value (403 Forbidden) in response to PROPFIND request for '/' Ну вот только если удастся получить доступ к корню репозитория... или перестать пользоваться такими svn репозиториями. Пока смотрю в сторону Git, там нет такой проблемы.