15 декабря 2008 г.

queryset в поле формы

Продолжаем разбираться с формами. Имем такую форму:

festival = forms.ModelMultipleChoiceField(label=u'Посещенные фестивали',
required=False,
queryset=Festival.objects.all(),
)

Виджет ес-но не создает select.
Меня озаботило следующее.
А queryset действительно запрашивает все объекты базы или просто используется в виде Festival.objects.all().get(pk=355) .
И вообще, чем отличается Festival.objects.get(pk=355) и Festival.objects.all().get(pk=355) я пока не знаю.
Начинаем искать:

Что делает all()?
Во-первых - читаем документацию:
The all() method returns a QuerySet of all the objects in the database.

(If Entry.objects is a QuerySet, why can't we just do Entry.objects? That's because Entry.objects, the root QuerySet, is a special case that cannot be evaluated. The all() method returns a QuerySet that can be evaluated.)


Entry.objects, корень QuerySet - не может выдать данные. all() - возвращает объект QuerySet, который может выдать данные.

Далее код самой функции

def all(self):
return self.get_query_set()

def get_query_set(self):
"""Returns a new QuerySet object. Subclasses can override this method
to easily customize the behavior of the Manager.
"""
return QuerySet(self.model)

Возвращает новый объект QuerySet.

class QuerySet(object):
"""
Represents a lazy database lookup for a set of objects.
"""
def __init__(self, model=None, query=None):
self.model = model
self.query = query or sql.Query(self.model, connection)

Так query у нас нет, значит запрос выглядит так

# Use the backend's custom Query class if it defines one. Otherwise, use the
# default.
if connection.features.uses_custom_query_class:
Query = connection.ops.query_class(BaseQuery)
else:
Query = BaseQuery

Создается пустой BaseQuery. Далее:

class BaseQuery(object):
def __init__(self, model, connection, where=WhereNode):
self.model = model
self.connection = connection


А вообще, когда queryset возвращает данные, хорошо написано на странице When QuerySets are evaluated:

Internally, a QuerySet can be constructed, filter, sliced, and generally passed around without actually hitting the database. No database activity actually occurs until you do something to evaluate the queryset.


То есть, не происходит никаких обращений к базе, при конструировании queryset'а .
Обращение к базе происходит при следующих запросах к queryset:

Iteration.

for e in Entry.objects.all():
print e.headline

Slicing.

Pickling/Caching.

repr().

len().

list(). Force evaluation of a QuerySet by calling list() on it. For example:

entry_list = list(Entry.objects.all())

Поэтому надо быть внимательным при множественных "вычислениях" queryset, в иных случаях использовать select_related, в других определять queryset единожды:

frndall=Friends.objects.all()#to cache qs
friends_ids=[f.id for f in frndall]
friends_names=[f.name for f in frndall]

но не

friends_ids=[f.id for f in Friends.objects.all()]
friends_names=[f.name for f in Friends.objects.all()]

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

Выходит, если я ничего не напутал, при создании queryset all() к базе обращений не происходит.
Поэтому можно смело передавать,

fest_qs=Festival.objects.all()
queryset=fest_qs

, если нам требуется валидация значений по всей таблице для поля.

Формы в джанго

Даа, сложновато разобраться формами в django.
Этот топик - заметки в этапах ковыряния в исходниках и понятие структуры (может быть ошибочное мнение).
Итак,
1. форма содержит поля (fields).
2. в полях есть виджеты (widgets).

Инициализация формы:
Для вывода формы (а точнее преобразования значения поля из запроса в вид на форме), а также для обратного преобразования этих данных используются виджеты (которые используют данные instance или initial поля, а также POST данные).
Для проверки POST данных, преобразованных виджетом, используется метод clean поля (см ниже).

Вспомним, как мы сохраняем форму. Примерный процесс проверки POST данных:

if request.method == 'POST': # If the form has been submitted...
if form.is_valid():
# Process the data in form.cleaned_data
# ...
form.save()


Наша форма имеет предка - класс BaseForm, в котором есть функция is_valid():


def is_valid(self):
"""
Returns True if the form has no errors. Otherwise, False. If errors are
being ignored, returns False.
"""
return self.is_bound and not bool(self.errors)

Свойство is_bound:

self.is_bound = data is not None or files is not None

self.errors:

errors = property(_get_errors)

Метод свойства:

def _get_errors(self):
"Returns an ErrorDict for the data provided for the form"
if self._errors is None:
self.full_clean()
return self._errors

А вот сама функция
 
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
self.cleaned_data.
"""
self._errors = ErrorDict()
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError, e:
self._errors[name] = e.messages
if name in self.cleaned_data:
del self.cleaned_data[name]
try:
self.cleaned_data = self.clean()
except ValidationError, e:
self._errors[NON_FIELD_ERRORS] = e.messages
if self._errors:
delattr(self, 'cleaned_data')

Метод clean поля:
 
class Field(object):
def clean(self, value):
"""
Validates the given value and returns its "cleaned" value as an
appropriate Python object.

Raises ValidationError for any errors.
"""
if self.required and value in EMPTY_VALUES:
raise ValidationError(self.error_messages['required'])
return value

Например, для поля CharField

def clean(self, value):
"Validates max_length and min_length. Returns a Unicode object."
super(CharField, self).clean(value)
if value in EMPTY_VALUES:
return u''
value = smart_unicode(value)
value_length = len(value)
if self.max_length is not None and value_length > self.max_length:
raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length})
if self.min_length is not None and value_length < self.min_length:
raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length})
return value

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

Моя задача: передать для одного поля ModelChoiceField динамический queryset, в зависимости от другого значения в POST данных (это нужно из-за того, что поле ModelChoiceField меняется динамически через AJAX, в зависимости от другого значения).
Изучив вышеприведенный код я нашел несколько путей для реализации:
1. Переопределив метод clean формы forms.ModelChoiceField
2. Передать в поле полный queryset, изменив виджет (чтобы он принимал наш "подставной" queryset), для вывода ограниченного числа значений.
3. Передавать нужный queryset в зависимости от data (то есть POST).

Последний вариант самый предпочтительный - самый простой, плюс доп проверка queryset, в зависимости от поля (сохранится структура базы) и у меня работает)

11 декабря 2008 г.

Преобразование словаря в строку в Python

Вот собственно:

";".join(["%s=%s" % (k, v) for k, v in params.items()])

params - словарь.
; - разделитель между элементами словаря
= - разделитель между парами ключ\значение.

Все просто =)

9 декабря 2008 г.

Создание блочной структуры, выборка блока по маске в зависимости от текущего url в django

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

Чтобы было понятнее, логика обратная следующей

Block.objects.get(path_mask__contains='url')

Здесь идет поиск по полю path_mask, содержащему 'url'.
Нам же надо найти объект, для которого значение 'url', содежит path_mask.

Код для регистрации inclusion_tag

import re

@register.inclusion_tag("show_block.html")
def show_left_blocks(url):
blocks = Block.objects.filter(column="left", active=True)
blocks_out = []
for block in blocks:
path_r=re.compile(block.path_mask)#создаем регулярное выражение
if path_r.search(url): #ищем в строке url совпадение
blocks_out.append(block) #если есть совпадение - добавляем блок
return {'blocks': blocks_out}


Минусы:
-приходится запрашивать все блоки, поиск идет на уровне приложения а не базы
(в принципе не особо критично, т.к. блоков даже в крупных проектах не сотни тысяч)

ps. Может я просмотрел, и джанго может сама выполнять данную работу на уровне базы?

8 декабря 2008 г.

Прерывание перехода по ссылке "#" в javascript

Переход по ссылке при обработке ее яваскриптом нам не нужен. Например, при:

link

Почему не нужен? Потому что в адресной строке # нам ни к чему, плюс некоторые браузеры (фаерфокс, ie) выполняют переход по новой ссылке без перезагрузки страницы, что приводит по крайней мере к потере позиции скроллинга страницы.
Посему возвращаем false , тем самым прерывая переход по ссылке:

link

Немного разобрался с созданием форм из моделей

Думал раньше, что если instance содержит данные, значит редактирование, если нет, значит новая запись.
Однако это еще не все. Должна быть data:

f = Form(data=request.POST, instance=a)

Именно поэтому в инете очень много вопросов, почему при создании формы из модели при существующем instance не проходит валидация формы, да потому что данных для валидации при единственном instance на самом деле нет.

О чем я начал тему: http://softwaremaniacs.org/forum/django/7303/

2 декабря 2008 г.

Два formset'а в одном виде, дополнительное отображение

Все задачи без детального продумывания и без идеального знания Питона (или Пайтона).
Это скорее заметки для себя, а не руководство.


Первая ласточка: есть поле manytomany с аргументом through

class Person(models.Model):
name = models.CharField(max_length=128)

class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()

Надо сделать одновременную форму для редактирования Группы и Членства для персоны.
И Членство для Персоны может быть не одно.
Считая, что у нас есть инстанс персоны, создаем два формсета (можно использовать inline_formset для каждого, а можно не использовать, нет разницы, по-моему (лишь использование instance вместо queryset, чего можно добиться просто изменив запрос):

Как создаются формсеты рассказывать не буду. В документации все подробно расписано. Будем создавать формсет на основе модели.
Итого: у нас два одинаковых формсета (в одном Членство, в другом Группа для этого членства). Но нам надо редактировать каждую форму данных формсетов вместе.

Вот простое решение как подружить два формсета, плюс добавить в счетчик.
Добавляем во view:

class MergeForm(object):
def __init__(self, groupform, memebershipform,index):
self.groupform=groupform
self.memebershipform=memebershipform
self.index=index

output=[]
idx=0

for group_form in groupformset.forms:
memebership_form=memebershipformset.forms[idx]
output.append(MergeForm(group_form, memebership_form, idx+1))
idx+=1

Передаем в render_to_response два формсета и наш объект output.

Ну а управление дополнительными формами я планирую вести с помощью jquery