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

Bzr-репозиторий http://softwaremaniacs.org/code/cicero/
Работающий форум http://softwaremaniacs.org/forum/

Вообще, над редактированием профиля пришлось подумать дольше, чем мне казалось в начале. Со всеми этими красивостями типа OpenID, мутантов и считывания данных из hCard уже не получается сделать одну плоскую форму, которая будет апдейтить один объект. Например уже потому, что изменение поля OpenID нужно прогонять через всю схему авторизации, чтобы человек мог поставить только тот OpenID, которым реально владеет, иначе авторизация теряет смысл :-).

В итоге редактирование профиля состоит аж из трех форм. И если с выводом данных проблем никаких нет — просто в шаблон передаются три формы вместо одной — то обработка POST'ов представляет собой некоторый интерес. Это можно сделать многими разными способами, и мне кажется, что я нашел достаточно хорошее решение.

Начну с того, что несколько разных POST'ов неудобно обрабатывать в одной view. Например потому что форма с OpenID дико "особенная", и в одной view-функции пришлось бы лепить некрасивые if'ы в нескольких местах (я знаю, я так с первого раза и сделал, и мне не понравилось :-) ). Поэтому все формы профиля будут обрабатываться разными view по разным URL'ам:

# показывание страницы
(r'^users/self/$', views.edit_profile),

# двухшаговая обработка OpenID
(r'^users/self/openid/$', views.change_openid),
(r'^users/self/openid_complete/$', views.change_openid_complete),

# обновление данных профиля
(r'^users/self/(personal|settings)/$', views.post_profile),

# чтение hCard
(r'^users/self/hcard/$', views.read_hcard),

Это дает красивое разделение логики, но приносит дублирование: в каждой view должен быть код, который собственно показывает страницу профайла. В каждой, потому что при обработке любой формы могут возникать ошибки, и она должна перепоказать всю страницу, в которой есть и она сама, и остальные формы. К счастью, Django не навязывает на организацию view никаких ограничений — это просто питоновские функции, поэтому оформить общую часть в отдельную функцию проблем не составляет:

def _profile_forms(request):
  from cicero.forms import AuthForm, PersonalForm, SettingsForm
  profile = request.user.cicero_profile
  return {
    'openid': AuthForm(request.session, initial={'openid_url': profile.openid}),
    'personal': PersonalForm(profile, initial=profile.__dict__),
    'settings': SettingsForm(profile, initial=profile.__dict__),
  }

Все, что тут происходит — это создаются три объекта форм, которые заполняются начальными данными из профиля пользователя.

OpenID

С формой для обработки OpenID получилось все очень красиво: я использовал ту же самую уже написанную форму, которая занимается собственно авторизацией. Единственное отличие в том, где именно будет происходить обработка второго редиректа от OpenID-провайдера. Раньше в форме была жестко зашита view авторизации ("auth"), а теперь я вынес это в параметр, и в случае редактирования профиля туда передается редирект на функцию обработки смены OpenID.

Сам же процесс смены тоже частично напоминает авторизацию: там полностью завершается весь процесс, в результате которого создается пользователь с новым OpenID. Но дальше вместо прописывания его в кукесы, его OpenID копируется текущему залогиненному пользователю, а сам новый пользователь просто удаляется. Точнее, не сразу удаляется... В некоторых ситуациях этот пользователь может оказаться уже существующим. Ничто не мешает человеку участвовать на форуме двумя OpenID, а потом захотеть в одном из профилей прописать OpenID другого. В этом случае все статьи найденного пользователя переписываются к текущему, и вместо двух разных пользователей остается один.

@login_required
def change_openid_complete(request):

  # сначала все как при авторизации
  from django.contrib.auth import authenticate
  user = authenticate(session=request.session, query=request.GET)
  if not user:
    return HttpResponseForbidden('Ошибка авторизации')

  # получили два профиля -- новый (или найденный) и текущий
  new_profile = user.cicero_profile
  profile = request.user.cicero_profile
  if profile != new_profile:

    # скопировать openid-данные из нового
    profile.openid, profile.openid_server = new_profile.openid, new_profile.openid_server
    new_profile.delete()
    profile.save()

    # переписать статьи найденного юзера текущему
    for article in user.article_set.all():
      article.author = request.user
      article.save()

    # удалить юзера
    user.delete()

    # новая картинка -- ради нее все задумано :-)
    profile.generate_mutant()
  return HttpResponseRedirect(request.GET.get('redirect', '/'))

Данные профиля

Несмотря на то, что данные профиля разделены на два разных блока, вся их обработка совершенно одинакова — это практически стандартное сохранение полей из формы в объект. Поэтому обе эти формы обрабатываются одной функцией, которая прямо из URL'а принимает название того, с чем работает. Напомню, как это выглядит в urlconf'е:

(r'^users/self/(personal|settings)/$', views.post_profile)

Слова "profile" или "settings" передаются в view в виде параметра, и что особенно приятно, это избавляет от необходимости проверять в самой view, не пришло ли туда из сети что-нибудь неправильное, потому что любое неправильное выдаст 404 еще на этапе разрешения URL'а.

Слова эти совсем неслучайно совпадают с названиями форм в dict'е, который отдает вспомогательная функция _profile_forms. Это дает возможность написать очень короткий код, который заменяет одну из трех форм профиля на такую же, но уже с привязанными к ней POST-данными:

forms = _profile_forms(request)
form = forms[form_name].__class__(forms[form_name].profile, request.POST)
forms[form_name] = form

А дальше уже form проходит стандартную процедуру валидации.

Сами классы форм, правда, создаются слегка криво. В идеале, все что нужно было бы сделать — это сказать "сделать форму на основе таких-то полей модели Profile". К сожалению, новые джанговские формы, которые я использую, слегка не дописаны. Здесь это вылезло тем, что для поля текстовых фильтров (bbcode/markdown) был автоматически создан не <select>, а просто <input type="text">. Поэтому пришлось пока прописать все поля в объекты форм вручную.

hCard

В персональных данных есть еще одна ма-а-аленькая форма из одной кнопки. Все, что делает ее view — вызывает функцию считывания hCard, который вызывается также и при первичной регистрации юзера. Мне слегка не нравится, как она выглядит и работает. Она страдает многими юзабилическими грехами, но главное, она никак не сообщает о том, что реально отработала, и юзеру непонятно, то ли она не сработала вообще, то ли там нет hCard'а, то ли отработала, но данные в hCard те же, что и в профиле. Надо над этим подумать еще...

Комментарии: 5 (особо ценных: 1)

  1. dp_wiz

    Особо ценный комментарий

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

  2. Иван Сагалаев

    Спасибо за наводку, про callback'и я совсем позабыл. Переписал, в итоге, формы так:

    def PersonalForm(profile, *args, **kwargs):
      def callback(field, **kwargs):
        if field.name in ['name']:
          return field.formfield(**kwargs)
    
      return form_for_instance(profile, formfield_callback=callback)(*args, **kwargs)
    
    def SettingsForm(profile, *args, **kwargs):
      def callback(field, **kwargs):
        if field.name == 'filter':
          return ChoiceField(label=field.verbose_name, choices=field.get_choices(False), **kwargs)
    
      return form_for_instance(profile, formfield_callback=callback)(*args, **kwargs)
    

    Читаемость, конечно, дико пострадала, но зато оно гораздо больше DRY.

  3. greg

    Иван, вы изменили своим правилам и теперь в slug поле пишете по-русски? :)

  4. Иван Сагалаев

    Да нет конечно, забыл просто :-).

  5. Nick

    Иван,

    забегаю наперед, конечно. В списке работ по форуму под номером 15 значится Ajax'изация. Какую библиотеку планируете использовать? Можно ответить в форум :)

Добавить комментарий