Index: trunk/call/app.yaml
===================================================================
--- trunk/call/app.yaml	(revision 460)
+++ trunk/call/app.yaml	(revision 460)
@@ -0,0 +1,8 @@
+application: call
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /.*
+  script: call.py
Index: trunk/call/base.html
===================================================================
--- trunk/call/base.html	(revision 460)
+++ trunk/call/base.html	(revision 460)
@@ -0,0 +1,42 @@
+<html>
+<head>
+<title>the Rileys: Call Us</title>
+<link href="http://rileys.us/css/rileys.css" rel="stylesheet" type="text/css">
+</head>
+<script language="JavaScript"><!--
+function firstFocus() {
+  var df = document.forms;
+  if (df.length > 0) {
+    var f = df[0];
+    var fe = f.elements;
+    for (i=0;i<f.length;i++) {
+      if ((fe[i].type=="text")||(fe[i].type=="textarea")||(fe.type.toString().charAt(0)=="s")) {
+        fe[i].focus();
+        break;
+      }
+    }
+  }
+} // -->
+</script>
+<body bgcolor="#fafafa" onLoad="firstFocus()">
+  <p><img name="therileys" src="http://rileys.us/images/the-rileys.png" width="205" height="48" border="0" alt="">
+  </p>
+  <h1>{% block title %}Call Us {% endblock %}</h1>
+  <p>
+    {% if unavailable %}
+    {{ unavailable }}
+    <p>
+    <a href="{{ logout_url }}">Log in again?</a>
+    {% else %}
+    {{ message }}
+    {% if error %}
+    <div style="color: red">{{ error }}</div>
+    {% endif %}
+    {% if failure %}
+    <pre>{{ failure }}</pre>
+    {% endif %}
+    {% block available %}{% endblock %}
+    {% endif %}
+  <p class="caption">Last updated on 8 April 2008.</p>
+</body>
+</html>
Index: trunk/call/call.py
===================================================================
--- trunk/call/call.py	(revision 460)
+++ trunk/call/call.py	(revision 460)
@@ -0,0 +1,160 @@
+from google.appengine.api import urlfetch, users
+from google.appengine.ext import db
+from google.appengine.ext.webapp import RequestHandler, WSGIApplication, template
+from google.appengine.ext.db import djangoforms
+from urllib import urlencode
+from wsgiref.handlers import CGIHandler
+
+try:
+    from django import newforms as forms
+except ImportError:
+    from django import forms
+
+try:
+    from django.contrib.localflavor.us.forms import USPhoneNumberField
+except ImportError:
+    from django.contrib.localflavor.usa.forms import USPhoneNumberField
+
+import logging, os, re
+
+logging.getLogger().setLevel(logging.DEBUG)
+
+CLICK2CALL_URL = 'https://secure.click2callu.com/tpcc/makecall'
+
+def vonageize_number(number):
+    number = re.sub(r'\D', '', number)
+    if len(number) == 0:
+        raise forms.ValidationError('Please enter a telephone number.')
+    if number[0] == '1':
+        number = number[1:]
+    if len(number) != 10:
+        raise forms.ValidationError('Please enter a 10-digit US telephone number.')
+    return '1' + number
+
+class VonageUSNumberProperty(db.PhoneNumberProperty):
+    def validate(self, value):
+        return vonageize_number(value)
+
+class VonageNumber(db.Model):
+    label = db.StringProperty()
+    username = db.StringProperty(required=True)
+    password = db.StringProperty(required=True)
+    number = VonageUSNumberProperty(required=True)
+
+class AllowedUser(db.Model):
+    user = db.UserProperty(required=True)
+
+def login_required(f):
+    def handle(self):
+        user = users.get_current_user()
+        if user is None:
+            return self.redirect(users.create_login_url(self.request.uri))
+        if AllowedUser.gql('WHERE user = :1', user).get() is None:
+            if users.is_current_user_admin():
+                AllowedUser(user=user).put()
+            else:
+                email = user.email()
+                logging.error('Denied access to ' + email)
+                return self.unavailable(
+                    'Your account <b>%s</b> does not have permission to use this service.' %
+                    email)
+        return f(self)
+    return handle
+
+def admin_required(f):
+    def handle(self):
+        if not users.is_current_user_admin():
+            return self.unavailable('This function requires you to be an administrator.')
+        return f(self)
+    return handle
+
+class NumberForm(djangoforms.ModelForm):
+    password = forms.CharField(widget=forms.widgets.PasswordInput)
+    number = USPhoneNumberField(label='Vonage number')
+
+    class Meta:
+        model = VonageNumber
+        exclude = ['label']
+
+class BasePage(RequestHandler):
+    def render(self, **kw):
+        self.response.out.write(template.render(self.template_path, kw))
+
+    def unavailable(self, msg):
+        self.render(unavailable=msg,
+                    logout_url=users.create_logout_url(self.request.uri))
+
+class NumbersPage(BasePage):
+    template_path = os.path.join(os.path.dirname(__file__), 'numbers.html')
+
+    @admin_required
+    def get(self):
+        self.render(form=NumberForm(instance=VonageNumber.all().get()))
+
+    @admin_required
+    def post(self):
+        form = NumberForm(data=self.request.POST,
+                          instance=VonageNumber.all().get())
+        if not form.is_valid():
+            return self.render(form=form)
+        try:
+            form.save()
+        except Exception, e:
+            return self.render(form=form, error=e.message)
+        self.redirect('/')
+
+class MainPage(BasePage):
+    template_path = os.path.join(os.path.dirname(__file__), 'index.html')
+
+    def error(self, msg, **kw):
+        self.render(error=msg, tonumber=self.request.get('tonumber', ''), **kw)
+
+    def failure(self, msg):
+        logging.error('TPCC server error: ' + msg)
+        self.error('The Vonage Click2Call server could not complete your call.',
+                   failure=msg)
+
+    def no_number(self):
+        if users.is_current_user_admin():
+            return self.redirect('/numbers')
+        self.unavailable('No phone numbers have been configured. Please ask an administrator to log in and add a phone number.')
+
+    @login_required
+    def get(self):
+        if VonageNumber.all().get() is None:
+            return self.no_number()
+        self.render()
+
+    @login_required
+    def post(self):
+        fromnumber = VonageNumber.all().get()
+        if fromnumber is None:
+            return self.no_number()
+
+        try:
+            tonumber = vonageize_number(self.request.get('tonumber', ''))
+        except Exception, e:
+            return self.error(e.message)
+
+        body = urlencode(
+            dict(username=fromnumber.username, password=fromnumber.password,
+                 fromnumber=fromnumber.number, tonumber=tonumber))
+        try:
+            response = urlfetch.fetch(CLICK2CALL_URL + '?' + body)
+            # get random auth errors with body, urlfetch.POST)
+        except urlfetch.Error, e:
+            return self.failure(e.message)
+
+        if response.status_code != 200 or response.content[:3] != '000':
+            return self.failure(response.content)
+
+        logging.info('Dialing %s' % tonumber)
+        return self.render(
+            message='Dialing. Please answer your phone when it rings.')
+
+def main():
+    CGIHandler().run(WSGIApplication([('/', MainPage),
+                                      ('/numbers', NumbersPage)], debug=True))
+
+if __name__ == "__main__":
+    main()
Index: trunk/call/index.html
===================================================================
--- trunk/call/index.html	(revision 460)
+++ trunk/call/index.html	(revision 460)
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block available %}
+    <form action="./" method="post">
+      Your phone number: <input name="tonumber" value="{{ tonumber }}">
+      <input type="submit" value="Call">
+    </form>
+{% endblock %}
+
Index: trunk/call/index.yaml
===================================================================
--- trunk/call/index.yaml	(revision 460)
+++ trunk/call/index.yaml	(revision 460)
@@ -0,0 +1,11 @@
+indexes:
+
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run.  If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED").  If you want to manage some indexes
+# manually, move them above the marker line.  The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
Index: trunk/call/numbers.html
===================================================================
--- trunk/call/numbers.html	(revision 460)
+++ trunk/call/numbers.html	(revision 460)
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Configure Outgoing Number{% endblock %}
+
+{% block available %}
+    <form action="./numbers" method="post">
+      <table>
+	{{ form }}
+      </table>
+      <input type="submit" value="Save">
+    </form>
+{% endblock %}
+
