diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
index 1bc78f8..3941752 100644
a
|
b
|
function html_unescape(text) {
|
11 | 11 | return text; |
12 | 12 | } |
13 | 13 | |
| 14 | function html_escape(text) { |
| 15 | text = text.replace(/&/g, '&'); |
| 16 | text = text.replace(/</g, '<'); |
| 17 | text = text.replace(/>/g, '>'); |
| 18 | text = text.replace(/"/g, '"'); |
| 19 | text = text.replace(/'/g, '''); |
| 20 | return text; |
| 21 | } |
| 22 | |
14 | 23 | // IE doesn't accept periods or dashes in the window name, but the element IDs |
15 | 24 | // we use to generate popup window names may contain them, therefore we map them |
16 | | // to allowed characters in a reversible way so that we can locate the correct |
| 25 | // to allowed characters in a reversible way so that we can locate the correct |
17 | 26 | // element when the popup window is dismissed. |
18 | 27 | function id_to_windowname(text) { |
19 | 28 | text = text.replace(/\./g, '__dot__'); |
… |
… |
function windowname_to_id(text) {
|
27 | 36 | return text; |
28 | 37 | } |
29 | 38 | |
| 39 | function showRelatedObjectPopup(triggeringLink) { |
| 40 | // TODO: use proper id in name to update the label on change |
| 41 | // see admin/options.py#641 |
| 42 | return openPopupWindow(triggeringLink.href, '_popup', 'showrelatedobject'); |
| 43 | } |
| 44 | |
30 | 45 | function showRelatedObjectLookupPopup(triggeringLink) { |
31 | 46 | var name = triggeringLink.id.replace(/^lookup_/, ''); |
32 | 47 | name = id_to_windowname(name); |
33 | | var href; |
34 | | if (triggeringLink.href.search(/\?/) >= 0) { |
35 | | href = triggeringLink.href + '&pop=1'; |
36 | | } else { |
37 | | href = triggeringLink.href + '?pop=1'; |
38 | | } |
39 | | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); |
40 | | win.focus(); |
41 | | return false; |
| 48 | return openPopupWindow(triggeringLink.href, 'pop', name); |
42 | 49 | } |
43 | 50 | |
44 | | function dismissRelatedLookupPopup(win, chosenId) { |
| 51 | function dismissRelatedLookupPopup(win, chosenId, chosenIdHref, chosenName) { |
45 | 52 | var name = windowname_to_id(win.name); |
46 | 53 | var elem = document.getElementById(name); |
| 54 | var nameElem = document.getElementById("view_lookup_" + name); |
| 55 | |
47 | 56 | if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { |
48 | 57 | elem.value += ',' + chosenId; |
49 | 58 | } else { |
50 | 59 | document.getElementById(name).value = chosenId; |
51 | 60 | } |
| 61 | |
| 62 | if (nameElem) { |
| 63 | nameElem.innerHTML = '<a href="' + chosenIdHref + '" ' + |
| 64 | 'onclick="return showRelatedObjectPopup(this);">' + |
| 65 | html_escape(chosenName) + '</a>'; |
| 66 | } |
| 67 | |
52 | 68 | win.close(); |
53 | 69 | } |
54 | 70 | |
55 | 71 | function showAddAnotherPopup(triggeringLink) { |
56 | 72 | var name = triggeringLink.id.replace(/^add_/, ''); |
57 | 73 | name = id_to_windowname(name); |
58 | | href = triggeringLink.href |
59 | | if (href.indexOf('?') == -1) { |
60 | | href += '?_popup=1'; |
61 | | } else { |
62 | | href += '&_popup=1'; |
63 | | } |
64 | | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); |
65 | | win.focus(); |
66 | | return false; |
| 74 | return openPopupWindow(triggeringLink.href, '_popup', name); |
67 | 75 | } |
68 | 76 | |
69 | 77 | function dismissAddAnotherPopup(win, newId, newRepr) { |
… |
… |
function dismissAddAnotherPopup(win, newId, newRepr) {
|
94 | 102 | } |
95 | 103 | win.close(); |
96 | 104 | } |
| 105 | |
| 106 | function openPopupWindow(href, popup_var, name) { |
| 107 | if (href.indexOf('?') == -1) { |
| 108 | href += '?' + popup_var + '=1'; |
| 109 | } else { |
| 110 | href += '&' + popup_var + '=1'; |
| 111 | } |
| 112 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); |
| 113 | win.focus(); |
| 114 | return false; |
| 115 | } |
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 3144a22..1d05359 100644
a
|
b
|
class ModelAdmin(BaseModelAdmin):
|
638 | 638 | return HttpResponseRedirect(request.path + "?_popup=1") |
639 | 639 | else: |
640 | 640 | return HttpResponseRedirect(request.path) |
| 641 | elif request.POST.has_key("_popup"): |
| 642 | # object changed via raw id link popup |
| 643 | return HttpResponse('<script type="text/javascript">window.close();</script>') |
| 644 | # TODO: this should update the object label in the href tag |
| 645 | # see admin/media/js/admin/RelatedObjectLookups.js#39 |
| 646 | # return HttpResponse('<script type="text/javascript"> |
| 647 | # opener.dismissRelatedObjectPopup(window, "%s", "%s");</script>' % \ |
| 648 | # escape() calls force_unicode. |
| 649 | # (escape(pk_value), escape(obj))) |
641 | 650 | elif request.POST.has_key("_saveasnew"): |
642 | 651 | msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': obj} |
643 | 652 | self.message_user(request, msg) |
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 9a4ce3b..043f68e 100644
a
|
b
|
|
1 | 1 | from django.conf import settings |
2 | 2 | from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE |
3 | 3 | from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR |
| 4 | from django.contrib.admin.util import obj_label, get_related_url |
4 | 5 | from django.core.exceptions import ObjectDoesNotExist |
5 | 6 | from django.db import models |
6 | 7 | from django.utils import dateformat |
… |
… |
def items_for_result(cl, result, form):
|
224 | 225 | attr = pk |
225 | 226 | value = result.serializable_value(attr) |
226 | 227 | result_id = repr(force_unicode(value))[1:] |
227 | | yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \ |
228 | | (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 | result_name = obj_label(result) |
| 229 | result_url = get_related_url(result, result.pk) |
| 230 | yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % |
| 231 | (table_tag, row_class, url, |
| 232 | (cl.is_popup |
| 233 | and ' onclick="opener.dismissRelatedLookupPopup(' |
| 234 | "window, %s, '%s', '%s'); return false;\"" % |
| 235 | (result_id, result_url, |
| 236 | result_name.replace("'", r"\'")) |
| 237 | or ''), |
| 238 | conditional_escape(result_repr), table_tag)) |
229 | 239 | else: |
230 | 240 | # By default the fields come from ModelAdmin.list_editable, but if we pull |
231 | 241 | # the fields out of the form instead of list_editable custom admins |
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
index 4bdce45..7888833 100644
a
|
b
|
from django.core.exceptions import ObjectDoesNotExist
|
2 | 2 | from django.db import models |
3 | 3 | from django.utils.html import escape |
4 | 4 | from django.utils.safestring import mark_safe |
5 | | from django.utils.text import capfirst |
| 5 | from django.utils.text import capfirst, truncate_words |
6 | 6 | from django.utils.encoding import force_unicode |
7 | 7 | from django.utils.translation import ungettext, ugettext as _ |
8 | 8 | from django.core.urlresolvers import reverse, NoReverseMatch |
… |
… |
def model_ngettext(obj, n=None):
|
221 | 221 | d = model_format_dict(obj) |
222 | 222 | singular, plural = d["verbose_name"], d["verbose_name_plural"] |
223 | 223 | return ungettext(singular, plural, n or 0) |
| 224 | |
| 225 | def get_related_url(rel_to, action=None, admin_site=None): |
| 226 | reverse_path = 'admin:%s_%s' |
| 227 | rel_path = '%s%s/%s/' |
| 228 | params = [rel_to._meta.app_label, rel_to._meta.object_name.lower()] |
| 229 | |
| 230 | if action: |
| 231 | reverse_path += '_%s' |
| 232 | rel_path += '%s/' |
| 233 | params.append(action) |
| 234 | |
| 235 | if admin_site: |
| 236 | try: |
| 237 | return reverse(reverse_path % tuple(params), |
| 238 | current_app=admin_site.name) |
| 239 | except NoReverseMatch: |
| 240 | params.insert(0, admin_site.root_path) |
| 241 | return rel_path % tuple(params) |
| 242 | |
| 243 | params.insert(0, '../../../') |
| 244 | return rel_path % tuple(params) |
| 245 | |
| 246 | def obj_label(obj): |
| 247 | return escape(truncate_words(obj, 7)) |
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index fb5acb5..e3af036 100644
a
|
b
|
import copy
|
7 | 7 | from django import forms |
8 | 8 | from django.forms.widgets import RadioFieldRenderer |
9 | 9 | from django.forms.util import flatatt |
10 | | from django.utils.html import escape |
11 | | from django.utils.text import truncate_words |
12 | 10 | from django.utils.translation import ugettext as _ |
13 | 11 | from django.utils.safestring import mark_safe |
14 | 12 | from django.utils.encoding import force_unicode |
15 | 13 | from django.conf import settings |
16 | | from django.core.urlresolvers import reverse, NoReverseMatch |
| 14 | from django.contrib.admin.util import get_related_url, obj_label |
17 | 15 | |
18 | 16 | class FilteredSelectMultiple(forms.SelectMultiple): |
19 | 17 | """ |
… |
… |
class ForeignKeyRawIdWidget(forms.TextInput):
|
109 | 107 | def render(self, name, value, attrs=None): |
110 | 108 | if attrs is None: |
111 | 109 | attrs = {} |
112 | | related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower()) |
| 110 | # it would make sense to bind admin_site to ForeignKeyRawIdWidget as |
| 111 | # well (see RelatedFieldWidgetWrapper.__init__) for proper URL reversing |
| 112 | related_url = get_related_url(self.rel.to) |
113 | 113 | params = self.url_parameters() |
114 | 114 | if params: |
115 | 115 | url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) |
… |
… |
class ForeignKeyRawIdWidget(forms.TextInput):
|
123 | 123 | output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \ |
124 | 124 | (related_url, url, name)) |
125 | 125 | output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) |
126 | | if value: |
127 | | output.append(self.label_for_value(value)) |
| 126 | output.append(self.label_for_value(value, 'view_lookup_id_%s' % name)) |
128 | 127 | return mark_safe(u''.join(output)) |
129 | 128 | |
130 | 129 | def base_url_parameters(self): |
… |
… |
class ForeignKeyRawIdWidget(forms.TextInput):
|
146 | 145 | params.update({TO_FIELD_VAR: self.rel.get_related_field().name}) |
147 | 146 | return params |
148 | 147 | |
149 | | def label_for_value(self, value): |
150 | | key = self.rel.get_related_field().name |
151 | | obj = self.rel.to._default_manager.get(**{key: value}) |
152 | | return ' <strong>%s</strong>' % escape(truncate_words(obj, 14)) |
| 148 | def label_for_value(self, value, name): |
| 149 | if value: |
| 150 | key = self.rel.get_related_field().name |
| 151 | obj = self.rel.to._default_manager.get(**{key: value}) |
| 152 | related_url = get_related_url(obj, obj.pk) |
| 153 | return (' <strong id="%s"><a href="%s" ' |
| 154 | 'onclick="return showRelatedObjectPopup(this);">%s</a>' |
| 155 | '</strong>' % (name, related_url, obj_label(obj))) |
| 156 | else: |
| 157 | # a placeholder that will be filled in |
| 158 | # JavaScript dismissRelatedLookupPopup() |
| 159 | return ' <strong id="%s"></strong>' % name |
153 | 160 | |
154 | 161 | class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): |
155 | 162 | """ |
… |
… |
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
170 | 177 | def url_parameters(self): |
171 | 178 | return self.base_url_parameters() |
172 | 179 | |
173 | | def label_for_value(self, value): |
| 180 | def label_for_value(self, value, name): |
174 | 181 | return '' |
175 | 182 | |
176 | 183 | def value_from_datadict(self, data, files, name): |
… |
… |
class RelatedFieldWidgetWrapper(forms.Widget):
|
220 | 227 | media = property(_media) |
221 | 228 | |
222 | 229 | def render(self, name, value, *args, **kwargs): |
223 | | rel_to = self.rel.to |
224 | | info = (rel_to._meta.app_label, rel_to._meta.object_name.lower()) |
225 | | try: |
226 | | related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name) |
227 | | except NoReverseMatch: |
228 | | info = (self.admin_site.root_path, rel_to._meta.app_label, rel_to._meta.object_name.lower()) |
229 | | related_url = '%s%s/%s/add/' % info |
230 | 230 | self.widget.choices = self.choices |
231 | 231 | output = [self.widget.render(name, value, *args, **kwargs)] |
| 232 | rel_to = self.rel.to |
232 | 233 | if rel_to in self.admin_site._registry: # If the related object has an admin interface: |
| 234 | related_url = get_related_url(rel_to, 'add', self.admin_site) |
233 | 235 | # TODO: "id_" is hard-coded here. This should instead use the correct |
234 | 236 | # API to determine the ID dynamically. |
235 | 237 | output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \ |
diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
index 0c81ed3..2eef69b 100644
a
|
b
|
from django.db import models
|
4 | 4 | from django.core.files.storage import default_storage |
5 | 5 | from django.contrib.auth.models import User |
6 | 6 | |
7 | | class MyFileField(models.FileField): |
8 | | pass |
| 7 | class MyFileField(models.FileField): |
| 8 | pass |
9 | 9 | |
10 | 10 | class Member(models.Model): |
11 | 11 | name = models.CharField(max_length=100) |
… |
… |
Currently: <a target="_blank" href="%(STORAGE_URL)salbums/hybrid_theory.jpg">alb
|
107 | 107 | >>> rel = Album._meta.get_field('band').rel |
108 | 108 | >>> w = ForeignKeyRawIdWidget(rel) |
109 | 109 | >>> print conditional_escape(w.render('test', band.pk, attrs={})) |
110 | | <input type="text" name="test" value="1" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Linkin Park</strong> |
| 110 | <input type="text" name="test" value="1" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong id="view_lookup_id_test"><a href="../../../admin_widgets/band/1/" onclick="return showRelatedObjectPopup(this);">Linkin Park</a></strong> |
111 | 111 | |
112 | 112 | >>> m1 = Member.objects.create(pk=1, name='Chester') |
113 | 113 | >>> m2 = Member.objects.create(pk=2, name='Mike') |
… |
… |
True
|
138 | 138 | >>> rel = Inventory._meta.get_field('parent').rel |
139 | 139 | >>> w = ForeignKeyRawIdWidget(rel) |
140 | 140 | >>> print w.render('test', core.parent_id, attrs={}) |
141 | | <input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Apple</strong> |
| 141 | <input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong id="view_lookup_id_test"><a href="../../../admin_widgets/inventory/1/" onclick="return showRelatedObjectPopup(this);">Apple</a></strong> |
142 | 142 | |
143 | 143 | # see #9258 |
144 | 144 | >>> hidden = Inventory.objects.create(barcode=93, name='Hidden', hidden=True) |
145 | 145 | >>> child_of_hidden = Inventory.objects.create(barcode=94, name='Child of hidden', parent=hidden) |
146 | 146 | >>> print w.render('test', child_of_hidden.parent_id, attrs={}) |
147 | | <input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Hidden</strong> |
| 147 | <input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong id="view_lookup_id_test"><a href="../../../admin_widgets/inventory/4/" onclick="return showRelatedObjectPopup(this);">Hidden</a></strong> |
| 148 | |
| 149 | >>> apostrophe = Inventory.objects.create(barcode=88, name="'Apostrophe', and <javascript> and a really really` really really really really really long one!") |
| 150 | >>> apostrophic_child = Inventory.objects.create(barcode=89, name="Apostrophe's child", parent=apostrophe) |
| 151 | >>> print w.render('test', apostrophic_child.parent_id, attrs={}) |
| 152 | <input type="text" name="test" value="88" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong id="view_lookup_id_test"><a href="../../../admin_widgets/inventory/6/" onclick="return showRelatedObjectPopup(this);">'Apostrophe', and <javascript> and a really really` ...</a></strong> |
148 | 153 | """ % { |
149 | 154 | 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, |
150 | 155 | 'STORAGE_URL': default_storage.url(''), |