Проблема Django, наследующая formfield_callback в ModelForms

Я использую Django уже пару недель, поэтому я могу подходить к этому всевозможные ошибки, но:

У меня есть базовый ModelForm, в который я помещал некоторые файлы шаблонов, чтобы как можно более сухие вещи, и все мои фактические ModelForms просто подклассы, которые являются базовыми. Это отлично работает для error_css_class = 'error' и required_css_class = 'required', но formfield_callback = add_css_classes работает не так, как я ожидал.

forms.py

# snippet I found
def add_css_classes(f, **kwargs):
    field = f.formfield(**kwargs)
    if field and 'class' not in field.widget.attrs:
        field.widget.attrs['class'] = '%s' % field.__class__.__name__.lower()
    return field

class BaseForm(forms.ModelForm):
    formfield_callback = add_css_classes  # not working

    error_css_class = 'error'
    required_css_class = 'required'
    class Meta:
        pass

class TimeLogForm(BaseForm):
    # I want the next line to be in the parent class
    # formfield_callback = add_css_classes
    class Meta(BaseForm.Meta):
        model = TimeLog

Конечная цель состоит в том, чтобы пощекотать некоторые сборщики jquery datetime в формах с классом datefield/timefield/datetimefield. Я хочу, чтобы все поля времени даты в приложении использовали один и тот же виджетов, поэтому я решил сделать это таким образом, чем это явно для каждого поля в каждой модели. Добавление дополнительной строки к каждому классу формы не является чем-то большим, но это просто исказило меня, что я не мог понять это. Копание в источнике django показало, что это, вероятно, делает то, что я не понимаю:

django.forms.models

class ModelFormMetaclass(type):
    def __new__(cls, name, bases, attrs):
        formfield_callback = attrs.pop('formfield_callback', None)

Но я не знаю, как все __init__ и __new__ все перепутаны. В BaseForm я попытался переопределить __init__ и установить formfield_callback до и после вызова super, но я предполагаю, что он должен быть где-то в args или kwargs.

Ответ 1

__ new__ вызывается перед построением объекта. На самом деле это метод factory, который возвращает экземпляр вновь созданного объекта.

Итак, в ModelFormMetaclass есть 3 ключевых строки:

formfield_callback = attrs.pop('formfield_callback', None) #1
fields = fields_for_model(opts.model, opts.fields, 
                                      opts.exclude, opts.widgets, formfield_callback) #2
new_class.base_fields = fields #3

В классе мы присоединяем base_fields к нашей форме.

Теперь посмотрим на класс ModelForm:

class ModelForm(BaseModelForm):
    __metaclass__ = ModelFormMetaclass

Это означает, что ModelFormMetaclass.__ new __ (...) будет вызываться, когда мы создаем экземпляр ModelForm для изменения структуры будущего экземпляра. И attrs __new__ (def __new __ (cls, name, databases, attrs)) в ModelFormMetaclass - это спецификация всех атрибутов класса ModelForm.

Таким образом, решение заключается в создании нового InheritedFormMetaclass для нашего случая (наследующего его от ModelFormMetaclass). Не забудьте вызвать новый родителя в InheritedFormMetaclass. Затем создайте наш класс BaseForm и скажите:

__metaclass__ = InheritedFormMetaclass

В __new __ (...) реализации InheritedFormMetaclass мы можем делать все, что хотим.

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

Ответ 2

Вы можете установить класс виджетов следующим образом:

class TimeLogForm(BaseForm):
# I want the next line to be in the parent class
# formfield_callback = add_css_classes
class Meta(BaseForm.Meta):
    model = TimeLog
    widgets = {
            'some_fields' : SomeWidgets(attrs={'class' : 'myclass'})
    }

Ответ 3

Для того, что вы пытаетесь выполнить, я думаю, что вам лучше просто перебирать поля в форме init. Например,

class BaseForm(forms.ModelForm):
  def __init__(self, *args, **kwargs):
    super(BaseForm, self).__init__(*args, **kwargs)
    for name, field in self.fields.items():
      field.widget.attrs['class'] = 'error'

Очевидно, вам понадобится немного больше логики для вашего конкретного случая. Если вы хотите использовать подход, который предложил серджач (излишний для вашей конкретной проблемы, я думаю), вот какой-то код для вас, который вызовет formfield_callback в базовом классе в том случае, если подкласс не определяет его.

baseform_formfield_callback(field):
    # do some stuff
    return field.formfield()

class BaseModelFormMetaclass(forms.models.ModelFormMetaclass):
    def __new__(cls, name, bases, attrs):
        if not attrs.has_key('formfield_callback'):
            attrs['formfield_callback'] = baseform_formfield_callback
        new_class = super(BaseModelFormMetaclass, cls).__new__(
            cls, name, bases, attrs)
        return new_class

class BaseModelForm(forms.ModelForm):
    __metaclass__ = OrganizationModelFormMetaclass
    # other form stuff

Наконец, вы можете захотеть взглянуть на хрустящие формы: https://github.com/maraujop/django-crispy-forms

Ответ 4

sergzach правильно, что вам нужно использовать метаклассы; переопределить __init__ недостаточно. Причина в том, что метакласс для ModelForm (который будет вызываться для всех подклассов ModelForm, если вы не укажете другой метакласс в подклассе) принимает определение класса, а использование значений в определении класса создает класс с атрибутами класса. Например, как META.fields, так и наш formfield_callback используются для создания полей формы с различными параметрами (например, с виджетами).

Это означает, что AFAIU formfield_callback является параметром метакласса, используемого при создании собственного класса формы модели, а не некоторого значения, используемого во время выполнения, когда создаются экземпляры реальной формы. Это делает размещение formfield_callback в __init__ бесполезным.

Я решил аналогичную проблему с пользовательским метаклассом, например

from django.forms.models import ModelFormMetaclass

class MyModelFormMetaclass(ModelFormMetaclass):
    def __new__(cls,name,bases,attrs):
    attrs['formfield_callback']=my_callback_function
    return super(MyModelFormMetaclass,cls).__new__(cls,name,bases,attrs)

и в базовом классе для всех моих модельных форм, устанавливающих метакласс

class MyBaseModelForm(ModelForm):
    __metaclass__=MyModelFormMetaclass
    ...

который может использоваться как (по крайней мере, в Django 1.6)

class MyConcreteModelForm(MyBaseModelForm):
    # no need setting formfield_callback here
    ...