Index: django/contrib/admin/models.py
===================================================================
--- django/contrib/admin/models.py	(revision 6898)
+++ django/contrib/admin/models.py	(working copy)
@@ -4,11 +4,48 @@
 from django.utils.translation import ugettext_lazy as _
 from django.utils.encoding import smart_unicode
 from django.utils.safestring import mark_safe
+from urllib import quote, unquote
 
 ADDITION = 1
 CHANGE = 2
 DELETION = 3
 
+def pk_url_quote(s):
+    """
+    Ensure that primary key values do not confuse the admin URLs by escaping
+    any special URL characters. Similar to urllib.quote, except that the
+    quoting is slightly different so that it doesn't get automatically
+    unquoted by the Web browser.
+    """
+    if type(s) != str and type(s) != unicode:
+        return s
+    res = list(s)
+    for i in range(len(res)):
+        c = res[i]
+        if c in ':/.#?@=&;':
+            res[i] = '.%02X' % ord(c)
+    return ''.join(res)
+
+def pk_url_unquote(s):
+    """
+    Undo the effects of pk_url_quote(). Based heavily on urllib.unquote().
+    """
+    mychr = chr
+    myatoi = int
+    list = s.split('.')
+    res = [list[0]]
+    myappend = res.append
+    del list[0]
+    for item in list:
+        if item[1:2]:
+            try:
+                myappend(mychr(myatoi(item[:2], 16)) + item[2:])
+            except ValueError:
+                myappend('.' + item)
+        else:
+            myappend('.' + item)
+    return "".join(res)
+
 class LogEntryManager(models.Manager):
     def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''):
         e = self.model(None, None, user_id, content_type_id, smart_unicode(object_id), object_repr[:200], action_flag, change_message)
@@ -50,4 +87,4 @@
         Returns the admin URL to edit the object represented by this log entry.
         This is relative to the Django admin index page.
         """
-        return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id))
+        return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, quote(pk_url_quote(self.object_id))))
Index: django/contrib/admin/templatetags/admin_list.py
===================================================================
--- django/contrib/admin/templatetags/admin_list.py	(revision 6898)
+++ django/contrib/admin/templatetags/admin_list.py	(working copy)
@@ -11,6 +11,7 @@
 from django.utils.encoding import smart_unicode, smart_str, force_unicode
 from django.template import Library
 import datetime
+from urllib import quote
 
 register = Library()
 
@@ -197,7 +198,7 @@
             # Problem cases are long ints (23L) and non-ASCII strings.
             result_id = repr(force_unicode(getattr(result, pk)))[1:]
             yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
-                (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))
+                (table_tag, row_class, quote(url), (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag))
         else:
             yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
 
Index: django/contrib/admin/views/main.py
===================================================================
--- django/contrib/admin/views/main.py	(revision 6898)
+++ django/contrib/admin/views/main.py	(working copy)
@@ -22,7 +22,7 @@
 except NameError:
     from sets import Set as set   # Python 2.3 fallback
 
-from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
+from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION, pk_url_quote, pk_url_unquote
 if not LogEntry._meta.installed:
     raise ImproperlyConfigured, "You'll need to put 'django.contrib.admin' in your INSTALLED_APPS setting before you can use the admin application."
 
@@ -50,42 +50,6 @@
 class IncorrectLookupParameters(Exception):
     pass
 
-def quote(s):
-    """
-    Ensure that primary key values do not confuse the admin URLs by escaping
-    any '/', '_' and ':' characters. Similar to urllib.quote, except that the
-    quoting is slightly different so that it doesn't get automatically
-    unquoted by the Web browser.
-    """
-    if type(s) != type(''):
-        return s
-    res = list(s)
-    for i in range(len(res)):
-        c = res[i]
-        if c in ':/_':
-            res[i] = '_%02X' % ord(c)
-    return ''.join(res)
-
-def unquote(s):
-    """
-    Undo the effects of quote(). Based heavily on urllib.unquote().
-    """
-    mychr = chr
-    myatoi = int
-    list = s.split('_')
-    res = [list[0]]
-    myappend = res.append
-    del list[0]
-    for item in list:
-        if item[1:2]:
-            try:
-                myappend(mychr(myatoi(item[:2], 16)) + item[2:])
-            except ValueError:
-                myappend('_' + item)
-        else:
-            myappend('_' + item)
-    return "".join(res)
-
 def get_javascript_imports(opts, auto_populated_fields, field_sets):
 # Put in any necessary JavaScript imports.
     js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
@@ -309,7 +273,7 @@
 
 def change_stage(request, app_label, model_name, object_id):
     model = models.get_model(app_label, model_name)
-    object_id = unquote(object_id)
+    object_id = pk_url_unquote(object_id)
     if model is None:
         raise Http404("App %r, model %r, not found" % (app_label, model_name))
     opts = model._meta
@@ -501,7 +465,7 @@
 
 def delete_stage(request, app_label, model_name, object_id):
     model = models.get_model(app_label, model_name)
-    object_id = unquote(object_id)
+    object_id = pk_url_unquote(object_id)
     if model is None:
         raise Http404("App %r, model %r, not found" % (app_label, model_name))
     opts = model._meta
@@ -511,7 +475,7 @@
 
     # Populate deleted_objects, a data structure of all related objects that
     # will also be deleted.
-    deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), force_unicode(object_id), escape(obj))), []]
+    deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), pk_url_quote(force_unicode(object_id)), escape(obj))), []]
     perms_needed = set()
     _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)
 
@@ -538,7 +502,7 @@
 
 def history(request, app_label, model_name, object_id):
     model = models.get_model(app_label, model_name)
-    object_id = unquote(object_id)
+    object_id = pk_url_unquote(object_id)
     if model is None:
         raise Http404("App %r, model %r, not found" % (app_label, model_name))
     action_list = LogEntry.objects.filter(object_id=object_id,
@@ -764,7 +728,7 @@
         return qs
 
     def url_for_result(self, result):
-        return "%s/" % quote(getattr(result, self.pk_attname))
+        return "%s/" % pk_url_quote(getattr(result, self.pk_attname))
 
 def change_list(request, app_label, model_name):
     model = models.get_model(app_label, model_name)
