Я только что написал подклассы Field и Widget, которые решают эту конкретную проблему и могут использоваться, например, с автозаполнением JS - и могут использоваться повторно. Тем не менее, это потребовало больше работы, чем ваше решение, и я не уверен, захотите ли вы использовать мой или нет. В любом случае - надеюсь, я получу немного голосов - я потратил немало времени и усилий, чтобы написать это ...
Вместо того, чтобы определять вашу ModelForm, как вы делали, и возиться с clean_
Я предлагаю что-то вроде этого:
class SongForm(forms.ModelForm):
artist = CustomModelChoiceField( queryset = Artist.objects.all(), query_field = "name" )
class Meta:
model = Song
Теперь CustomModelChoiceField (я не могу придумать лучшего названия для класса) - это подкласс ModelChoiceField, что хорошо, потому что мы можем использовать аргумент queryset
для сужения приемлемых вариантов. Если аргумент widget
отсутствует, как указано выше, используется значение по умолчанию для этого поля (подробнее об этом позже). query_field
является необязательным и по умолчанию "pk"
. Итак, вот код поля:
class CustomModelChoiceField( forms.ModelChoiceField ):
def __init__( self, queryset, query_field = "pk", **kwargs ):
if "widget" not in kwargs:
kwargs["widget"] = ModelTextInput( model_class = queryset.model, query_field = query_field )
super( CustomModelChoiceField, self ).__init__( queryset, **kwargs )
def to_python( self, value ):
try:
int(value)
except:
from django.core.exceptions import ValidationError
raise ValidationError(self.error_messages['invalid_choice'])
return super( CustomModelChoiceField, self ).to_python( value )
Что означает __init__
, так это то, что установка widget = None
во время создания CustomModelChoiceField
дает нам простое ModelChoiceField
(что было очень полезно при отладке ...). Теперь фактическая работа выполняется в виджете ModelTextInput
:
class ModelTextInput( forms.TextInput ):
def __init__( self, model_class, query_field, attrs = None ):
self.model_class = model_class
self.query_field = query_field
super( ModelTextInput, self ).__init__( attrs )
def render(self, name, value, attrs = None ):
try:
obj = self.model_class.objects.get( pk = value )
value = getattr( obj, self.query_field )
except:
pass
return super(ModelTextInput, self).render( name, value, attrs )
def value_from_datadict( self, data, files, name ):
try:
return self.model_class.objects.get( **{ self.query_field : data[name] } ).id
except:
return data[name]
По сути, это TextInput, который знает о двух дополнительных вещах - какой атрибут какой модели он представляет. (model_class
следует заменить на queryset
для сужения возможных вариантов на самом деле работать, я исправлю это позже). Глядя на реализацию value_from_datadict
, легко определить, почему to_python
в поле пришлось переопределить - он ожидает значение int
, но не проверяет, является ли оно истинным, - и просто передает значение в связанную модель, которая завершается неудачно с безобразное исключение.
Я тестировал это некоторое время, и оно работает - вы можете указать различные поля модели, по которым поле формы будет пытаться найти ваш artist
, обработка ошибок формы выполняется автоматически базовыми классами, и вам не нужно писать пользовательские clean_
метод каждый раз, когда вы хотите использовать аналогичные функции.
Я слишком устал сейчас, но я постараюсь редактировать этот пост (и код) завтра.