add demonstration of geosearch
[gae-samples.git] / tasks / tasks.py
blob4a6f431f55c1bf483893d2742427d2be9d40d97d
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.
18 """A collaborative task list web application built on Google App Engine."""
21 __author__ = 'Bret Taylor'
24 import datetime
25 import os
26 import random
27 import string
28 import sys
31 from google.appengine.api import users
32 from google.appengine.ext import db
33 from google.appengine.ext import webapp
34 from google.appengine.ext.webapp import template
35 from google.appengine.ext.webapp.util import login_required
36 from google.appengine.ext.webapp.util import run_wsgi_app
39 # Set to true if we want to have our webapp print stack traces, etc
40 _DEBUG = True
43 # Add our custom Django template filters to the built in filters
44 template.register_template_library('templatefilters')
47 class TaskList(db.Model):
48 """A TaskList is the entity tasks refer to to form a list.
50 Other than the tasks referring to it, a TaskList just has meta-data, like
51 whether it is published and the date at which it was last updated.
52 """
53 name = db.StringProperty(required=True)
54 created = db.DateTimeProperty(auto_now_add=True)
55 updated = db.DateTimeProperty(auto_now=True)
56 archived = db.BooleanProperty(default=False)
57 published = db.BooleanProperty(default=False)
59 @staticmethod
60 def get_current_user_lists():
61 """Returns the task lists that the current user has access to."""
62 return TaskList.get_user_lists(users.get_current_user())
64 @staticmethod
65 def get_user_lists(user):
66 """Returns the task lists that the given user has access to."""
67 if not user: return []
68 memberships = db.Query(TaskListMember).filter('user =', user)
69 return [m.task_list for m in memberships]
71 def current_user_has_access(self):
72 """Returns true if the current user has access to this task list."""
73 return self.user_has_access(users.get_current_user())
75 def user_has_access(self, user):
76 """Returns true if the given user has access to this task list."""
77 if not user: return False
78 query = db.Query(TaskListMember)
79 query.filter('task_list =', self)
80 query.filter('user =', user)
81 return query.get()
84 class TaskListMember(db.Model):
85 """Represents the many-to-many relationship between TaskLists and Users.
87 This is essentially the task list Access Control List (ACL).
88 """
89 task_list = db.Reference(TaskList, required=True)
90 user = db.UserProperty(required=True)
93 class Task(db.Model):
94 """Represents a single task in a task list.
96 A task basically only has a description. We use the priority field to
97 order the tasks so that users can specify task order manually.
99 The completed field is a DateTime, not a bool; if it is not None, the
100 task is completed, and the timestamp represents the time at which it was
101 marked completed.
103 description = db.TextProperty(required=True)
104 completed = db.DateTimeProperty()
105 archived = db.BooleanProperty(default=False)
106 priority = db.IntegerProperty(required=True, default=0)
107 task_list = db.Reference(TaskList)
108 created = db.DateTimeProperty(auto_now_add=True)
109 updated = db.DateTimeProperty(auto_now=True)
112 class BaseRequestHandler(webapp.RequestHandler):
113 """Supplies a common template generation function.
115 When you call generate(), we augment the template variables supplied with
116 the current user in the 'user' variable and the current webapp request
117 in the 'request' variable.
119 def generate(self, template_name, template_values={}):
120 values = {
121 'request': self.request,
122 'user': users.get_current_user(),
123 'login_url': users.create_login_url(self.request.uri),
124 'logout_url': users.create_logout_url('http://%s/' % (
125 self.request.host,)),
126 'debug': self.request.get('deb'),
127 'application_name': 'Task Manager',}
128 values.update(template_values)
129 directory = os.path.dirname(__file__)
130 path = os.path.join(directory, os.path.join('templates', template_name))
131 self.response.out.write(template.render(path, values, debug=_DEBUG))
134 class InboxPage(BaseRequestHandler):
135 """Lists the task list "inbox" for the current user."""
136 @login_required
137 def get(self):
138 lists = TaskList.get_current_user_lists()
139 show_archive = self.request.get('archive')
140 if not show_archive:
141 non_archived = []
142 for task_list in lists:
143 if not task_list.archived:
144 non_archived.append(task_list)
145 lists = non_archived
146 self.generate('index.html', {
147 'lists': lists,
148 'archive': show_archive,})
151 class TaskListPage(BaseRequestHandler):
152 """Displays a single task list based on ID.
154 If the task list is not published, we give a 403 unless the user is a
155 collaborator on the list. If it is published, but the user is not a
156 collaborator, we show the more limited HTML view of the task list rather
157 than the interactive AJAXy edit page.
160 # The different task list output types we support: content types and
161 # template file extensions
162 _OUTPUT_TYPES = {
163 'default': ['text/html', 'html'],
164 'html': ['text/html', 'html'],
165 'atom': ['application/atom+xml', 'xml'],}
167 def get(self):
168 task_list = TaskList.get(self.request.get('id'))
169 if not task_list:
170 self.error(403)
171 return
173 # Choose a template based on the output type
174 output_name = self.request.get('output')
175 if output_name not in TaskListPage._OUTPUT_TYPES:
176 output_name = 'default'
177 output_type = TaskListPage._OUTPUT_TYPES[output_name]
179 # Validate this user has access to this task list. If not, they can
180 # access the html view of this list only if it is published.
181 if not task_list.current_user_has_access():
182 if task_list.published:
183 if output_name == 'default':
184 output_name = 'html'
185 output_type = TaskListPage._OUTPUT_TYPES[output_name]
186 else:
187 user = users.get_current_user()
188 if not user:
189 self.redirect(users.create_login_url(self.request.uri))
190 else:
191 self.error(403)
192 return
194 # Filter out archived tasks by default
195 show_archive = self.request.get('archive')
196 tasks = task_list.task_set.order('-priority').order('created')
197 if not show_archive:
198 tasks.filter('archived =', False)
199 tasks = list(tasks)
201 # Get the last updated date from the list of tasks
202 if len(tasks) > 0:
203 updated = max([task.updated for task in tasks])
204 else:
205 updated = None
207 self.response.headers['Content-Type'] = output_type[0]
208 self.generate('tasklist_%s.%s' % (output_name, output_type[1]), {
209 'task_list': task_list,
210 'tasks': tasks,
211 'archive': show_archive,
212 'updated': updated,})
215 class CreateTaskListAction(BaseRequestHandler):
216 """Creates a new task list for the current user."""
217 def post(self):
218 user = users.get_current_user()
219 name = self.request.get('name')
220 if not user or not name:
221 self.error(403)
222 return
224 task_list = TaskList(name=name)
225 task_list.put()
226 task_list_member = TaskListMember(task_list=task_list, user=user)
227 task_list_member.put()
229 if self.request.get('next'):
230 self.redirect(self.request.get('next'))
231 else:
232 self.redirect('/list?id=' + str(task_list.key()))
235 class EditTaskAction(BaseRequestHandler):
236 """Edits a specific task, changing its description.
238 We also updated the last modified date of the task list so that the
239 task list inbox shows the correct last modified date for the list.
241 This can be used in an AJAX way or in a form. In a form, you should
242 supply a "next" argument that denotes the URL we should redirect to
243 after the edit is complete.
245 def post(self):
246 description = self.request.get('description')
247 if not description:
248 self.error(403)
249 return
251 # Get the existing task that we are editing
252 task_key = self.request.get('task')
253 if task_key:
254 task = Task.get(task_key)
255 if not task:
256 self.error(403)
257 return
258 task_list = task.task_list
259 else:
260 task = None
261 task_list = TaskList.get(self.request.get('list'))
263 # Validate this user has access to this task list
264 if not task_list or not task_list.current_user_has_access():
265 self.error(403)
266 return
268 # Create the task
269 if task:
270 task.description = db.Text(description)
271 else:
272 task = Task(description=db.Text(description), task_list=task_list)
273 task.put()
275 # Update the task list so it's updated date is updated. Saving it is all
276 # we need to do since that field has auto_now=True
277 task_list.put()
279 # Only redirect if "next" is given
280 next = self.request.get('next')
281 if next:
282 self.redirect(next)
283 else:
284 self.response.headers['Content-Type'] = 'text/plain'
285 self.response.out.write(str(task.key()))
288 class AddMemberAction(BaseRequestHandler):
289 """Adds a new User to a TaskList ACL."""
290 def post(self):
291 task_list = TaskList.get(self.request.get('list'))
292 email = self.request.get('email')
293 if not task_list or not email:
294 self.error(403)
295 return
297 # Validate this user has access to this task list
298 if not task_list.current_user_has_access():
299 self.error(403)
300 return
302 # Don't duplicate entries in the permissions datastore
303 user = users.User(email)
304 if not task_list.user_has_access(user):
305 member = TaskListMember(user=user, task_list=task_list)
306 member.put()
307 self.redirect(self.request.get('next'))
310 class InboxAction(BaseRequestHandler):
311 """Performs an action in the user's TaskList inbox.
313 We support Archive, Unarchive, and Delete actions. The action is specified
314 by the "action" argument in the POST. The names are capitalized because
315 they correspond to the text in the buttons in the form, which all have the
316 name "action".
318 def post(self):
319 action = self.request.get('action')
320 lists = self.request.get('list', allow_multiple=True)
321 if not action in ['Archive', 'Unarchive', 'Delete']:
322 self.error(403)
323 return
325 for key in lists:
326 task_list = TaskList.get(key)
328 # Validate this user has access to this task list
329 if not task_list or not task_list.current_user_has_access():
330 self.error(403)
331 return
333 if action == 'Archive':
334 task_list.archived = True
335 task_list.put()
336 elif action == 'Unarchive':
337 task_list.archived = False
338 task_list.put()
339 else:
340 for member in task_list.tasklistmember_set:
341 member.delete()
342 for task in task_list.task_set:
343 task.delete()
344 task_list.delete()
346 self.redirect(self.request.get('next'))
349 class TaskListAction(BaseRequestHandler):
350 """Performs an action on a specific task list.
352 The actions we support are "Archive Completed" and "Delete", as specified
353 by the "action" argument in the POST.
355 def post(self):
356 action = self.request.get('action')
357 tasks = self.request.get('task', allow_multiple=True)
358 if not action in ['Archive Completed', 'Delete']:
359 self.error(403)
360 return
362 for key in tasks:
363 task = Task.get(key)
365 # Validate this user has access to this task list
366 if not task or not task.task_list.current_user_has_access():
367 self.error(403)
368 return
370 if action == 'Delete':
371 task.delete()
372 else:
373 if task.completed and not task.archived:
374 task.priority = 0
375 task.archived = True
376 task.put()
378 self.redirect(self.request.get('next'))
381 class SetTaskCompletedAction(BaseRequestHandler):
382 """Sets a given task to be completed at the current time."""
383 def post(self):
384 task = Task.get(self.request.get('id'))
385 if not task or not task.task_list.current_user_has_access():
386 self.error(403)
387 return
389 completed = self.request.get('completed')
390 if completed:
391 task.completed = datetime.datetime.now()
392 else:
393 task.completed = None
394 task.archived = False
395 task.put()
398 class SetTaskPositionsAction(BaseRequestHandler):
399 """Orders the tasks in a task lists.
401 The input to this handler is a comma-separated list of task keys in the
402 "tasks" argument to the post. We assign priorities to the given tasks
403 based on that order (e.g., 1 through N for N tasks).
405 def post(self):
406 keys = self.request.get('tasks').split(',')
407 if not keys:
408 self.error(403)
409 return
410 num_keys = len(keys)
411 for i, key in enumerate(keys):
412 key = keys[i]
413 task = Task.get(key)
414 if not task or not task.task_list.current_user_has_access():
415 self.error(403)
416 return
417 task.priority = num_keys - i - 1
418 task.put()
421 class PublishTaskListAction(BaseRequestHandler):
422 """Publishes a given task list, which makes it viewable by everybody."""
423 def post(self):
424 task_list = TaskList.get(self.request.get('id'))
425 if not task_list or not task_list.current_user_has_access():
426 self.error(403)
427 return
429 task_list.published = bool(self.request.get('publish'))
430 task_list.put()
433 def main():
434 application = webapp.WSGIApplication([
435 ('/', InboxPage),
436 ('/list', TaskListPage),
437 ('/edittask.do', EditTaskAction),
438 ('/createtasklist.do', CreateTaskListAction),
439 ('/addmember.do', AddMemberAction),
440 ('/inboxaction.do', InboxAction),
441 ('/tasklist.do', TaskListAction),
442 ('/publishtasklist.do', PublishTaskListAction),
443 ('/settaskcompleted.do', SetTaskCompletedAction),
444 ('/settaskpositions.do', SetTaskPositionsAction)], debug=_DEBUG)
445 run_wsgi_app(application)
448 if __name__ == '__main__':
449 main()