пятница, 21 декабря 2007 г.

Ленивый сбор статистики и не только

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

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


# Wrappers for lazy database access

class LazySingleton:
_singelton = None
def __init__(self, singelton_class):
self._singelton_class = singelton_class
def __getattr__(self, name):
if not self._singelton:
self._singelton = self._singelton_class.get_instance()
return getattr(self._singelton, name)

class LazyFirst:
_obj = None
def __init__(self, query_set):
self._query_set = query_set
def __getattr__(self, name):
if not self._obj:
self._obj = self._query_set[0]
return getattr(self._obj, name)

class LazyCount:
def __init__(self, query_set):
self._query_set = query_set
def __int__(self):
if not hasattr(self, '_count'):
self._count = self._query_set.count()
return self._count
def __str__(self):
return str(self.__int__())
def __unicode__(self):
return unicode(self.__str__())


LazySingleton - это ленивый запрос на получение экземляра, класс которого определяет метод get_instance. Я использую его для получения объекта настроек (Preferences), таблица содержит тольку одну запись. И в большинстве случаев этот объект будет необходим только на страницах этого приложения. И возможность делать показанный ниже запрос из шаблонов, не задумыватясь о передаче нужного объекта представляется довольно удобной.


{{ pref.forum.posts_on_page }}


LazyFirst и LazyCount вытаскивают соответсвенно первый объект и число объектов из переданных в качестве параметров QuerySet'ов.

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

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

Я это использую при сборе статистики из приложений, которые возвращают словарь функцией get_statistics.

Сбор общей статистики в ядре cms:


def get_statistics():
stat = {}
stat['user_number'] = LazyCount(User.objects)
stat['last_user'] = LazyFirst(User.objects.order_by('-date_joined'))
return stat


Сбор статистики форума:


def get_statistics():
stat = {}
stat['post_number'] = LazyCount(Post.objects)
stat['topic_number'] = LazyCount(Topic.objects)
stat['forum_number'] = LazyCount(Forum.objects)
return stat


При большом кол-ве данных, которые могут и не потребоваться, этот подход может серьёзно сократить нагрузку на СУБД. Хотя это ещё нужно протестировать, ведь создание объектов обёрток тоже не совсем бесплатно.

четверг, 13 декабря 2007 г.

Ещё один форум на Django или на перегонки с самим собой

Много людей пишут в своих блогах о Django. Что я рыжый? К тому же, есть что сказать. ;)

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

Однако это был не первый мой проект на Django. http://bulldozer-nk.com - был сделан ещё год назад (после чего я надолго забросил Django), в процессе первого знакомства с этим фреймворком, и он врядли страдает от идеальности дизайна и реализации.

Времени был месяц, почему объяснять не буду. =) Тут я сам себе злобный буратина.

Форумы на Django пытаются писать все кому не лень. Ведь видимая простота и элегантность реализации завораживает. Но тем не менее реально опробованного и проверенного временем решения до сих пор не видно.

Ничего особенно от форума не требовалось, всё должно было быть просто и без излишеств.

Главное было желаение проверить, а реально ли в столь короткие сроки разработать весь этот функционал: форум, галерея, новости, каталог ссылок и др. И не просто написать, а и развернуть в боевых условиях.

Дык вот: http://skatehouse.ru.

Был соствлен список требований. Впрочем вполне стандартный для подобных приложений. И понеслась.

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


cmscore <-
forum
gallery
links
....


В смысле зависимости слоёв, а не структуры пакетов.

Каждое приложение имеет свои настройки, которые храняться в единственной записи модели myapp.Preferences, специфичный для приложения профиль пользователя (который так и не был заюзан), и набор действий (о действиях будет сказано ниже), который можно производить над ним и его моделями и др.

Базывый функционал, основные атрибуты для профилей пользоватей и множество утилит ушло в cmscore. Опредилился интерфейс модулей. Простой до опупения, в __init__.py джанговского аппа прописывается следующее:


get_preferences() -> экземпляр Preferences
get_statisticis() -> словарь со статистическими данными


и


actions = ...


Как бы то ни было, к этому можно добавлять что угодно. А cmscore всё это собирает и показывает где нужно. Вообще приложение не обязано это определять, оно будет проигнорировано если будет ImportError, т.е. можно подключать стандатные джанговские аппы.

Большинство форм, по крайней мере на начальной стадии, было создано так:


{% include "generic_form.html" %}


где generic_form.html


{% if form.is_multipart %}
<form enctype="multipart/form-data" method="post" action=".">
{% else %}
<form method="post" action=".">
{% endif %}

<table class="form">
{{ form }}
</table>

<div class="buttons">
<input type="submit" value="{% trans "Save" %}">
</div>

</form>


Через процессор контекста в шаблоны передаётся список аппов, чтобы забрать у них экшены, и преференсы. Хотя преференсы также добавляются в словарь pref из контекст процессоров, как и статистика и даже ссылка модуль settings (в основном чтобы иметь доступ к MEDIA_URL).


'cmscore.context_processors.apps',
'cmscore.context_processors.preferences',
'cmscore.context_processors.statistics',
'cmscore.context_processors.settings'


И доступ к ним такой.


{{ stat.cmscore.last_user }}
{{ stat.forum.post_number }}


или


{{ pref.gallery.images_on_page }}


Портлеты. Терминологически появились под влиянием Plone. :) В шаблоны предаются тоже процессорами контекста.


'cmscore.context_processors.portlet_navigator',
'cmscore.context_processors.portlet_login',
'cmscore.context_processors.portlet_counter'


... и используеются так.


{{ portlet_cmscore_login }}


Хотя нужно их наверно собирать в список portlets приложения. ;) И передавать в шаблон именно этот список.

Примитивный счётчик посещений был реализован через middleware, который выдёргивает из базы объект UrlCounter по pk, который равен request.path или создаёт его. Ну и считает отдельно для всего, отдельно для месяца, отдельно для дня и сбрасывает эти отдельные счётчики отдельно.

Действия. Вообще отдельная концепция, хорошая идея и плохая реализация. ;(

Действия для аппа:


actions = (Action(_('Add a group'),
'/forum/group_add/',
'forum.add_group'),
Action(_('Preferences'),
'/forum/preferences/',
'forum.change_preferences'))


... или для модели:


actions = (Action(_('Add a forum'), '/forum/groups/%s/add_forum/', 'forum.add_forum'),
Action(_('Change'), '/forum/groups/%s/change/', 'forum.change_group'),
ActionWithPrompt(_('Delete'), '/forum/groups/%s/delete/', 'forum.delete_group', None,
_('Do you realy want to delete the group?')),
Action(_('Up'), '/forum/groups/%s/up/', 'forum.change_forum'),
Action(_('Down'), '/forum/groups/%s/down/', 'forum.change_forum'),
)


Различия в том, что в URL действий для модели предаётя параметр id объекта. Да, на данный момент поддерживаются урлы только с одной переменной (первый крестик к неудачной реализации), что в моём проекте не было проблемой. Остальное заголовок, условия появления (пермишены и/или функции, принимающии объект пользователя) и всё. ActionWithPrompt через JavaScript показывает диалог перед тем как выполнить переход по нужному урлу, удобно для запроса подтверждения удаления.

Рендерится это всё так, специальным тегом (второй крестик к неудачной реализации).


{% actions apps.forum %}


или


{% actions image %}
{% actions post %}


Потом до меня допёрло, что лучше сделать объект коллектор экшенов, типа ActionGroup и пусть он сам себя рендерит, это более универсально и позволит даже определять контексты, т.е. где и как действия должны рисоваться, можно исключать некоторые действия из некоторых контекстов, где они не имёют смысла, например. Об этом позже, в другой заметке. ;)

Итог. Работа была начата 10-го ноября, а 3-го декабря всё уже работало и радовалo заказчика. Благо он был не особо придирчив к дизайну, т.к. всё было выполнено мной в одиночку, и будь я даже хорошим дизайнером, у меня бы просто не было времени заниматься оформлением. Вообще фреймворк Django оправдал мои надежды и был успешно применён для разработки нескольких веб-приложений (не особо сложных и по минимому, но всё-таки) в весьма сжатые сроки.

Тестирование проводилось только на уровне юзкейсов, методом тыка, т.е. никакого автоматического тестирования не применялось.

После этого было выявлено несколько багов, критичных и не очень, которые были быстро исправлены, ну а пока всё пучком, 10 дней полёт нормальный. :)

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