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.
25 __author__
= 'Bret Taylor'
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
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.
53 def generate(self
, template_name
, template_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
):
72 def post(self
, *args
):
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
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.
91 page_name: The wikified name of the current page.
93 # Load the main page by default
95 page_name
= 'MainPage'
96 page
= Page
.load(page_name
)
98 # Default to edit for pages that do not yet exist
102 modes
= ['view', 'edit']
103 mode
= self
.request
.get('mode')
104 if not mode
in modes
:
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
))
112 # Genertate the appropriate template
113 self
.generate(mode
+ '.html', {
117 def post(self
, page_name
):
118 """Handle HTTP POST requests throughout the application, used to handle
119 posting new or edited wiki pages.
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
))
131 # We need an explicit page name for editing
135 # Create or overwrite the page
136 page
= Page
.load(page_name
)
137 page
.content
= self
.request
.get('content')
139 self
.redirect(page
.view_url())
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
149 def __init__(self
, name
, entity
=None):
153 self
.content
= entity
['content']
154 if entity
.has_key('user'):
155 self
.user
= entity
['user']
158 self
.created
= entity
['created']
159 self
.modified
= entity
['modified']
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
),)
172 return '/%s?mode=edit' % (self
.name
,)
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.
184 The wikified version of the page contents.
191 content
= self
.content
192 for transform
in transforms
:
193 content
= transform
.run(content
)
197 """Creates or edits this page in the datastore."""
198 now
= datetime
.datetime
.now()
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'):
213 datastore
.Put(entity
)
217 """Loads the page with the given name.
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()
224 query
= datastore
.Query('Page')
225 query
['name ='] = name
226 entities
= query
.Get(1)
227 if len(entities
) < 1:
230 return Page(name
, entities
[0])
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
253 def run(self
, content
):
254 """Runs this transform over the given content.
257 content: The string data to apply a transformation to.
260 A new string that is the result of this transform.
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.
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
)
288 class AutoLink(Transform
):
289 """A transform that auto-links URLs."""
291 self
.regexp
= re
.compile(r
'([^"])\b((http|https)://[^ \t\n\r<>\(\)&"]+' \
292 r
'[^ \t\n\r<>\(\)&"\.])')
294 def replace(self
, match
):
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."""
303 self
.regexp
= re
.compile(r
'href="(http[^"]+)"')
305 def replace(self
, match
):
307 scheme
, host
, path
, parameters
, query
, fragment
= urlparse
.urlparse(url
)
308 url
= 'http://www.google.com/url?sa=D&q=' + urllib
.quote(url
)
309 return 'href="%s"' % (url
,)
313 application
= webapp
.WSGIApplication([('/(.*)', WikiPage
)], debug
=_DEBUG
)
314 run_wsgi_app(application
)
317 if __name__
== '__main__':