diff options
-rw-r--r-- | beweb/beweb/controllers.py | 90 | ||||
-rw-r--r-- | beweb/beweb/restresource.py | 195 | ||||
-rw-r--r-- | beweb/beweb/templates/bugs.kid | 9 | ||||
-rw-r--r-- | beweb/beweb/templates/projects.kid | 2 |
4 files changed, 289 insertions, 7 deletions
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 @@ <!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> diff --git a/beweb/beweb/templates/projects.kid b/beweb/beweb/templates/projects.kid index 21b2777..09bde77 100644 --- a/beweb/beweb/templates/projects.kid +++ b/beweb/beweb/templates/projects.kid @@ -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> |