Permalink
Browse files

Refactoring in progress: move http stuff out of handle_mailin.py and …

…into utils.py. Haven't refactored twitter yet
  • Loading branch information...
1 parent 262f54d commit bd6b0a2ae17e0fed751634c8fba132c28525d707 @slinkp committed May 3, 2010
@@ -18,26 +18,24 @@
from datetime import datetime
from optparse import make_option
-from poster.encode import multipart_encode
from stat import S_IRWXU, S_IRWXG, S_IRWXO
+
import email.Header
-import httplib2
import mimetypes
import os
import re
-import socket
import sys
import tempfile
import time
import traceback
import unicodedata
-import urlparse
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.mail import send_mail
from django.core.management.base import BaseCommand
-from django.utils import simplejson as json
+from utils import FixcityHttp
+
from django.conf import settings
logger = settings.LOGGER
@@ -384,10 +382,13 @@ def get_photos(self, message_parts):
class RackMaker(object):
- def __init__(self, notifier, options):
+ # XXX Maybe this class doesn't need to exist anymore?
+ # FixcityHttp might be directly usable for email, twitter, etc?
+
+ def __init__(self, notifier, options, error_adapter):
self.notifier = notifier
self.options = options
- self.error_adapter = ErrorAdapter()
+ self.error_adapter = error_adapter
def submit(self, data):
"""
@@ -397,100 +398,13 @@ def submit(self, data):
logger.debug("would save rack here")
return
- photos = data.pop('photos', {})
- url = settings.RACK_POSTING_URL
- result = self.do_post_json(url, data)
- if not result:
- return
+ return FixcityHttp(self.notifier, self.error_adapter).submit(data)
- # Lots of rack-specific stuff below
- parsed_url = urlparse.urlparse(url)
- base_url = parsed_url[0] + '://' + parsed_url[1]
- photo_url = base_url + result['photo_post_url']
- rack_url = base_url + result['rack_url']
- rack_user = result.get('user')
-
- if photos.has_key('photo'):
- datagen, headers = multipart_encode({'photo': photos['photo']})
- # httplib2 doesn't like poster's integer headers.
- headers['Content-Length'] = str(headers['Content-Length'])
- body = ''.join([s for s in datagen])
- result = self.do_post(photo_url, body, headers=headers)
- logger.debug("result from photo upload:")
- logger.debug(result)
- # XXX need to add links per
- # https://projects.openplans.org/fixcity/wiki/EmailText
- # ... will need an HTML version.
- reply = "Thanks for your rack suggestion!\n\n"
- reply += "You must verify that your spot meets DOT requirements\n"
- reply += "before we can submit it.\n"
- reply += "To verify, go to: %(rack_url)sedit/\n\n"
- if not rack_user:
- # XXX Create an inactive account and add a confirmation link.
- reply += "To create an account, go to %(base_url)s/accounts/register/ .\n\n"
- reply += "Thanks!\n\n"
- reply += "- The Open Planning Project & Livable Streets Initiative\n"
- reply = reply % locals()
- self.notifier.reply("FixCity Rack Confirmation", reply)
-
-
- def do_post(self, url, body, headers={}):
- """POST the body to the URL. Returns response body
- on success, or None on failure.
- """
- error_subject = "Unsuccessful Rack Request"
- http = httplib2.Http()
- try:
- response, content = http.request(url, 'POST',
- headers=headers,
- body=body)
- except (socket.error, AttributeError):
- # it's absurd that we have to catch AttributeError here,
- # but apparently that's what httplib2 0.5.0 raises because
- # the socket ends up being None. Lame!
- # Known issue: http://code.google.com/p/httplib2/issues/detail?id=62&can=1&q=AttributeError
- logger.debug('Server down? %r' % url)
- self.notifier.bounce(
- error_subject,
- self.error_adapter.server_error_retry,
- notify_admin='Server down??'
- )
- return None
-
- logger.debug("server %r responded with:\n%s" % (url, content))
-
- if response.status >= 500:
- err_msg = self.error_adapter.server_error_permanent
- self.notifier.bounce(
- error_subject, err_msg, notify_admin='500 Server error',
- notify_admin_body=content)
- return None
- return content
-
- def do_post_json(self, url, data, headers={}):
- """Post some data as json to the given URL.
- Expect the response to be JSON data, and return it decoded.
-
- If the server detects validation errors, it should include an
- 'errors' key in the response data. The value for 'errors'
- should be a mapping of field name to a list of error strings
- for that field. (Not coincidentally, django forms yield
- validation errors in that format.)
- """
- body = json.dumps(data)
- error_subject = "Unsuccessful Rack Request"
- headers.setdefault('Content-type', 'application/json')
- response_body = self.do_post(url, body, headers)
- if response_body:
- result = json.loads(response_body)
- else:
- result = {}
- if result.has_key('errors'):
- err_msg = self.error_adapter.validation_errors(result['errors'])
- self.notifier.bounce(error_subject, err_msg)
- result = None
- return result
+ def do_post(self, *args, **kw):
+ return FixcityHttp(self.notifier, self.error_adapter).do_post(*args, **kw)
+ def do_post_json(self, *args, **kw):
+ return FixcityHttp(self.notifier, self.error_adapter).do_post_json(*args, **kw)
class Notifier(object):
@@ -543,6 +457,25 @@ def notify_admin(self, subject, body):
from_addr = 'racks@fixcity.org'
send_mail(subject, body, from_addr, [admin_email], fail_silently=False)
+ def on_submit_success(self, vars):
+ """
+ Callback that the submitter will use to notify the user of success.
+ """
+ # XXX need to add links per
+ # https://projects.openplans.org/fixcity/wiki/EmailText
+ # ... will need an HTML version.
+ reply = "Thanks for your rack suggestion!\n\n"
+ reply += "You must verify that your spot meets DOT requirements\n"
+ reply += "before we can submit it.\n"
+ reply += "To verify, go to: %(rack_url)sedit/\n\n"
+ if not vars['rack_user']:
+ # XXX Create an inactive account and add a confirmation link.
+ reply += "To create an account, go to %(base_url)s/accounts/register/ .\n\n"
+ reply += "Thanks!\n\n"
+ reply += "- OpenPlans & Livable Streets Initiative\n"
+ reply = reply % vars
+ self.reply("FixCity Rack Confirmation", reply)
+
class ErrorAdapter(object):
@@ -21,9 +21,10 @@
class TwitterFetcher(object):
- def __init__(self, twitter_api, username):
+ def __init__(self, twitter_api, username, notifier):
self.twitter_api = twitter_api
self.username = username
+ self.notifier = notifier
def parse(self, tweet):
msg = tweet.text.replace('@' + self.username, '')
@@ -73,12 +74,13 @@ class RackMaker(object):
"format e.g. http://bit.ly/76pXSi and try again "
"or @ us w/questions.")
- def __init__(self, config, api):
+ def __init__(self, config, api, notifier):
self.url = config.RACK_POSTING_URL
self.username = config.TWITTER_USER
self.password = config.TWITTER_PASSWORD
self.twitter_api = api
self.status_file_path = config.TWITTER_STATUS_PATH
+ self.notifier = notifier
def load_last_status(self, recent_only):
last_processed_id = None
@@ -111,17 +113,17 @@ def main(self, recent_only=True):
# Twitter is feeling sad again.
# Let's bail out and hope they're back soon.
return
- twit = TwitterFetcher(self.twitter_api, self.username)
+ twit = TwitterFetcher(self.twitter_api, self.username, self.notifier)
all_tweets = twit.get_tweets(last_processed_id)
for tweet in reversed(all_tweets):
parsed = twit.parse(tweet)
user = tweet.user.screen_name
if parsed:
- self.new_rack(**parsed)
+ self.submit(**parsed)
else:
- self.bounce(user, self.general_error_message)
+ self.notifier.bounce(user, self.general_error_message)
# XXX We should lock the status file in case this script
# ever takes so long that it overlaps with the next
# run. Or something.
@@ -150,7 +152,7 @@ def main(self, recent_only=True):
# and notify user if they eventually succeed?
- def new_rack(self, title, address, user, date, tweetid):
+ def submit(self, title, address, user, date, tweetid):
url = self.url
# XXX UGH, copy-pasted from handle_mailin.py. Refactoring time!
description = ''
@@ -173,8 +175,9 @@ def new_rack(self, title, address, user, date, tweetid):
headers=headers,
body=jsondata)
except socket.error:
- _notify_admin('Server down??',
- 'Could not post some tweets, fixcity.org dead?')
+ self.notifier.notify_admin(
+ 'Server down??',
+ 'Could not post some tweets, fixcity.org dead?')
# Important to re-raise here, to prevent storing this tweet's
# ID as the last successfully processed one.
raise
@@ -183,7 +186,7 @@ def new_rack(self, title, address, user, date, tweetid):
# XXX give a URL to a help page w/ more info?
# Maybe even a private URL to a page w/ this user's exact errors?
err_msg = self.general_error_message
- self.bounce(
+ self.notifier.bounce(
user, err_msg,
notify_admin='fixcity: twitter: 500 Server error',
notify_admin_body=content)
@@ -194,18 +197,32 @@ def new_rack(self, title, address, user, date, tweetid):
## errors = adapt_errors(result['errors'])
## for k, v in sorted(errors.items()):
## err_msg += "%s: %s\n" % (k, '; '.join(v))
-
- self.bounce(user, err_msg)
+
+ self.notifier.bounce(user, err_msg)
return
else:
# XXX handle errors from bitly.
shortened_url = shorten_url('%s%s/' % (self.url, result['rack']))
- self.bounce(user,
- "Thank you! Here's the rack request %s; now you can "
- "register to verify your request "
- "http://bit.ly/84Myis" % shortened_url)
+ self.notifier.bounce(
+ user,
+ "Thank you! Here's the rack request %s; now you can register "
+ "to verify your request "
+ % shortened_url)
+
+
+class Notifier(object):
+
+ def __init__(self, twitter_api):
+ self.twitter_api = twitter_api
def bounce(self, user, message, notify_admin='', notify_admin_body=''):
+ """Bounce a message, with additional info for explanation.
+
+ If the notify_admin string is non-empty, the site admin will
+ be notified, with that string appended to the subject.
+ If notify_admin_body is non-empty, it will be added to the body
+ sent to the admin.
+ """
message = '@%s %s' % (user, message)
message = message[:140]
try:
@@ -222,20 +239,22 @@ def bounce(self, user, message, notify_admin='', notify_admin_body=''):
if notify_admin_body:
admin_body += 'Additional info:\n'
admin_body += notify_admin_body
- _notify_admin(admin_subject, admin_body)
+ self.notify_admin(admin_subject, admin_body)
-def _notify_admin(subject, body):
- admin_email = settings.SERVICE_FAILURE_EMAIL
- from_addr = 'racks@fixcity.org'
- send_mail(subject, body, from_addr, [admin_email], fail_silently=False)
- # person receiving cron messages will get stdout
- logger.info(body)
+ @staticmethod
+ def notify_admin(subject, body):
+ admin_email = settings.SERVICE_FAILURE_EMAIL
+ from_addr = 'racks@fixcity.org'
+ send_mail(subject, body, from_addr, [admin_email], fail_silently=False)
+ # person receiving cron messages will get stdout
+ logger.info(body)
-def adapt_errors(adict):
- # XXX TODO
- return adict
+class ErrorAdapter(object):
+ def adapt_errors(self, adict):
+ # XXX TODO
+ return adict
def api_factory(settings):
@@ -249,5 +268,6 @@ class Command(BaseCommand):
def handle(self, *args, **options):
api = api_factory(settings)
- builder = RackMaker(settings, api)
+ notifier = Notifier(api)
+ builder = RackMaker(settings, api, notifier)
builder.main(recent_only=True)
@@ -1,7 +1,28 @@
-from test_models import *
-from test_templatetags import *
-from test_tweeter import *
-from test_utils import *
-from test_bulkorder import *
-from test_views import *
-from test_handle_mailin import *
+"""
+Work around Django only supporting a single tests.py file by default.
+
+This will load test suites from all files named test*py in the tests package.
+
+Could have been done by creating a custom test runner as per
+http://docs.djangoproject.com/en/dev/topics/testing/#using-different-testing-frameworks
+but this is easier.
+
+"""
+
+import glob
+import os
+import unittest
+
+def suite():
+ # for some reason django does not expect this to be a TestSuite instance;
+ # rather it must be a zero-arg callable that returns a TestSuite.
+ here = os.path.abspath(os.path.dirname(__file__))
+ testfiles = glob.glob(here + '/test*py')
+ testmodules = [os.path.splitext(os.path.basename(name))[0]
+ for name in testfiles]
+ testmodules = [__name__ + '.' + name for name in testmodules]
+
+ #_suite = unittest.TestSuite()
+ return unittest.defaultTestLoader.loadTestsFromNames(testmodules)
+ #return _suite
+
Oops, something went wrong.

0 comments on commit bd6b0a2

Please sign in to comment.