How do I grab variables from arbitrary URLs?

Example using IPublishTraverse? customizations

There was a discussion on using this technique at http://www.nabble.com/Specialized-URL-traversal..-Best-way--t824862.html on the Zope 3 users mailing list:

from zope.interface import implements
from zope.publisher.interfaces import IPublishTraverse
from zope.app.publisher.browser import BrowserView
from zope.app import zapi

class UrlDispatcher(BrowserView):
    implements(IPublishTraverse)
    def __init__(self, context, request):
        self.context = context
        self.request = request
        self.traverse_subpath = []

    def publishTraverse(self, request, name):
        self.traverse_subpath.append(name)
        return self

    def __call__(self):
        """Call another view"""

        # Could also match via regexs like django
        if 'view1' in self.traverse_subpath:
            return zapi.getMultiAdapter((self.context, self.request), name="view1")()
        elif self.traverse_subpath[-1] == 'someValue':
            # Random untested example:
            self.request['subpaths'] = self.traverse_subpath[:-1]
            return zapi.getMultiAdapter((self.context, self.request), name="view2")()

class TheFirstView(BrowserView):
    def __init__(self, context, request ):
        self.context = context
        self.request = request


class TheSecondView(BrowserView):
    def __init__(self, context, request ):
        self.context = context
        self.request = request
        self.subpaths = self.request['subpaths']
        # may want to look up DB based on contents of subpaths

###
<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:browser="http://namespaces.zope.org/browser"
    i18n_domain="travtest"
    >
  <browser:page
      for="*"
      name="root"
      class=".travtest.UrlDispatcher"
      permission="zope.Public"
      />
  <browser:page
      for="*"
      name="view1"
      class=".travtest.TheFirstView"
      template="./first.html"
      permission="zope.Public"
      />
  <browser:page
      for="*"
      name="view2"
      class=".travtest.TheSecondView"
      template="./second.html"
      permission="zope.Public"
      />
</configure>
###

The example uses __init__ to set up an empty list publishTraverse to add each url portion to the list and __call__ to dispatch to the appropriate browser view.

URL examples using Grok

The Grokstar application grabs variables from URLs?, see traverse.py and calendary.py in http://svn.zope.org/Grokstar/trunk/src/grokstar/ for an example. Grokstar breaks a URL such as /grokstar/2007/02/11 into a series of Year, Month and Day models that have corresponding views for each model using a custom Traverser class that sub-classes from a grok.Traverser class, which in turn implements the zope.publisher.interfaces.browser.IBrowserPublisher? interface.

Another possible approach to URL traversal using Grok is to provide a dateindex view for your collection, and then fiddle with the traversal stack, stuffing arguments that are embedded in the traversal URL into the request.form dictionary. For example, a human-friendly URL such as /myblogentries/2007/01/20 can be thought of as shorthand for a more traditional Zope 3 URL such as /myblogentries/@@dateindex?year:int=2007&month=:int=01&day:int=20.

A possible date range traverser based on this assumption might look like this:

class YearBasedDateTraverser(grok.Traverser):
    grok.context(Blog)

    def traverse(self, name):

        try:
            self.request.form['year'] = int(name)
            stack = self.request.getTraversalStack()
            if stack:
                month = stack.pop()
                self.request.form['month'] = int(month)
                self.request._traversed_names.append(month)
            if stack:
                day = stack.pop()
                self.request.form['day'] = int(day)
                self.request._traversed_names.append(day)
            self.request.setTraversalStack([])

                        # throw a 404 if we are still seeing more parts to the URL
            if stack:
                raise NotFound(self.context, name, self.request)

            # return a BlogDateRange view
            return component.queryMultiAdapter((self.context, self.request), name='dateindex')
        except ValueError:
            return self.entry_traverse(name)

    def entry_traverse(self, name):
        return self.context['entries'].get(name, None)

Note that this requires fiddling with _traversed_names, which may not be entirely safe or proper? It seems to work, at least request/URL does the right thing ...

Then a BlogDateRange? view might then look like this:

class BlogDateRange(grok.View):
    grok.name('dateindex')
    grok.context(Blog)

    # update is called once before the view is rendered
    def update(self):
        "make the form data more convienent to access in the view"
        form = self.request.form
        self.year = form.get('year', 0)
        self.month = form.get('month', 0)
        self.day = form.get('day', 0)

    def entries(self):
        from_, until = extractDateRange( int(self.year),
                                         int(self.month),
                                         int(self.day),
                                       )
        return entriesInDateRange(from_, until)

    def url(self, obj=None, name=None):
        "customize Grok view.url() behaviour"
        if not obj and not name:
            url = component.getMultiAdapter((self.context, self.request), IAbsoluteURL)()
            if self.year:
                url += '/' + self.year
            if self.month:
                url += '/' + self.month
            if self.day:
                url += '/' + self.day
            return url
        else:
            return super(BlogDateRange, self).url( obj=obj, name=name )

With the work of extracting a date range from a year, and/or month, and/or day factored out into it's own function, as well as the work of searching for entries within the date range:

def extractDateRange(year, month, day):
    """
    Extract a (from, until) date range given a year,
    and optionally a month and optionally a day
    """

    from_, until = None, None

    # year range, e.g. /blogcontainer/2007/
    if not month:
        from_ = datetime(year, 1, 1)
        until = datetime(year + 1, 1, 1)

    # month range, e.g. /blogcontainer/2007/02/
    elif not day:
        from_ = datetime(year,
                         month,
                         1)
        month = month + 1
        if month > 12:
            month = 1
            year += 1
        until = datetime(year, month, 1)

    # day range, e.g. /blogcontainer/2007/02/11/
    else:
        from_ = datetime(year,
                         month,
                         day)
        until = from_ + timedelta(days=1)

    return from_, until

def entriesInDateRange(from_, until):
    entries = Query().searchResults(
        query.And(query.Between(('entry_catalog', 'published'), from_, until),
                  query.Eq(('entry_catalog', 'workflow_state'), PUBLISHED)))

    return sorted(
        entries, key=lambda entry: entry.published, reverse=True
        )

.



( 97 subscribers )