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:
- Give an URL to pages (i.e a components state)
- 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:
and after a click on the Time Zone link:
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:
- self -- the root object
- url -- a tuple of the unicode parts of the URL path (starting after the application name)
- comp -- the component wrapping the root object
- http_method -- the HTTP method (GET, POST, PUT ...)
- 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:
and the navigation between the tabs and the time/timezone views is totally operational:
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:
- url -- the rest of the URL parts
- http_method -- the HTTP method (GET, POST, PUT ...)
- 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.