|
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).exception |
|
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) |
|
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").exception |
|
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 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' |