from __future__ import unicode_literals
import six
from django import forms
from django.db import models
from django.db.models.query import QuerySet
from .registry import registry as default_registry
from .widgets import ChoiceWidget, MultipleChoiceWidget
from .exceptions import AutocompleteChoicesMustBeQuerySet
__all__ = ['FieldBase', 'ChoiceField', 'MultipleChoiceField',
'ModelChoiceField', 'ModelMultipleChoiceField', 'GenericModelChoiceField',
'GenericModelMultipleChoiceField']
[docs]class FieldBase(object):
default_error_messages = {
'invalid': '%(autocomplete)s cannot validate %(value)s',
}
def __init__(self, autocomplete=None, registry=None, widget=None,
widget_js_attributes=None, autocomplete_js_attributes=None,
extra_context=None, *args, **kwargs):
self.autocomplete = self.get_autocomplete(autocomplete, registry,
widget)
widget = widget or self.widget
if isinstance(widget, type):
widget = widget(autocomplete, widget_js_attributes,
autocomplete_js_attributes, extra_context)
kwargs['widget'] = widget
# Does the subclass have ModelChoiceField or ModelMultipleChoiceField
# as a base class?
parents = type(self).__mro__
if (forms.ModelChoiceField in parents or
forms.ModelMultipleChoiceField in parents):
if not isinstance(self.autocomplete.choices, QuerySet):
raise AutocompleteChoicesMustBeQuerySet(self.autocomplete)
kwargs['queryset'] = self.autocomplete.choices
super(FieldBase, self).__init__(*args, **kwargs)
def get_autocomplete(self, autocomplete, registry, widget):
if widget:
# BC: maybe defining the autocomplete as a widget argument ?
autocomplete = getattr(widget, 'autocomplete', None)
registry = registry or default_registry
return registry.get_autocomplete_from_arg(autocomplete)
def validate(self, value):
"""
Wrap around forms.Field and Autocomplete.validate_values().
Field.validate_values() handles the required option.
"""
forms.Field.validate(self, value)
# FIXME: we might actually want to change the Autocomplete API to
# support python values instead of raw values, that would probably be
# more performant.
values = self.prepare_value(value)
if value and not self.autocomplete(values=values).validate_values():
error_params = {
'autocomplete': self.autocomplete.__name__, 'value': value
}
raise forms.ValidationError(self.error_messages['invalid'],
code='invalid', params=error_params)
[docs]class ChoiceField(FieldBase, forms.ChoiceField):
widget = ChoiceWidget
def __init__(self, autocomplete=None, registry=None, widget=None,
widget_js_attributes=None, autocomplete_js_attributes=None,
extra_context=None, *args, **kwargs):
kwargs.update({'choices':
self.get_choices(autocomplete, registry, widget)})
super(ChoiceField, self).__init__(autocomplete, registry, widget,
widget_js_attributes, autocomplete_js_attributes,
extra_context, *args, **kwargs)
def get_choices(self, autocomplete, registry, widget):
a = self.get_autocomplete(autocomplete, registry, widget)()
return ((a.choice_value(c), a.choice_label(c)) for c in a.choices)
[docs]class MultipleChoiceField(ChoiceField, forms.MultipleChoiceField):
widget = MultipleChoiceWidget
class ModelChoiceFieldBase(FieldBase):
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
# Also update autocomplete choices
self.autocomplete.choices = queryset
queryset = property(_get_queryset, _set_queryset)
[docs]class ModelChoiceField(ModelChoiceFieldBase, forms.ModelChoiceField):
widget = ChoiceWidget
[docs]class ModelMultipleChoiceField(ModelChoiceFieldBase,
forms.ModelMultipleChoiceField):
widget = MultipleChoiceWidget
[docs]class GenericModelChoiceField(ModelChoiceFieldBase, forms.Field):
"""
Simple form field that converts strings to models.
"""
widget = ChoiceWidget
[docs] def prepare_value(self, value):
"""
Given a model instance as value, with content type id of 3 and pk of 5,
return such a string '3-5'.
"""
from django.contrib.contenttypes.models import ContentType
if isinstance(value, six.string_types):
# Apparently there's a bug in django, that causes a python value to
# be passed here. This ONLY happens when in an inline ....
return value
elif isinstance(value, models.Model):
content_type = ContentType.objects.get_for_model(
value, for_concrete_model=False)
return '%s-%s' % (content_type.pk, value.pk)
[docs] def to_python(self, value):
"""
Given a string like '3-5', return the model of content type id 3 and pk
5.
"""
from django.contrib.contenttypes.models import ContentType
if not value:
return value
content_type_id, object_id = value.split('-', 1)
try:
content_type = ContentType.objects.get_for_id(content_type_id)
except ContentType.DoesNotExist:
raise forms.ValidationError('Wrong content type')
else:
model = content_type.model_class()
try:
return model.objects.get(pk=object_id)
except model.DoesNotExist:
raise forms.ValidationError('Wrong object id')
[docs]class GenericModelMultipleChoiceField(GenericModelChoiceField):
"""
Simple form field that converts strings to models.
"""
widget = MultipleChoiceWidget
def prepare_value(self, value):
return [super(GenericModelMultipleChoiceField, self
).prepare_value(v) for v in value]
def to_python(self, value):
return [super(GenericModelMultipleChoiceField, self).to_python(v)
for v in value]