diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index cae33cc..21e87f5 100644
a
|
b
|
def items_for_result(cl, result):
|
222 | 222 | url = cl.url_for_result(result) |
223 | 223 | # Convert the pk to something that can be used in Javascript. |
224 | 224 | # Problem cases are long ints (23L) and non-ASCII strings. |
225 | | result_id = repr(force_unicode(getattr(result, pk)))[1:] |
| 225 | if cl.to_field: |
| 226 | attr = str(cl.to_field) |
| 227 | else: |
| 228 | attr = pk |
| 229 | result_id = repr(force_unicode(getattr(result, attr)))[1:] |
226 | 230 | yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \ |
227 | 231 | (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag)) |
228 | 232 | else: |
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 0a5ab37..a6a206d 100644
a
|
b
|
ORDER_VAR = 'o'
|
24 | 24 | ORDER_TYPE_VAR = 'ot' |
25 | 25 | PAGE_VAR = 'p' |
26 | 26 | SEARCH_VAR = 'q' |
| 27 | TO_FIELD_VAR = 't' |
27 | 28 | IS_POPUP_VAR = 'pop' |
28 | 29 | ERROR_FLAG = 'e' |
29 | 30 | |
… |
… |
class ChangeList(object):
|
52 | 53 | self.page_num = 0 |
53 | 54 | self.show_all = ALL_VAR in request.GET |
54 | 55 | self.is_popup = IS_POPUP_VAR in request.GET |
| 56 | self.to_field = request.GET.get(TO_FIELD_VAR) |
55 | 57 | self.params = dict(request.GET.items()) |
56 | 58 | if PAGE_VAR in self.params: |
57 | 59 | del self.params[PAGE_VAR] |
| 60 | if TO_FIELD_VAR in self.params: |
| 61 | del self.params[TO_FIELD_VAR] |
58 | 62 | if ERROR_FLAG in self.params: |
59 | 63 | del self.params[ERROR_FLAG] |
60 | 64 | |
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 15720a7..50e55dc 100644
a
|
b
|
class FilteredSelectMultiple(forms.SelectMultiple):
|
41 | 41 | |
42 | 42 | class AdminDateWidget(forms.TextInput): |
43 | 43 | class Media: |
44 | | js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", |
| 44 | js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", |
45 | 45 | settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") |
46 | | |
| 46 | |
47 | 47 | def __init__(self, attrs={}): |
48 | 48 | super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}) |
49 | 49 | |
50 | 50 | class AdminTimeWidget(forms.TextInput): |
51 | 51 | class Media: |
52 | | js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", |
| 52 | js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", |
53 | 53 | settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") |
54 | 54 | |
55 | 55 | def __init__(self, attrs={}): |
56 | 56 | super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}) |
57 | | |
| 57 | |
58 | 58 | class AdminSplitDateTime(forms.SplitDateTimeWidget): |
59 | 59 | """ |
60 | 60 | A SplitDateTime Widget that has some admin-specific styling. |
… |
… |
class AdminFileWidget(forms.FileInput):
|
86 | 86 | """ |
87 | 87 | def __init__(self, attrs={}): |
88 | 88 | super(AdminFileWidget, self).__init__(attrs) |
89 | | |
| 89 | |
90 | 90 | def render(self, name, value, attrs=None): |
91 | 91 | output = [] |
92 | 92 | if value and hasattr(value, "url"): |
… |
… |
class ForeignKeyRawIdWidget(forms.TextInput):
|
105 | 105 | super(ForeignKeyRawIdWidget, self).__init__(attrs) |
106 | 106 | |
107 | 107 | def render(self, name, value, attrs=None): |
| 108 | from django.contrib.admin.views.main import TO_FIELD_VAR |
108 | 109 | related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower()) |
| 110 | params = {} |
109 | 111 | if self.rel.limit_choices_to: |
110 | | url = '?' + '&'.join(['%s=%s' % (k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()]) |
111 | | else: |
112 | | url = '' |
| 112 | params.update(dict([(k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()])) |
| 113 | params.update({TO_FIELD_VAR: self.rel.get_related_field().name}) |
| 114 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) |
113 | 115 | if not attrs.has_key('class'): |
114 | 116 | attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook. |
115 | 117 | output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] |
… |
… |
class ForeignKeyRawIdWidget(forms.TextInput):
|
121 | 123 | if value: |
122 | 124 | output.append(self.label_for_value(value)) |
123 | 125 | return mark_safe(u''.join(output)) |
124 | | |
| 126 | |
125 | 127 | def label_for_value(self, value): |
126 | | return ' <strong>%s</strong>' % \ |
127 | | truncate_words(self.rel.to.objects.get(pk=value), 14) |
128 | | |
| 128 | key = self.rel.get_related_field().name |
| 129 | obj = self.rel.to.objects.get(**{key: value}) |
| 130 | return ' <strong>%s</strong>' % truncate_words(obj, 14) |
| 131 | |
129 | 132 | class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): |
130 | 133 | """ |
131 | 134 | A Widget for displaying ManyToMany ids in the "raw_id" interface rather than |
… |
… |
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
133 | 136 | """ |
134 | 137 | def __init__(self, rel, attrs=None): |
135 | 138 | super(ManyToManyRawIdWidget, self).__init__(rel, attrs) |
136 | | |
| 139 | |
137 | 140 | def render(self, name, value, attrs=None): |
138 | 141 | attrs['class'] = 'vManyToManyRawIdAdminField' |
139 | 142 | if value: |
… |
… |
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
141 | 144 | else: |
142 | 145 | value = '' |
143 | 146 | return super(ManyToManyRawIdWidget, self).render(name, value, attrs) |
144 | | |
| 147 | |
145 | 148 | def label_for_value(self, value): |
146 | 149 | return '' |
147 | 150 | |
… |
… |
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
152 | 155 | if value: |
153 | 156 | return [value] |
154 | 157 | return None |
155 | | |
| 158 | |
156 | 159 | def _has_changed(self, initial, data): |
157 | 160 | if initial is None: |
158 | 161 | initial = [] |
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index ef19477..679f63c 100644
a
|
b
|
class ForeignKey(RelatedField, Field):
|
691 | 691 | setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) |
692 | 692 | |
693 | 693 | def formfield(self, **kwargs): |
694 | | defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to)} |
| 694 | defaults = { |
| 695 | 'form_class': forms.ModelChoiceField, |
| 696 | 'queryset': self.rel.to._default_manager.complex_filter( |
| 697 | self.rel.limit_choices_to), |
| 698 | 'to_field_name': self.rel.field_name, |
| 699 | } |
695 | 700 | defaults.update(kwargs) |
696 | 701 | return super(ForeignKey, self).formfield(**defaults) |
697 | 702 | |
diff --git a/django/forms/models.py b/django/forms/models.py
index c153512..3e5d9f8 100644
a
|
b
|
class ModelChoiceIterator(object):
|
548 | 548 | if self.field.cache_choices: |
549 | 549 | if self.field.choice_cache is None: |
550 | 550 | self.field.choice_cache = [ |
551 | | (obj.pk, self.field.label_from_instance(obj)) |
552 | | for obj in self.queryset.all() |
| 551 | self.choice(obj) for obj in self.queryset.all() |
553 | 552 | ] |
554 | 553 | for choice in self.field.choice_cache: |
555 | 554 | yield choice |
556 | 555 | else: |
557 | 556 | for obj in self.queryset.all(): |
558 | | yield (obj.pk, self.field.label_from_instance(obj)) |
| 557 | yield self.choice(obj) |
| 558 | |
| 559 | def choice(self, obj): |
| 560 | if self.field.to_field_name: |
| 561 | key = getattr(obj, self.field.to_field_name) |
| 562 | else: |
| 563 | key = obj.pk |
| 564 | return (key, self.field.label_from_instance(obj)) |
| 565 | |
559 | 566 | |
560 | 567 | class ModelChoiceField(ChoiceField): |
561 | 568 | """A ChoiceField whose choices are a model QuerySet.""" |
… |
… |
class ModelChoiceField(ChoiceField):
|
568 | 575 | |
569 | 576 | def __init__(self, queryset, empty_label=u"---------", cache_choices=False, |
570 | 577 | required=True, widget=None, label=None, initial=None, |
571 | | help_text=None, *args, **kwargs): |
| 578 | help_text=None, to_field_name=None, *args, **kwargs): |
572 | 579 | self.empty_label = empty_label |
573 | 580 | self.cache_choices = cache_choices |
574 | 581 | |
… |
… |
class ModelChoiceField(ChoiceField):
|
578 | 585 | *args, **kwargs) |
579 | 586 | self.queryset = queryset |
580 | 587 | self.choice_cache = None |
| 588 | self.to_field_name = to_field_name |
581 | 589 | |
582 | 590 | def _get_queryset(self): |
583 | 591 | return self._queryset |
… |
… |
class ModelChoiceField(ChoiceField):
|
620 | 628 | if value in EMPTY_VALUES: |
621 | 629 | return None |
622 | 630 | try: |
623 | | value = self.queryset.get(pk=value) |
| 631 | key = self.to_field_name or 'pk' |
| 632 | value = self.queryset.get(**{key: value}) |
624 | 633 | except self.queryset.model.DoesNotExist: |
625 | 634 | raise ValidationError(self.error_messages['invalid_choice']) |
626 | 635 | return value |
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
index 252d46d..9c9ac82 100644
a
|
b
|
class BetterWriter(Writer):
|
78 | 78 | class WriterProfile(models.Model): |
79 | 79 | writer = models.OneToOneField(Writer, primary_key=True) |
80 | 80 | age = models.PositiveIntegerField() |
81 | | |
| 81 | |
82 | 82 | def __unicode__(self): |
83 | 83 | return "%s is %s" % (self.writer, self.age) |
84 | 84 | |
… |
… |
class Price(models.Model):
|
136 | 136 | class ArticleStatus(models.Model): |
137 | 137 | status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True) |
138 | 138 | |
| 139 | class Inventory(models.Model): |
| 140 | barcode = models.PositiveIntegerField(unique=True) |
| 141 | parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) |
| 142 | name = models.CharField(blank=False, max_length=20) |
139 | 143 | |
| 144 | def __unicode__(self): |
| 145 | return self.name |
| 146 | |
140 | 147 | __test__ = {'API_TESTS': """ |
141 | 148 | >>> from django import forms |
142 | 149 | >>> from django.forms.models import ModelForm, model_to_dict |
… |
… |
u'1,2,3'
|
1134 | 1141 | Traceback (most recent call last): |
1135 | 1142 | ... |
1136 | 1143 | ValidationError: [u'Enter only digits separated by commas.'] |
1137 | | >>> f.clean(',,,,') |
| 1144 | >>> f.clean(',,,,') |
1138 | 1145 | u',,,,' |
1139 | 1146 | >>> f.clean('1.2') |
1140 | 1147 | Traceback (most recent call last): |
… |
… |
Traceback (most recent call last):
|
1203 | 1210 | ... |
1204 | 1211 | ValidationError: [u'Select a valid choice. z is not one of the available choices.'] |
1205 | 1212 | |
| 1213 | # Foreign keys which use to_field ############################################# |
| 1214 | |
| 1215 | >>> apple = Inventory.objects.create(barcode=86, name='Apple') |
| 1216 | >>> pear = Inventory.objects.create(barcode=22, name='Pear') |
| 1217 | >>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple) |
| 1218 | |
| 1219 | >>> field = ModelChoiceField(Inventory.objects.all(), to_field_name='barcode') |
| 1220 | >>> for choice in field.choices: |
| 1221 | ... print choice |
| 1222 | (u'', u'---------') |
| 1223 | (86, u'Apple') |
| 1224 | (22, u'Pear') |
| 1225 | (87, u'Core') |
| 1226 | |
| 1227 | >>> class InventoryForm(ModelForm): |
| 1228 | ... class Meta: |
| 1229 | ... model = Inventory |
| 1230 | >>> form = InventoryForm(instance=core) |
| 1231 | >>> print form['parent'] |
| 1232 | <select name="parent" id="id_parent"> |
| 1233 | <option value="">---------</option> |
| 1234 | <option value="86" selected="selected">Apple</option> |
| 1235 | <option value="22">Pear</option> |
| 1236 | <option value="87">Core</option> |
| 1237 | </select> |
| 1238 | |
| 1239 | >>> data = model_to_dict(core) |
| 1240 | >>> data['parent'] = '22' |
| 1241 | >>> form = InventoryForm(data=data, instance=core) |
| 1242 | >>> core = form.save() |
| 1243 | >>> core.parent |
| 1244 | <Inventory: Pear> |
1206 | 1245 | """} |
diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
index e178a75..2bc9075 100644
a
|
b
|
from django.core.files.storage import default_storage
|
5 | 5 | |
6 | 6 | class Member(models.Model): |
7 | 7 | name = models.CharField(max_length=100) |
8 | | |
| 8 | |
9 | 9 | def __unicode__(self): |
10 | 10 | return self.name |
11 | 11 | |
12 | 12 | class Band(models.Model): |
13 | 13 | name = models.CharField(max_length=100) |
14 | 14 | members = models.ManyToManyField(Member) |
15 | | |
| 15 | |
16 | 16 | def __unicode__(self): |
17 | 17 | return self.name |
18 | 18 | |
… |
… |
class Album(models.Model):
|
20 | 20 | band = models.ForeignKey(Band) |
21 | 21 | name = models.CharField(max_length=100) |
22 | 22 | cover_art = models.FileField(upload_to='albums') |
23 | | |
| 23 | |
24 | 24 | def __unicode__(self): |
25 | 25 | return self.name |
26 | 26 | |
| 27 | class Inventory(models.Model): |
| 28 | barcode = models.PositiveIntegerField(unique=True) |
| 29 | parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) |
| 30 | name = models.CharField(blank=False, max_length=20) |
| 31 | |
| 32 | def __unicode__(self): |
| 33 | return self.name |
| 34 | |
27 | 35 | __test__ = {'WIDGETS_TESTS': """ |
28 | 36 | >>> from datetime import datetime |
29 | 37 | >>> from django.utils.html import escape, conditional_escape |
… |
… |
True
|
84 | 92 | >>> w._has_changed([1, 2], [u'1', u'3']) |
85 | 93 | True |
86 | 94 | |
| 95 | # Check that ForeignKeyRawIdWidget works with fields which aren't related to |
| 96 | # the model's primary key. |
| 97 | >>> apple = Inventory.objects.create(barcode=86, name='Apple') |
| 98 | >>> pear = Inventory.objects.create(barcode=22, name='Pear') |
| 99 | >>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple) |
| 100 | >>> rel = Inventory._meta.get_field('parent').rel |
| 101 | >>> w = ForeignKeyRawIdWidget(rel) |
| 102 | >>> print w.render('test', core.parent_id, attrs={}) |
| 103 | <input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="/admin_media/img/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Apple</strong> |
87 | 104 | """ % { |
88 | 105 | 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, |
89 | 106 | 'STORAGE_URL': default_storage.url(''), |