diff --git a/apps/feed_import/models.py b/apps/feed_import/models.py index e144ca136..5164f17b8 100644 --- a/apps/feed_import/models.py +++ b/apps/feed_import/models.py @@ -1,5 +1,6 @@ import datetime import oauth2 as oauth +import mongoengine as mongo from collections import defaultdict from StringIO import StringIO from xml.etree.ElementTree import Element, SubElement, Comment, tostring @@ -13,7 +14,7 @@ from apps.rss_feeds.models import Feed, DuplicateFeed, MStarredStory from apps.reader.models import UserSubscription, UserSubscriptionFolders from utils import json_functions as json, urlnorm from utils import log as logging - +from utils.feed_functions import timelimit class OAuthToken(models.Model): user = models.OneToOneField(User, null=True, blank=True) @@ -96,12 +97,17 @@ class OPMLImporter(Importer): def __init__(self, opml_xml, user): self.user = user self.opml_xml = opml_xml - + + def try_processing(self): + folders = timelimit(20)(self.process)() + return folders + def process(self): - outline = opml.from_string(self.opml_xml) self.clear_feeds() + outline = opml.from_string(self.opml_xml) folders = self.process_outline(outline) UserSubscriptionFolders.objects.create(user=self.user, folders=json.encode(folders)) + return folders def process_outline(self, outline): @@ -169,7 +175,30 @@ class OPMLImporter(Importer): us.save() folders.append(feed_db.pk) return folders + + def count_feeds_in_opml(self): + opml_count = len(opml.from_string(self.opml_xml)) + sub_count = UserSubscription.objects.filter(user=self.user).count() + return opml_count + sub_count + + +class UploadedOPML(mongo.Document): + user_id = mongo.IntField() + opml_file = mongo.StringField() + upload_date = mongo.DateTimeField(default=datetime.datetime.now) + + def __unicode__(self): + user = User.objects.get(pk=self.user_id) + return "%s: %s characters" % (user.username, len(self.opml_file)) + + meta = { + 'collection': 'uploaded_opml', + 'allow_inheritance': False, + 'order': '-upload_date', + 'indexes': ['user_id', '-upload_date'], + } + class GoogleReaderImporter(Importer): diff --git a/apps/feed_import/tasks.py b/apps/feed_import/tasks.py new file mode 100644 index 000000000..d0f6c1c7c --- /dev/null +++ b/apps/feed_import/tasks.py @@ -0,0 +1,21 @@ +from celery.task import Task +from django.contrib.auth.models import User +from apps.feed_import.models import UploadedOPML, OPMLImporter +from apps.reader.models import UserSubscription +from utils import log as logging + + +class ProcessOPML(Task): + + def run(self, user_id): + user = User.objects.get(pk=user_id) + logging.user(user, "~FR~SBOPML upload (task) starting...") + + opml = UploadedOPML.objects.filter(user_id=user_id).first() + opml_importer = OPMLImporter(opml.opml_file, user) + opml_importer.process() + + feed_count = UserSubscription.objects.filter(user=user).count() + user.profile.send_upload_opml_finished_email(feed_count) + logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count)) + diff --git a/apps/feed_import/views.py b/apps/feed_import/views.py index ba8095135..659decd0b 100644 --- a/apps/feed_import/views.py +++ b/apps/feed_import/views.py @@ -14,9 +14,12 @@ from django.contrib.auth import login as login_user from django.shortcuts import render_to_response from apps.reader.forms import SignupForm from apps.reader.models import UserSubscription -from apps.feed_import.models import OAuthToken, OPMLImporter, OPMLExporter, GoogleReaderImporter +from apps.feed_import.models import OAuthToken, GoogleReaderImporter +from apps.feed_import.models import OPMLImporter, OPMLExporter, UploadedOPML +from apps.feed_import.tasks import ProcessOPML from utils import json_functions as json from utils.user_functions import ajax_login_required, get_user +from utils.feed_functions import TimeoutError @ajax_login_required @@ -31,11 +34,24 @@ def opml_upload(request): logging.user(request, "~FR~SBOPML upload starting...") file = request.FILES['file'] xml_opml = file.read() + UploadedOPML.objects.create(user_id=request.user.pk, opml_file=xml_opml) + opml_importer = OPMLImporter(xml_opml, request.user) - folders = opml_importer.process() - feeds = UserSubscription.objects.filter(user=request.user).values() - payload = dict(folders=folders, feeds=feeds) - logging.user(request, "~FR~SBOPML Upload: ~SK%s~SN~SB~FR feeds" % (len(feeds))) + try: + folders = opml_importer.try_processing() + except TimeoutError: + folders = None + ProcessOPML.delay(request.user.pk) + feed_count = opml_importer.count_feeds_in_opml() + logging.user(request, "~FR~SBOPML pload took too long, found %s feeds. Tasking..." % feed_count) + payload = dict(folders=folders, delayed=True, feed_count=feed_count) + code = 2 + message = "" + + if folders: + feeds = UserSubscription.objects.filter(user=request.user).values() + payload = dict(folders=folders, feeds=feeds) + logging.user(request, "~FR~SBOPML Upload: ~SK%s~SN~SB~FR feeds" % (len(feeds))) request.session['import_from_google_reader'] = False else: diff --git a/apps/profile/models.py b/apps/profile/models.py index d2453b149..7ec2c4c91 100644 --- a/apps/profile/models.py +++ b/apps/profile/models.py @@ -214,6 +214,23 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()} logging.user(self.user, "~BB~FM~SBSending email for social beta: %s" % self.user.email) + def send_upload_opml_finished_email(self, feed_count): + if not self.user.email: + print "Please provide an email address." + return + + user = self.user + text = render_to_string('mail/email_upload_opml_finished.txt', locals()) + html = render_to_string('mail/email_upload_opml_finished.xhtml', locals()) + subject = "Your OPML upload is complete. Get going with NewsBlur!" + msg = EmailMultiAlternatives(subject, text, + from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, + to=['%s <%s>' % (user, user.email)]) + msg.attach_alternative(html, "text/html") + msg.send() + + logging.user(self.user, "~BB~FM~SBSending email for OPML upload: %s" % self.user.email) + def autologin_url(self, next=None): return reverse('autologin', kwargs={ 'username': self.user.username, diff --git a/fabfile.py b/fabfile.py index e7e103aad..b8fda3a51 100644 --- a/fabfile.py +++ b/fabfile.py @@ -374,7 +374,7 @@ def setup_psycopg(): def setup_python(): # sudo('easy_install -U pip') - sudo('easy_install -U fabric django==1.3.1 readline pyflakes iconv celery django-celery django-celery-with-redis django-compress South django-extensions pymongo==2.2.0 stripe BeautifulSoup pyyaml nltk lxml oauth2 pytz boto seacucumber django_ses mongoengine redis requests') + sudo('easy_install -U fabric django==1.3.1 readline pyflakes iconv celery django-celery django-celery-with-redis django-compress South django-extensions pymongo==2.2.0 stripe BeautifulSoup pyyaml nltk lxml oauth2 pytz boto seacucumber django_ses mongoengine redis requests psutil') put('config/pystartup.py', '.pystartup') # with cd(os.path.join(env.NEWSBLUR_PATH, 'vendor/cjson')): diff --git a/media/css/reader.css b/media/css/reader.css index b22580162..cbf5959fd 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -5872,6 +5872,13 @@ form.opml_import_form input { min-height: 234px; } +.NB-modal-intro .NB-intro-import-delayed { + color: #808080; + margin: 12px 0 0; + font-size: 12px; + text-align: center; + display: none; +} .NB-modal-intro .NB-intro-import { width: 300px; float: left; @@ -6337,16 +6344,15 @@ form.opml_import_form input { } .NB-modal-feedchooser .NB-modal-subtitle { - font-weight: normal; - font-size: 12px; - color: #606060; - text-shadow: 1px 1px 0 #F0F0F0; - width: 715px; + font-weight: normal; + font-size: 12px; + color: #606060; + text-shadow: 1px 1px 0 #F0F0F0; + width: 715px; } .NB-modal-feedchooser .NB-modal-subtitle b { -/* margin: 0 0 4px 0;*/ padding-right: 8px; - color: #303030; + color: #303030; } .NB-modal-feedchooser .NB-feedchooser-subtitle-type-prefix { @@ -6354,19 +6360,33 @@ form.opml_import_form input { } .NB-modal-feedchooser .NB-feedchooser-type { - float: left; - width: 345px; - margin: 0 12px 0 0; - padding: 0 12px 0 0; + float: left; + width: 345px; + margin: 0 52px 0 26px; + padding: 0; } .NB-modal-feedchooser .NB-feedchooser-type.NB-last { - margin: 0 0 0 0; - padding: 0 0 0 24px; - margin-right: 0; - border-left: 1px solid #B0B0B0; + margin: 0 0 0 0; + padding: 0 0 0 52px; + margin-right: 0; + border-left: 1px solid #B0B0B0; } +.NB-modal-feedchooser .NB-feedchooser-porpoise { + border-radius: 16px; + line-height: 48px; + color: #808080; + font-size: 18px; + padding: 8px 2px; + background-color: white; + position: absolute; + margin-top: -24px; + width: 30px; + text-align: center; + top: 50%; + left: -17px +} .NB-modal-feedchooser .NB-feedchooser-info { overflow: hidden; } @@ -6510,22 +6530,20 @@ form.opml_import_form input { position: absolute; width: 40px; height: 40px; - top: 0; + top: -10px; left: -40px; + display: none; } .NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-selected.NB-1 .NB-feedchooser-dollar-image { - top: -8px; - background: transparent url('/media/embed/reader/hamburgers.png') no-repeat 0 0; + display: block; } .NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-selected.NB-2 .NB-feedchooser-dollar-image { - top: -8px; - background: transparent url('/media/embed/reader/hamburgers.png') no-repeat 0 -36px; + display: block; } .NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-selected.NB-3 .NB-feedchooser-dollar-image { - top: -10px; - background: transparent url('/media/embed/reader/hamburgers.png') no-repeat 0 -72px; + display: block; } .NB-modal-feedchooser .NB-feedchooser-dollar-month { diff --git a/media/img/reader/hamburger_l.png b/media/img/reader/hamburger_l.png new file mode 100644 index 000000000..c0f28f385 Binary files /dev/null and b/media/img/reader/hamburger_l.png differ diff --git a/media/img/reader/hamburger_m.png b/media/img/reader/hamburger_m.png new file mode 100644 index 000000000..f9e78c9ef Binary files /dev/null and b/media/img/reader/hamburger_m.png differ diff --git a/media/img/reader/hamburger_s.png b/media/img/reader/hamburger_s.png new file mode 100644 index 000000000..aa612eb86 Binary files /dev/null and b/media/img/reader/hamburger_s.png differ diff --git a/media/js/newsblur/reader/reader.js b/media/js/newsblur/reader/reader.js index 80f8b1c86..360552bf8 100644 --- a/media/js/newsblur/reader/reader.js +++ b/media/js/newsblur/reader/reader.js @@ -774,11 +774,14 @@ this.hide_progress_bar(); }, - open_dialog_after_feeds_loaded: function() { + open_dialog_after_feeds_loaded: function(options) { + options = options || {}; if (!NEWSBLUR.Globals.is_authenticated) return; if (!NEWSBLUR.assets.folders.length || !NEWSBLUR.assets.preference('has_setup_feeds')) { - if (NEWSBLUR.assets.preference('has_setup_feeds')) { + if (options.delayed_import || this.flags.delayed_import) { + this.setup_ftux_add_feed_callout("Check your email..."); + } else if (NEWSBLUR.assets.preference('has_setup_feeds')) { this.setup_ftux_add_feed_callout(); } else if (!NEWSBLUR.intro || !NEWSBLUR.intro.flags.open) { _.defer(_.bind(this.open_intro_modal, this), 100); @@ -3774,11 +3777,11 @@ // = FTUX = // ======== - setup_ftux_add_feed_callout: function() { + setup_ftux_add_feed_callout: function(message) { var self = this; if (this.flags['bouncing_callout']) return; - $('.NB-callout-ftux .NB-callout-text').text('First things first...'); + $('.NB-callout-ftux .NB-callout-text').text(message || 'First things first...'); $('.NB-callout-ftux').corner('5px'); $('.NB-callout-ftux').css({ 'opacity': 0, diff --git a/media/js/newsblur/reader/reader_feedchooser.js b/media/js/newsblur/reader/reader_feedchooser.js index 11b60d7d9..ed355047b 100644 --- a/media/js/newsblur/reader/reader_feedchooser.js +++ b/media/js/newsblur/reader/reader_feedchooser.js @@ -60,7 +60,9 @@ NEWSBLUR.ReaderFeedchooser.prototype = { return false; }) ]), - $.make('div', { className: 'NB-feedchooser-type NB-last'}, [ + $.make('div', { className: 'NB-feedchooser-type NB-last', style: 'position: relative'}, [ + $.make('div', { className: 'NB-feedchooser-porpoise' }, 'OR'), + $.make('div', { className: 'NB-feedchooser-info'}, [ $.make('div', { className: 'NB-feedchooser-info-type' }, [ $.make('span', { className: 'NB-feedchooser-subtitle-type-prefix' }, 'Super-Mega'), @@ -105,21 +107,30 @@ NEWSBLUR.ReaderFeedchooser.prototype = { $.make('div', { className: 'NB-feedchooser-dollar' }, [ $.make('div', { className: 'NB-feedchooser-dollar-value NB-1' }, [ $.make('div', { className: 'NB-feedchooser-dollar-month' }, [ - $.make('div', { className: 'NB-feedchooser-dollar-image' }), + $.make('div', { className: 'NB-feedchooser-dollar-image' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/hamburger_s.png' }) + ]), '$12/year' ]), $.make('div', { className: 'NB-feedchooser-dollar-year' }, '($1/month)') ]), $.make('div', { className: 'NB-feedchooser-dollar-value NB-2' }, [ $.make('div', { className: 'NB-feedchooser-dollar-month' }, [ - $.make('div', { className: 'NB-feedchooser-dollar-image' }), + $.make('div', { className: 'NB-feedchooser-dollar-image' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/hamburger_s.png', style: "position: absolute; left: -26px;top: 1px" }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/hamburger_m.png', style: "position: absolute; left: 0px;top: 2px" }) + ]), '$24/year' ]), $.make('div', { className: 'NB-feedchooser-dollar-year' }, '($2/month)') ]), $.make('div', { className: 'NB-feedchooser-dollar-value NB-3' }, [ $.make('div', { className: 'NB-feedchooser-dollar-month' }, [ - $.make('div', { className: 'NB-feedchooser-dollar-image' }), + $.make('div', { className: 'NB-feedchooser-dollar-image' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/hamburger_s.png', style: "position: absolute; left: -58px;top: 0" }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/hamburger_m.png', style: "position: absolute; left: -31px;top: 2px" }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/hamburger_l.png', style: "position: absolute; left: 0; top: 0" }) + ]), '$36/year' ]), $.make('div', { className: 'NB-feedchooser-dollar-year' }, '($3/month)') @@ -205,8 +216,8 @@ NEWSBLUR.ReaderFeedchooser.prototype = { open_modal: function() { var self = this; this.$modal.modal({ - 'minWidth': 780, - 'maxWidth': 780, + 'minWidth': 860, + 'maxWidth': 860, 'overlayClose': true, 'onOpen': function (dialog) { dialog.overlay.fadeIn(200, function () { diff --git a/media/js/newsblur/reader/reader_intro.js b/media/js/newsblur/reader/reader_intro.js index 51ff54541..51d0e74be 100644 --- a/media/js/newsblur/reader/reader_intro.js +++ b/media/js/newsblur/reader/reader_intro.js @@ -10,6 +10,7 @@ NEWSBLUR.ReaderIntro = function(options) { 'twitter': {}, 'facebook': {} }; + this.flags = {}; this.autofollow = true; this.page_number = this.options.page_number; @@ -79,6 +80,11 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { $.make('h4'), $.make('div', { className: 'NB-intro-import-restart NB-modal-submit-grey NB-modal-submit-button' }, [ '« Restart and re-import your sites' + ]), + $.make('div', { className: 'NB-intro-import-delayed' }, [ + 'There are too many sites to process...', + $.make('br'), + 'You will be emailed within a minute or three.' ]) ]) ]), @@ -303,9 +309,9 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { } else if (page_number > page_count) { NEWSBLUR.assets.preference('has_setup_feeds', true); NEWSBLUR.reader.check_hide_getting_started(); - this.close(function() { - NEWSBLUR.reader.open_dialog_after_feeds_loaded(); - }); + this.close(_.bind(function() { + NEWSBLUR.reader.open_dialog_after_feeds_loaded({delayed_import: this.flags.delayed_import}); + }, this)); return; } else if (page_number == 1) { $('.NB-tutorial-next-page-text', this.$modal).text("Let's Get Started "); @@ -345,6 +351,7 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { if (NEWSBLUR.assets.feeds.size() && !this.options.force_import) { page = 2; $('.NB-intro-imports-sites', this.$modal).addClass('active'); + $('.NB-intro-import-delayed', this.$modal).hide(); } else { page = 0; $('.NB-intro-imports-start', this.$modal).addClass('active'); @@ -360,11 +367,12 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { this.count_feeds(); }, - count_feeds: function() { - var feed_count = NEWSBLUR.assets.feeds.size(); + count_feeds: function(fake_feed_count) { + var feed_count = fake_feed_count || NEWSBLUR.assets.feeds.size(); $(".NB-intro-imports-sites h4", this.$modal).text([ 'You are subscribed to ', + (fake_feed_count && 'at least '), Inflector.pluralize(' site', feed_count, true), '.' ].join("")); @@ -464,10 +472,16 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { var params = { url: NEWSBLUR.URLs['opml-upload'], type: 'POST', + dataType: 'json', success: function (data, status) { NEWSBLUR.assets.load_feeds(function() { $loading.removeClass('NB-active'); self.advance_import_carousel(2); + if (data.payload.delayed) { + NEWSBLUR.reader.flags.delayed_import = true; + self.count_feeds(data.payload.feed_count); + $('.NB-intro-import-delayed', self.$modal).show(); + } }); NEWSBLUR.reader.load_recommended_feed(); }, diff --git a/settings.py b/settings.py index 378747e50..d47e5df5e 100644 --- a/settings.py +++ b/settings.py @@ -310,7 +310,10 @@ BROKER_URL = "redis://db01:6379/0" CELERY_REDIS_HOST = "db01" CELERYD_PREFETCH_MULTIPLIER = 1 -CELERY_IMPORTS = ("apps.rss_feeds.tasks", "apps.social.tasks", "apps.reader.tasks",) +CELERY_IMPORTS = ("apps.rss_feeds.tasks", + "apps.social.tasks", + "apps.reader.tasks", + "apps.feed_import.tasks",) CELERYD_CONCURRENCY = 4 CELERY_IGNORE_RESULT = True CELERY_ACKS_LATE = True # Retry if task fails diff --git a/templates/mail/email_base.txt b/templates/mail/email_base.txt index b5a340a52..26e73ba43 100644 --- a/templates/mail/email_base.txt +++ b/templates/mail/email_base.txt @@ -1,3 +1,5 @@ +{% load utils_tags %} + {% block body %}{% endblock body %} - Samuel & Roy @@ -18,10 +20,10 @@ Stay up to date and in touch with us, yr. developers, in a few different ways: There's plenty of ways to use NewsBlur beyond the website: - * Download the free iPhone App: http://www.newsblur.com/iphone/ + * Download the free iPhone App: http://{% current_domain %}/iphone/ * Download the Android App on the Android Market: https://market.android.com/details?id=bitwrit.Blar - * Download browser extensions for Safari, Firefox, and Chrome: http://www.newsblur.com{{ user.profile.autologin_url }}?next=goodies + * Download browser extensions for Safari, Firefox, and Chrome: http://{% current_domain %}{{ user.profile.autologin_url }}?next=goodies ----------------------------------------------------------------------------- -Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur: http://www.newsblur.com{{ user.profile.autologin_url }}?next=optout \ No newline at end of file +Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur: http://{% current_domain %}{{ user.profile.autologin_url }}?next=optout \ No newline at end of file diff --git a/templates/mail/email_base.xhtml b/templates/mail/email_base.xhtml index d395e97e9..5f1c7d0e9 100644 --- a/templates/mail/email_base.xhtml +++ b/templates/mail/email_base.xhtml @@ -1,3 +1,5 @@ +{% load utils_tags %} + @@ -12,11 +14,11 @@ - @@ -62,7 +64,7 @@ @@ -71,7 +73,7 @@
- NewsBlur + NewsBlur
+ {% block body %}{% endblock %} @@ -32,23 +34,23 @@

Stay up to date and in touch with us, yr. developers, in a few different ways:

{% block resources_header %}To get the most out of NewsBlur, here are a few resources:{% endblock resources_header %}

There's plenty of ways to use NewsBlur beyond the web:

- Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur. + Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur.

- NewsBlur + NewsBlur
diff --git a/templates/mail/email_upload_opml_finished.txt b/templates/mail/email_upload_opml_finished.txt new file mode 100644 index 000000000..7383d4824 --- /dev/null +++ b/templates/mail/email_upload_opml_finished.txt @@ -0,0 +1,7 @@ +{% extends "mail/email_base.txt" %} + +{% load utils_tags %} + +{% block body %}Good news! NewsBlur has finished importing your OPML file. You are now subscribed to {{ feed_count }} site{{ feed_count|pluralize }}. + +Head over to NewsBlur and get reading: http://{% current_domain %}{% endblock body %} \ No newline at end of file diff --git a/templates/mail/email_upload_opml_finished.xhtml b/templates/mail/email_upload_opml_finished.xhtml new file mode 100644 index 000000000..a8fe8a646 --- /dev/null +++ b/templates/mail/email_upload_opml_finished.xhtml @@ -0,0 +1,15 @@ +{% extends "mail/email_base.xhtml" %} + +{% load utils_tags %} + +{% block body %} + +

+ Good news! NewsBlur has finished importing your OPML file. You are now + subscribed to {{ feed_count }} site{{ feed_count|pluralize }}. +

+ +

+ Head over to NewsBlur and get reading. +

+{% endblock %} diff --git a/utils/feed_functions.py b/utils/feed_functions.py index 468185824..4b59516cb 100644 --- a/utils/feed_functions.py +++ b/utils/feed_functions.py @@ -27,19 +27,18 @@ def timelimit(timeout): self.result = function(*args, **kw) except: self.error = sys.exc_info() - if not settings.DEBUG and not settings.TEST_DEBUG: - c = Dispatch() - c.join(timeout) - if c.isAlive(): - raise TimeoutError, 'took too long' - if c.error: - tb = ''.join(traceback.format_exception(c.error[0], c.error[1], c.error[2])) - logging.debug(tb) - mail_admins('Error in timeout: %s' % c.error[0], tb) - raise c.error[0], c.error[1] - return c.result - else: - return function(*args, **kw) + c = Dispatch() + c.join(timeout) + if c.isAlive(): + raise TimeoutError, 'took too long' + if not settings.DEBUG and not settings.TEST_DEBUG and c.error: + tb = ''.join(traceback.format_exception(c.error[0], c.error[1], c.error[2])) + logging.debug(tb) + mail_admins('Error in timeout: %s' % c.error[0], tb) + raise c.error[0], c.error[1] + return c.result + # else: + # return function(*args, **kw) return _2 return _1