From e0e29d35bb901fd3a6b0be1289c3b5afac130be4 Mon Sep 17 00:00:00 2001
From: Aaron Bentley
Date: Thu, 15 Dec 2005 16:02:37 -0500
Subject: [PATCH] Switched to restresource for resolving URLs
---
beweb/beweb/controllers.py | 90 ++++++++++++-
beweb/beweb/restresource.py | 195 +++++++++++++++++++++++++++++
beweb/beweb/templates/bugs.kid | 9 +-
beweb/beweb/templates/projects.kid | 2 +-
4 files changed, 289 insertions(+), 7 deletions(-)
create mode 100644 beweb/beweb/restresource.py
diff --git a/beweb/beweb/controllers.py b/beweb/beweb/controllers.py
index 2a8d0aa..8ba1595 100644
--- a/beweb/beweb/controllers.py
+++ b/beweb/beweb/controllers.py
@@ -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
index 0000000..47db637
--- /dev/null
+++ b/beweb/beweb/restresource.py
@@ -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
diff --git a/beweb/beweb/templates/bugs.kid b/beweb/beweb/templates/bugs.kid
index c5014c8..b8b2ff7 100644
--- a/beweb/beweb/templates/bugs.kid
+++ b/beweb/beweb/templates/bugs.kid
@@ -1,6 +1,7 @@
Bug list for ${project_name}
ID | Status | Severity | Assigned To | Summary |
-
-Project list
-Toggle closed
-
Project List