Index: django/http/__init__.py
===================================================================
--- django/http/__init__.py	(revision 6315)
+++ django/http/__init__.py	(working copy)
@@ -26,6 +26,7 @@
     def __init__(self):
         self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {}
         self.path = ''
+        self.full_path = ''
         self.method = None
 
     def __repr__(self):
@@ -71,7 +72,7 @@
             location = self.get_full_path()
         if not ':' in location:
             current_uri = '%s://%s%s' % (self.is_secure() and 'https' or 'http',
-                                         self.get_host(), self.path)
+                                         self.get_host(), self.full_path)
             location = urljoin(current_uri, location)
         return location
 
Index: django/test/client.py
===================================================================
--- django/test/client.py	(revision 6315)
+++ django/test/client.py	(working copy)
@@ -151,7 +151,7 @@
             'PATH_INFO':         '/',
             'QUERY_STRING':      '',
             'REQUEST_METHOD':    'GET',
-            'SCRIPT_NAME':       None,
+            'SCRIPT_NAME':       '',
             'SERVER_NAME':       'testserver',
             'SERVER_PORT':       80,
             'SERVER_PROTOCOL':   'HTTP/1.1',
Index: django/core/handlers/wsgi.py
===================================================================
--- django/core/handlers/wsgi.py	(revision 6315)
+++ django/core/handlers/wsgi.py	(working copy)
@@ -75,8 +75,12 @@
 class WSGIRequest(http.HttpRequest):
     def __init__(self, environ):
         self.environ = environ
-        self.path = force_unicode(environ['PATH_INFO'])
+        self.path = force_unicode(environ.get('PATH_INFO', '/'))
+        self.full_path = (force_unicode(environ.get('SCRIPT_NAME', ''))
+                          + force_unicode(environ.get('PATH_INFO', '/')))
         self.META = environ
+        self.META['PATH_INFO'] = self.path
+        self.META['SCRIPT_NAME'] = force_unicode(environ.get('SCRIPT_NAME', ''))
         self.method = environ['REQUEST_METHOD'].upper()
 
     def __repr__(self):
@@ -102,7 +106,7 @@
             (get, post, cookies, meta)
 
     def get_full_path(self):
-        return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + self.environ.get('QUERY_STRING', '')) or '')
+        return '%s%s' % (self.full_path, self.environ.get('QUERY_STRING', '') and ('?' + self.environ.get('QUERY_STRING', '')) or '')
 
     def is_secure(self):
         return 'HTTPS' in self.environ and self.environ['HTTPS'] == 'on'
Index: django/core/handlers/modpython.py
===================================================================
--- django/core/handlers/modpython.py	(revision 6315)
+++ django/core/handlers/modpython.py	(working copy)
@@ -14,7 +14,13 @@
 class ModPythonRequest(http.HttpRequest):
     def __init__(self, req):
         self._req = req
-        self.path = force_unicode(req.uri)
+        self.full_path = force_unicode(req.uri)
+        root = req.get_options().get('django.root', '')
+        self._django_root = force_unicode(root)
+        if root and req.uri.startswith(root):
+            self.path = force_unicode(req.uri[len(root):])
+        else:
+            self.path = self.full_path
 
     def __repr__(self):
         # Since this is called as part of error handling, we need to be very
@@ -39,7 +45,7 @@
             (self.path, get, post, cookies, meta)
 
     def get_full_path(self):
-        return '%s%s' % (self.path, self._req.args and ('?' + self._req.args) or '')
+        return '%s%s' % (self.full_path, self._req.args and ('?' + self._req.args) or '')
 
     def is_secure(self):
         # Note: modpython 3.2.10+ has req.is_https(), but we need to support previous versions
@@ -94,7 +100,7 @@
                 'CONTENT_LENGTH':    self._req.clength, # This may be wrong
                 'CONTENT_TYPE':      self._req.content_type, # This may be wrong
                 'GATEWAY_INTERFACE': 'CGI/1.1',
-                'PATH_INFO':         self._req.path_info,
+                'PATH_INFO':         self.path,
                 'PATH_TRANSLATED':   None, # Not supported
                 'QUERY_STRING':      self._req.args,
                 'REMOTE_ADDR':       self._req.connection.remote_ip,
@@ -102,7 +108,7 @@
                 'REMOTE_IDENT':      self._req.connection.remote_logname,
                 'REMOTE_USER':       self._req.user,
                 'REQUEST_METHOD':    self._req.method,
-                'SCRIPT_NAME':       None, # Not supported
+                'SCRIPT_NAME':       self._django_root,
                 'SERVER_NAME':       self._req.server.server_hostname,
                 'SERVER_PORT':       self._req.server.port,
                 'SERVER_PROTOCOL':   self._req.protocol,
Index: django/views/generic/create_update.py
===================================================================
--- django/views/generic/create_update.py	(revision 6315)
+++ django/views/generic/create_update.py	(working copy)
@@ -21,7 +21,7 @@
     """
     if extra_context is None: extra_context = {}
     if login_required and not request.user.is_authenticated():
-        return redirect_to_login(request.path)
+        return redirect_to_login(request.full_path)
 
     manipulator = model.AddManipulator(follow=follow)
     if request.POST:
@@ -87,7 +87,7 @@
     """
     if extra_context is None: extra_context = {}
     if login_required and not request.user.is_authenticated():
-        return redirect_to_login(request.path)
+        return redirect_to_login(request.full_path)
 
     # Look up the object to be edited
     lookup_kwargs = {}
@@ -163,7 +163,7 @@
     """
     if extra_context is None: extra_context = {}
     if login_required and not request.user.is_authenticated():
-        return redirect_to_login(request.path)
+        return redirect_to_login(request.full_path)
 
     # Look up the object to be edited
     lookup_kwargs = {}
Index: django/views/debug.py
===================================================================
--- django/views/debug.py	(revision 6315)
+++ django/views/debug.py	(working copy)
@@ -345,7 +345,7 @@
     </tr>
     <tr>
       <th>Request URL:</th>
-      <td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path|escape }}</td>
+      <td>{{ request_protocol }}://{{ request.get_host }}{{ request.full_path|escape }}</td>
     </tr>
     <tr>
       <th>Exception Type:</th>
@@ -634,7 +634,7 @@
       </tr>
       <tr>
         <th>Request URL:</th>
-      <td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path|escape }}</td>
+      <td>{{ request_protocol }}://{{ request.get_host }}{{ request.full_path|escape }}</td>
       </tr>
     </table>
   </div>
Index: django/contrib/syndication/feeds.py
===================================================================
--- django/contrib/syndication/feeds.py	(revision 6315)
+++ django/contrib/syndication/feeds.py	(working copy)
@@ -25,7 +25,7 @@
     def __init__(self, slug, request):
         self.slug = slug
         self.request = request
-        self.feed_url = request.path
+        self.feed_url = request.full_path
         self.title_template_name = self.title_template or ('feeds/%s_title.html' % slug)
         self.description_template_name = self.description_template or ('feeds/%s_description.html' % slug)
 
Index: django/contrib/comments/views/userflags.py
===================================================================
--- django/contrib/comments/views/userflags.py	(revision 6315)
+++ django/contrib/comments/views/userflags.py	(working copy)
@@ -19,7 +19,7 @@
     comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID)
     if request.POST:
         UserFlag.objects.flag(comment, request.user)
-        return HttpResponseRedirect('%sdone/' % request.path)
+        return HttpResponseRedirect('%sdone/' % request.full_path)
     return render_to_response('comments/flag_verify.html', {'comment': comment},
         context_instance=RequestContext(request, extra_context, context_processors))
 flag = login_required(flag)
@@ -50,7 +50,7 @@
             comment.save()
             m = ModeratorDeletion(None, request.user.id, comment.id, None)
             m.save()
-        return HttpResponseRedirect('%sdone/' % request.path)
+        return HttpResponseRedirect('%sdone/' % request.full_path)
     return render_to_response('comments/delete_verify.html', {'comment': comment},
         context_instance=RequestContext(request, extra_context, context_processors))
 delete = login_required(delete)
Index: django/contrib/admin/views/auth.py
===================================================================
--- django/contrib/admin/views/auth.py	(revision 6315)
+++ django/contrib/admin/views/auth.py	(working copy)
@@ -20,7 +20,7 @@
             msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user}
             if "_addanother" in request.POST:
                 request.user.message_set.create(message=msg)
-                return HttpResponseRedirect(request.path)
+                return HttpResponseRedirect(request.full_path)
             else:
                 request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
                 return HttpResponseRedirect('../%s/' % new_user.id)
Index: django/contrib/admin/views/main.py
===================================================================
--- django/contrib/admin/views/main.py	(revision 6315)
+++ django/contrib/admin/views/main.py	(working copy)
@@ -276,7 +276,7 @@
                     (pk_value, force_unicode(new_object).replace('"', '\\"')))
             elif "_addanother" in request.POST:
                 request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
-                return HttpResponseRedirect(request.path)
+                return HttpResponseRedirect(request.full_path)
             else:
                 request.user.message_set.create(message=msg)
                 return HttpResponseRedirect(post_url)
@@ -353,9 +353,9 @@
             if "_continue" in request.POST:
                 request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
                 if '_popup' in request.REQUEST:
-                    return HttpResponseRedirect(request.path + "?_popup=1")
+                    return HttpResponseRedirect(request.full_path + "?_popup=1")
                 else:
-                    return HttpResponseRedirect(request.path)
+                    return HttpResponseRedirect(request.full_path)
             elif "_saveasnew" in request.POST:
                 request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)})
                 return HttpResponseRedirect("../%s/" % pk_value)
@@ -778,7 +778,7 @@
         # is screwed up with the database, so display an error page.
         if ERROR_FLAG in request.GET.keys():
             return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
-        return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
+        return HttpResponseRedirect(request.full_path + '?' + ERROR_FLAG + '=1')
     c = template.RequestContext(request, {
         'title': cl.title,
         'is_popup': cl.is_popup,
Index: django/contrib/admin/views/decorators.py
===================================================================
--- django/contrib/admin/views/decorators.py	(revision 6315)
+++ django/contrib/admin/views/decorators.py	(working copy)
@@ -22,7 +22,7 @@
         post_data = _encode_post_data({})
     return render_to_response('admin/login.html', {
         'title': _('Log in'),
-        'app_path': request.path,
+        'app_path': request.full_path,
         'post_data': post_data,
         'error_message': error_message
     }, context_instance=template.RequestContext(request))
@@ -99,7 +99,7 @@
                         return view_func(request, *args, **kwargs)
                     else:
                         request.session.delete_test_cookie()
-                        return http.HttpResponseRedirect(request.path)
+                        return http.HttpResponseRedirect(request.full_path)
             else:
                 return _display_login_form(request, ERROR_MESSAGE)
 
Index: django/contrib/admin/views/doc.py
===================================================================
--- django/contrib/admin/views/doc.py	(revision 6315)
+++ django/contrib/admin/views/doc.py	(working copy)
@@ -27,7 +27,7 @@
 
 def bookmarklets(request):
     # Hack! This couples this view to the URL it lives at.
-    admin_root = request.path[:-len('doc/bookmarklets/')]
+    admin_root = request.full_path[:-len('doc/bookmarklets/')]
     return render_to_response('admin_doc/bookmarklets.html', {
         'admin_url': "%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root),
     }, context_instance=RequestContext(request))
Index: django/contrib/databrowse/plugins/objects.py
===================================================================
--- django/contrib/databrowse/plugins/objects.py	(revision 6315)
+++ django/contrib/databrowse/plugins/objects.py	(working copy)
@@ -8,7 +8,7 @@
     def model_view(self, request, model_databrowse, url):
         # If the object ID wasn't provided, redirect to the model page, which is one level up.
         if url is None:
-            return http.HttpResponseRedirect(urlparse.urljoin(request.path, '../'))
+            return http.HttpResponseRedirect(urlparse.urljoin(request.full_path, '../'))
         easy_model = EasyModel(model_databrowse.site, model_databrowse.model)
         obj = easy_model.object_by_pk(url)
         return render_to_response('databrowse/object_detail.html', {'object': obj, 'root_url': model_databrowse.site.root_url})
Index: django/contrib/databrowse/sites.py
===================================================================
--- django/contrib/databrowse/sites.py	(revision 6315)
+++ django/contrib/databrowse/sites.py	(working copy)
@@ -110,7 +110,7 @@
 
         `url` is the remainder of the URL -- e.g. 'comments/comment/'.
         """
-        self.root_url = request.path[:len(request.path) - len(url)]
+        self.root_url = request.full_path[:len(request.full_path) - len(url)]
         url = url.rstrip('/') # Trim trailing slash, if it exists.
 
         if url == '':
Index: django/contrib/auth/views.py
===================================================================
--- django/contrib/auth/views.py	(revision 6315)
+++ django/contrib/auth/views.py	(working copy)
@@ -47,7 +47,7 @@
         return render_to_response(template_name, {'title': _('Logged out')}, context_instance=RequestContext(request))
     else:
         # Redirect to this page until the session has been cleared.
-        return HttpResponseRedirect(next_page or request.path)
+        return HttpResponseRedirect(next_page or request.full_path)
 
 def logout_then_login(request, login_url=None):
     "Logs out the user if he is logged in. Then redirects to the log-in page."
@@ -75,7 +75,7 @@
                 form.save(domain_override=request.META['HTTP_HOST'])
             else:
                 form.save(email_template_name=email_template_name)
-            return HttpResponseRedirect('%sdone/' % request.path)
+            return HttpResponseRedirect('%sdone/' % request.full_path)
     return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)},
         context_instance=RequestContext(request))
 
@@ -90,7 +90,7 @@
         errors = form.get_validation_errors(new_data)
         if not errors:
             form.save(new_data)
-            return HttpResponseRedirect('%sdone/' % request.path)
+            return HttpResponseRedirect('%sdone/' % request.full_path)
     return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)},
         context_instance=RequestContext(request))
 password_change = login_required(password_change)
Index: django/contrib/flatpages/views.py
===================================================================
--- django/contrib/flatpages/views.py	(revision 6315)
+++ django/contrib/flatpages/views.py	(working copy)
@@ -25,7 +25,7 @@
     # logged in, redirect to the login page.
     if f.registration_required and not request.user.is_authenticated():
         from django.contrib.auth.views import redirect_to_login
-        return redirect_to_login(request.path)
+        return redirect_to_login(request.full_path)
     if f.template_name:
         t = loader.select_template((f.template_name, DEFAULT_TEMPLATE))
     else:
Index: django/middleware/common.py
===================================================================
--- django/middleware/common.py	(revision 6315)
+++ django/middleware/common.py	(working copy)
@@ -33,7 +33,7 @@
 
         # Check for a redirect based on settings.APPEND_SLASH and settings.PREPEND_WWW
         host = request.get_host()
-        old_url = [host, request.path]
+        old_url = [host, request.full_path]
         new_url = old_url[:]
         if settings.PREPEND_WWW and old_url[0] and not old_url[0].startswith('www.'):
             new_url[0] = 'www.' + old_url[0]
Index: AUTHORS
===================================================================
--- AUTHORS	(revision 6315)
+++ AUTHORS	(working copy)
@@ -319,6 +319,7 @@
     ymasuda@ethercube.com
     Jarek Zgoda <jarek.zgoda@gmail.com>
     Cheng Zhang
+    John Melesky
 
 A big THANK YOU goes to:
 
Index: docs/request_response.txt
===================================================================
--- docs/request_response.txt	(revision 6315)
+++ docs/request_response.txt	(working copy)
@@ -24,6 +24,15 @@
 All attributes except ``session`` should be considered read-only.
 
 ``path``
+    A string representing the path to the requested page relative to
+    the Django root, not including the domain. For WSGI servers, this
+    means it does not include SCRIPT_NAME. For mod_python servers,
+    this does not include the user-configurable django.root
+    PythonOption.
+
+    Example: ``"/bands/the_beatles/"``
+
+``full_path``
     A string representing the full path to the requested page, not including
     the domain.
 
@@ -112,6 +121,7 @@
         * ``REQUEST_METHOD`` -- A string such as ``"GET"`` or ``"POST"``.
         * ``SERVER_NAME`` -- The hostname of the server.
         * ``SERVER_PORT`` -- The port of the server.
+        * ``SCRIPT_NAME`` -- The location of the Django app (e.g. "/mysite")
 
 ``user``
     A ``django.contrib.auth.models.User`` object representing the currently
@@ -157,7 +167,7 @@
    ``request.POST`` has the given key.
 
 ``get_full_path()``
-   Returns the ``path``, plus an appended query string, if applicable.
+   Returns the ``full_path``, plus an appended query string, if applicable.
 
    Example: ``"/music/bands/the_beatles/?print=true"``
 
Index: docs/modpython.txt
===================================================================
--- docs/modpython.txt	(revision 6315)
+++ docs/modpython.txt	(working copy)
@@ -36,6 +36,7 @@
         PythonHandler django.core.handlers.modpython
         SetEnv DJANGO_SETTINGS_MODULE mysite.settings
         PythonDebug On
+        PythonOption django.root /mysite
     </Location>
 
 ...and replace ``mysite.settings`` with the Python import path to your Django
@@ -45,6 +46,11 @@
 Django mod_python handler." It passes the value of ``DJANGO_SETTINGS_MODULE``
 so mod_python knows which settings to use.
 
+The PythonOption tells Django: "Pass all requests through to the application as
+if they were rooted at '/', but generate absolute URLs under '/mysite'." This
+way, Django applications can be installed in any subdirectory URL without
+having the full path hardwired in. 
+
 Note that we're using the ``<Location>`` directive, not the ``<Directory>``
 directive. The latter is used for pointing at places on your filesystem,
 whereas ``<Location>`` points at places in the URL structure of a Web site.
@@ -60,6 +66,7 @@
         PythonHandler django.core.handlers.modpython
         SetEnv DJANGO_SETTINGS_MODULE mysite.settings
         PythonDebug On
+        PythonOption django.root /mysite
         **PythonPath "['/path/to/project'] + sys.path"**
     </Location>
 
