Blog Trac Lyrics for music

All articles, tagged with “django”

Видео с Exception #07

Оказывается, некоторое видео с седьмого Эксепшена уже появилось неделю-две как, а никто о том и не знает. Пока больше ничего нет, насколько я понимаю, поэтому смотрим то, что есть. К сожалению, пока не выложили самого нужного видео — с докладом Дмитрия Кожевина :). Да, из-за того, что выложено на ютубе, картинка качеством не блещет и бывает сложно разобрать, что же там на экране изображено.

Доклады

PickledManyToManyField в Django

Однажды в студёную зимнюю пору понадобилось срочно подняться нам в гору сохранять ManyToMany в истории изменений. Само приложение для истории изменений обитает здесь, но там пока что старая версия, которая даже ForeignKey не умеет сохранять (когда доделаю всё - выложу). Два слова о том, как работает эта самая история работает.

Хранение истории

Для каждой модели, которая будет архивироваться создаётся своя собственная модель-история. Из нужной модели через TargetModel._meta.fields вытягиваются все поля и пересоздаются для новой модели. Ненужные поля отсекаются (например, AutoField), ненужные настройки - тоже (таких целая куча). Также в модель-историю добавляются несколько своих полей: для сохранения IP-адреса, номера ревизии, времени изменения, ссылки на оригинальную модель.

Потом через dispatcher к сигналу post_save присоединяется функция, которая определяет изменилась ли модель, и если да - сохраняется новая ревизия, в которую копируются все поля.

Хранение ManyToMany

Если хранить m2m таким же способом, как все остальные поля, то в таблице в которой будут храниться эти отношения достаточно несложно достичь огромного количества записей. Если в среднем около пятидесяти связей и двухсот ревизий на объект, то нужно всего 10‘000 объектов для ста миллионов отношений в m2m-таблице. Значит, хранить нужно каким-то альтернативным образом. Выход - сохранять отношения в той же таблице.

Задача - написать своё поле, которое внешне будет выглядеть как обычное ManyToManyField, но хранить данные «по месту прописки». Единственное - у него не может быть такой же обратной связи, по понятным причинам. Проведённые исследования показали, что проще всего унаследоваться от базового Field, а не от ManyToManyField, так как переписывать нужно практически все значимые методы, и дополнительные классы (дескриптор, менеджер, rel-класс). А также, информацию про отношение (rel-класс) нельзя хранить в field.rel, как это делается во всех остальных полях с отношениями, потому что в таком случае Django пытается создать обратное отношение (от целевой модели к стартовой), а в случае с Pickled организовать такую связь на уровне таблицы невозможно.

Так как сигналы для ManyToMany пока ещё не включены в Django, то это поле ещё немного бесполезно. А пока кода в django-history нет, на него можно посмотреть на dumpz.org, а покритиковать здесь.

Интроспекция в Python

При создании своей history к Django возник вопрос: “А как же получать юзера при сохранении очередной ревизии?”. Первым делом это было решено при помощи грязных хаков - с помощью функции-хелпера во view к объекту формы добавлялись юзер, айпишник, а потом в форме руками приходилось передавать дополнительные параметры в save(), которые хватаются декоратором, ну а он уже выполняет всю работу. Очень некрасиво, много лишних телодвижений, и можно что-нибудь забыть. Хотелось бы переделать это дело на сигналы… Но в сигналах нет информации о том, кто же это выполняет действие - а значит, надо каким-то образом их получить. Интроспекция - вот выход из ситуации!

Внимательный осмотр бранча full-history (который работает на сигналах) из репозитория Django ни к чему не привёл - у них хоть и есть user = models.ForeignKey(User, default="1"), но он нигде не используется и не заполняется. Потом был осмотрен проект django-modelhistory, который фактически является улучшенным и дополненным бранчем full-history, и там был найден код, который, используя интроспекцию, возвращал юзера. Выглядит этот код как Smart Fortwo, попросту говоря - ужасно. Вот он:

def snoop_the_call_chain():
    """
    Currently, a hackish (and a bit naive) way of walking up the call chain
    to determine the user (if any) that initiated the change in the system.
    """

    from django.contrib.auth.models         import User

    cur = inspect.currentframe()
    desiredFrame = None
    desiredFrameCount = 0
    f = None

    try:
        ancestors = inspect.getouterframes(cur)
        count = 0
        for frame in ancestors:
            if frame[3] == "save":
                desiredFrameCount = count
            count = count+1

        # Ensure that we have a callee for the save methods
        # which we're auditing.
        if debug_mode: print "Desired Frame: (%d)\n" % (desiredFrameCount+1)
        if desiredFrameCount >= len(ancestors):
            if debug_mode: print "No callee in call chain for save method. Bailing ..."
            return None

        desiredFrame = ancestors[desiredFrameCount+1][0]
        if inspect.isframe(desiredFrame):
            for name,value in inspect.getmembers(desiredFrame):
                if name == "f_locals":

                    dictionary = dict(value)
                    count = 1
                    if debug_mode: print "=========== Begin Frame Objects ==============="
                    for key,val in dictionary.items():
                        if debug_mode: print "   (%d) %s: %s" % (count,key,val)
                        count = count + 1
                    if debug_mode: print "============ End Frame Objects ================\n"

                    if 'request' in dictionary.keys() and\
                           dictionary['request'].user:

                        if debug_mode: print "*** Found Calling User ***"
                        return dictionary['request'].user

    finally:
        del cur

Гммм… Не самый очевидный код, плюс в нём есть несколько багов. При помощи мануала по Python и дополнительного мозга код был понят. А потом написан заново, адекватный и рабочий:

def obtain_request():
    """
    True hackish way of walking up the call chain to get the HttpRequest object
    that initiated the change in the system.
    """
    ancestors = inspect.getouterframes(inspect.currentframe())
    for frame_record in ancestors:
        frame = frame_record[0]
        if 'request' in frame.f_locals:
            req = frame.f_locals['request']
            if isinstance(req, HttpRequest):
                return req

Работает отлично. Какие есть замечания, предложения, пожелания, может вопросы?