Switched to restresource for resolving URLs
authorAaron Bentley <abentley@panoramicfeedback.com>
Thu, 15 Dec 2005 21:02:37 +0000 (16:02 -0500)
committerAaron Bentley <abentley@panoramicfeedback.com>
Thu, 15 Dec 2005 21:02:37 +0000 (16:02 -0500)
beweb/beweb/controllers.py
beweb/beweb/restresource.py [new file with mode: 0644]
beweb/beweb/templates/bugs.kid
beweb/beweb/templates/projects.kid

index 2a8d0aab9506886d8f8d84ecb1524e76d96349c5..8ba1595f1e323caf29d4aa28201c8361e9fe7254 100644 (file)
@@ -4,6 +4,7 @@ import cherrypy
 from libbe.bugdir import tree_root, cmp_severity
 from libbe import names
 from config import projects
+from restresource import RESTResource
 
 def project_tree(project):
     try:
@@ -11,10 +12,95 @@ def project_tree(project):
     except KeyError:
         raise Exception("Unknown project %s" % project)
 
+def expose_resource(html=None):
+    def exposer(func):
+        func = turbogears.expose(html=html)(func)
+        func.expose_resource = True
+        return func
+    return exposer 
+
+class Bug(RESTResource):
+    @expose_resource(html="beweb.templates.edit_bug")
+    def index(self, bug):
+        return {"bug": bug, "project_id": self.parent}
+    
+    @turbogears.expose(html="beweb.templates.bugs")
+    def list(self, sort_by=None, show_closed=False, action=None):
+        if action == "New bug":
+            self.new_bug()
+        if show_closed == "False":
+            show_closed = False
+        bug_tree = project_tree(self.parent)
+        bugs = list(bug_tree.list())
+        if sort_by is None:
+            def cmp_date(bug1, bug2):
+                return -cmp(bug1.time, bug2.time)
+            bugs.sort(cmp_date)
+            bugs.sort(cmp_severity)
+        return {"project_id"      : self.parent,
+                "project_name"    : projects[self.parent][0],
+                "bugs"            : bugs,
+                "show_closed"     : show_closed,
+               }
+
+    def new_bug(self):
+        bug = self.bug_tree().new_bug()
+        bug.creator = names.creator()
+        bug.severity = "minor"
+        bug.status = "open"
+        bug.save()
+        raise cherrypy.HTTPRedirect(bug_url(self.parent, bug.uuid))
+
+    @expose_resource()
+    def update(self, bug, status, severity, summary, action):
+        bug.status = status
+        bug.severity = severity
+        bug.summary = summary
+        bug.save()
+        raise cherrypy.HTTPRedirect(bug_list_url(self.parent))
+
+    def REST_instantiate(self, bug_uuid):
+        return self.bug_tree().get_bug(bug_uuid)
+
+    def bug_tree(self):
+        return project_tree(self.parent)
+
+def project_url(project_id=None):
+    project_url = "/project/"
+    if project_id is not None:
+        project_url += "%s/" % project_id
+    return turbogears.url(project_url)
+
+def bug_url(project_id, bug_uuid=None):
+    bug_url = "/project/%s/bug/" % project_id
+    if bug_uuid is not None:
+        bug_url += "%s/" % bug_uuid
+    return turbogears.url(bug_url)
+
+def bug_list_url(project_id, show_closed=False):
+    bug_url = "/project/%s/bug/?show_closed=%s" % (project_id, 
+                                                   str(show_closed))
+    return turbogears.url(bug_url)
+
+
+class Project(RESTResource):
+    REST_children = {"bug": Bug()}
+    @expose_resource(html="beweb.templates.projects")
+    def index(self, project_id=None):
+        if project_id is not None:
+            raise cherrypy.HTTPRedirect(bug_url(project_id)) 
+        else:
+            return {"projects": projects}
+
+    def REST_instantiate(self, project_id):
+        return project_id
+
+
 class Root(controllers.Root):
-    @turbogears.expose(html="beweb.templates.projects")
+    project = Project()
+    @turbogears.expose()
     def index(self):
-        return {"projects" : projects}
+        raise cherrypy.HTTPRedirect(project_url()) 
 
     @turbogears.expose()
     def default(self, *args, **kwargs):
diff --git a/beweb/beweb/restresource.py b/beweb/beweb/restresource.py
new file mode 100644 (file)
index 0000000..47db637
--- /dev/null
@@ -0,0 +1,195 @@
+"""
+REST Resource
+
+cherrypy controller mixin to make it easy to build REST applications.
+
+handles nested resources and method-based dispatching.
+
+here's a rough sample of what a controller would look like using this:
+
+cherrypy.root = MainController()
+cherrypy.root.user = UserController()
+
+class PostController(RESTResource):
+    def index(self,post):
+        return post.as_html()
+    index.expose_resource = True
+
+    def delete(self,post):
+        post.destroySelf()
+        return "ok"
+    delete.expose_resource = True
+
+    def update(self,post,title="",body=""):
+        post.title = title
+        post.body = body
+        return "ok"
+    update.expose_resource = True
+
+    def add(self, post, title="", body="")
+        post.title = title
+        post.body = body
+        return "ok"
+    update.expose_resource = True
+
+    def REST_instantiate(self, slug):
+        try:
+            return Post.select(Post.q.slug == slug, Post.q.userID = self.parent.id)[0]
+        except:
+            return None
+
+    def REST_create(self, slug):
+        return Post(slug=slug,user=self.parent)
+
+class UserController(RESTResource):
+    REST_children = {'posts' : PostController()}
+
+    def index(self,user):
+        return user.as_html()
+    index.expose_resource = True
+
+    def delete(self,user):
+        user.destroySelf()
+        return "ok"
+    delete.expose_resource = True
+
+    def update(self,user,fullname="",email=""):
+        user.fullname = fullname
+        user.email = email
+        return "ok"
+    update.expose_resource = True
+
+    def add(self, user, fullname="", email=""):
+        user.fullname = fullname
+        user.email = email
+        return "ok"
+    add.expose_resource = True
+
+    def extra_action(self,user):
+        # do something else
+    extra_action.expose_resource = True
+
+    def REST_instantiate(self, username):
+        try:
+            return User.byUsername(username)
+        except:
+            return None
+
+    def REST_create(self, username):
+        return User(username=username)
+
+then, the site would have urls like:
+
+    /user/bob
+    /user/bob/posts/my-first-post
+    /user/bob/posts/my-second-post
+
+which represent REST resources. calling 'GET /usr/bob' would call the index() method on UserController
+for the user bob. 'PUT /usr/joe' would create a new user with username 'joe'. 'DELETE /usr/joe'
+would delete that user. 'GET /usr/bob/posts/my-first-post' would call index() on the Post Controller
+with the post with the slug 'my-first-post' that is owned by bob.
+
+
+"""
+
+
+import cherrypy
+class RESTResource:
+    # default method mapping. ie, if a GET request is made for
+    # the resource's url, it will try to call an index() method (if it exists);
+    # if a PUT request is made, it will try to call an add() method.
+    # if you prefer other method names, just override these values in your
+    # controller with REST_map
+    REST_defaults = {'DELETE' : 'delete',
+                     'GET' : 'index',
+                     'POST' : 'update',
+                     'PUT' : 'add'}
+    REST_map = {}
+    # if the resource has children resources, list them here. format is
+    # a dictionary of name -> resource mappings. ie,
+    #
+    # REST_children = {'posts' : PostController()}
+
+    REST_children = {}
+
+    def REST_dispatch(self, resource, **params):
+        # if this gets called, we assume that default has already
+        # traversed down the tree to the right location and this is
+        # being called for a raw resource
+        method = cherrypy.request.method
+        if self.REST_map.has_key(method):
+            m = getattr(self,self.REST_map[method])
+            if m and getattr(m, "expose_resource"):
+                return m(resource,**params)
+        else:
+            if self.REST_defaults.has_key(method):
+                m = getattr(self,self.REST_defaults[method])
+                try:
+                    if m and getattr(m, "expose_resource"):
+                        return m(resource,**params)
+                except:
+                    raise
+                    raise Exception("can't find expose_resource on %r", m)
+
+        raise cherrypy.NotFound
+
+    @cherrypy.expose
+    def default(self, *vpath, **params):
+        if not vpath:
+            return self.list(**params)
+        # Make a copy of vpath in a list
+        vpath = list(vpath)
+        atom = vpath.pop(0)
+
+        # Coerce the ID to the correct db type
+        resource = self.REST_instantiate(atom)
+        if resource is None:
+            if cherrypy.request.method == "PUT":
+                # PUT is special since it can be used to create
+                # a resource
+                resource = self.REST_create(atom)
+            else:
+                raise cherrypy.NotFound
+
+        # There may be further virtual path components.
+        # Try to map them to methods in children or this class.
+        if vpath:
+            a = vpath.pop(0)
+            if self.REST_children.has_key(a):
+                c = self.REST_children[a]
+                c.parent = resource
+                return c.default(*vpath, **params)
+            method = getattr(self, a, None)
+            if method and getattr(method, "expose_resource"):
+                return method(resource, *vpath, **params)
+            else:
+                # path component was specified but doesn't
+                # map to anything exposed and callable
+                raise cherrypy.NotFound
+
+        # No further known vpath components. Call a default handler
+        # based on the method
+        return self.REST_dispatch(resource,**params)
+
+    def REST_instantiate(self,id):
+        """ instantiate a REST resource based on the id
+
+        this method MUST be overridden in your class. it will be passed
+        the id (from the url fragment) and should return a model object
+        corresponding to the resource.
+
+        if the object doesn't exist, it should return None rather than throwing
+        an error. if this method returns None and it is a PUT request,
+        REST_create() will be called so you can actually create the resource.
+        """
+        raise cherrypy.NotFound
+
+    def REST_create(self,id):
+        """ create a REST resource with the specified id
+
+        this method should be overridden in your class.
+        this method will be called when a PUT request is made for a resource
+        that doesn't already exist. you should create the resource in this method
+        and return it.
+        """
+        raise cherrypy.NotFound
index c5014c8b8bc7a6b66d53b81551277db3c534f08e..b8b2ff794568d16868a53b22b56fcd09af453ef6 100644 (file)
@@ -1,6 +1,7 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <?python
 from libbe.cmdutil import unique_name
+from beweb.controllers import bug_url, project_url, bug_list_url
 def row_class(bug):
     if bug.status == "closed":
         return "closed"
@@ -19,12 +20,12 @@ def row_class(bug):
 <h1>Bug list for ${project_name}</h1>
 <table>
 <tr><td>ID</td><td>Status</td><td>Severity</td><td>Assigned To</td><td>Summary</td></tr>
-<div py:for="bug in bugs" py:strip="True"><tr class="${row_class(bug)}" py:if="bug.status != 'closed' or show_closed"><td><a href="${'/%s/%s/' % (project_id, bug.uuid)}">${unique_name(bug, bugs[:])}</a></td><td>${bug.status}</td><td>${bug.severity}</td><td>${bug.assigned}</td><td>${bug.summary}</td></tr>
+<div py:for="bug in bugs" py:strip="True"><tr class="${row_class(bug)}" py:if="bug.status != 'closed' or show_closed"><td><a href="${bug_url(project_id, bug.uuid)}">${unique_name(bug, bugs[:])}</a></td><td>${bug.status}</td><td>${bug.severity}</td><td>${bug.assigned}</td><td>${bug.summary}</td></tr>
 </div>
 </table>
-<a href="/">Project list</a>
-<a href="${'/%s/?show_closed=%s' % (project_id, str(not show_closed))}">Toggle closed</a>
-<form action="/$project_id/new/" method="post">
+<a href="${project_url()}">Project list</a>
+<a href="${bug_list_url(project_id, not show_closed)}">Toggle closed</a>
+<form action="${bug_list_url(project_id)}" method="post">
 <input type="submit" name="action" value="New bug"/>
 </form>
 </body>
index 21b2777b53742f282a894a87e6931a23ae42c9c3..09bde774d5b9473854e6370339d5573cf458a978 100644 (file)
@@ -26,7 +26,7 @@ project_triples.sort()
 <body>
 <h1>Project List</h1>
 <table>
-<tr py:for="project_name, project_id, project_loc in project_triples"><td><a href="/${project_id}/">${project_name}</a></td></tr>
+<tr py:for="project_name, project_id, project_loc in project_triples"><td><a href="/project/${project_id}/">${project_name}</a></td></tr>
 </table>
 </body>
 </html>