allow history to work in webkit browsers
[gae-samples.git] / crowdguru / guru.py
blob3ff1c20bad10908e0a4310048661135e453c7437
1 # Copyright 2009 Google Inc.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 import datetime
16 import logging
17 import os
18 import re
19 import wsgiref.handlers
20 from google.appengine.api import xmpp
21 from google.appengine.api import users
22 from google.appengine.ext import db
23 from google.appengine.ext import webapp
24 from google.appengine.ext.ereporter import report_generator
25 from google.appengine.ext.webapp import template
26 from google.appengine.ext.webapp import xmpp_handlers
29 PONDER_MSG = "Hmm. Let me think on that a bit."
30 TELLME_MSG = "While I'm thinking, perhaps you can answer me this: %s"
31 SOMEONE_ANSWERED_MSG = ("We seek those who are wise and fast. One out of two "
32 "is not enough. Another has answered my question.")
33 ANSWER_INTRO_MSG = "You asked me: %s"
34 ANSWER_MSG = "I have thought long and hard, and concluded: %s"
35 WAIT_MSG = ("Please! One question at a time! You can ask me another once you "
36 "have an answer to your current question.")
37 THANKS_MSG = "Thank you for your wisdom."
38 TELLME_THANKS_MSG = ("Thank you for your wisdom."
39 " I'm still thinking about your question.")
40 EMPTYQ_MSG = "Sorry, I don't have anything to ask you at the moment."
41 HELP_MSG = ("I am the amazing Crowd Guru. Ask me a question by typing '/tellme "
42 "the meaning of life', and I will answer you forthwith! To learn "
43 "more, go to %s/")
44 MAX_ANSWER_TIME = 120
47 class Question(db.Model):
48 question = db.TextProperty(required=True)
49 asker = db.IMProperty(required=True)
50 asked = db.DateTimeProperty(required=True, auto_now_add=True)
52 assignees = db.ListProperty(db.IM)
53 last_assigned = db.DateTimeProperty()
55 answer = db.TextProperty()
56 answerer = db.IMProperty()
57 answered = db.DateTimeProperty()
59 @staticmethod
60 def _tryAssignTx(key, user, expiry):
61 """Assigns and returns the question if it's not assigned already.
63 Args:
64 key: db.Key: The key of a Question to try and assign.
65 user: db.IM: The user to assign the question to.
66 Returns:
67 The Question object. If it was already assigned, no change is made
68 """
69 question = Question.get(key)
70 if not question.last_assigned or question.last_assigned < expiry:
71 question.assignees.append(user)
72 question.last_assigned = datetime.datetime.now()
73 question.put()
74 return question
76 @staticmethod
77 def assignQuestion(user):
78 """Gets an unanswered question and assigns it to a user to answer.
80 Args:
81 user: db.IM: The identity of the user to assign a question to.
82 Returns:
83 The Question entity assigned to the user, or None if there are no
84 unanswered questions.
85 """
86 question = None
87 while question == None or user not in question.assignees:
88 # Assignments made before this timestamp have expired.
89 expiry = (datetime.datetime.now()
90 - datetime.timedelta(seconds=MAX_ANSWER_TIME))
92 # Find a candidate question
93 q = Question.all()
94 q.filter("answerer =", None)
95 q.filter("last_assigned <", expiry).order("last_assigned")
96 # If a question has never been assigned, order by when it was asked
97 q.order("asked")
98 candidates = [x for x in q.fetch(2) if x.asker != user]
99 if not candidates:
100 # No valid questions in queue.
101 break
103 # Try and assign it
104 question = db.run_in_transaction(Question._tryAssignTx,
105 candidates[0].key(), user, expiry)
107 # Expire the assignment after a couple of minutes
108 return question
110 def _unassignTx(self, user):
111 question = Question.get(self.key())
112 if user in question.assignees:
113 question.assignees.remove(user)
114 question.put()
116 def unassign(self, user):
117 """Unassigns the given user to this question.
119 Args:
120 user: db.IM: The user who will no longer be answering this question.
122 db.run_in_transaction(self._unassignTx, user)
125 class XmppHandler(xmpp_handlers.CommandHandler):
126 """Handler class for all XMPP activity."""
128 def _GetAsked(self, user):
129 """Returns the user's outstanding asked question, if any."""
130 q = Question.all()
131 q.filter("asker =", user)
132 q.filter("answer =", None)
133 return q.get()
135 def _GetAnswering(self, user):
136 """Returns the question the user is answering, if any."""
137 q = Question.all()
138 q.filter("assignees =", user)
139 q.filter("answer =", None)
140 return q.get()
142 def unhandled_command(self, message=None):
143 # Show help text
144 message.reply(HELP_MSG % (self.request.host_url,))
146 def askme_command(self, message=None):
147 im_from = db.IM("xmpp", message.sender)
148 currently_answering = self._GetAnswering(im_from)
149 question = Question.assignQuestion(im_from)
150 if question:
151 message.reply(TELLME_MSG % (question.question,))
152 else:
153 message.reply(EMPTYQ_MSG)
154 # Don't unassign their current question until we've picked a new one.
155 if currently_answering:
156 currently_answering.unassign(im_from)
158 def text_message(self, message=None):
159 im_from = db.IM("xmpp", message.sender)
160 question = self._GetAnswering(im_from)
161 if question:
162 other_assignees = question.assignees
163 other_assignees.remove(im_from)
165 # Answering a question
166 question.answer = message.arg
167 question.answerer = im_from
168 question.assignees = []
169 question.answered = datetime.datetime.now()
170 question.put()
172 # Send the answer to the asker
173 xmpp.send_message([question.asker.address],
174 ANSWER_INTRO_MSG % (question.question,))
175 xmpp.send_message([question.asker.address], ANSWER_MSG % (message.arg,))
177 # Send acknowledgement to the answerer
178 asked_question = self._GetAsked(im_from)
179 if asked_question:
180 message.reply(TELLME_THANKS_MSG)
181 else:
182 message.reply(THANKS_MSG)
184 # Tell any other assignees their help is no longer required
185 if other_assignees:
186 xmpp.send_message([x.address for x in other_assignees],
187 SOMEONE_ANSWERED_MSG)
188 else:
189 self.unhandled_command(message)
191 def tellme_command(self, message=None):
192 im_from = db.IM("xmpp", message.sender)
193 asked_question = self._GetAsked(im_from)
194 currently_answering = self._GetAnswering(im_from)
196 if asked_question:
197 # Already have a question
198 message.reply(WAIT_MSG)
199 else:
200 # Asking a question
201 asked_question = Question(question=message.arg, asker=im_from)
202 asked_question.put()
204 if not currently_answering:
205 # Try and find one for them to answer
206 question = Question.assignQuestion(im_from)
207 if question:
208 message.reply(TELLME_MSG % (question.question,))
209 return
210 message.reply(PONDER_MSG)
213 class LatestHandler(webapp.RequestHandler):
214 """Displays the most recently answered questions."""
216 def Render(self, template_file, template_values):
217 path = os.path.join(os.path.dirname(__file__), 'templates', template_file)
218 self.response.out.write(template.render(path, template_values))
220 def get(self):
221 q = Question.all().order('-answered').filter('answered >', None)
222 template_values = {
223 'questions': q.fetch(20),
225 self.Render("latest.html", template_values)
228 def main():
229 app = webapp.WSGIApplication([
230 ('/', LatestHandler),
231 ('/_ah/xmpp/message/chat/', XmppHandler),
232 ], debug=True)
233 wsgiref.handlers.CGIHandler().run(app)
236 if __name__ == '__main__':
237 main()