среда, 28 декабря 2011 г.

Django: "Кто на сайте" ?


В одном из проектов понадобилось реализовать функцию "Кто на сайте". Средней продолжительности копания привели к модулю django-onlineiser.


$sudo easy install django-onlineuser


далее в settings.py:

#добавляем приложение в список используемых в проекте
INSTALLED_APPS += ('oblineuser')

#регистрируем его middleware в соответствующем списке
MIDDLEWARE_CLASSES += ('onlineuser.middleware.OnlineUserMiddleware')



в нужном html шаблоне подгружаем теги этого приложения и там же отображаем html-код этой приблуды

{% load onlineuser_tags %}
Кто здесь?
{%onlineinfos%}


У меня, чтобы заработало пришлось немного модифицировать код класса из middleware.py, обернув имеющийся там код в try except

class OnlineUserMiddleware:
    def process_request(self, request):
        try:
            user = request.user
            ip=request.META['REMOTE_ADDR']
            if user.is_authenticated():
                o, created = Online.objects.get_or_create(user=user)
                o.session_key = ip
            else:
                o, created = Online.objects.get_or_create(ident=ip)
                o.session_key = ip
            if not created:
                o.save()
        except:
            return


В результате видим простейшую статистику о гостях и онлайн юзерах.

С первого взгляда все ок и автору приложения в любом случае спасибо.

Но чуть позже стало понятно 2 факта (помимо бага описанного выше):
1. сразу после логаута пользователя счетчик пользователей не уменьшается, а счетик гостей не увеличивается
2. после логина счетчик гостей не уменьшается, хотя счетчик пользователей увеличивается

Не совсем релевантно получается.

Я допилил это приложение с помощью сигналов о логине и логауте:


#этот код я добавил в middleware.py рассматриваемого приложения, хотя можно и любое другое удобное место 
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver
@receiver(user_logged_in)
def count_logins(sender,request,user,**kwargs):
    ip=request.META['REMOTE_ADDR']
    g=Online.objects.get(session_key=ip, user=None)
    if(g):
        g.delete() #удаляем гостя , после того , как он стал залогиненным юзером
    #создаем или обновляем запись в таблице приложения (этот код я скопипастил из имеющегося миддлваре-класса
    o, created = Online.objects.get_or_create(user=user) 
    o.session_key = ip
    if not created:
        o.save()    
 
@receiver(user_logged_out)
def count_logouts(sender,request,user,**kwargs):
    o=Online.objects.get(user=user) 
    o.delete() #сразу при логауте удаляем запись из таблицы



Если в ближайшее время не придумаю как более красиво это все дело улучшить, отпишу автору или посто форкну это приложение )

четверг, 1 декабря 2011 г.

Django: Добавляем свои собственные проверки объектов в админку



Если понадобилось дополнить стандарные проверки, которые делает Django при создании, редактировании, удалении объектов через панель администратора, то существует несколько извращенных способов.
Опишу самый простой, наглядный и незатратный.

Начиная с версии 1.2 в Django позволяет добавлять в модель метод clean, в котором и предлагается реализовывать логику нужных вам проверок.

Например у вас есть таблица "Матч" , в полях которой есть две ссылки (ForeignKey) на таблицу "Команда". То есть играют две команды ) Сама с собой команда, понятно, играть не может. По крайней мере в тех командных видах спорта , о которых доводилось слышать )

Код проверки будет примерно прост (добавить метод в класс модели "Матч"):


    def clean(self):
        from django.core.exceptions import ValidationError
        if(self.team1.id == self.team2.id):
            raise ValidationError('Команда не может играть сама c собой.')            

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

p.s. сейчас в голову пришла мыслишка: будет ли вызываться этот метод, при сохранении объекта не через админку..хм.. надо будет матчасть почитать или выяснить эмпирически.

Если кто уже в курсе - буду признателен за ответ. Хотя кому это я, читателей пока у меня судя по всему нет )))))

воскресенье, 27 ноября 2011 г.

Django: пишем свой менеджер модели


Недавно натолкнулся на часто востребованную операцию "получить следующее по дате" в моих Django-моделях. Встроенный менеджер objects имеет прекрасный метод latest. Симметричного ему , что-то типа next, я не нашел (может плохо искал).
И так как сейчас я как раз в самом разгаре прокачки django-скиллов (подобно новому персонажу в diablo , когда ну очень много нового и очень быстро происходят level-апы :), то я с радостью покопал тему расширения функционала менеджера и сделал нужный мне метод.



#models.py

#Менеджер (обратите внимание на базовый класс)
class EventManager(models.Manager):
    def next(self):
        import datetime
        try:
            return Event.objects.order_by('date').filter(date__gte=datetime.datetime.now())[0]
        except:
            return None
#Модель, для которой нужен метод next
class Event(models.Model):
    title = models.CharField()
    date  = models.DateTimeField()
    objects = MatchManager() #Явно указываем, кто нами рулит


После этого в соответствующем view я спокойно могу использовать:

Event.objects.next(), что гораздо удобнее и менее хрупко , чем конструирование фильтра.

Пара пояснений, любая модель, пронаследованная явно или опосредованно от models.Model имеет поле objects, значением которого является встроенный менеджер модели (объект класса models.Manager), который имеет кучу полезных методов. Мы создали класс-потомок и чуток его дополнили.
При желании можно параметризовать поле, по которому будет искать объекты метод next - подобно тому, как это делает latest. Но пока этого не требовалось, буду тратить время на что-то другое.
Что такое менеджер модели, во всяком случае, мне стало чуток понятнее =)

Django: лимитированные запросы - не забываем про исключения


Для того, чтобы составить лимитированный запрос средствами Django используется нотация доступа к элементам массива.
Например, нам нужно получить ближайшее событие. Для этого список событий надо осортировать, выбрать только те, дата которых больше текущей и вернуть событие с наименьшей датой.


event = Event.objects.order_by('date').filter(date__gte=datetime.datetime.now())[0]



[0] - как раз и отвечает за лимитирование.

Но здесь есть небольшой подводный камушек. Результат этого вызова нельзя сразу передавать в шаблон. Потому что , если событий , соответствующих такому критерию не будет найдено , возникнет исключение IndexOutOfRange.
Избежать 500-ток или дебаг-страниц можно просто:


try:
  event = Event.objects.order_by('date').filter(date__gte=datetime.datetime.now())[0]
except:
  event = None


понедельник, 21 ноября 2011 г.

Django: кастомизируем форму комментариев от django-comments


DJANGO-фреймворк включает в себя встроенный механизм комментирования контента произвольного типа. Довольно удобная штука. Имеет даже встроенные механизмы борьбы со спамом, модерацией, удобный интерфейс администрирования... Но, как и любое коробочное решение , рано или поздно требует доводки.
В моем случае не потребовалось писать кастомное приложение с наследованием классов встроенных комментариев - надо было лишь изменить интерфейс формы комментария.
Вот, что надо сделать, чтобы отрисовать свою форму:

1. включить шаблонный тег: {%load comments%} в самую "верхушку" шаблона страницы, откуда будет приниматься комментарий пользователя
2. в том месте, где будет показываться форма , поместить шаблонный тег: {% render_comment_form for entry%}
3. в основной директории для хранения шаблонов создать подкаталог 'comments' и поместить в него файл form.html , который будет, например с таким содержанием:

{% if user.is_authenticated %}
<form action="/comments/post/" method="post">
{%csrf_token%}
<textarea name="comment" id="id_comment" rows="2" style="width: 90%;"></textarea>
<input type="submit" name="post" value="Add">
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
</form>
{% else %}
<p class="red big">авторизуйтесь!<br>только зарегистрированные пользователи могут оставлять комментарии.</p>
{% endif %}

Инструкция 'if user.is_authenticated' необязательна - Вы можете и разрешить комментарии для неавторизованных пользователей (вы можете даже вообще не использовать встроенный механизм авторизации =)

Тег {%csrf_token%} также не имеет никакого отношения к топику, но для полноты картины оставил и его.

Далее идет html-код моей формы, в которой от кучи "коробочных" полей , я оставил только текст комментария. Остальное мне не нужно было. Автор комментария (а это будет id - авторизованного пользователя) проставится в соответствующее поле самим приложением comments.

Шаблонный цикл, вставляющий некие hidden-поля ОБЯЗАТЕЛЕН - без этих самых полей засабмитить форму не получится. Для понимания: в скрытых полях , которые я добавил таким образом, передаются тип контента (ContentType), идентификатор объекта (ForeignKey), которому относится комментарий и еще поле для защиты от спама, которое не-бот никогда не увидит и не заполнит, а вот тупой бот может и перемудрить, отправив заполненным и его =)

С удалением полей просто. Но следует понимать, что ДОБАВЛЯЯ свои собственные поля, надо не забыить НАУЧИТЬ понимать и принимать эти поля само приложение. Правильнее всего в этом случае написать свое кастомное приложение для комментирования. Об этом неплохо написано на djangoproject.com - копипастить уже написанное смысла не вижу :)

пятница, 18 ноября 2011 г.

Django: превращаем RawQuerySet в JSON-ответ


Всем хорош Djanfo-фреймворк. Да только вот ORM далеко не самый удобный. Хорошо еще что появилась возможность выполнять Raw-запросы. Например, у менеджера objects() есть метод raw(), с помощью которого можно сконструировать произвольный select.
Но радость от такой возможности омрачается тем, что у RawQuerySet, который получаем на выходе из raw() , нет привычных методов count(), values(), values_list() и тому подобных, так любезно предоставляемых нам классом QuerySet.
Я натолкнулся на это, когда мне потребовалось сформировать HTTP-ответ в формате JSON, содержащий результат довольно хитрой выборки из БД...В итоге список словарей пришлось конструировать вручную.

Примерно так это выглядит:

from django.core.serializers.json import DjangoJSONEncoder
from django.utils import simplejson

def sample_view(request):
    mymodels = MyModels.objects.raw("select * from myapp_mymodel")
    mlist = list()
    for m in mymodels:
        mlist.append(
         {'id': m.id,'title': m.title,'start': m.date,'end':m.date,}
    )
    data = simplejson.dumps(mlist, cls=DjangoJSONEncoder)
    return HttpResponse(data)

среда, 16 ноября 2011 г.

django: контроль операций INSERT, UPDATE, DELETE

Механизм моделей Django позволяет контролировать операции вставки , изменения и удаления объектов из БД двумя способами (не считая триггеров непосредственно в БД):

Способ первый: Cигналы

Модуль django.dj.models содержит основной набор сигналов, связанных с операциями с БД: pre_save, ost_save, pre_delete, post_delete.
Довольно подробно работа с сигналами описана здесь
Мне, как еще не до конца искушенному django_разработчику для решения реальных задач сигналы пока не приходилось использовать. Поэтому какими-либо тонкостями работы с ними делиться было бы глупо))
А вот второй способ использую довольно активно.

Способ второй: переопределение методов Save, Delete класса Models в классах-наследниках.

А именно такими классами и описываются модели Django-приложений.
- Почему нет отдельного метода для update?
- Потому что для этого используется save.
Сигнатура метода save содержит 1 параметр, которым должен быть self. То есть ссылка на сам объект. У объекта, который еще ни разу не сохранялся в БД поле id пустое, а у того, который был считан из БД и предназначен для обновления (update) идентификатор уже инициализирован. Соответственно, простейшая проверка внутри обработчика сигнала позволяет разделить код обработки save от кода обработки update.

понедельник, 31 октября 2011 г.

django: пример Ajax-аутентификации


В данной статье описывается пример реализации ajax-аутентификации в django.
На стороне клиента используется js-фреймворк Dojo. Ничто не мешает взять любой другой, например jquery.
Итак приступим:
1. определяем специальный view для обработки ajax-запросов (используем только POST)
#views.py
from django.contrib.auth.forms import AuthenticationForm
@csrf_protect #предварительно в settings.py надо подключить соответствующее moddleware
@never_cache
def ajax_login(request, template_name='registration/login.html',
          redirect_field_name=REDIRECT_FIELD_NAME,
          authentication_form=AuthenticationForm,
          current_app=None, extra_context=None):
    logger = logging.getLogger(__name__)
    manipulator = AuthenticationForm(request)
    redirect_to = request.REQUEST.get(redirect_field_name, '')
    if request.method == "POST":
        form = authentication_form(data=request.POST)
        if form.is_valid():
            netloc = urlparse.urlparse(redirect_to)[1]
            auth_login(request, form.get_user())
            if request.session.test_cookie_worked():
                request.session.delete_test_cookie()
            logger.error('SUCCESS')
            return HttpResponse(simplejson.dumps("OK"))
        else:
            logger.error('FAILED')
            return HttpResponse(simplejson.dumps("FAIL"))
    else:
        return HttpResponseRedirect('/');
        form = authentication_form(request)
    request.session.set_test_cookie()
    current_site = get_current_site(request)
    context = {
        'form': form,
        redirect_field_name: redirect_to,
        'site': current_site,
        'site_name': current_site.name,
    }
    context.update(extra_context or {})
    return render_to_response(template_name, context,
                              context_instance=RequestContext(request, current_app=current_app))

2. вставим в соответствие конкретному url определенную выше view

from my_auth_app.views import ajax_login

urlpatterns += patterns('',
    url(r'^login/ajax', ajax_login),
)

3. Описываем шаблон формы и клиентскую js-часть:

<script>
    var sendLoginButton;
    function sendLogin(){
      dojo.xhrPost({
        url: '/login/ajax/',
        load:sendLoginCallback,
        form: dojo.byId('loginForm'),
        handleAs: "json",
        error: function(response,ioArgs) {
         dojo.byId("messagestatus").innerHTML = "Попробуйте еще раз!";
         dojo.byId("loginForm").style.display = "yes";
      });
      dojo.byId("loginForm").style.display = "none";
      dojo.byId("messagestatus").innerHTML = "Проверка...";
    }

    function sendLoginCallback(type, data, evt) {
      console.error(type);
      console.error(data);
      console.error(evt);
      if (type == 'FAIL'){
         dojo.byId("messagestatus").innerHTML = "Попробуйте еще раз!";
         dojo.byId("loginForm").style.display = "";
      }
      else {
        if (data == "false") {
          dojo.byId("loginForm").style.display = "";
          dojo.byId("messagestatus").innerHTML = "Your password or username is invalid";
          dojo.disconnect(sendLoginButton, 'onclick', 'sendLogin');
          var anim = dojo.lfx.html.highlight("messagestatus", [255, 0, 0], 200).play(100);
          dojo.connect(anim, "onEnd", function() {  dojo.connect(sendLoginButton, 'onclick', 'sendLogin'); });

        }
        else {
           window.location="/";
        }
      }
    }
    function init(){
      console.log(document.cookie);
      sendLoginButton = dojo.byId('loginButton');
      var loginForm = dojo.byId('loginForm');
      if(loginForm){
          loginForm.onsubmit = function() { return false; }
          dojo.connect(sendLoginButton, 'onclick', 'sendLogin');
      }
   }
   dojo.addOnLoad(init);
</script>
<div id="messagestatus"></div>
{% if user.is_authenticated %}
<p id="welcome">Хелло, {{user.username}}</p>
{% else %}
  <form  method="post" id="loginForm" class="blocks">
  {%if form.errors %}
    Неудачный вход.
  {% endif %}
     <label for="id_user">Ник:</label> {{ form.username }}
   Ошибка:{{form.username.errors}}
Пояснение:{{ form.username.help_text }}
  <label for="id_password">Пароль:</label> {{ form.password }}
 Ошибка:{{form.password.errors }}
Пояснение:{{ form.password.help_text }}
<input id="loginButton" class="large button" type="submit" value="ВОЙТИ" />
  </form>
{% endif %}

среда, 12 октября 2011 г.

perl: несколько проблем установкой GD и способы их решения

Сегодня потребовалась установка CPAN-модуля GD.pm на один из серверов.
При установке возникла ошибка:

perl -MCPAN -e 'install "GD"'

we get the following fatal error message:
**UNRECOVERABLE ERROR**
Could not find gdlib-config in the search path. Please install libgd
2.0.28 or higher.
If you want to try to compile anyway, please rerun this script with
the option --ignore_missing_gd. 
...

Ок, пытаюсь ставить libgd из репозитория (Debian 5 Lenny):

$ apt-get install libgd2-xpm
$ apt-get install libgd2-xpm-dev

Пробую ставить опять perl-модуль - та же ошибка.

Раскопки и гуглеж позволили выяснить, что дело в том, что в штатный пакет не входит скрипт gdlib-config.

Решить проблему можно , поставив libgd самостоятельно, взяв исходники, например, отсюда

$ wget http://www.boutell.com/gd/http/gd-2.0.28.tar.gz
$ tar xvf gd-2.0.28.tar.gz
$ cd gd-2.0.28
$ ./configure --prefix=/usr
$ make
$ sudo make install

После этого повторяем попытку поставить GD.pm.

Может возникнуть еще одна проблема:
Manifying blib/man3/GD::Polygon.3pm
  LDS/GD-2.46.tar.gz
  /usr/bin/make -- OK
Warning (usually harmless): 'YAML' not installed, will not store persistent state
Running make test
PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e" "test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/GD..........Testing using png support.
t/GD..........FAILED test 10                                                 
        Failed 1/12 tests, 91.67% okay
t/Polyline....ok                                                             
Failed Test Stat Wstat Total Fail  List of Failed
-------------------------------------------------------------------------------
t/GD.t                    12    1  10
Failed 1/2 test scripts. 1/13 subtests failed.
Files=2, Tests=13,  1 wallclock secs ( 0.12 cusr +  0.02 csys =  0.14 CPU)
Failed 1/2 test programs. 1/13 subtests failed.
make: *** [test_dynamic] Ошибка 255
  LDS/GD-2.46.tar.gz
  /usr/bin/make test -- NOT OK
//hint// to see the cpan-testers results for installing this module, try:
  reports LDS/GD-2.46.tar.gz
Warning (usually harmless): 'YAML' not installed, will not store persistent state
Running make install
  make test had returned bad status, won't install without force


Подозрение на отсутствие библиотеки libpng. Ставим ее:
# apt-get install libpng3 libpng3-dev

Опять пробуем ставить perl-модуль - опять то же самое.

Удаляем libgd, конфигурим , собираем и ставим ее заново.Делаем так, потому что изначально у нас не было libpng и , соответственно, libgd была собрана без ее поддержки. Надо пересобрать:

# в директории с исходниками GD
$ make clean 
$ ./configure --prefix=/usr
$ make 
$ sudo make install

После этого GD.pm должен установиться успешно.

вторник, 27 сентября 2011 г.

Django: "Accessor for field... clashes with related field" error after syncdb


If you see error message
Accessor for field ... clashes with related field
after syncdb command then likely problem in that one of your tables reffers another tables by 2 (or more) of its fields..

Example:


class User(models.Model):
     name = models.TextField()
     birth_date = models.DateTimeField()

class Message(models.Model):
     from = models.ForeignKey(User)
     to = models.ForeignKey(User)
     text = models.TextField()
#table 'Messages' has two links to table 'User'

Django could resolve this issue by defaulf by adding incremental postfix to accessor names for example.. But I have never hear about such django feature :)

To cut a long story short.. problem is solved by adding extra attributes for every referencing field:


class User(models.Model):
     name = models.TextField()
     birth_date = models.DateTimeField()

class Message(models.Model):
     from = models.ForeignKey(User,related_name="message_sender")
     to = models.ForeignKey(User,related_name="message_reciever")
     text = models.TextField()

syncdb shouldnt show accessor errors after that..