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'
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
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.
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)
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())
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
)
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).
89 task_list
= db
.Reference(TaskList
, required
=True)
90 user
= db
.UserProperty(required
=True)
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
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
={}):
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."""
138 lists
= TaskList
.get_current_user_lists()
139 show_archive
= self
.request
.get('archive')
142 for task_list
in lists
:
143 if not task_list
.archived
:
144 non_archived
.append(task_list
)
146 self
.generate('index.html', {
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
163 'default': ['text/html', 'html'],
164 'html': ['text/html', 'html'],
165 'atom': ['application/atom+xml', 'xml'],}
168 task_list
= TaskList
.get(self
.request
.get('id'))
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':
185 output_type
= TaskListPage
._OUTPUT
_TYPES
[output_name
]
187 user
= users
.get_current_user()
189 self
.redirect(users
.create_login_url(self
.request
.uri
))
194 # Filter out archived tasks by default
195 show_archive
= self
.request
.get('archive')
196 tasks
= task_list
.task_set
.order('-priority').order('created')
198 tasks
.filter('archived =', False)
201 # Get the last updated date from the list of tasks
203 updated
= max([task
.updated
for task
in tasks
])
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
,
211 'archive': show_archive
,
212 'updated': updated
,})
215 class CreateTaskListAction(BaseRequestHandler
):
216 """Creates a new task list for the current user."""
218 user
= users
.get_current_user()
219 name
= self
.request
.get('name')
220 if not user
or not name
:
224 task_list
= TaskList(name
=name
)
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'))
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.
246 description
= self
.request
.get('description')
251 # Get the existing task that we are editing
252 task_key
= self
.request
.get('task')
254 task
= Task
.get(task_key
)
258 task_list
= task
.task_list
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():
270 task
.description
= db
.Text(description
)
272 task
= Task(description
=db
.Text(description
), task_list
=task_list
)
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
279 # Only redirect if "next" is given
280 next
= self
.request
.get('next')
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."""
291 task_list
= TaskList
.get(self
.request
.get('list'))
292 email
= self
.request
.get('email')
293 if not task_list
or not email
:
297 # Validate this user has access to this task list
298 if not task_list
.current_user_has_access():
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
)
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
319 action
= self
.request
.get('action')
320 lists
= self
.request
.get('list', allow_multiple
=True)
321 if not action
in ['Archive', 'Unarchive', 'Delete']:
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():
333 if action
== 'Archive':
334 task_list
.archived
= True
336 elif action
== 'Unarchive':
337 task_list
.archived
= False
340 for member
in task_list
.tasklistmember_set
:
342 for task
in task_list
.task_set
:
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.
356 action
= self
.request
.get('action')
357 tasks
= self
.request
.get('task', allow_multiple
=True)
358 if not action
in ['Archive Completed', 'Delete']:
365 # Validate this user has access to this task list
366 if not task
or not task
.task_list
.current_user_has_access():
370 if action
== 'Delete':
373 if task
.completed
and not task
.archived
:
378 self
.redirect(self
.request
.get('next'))
381 class SetTaskCompletedAction(BaseRequestHandler
):
382 """Sets a given task to be completed at the current time."""
384 task
= Task
.get(self
.request
.get('id'))
385 if not task
or not task
.task_list
.current_user_has_access():
389 completed
= self
.request
.get('completed')
391 task
.completed
= datetime
.datetime
.now()
393 task
.completed
= None
394 task
.archived
= False
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).
406 keys
= self
.request
.get('tasks').split(',')
411 for i
, key
in enumerate(keys
):
414 if not task
or not task
.task_list
.current_user_has_access():
417 task
.priority
= num_keys
- i
- 1
421 class PublishTaskListAction(BaseRequestHandler
):
422 """Publishes a given task list, which makes it viewable by everybody."""
424 task_list
= TaskList
.get(self
.request
.get('id'))
425 if not task_list
or not task_list
.current_user_has_access():
429 task_list
.published
= bool(self
.request
.get('publish'))
434 application
= webapp
.WSGIApplication([
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__':