| import os |
| import re |
| from webob import Request, Response |
| from webob import exc |
| from tempita import HTMLTemplate |
| |
| VIEW_TEMPLATE = HTMLTemplate( |
| """\ |
| <html> |
| <head> |
| <title>{{page.title}}</title> |
| </head> |
| <body> |
| <h1>{{page.title}}</h1> |
| {{if message}} |
| <div style="background-color: #99f">{{message}}</div> |
| {{endif}} |
| |
| <div>{{page.content|html}}</div> |
| |
| <hr> |
| <a href="{{req.url}}?action=edit">Edit</a> |
| </body> |
| </html> |
| """ |
| ) |
| |
| EDIT_TEMPLATE = HTMLTemplate( |
| """\ |
| <html> |
| <head> |
| <title>Edit: {{page.title}}</title> |
| </head> |
| <body> |
| {{if page.exists}} |
| <h1>Edit: {{page.title}}</h1> |
| {{else}} |
| <h1>Create: {{page.title}}</h1> |
| {{endif}} |
| |
| <form action="{{req.path_url}}" method="POST"> |
| <input type="hidden" name="mtime" value="{{page.mtime}}"> |
| Title: <input type="text" name="title" style="width: 70%" value="{{page.title}}"><br> |
| Content: <input type="submit" value="Save"> |
| <a href="{{req.path_url}}">Cancel</a> |
| <br> |
| <textarea name="content" style="width: 100%; height: 75%" rows="40">{{page.content}}</textarea> |
| <br> |
| <input type="submit" value="Save"> |
| <a href="{{req.path_url}}">Cancel</a> |
| </form> |
| </body></html> |
| """ |
| ) |
| |
| |
| class WikiApp: |
| view_template = VIEW_TEMPLATE |
| edit_template = EDIT_TEMPLATE |
| |
| def __init__(self, storage_dir): |
| self.storage_dir = os.path.abspath(os.path.normpath(storage_dir)) |
| |
| def __call__(self, environ, start_response): |
| req = Request(environ) |
| action = req.params.get("action", "view") |
| page = self.get_page(req.path_info) |
| try: |
| try: |
| meth = getattr(self, "action_{}_{}".format(action, req.method)) |
| except AttributeError: |
| raise exc.HTTPBadRequest("No such action %r" % action) |
| resp = meth(req, page) |
| except exc.HTTPException as e: |
| resp = e |
| return resp(environ, start_response) |
| |
| def get_page(self, path): |
| path = path.lstrip("/") |
| if not path: |
| path = "index" |
| path = os.path.join(self.storage_dir, path) |
| path = os.path.normpath(path) |
| if path.endswith("/"): |
| path += "index" |
| if not path.startswith(self.storage_dir): |
| raise exc.HTTPBadRequest("Bad path") |
| path += ".html" |
| return Page(path) |
| |
| def action_view_GET(self, req, page): |
| if not page.exists: |
| return exc.HTTPTemporaryRedirect(location=req.url + "?action=edit") |
| if req.cookies.get("message"): |
| message = req.cookies["message"] |
| else: |
| message = None |
| text = self.view_template.substitute(page=page, req=req, message=message) |
| resp = Response(text) |
| if message: |
| resp.delete_cookie("message") |
| else: |
| resp.last_modified = page.mtime |
| resp.conditional_response = True |
| return resp |
| |
| def action_view_POST(self, req, page): |
| submit_mtime = int(req.params.get("mtime") or "0") or None |
| if page.mtime != submit_mtime: |
| return exc.HTTPPreconditionFailed( |
| "The page has been updated since you started editing it" |
| ) |
| page.set(title=req.params["title"], content=req.params["content"]) |
| resp = exc.HTTPSeeOther(location=req.path_url) |
| resp.set_cookie("message", "Page updated") |
| return resp |
| |
| def action_edit_GET(self, req, page): |
| text = self.edit_template.substitute(page=page, req=req) |
| return Response(text) |
| |
| |
| class Page: |
| def __init__(self, filename): |
| self.filename = filename |
| |
| @property |
| def exists(self): |
| return os.path.exists(self.filename) |
| |
| @property |
| def title(self): |
| if not self.exists: |
| # we need to guess the title |
| basename = os.path.splitext(os.path.basename(self.filename))[0] |
| basename = re.sub(r"[_-]", " ", basename) |
| return basename.capitalize() |
| content = self.full_content |
| match = re.search(r"<title>(.*?)</title>", content, re.I | re.S) |
| return match.group(1) |
| |
| @property |
| def full_content(self): |
| f = open(self.filename, "rb") |
| try: |
| return f.read() |
| finally: |
| f.close() |
| |
| @property |
| def content(self): |
| if not self.exists: |
| return "" |
| content = self.full_content |
| match = re.search(r"<body[^>]*>(.*?)</body>", content, re.I | re.S) |
| return match.group(1) |
| |
| @property |
| def mtime(self): |
| if not self.exists: |
| return None |
| else: |
| return int(os.stat(self.filename).st_mtime) |
| |
| def set(self, title, content): |
| dir = os.path.dirname(self.filename) |
| if not os.path.exists(dir): |
| os.makedirs(dir) |
| new_content = ( |
| """<html><head><title>%s</title></head><body>%s</body></html>""" |
| % (title, content) |
| ) |
| f = open(self.filename, "wb") |
| f.write(new_content) |
| f.close() |
| |
| |
| if __name__ == "__main__": |
| import optparse |
| |
| parser = optparse.OptionParser(usage="%prog --port=PORT") |
| parser.add_option( |
| "-p", |
| "--port", |
| default="8080", |
| dest="port", |
| type="int", |
| help="Port to serve on (default 8080)", |
| ) |
| parser.add_option( |
| "--wiki-data", |
| default="./wiki", |
| dest="wiki_data", |
| help="Place to put wiki data into (default ./wiki/)", |
| ) |
| options, args = parser.parse_args() |
| print("Writing wiki pages to %s" % options.wiki_data) |
| app = WikiApp(options.wiki_data) |
| from wsgiref.simple_server import make_server |
| |
| httpd = make_server("localhost", options.port, app) |
| print("Serving on http://localhost:%s" % options.port) |
| try: |
| httpd.serve_forever() |
| except KeyboardInterrupt: |
| print("^C") |