README update for latest SDK; misc small cleanup
[gae-samples.git] / cccwiki / wiki.py
blobfd3d729f4e6b832f145bea9a41aaef388afafe50
1 #!/usr/bin/env python
3 # Copyright 2008 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """A simple Google App Engine wiki application.
19 The main distinguishing feature is that editing is in a WYSIWYG editor
20 rather than a text editor with special syntax. This application uses
21 google.appengine.api.datastore to access the datastore. This is a
22 lower-level API on which google.appengine.ext.db depends.
23 """
25 __author__ = 'Bret Taylor'
27 import cgi
28 import datetime
29 import os
30 import re
31 import sys
32 import urllib
33 import urlparse
35 from google.appengine.api import datastore
36 from google.appengine.api import datastore_types
37 from google.appengine.api import users
38 from google.appengine.ext import webapp
39 from google.appengine.ext.webapp import template
40 from google.appengine.ext.webapp.util import run_wsgi_app
42 # Set to true if we want to have our webapp print stack traces, etc
43 _DEBUG = True
46 class BaseRequestHandler(webapp.RequestHandler):
47 """Supplies a common template generation function.
49 When you call generate(), we augment the template variables supplied with
50 the current user in the 'user' variable and the current webapp request
51 in the 'request' variable.
52 """
53 def generate(self, template_name, template_values={}):
54 values = {
55 'request': self.request,
56 'user': users.get_current_user(),
57 'login_url': users.create_login_url(self.request.uri),
58 'logout_url': users.create_logout_url(self.request.uri),
59 'application_name': 'Wiki',
61 values.update(template_values)
62 directory = os.path.dirname(__file__)
63 path = os.path.join(directory, os.path.join('templates', template_name))
64 self.response.out.write(template.render(path, values, debug=_DEBUG))
66 def head(self, *args):
67 pass
69 def get(self, *args):
70 pass
72 def post(self, *args):
73 pass
76 class WikiPage(BaseRequestHandler):
77 """Our one and only request handler.
79 We first determine which page we are editing, using "MainPage" if no
80 page is specified in the URI. We then determine the mode we are in (view
81 or edit), choosing "view" by default.
83 POST requests to this handler handle edit operations, writing the new page
84 to the datastore.
85 """
86 def get(self, page_name):
87 """Handle HTTP GET requests throughout the application, used to present
88 the view and edit modes of wiki pages.
90 Args:
91 page_name: The wikified name of the current page.
92 """
93 # Load the main page by default
94 if not page_name:
95 page_name = 'MainPage'
96 page = Page.load(page_name)
98 # Default to edit for pages that do not yet exist
99 if not page.entity:
100 mode = 'edit'
101 else:
102 modes = ['view', 'edit']
103 mode = self.request.get('mode')
104 if not mode in modes:
105 mode = 'view'
107 # User must be logged in to edit
108 if mode == 'edit' and not users.get_current_user():
109 self.redirect(users.create_login_url(self.request.uri))
110 return
112 # Genertate the appropriate template
113 self.generate(mode + '.html', {
114 'page': page,
117 def post(self, page_name):
118 """Handle HTTP POST requests throughout the application, used to handle
119 posting new or edited wiki pages.
121 Args:
122 page_name: The wikified name of the current page.
124 # User must be logged in to edit
125 if not users.get_current_user():
126 # The GET version of this URI is just the view/edit mode, which is a
127 # reasonable thing to redirect to
128 self.redirect(users.create_login_url(self.request.uri))
129 return
131 # We need an explicit page name for editing
132 if not page_name:
133 self.redirect('/')
135 # Create or overwrite the page
136 page = Page.load(page_name)
137 page.content = self.request.get('content')
138 page.save()
139 self.redirect(page.view_url())
142 class Page(object):
143 """Our abstraction for a Wiki page.
145 We handle all datastore operations so that new pages are handled
146 seamlessly. To create OR edit a page, just create a Page instance and
147 clal save().
149 def __init__(self, name, entity=None):
150 self.name = name
151 self.entity = entity
152 if entity:
153 self.content = entity['content']
154 if entity.has_key('user'):
155 self.user = entity['user']
156 else:
157 self.user = None
158 self.created = entity['created']
159 self.modified = entity['modified']
160 else:
161 # New pages should start out with a simple title to get the user going
162 now = datetime.datetime.now()
163 self.content = '<h1>%s</h1>' % (cgi.escape(name),)
164 self.user = None
165 self.created = now
166 self.modified = now
168 def entity(self):
169 return self.entity
171 def edit_url(self):
172 return '/%s?mode=edit' % (self.name,)
174 def view_url(self):
175 return '/' + self.name
177 def wikified_content(self):
178 """Applies our wiki transforms to our content for HTML display.
180 We auto-link URLs, link WikiWords, and hide referers on links that
181 go outside of the Wiki.
183 Returns:
184 The wikified version of the page contents.
186 transforms = [
187 AutoLink(),
188 WikiWords(),
189 HideReferers(),
191 content = self.content
192 for transform in transforms:
193 content = transform.run(content)
194 return content
196 def save(self):
197 """Creates or edits this page in the datastore."""
198 now = datetime.datetime.now()
199 if self.entity:
200 entity = self.entity
201 else:
202 entity = datastore.Entity('Page')
203 entity['name'] = self.name
204 entity['created'] = now
205 entity['content'] = datastore_types.Text(self.content)
206 entity['modified'] = now
208 if users.get_current_user():
209 entity['user'] = users.get_current_user()
210 elif entity.has_key('user'):
211 del entity['user']
213 datastore.Put(entity)
215 @staticmethod
216 def load(name):
217 """Loads the page with the given name.
219 Returns:
220 We always return a Page instance, even if the given name isn't yet in
221 the database. In that case, the Page object will be created when save()
222 is called.
224 query = datastore.Query('Page')
225 query['name ='] = name
226 entities = query.Get(1)
227 if len(entities) < 1:
228 return Page(name)
229 else:
230 return Page(name, entities[0])
232 @staticmethod
233 def exists(name):
234 """Returns true if the page with the given name exists in the datastore."""
235 return Page.load(name).entity
238 class Transform(object):
239 """Abstraction for a regular expression transform.
241 Transform subclasses have two properties:
242 regexp: the regular expression defining what will be replaced
243 replace(MatchObject): returns a string replacement for a regexp match
245 We iterate over all matches for that regular expression, calling replace()
246 on the match to determine what text should replace the matched text.
248 The Transform class is more expressive than regular expression replacement
249 because the replace() method can execute arbitrary code to, e.g., look
250 up a WikiWord to see if the page exists before determining if the WikiWord
251 should be a link.
253 def run(self, content):
254 """Runs this transform over the given content.
256 Args:
257 content: The string data to apply a transformation to.
259 Returns:
260 A new string that is the result of this transform.
262 parts = []
263 offset = 0
264 for match in self.regexp.finditer(content):
265 parts.append(content[offset:match.start(0)])
266 parts.append(self.replace(match))
267 offset = match.end(0)
268 parts.append(content[offset:])
269 return ''.join(parts)
272 class WikiWords(Transform):
273 """Translates WikiWords to links.
275 We look up all words, and we only link those words that currently exist.
277 def __init__(self):
278 self.regexp = re.compile(r'[A-Z][a-z]+([A-Z][a-z]+)+')
280 def replace(self, match):
281 wikiword = match.group(0)
282 if Page.exists(wikiword):
283 return '<a class="wikiword" href="/%s">%s</a>' % (wikiword, wikiword)
284 else:
285 return wikiword
288 class AutoLink(Transform):
289 """A transform that auto-links URLs."""
290 def __init__(self):
291 self.regexp = re.compile(r'([^"])\b((http|https)://[^ \t\n\r<>\(\)&"]+' \
292 r'[^ \t\n\r<>\(\)&"\.])')
294 def replace(self, match):
295 url = match.group(2)
296 return match.group(1) + '<a class="autourl" href="%s">%s</a>' % (url, url)
299 class HideReferers(Transform):
300 """A transform that hides referers for external hyperlinks."""
302 def __init__(self):
303 self.regexp = re.compile(r'href="(http[^"]+)"')
305 def replace(self, match):
306 url = match.group(1)
307 scheme, host, path, parameters, query, fragment = urlparse.urlparse(url)
308 url = 'http://www.google.com/url?sa=D&amp;q=' + urllib.quote(url)
309 return 'href="%s"' % (url,)
312 def main():
313 application = webapp.WSGIApplication([('/(.*)', WikiPage)], debug=_DEBUG)
314 run_wsgi_app(application)
317 if __name__ == '__main__':
318 main()