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