wiki:RestfulUrl

Significative "RESTful" URLs

Nagare doesn't force you to explicitly map URLs to controllers. With Nagare you can develop your application and run it without them and, afterwards, set significative URL only for the resources you want to be able to bookmark.

Principle

An URL representes a well-defined state of the application components. The mapping of significative URL to the components state (the URL scheme) is made in 2 steps:

  1. Give an URL to pages (i.e a components state)
  2. When an URL is received, initialize the components state from it

To illustrate these steps, we will use a timezone-aware clock example. First, install the require pytz python module:

<NAGARE_HOME>/bin/easy_install pytz

Then we can define our Clock component with two associated views, the default view which displays the local time and a view called timezone to display its timezone offset:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 import datetime
 import pytz
 from nagare import presentation, component

 class Clock(object):
     # A ``Clock`` object is initialized with a timezone name
     def __init__(self, timezone):
         self.tz = pytz.timezone(timezone) # Create a timezone object


 # The default view of a ``Clock`` object displays the local time
 @presentation.render_for(Clock)
 def render(self, h, comp, *args):
     # Retrieve the current UTC time, convert it to local time and display it
     dt = datetime.datetime.now(pytz.utc).astimezone(self.tz)
     h << h.div(dt.strftime('%H:%M'), style='font-size: 150%')

     # When the ``Time Zone`` link is clicked, change to the ``timezone`` view of this object
     h << h.a('Time Zone').action(lambda: comp.becomes(self, model='timezone'))

     return h.root

 # The ``timezone`` view of a ``Clock`` object displays its timezone offset
 @presentation.render_for(Clock, model='timezone')
 def render(self, h, comp, *args):
     # Retrieve the current UTC time, convert it to local time and display its timezone
     dt = datetime.datetime.now(pytz.utc).astimezone(self.tz)
     h << h.div(dt.strftime('%z %Z'), style='font-size: 150%')

     # When the ``Time`` link is clicked, change back to the default view of this object
     h << h.a('Time').action(lambda: comp.becomes(self, model=None))

     return h.root

To test this component, we define a factory:

def test1():
    return Clock('America/New_York')

and then launch it with:

<NAGARE_HOME>/bin/nagare-admin module-serve clock.py:test1 test1

Browsing to http://localhost:8080/test1 gives:

08:15
Time Zone

and after a click on the Time Zone link:

-0400 EDT
Time

The Clock component was developped in straight pure Python, without thinking about a URL scheme. You can see that switching between the Time Zone and Time views don't change the URL path http://localhost:8080/test1/.

1. Adding URL

It's easy to change the URL path when a link is clicked. Just add a normal relative href attribut to the link definition.

In our example, we modify the lines #18 and #30:

...
# When the ``Time zone`` link is clicked, change to the ``timezone`` view of this object
h << h.a('Time Zone', href='timezone').action(lambda: comp.becomes(self, model='timezone'))
...

...
# When the ``Time`` link is clicked, change back to the default view of this object
h << h.a('Time', href='time').action(lambda: comp.becomes(self, model=None))
    ...

Now, when you click on the links, the URL path is changed to http://localhost:8080/test1/time or http://localhost:8080/test1/timezone.

But if you directly enter the URL http://localhost:8080/test1/timezone in your browser, the default view if displayed, not the timezone one. The following chapter describes how to initialize the components state with the received URL.

2. Components initialization

When an URL is received, without a session and a continuation parameters or when the session is expired, Nagare tries to initialized the application components from the URL path by calling the generic method presentation.init_for() on the application root component, with the parameters:

  1. self -- the root object
  2. url -- a tuple of the unicode parts of the URL path (starting after the application name)
  3. comp -- the component wrapping the root object
  4. http_method -- the HTTP method (GET, POST, PUT ...)
  5. request -- the HTTP webob request object

It's up to the developper to implement the generic methods to initialize the root component.

A presentation.HTTPNotFound() exception can be raised to send a 404 Not Found result to the client.

Note

  • If no generic method matches, Nagare automatically raises the presentation.HTTPNotFound() exception
  • Any webob.exc exception can also be raised (HTTPMethodNotAllowed, HTTPForbidden ...)

In our example, we implement 2 conditions:

# If no URL is given or url is 'time', set the default view
@presentation.init_for(Clock, 'not url or url == (u"time",)')
def init(self, url, comp, http_method, request):
    comp.becomes(self, model=None)

# If the url is 'timezone', set the ``timezone`` view
@presentation.init_for(Clock, 'url == (u"timezone",)')
def init(self, url, comp, http_method, request):
    comp.becomes(self, model='timezone')

Note these 2 methods do the exact same actions than when the links are manually clicked.

Note

If you want to set the state of a component as if it was called, don't directly do:

comp.call(o)

but use the component.call_wrapper() function:

from nagare import component

component.call_wrapper(lambda: comp.call(o))

You can now directly enter the http://localhost:8080/test1/time or http://localhost:8080/test1/timezone URL. Of course, you can also bookmark them, send them in an email ...

Components composition

The example is now enhanced with a Clocks component that can display several Clock components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 class Clocks:
     # A ``Clocks`` has several ``Clock`` objects and a ``content`` component
     def __init__(self):
         # The ``Clock`` objects, each associated to a city name
         self.cities = {
                         'New-York' : Clock('America/New_York'),
                         'Paris' : Clock('Europe/Paris'),
                         'Sydney' : Clock('Australia/Sydney')
                       }

         # The ``content`` component, when created, wraps the ``None`` object
         self.content = component.Component(None)

         # The ``change_city()`` method change the ``content`` component to
         # the ``Clock`` object of the city
         self.change_city('New-York')

     def change_city(self, name):
         # The ``content`` component now wraps an other ``Clock`` object,
         # which will be rendered with its default view
         self.content.becomes(self.cities[name], model=None)


 # The default view of the ``Clocks`` objects
 @presentation.render_for(Clocks)
 def render(self, h, *args):
     # A bit of CSS to display a tabbed panel
     h.head.css('clocks_css', '''
         .clocks { display: inline-block }
         .clocks .tab { padding: 0 5px 0 5px; border: 1px solid gray; background-color: #aaa }
         .clocks .selected_tab { padding-top: 5px; border-bottom: none; background-color: #eee }
         .clocks .content { border: 1px solid gray; border-top: none; background-color: #eee; text-align: center; padding: 10px }
     ''')

     with h.span(class_='clocks'):
         # Display all the city names
         for (name, city) in sorted(self.cities.items()):
             if city is self.content():      # ``self.content()`` returns the current wrapped object
                 # The selected city name is no more clickable
                 h << h.span(name, class_='tab selected_tab')
             else:
                 # The others city names, when clicked, invoke the ``change_city()`` method
                 link = h.a(name).action(lambda name=name: self.change_city(name))
                 h << h.span(link, class_='tab')

         # Delegate the rendering to the ``content`` component
         h << h.div(self.content, class_='content')

     return h.root

Once again, this component is developed without taking care about the URL management and can be launched with:

<NAGARE_HOME>/bin/nagare-admin module-serve clock.py:Clocks test2

Browsing to http://localhost:8080/test2 gives:

New-YorkParisSydney
08:15
Time Zone

and the navigation between the tabs and the time/timezone views is totally operational:

New-YorkParisSydney
+0200 CEST
Time

1. Adding URL

A Nagare application is made of a dynamic tree of components. A parent component can fix the URL prefix their children will use for their links, by using the url parameter of:

  • component.Component(o, url='...')
  • comp.becomes(o, url='...')
  • comp.call(o, url='...')

In our example, we decide that the URL of the links generated by the Clock views will began by the name of the city. So we add the parameter url=<city name> line #21 and href=<city_name> line #43:

    ...
    def select_city(self, name):
        self.content.becomes(self.cities[name], model=None, url=name)
    ...

    ...
    # The others city names, when clicked, invoke the ``change_city()`` method
    link = h.a(name, href=name).action(lambda name=name: self.change_city(name))
    ...

Now, the URL path changes to http://localhost:8080/test2/Paris/time or http://localhost:8080/test2/Sydney/timezone ... Where the first part (Paris, Sydney ...) is set by the Clocks component and the last part (time or timezone) is set by the Clock component.

2. Components initialization

First a parent component initializes itself with parts of the received URL. Then it delegates the initialization to their children by calling their init() method with the parameters:

  1. url -- the rest of the URL parts
  2. http_method -- the HTTP method (GET, POST, PUT ...)
  3. request -- the HTTP webob request object

So, we add a presentation.init_for() implementation for the Clocks component that uses the first part of the URL (the city name) to initialize itself. Then we delegate the finalisation of the initialization to the content component, passing it the rest of the URL:

@presentation.init_for(Clocks, 'len(url) >= 1') # The url must have at least 1 part
def init(self, url, comp, http_method, request):
    city = url[0]   # The first part of the url is the city name
    if city not in self.cities: # Check if the city name is valid
        raise presentation.HTTPNotFound() # Else, send back a ``404``

    self.change_city(city)  # Set the ``content`` component

    # The ``content`` component is initialized with the rest of the URL
    self.content.init(url[1:], http_method, request)

RESTful URL

You may have noticed the presentation.init_for() also received the http_method parameter (typically GET, POST, PUT and DELETE) and the full request webob object. In true RESTful style, both these parameters can be used to initialized the components tree.

Note

The HTTP method can also be sent by the client though the _method parameter, using GET or POST requests.