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