Index: django/contrib/admin/widgets.py
===================================================================
--- django/contrib/admin/widgets.py (revision 17048)
+++ django/contrib/admin/widgets.py (working copy)
@@ -130,6 +130,13 @@
def render(self, name, value, attrs=None):
rel_to = self.rel.to
+ # custom fields may need the to_python
+ # conversion in order to facilitate
+ # unicode conversion
+ try:
+ value = self.rel.to._meta.get_field(self.rel.field_name).to_python(value)
+ except AttributeError:
+ pass# 'ManyToManyRel' object has no attribute 'field_name'
if attrs is None:
attrs = {}
extra = []
@@ -139,7 +146,7 @@
(rel_to._meta.app_label,
rel_to._meta.module_name),
current_app=self.admin_site.name)
-
+
params = self.url_parameters()
if params:
url = u'?' + u'&'.join([u'%s=%s' % (k, v) for k, v in params.items()])
@@ -248,6 +255,13 @@
def render(self, name, value, *args, **kwargs):
rel_to = self.rel.to
+ # custom fields may need the to_python
+ # conversion in order to facilitate
+ # unicode conversion
+ try:
+ value = self.rel.to._meta.get_field(self.rel.field_name).to_python(value)
+ except AttributeError:
+ pass# 'ManyToManyRel' object has no attribute 'field_name'
info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
self.widget.choices = self.choices
output = [self.widget.render(name, value, *args, **kwargs)]
Index: django/forms/forms.py
===================================================================
--- django/forms/forms.py (revision 17048)
+++ django/forms/forms.py (working copy)
@@ -341,6 +341,14 @@
hidden_widget = field.hidden_widget()
initial_value = hidden_widget.value_from_datadict(
self.data, self.files, initial_prefixed_name)
+ # custom fields may need the to_python
+ # conversion in order to facilitate
+ # unicode conversion
+ if isinstance(initial_value,list):
+ # ManyToManyField uses a list
+ initial_value = [field.to_python(v) for v in initial_value]
+ else:
+ initial_value = field.to_python(initial_value)
if field.widget._has_changed(initial_value, data_value):
self._changed_data.append(name)
return self._changed_data
Index: tests/regressiontests/admin_widgets/fields.py
===================================================================
--- tests/regressiontests/admin_widgets/fields.py (revision 0)
+++ tests/regressiontests/admin_widgets/fields.py (working copy)
@@ -0,0 +1,139 @@
+from django.db import models
+import uuid
+from sqlite3 import Binary
+
+class UUIDVersionError(Exception):
+ pass
+
+class UUIDField(models.Field):
+
+ __metaclass__ = models.SubfieldBase
+ empty_strings_allowed = False
+
+ def __init__(self, primary_key=False, verbose_name=None, name=None, auto=True, version=1, node=None, clock_seq=None, namespace=None, **kwargs):
+ if primary_key and not auto:
+ auto = True
+ if auto:
+ kwargs['blank'] = True
+ kwargs.setdefault('editable', False)
+ self.auto = auto
+ self.version = version
+ if version == 1:
+ self.node, self.clock_seq = node, clock_seq
+ elif version == 3 or version == 5:
+ self.namespace, self.name = namespace, name
+ kwargs['max_length'] = 36
+ super(UUIDField,self).__init__(verbose_name, name, primary_key, **kwargs)
+
+ def contribute_to_class(self, cls, name):
+ if self.primary_key:
+ assert not cls._meta.has_auto_field, \
+ "A model can't have more than one AutoField: %s %s %s; have %s" % \
+ (self, cls, name, cls._meta.auto_field)
+ super(UUIDField, self).contribute_to_class(cls, name)
+ cls._meta.has_auto_field = True
+ cls._meta.auto_field = self
+ else:
+ super(UUIDField, self).contribute_to_class(cls, name)
+
+ def create_uuid(self):
+ if not self.version or self.version == 4:
+ return uuid.uuid4()
+ elif self.version == 1:
+ return uuid.uuid1(self.node, self.clock_seq)
+ elif self.version == 2:
+ raise UUIDVersionError("UUID version 2 is not supported.")
+ elif self.version == 3:
+ return uuid.uuid3(self.namespace, self.name)
+ elif self.version == 5:
+ return uuid.uuid5(self.namespace, self.name)
+ else:
+ raise UUIDVersionError("UUID version %s is not valid." % self.version)
+
+ def db_type(self, connection):
+ """
+ Returns the database column data type for the Field,
+ taking into account the connection object, and the
+ settings associated with it.
+ """
+ return 'Binary(16)'
+
+ def to_python(self, value):
+ """
+ Converts a value as returned by your database
+ (or a serializer) to a Python object.
+ """
+ if isinstance(value,models.Model):
+ # This happens with related fields
+ # when the relation uses a UUIDField
+ # as the primary key.
+ value = value.pk
+ if isinstance(value,buffer):
+ value = "%s" % value
+ if value is None:
+ value = ''
+ if isinstance(value,basestring) and not value:
+ pass
+ elif not isinstance(value,uuid.UUID):
+ try:
+ value = uuid.UUID(value)
+ except ValueError:
+ value = uuid.UUID(bytes=value)
+ return unicode(value)
+
+ def get_prep_value(self, value):
+ """
+ New in Django 1.2
+ This is the reverse of to_python() when working with
+ the database backends (as opposed to serialization).
+ """
+ if isinstance(value,buffer):
+ value = "%s" % value
+ if value is None:
+ value = ''
+ if isinstance(value,basestring) and not value:
+ pass
+ elif not isinstance(value,uuid.UUID):
+ try:
+ value = uuid.UUID(value).bytes
+ except ValueError:
+ value = uuid.UUID(bytes=value).bytes
+ return value
+
+ def get_db_prep_value(self, value, connection, prepared=False):
+ value = super(UUIDField,self).get_db_prep_value(value, connection=connection, prepared=prepared)
+ if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
+ value = Binary(value)
+ return value
+
+ def pre_save(self, model_instance, add):
+ """
+ This method is called just prior to get_db_prep_save()
+ and should return the value of the appropriate attribute
+ from model_instance for this field. If the model is
+ being saved to the database for the first time, the add
+ parameter will be True, otherwise it will be False.
+ """
+ if self.auto and add:
+ value = unicode(self.create_uuid())
+ setattr(model_instance, self.attname, value)
+ else:
+ value = super(UUIDField, self).pre_save(model_instance, add)
+ if self.auto and not value:
+ value = unicode(self.create_uuid())
+ setattr(model_instance, self.attname, value)
+ return value
+
+ def formfield(self, **kwargs):
+ if self.primary_key:
+ return None
+ else:
+ return super(UUIDField,self).formfield(**kwargs)
+
+ def value_to_string(self, obj):
+ """
+ This method is used by the serializers to convert the
+ field into a string for output.
+ """
+ value = self._get_val_from_obj(obj)
+ return self.to_python(value)
\ No newline at end of file
Index: tests/regressiontests/admin_widgets/models.py
===================================================================
--- tests/regressiontests/admin_widgets/models.py (revision 17048)
+++ tests/regressiontests/admin_widgets/models.py (working copy)
@@ -1,5 +1,7 @@
from django.db import models
from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from fields import UUIDField
class MyFileField(models.FileField):
@@ -99,3 +101,36 @@
"""
name = models.CharField(max_length=20)
companies = models.ManyToManyField(Company)
+
+class Base(models.Model):
+ class Meta():
+ abstract = True
+
+ binary_id = UUIDField(primary_key=True)
+
+ def __unicode__(self):
+ return u"%s.__unicode__() resulting pk: %s" % (self.__class__.__name__, self.pk)
+
+ def get_change_url(self):
+ return "/widget_admin/admin_widgets/%s/%s/" % (
+ self.__class__.__name__.lower(),
+ self.pk
+ )
+
+class A(Base):
+ pass
+
+class B(Base):
+ relation = models.ForeignKey(A)
+
+class C(Base):
+ relation = models.OneToOneField(A)
+
+class D(Base):
+ relation = models.ManyToManyField(A)
+
+class G(Base):
+ relation = models.ForeignKey(A)
+
+class H(Base):
+ relation = models.ManyToManyField(A)
\ No newline at end of file
Index: tests/regressiontests/admin_widgets/tests.py
===================================================================
--- tests/regressiontests/admin_widgets/tests.py (revision 17048)
+++ tests/regressiontests/admin_widgets/tests.py (working copy)
@@ -7,18 +7,25 @@
from django.conf import settings
from django.contrib import admin
from django.contrib.admin import widgets
+from django.contrib.auth.models import User
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import DateField
+from django.http import HttpResponse
from django.test import TestCase as DjangoTestCase
from django.utils import translation
+from django.utils.encoding import force_unicode
from django.utils.html import conditional_escape
from django.utils.unittest import TestCase
from . import models
from .widgetadmin import site as widget_admin_site
+from re import search,DOTALL
+from .models import A,B,C,D,G,H
+
+
admin_media_prefix = lambda: {
'ADMIN_MEDIA_PREFIX': "%sadmin/" % settings.STATIC_URL,
}
@@ -372,3 +379,157 @@
# Used to fail with a name error.
w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
self.assertFalse(w.can_add_related)
+
+class TestsFor17122(DjangoTestCase):
+ def setUp(self):
+ # create the admin user
+ user = User()
+ user.username = "admin"
+ user.set_password("admin")
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+
+ # log in
+ result = self.client.login(username="admin", password="admin")
+ self.assertEqual(result, True)
+
+ # create instances to work with
+ a1 = A()
+ a2 = A()
+ a3 = A()
+ a1.save()
+ a2.save()
+ a3.save()
+ self.a1 = a1
+ self.a2 = a2
+ self.a3 = a3
+
+ # create foreign keys to test
+ b1 = B(relation=a1)
+ b2 = B(relation=a2)
+ b3 = B(relation=a3)
+ b1.save()
+ b2.save()
+ b3.save()
+ self.b1 = b1
+ self.b2 = b2
+ self.b3 = b3
+
+ # create one to ones for testing
+ c1 = C(relation=a1)
+ c1.save()
+ self.c1 = c1
+
+ # create many to manys for testing
+ d1 = D()
+ d1.save()
+ d1.relation.add(a1)
+ d1.relation.add(a2)
+ d1.save()
+ self.d1 = d1
+
+ # create foreign keys to test raw id widget
+ g1 = G(relation=a1)
+ g1.save()
+ self.g1 = g1
+
+ # create m2m to test raw id widget
+ h1 = H()
+ h1.save()
+ h1.relation.add(a1)
+ h1.relation.add(a2)
+ h1.save()
+ self.h1 = h1
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_ForeignKey_render(self):
+ response = self.client.get(self.b1.get_change_url())
+ self.assertContains(response,
+ '' % (
+ self.b1.relation.pk,
+ self.b1.relation
+ )
+ )
+ for a in A.objects.all().exclude(pk=self.b1.relation.pk):
+ self.assertContains(response,
+ '' % (
+ a.pk,
+ a
+ )
+ )
+
+ def test_OneToOneField_render(self):
+ response = self.client.get(self.c1.get_change_url())
+ self.assertContains(response,
+ '' % (
+ self.c1.relation.pk,
+ self.c1.relation
+ )
+ )
+ for a in A.objects.all().exclude(pk=self.c1.relation.pk):
+ self.assertContains(response,
+ '' % (
+ a.pk,
+ a
+ )
+ )
+
+ def test_ManyToManyField_render(self):
+ response = self.client.get(self.d1.get_change_url())
+ others = A.objects.all()
+ for a in self.d1.relation.all():
+ self.assertContains(response,
+ '' % (
+ a.pk,
+ a
+ )
+ )
+ others = others.exclude(pk=a.pk)
+ for a in others:
+ self.assertContains(response,
+ '' % (
+ a.pk,
+ a
+ )
+ )
+
+ def test_ForeignKeyRawIdWidget_render(self):
+ response = self.client.get(self.g1.get_change_url())
+ result = search(
+ r''%(
+ self.g1.relation.pk,
+ self.g1.relation.pk,
+ ),
+ str(response),
+ DOTALL
+ )
+ self.assertTrue(result,"ForeignKeyRawIdWidget failed with non-unicode pk.")
+
+ def test_ManyToManyRawIdWidget_render(self):
+ response = self.client.get(self.h1.get_change_url())
+ result = search(
+ r'[a-zA-Z0-9,-]*?)"|value="(?P[a-zA-Z0-9,-]*?)".*?name="relation").*?>',
+ str(response),
+ DOTALL
+ )
+ if result.group("value1"):
+ value = result.group("value1")
+ elif result.group("value2"):
+ value = result.group("value2")
+ else:
+ value = ""
+ observed_pks = set([force_unicode(pk) for pk in value.split(",")])
+ relation_pks = set([force_unicode(h.pk) for h in self.h1.relation.all()])
+ msg = "ManyToManyRawIdWidget did not render properly."
+ if hasattr(self,"longMessage") and not self.longMessage:
+ msg = "%s Enable longMessage to see the difference between rendered pks and stored pks." % msg
+ if hasattr(self,"assertSetEqual"):
+ self.assertSetEqual(observed_pks, relation_pks, msg)
+ else:
+ diff1 = observed_pks.difference(relation_pks)
+ diff2 = relation_pks.difference(observed_pks)
+ if diff1 or diff2:
+ self.fail(msg)
Index: tests/regressiontests/admin_widgets/widgetadmin.py
===================================================================
--- tests/regressiontests/admin_widgets/widgetadmin.py (revision 17048)
+++ tests/regressiontests/admin_widgets/widgetadmin.py (working copy)
@@ -25,6 +25,13 @@
class EventAdmin(admin.ModelAdmin):
raw_id_fields = ['band']
+class GAdmin(admin.ModelAdmin):
+ raw_id_fields = ("relation",)
+
+class HAdmin(admin.ModelAdmin):
+ raw_id_fields = ("relation",)
+
+
site = WidgetAdmin(name='widget-admin')
site.register(models.User)
@@ -41,3 +48,11 @@
site.register(models.Bee)
site.register(models.Advisor)
+
+
+site.register(models.A)
+site.register(models.B)
+site.register(models.C)
+site.register(models.D)
+site.register(models.G,GAdmin)
+site.register(models.H,HAdmin)
\ No newline at end of file