Commits

Jan-Jaap Driessen committed d76cbd0

Merged the ticket #76 and pull request #1 changes into a feature branch.

Comments (0)

Files changed (4)

     If a slot is required, it must be filled in by passing an extra
     dictionary parameter to the ``.need`` method, containing a mapping
     from the required :py:class:`Slot` to :py:class:`Resource`.
-    
+
     When a slot is filled, the resource filled in should have
     the same dependencies as the slot, or a subset of the dependencies
     of the slot. It should also have the same extension as the slot.
     If this is not the case, it is an error.
     """
-    
+
 class Library(object):
     """The resource library.
 
         self._library_deps = set()
         self.known_resources = {}
         self.library_nr = None
-        
+
     def __repr__(self):
         return "<Library '%s' at '%s'>" % (self.name, self.path)
 
                     'Library cycle detected in resource %s' % resource)
 
     def register(self, resource):
-        """Register a Resource with this Library. 
+        """Register a Resource with this Library.
 
         A Resource knows about its Library. After a Resource has registered
-        itself with its Library, the Library knows about the Resources 
-        associated to it. 
+        itself with its Library, the Library knows about the Resources
+        associated to it.
         """
         if resource.relpath in self.known_resources:
             raise ConfigurationError(
 
     :param depends: optionally, a list of resources that this resource
       depends on. Entries in the list are :py:class:`Resource`
-      instances. 
+      instances.
 
     :param supersedes: optionally, a list of :py:class:`Resource`
       instances that this resource supersedes as a rollup
             elif isinstance(argument, basestring):
                 mode_resource = Resource(library, argument)
             else:
-                # The dependencies of a mode resource should be the same 
+                # The dependencies of a mode resource should be the same
                 # or a subset of the dependencies this mode replaces.
                 if len(argument.depends - self.depends) > 0:
                     raise ModeResourceDependencyError
             dependency_nr = max(depend.dependency_nr + 1,
                                 dependency_nr)
         self.dependency_nr = dependency_nr
-   
+
     def render(self, library_url):
         return self.renderer('%s/%s' % (library_url, self.relpath))
 
     real resource by the application when you ``.need()`` that
     resource (or when you need something that depends on the slot
     indirectly).
-  
+
     :param library: the :py:class:`Library` this slot is in.
 
     :param ext: the extension of the slot, for instance '.js'. This
       required to be filled in when a resource that depends on a slot
       is needed, or whether it's optional. By default filling in a
       slot is required.
-      
+
     :param depends: optionally, a list of resources that this slot
       depends on. Resources that are slotted in here need to have
       the same dependencies as that of the slot, or a strict subset.
         assert extension.startswith('.')
         self.ext = extension
         self.required = required
-        
+
         assert not isinstance(depends, basestring)
         self.depends = set()
         if depends is not None:
             dependency_nr = max(depend.dependency_nr + 1,
                                 dependency_nr)
         self.dependency_nr = dependency_nr
-        
+
 class FilledSlot(Renderable, Dependable):
     def __init__(self, slot, resource):
         self.library = resource.library
             raise SlotError(
                 "slot filled in with resource that has dependencies that "
                 "are not a strict subset of dependencies of slot")
-        
+
         # XXX how do slots interact with rollups?
 
     def render(self, library_url):
         except KeyError:
             # fall back on the default mode if mode not found
             return self
-        
+
 class Group(Dependable):
     """A resource used to group resources together.
 
 
     :param base_url: This URL will be prefixed in front of all resource
       URLs. This can be useful if your web framework wants the resources
-      to be published on a sub-URL. Note that this can also be set
-      with the set_base_url method on a ``NeededResources`` instance.
+      to be published on a sub-URL. By default, there is no ``base_url``,
+      and resources are served in the script root. Note that this can
+      also be set with the set_base_url method on a ``NeededResources``
+      instance.
 
     :param publisher_signature: The name under which resource libraries
       should be served in the URL. By default this is ``fanstatic``, so
                  debug=False,
                  rollup=False,
                  base_url=None,
+                 script_name=None,
                  publisher_signature=DEFAULT_SIGNATURE,
                  bundle=False,
                  resources=None,
         self._bottom = bottom
         self._force_bottom = force_bottom
         self._base_url = base_url
+        self._script_name = script_name
         self._publisher_signature = publisher_signature
         self._rollup = rollup
         self._bundle = bundle
         slots = slots or {}
         self._resources.add(resource)
         self._slots.update(slots)
-        
+
     def resources(self):
         """Retrieve the list of resources needed.
 
                                 resource)
             result.add(FilledSlot(resource, fill_resource))
         return result
-            
+
     def clear(self):
         # Clear out any resources "needed" thusfar.
         # XXX or should we rather revert to the list with resources
 
         :param library: A :py:class:`Library` instance.
         """
-        path = [self._base_url or '']
+        start = self._base_url or ''
+        if self._script_name:
+            # script_name typically starts with a '/', so we
+            # want to consume it early in order to avoid double forward slashes
+            # when joining the path segments later.
+            start += self._script_name
+
+        path = [start]
         if self._publisher_signature:
             path.append(self._publisher_signature)
         path.append(library.name)
             # nothing to supersede resource so use it directly
             result.append(resource)
     return result
-    
+
 def sort_resources(resources):
     """Sort resources for inclusion on web page.
 
     """
     for resource in resources:
         resource.library.init_library_nr()
-        
+
     def key(resource):
         return (
             resource.order,

fanstatic/injector.py

         # XXX this will set the needed on the thread local data, even
         # if the wrapped framework only gets the needed from the WSGI
         # environ.
-        needed = fanstatic.init_needed(**self.config)
+        needed = fanstatic.init_needed(script_name=request.environ.get('SCRIPT_NAME'),
+                                       **self.config
+                                       )
 
         # Make sure the needed resource object is put in the WSGI
         # environment as well, for frameworks that choose to use it

fanstatic/test_core.py

     # Can not use the same relpath for two Resource declarations.
     with pytest.raises(ConfigurationError):
         x2 = Resource(foo, 'a.js')
-    
+
 
 def test_group_resource():
     foo = Library('foo', '')
     c = Resource(foo, 'c.js', depends=[g])
     g2 = Group([g])
     g3 = Group([g, g2])
-    
+
     assert c.depends == set([a, b])
     assert g2.depends == set([a, b])
     assert g3.depends == set([a, b])
-    
+
     needed = NeededResources()
     needed.need(c)
     assert needed.resources() == [a, b, c]
-    
+
 def test_redundant_resource():
     foo = Library('foo', '')
     x1 = Resource(foo, 'a.js')
     needed = NeededResources(resources=[h1, h2], rollup=True, debug=True)
     # no mode available for rollup, use the rollup.
     assert needed.resources() == [gianth]
-     
+
 
 def test_rendering():
     foo = Library('foo', '')
     assert (needed.library_url(foo) ==
             'http://example.com/something/fanstatic/foo')
 
+def test_library_url_script_name():
+    foo = Library('foo', '')
+    needed = NeededResources(script_name='/root')
+    assert needed.library_url(foo) == '/root/fanstatic/foo'
+
+def test_library_url_script_name_base_url():
+    foo = Library('foo', '')
+    needed = NeededResources(
+        base_url='http://static.example.com', script_name='/root')
+    assert needed.library_url(foo) == \
+        'http://static.example.com/root/fanstatic/foo'
 
 def test_library_url_version_hashing(tmpdir):
     foo = Library('foo', tmpdir.strpath)
     X.init_library_nr()
     Y.init_library_nr()
     Z.init_library_nr()
-                              
+
     assert a.library.library_nr == 0
     assert c.library.library_nr == 0
     assert b.library.library_nr == 1
     lib2 = Library('lib2', '')
     lib3 = Library('lib3', '')
     lib4 = Library('lib4', '')
-    
+
     js1 = Resource(lib1, 'js1.js')
-    js2 = Resource(lib2, 'js2.js', depends=[js1]) 
+    js2 = Resource(lib2, 'js2.js', depends=[js1])
     js3 = Resource(lib3, 'js3.js', depends=[js2])
-    
+
     style1 = Resource(lib3, 'style1.css')
     style2 = Resource(lib4, 'style2.css', depends=[style1])
 
     obviel_lib = Library('obviel', '')
     bread_lib = Library('bread', '')
     app_lib = Library('app', '')
-    
+
     jquery = Resource(jquery_lib, 'jquery.js')
     jqueryui = Resource(jqueryui_lib, 'jqueryui.js', depends=[jquery])
 
     vtab = Resource(bread_lib, 'vtab.js', depends=[jqueryui])
 
     tabview = Resource(bread_lib, 'tabview.js', depends=[obviel, vtab])
-    
+
     bread = Resource(bread_lib, 'bread.js', depends=[tabview, obviel_forms])
 
     app = Resource(app_lib, 'app.js', depends=[bread, obviel_datepicker])
-    
+
     needed = NeededResources()
 
     needed.need(app)
         print resource, resource.library.library_nr
     assert resources == [jquery, jqueryui, obviel, obviel_forms,
                          obviel_datepicker, vtab, tabview, bread, app]
-    
+
 
     #assert resources == [obviel, forms, forms_autocomplete, tabview, bread,
     #                     zorgdas]
-    
+
 # XXX tests for hashed resources when this is enabled. Needs some plausible
 # directory to test for hashes
 

fanstatic/test_wsgi.py

     wrapped_app = Fanstatic(app, base_url='http://testapp')
 
     request = webob.Request.blank('/')
+    request.environ['SCRIPT_NAME'] = '/root' # base_url is defined so SCRIPT_NAME
+                                             # shouldn't be taken into account
     response = request.get_response(wrapped_app)
     assert response.body == '''\
 <html><head>
 <script type="text/javascript" src="http://testapp/fanstatic/foo/c.js"></script>
 </head><body</body></html>'''
 
+def test_inject_script_name():
+    foo = Library('foo', '')
+    x1 = Resource(foo, 'a.js')
+    x2 = Resource(foo, 'b.css')
+    y1 = Resource(foo, 'c.js', depends=[x1, x2])
+
+    def app(environ, start_response):
+        start_response('200 OK', [])
+        needed = get_needed()
+        needed.need(y1)
+        return ['<html><head></head><body</body></html>']
+
+    wrapped_app = Fanstatic(app)
+
+    request = webob.Request.blank('/path')
+    request.environ['SCRIPT_NAME'] = '/root'
+    response = request.get_response(wrapped_app)
+    assert response.body == '''\
+<html><head>
+    <link rel="stylesheet" type="text/css" href="/root/fanstatic/foo/b.css" />
+<script type="text/javascript" src="/root/fanstatic/foo/a.js"></script>
+<script type="text/javascript" src="/root/fanstatic/foo/c.js"></script>
+</head><body</body></html>'''
 
 def test_incorrect_configuration_options():
     app = None