diff --git a/apps/profile/forms.py b/apps/profile/forms.py
new file mode 100644
index 000000000..2b0e64cbc
--- /dev/null
+++ b/apps/profile/forms.py
@@ -0,0 +1,33 @@
+from django import forms
+from vendor.zebra.forms import StripePaymentForm
+from django.utils.safestring import mark_safe
+
+PLANS = [
+ ("newsblur-premium-12", mark_safe("$12 / year ($1/month)")),
+ ("newsblur-premium-24", mark_safe("$24 / year ($2/month)")),
+ ("newsblur-premium-36", mark_safe("$36 / year ($3/month)")),
+]
+
+class HorizRadioRenderer(forms.RadioSelect.renderer):
+ """ this overrides widget method to put radio buttons horizontally
+ instead of vertically.
+ """
+ def render(self):
+ """Outputs radios"""
+ choices = '\n'.join(['%s\n' % w for w in self])
+ return mark_safe('
%s
' % choices)
+
+class StripePlusPaymentForm(StripePaymentForm):
+ def __init__(self, *args, **kwargs):
+ email = kwargs.pop('email')
+ plan = kwargs.pop('plan', '')
+ super(StripePlusPaymentForm, self).__init__(*args, **kwargs)
+ self.fields['email'].initial = email
+ if plan:
+ self.fields['plan'].initial = plan
+
+ email = forms.EmailField(widget=forms.TextInput(attrs=dict(maxlength=75)),
+ label='Email address',
+ required=False)
+ plan = forms.ChoiceField(required=False, widget=forms.RadioSelect(renderer=HorizRadioRenderer),
+ choices=PLANS, label='Plan')
diff --git a/apps/profile/migrations/0016_profile_stripe.py b/apps/profile/migrations/0016_profile_stripe.py
new file mode 100644
index 000000000..cb5ace18c
--- /dev/null
+++ b/apps/profile/migrations/0016_profile_stripe.py
@@ -0,0 +1,85 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Profile.stripe_4_digits'
+ db.add_column('profile_profile', 'stripe_4_digits', self.gf('django.db.models.fields.CharField')(max_length=4, null=True, blank=True), keep_default=False)
+
+ # Adding field 'Profile.stripe_id'
+ db.add_column('profile_profile', 'stripe_id', self.gf('django.db.models.fields.CharField')(max_length=24, null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Profile.stripe_4_digits'
+ db.delete_column('profile_profile', 'stripe_4_digits')
+
+ # Deleting field 'Profile.stripe_id'
+ db.delete_column('profile_profile', 'stripe_id')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'profile.profile': {
+ 'Meta': {'object_name': 'Profile'},
+ 'collapsed_folders': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+ 'feed_pane_size': ('django.db.models.fields.IntegerField', [], {'default': '240'}),
+ 'hide_mobile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_premium': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_seen_ip': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'last_seen_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'preferences': ('django.db.models.fields.TextField', [], {'default': "'{}'"}),
+ 'secret_token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+ 'send_emails': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'stripe_4_digits': ('django.db.models.fields.CharField', [], {'max_length': '4', 'null': 'True', 'blank': 'True'}),
+ 'stripe_id': ('django.db.models.fields.CharField', [], {'max_length': '24', 'null': 'True', 'blank': 'True'}),
+ 'timezone': ('vendor.timezones.fields.TimeZoneField', [], {'default': "'America/New_York'", 'max_length': '100'}),
+ 'tutorial_finished': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
+ 'view_settings': ('django.db.models.fields.TextField', [], {'default': "'{}'"})
+ }
+ }
+
+ complete_apps = ['profile']
diff --git a/apps/profile/models.py b/apps/profile/models.py
index 0a9b23ce7..fb8aec045 100644
--- a/apps/profile/models.py
+++ b/apps/profile/models.py
@@ -18,7 +18,7 @@ from utils import log as logging
from utils.user_functions import generate_secret_token
from vendor.timezones.fields import TimeZoneField
from vendor.paypal.standard.ipn.signals import subscription_signup
-
+from zebra.signals import zebra_webhook_customer_subscription_created
class Profile(models.Model):
user = models.OneToOneField(User, unique=True, related_name="profile")
@@ -34,6 +34,8 @@ class Profile(models.Model):
last_seen_ip = models.CharField(max_length=50, blank=True, null=True)
timezone = TimeZoneField(default="America/New_York")
secret_token = models.CharField(max_length=12, blank=True, null=True)
+ stripe_4_digits = models.CharField(max_length=4, blank=True, null=True)
+ stripe_id = models.CharField(max_length=24, blank=True, null=True)
def __unicode__(self):
return "%s <%s> (Premium: %s)" % (self.user, self.user.email, self.is_premium)
@@ -158,9 +160,6 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
msg.attach_alternative(html, "text/html")
msg.send(fail_silently=True)
- user.set_password('')
- user.save()
-
logging.user(self.user, "~BB~FM~SBSending email for forgotten password: %s" % self.user.email)
def autologin_url(self, next=None):
@@ -190,6 +189,11 @@ def paypal_signup(sender, **kwargs):
user.profile.activate_premium()
subscription_signup.connect(paypal_signup)
+def stripe_signup(sender, full_json, **kwargs):
+ profile = Profile.objects.get(stripe_id=full_json['data']['object']['customer'])
+ profile.activate_premium()
+zebra_webhook_customer_subscription_created.connect(stripe_signup)
+
def change_password(user, old_password, new_password):
user_db = authenticate(username=user.username, password=old_password)
if user_db is None:
diff --git a/apps/profile/urls.py b/apps/profile/urls.py
index 855a92ace..18f5fe293 100644
--- a/apps/profile/urls.py
+++ b/apps/profile/urls.py
@@ -12,4 +12,5 @@ urlpatterns = patterns('',
url(r'^paypal_return/?', views.paypal_return, name='paypal-return'),
url(r'^is_premium/?', views.profile_is_premium, name='profile-is-premium'),
url(r'^paypal_ipn/?', include('paypal.standard.ipn.urls'), name='paypal-ipn'),
+ url(r'^stripe_form/?', views.stripe_form, name='stripe-form'),
)
diff --git a/apps/profile/views.py b/apps/profile/views.py
index e4c25d3d1..2b3d918d3 100644
--- a/apps/profile/views.py
+++ b/apps/profile/views.py
@@ -1,3 +1,4 @@
+import stripe
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.http import HttpResponse, HttpResponseRedirect
@@ -7,11 +8,13 @@ from django.core.urlresolvers import reverse
from django.template import RequestContext
from django.shortcuts import render_to_response
from django.core.mail import mail_admins
-from utils import json_functions as json
-from vendor.paypal.standard.forms import PayPalPaymentsForm
-from utils.user_functions import ajax_login_required
+from django.conf import settings
from apps.profile.models import Profile, change_password
from apps.reader.models import UserSubscription
+from apps.profile.forms import StripePlusPaymentForm, PLANS
+from utils import json_functions as json
+from utils.user_functions import ajax_login_required
+from vendor.paypal.standard.forms import PayPalPaymentsForm
SINGLE_FIELD_PREFS = ('timezone','feed_pane_size','tutorial_finished','hide_mobile','send_emails',)
SPECIAL_PREFERENCES = ('old_password', 'new_password',)
@@ -60,28 +63,28 @@ def get_preference(request):
def set_account_settings(request):
code = 1
message = ''
- settings = request.POST
+ post_settings = request.POST
- if settings['username'] and request.user.username != settings['username']:
+ if post_settings['username'] and request.user.username != post_settings['username']:
try:
- User.objects.get(username__iexact=settings['username'])
+ User.objects.get(username__iexact=post_settings['username'])
except User.DoesNotExist:
- request.user.username = settings['username']
+ request.user.username = post_settings['username']
request.user.save()
else:
code = -1
message = "This username is already taken. Try something different."
- if request.user.email != settings['email']:
- if not User.objects.filter(email=settings['email']).count():
- request.user.email = settings['email']
+ if request.user.email != post_settings['email']:
+ if not User.objects.filter(email=post_settings['email']).count():
+ request.user.email = post_settings['email']
request.user.save()
else:
code = -2
message = "This email is already being used by another account. Try something different."
- if code != -1 and (settings['old_password'] or settings['new_password']):
- code = change_password(request.user, settings['old_password'], settings['new_password'])
+ if code != -1 and (post_settings['old_password'] or post_settings['new_password']):
+ code = change_password(request.user, post_settings['old_password'], post_settings['new_password'])
if code == -3:
message = "Your old password is incorrect."
@@ -192,4 +195,46 @@ def profile_is_premium(request):
'activated_subs': activated_subs,
'total_subs': total_subs,
}
-
\ No newline at end of file
+
+@login_required
+def stripe_form(request):
+ user = request.user
+ success_updating = False
+ stripe.api_key = settings.STRIPE_SECRET
+ plan = int(request.GET.get('plan', 2))
+ plan = PLANS[plan-1][0]
+
+ if request.method == 'POST':
+ zebra_form = StripePlusPaymentForm(request.POST, email=user.email)
+ if zebra_form.is_valid():
+ user.email = zebra_form.cleaned_data['email']
+ user.save()
+
+ customer = stripe.Customer.create(**{
+ 'card': zebra_form.cleaned_data['stripe_token'],
+ 'plan': zebra_form.cleaned_data['plan'],
+ 'email': user.email,
+ 'description': user.username,
+ })
+
+ user.profile.strip_4_digits = zebra_form.cleaned_data['last_4_digits']
+ user.profile.stripe_id = customer.id
+ user.profile.save()
+
+ success_updating = True
+
+ else:
+ zebra_form = StripePlusPaymentForm(email=user.email, plan=plan)
+
+ if success_updating:
+ return render_to_response('reader/paypal_return.xhtml',
+ {}, context_instance=RequestContext(request))
+
+ return render_to_response('profile/stripe_form.xhtml',
+ {
+ 'zebra_form': zebra_form,
+ 'publishable': settings.STRIPE_PUBLISHABLE,
+ 'success_updating': success_updating,
+ },
+ context_instance=RequestContext(request)
+ )
diff --git a/apps/reader/forms.py b/apps/reader/forms.py
index df4a743c4..a7fb0b1ef 100644
--- a/apps/reader/forms.py
+++ b/apps/reader/forms.py
@@ -27,25 +27,29 @@ class LoginForm(forms.Form):
user = User.objects.filter(Q(username__iexact=username) | Q(email=username))
if username and user:
self.user_cache = authenticate(username=user[0].username, password=password)
+ if self.user_cache is None:
+ self.user_cache = authenticate(username=user[0].username, password="")
if self.user_cache is None:
email_username = User.objects.filter(email=username)
if email_username:
self.user_cache = authenticate(username=email_username[0].username, password=password)
- if self.user_cache is None:
- # logging.info(" ***> [%s] Bad Login: TRYING JK-LESS PASSWORD" % username)
- jkless_password = password.replace('j', '').replace('k', '')
- self.user_cache = authenticate(username=username, password=jkless_password)
if self.user_cache is None:
- logging.info(" ***> [%s] Bad Login" % username)
- raise forms.ValidationError(_("Whoopsy-daisy. Try again."))
- else:
- # Supreme fuck-up. Accidentally removed the letters J and K from
- # all user passwords. Re-save with correct password.
- logging.info(" ***> [%s] FIXING JK-LESS PASSWORD" % username)
- self.user_cache.set_password(password)
- self.user_cache.save()
- elif not self.user_cache.is_active:
- raise forms.ValidationError(_("This account is inactive."))
+ self.user_cache = authenticate(username=email_username[0].username, password="")
+ if self.user_cache is None:
+ # logging.info(" ***> [%s] Bad Login: TRYING JK-LESS PASSWORD" % username)
+ jkless_password = password.replace('j', '').replace('k', '')
+ self.user_cache = authenticate(username=username, password=jkless_password)
+ if self.user_cache is None:
+ logging.info(" ***> [%s] Bad Login" % username)
+ raise forms.ValidationError(_("Whoopsy-daisy. Try again."))
+ else:
+ # Supreme fuck-up. Accidentally removed the letters J and K from
+ # all user passwords. Re-save with correct password.
+ logging.info(" ***> [%s] FIXING JK-LESS PASSWORD" % username)
+ self.user_cache.set_password(password)
+ self.user_cache.save()
+ if not self.user_cache.is_active:
+ raise forms.ValidationError(_("This account is inactive."))
elif username and not user:
raise forms.ValidationError(_("That username is not registered. Create an account with it instead."))
@@ -80,11 +84,6 @@ class SignupForm(forms.Form):
def clean_username(self):
username = self.cleaned_data['username']
- try:
- User.objects.get(username__iexact=username)
- except User.DoesNotExist:
- return username
- raise forms.ValidationError(_(u'Someone is already using that username.'))
return username
def clean_password(self):
@@ -96,15 +95,36 @@ class SignupForm(forms.Form):
if not self.cleaned_data['email']:
return ""
return self.cleaned_data['email']
-
+
+ def clean(self):
+ username = self.cleaned_data.get('username', '')
+ password = self.cleaned_data.get('password', '')
+ exists = User.objects.filter(username__iexact=username).count()
+ if exists:
+ user_auth = authenticate(username=username, password=password)
+ if not user_auth:
+ raise forms.ValidationError(_(u'Someone is already using that username.'))
+ return self.cleaned_data
+
def save(self, profile_callback=None):
- new_user = User(username=self.cleaned_data['username'])
- new_user.set_password(self.cleaned_data['password'])
+ username = self.cleaned_data['username']
+ password = self.cleaned_data['password']
+
+ exists = User.objects.filter(username__iexact=username).count()
+ if exists:
+ user_auth = authenticate(username=username, password=password)
+ if not user_auth:
+ raise forms.ValidationError(_(u'Someone is already using that username.'))
+ else:
+ return user_auth
+
+ new_user = User(username=username)
+ new_user.set_password(password)
new_user.is_active = True
new_user.email = self.cleaned_data['email']
new_user.save()
- new_user = authenticate(username=self.cleaned_data['username'],
- password=self.cleaned_data['password'])
+ new_user = authenticate(username=username,
+ password=password)
new_user.profile.send_new_user_email()
return new_user
diff --git a/apps/rss_feeds/management/commands/refresh_feeds.py b/apps/rss_feeds/management/commands/refresh_feeds.py
index 20cfb4418..b257aab71 100644
--- a/apps/rss_feeds/management/commands/refresh_feeds.py
+++ b/apps/rss_feeds/management/commands/refresh_feeds.py
@@ -1,6 +1,7 @@
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
+from apps.statistics.models import MStatistics
from apps.rss_feeds.models import Feed
from optparse import make_option
from utils import feed_fetcher
@@ -31,7 +32,9 @@ class Command(BaseCommand):
def handle(self, *args, **options):
if options['daemonize']:
daemonize()
-
+
+ options['fake'] = bool(MStatistics.get('fake_fetch'))
+
settings.LOG_TO_STREAM = True
now = datetime.datetime.utcnow()
@@ -64,6 +67,7 @@ class Command(BaseCommand):
num_workers = 1
options['compute_scores'] = True
+ options['quick'] = ".5"
disp = feed_fetcher.Dispatcher(options, num_workers)
diff --git a/apps/rss_feeds/management/commands/task_feeds.py b/apps/rss_feeds/management/commands/task_feeds.py
index fb5e758c2..0eeb9687f 100644
--- a/apps/rss_feeds/management/commands/task_feeds.py
+++ b/apps/rss_feeds/management/commands/task_feeds.py
@@ -48,10 +48,10 @@ class Command(BaseCommand):
).order_by('?')
if feeds: Feed.task_feeds(feeds)
- feeds = Feed.objects.filter(
- last_update__lte=day,
- active_subscribers__gte=1,
- active=False,
- known_good=True
- ).order_by('?')
- if feeds: Feed.task_feeds(feeds)
\ No newline at end of file
+ # feeds = Feed.objects.filter(
+ # last_update__lte=day,
+ # active_subscribers__gte=1,
+ # active=False,
+ # known_good=True
+ # ).order_by('?')
+ # if feeds: Feed.task_feeds(feeds)
\ No newline at end of file
diff --git a/apps/rss_feeds/models.py b/apps/rss_feeds/models.py
index bb0fdf06e..78809ef10 100644
--- a/apps/rss_feeds/models.py
+++ b/apps/rss_feeds/models.py
@@ -4,7 +4,6 @@ import random
import re
import math
import mongoengine as mongo
-import redis
import zlib
import urllib
import hashlib
@@ -13,7 +12,6 @@ from operator import itemgetter
# from nltk.collocations import TrigramCollocationFinder, BigramCollocationFinder, TrigramAssocMeasures, BigramAssocMeasures
from django.db import models
from django.db import IntegrityError
-from django.core.cache import cache
from django.conf import settings
from django.db.models.query import QuerySet
from mongoengine.queryset import OperationError
@@ -287,6 +285,10 @@ class Feed(models.Model):
self.save_feed_history(505, 'Timeout', '')
feed_address = None
+ if feed_address:
+ self.has_feed_exception = True
+ self.schedule_feed_fetch_immediately()
+
return not not feed_address
def save_feed_history(self, status_code, message, exception=None):
@@ -304,7 +306,8 @@ class Feed(models.Model):
# for history in old_fetch_histories:
# history.delete()
if status_code not in (200, 304):
- self.count_errors_in_history('feed', status_code)
+ errors, non_errors = self.count_errors_in_history('feed', status_code)
+ self.set_next_scheduled_update(error_count=len(errors), non_error_count=len(non_errors))
elif self.has_feed_exception:
self.has_feed_exception = False
self.active = True
@@ -328,13 +331,14 @@ class Feed(models.Model):
self.save()
def count_errors_in_history(self, exception_type='feed', status_code=None):
+ logging.debug(' ---> [%-30s] Counting errors in history...' % (unicode(self)[:30]))
history_class = MFeedFetchHistory if exception_type == 'feed' else MPageFetchHistory
fetch_history = map(lambda h: h.status_code,
history_class.objects(feed_id=self.pk)[:50])
non_errors = [h for h in fetch_history if int(h) in (200, 304)]
errors = [h for h in fetch_history if int(h) not in (200, 304)]
-
- if len(non_errors) == 0 and len(errors) >= 1:
+
+ if len(non_errors) == 0 and len(errors) > 1:
if exception_type == 'feed':
self.has_feed_exception = True
self.active = False
@@ -345,6 +349,10 @@ class Feed(models.Model):
elif self.exception_code > 0:
self.active = True
self.exception_code = 0
+ if exception_type == 'feed':
+ self.has_feed_exception = False
+ elif exception_type == 'page':
+ self.has_page_exception = False
self.save()
return errors, non_errors
@@ -595,21 +603,26 @@ class Feed(models.Model):
self.data.feed_classifier_counts = json.encode(scores)
self.data.save()
- def update(self, verbose=False, force=False, single_threaded=True, compute_scores=True):
+ def update(self, verbose=False, force=False, single_threaded=True, compute_scores=True, options=None):
from utils import feed_fetcher
+ if not options:
+ options = {}
if settings.DEBUG:
self.feed_address = self.feed_address % {'NEWSBLUR_DIR': settings.NEWSBLUR_DIR}
self.feed_link = self.feed_link % {'NEWSBLUR_DIR': settings.NEWSBLUR_DIR}
+ self.last_update = datetime.datetime.utcnow()
self.set_next_scheduled_update()
- options = {
+ options.update({
'verbose': verbose,
'timeout': 10,
'single_threaded': single_threaded,
'force': force,
'compute_scores': compute_scores,
- }
+ 'fake': options.get('fake'),
+ 'quick': options.get('quick'),
+ })
disp = feed_fetcher.Dispatcher(options, 1)
disp.add_jobs([[self.pk]])
disp.run_jobs()
@@ -665,9 +678,15 @@ class Feed(models.Model):
original_content = None
try:
if existing_story and existing_story.id:
- existing_story = MStory.objects.get(id=existing_story.id)
+ try:
+ existing_story = MStory.objects.get(story_feed_id=existing_story.story_feed_id,
+ id=existing_story.id)
+ except ValidationError:
+ existing_story = MStory.objects.get(story_feed_id=existing_story.story_feed_id,
+ story_guid=existing_story.id)
elif existing_story and existing_story.story_guid:
- existing_story = MStory.objects.get(story_feed_id=existing_story.story_feed_id, story_guid=existing_story.story_guid)
+ existing_story = MStory.objects.get(story_feed_id=existing_story.story_feed_id,
+ story_guid=existing_story.story_guid)
else:
raise MStory.DoesNotExist
except (MStory.DoesNotExist, OperationError), e:
@@ -1007,11 +1026,12 @@ class Feed(models.Model):
return total, random_factor*2
- def set_next_scheduled_update(self, multiplier=1):
+ def set_next_scheduled_update(self, error_count=0, non_error_count=0):
total, random_factor = self.get_next_scheduled_update(force=True, verbose=False)
- if multiplier > 1:
- total = total * multiplier
+ if error_count:
+ total = total * error_count
+ logging.debug(' ---> [%-30s] ~FBScheduling feed fetch geometrically: ~SB%s errors, %s non-errors. Total: %s' % (unicode(self)[:30], error_count, non_error_count, total))
next_scheduled_update = datetime.datetime.utcnow() + datetime.timedelta(
minutes = total + random_factor)
@@ -1022,14 +1042,11 @@ class Feed(models.Model):
self.save()
def schedule_feed_fetch_immediately(self):
+ logging.debug(' ---> [%-30s] Scheduling feed fetch immediately...' % (unicode(self)[:30]))
self.next_scheduled_update = datetime.datetime.utcnow()
self.save()
- def schedule_feed_fetch_geometrically(self):
- errors, non_errors = self.count_errors_in_history('feed')
- self.set_next_scheduled_update(multiplier=len(errors))
-
# def calculate_collocations_story_content(self,
# collocation_measures=TrigramAssocMeasures,
# collocation_finder=TrigramCollocationFinder):
diff --git a/apps/rss_feeds/page_importer.py b/apps/rss_feeds/page_importer.py
index a81e3a6da..18e9cba41 100644
--- a/apps/rss_feeds/page_importer.py
+++ b/apps/rss_feeds/page_importer.py
@@ -75,7 +75,8 @@ class PageImporter(object):
else:
self.save_no_page()
return
- except (ValueError, urllib2.URLError, httplib.BadStatusLine, httplib.InvalidURL), e:
+ except (ValueError, urllib2.URLError, httplib.BadStatusLine, httplib.InvalidURL,
+ requests.exceptions.ConnectionError), e:
self.feed.save_page_history(401, "Bad URL", e)
fp = feedparser.parse(self.feed.feed_address)
feed_link = fp.feed.get('link', "")
@@ -88,6 +89,7 @@ class PageImporter(object):
LookupError,
requests.packages.urllib3.exceptions.HTTPError), e:
logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed, e))
+ mail_feed_error_to_admin(self.feed, e, locals())
return self.fetch_page(urllib_fallback=True, requests_exception=e)
except Exception, e:
logging.debug('[%d] ! -------------------------' % (self.feed.id,))
diff --git a/apps/rss_feeds/tasks.py b/apps/rss_feeds/tasks.py
index e22871e04..19fa6f6fc 100644
--- a/apps/rss_feeds/tasks.py
+++ b/apps/rss_feeds/tasks.py
@@ -9,13 +9,20 @@ class UpdateFeeds(Task):
def run(self, feed_pks, **kwargs):
from apps.rss_feeds.models import Feed
+ from apps.statistics.models import MStatistics
+
+ options = {
+ 'fake': bool(MStatistics.get('fake_fetch')),
+ 'quick': float(MStatistics.get('quick_fetch', 0)),
+ }
+
if not isinstance(feed_pks, list):
feed_pks = [feed_pks]
for feed_pk in feed_pks:
try:
feed = Feed.objects.get(pk=feed_pk)
- feed.update()
+ feed.update(options=options)
except Feed.DoesNotExist:
logging.info(" ---> Feed doesn't exist: [%s]" % feed_pk)
# logging.debug(' Updating: [%s] %s' % (feed_pks, feed))
diff --git a/apps/rss_feeds/views.py b/apps/rss_feeds/views.py
index 0e29a488e..bb95bf95d 100644
--- a/apps/rss_feeds/views.py
+++ b/apps/rss_feeds/views.py
@@ -19,8 +19,11 @@ from utils.view_functions import get_argument_or_404
@json.json_view
def search_feed(request):
- address = request.REQUEST['address']
+ address = request.REQUEST.get('address')
offset = int(request.REQUEST.get('offset', 0))
+ if not address:
+ return dict(code=-1, message="Please provide a URL/address.")
+
feed = Feed.get_feed_from_url(address, create=False, aggressive=True, offset=offset)
if feed:
diff --git a/apps/statistics/models.py b/apps/statistics/models.py
index 339c2aaa4..26cbbe02e 100644
--- a/apps/statistics/models.py
+++ b/apps/statistics/models.py
@@ -20,6 +20,19 @@ class MStatistics(mongo.Document):
def __unicode__(self):
return "%s: %s" % (self.key, self.value)
+ @classmethod
+ def get(cls, key, default=None):
+ obj = cls.objects.filter(key=key).first()
+ if not obj:
+ return default
+ return obj.value
+
+ @classmethod
+ def set(cls, key, value):
+ obj, _ = cls.objects.get_or_create(key=key)
+ obj.value = value
+ obj.save()
+
@classmethod
def all(cls):
values = dict([(stat.key, stat.value) for stat in cls.objects.all()])
@@ -51,11 +64,11 @@ class MStatistics(mongo.Document):
def collect_statistics_feeds_fetched(cls, last_day=None):
if not last_day:
last_day = datetime.datetime.now() - datetime.timedelta(hours=24)
- last_biweek = datetime.datetime.now() - datetime.timedelta(days=14)
+ last_month = datetime.datetime.now() - datetime.timedelta(days=30)
- feeds_fetched = MFeedFetchHistory.objects.filter(fetch_date__lt=last_day).count()
+ feeds_fetched = MFeedFetchHistory.objects.filter(fetch_date__gte=last_day).count()
cls.objects(key='feeds_fetched').update_one(upsert=True, key='feeds_fetched', value=feeds_fetched)
- pages_fetched = MPageFetchHistory.objects.filter(fetch_date__lt=last_day).count()
+ pages_fetched = MPageFetchHistory.objects.filter(fetch_date__gte=last_day).count()
cls.objects(key='pages_fetched').update_one(upsert=True, key='pages_fetched', value=pages_fetched)
from utils.feed_functions import timelimit, TimeoutError
@@ -63,8 +76,8 @@ class MStatistics(mongo.Document):
def delete_old_history():
MFeedFetchHistory.objects(fetch_date__lt=last_day, status_code__in=[200, 304]).delete()
MPageFetchHistory.objects(fetch_date__lt=last_day, status_code__in=[200, 304]).delete()
- MFeedFetchHistory.objects(fetch_date__lt=last_biweek).delete()
- MPageFetchHistory.objects(fetch_date__lt=last_biweek).delete()
+ MFeedFetchHistory.objects(fetch_date__lt=last_month).delete()
+ MPageFetchHistory.objects(fetch_date__lt=last_month).delete()
try:
delete_old_history()
except TimeoutError:
diff --git a/assets.yml b/assets.yml
index 008d5b9cb..f5577c18a 100644
--- a/assets.yml
+++ b/assets.yml
@@ -87,7 +87,8 @@ javascripts:
- media/js/newsblur/reader_utils.js
- media/js/newsblur/assetmodel.js
- media/js/mobile/newsblur/mobile_workspace.js
- paypal:
+ payments:
+ - media/js/newsblur/stripe_form.js
- media/js/newsblur/paypal_return.js
bookmarklet:
- media/js/jquery-1.5.1.min.js
diff --git a/fabfile.py b/fabfile.py
index e1b974aa2..41c0ec584 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -65,32 +65,35 @@ def task():
# = Deploy =
# ==========
+@parallel
def pull():
with cd(env.NEWSBLUR_PATH):
run('git pull')
def pre_deploy():
- compress_assets()
+ compress_assets(bundle=True)
def post_deploy():
cleanup_assets()
@parallel
def deploy():
- deploy_code()
+ deploy_code(copy_assets=True)
post_deploy()
def deploy_full():
deploy_code(full=True)
post_deploy()
-def deploy_code(full=False):
+@parallel
+def deploy_code(copy_assets=False, full=False):
with cd(env.NEWSBLUR_PATH):
run('git pull')
run('mkdir -p static')
if full:
run('rm -fr static/*')
- transfer_assets()
+ if copy_assets:
+ transfer_assets()
if full:
with settings(warn_only=True):
run('sudo supervisorctl restart gunicorn')
@@ -131,12 +134,14 @@ def celery():
celery_stop()
celery_start()
+@parallel
def celery_stop():
with cd(env.NEWSBLUR_PATH):
run('sudo supervisorctl stop celery')
with settings(warn_only=True):
run('./utils/kill_celery.sh')
+@parallel
def celery_start():
with cd(env.NEWSBLUR_PATH):
run('sudo supervisorctl start celery')
@@ -146,7 +151,7 @@ def kill_celery():
with cd(env.NEWSBLUR_PATH):
run('ps aux | grep celeryd | egrep -v grep | awk \'{print $2}\' | sudo xargs kill -9')
-def compress_assets():
+def compress_assets(bundle=False):
local('jammit -c assets.yml --base-url http://www.newsblur.com --output static')
local('tar -czf static.tgz static/*')
@@ -216,7 +221,7 @@ def setup_db():
setup_common()
setup_db_firewall()
setup_db_motd()
- setup_rabbitmq()
+ # setup_rabbitmq()
setup_memcached()
setup_postgres()
setup_mongo()
@@ -307,7 +312,7 @@ def setup_psycopg():
def setup_python():
sudo('easy_install -U pip')
- sudo('easy_install -U fabric django readline pyflakes iconv celery django-celery django-compress South django-extensions pymongo BeautifulSoup pyyaml nltk==0.9.9 lxml oauth2 pytz boto seacucumber django_ses mongoengine redis requests')
+ sudo('easy_install -U fabric django readline pyflakes iconv celery django-celery django-celery-with-redis django-compress South django-extensions pymongo stripe BeautifulSoup pyyaml nltk==0.9.9 lxml oauth2 pytz boto seacucumber django_ses mongoengine redis requests')
put('config/pystartup.py', '.pystartup')
with cd(os.path.join(env.NEWSBLUR_PATH, 'vendor/cjson')):
@@ -416,6 +421,7 @@ def setup_app_firewall():
sudo('ufw allow ssh')
sudo('ufw allow 80')
sudo('ufw allow 8888')
+ sudo('ufw allow 443')
sudo('ufw --force enable')
def setup_app_motd():
@@ -464,7 +470,7 @@ def setup_db_firewall():
sudo('ufw allow 80')
sudo('ufw allow from 199.15.250.0/22 to any port 5432 ') # PostgreSQL
sudo('ufw allow from 199.15.250.0/22 to any port 27017') # MongoDB
- sudo('ufw allow from 199.15.250.0/22 to any port 5672 ') # RabbitMQ
+ # sudo('ufw allow from 199.15.250.0/22 to any port 5672 ') # RabbitMQ
sudo('ufw allow from 199.15.250.0/22 to any port 6379 ') # Redis
sudo('ufw allow from 199.15.250.0/22 to any port 11211 ') # Memcached
sudo('ufw --force enable')
diff --git a/local_settings.py.template b/local_settings.py.template
index 0ad9c676f..5a53fbdbb 100644
--- a/local_settings.py.template
+++ b/local_settings.py.template
@@ -38,6 +38,9 @@ S3_ACCESS_KEY = 'XXX'
S3_SECRET = 'SECRET'
S3_BACKUP_BUCKET = 'newsblur_backups'
+STRIPE_SECRET = "YOUR-SECRET-API-KEY"
+STRIPE_PUBLISHABLE = "YOUR-PUBLISHABLE-API-KEY"
+
# =============
# = Databases =
# =============
diff --git a/media/css/payments.css b/media/css/payments.css
new file mode 100644
index 000000000..5d374c188
--- /dev/null
+++ b/media/css/payments.css
@@ -0,0 +1,154 @@
+/* ========== */
+/* = Paypal = */
+/* ========== */
+
+.NB-paypal-return {
+ margin: 176px 0 0;
+ background-color: #D3E7BA;
+ border-top: 1px solid #A0A0A0;
+ border-bottom: 1px solid #A0A0A0;
+ padding: 24px 0;
+ background-image: linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+ background-image: -moz-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+ background-image: -webkit-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+ background-image: -ms-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+ text-align: center;
+}
+
+.NB-paypal-return .NB-paypal-return-title {
+ font-size: 36px;
+ margin: 0 0 12px;
+ color: #303030;
+ text-shadow: 1px 1px 0 #FFF;
+ text-transform: uppercase;
+}
+
+.NB-paypal-return .NB-paypal-return-subtitle {
+ font-size: 24px;
+ color: #324A15;
+ text-shadow: 1px 1px 0 #FFF;
+}
+
+.NB-paypal-return .NB-paypal-return-loading {
+ margin: 18px auto 0;
+ height: 16px;
+ width: 300px;
+}
+
+/* ========== */
+/* = Stripe = */
+/* ========== */
+
+.NB-stripe-form-wrapper {
+ margin: 56px 0 18px;
+ background-color: #D3E7BA;
+ border-top: 1px solid #A0A0A0;
+ border-bottom: 1px solid #A0A0A0;
+ padding: 24px 0;
+ background-image: linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+ background-image: -moz-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+ background-image: -webkit-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+ background-image: -ms-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%);
+}
+.NB-stripe-form {
+ margin: 0 auto;
+ width: 360px;
+ overflow: hidden;
+}
+
+.NB-stripe-form input,
+.NB-stripe-form select {
+ margin: 6px 0 2px;
+ width: 200px;
+ font-size: 14px;
+ padding: 2px;
+ border: 1px solid #606060;
+ -moz-box-shadow:2px 2px 0 #A0B998;
+ -webkit-box-shadow:2px 2px 0 #A0B998;
+ box-shadow:2px 2px 0 #A0B998;
+}
+
+.NB-stripe-form input.error,
+.NB-stripe-form select.error {
+ border-color: #830C0C;
+}
+.NB-stripe-form button {
+ width: 200px;
+ margin: 12px 0 4px 150px;
+
+ -moz-box-shadow:2px 2px 0 #A0B998;
+ -webkit-box-shadow:2px 2px 0 #A0B998;
+ box-shadow:2px 2px 0 #A0B998;
+}
+.NB-stripe-form .helptext {
+ display: none;
+}
+
+.NB-stripe-form #id_card_cvv {
+ width: 42px;
+}
+
+.NB-stripe-form .NB-stripe-username {
+ margin: 6px 0 12px;
+}
+
+.NB-stripe-form label {
+ width: 150px;
+ display: block;
+ float: left;
+ clear: both;
+ margin: 6px 0 0;
+ padding: 2px 0 0;
+ text-shadow: 0 1px 0 #F3FFED;
+ text-transform: uppercase;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.NB-stripe-form .NB-creditcards {
+ margin: 8px 0 0 150px;
+}
+
+.NB-stripe-form input[type=submit] {
+ margin-left: 150px;
+ width: 200px;
+}
+
+.NB-stripe-form label.error {
+ width: 200px;
+ margin-left: 150px;
+ text-transform: none;
+}
+
+.NB-stripe-form p {
+ overflow: hidden;
+}
+
+.NB-stripe-form .NB-stripe-plan-choice {
+ float: left;
+ width: 200px;
+ margin-top: 4px;
+ padding: 0 2px;
+ font-weight: bold;
+}
+.NB-stripe-form .NB-stripe-plan-choice label {
+ width: auto;
+ float: left;
+ margin-top: 0;
+}
+.NB-stripe-form .NB-stripe-plan-choice input {
+ width: auto;
+ margin-right: 4px;
+}
+
+.NB-stripe-form .NB-small {
+ font-size: 10px;
+ margin-left: 4px;
+ color: #575857;
+}
+.NB-stripe-form .payment-errors {
+ margin: 8px 0 0 150px;
+ color: #600000;
+ display: block;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/media/css/paypal_return.css b/media/css/paypal_return.css
deleted file mode 100644
index 04e039c91..000000000
--- a/media/css/paypal_return.css
+++ /dev/null
@@ -1,28 +0,0 @@
-.NB-paypal-return {
- margin: 176px 0 0;
- background-color: #CBE5C7;
- border-top: 1px solid #A0A0A0;
- border-bottom: 1px solid #A0A0A0;
- padding: 24px 0;
- text-align: center;
-}
-
-.NB-paypal-return .NB-paypal-return-title {
- font-size: 36px;
- margin: 0 0 12px;
- color: #303030;
- text-shadow: 1px 1px 0 #FFF;
- text-transform: uppercase;
-}
-
-.NB-paypal-return .NB-paypal-return-subtitle {
- font-size: 24px;
- color: #324A15;
- text-shadow: 1px 1px 0 #FFF;
-}
-
-.NB-paypal-return .NB-paypal-return-loading {
- margin: 18px auto 0;
- height: 16px;
- width: 300px;
-}
\ No newline at end of file
diff --git a/media/css/reader.css b/media/css/reader.css
index 5d362e692..b0c439b93 100644
--- a/media/css/reader.css
+++ b/media/css/reader.css
@@ -4601,7 +4601,7 @@ background: transparent;
.NB-modal-statistics .NB-statistics-history-fetch.NB-ok {
color: #135500;
}
-.NB-modal-statistics .NB-statistics-history-fetch.NB-error {
+.NB-modal-statistics .NB-statistics-history-fetch.NB-errorcode {
color: #6A1000;
}
.NB-modal-statistics .NB-statistics-history-fetch .NB-statistics-history-fetch-code {
@@ -5133,7 +5133,7 @@ background: transparent;
font-size: 14px;
line-height: 16px;
color: #427700;
- margin: 24px 0 1px;
+ margin: 12px 0 1px;
font-weight: bold;
text-shadow:1px 1px 0 #F0F0F0;
}
@@ -5141,7 +5141,7 @@ background: transparent;
font-size: 12px;
line-height: 16px;
color: #C0C0C0;
- margin: 24px 0 0;
+ margin: 12px 0 0;
font-weight: bold;
text-transform: uppercase;
text-shadow:1px 1px 0 #F6F6F6;
@@ -5155,6 +5155,9 @@ background: transparent;
color: #C05050;
}
+.NB-modal-feedchooser .NB-modal-subtitle {
+ width: auto;
+}
.NB-modal-feedchooser .NB-feedchooser {
background-color: #D7DDE6;
overflow-y: auto;
@@ -5189,16 +5192,38 @@ background: transparent;
.NB-modal-feedchooser .NB-feedchooser-paypal {
min-height: 48px;
- width: 135px;
+ width: 50%;
text-align: center;
overflow: hidden;
- float: right;
- margin-top: 16px;
+ float: left;
+ clear: both;
+ padding: 16px 0 0;
}
-.NB-modal-feedchooser .NB-feedchooser-dollar {
+.NB-modal-feedchooser .NB-feedchooser-paypal img {
+ margin: 0 auto;
+}
+.NB-modal-feedchooser .NB-feedchooser-stripe {
+ min-height: 48px;
+ width: 44%;
+ text-align: center;
+ overflow: hidden;
float: left;
- margin: 0px 0px 0px 0;
+ margin: 12px 0 8px;
+ padding-left: 12px;
+ border-left: 1px solid #C6B400;
+}
+
+.NB-modal-feedchooser .NB-feedchooser-stripe .NB-modal-submit-green {
+ -moz-box-shadow:2px 2px 0 #E2D121;
+ box-shadow:2px 2px 0 #E2D121;
+}
+.NB-modal-feedchooser .NB-creditcards img {
+ width: 28px;
+ margin: 0 2px 0 0;
+}
+.NB-modal-feedchooser .NB-feedchooser-dollar {
+ margin: 0px auto;
padding: 4px 0 4px 2px;
font-weight: bold;
}
@@ -5223,7 +5248,7 @@ background: transparent;
width: 40px;
height: 40px;
top: 0;
- left: 0;
+ left: -40px;
}
.NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-selected.NB-1 .NB-feedchooser-dollar-image {
top: -8px;
@@ -5245,6 +5270,7 @@ background: transparent;
display: inline;
padding: 0 4px 0 0;
font-size: 15px;
+ position: relative;
}
.NB-modal-feedchooser .NB-selected .NB-feedchooser-dollar-month {
@@ -5686,6 +5712,10 @@ background: transparent;
vertical-align: middle;
margin: -3px 6px 0 2px;
}
+.NB-modal-preferences .NB-preference-ssl label img {
+ vertical-align: middle;
+ margin: -5px 6px 0 2px;
+}
.NB-modal-preferences .NB-preference-password .NB-preference-option {
float: left;
margin: 0 12px 0 0;
diff --git a/media/img/reader/logo-paypal.png b/media/img/reader/logo-paypal.png
new file mode 100644
index 000000000..dc4683609
Binary files /dev/null and b/media/img/reader/logo-paypal.png differ
diff --git a/media/js/newsblur/paypal_return.js b/media/js/newsblur/paypal_return.js
index d85c3c1c6..fbcffed63 100644
--- a/media/js/newsblur/paypal_return.js
+++ b/media/js/newsblur/paypal_return.js
@@ -1,7 +1,9 @@
(function($) {
$(document).ready(function() {
- NEWSBLUR.paypal_return = new NEWSBLUR.PaypalReturn();
+ if($('.NB-paypal-return').length) {
+ NEWSBLUR.paypal_return = new NEWSBLUR.PaypalReturn();
+ }
});
NEWSBLUR.PaypalReturn = function() {
@@ -38,7 +40,7 @@
},
homepage: function() {
- window.location.href = '/';
+ window.location.href = 'http://' + NEWSBLUR.URLs.domain + '/';
}
};
diff --git a/media/js/newsblur/reader.js b/media/js/newsblur/reader.js
index ffb7d5bdc..b0d4f35ad 100644
--- a/media/js/newsblur/reader.js
+++ b/media/js/newsblur/reader.js
@@ -90,6 +90,8 @@
// = Initialization =
// ==================
+ var refresh_page = this.check_and_load_ssl();
+ if (refresh_page) return;
this.load_javascript_elements_on_page();
this.unload_feed_iframe();
this.unload_story_iframe();
@@ -122,6 +124,13 @@
// = Page =
// ========
+ check_and_load_ssl: function() {
+ if (window.location.protocol == 'http:' && this.model.preference('ssl')) {
+ window.location.href = window.location.href.replace('http:', 'https:');
+ return true;
+ }
+ },
+
load_javascript_elements_on_page: function() {
$('.NB-javascript').removeClass('NB-javascript');
},
@@ -732,7 +741,7 @@
var $next_story;
var unread_count = this.get_unread_count(true);
- // NEWSBLUR.log(['show_next_unread_story', unread_count, $current_story, second_pass]);
+ // NEWSBLUR.log(['show_next_unread_story', unread_count, $current_story]);
if (unread_count) {
if (!$current_story.length) {
@@ -782,7 +791,7 @@
this.open_feed(next_feed_id, true, $next_feed);
}
}
-
+
this.show_next_unread_story();
},
@@ -1014,7 +1023,7 @@
find_story_with_action_preference_on_open_feed: function() {
var open_feed_action = this.model.preference('open_feed_action');
- console.log(["action_preference_on_open_feed", open_feed_action, this.counts['page']]);
+
if (this.counts['page'] != 1) return;
if (open_feed_action == 'newest') {
@@ -1841,7 +1850,7 @@
}
} else if (this.story_view == 'feed') {
this.prefetch_story_locations_in_feed_view();
- } else if (this.story_view == 'story') {
+ } else if (this.story_view == 'story' && !this.counts['find_next_unread_on_page_of_feed_stories_load']) {
this.show_next_story(1);
}
}
@@ -2001,6 +2010,7 @@
post_open_starred_stories: function(data, first_load) {
if (this.active_feed == 'starred') {
// NEWSBLUR.log(['post_open_starred_stories', data.stories.length, first_load]);
+ this.flags['opening_feed'] = false;
this.flags['feed_view_positions_calculated'] = false;
this.story_titles_clear_loading_endbar();
this.create_story_titles(data.stories, {'river_stories': true});
@@ -2684,16 +2694,19 @@
trigger: 'manual',
offsetOpposite: -1
});
- $star.tipsy('enable');
- $star.tipsy('show');
+ var tipsy = $star.data('tipsy');
+ tipsy.enable();
+ tipsy.show();
$star.animate({
'opacity': 1
}, {
'duration': 850,
'queue': false,
'complete': function() {
- $(this).tipsy('hide');
- $(this).tipsy('disable');
+ if (tipsy.enabled) {
+ tipsy.hide();
+ tipsy.disable();
+ }
}
});
this.model.mark_story_as_starred(story_id, story.story_feed_id, function() {});
@@ -2712,11 +2725,14 @@
trigger: 'manual',
offsetOpposite: -1
});
- $star.tipsy('enable');
- $star.tipsy('show');
+ var tipsy = $star.data('tipsy');
+ tipsy.enable();
+ tipsy.show();
_.delay(function() {
- $star.tipsy('hide');
- $star.tipsy('disable');
+ if (tipsy.enabled) {
+ tipsy.hide();
+ tipsy.disable();
+ }
}, 850);
$story.removeClass('NB-story-starred');
this.model.mark_story_as_unstarred(story_id, function() {});
diff --git a/media/js/newsblur/reader_feedchooser.js b/media/js/newsblur/reader_feedchooser.js
index 5942aa6da..18c6c8044 100644
--- a/media/js/newsblur/reader_feedchooser.js
+++ b/media/js/newsblur/reader_feedchooser.js
@@ -31,20 +31,19 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
this.$modal = $.make('div', { className: 'NB-modal-feedchooser NB-modal' }, [
$.make('h2', { className: 'NB-modal-title' }, 'Choose Your '+this.MAX_FEEDS),
- $.make('h2', { className: 'NB-modal-subtitle' }, [
- $.make('b', [
- 'You have a ',
- $.make('span', { style: 'color: #303060;' }, 'Standard Account'),
- ', which can follow up to '+this.MAX_FEEDS+' sites.'
- ]),
- 'You can always change these.'
- ]),
$.make('div', { className: 'NB-feedchooser-type'}, [
$.make('div', { className: 'NB-feedchooser-info'}, [
$.make('div', { className: 'NB-feedchooser-info-type' }, [
$.make('span', { className: 'NB-feedchooser-subtitle-type-prefix' }, 'Free'),
' Standard Account'
]),
+ $.make('h2', { className: 'NB-modal-subtitle' }, [
+ $.make('b', [
+ 'You can follow up to '+this.MAX_FEEDS+' sites.'
+ ]),
+ $.make('br'),
+ 'You can always change these.'
+ ]),
$.make('div', { className: 'NB-feedchooser-info-counts'}),
$.make('div', { className: 'NB-feedchooser-info-sort'}, 'Auto-Selected By Popularity')
]),
@@ -70,7 +69,7 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
$.make('ul', { className: 'NB-feedchooser-premium-bullets' }, [
$.make('li', { className: 'NB-1' }, [
$.make('div', { className: 'NB-feedchooser-premium-bullet-image' }),
- 'Sites are updated 10x more often.'
+ 'Sites are updated up to 10x more often.'
]),
$.make('li', { className: 'NB-2' }, [
$.make('div', { className: 'NB-feedchooser-premium-bullet-image' }),
@@ -78,7 +77,7 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
]),
$.make('li', { className: 'NB-3' }, [
$.make('div', { className: 'NB-feedchooser-premium-bullet-image' }),
- 'Access to the premium-only River of News.'
+ 'River of News (reading by folder).'
]),
$.make('li', { className: 'NB-4' }, [
$.make('div', { className: 'NB-feedchooser-premium-bullet-image' }),
@@ -101,23 +100,49 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
]),
$.make('div', { className: 'NB-modal-submit NB-modal-submit-paypal' }, [
// this.make_google_checkout()
- $.make('div', { className: 'NB-feedchooser-paypal' }),
$.make('div', { className: 'NB-feedchooser-dollar' }, [
$.make('div', { className: 'NB-feedchooser-dollar-value NB-1' }, [
- $.make('div', { className: 'NB-feedchooser-dollar-image' }),
- $.make('div', { className: 'NB-feedchooser-dollar-month' }, '$12/year'),
+ $.make('div', { className: 'NB-feedchooser-dollar-month' }, [
+ $.make('div', { className: 'NB-feedchooser-dollar-image' }),
+ '$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-image' }),
- $.make('div', { className: 'NB-feedchooser-dollar-month' }, '$24/year'),
+ $.make('div', { className: 'NB-feedchooser-dollar-month' }, [
+ $.make('div', { className: 'NB-feedchooser-dollar-image' }),
+ '$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-image' }),
- $.make('div', { className: 'NB-feedchooser-dollar-month' }, '$36/year'),
+ $.make('div', { className: 'NB-feedchooser-dollar-month' }, [
+ $.make('div', { className: 'NB-feedchooser-dollar-image' }),
+ '$36/year'
+ ]),
$.make('div', { className: 'NB-feedchooser-dollar-year' }, '($3/month)')
])
+ ]),
+ $.make('div', { className: 'NB-feedchooser-processor' }, [
+ $.make('div', { className: 'NB-feedchooser-paypal' }, [
+ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/logo-paypal.png', height: 30 }),
+ $.make('div', { className: 'NB-feedchooser-paypal-form' })
+ ]),
+ $.make('div', { className: 'NB-feedchooser-stripe' }, [
+ $.make('div', { className: 'NB-creditcards' }, [
+ $.make('img', { src: "https://manage.stripe.com/img/credit_cards/visa.png" }),
+ $.make('img', { src: "https://manage.stripe.com/img/credit_cards/mastercard.png" }),
+ $.make('img', { src: "https://manage.stripe.com/img/credit_cards/amex.png" }),
+ $.make('img', { src: "https://manage.stripe.com/img/credit_cards/discover.png" })
+ ]),
+ $.make('div', {
+ className: "NB-stripe-button NB-modal-submit-button NB-modal-submit-green"
+ }, [
+ "Pay by",
+ $.make('br'),
+ "Credit Card"
+ ])
+ ])
])
])
])
@@ -126,7 +151,7 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
make_paypal_button: function() {
var self = this;
- var $paypal = $('.NB-feedchooser-paypal', this.$modal);
+ var $paypal = $('.NB-feedchooser-paypal-form', this.$modal);
$.get('/profile/paypal_form', function(response) {
$paypal.html(response);
self.choose_dollar_amount(2);
@@ -374,6 +399,10 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
});
},
+ open_stripe_form: function() {
+ window.location.href = "https://" + NEWSBLUR.URLs.domain + "/profile/stripe_form?plan=" + this.plan;
+ },
+
update_homepage_count: function() {
var $count = $('.NB-module-account-feedcount');
var $button = $('.NB-module-account-upgrade');
@@ -384,17 +413,19 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
$('.NB-module-account-trainer').removeClass('NB-hidden').hide().slideDown(500);
},
- choose_dollar_amount: function(step) {
+ choose_dollar_amount: function(plan) {
var $value = $('.NB-feedchooser-dollar-value', this.$modal);
var $input = $('input[name=a3]');
+
+ this.plan = plan;
$value.removeClass('NB-selected');
- $value.filter('.NB-'+step).addClass('NB-selected');
- if (step == 1) {
+ $value.filter('.NB-'+plan).addClass('NB-selected');
+ if (plan == 1) {
$input.val(12);
- } else if (step == 2) {
+ } else if (plan == 2) {
$input.val(24);
- } else if (step == 3) {
+ } else if (plan == 3) {
$input.val(36);
}
},
@@ -427,6 +458,11 @@ NEWSBLUR.ReaderFeedchooser.prototype = {
this.close_and_add();
}, this));
+ $.targetIs(e, { tagSelector: '.NB-stripe-button' }, _.bind(function($t, $p) {
+ e.preventDefault();
+ this.open_stripe_form();
+ }, this));
+
$.targetIs(e, { tagSelector: '.NB-feedchooser-dollar-value' }, _.bind(function($t, $p) {
e.preventDefault();
var step;
diff --git a/media/js/newsblur/reader_preferences.js b/media/js/newsblur/reader_preferences.js
index 991514d72..4cb7abb2c 100644
--- a/media/js/newsblur/reader_preferences.js
+++ b/media/js/newsblur/reader_preferences.js
@@ -268,6 +268,26 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
'Window title'
])
]),
+ $.make('div', { className: 'NB-preference NB-preference-ssl' }, [
+ $.make('div', { className: 'NB-preference-options' }, [
+ $.make('div', [
+ $.make('input', { id: 'NB-preference-ssl-1', type: 'radio', name: 'ssl', value: 0 }),
+ $.make('label', { 'for': 'NB-preference-ssl-1' }, [
+ 'Use a standard connection'
+ ])
+ ]),
+ $.make('div', [
+ $.make('input', { id: 'NB-preference-ssl-2', type: 'radio', name: 'ssl', value: 1 }),
+ $.make('label', { 'for': 'NB-preference-ssl-2' }, [
+ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/lock.png' }),
+ 'Only use a secure https connection'
+ ])
+ ])
+ ]),
+ $.make('div', { className: 'NB-preference-label'}, [
+ 'SSL'
+ ])
+ ]),
$.make('div', { className: 'NB-preference NB-preference-openfeedaction' }, [
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
@@ -534,6 +554,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
return false;
}
});
+ $('input[name=ssl]', this.$modal).each(function() {
+ if ($(this).val() == NEWSBLUR.Preferences.ssl) {
+ $(this).attr('checked', true);
+ return false;
+ }
+ });
$('input[name=show_unread_counts_in_title]', this.$modal).each(function() {
if (NEWSBLUR.Preferences.show_unread_counts_in_title) {
$(this).attr('checked', true);
@@ -627,6 +653,9 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
if (self.original_preferences['story_pane_anchor'] != form['story_pane_anchor']) {
NEWSBLUR.reader.apply_resizable_layout(true);
}
+ if (self.original_preferences['ssl'] != form['ssl']) {
+ NEWSBLUR.reader.check_and_load_ssl();
+ }
self.close();
});
},
diff --git a/media/js/newsblur/reader_statistics.js b/media/js/newsblur/reader_statistics.js
index 7f9f53b09..4d40eceda 100644
--- a/media/js/newsblur/reader_statistics.js
+++ b/media/js/newsblur/reader_statistics.js
@@ -214,7 +214,7 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
var $history = _.map(fetches, function(fetch) {
var feed_ok = _.contains([200, 304], fetch.status_code);
- var status_class = feed_ok ? ' NB-ok ' : ' NB-error ';
+ var status_class = feed_ok ? ' NB-ok ' : ' NB-errorcode ';
return $.make('div', { className: 'NB-statistics-history-fetch' + status_class, title: feed_ok ? '' : fetch.exception }, [
$.make('div', { className: 'NB-statistics-history-fetch-date' }, fetch.fetch_date),
$.make('div', { className: 'NB-statistics-history-fetch-message' }, [
diff --git a/media/js/newsblur/stripe_form.js b/media/js/newsblur/stripe_form.js
new file mode 100644
index 000000000..61f81b3e1
--- /dev/null
+++ b/media/js/newsblur/stripe_form.js
@@ -0,0 +1,117 @@
+$(function() {
+ if ($('.NB-stripe-form').length) {
+ // $("#id_card_number").parents("form").submit(function() {
+ // if ( $("#id_card_number").is(":visible")) {
+ // var form = this;
+ // var card = {
+ // number: $("#id_card_number").val(),
+ // expMonth: $("#id_card_expiry_month").val(),
+ // expYear: $("#id_card_expiry_year").val(),
+ // cvc: $("#id_card_cvv").val()
+ // };
+ //
+ // Stripe.createToken(card, function(status, response) {
+ // if (status === 200) {
+ // $("#credit-card-errors").hide();
+ // $("#id_last_4_digits").val(response.card.last4);
+ // $("#id_stripe_token").val(response.id);
+ // form.submit();
+ // $("button[type=submit]").attr("disabled","disabled").html("Submitting..");
+ // } else {
+ // $(".payment-errors").text(response.error.message);
+ // $("#user_submit").attr("disabled", false);
+ // }
+ // });
+ // return false;
+ // }
+ //
+ // return true;
+ // });
+
+ function addInputNames() {
+ // Not ideal, but jQuery's validate plugin requires fields to have names
+ // so we add them at the last possible minute, in case any javascript
+ // exceptions have caused other parts of the script to fail.
+ $(".card-number").attr("name", "card-number");
+ $(".card-cvv").attr("name", "card-cvc");
+ $(".card-expiry-year").attr("name", "card-expiry-year");
+ }
+
+ function removeInputNames() {
+ $(".card-number").removeAttr("name");
+ $(".card-cvv").removeAttr("name");
+ $(".card-expiry-year").removeAttr("name");
+ }
+
+ function submit(form) {
+ // remove the input field names for security
+ // we do this *before* anything else which might throw an exception
+ removeInputNames(); // THIS IS IMPORTANT!
+
+ // given a valid form, submit the payment details to stripe
+ $("button[type=submit]").attr("disabled", "disabled");
+ $("button[type=submit]").addClass("NB-disabled");
+ $("button[type=submit]").removeClass("NB-modal-submit-green");
+ $("button[type=submit]").text("Submitting...");
+
+ Stripe.createToken({
+ number: $('.card-number').val(),
+ cvc: $('.card-cvv').val(),
+ exp_month: $('.card-expiry-month').val(),
+ exp_year: $('.card-expiry-year').val()
+ }, function(status, response) {
+ if (response.error) {
+ // re-enable the submit button
+ $("button[type=submit]").removeAttr("disabled");
+ $("button[type=submit]").removeClass("NB-disabled");
+ $("button[type=submit]").addClass("NB-modal-submit-green");
+ $("button[type=submit]").text("Submit Payment");
+
+ // show the error
+ $(".payment-errors").html(response.error.message);
+
+ // we add these names back in so we can revalidate properly
+ addInputNames();
+ } else {
+ $("#id_last_4_digits").val(response.card.last4);
+ $("#id_stripe_token").val(response.id);
+
+ form.submit();
+ }
+ });
+
+ return false;
+ }
+
+ // add custom rules for credit card validating
+ jQuery.validator.addMethod("cardNumber", Stripe.validateCardNumber, "Please enter a valid card number");
+ jQuery.validator.addMethod("cardCVC", Stripe.validateCVC, "Please enter a valid security code");
+ jQuery.validator.addMethod("cardExpiry", function() {
+ return Stripe.validateExpiry($(".card-expiry-month").val(),
+ $(".card-expiry-year").val());
+ }, "Please enter a valid expiration");
+
+ // We use the jQuery validate plugin to validate required params on submit
+ $("#id_card_number").parents("form").validate({
+ submitHandler: submit,
+ rules: {
+ "card-cvc" : {
+ cardCVC: true,
+ required: true
+ },
+ "card-number" : {
+ cardNumber: true,
+ required: true
+ },
+ "card-expiry-year" : "cardExpiry", // we don't validate month separately
+ "email": {
+ required: true,
+ email: true
+ }
+ }
+ });
+
+ // adding the input field names is the last step, in case an earlier step errors
+ addInputNames();
+ }
+});
diff --git a/settings.py b/settings.py
index 9fe4ea477..f9b0db0fc 100644
--- a/settings.py
+++ b/settings.py
@@ -39,10 +39,12 @@ if '/utils' not in ' '.join(sys.path):
sys.path.append(UTILS_ROOT)
if '/vendor' not in ' '.join(sys.path):
sys.path.append(VENDOR_ROOT)
+
# ===================
# = Global Settings =
# ===================
+DEBUG = False
TEST_DEBUG = False
SEND_BROKEN_LINK_EMAILS = False
MANAGERS = ADMINS
@@ -60,7 +62,7 @@ ADMIN_MEDIA_PREFIX = '/media/admin/'
SECRET_KEY = 'YOUR_SECRET_KEY'
EMAIL_BACKEND = 'django_ses.SESBackend'
CIPHER_USERNAMES = False
-
+DEBUG_ASSETS = DEBUG
# ===============
# = Enviornment =
@@ -160,143 +162,6 @@ LOGGING = {
}
}
-# =====================
-# = Media Compression =
-# =====================
-
-COMPRESS_JS = {
- 'all': {
- 'source_filenames': (
- 'js/jquery-1.7.1.js',
- 'js/inflector.js',
- 'js/jquery.json.js',
- 'js/jquery.easing.js',
- 'js/jquery.newsblur.js',
- 'js/jquery.scrollTo.js',
- 'js/jquery.corners.js',
- 'js/jquery.hotkeys.js',
- 'js/jquery.ajaxupload.js',
- 'js/jquery.ajaxmanager.3.js',
- 'js/jquery.simplemodal-1.3.js',
- 'js/jquery.color.js',
- 'js/jquery.rightclick.js',
- 'js/jquery.ui.core.js',
- 'js/jquery.ui.widget.js',
- 'js/jquery.ui.mouse.js',
- 'js/jquery.ui.position.js',
- 'js/jquery.ui.draggable.js',
- 'js/jquery.ui.sortable.js',
- 'js/jquery.ui.slider.js',
- 'js/jquery.ui.autocomplete.js',
- 'js/jquery.ui.progressbar.js',
- 'js/jquery.layout.js',
- 'js/jquery.tinysort.js',
- 'js/jquery.fieldselection.js',
- 'js/jquery.flot.js',
- 'js/jquery.tipsy.js',
- # 'js/socket.io-client.0.8.7.js',
- 'js/underscore.js',
- 'js/underscore.string.js',
- 'js/newsblur/reader_utils.js',
- 'js/newsblur/assetmodel.js',
- 'js/newsblur/reader.js',
- 'js/newsblur/generate_bookmarklet.js',
- 'js/newsblur/modal.js',
- 'js/newsblur/reader_classifier.js',
- 'js/newsblur/reader_add_feed.js',
- 'js/newsblur/reader_mark_read.js',
- 'js/newsblur/reader_goodies.js',
- 'js/newsblur/reader_preferences.js',
- 'js/newsblur/reader_account.js',
- 'js/newsblur/reader_feedchooser.js',
- 'js/newsblur/reader_statistics.js',
- 'js/newsblur/reader_feed_exception.js',
- 'js/newsblur/reader_keyboard.js',
- 'js/newsblur/reader_recommend_feed.js',
- 'js/newsblur/reader_send_email.js',
- 'js/newsblur/reader_tutorial.js',
- 'js/newsblur/about.js',
- 'js/newsblur/faq.js',
- ),
- 'output_filename': 'js/all-compressed-?.js'
- },
- 'mobile': {
- 'source_filenames': (
- 'js/jquery-1.7.1.js',
- 'js/mobile/jquery.mobile-1.0b1.js',
- 'js/jquery.ajaxmanager.3.js',
- 'js/underscore.js',
- 'js/underscore.string.js',
- 'js/inflector.js',
- 'js/jquery.json.js',
- 'js/jquery.easing.js',
- 'js/jquery.newsblur.js',
- 'js/newsblur/reader_utils.js',
- 'js/newsblur/assetmodel.js',
- 'js/mobile/newsblur/mobile_workspace.js',
- ),
- 'output_filename': 'js/mobile-compressed-?.js',
- },
- 'paypal': {
- 'source_filenames': (
- 'js/newsblur/paypal_return.js',
- ),
- 'output_filename': 'js/paypal-compressed-?.js',
- },
- 'bookmarklet': {
- 'source_filenames': (
- 'js/jquery-1.5.1.min.js',
- 'js/jquery.noConflict.js',
- 'js/jquery.newsblur.js',
- 'js/jquery.tinysort.js',
- 'js/jquery.simplemodal-1.3.js',
- 'js/jquery.corners.js',
- ),
- 'output_filename': 'js/bookmarklet-compressed-?.js',
- },
-}
-
-COMPRESS_CSS = {
- 'all': {
- 'source_filenames': (
- 'css/reader.css',
- 'css/modals.css',
- 'css/status.css',
- 'css/jquery-ui/jquery.theme.css',
- 'css/jquery.tipsy.css',
- ),
- 'output_filename': 'css/all-compressed-?.css'
- },
- 'mobile': {
- 'source_filenames': (
- 'css/mobile/jquery.mobile-1.0b1.css',
- 'css/mobile/mobile.css',
- ),
- 'output_filename': 'css/mobile/mobile-compressed-?.css',
- },
- 'paypal': {
- 'source_filenames': (
- 'css/paypal_return.css',
- ),
- 'output_filename': 'css/paypal-compressed-?.css',
- },
- 'bookmarklet': {
- 'source_filenames': (
- 'css/reset.css',
- 'css/modals.css',
- ),
- 'output_filename': 'css/paypal-compressed-?.css',
- },
-}
-
-COMPRESS_VERSION = True
-COMPRESS_JS_FILTERS = ['compress.filters.jsmin.JSMinFilter']
-COMPRESS_CSS_FILTERS = []
-
-# YUI_DIR = ''.join([UTILS_ROOT, '/yuicompressor-2.4.2/build/yuicompressor-2.4.2.jar'])
-# COMPRESS_YUI_BINARY = 'java -jar ' + YUI_DIR
-# COMPRESS_YUI_JS_ARGUMENTS = '--preserve-semi --nomunge --disable-optimizations'
-
# ==========================
# = Miscellaneous Settings =
# ==========================
@@ -336,7 +201,6 @@ INSTALLED_APPS = (
'django.contrib.admin',
'django_extensions',
'djcelery',
- # 'seacucumber',
'django_ses',
'apps.rss_feeds',
'apps.reader',
@@ -352,12 +216,21 @@ INSTALLED_APPS = (
'vendor',
'vendor.typogrify',
'vendor.paypal.standard.ipn',
+ 'vendor.zebra',
)
if not DEVELOPMENT:
INSTALLED_APPS += (
'gunicorn',
)
+
+# ==========
+# = Stripe =
+# ==========
+
+STRIPE_SECRET = "YOUR-SECRET-API-KEY"
+STRIPE_PUBLISHABLE = "YOUR-PUBLISHABLE-API-KEY"
+ZEBRA_ENABLE_APP = True
# ==========
# = Celery =
@@ -388,21 +261,17 @@ CELERY_QUEUES = {
},
}
CELERY_DEFAULT_QUEUE = "update_feeds"
-BROKER_BACKEND = "amqplib"
-BROKER_HOST = "db01.newsblur.com"
-BROKER_PORT = 5672
-BROKER_USER = "newsblur"
-BROKER_PASSWORD = "newsblur"
-BROKER_VHOST = "newsblurvhost"
+BROKER_BACKEND = "redis"
+BROKER_URL = "redis://db01:6379/0"
+CELERY_REDIS_HOST = "db01"
-CELERY_RESULT_BACKEND = "amqp"
-CELERYD_LOG_LEVEL = 'ERROR'
+CELERYD_PREFETCH_MULTIPLIER = 1
CELERY_IMPORTS = ("apps.rss_feeds.tasks", )
CELERYD_CONCURRENCY = 4
CELERY_IGNORE_RESULT = True
CELERY_ACKS_LATE = True # Retry if task fails
CELERYD_MAX_TASKS_PER_CHILD = 10
-# CELERYD_TASK_TIME_LIMIT = 12 * 30
+CELERYD_TASK_TIME_LIMIT = 12 * 30
CELERY_DISABLE_RATE_LIMITS = True
# ====================
diff --git a/templates/base.html b/templates/base.html
index 407e76495..cb523fcc8 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -36,6 +36,7 @@
'hide_read_feeds' : 0,
'show_tooltips' : 1,
'feed_order' : 'ALPHABETICAL',
+ 'ssl' : 0,
'open_feed_action' : 0,
'hide_story_changes' : 1,
'feed_view_single_story' : 0,
@@ -62,17 +63,6 @@
{% include_stylesheets "common" %}
- {% block head_js %}
- {% include_javascripts "common" %}
- {% endblock head_js %}
- {% block extra_head_js %}
- {% endblock extra_head_js %}
-
-
{% if not debug %}
-
-
-
- {% endif %}
-
+ {% block head_js %}
+ {% include_javascripts "common" %}
+ {% endblock head_js %}
+ {% block extra_head_js %}
+ {% endblock extra_head_js %}
+
+