1 import os 2 import re 3 from webob import Request, Response 4 from webob import exc 5 from tempita import HTMLTemplate 6 7 VIEW_TEMPLATE = HTMLTemplate("""\ 8 <html> 9 <head> 10 <title>{{page.title}}</title> 11 </head> 12 <body> 13 <h1>{{page.title}}</h1> 14 {{if message}} 15 <div style="background-color: #99f">{{message}}</div> 16 {{endif}} 17 18 <div>{{page.content|html}}</div> 19 20 <hr> 21 <a href="{{req.url}}?action=edit">Edit</a> 22 </body> 23 </html> 24 """) 25 26 EDIT_TEMPLATE = HTMLTemplate("""\ 27 <html> 28 <head> 29 <title>Edit: {{page.title}}</title> 30 </head> 31 <body> 32 {{if page.exists}} 33 <h1>Edit: {{page.title}}</h1> 34 {{else}} 35 <h1>Create: {{page.title}}</h1> 36 {{endif}} 37 38 <form action="{{req.path_url}}" method="POST"> 39 <input type="hidden" name="mtime" value="{{page.mtime}}"> 40 Title: <input type="text" name="title" style="width: 70%" value="{{page.title}}"><br> 41 Content: <input type="submit" value="Save"> 42 <a href="{{req.path_url}}">Cancel</a> 43 <br> 44 <textarea name="content" style="width: 100%; height: 75%" rows="40">{{page.content}}</textarea> 45 <br> 46 <input type="submit" value="Save"> 47 <a href="{{req.path_url}}">Cancel</a> 48 </form> 49 </body></html> 50 """) 51 52 class WikiApp(object): 53 54 view_template = VIEW_TEMPLATE 55 edit_template = EDIT_TEMPLATE 56 57 def __init__(self, storage_dir): 58 self.storage_dir = os.path.abspath(os.path.normpath(storage_dir)) 59 60 def __call__(self, environ, start_response): 61 req = Request(environ) 62 action = req.params.get('action', 'view') 63 page = self.get_page(req.path_info) 64 try: 65 try: 66 meth = getattr(self, 'action_%s_%s' % (action, req.method)) 67 except AttributeError: 68 raise exc.HTTPBadRequest('No such action %r' % action) 69 resp = meth(req, page) 70 except exc.HTTPException, e: 71 resp = e 72 return resp(environ, start_response) 73 74 def get_page(self, path): 75 path = path.lstrip('/') 76 if not path: 77 path = 'index' 78 path = os.path.join(self.storage_dir, path) 79 path = os.path.normpath(path) 80 if path.endswith('/'): 81 path += 'index' 82 if not path.startswith(self.storage_dir): 83 raise exc.HTTPBadRequest("Bad path") 84 path += '.html' 85 return Page(path) 86 87 def action_view_GET(self, req, page): 88 if not page.exists: 89 return exc.HTTPTemporaryRedirect( 90 location=req.url + '?action=edit') 91 if req.cookies.get('message'): 92 message = req.cookies['message'] 93 else: 94 message = None 95 text = self.view_template.substitute( 96 page=page, req=req, message=message) 97 resp = Response(text) 98 if message: 99 resp.delete_cookie('message') 100 else: 101 resp.last_modified = page.mtime 102 resp.conditional_response = True 103 return resp 104 105 def action_view_POST(self, req, page): 106 submit_mtime = int(req.params.get('mtime') or '0') or None 107 if page.mtime != submit_mtime: 108 return exc.HTTPPreconditionFailed( 109 "The page has been updated since you started editing it") 110 page.set( 111 title=req.params['title'], 112 content=req.params['content']) 113 resp = exc.HTTPSeeOther( 114 location=req.path_url) 115 resp.set_cookie('message', 'Page updated') 116 return resp 117 118 def action_edit_GET(self, req, page): 119 text = self.edit_template.substitute( 120 page=page, req=req) 121 return Response(text) 122 123 class Page(object): 124 def __init__(self, filename): 125 self.filename = filename 126 127 @property 128 def exists(self): 129 return os.path.exists(self.filename) 130 131 @property 132 def title(self): 133 if not self.exists: 134 # we need to guess the title 135 basename = os.path.splitext(os.path.basename(self.filename))[0] 136 basename = re.sub(r'[_-]', ' ', basename) 137 return basename.capitalize() 138 content = self.full_content 139 match = re.search(r'<title>(.*?)</title>', content, re.I|re.S) 140 return match.group(1) 141 142 @property 143 def full_content(self): 144 f = open(self.filename, 'rb') 145 try: 146 return f.read() 147 finally: 148 f.close() 149 150 @property 151 def content(self): 152 if not self.exists: 153 return '' 154 content = self.full_content 155 match = re.search(r'<body[^>]*>(.*?)</body>', content, re.I|re.S) 156 return match.group(1) 157 158 @property 159 def mtime(self): 160 if not self.exists: 161 return None 162 else: 163 return int(os.stat(self.filename).st_mtime) 164 165 def set(self, title, content): 166 dir = os.path.dirname(self.filename) 167 if not os.path.exists(dir): 168 os.makedirs(dir) 169 new_content = """<html><head><title>%s</title></head><body>%s</body></html>""" % ( 170 title, content) 171 f = open(self.filename, 'wb') 172 f.write(new_content) 173 f.close() 174 175 if __name__ == '__main__': 176 import optparse 177 parser = optparse.OptionParser( 178 usage='%prog --port=PORT' 179 ) 180 parser.add_option( 181 '-p', '--port', 182 default='8080', 183 dest='port', 184 type='int', 185 help='Port to serve on (default 8080)') 186 parser.add_option( 187 '--wiki-data', 188 default='./wiki', 189 dest='wiki_data', 190 help='Place to put wiki data into (default ./wiki/)') 191 options, args = parser.parse_args() 192 print 'Writing wiki pages to %s' % options.wiki_data 193 app = WikiApp(options.wiki_data) 194 from wsgiref.simple_server import make_server 195 httpd = make_server('localhost', options.port, app) 196 print 'Serving on http://localhost:%s' % options.port 197 try: 198 httpd.serve_forever() 199 except KeyboardInterrupt: 200 print '^C' 201