Merge branch 'master' of github.com:samuelclay/NewsBlur

* 'master' of github.com:samuelclay/NewsBlur: (39 commits)
  Upgrading requests's ConnectionError to a first-class error, marking page as bad url.
  Python module requests now throwing ConnectionError on bad urls. Handling by not emailing me about it.
  Fixing bug where next unread story would be run twice when in Story view and opening a feed. Thanks to @dalmet for the issue.
  Adding preference for default SSL. Redirects user to https.
  Changing copy on premium/feedchooser dialog.
  Styling errors on stripe payment form.
  Final stripe.js tweaks before launch.
  Adding Pay by Credit Card as an option to feed chooser dialog. Thus concludes Stripe.js integration. Time to launch!
  Styling the stripe.js background.
  Using correct kwargs for stripe signal.
  Wrapping up stripe.js form. Has validation, styling, and does the right thing for new subscriptions. Needs a link to the form, though.
  Setting up correct customer model for stripe webhook.
  Moving JS assets to bottom of the page for faster loadtimes.
  Fixing exception on missing param in feed address searching.
  Fixing recommendation date serialization bug.
  Fixing bugs around login with blank password using full password. Also fixing bug in signups with no username.
  Stripe.js payments using zebra. Adding zebra as a vendored dependency. Webhooks and views all in. Needs styling, custom username and email fields, and loads of testing.
  Adding error checking on requests in page fetching.
  Using a probability from redis to determine whether or not to skip a fetch.
  Allowing any password to be used on accounts with no password set.
  ...
This commit is contained in:
Samuel Clay 2012-03-05 09:24:41 -08:00
commit a16c25fce9
62 changed files with 1654 additions and 379 deletions

33
apps/profile/forms.py Normal file
View file

@ -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 <span class='NB-small'>($1/month)</span>")),
("newsblur-premium-24", mark_safe("$24 / year <span class='NB-small'>($2/month)</span>")),
("newsblur-premium-36", mark_safe("$36 / year <span class='NB-small'>($3/month)</span>")),
]
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('<div class="NB-stripe-plan-choice">%s</div>' % 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')

View file

@ -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']

View file

@ -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:

View file

@ -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'),
)

View file

@ -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,
}
@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)
)

View file

@ -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

View file

@ -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)

View file

@ -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)
# 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)

View file

@ -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):

View file

@ -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,))

View file

@ -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))

View file

@ -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:

View file

@ -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:

View file

@ -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

22
fabfile.py vendored
View file

@ -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')

View file

@ -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 =
# =============

154
media/css/payments.css Normal file
View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -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 + '/';
}
};

View file

@ -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() {});

View file

@ -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;

View file

@ -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();
});
},

View file

@ -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' }, [

View file

@ -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();
}
});

View file

@ -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
# ====================

View file

@ -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 @@
</script>
{% include_stylesheets "common" %}
{% block head_js %}
{% include_javascripts "common" %}
{% endblock head_js %}
{% block extra_head_js %}
{% endblock extra_head_js %}
<script type="text/javascript" charset="utf-8">
$.extend(NEWSBLUR.Preferences, {% if user_profile.preferences %}{{ user_profile.preferences|safe }}{% else %}{}{% endif %});
$.extend(NEWSBLUR.Preferences['view_settings'], {% if user_profile.view_settings %}{{ user_profile.view_settings|safe }}{% else %}{}{% endif %});
$.extend(NEWSBLUR.Preferences['collapsed_folders'], {% if user_profile.collapsed_folders %}{{ user_profile.collapsed_folders|safe }}{% else %}[]{% endif %});
</script>
{% if not debug %}
<script type="text/javascript">
@ -121,19 +111,16 @@
{% block content %}{% endblock %}
{% if not debug %}
<!-- Start Quantcast tag -->
<script type="text/javascript">
_qoptions={
qacct:"p-0dE65XaLY51Og"
};
</script>
<script type="text/javascript" src="http://edge.quantserve.com/quant.js"></script>
<noscript>
<img src="http://pixel.quantserve.com/pixel/p-0dE65XaLY51Og.gif" style="display: none;" border="0" height="1" width="1" alt="Quantcast"/>
</noscript>
<!-- End Quantcast tag -->
{% endif %}
{% block head_js %}
{% include_javascripts "common" %}
{% endblock head_js %}
{% block extra_head_js %}
{% endblock extra_head_js %}
<script type="text/javascript" charset="utf-8">
$.extend(NEWSBLUR.Preferences, {% if user_profile.preferences %}{{ user_profile.preferences|safe }}{% else %}{}{% endif %});
$.extend(NEWSBLUR.Preferences['view_settings'], {% if user_profile.view_settings %}{{ user_profile.view_settings|safe }}{% else %}{}{% endif %});
$.extend(NEWSBLUR.Preferences['collapsed_folders'], {% if user_profile.collapsed_folders %}{{ user_profile.collapsed_folders|safe }}{% else %}[]{% endif %});
</script>
</body>
</html>

View file

@ -8,7 +8,7 @@ Stay up to date and in touch with me, yr. developer, in a few different ways:
* Follow @samuelclay on Twitter: http://twitter.com/samuelclay/
* Follow @newsblur on Twitter: http://twitter.com/newsblur/
* Follow the constantly evolving source code on GitHub: http://github.com/samuelclay/
* Follow @samuelclay on GitHub: http://github.com/samuelclay/
{% block resources_header %}There are a few resources you can use if you end up loving NewsBlur:{% endblock resources_header %}
@ -17,7 +17,7 @@ Stay up to date and in touch with me, yr. developer, in a few different ways:
There's plenty of ways to use NewsBlur beyond the website:
* Download the iPhone App -- Coming soon, it's in review on the App Store.
* Download the free iPhone App: http://www.newsblur.com/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

View file

@ -33,7 +33,7 @@
<ul style="list-style: none;">
<li style="line-height:22px;"><a href="http://twitter.com/samuelclay/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/twitter_icon_2.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow @samuelclay on Twitter</a>.</li>
<li style="line-height:22px;"><a href="http://twitter.com/newsblur/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/twitter.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow @newsblur on Twitter</a>.</li>
<li style="line-height:22px;"><a href="http://github.com/samuelclay/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/github_icon.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow the constantly evolving source code on GitHub</a>.</li>
<li style="line-height:22px;"><a href="http://github.com/samuelclay/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/github_icon.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow @samuelclay on GitHub</a>. &larr; I live on props.</li>
</ul>
</p>
<p style="line-height: 20px;">{% block resources_header %}There are a couple resources you can use if you end up loving NewsBlur:{% endblock resources_header %}</p>
@ -46,7 +46,7 @@
<p style="line-height: 20px;">There's plenty of ways to use NewsBlur beyond the website:</p>
<p style="line-height: 20px;">
<ul style="list-style: none;">
<li style="line-height:22px;"><img src="http://www.newsblur.com/media/img/reader/iphone_icon.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> <i>iPhone App -- Coming soon, it's in review on the App Store.</i></li>
<li style="line-height:22px;"><a href="http://www.newsblur.com/iphone/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/iphone_icon.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Download the free iPhone App</a></li>
<li style="line-height:22px;"><a href="https://market.android.com/details?id=bitwrit.Blar" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/android_icon.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Download the Android App on the Android Market</a>.</li>
<li style="line-height:22px;"><a href="http://www.newsblur.com{{ user.profile.autologin_url }}?next=goodies" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/icons/silk/package_green.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Download browser extensions for Safari, Firefox, and Chrome</a>.</li>
</ul>

View file

@ -0,0 +1,73 @@
{% extends 'base.html' %}
{% load typogrify_tags utils_tags zebra_tags %}
{% block bodyclass %}NB-static{% endblock %}
{% block extra_head_js %}
{% include_stylesheets "common" %}
{% include_javascripts "payments" %}
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.8.1/jquery.validate.min.js"></script>
<script type="text/javascript" src="https://js.stripe.com/v1/"></script>
{% zebra_head_and_stripe_key %}
{% endblock %}
{% block content %}
<div class="NB-stripe-form-wrapper">
<div class="NB-stripe-form">
<label>Username</label>
<div class="NB-stripe-username">{{ user.username }}</div>
<div class="NB-creditcards">
<img src="https://manage.stripe.com/img/credit_cards/visa.png">
<img src="https://manage.stripe.com/img/credit_cards/mastercard.png">
<img src="https://manage.stripe.com/img/credit_cards/amex.png">
<img src="https://manage.stripe.com/img/credit_cards/discover.png">
</div>
<form action="" method="POST" id="payment-form">{% csrf_token %}
<div>
{{ zebra_form.card_number.label_tag }}
{{ zebra_form.card_number }}
</div>
<div>
{{ zebra_form.card_cvv.label_tag }}
{{ zebra_form.card_cvv }}
</div>
<div>
{{ zebra_form.card_expiry_month.label_tag }}
{{ zebra_form.card_expiry_month }}
</div>
<div>
{{ zebra_form.card_expiry_year.label_tag }}
{{ zebra_form.card_expiry_year }}
</div>
<div>
{{ zebra_form.email.label_tag }}
{{ zebra_form.email }}
</div>
<div style="overflow: hidden">
{{ zebra_form.plan.label_tag }}
{{ zebra_form.plan|safe }}
</div>
{{ zebra_form.last_4_digits }}
{{ zebra_form.stripe_token }}
<noscript><h3>Note:&nbsp; this form requires Javascript to use.</h3></noscript>
<span class="payment-errors"></span>
<button type="submit" class="submit-button NB-modal-submit-button NB-modal-submit-green">Submit Payment</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -2,15 +2,29 @@
{% load typogrify_tags recommendations_tags utils_tags statistics_tags %}
{% block extra_head_js %}
<script>
$(document).ready(function() {
NEWSBLUR.reader = new NEWSBLUR.Reader();
});
</script>
{% if user.is_staff %}
<script>
$(document).ready(function() {
$('#add-feature-button').click(function(e) {
e.preventDefault();
$('#add-feature-form').fadeIn(500);
});
});
</script>
{% endif %}
{% endblock %}
{% block content %}
<script>
$(document).ready(function() {
NEWSBLUR.reader = new NEWSBLUR.Reader();
});
</script>
<h1 class="NB-splash-heading">NewsBlur</h1>
<h2 class="NB-splash-heading">- A visual feed reader with intelligence.</h2>
@ -28,14 +42,6 @@ $(document).ready(function() {
<div class="NB-features-add">
<a href="#" id="add-feature-button" class="NB-splash-link">Add</a>
</div>
<script>
$(document).ready(function() {
$('#add-feature-button').click(function(e) {
e.preventDefault();
$('#add-feature-form').fadeIn(500);
});
});
</script>
{% endif %}
<div class="NB-spinner NB-left"></div>
<a href="#" class="NB-module-direction NB-module-next-page NB-javascript"></a>

View file

@ -4,7 +4,7 @@
{% block extra_head_js %}
{% include_stylesheets "common" %}
{% include_javascripts "paypal" %}
{% include_javascripts "payments" %}
{% endblock %}
{% block content %}

View file

@ -21,6 +21,7 @@ urlpatterns = patterns('',
url(r'^press/?', static_views.press, name='press'),
url(r'^feedback/?', static_views.feedback, name='feedback'),
url(r'^iphone/?', static_views.iphone, name='iphone'),
url(r'zebra/', include('zebra.urls', namespace="zebra", app_name='zebra')),
)
if settings.DEVELOPMENT:

View file

@ -5,6 +5,7 @@ import multiprocessing
import urllib2
import xml.sax
import redis
import random
from django.core.cache import cache
from django.conf import settings
from django.db import IntegrityError
@ -47,11 +48,10 @@ class FetchFeed:
datetime.datetime.now() - self.feed.last_update)
logging.debug(log_msg)
self.feed.set_next_scheduled_update()
etag=self.feed.etag
modified = self.feed.last_modified.utctimetuple()[:7] if self.feed.last_modified else None
if self.options.get('force') or not self.feed.fetched_once:
if self.options.get('force') or not self.feed.fetched_once or not self.feed.known_good:
modified = None
etag = None
@ -104,8 +104,6 @@ class ProcessFeed:
ENTRY_ERR:0}
# logging.debug(u' ---> [%d] Processing %s' % (self.feed.id, self.feed.feed_title))
self.feed.last_update = datetime.datetime.utcnow()
if hasattr(self.fpf, 'status'):
if self.options['verbose']:
@ -126,10 +124,9 @@ class ProcessFeed:
if self.fpf.status in (302, 301):
if not self.fpf.href.endswith('feedburner.com/atom.xml'):
self.feed.feed_address = self.fpf.href
if not self.feed.fetched_once:
self.feed.has_feed_exception = True
if not self.feed.known_good:
self.feed.fetched_once = True
logging.debug(" ---> [%-30s] Feed is 302'ing, but it's not new. Refetching..." % (unicode(self.feed)[:30]))
logging.debug(" ---> [%-30s] Feed is %s'ing. Refetching..." % (unicode(self.feed)[:30], self.fpf.status))
self.feed.schedule_feed_fetch_immediately()
if not self.fpf.entries:
self.feed.save()
@ -142,9 +139,6 @@ class ProcessFeed:
fixed_feed = self.feed.check_feed_link_for_feed_address()
if not fixed_feed:
self.feed.save_feed_history(self.fpf.status, "HTTP Error")
else:
self.feed.has_feed_exception = True
self.feed.schedule_feed_fetch_geometrically()
self.feed.save()
return FEED_ERRHTTP, ret_values
@ -156,9 +150,6 @@ class ProcessFeed:
fixed_feed = self.feed.check_feed_link_for_feed_address()
if not fixed_feed:
self.feed.save_feed_history(502, 'Non-xml feed', self.fpf.bozo_exception)
else:
self.feed.has_feed_exception = True
self.feed.schedule_feed_fetch_immediately()
self.feed.save()
return FEED_ERRPARSE, ret_values
elif self.fpf.bozo and isinstance(self.fpf.bozo_exception, xml.sax._exceptions.SAXException):
@ -169,9 +160,6 @@ class ProcessFeed:
fixed_feed = self.feed.check_feed_link_for_feed_address()
if not fixed_feed:
self.feed.save_feed_history(503, 'SAX Exception', self.fpf.bozo_exception)
else:
self.feed.has_feed_exception = True
self.feed.schedule_feed_fetch_immediately()
self.feed.save()
return FEED_ERRPARSE, ret_values
@ -199,8 +187,6 @@ class ProcessFeed:
if not self.feed.feed_link_locked:
self.feed.feed_link = self.fpf.feed.get('link') or self.fpf.feed.get('id') or self.feed.feed_link
self.feed.last_update = datetime.datetime.utcnow()
guids = []
for entry in self.fpf.entries:
if entry.get('id', ''):
@ -294,6 +280,27 @@ class Dispatcher:
try:
feed = self.refresh_feed(feed_id)
skip = False
if self.options.get('fake'):
skip = True
weight = "-"
quick = "-"
rand = "-"
elif self.options.get('quick'):
weight = feed.stories_last_month * feed.num_subscribers
random_weight = random.randint(1, max(weight, 1))
quick = float(self.options['quick'])
rand = random.random()
if random_weight < 100 and rand < quick:
skip = True
if skip:
logging.debug(' ---> [%-30s] ~BGFaking fetch, skipping (%s/month, %s subs, %s < %s)...' % (
unicode(feed)[:30],
weight,
feed.num_subscribers,
rand, quick))
continue
ffeed = FetchFeed(feed_id, self.options)
ret_feed, fetched_feed = ffeed.fetch()

View file

@ -3924,7 +3924,10 @@ def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, refer
break
# if no luck and we have auto-detection library, try that
if (not known_encoding) and chardet:
proposed_encoding = unicode(chardet.detect(data)['encoding'], 'ascii', 'ignore')
# import pdb; pdb.set_trace()
proposed_encoding = chardet.detect(data)['encoding']
if proposed_encoding:
proposed_encoding = unicode(proposed_encoding, 'ascii', 'ignore')
if proposed_encoding and (proposed_encoding not in tried_encodings):
tried_encodings.append(proposed_encoding)
try:

View file

@ -36,7 +36,7 @@ class JammitAssets:
`use_compressed_assets` profile setting.
"""
tags = []
if not settings.DEBUG:
if not getattr(settings, 'DEBUG_ASSETS', settings.DEBUG):
if asset_type == 'javascripts':
asset_type_ext = 'js'
elif asset_type == 'stylesheets':

View file

@ -64,7 +64,7 @@ def json_encode(data, *args, **kwargs):
# see http://code.djangoproject.com/ticket/5868
elif isinstance(data, Promise):
ret = force_unicode(data)
elif isinstance(data, datetime.datetime):
elif isinstance(data, datetime.datetime) or isinstance(data, datetime.date):
ret = str(data)
else:
ret = data

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python
import redis
from utils.munin.base import MuninGraph
graph_config = {
@ -16,15 +17,16 @@ def calculate_metrics():
import datetime
import commands
from apps.rss_feeds.models import Feed
from django.conf import settings
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
update_feeds_query = "ssh -i ~sclay/.ssh/id_dsa sclay@db01 \"sudo rabbitmqctl list_queues -p newsblurvhost | grep %s\" | awk '{print $2}'"
r = redis.Redis(connection_pool=settings.REDIS_POOL)
return {
'update_queue': Feed.objects.filter(queued_date__gte=hour_ago).count(),
'feeds_fetched': Feed.objects.filter(last_update__gte=hour_ago).count(),
'celery_update_feeds': commands.getoutput(update_feeds_query % 'update_feeds'),
'celery_new_feeds': commands.getoutput(update_feeds_query % 'new_feeds'),
'celery_update_feeds': r.llen("update_feeds"),
'celery_new_feeds': r.llen("new_feeds"),
}
if __name__ == '__main__':

View file

@ -79,7 +79,7 @@ def pre_process_story(entry):
for media_content in chain(entry.get('media_content', []), entry.get('links', [])):
media_url = media_content.get('url', '')
media_type = media_content.get('type', '')
if media_url and media_type and media_url not in entry['story_content']:
if media_url and media_type and entry['story_content'] and media_url not in entry['story_content']:
media_type_name = media_type.split('/')[0]
if 'audio' in media_type and media_url:
entry['story_content'] += """<br><br>

View file

@ -16,6 +16,6 @@ SANDBOX_POSTBACK_ENDPOINT = "https://www.sandbox.paypal.com/cgi-bin/webscr"
# Images
IMAGE = getattr(settings, "PAYPAL_IMAGE", "http://images.paypal.com/images/x-click-but01.gif")
SUBSCRIPTION_IMAGE = "https://www.paypal.com/en_US/i/btn/btn_subscribeCC_LG.gif"
SUBSCRIPTION_IMAGE = "https://www.paypal.com/en_US/i/btn/btn_subscribe_LG.gif"
SANDBOX_IMAGE = getattr(settings, "PAYPAL_SANDBOX_IMAGE", "https://www.sandbox.paypal.com/en_US/i/btn/btn_buynowCC_LG.gif")
SUBSCRIPTION_SANDBOX_IMAGE = "https://www.sandbox.paypal.com/en_US/i/btn/btn_subscribeCC_LG.gif"
SUBSCRIPTION_SANDBOX_IMAGE = "https://www.sandbox.paypal.com/en_US/i/btn/btn_subscribe_LG.gif"

0
vendor/zebra/__init__.py vendored Executable file
View file

10
vendor/zebra/admin.py vendored Executable file
View file

@ -0,0 +1,10 @@
from django.contrib import admin
from zebra.conf import options
if options.ZEBRA_ENABLE_APP:
from zebra.models import Customer, Plan, Subscription
admin.site.register(Customer)
admin.site.register(Plan)
admin.site.register(Subscription)

0
vendor/zebra/conf/__init__.py vendored Executable file
View file

59
vendor/zebra/conf/options.py vendored Executable file
View file

@ -0,0 +1,59 @@
"""
Default settings for zebra
"""
import datetime
import os
from django.conf import settings as _settings
if hasattr(_settings, 'STRIPE_PUBLISHABLE'):
STRIPE_PUBLISHABLE = getattr(_settings, 'STRIPE_PUBLISHABLE')
else:
try:
STRIPE_PUBLISHABLE = os.environ['STRIPE_PUBLISHABLE']
except KeyError:
STRIPE_PUBLISHABLE = ''
if hasattr(_settings, 'STRIPE_SECRET'):
STRIPE_SECRET = getattr(_settings, 'STRIPE_SECRET')
else:
try:
STRIPE_SECRET = os.environ['STRIPE_SECRET']
except KeyError:
STRIPE_SECRET = ''
ZEBRA_ENABLE_APP = getattr(_settings, 'ZEBRA_ENABLE_APP', False)
ZEBRA_AUTO_CREATE_STRIPE_CUSTOMERS = getattr(_settings,
'ZEBRA_AUTO_CREATE_STRIPE_CUSTOMERS', True)
_today = datetime.date.today()
ZEBRA_CARD_YEARS = getattr(_settings, 'ZEBRA_CARD_YEARS',
range(_today.year, _today.year+12))
ZEBRA_CARD_YEARS_CHOICES = getattr(_settings, 'ZEBRA_CARD_YEARS_CHOICES',
[(i,i) for i in ZEBRA_CARD_YEARS])
ZEBRA_MAXIMUM_STRIPE_CUSTOMER_LIST_SIZE = getattr(_settings,
'ZEBRA_MAXIMUM_STRIPE_CUSTOMER_LIST_SIZE', 100)
_audit_defaults = {
'active': 'active',
'no_subscription': 'no_subscription',
'past_due': 'past_due',
'suspended': 'suspended',
'trialing': 'trialing',
'unpaid': 'unpaid',
'cancelled': 'cancelled'
}
ZEBRA_AUDIT_RESULTS = getattr(_settings, 'ZEBRA_AUDIT_RESULTS', _audit_defaults)
ZEBRA_ACTIVE_STATUSES = getattr(_settings, 'ZEBRA_ACTIVE_STATUSES',
('active', 'past_due', 'trialing'))
ZEBRA_INACTIVE_STATUSES = getattr(_settings, 'ZEBRA_INACTIVE_STATUSES',
('cancelled', 'suspended', 'unpaid', 'no_subscription'))
if ZEBRA_ENABLE_APP:
ZEBRA_CUSTOMER_MODEL = getattr(_settings, 'ZEBRA_CUSTOMER_MODEL', 'zebra.Customer')
else:
ZEBRA_CUSTOMER_MODEL = getattr(_settings, 'ZEBRA_CUSTOMER_MODEL', None)

36
vendor/zebra/forms.py vendored Executable file
View file

@ -0,0 +1,36 @@
from django import forms
from django.core.exceptions import NON_FIELD_ERRORS
from django.utils.dates import MONTHS
from zebra.conf import options
from zebra.widgets import NoNameSelect, NoNameTextInput
class MonospaceForm(forms.Form):
def addError(self, message):
self._errors[NON_FIELD_ERRORS] = self.error_class([message])
class CardForm(MonospaceForm):
last_4_digits = forms.CharField(required=True, min_length=4, max_length=4,
widget=forms.HiddenInput())
stripe_token = forms.CharField(required=True, widget=forms.HiddenInput())
class StripePaymentForm(CardForm):
def __init__(self, *args, **kwargs):
super(StripePaymentForm, self).__init__(*args, **kwargs)
self.fields['card_cvv'].label = "Card CVC"
self.fields['card_cvv'].help_text = "Card Verification Code; see rear of card."
months = [ (m[0], u'%02d - %s' % (m[0], unicode(m[1])))
for m in sorted(MONTHS.iteritems()) ]
self.fields['card_expiry_month'].choices = months
card_number = forms.CharField(required=False, max_length=20,
widget=NoNameTextInput())
card_cvv = forms.CharField(required=False, max_length=4,
widget=NoNameTextInput())
card_expiry_month = forms.ChoiceField(required=False, widget=NoNameSelect(),
choices=MONTHS.iteritems())
card_expiry_year = forms.ChoiceField(required=False, widget=NoNameSelect(),
choices=options.ZEBRA_CARD_YEARS_CHOICES)

0
vendor/zebra/management/__init__.py vendored Executable file
View file

View file

View file

@ -0,0 +1,42 @@
from django.core.management.base import BaseCommand
import stripe
from zebra.conf import options as zoptions
CLEAR_CHUNK_SIZE = zoptions.ZEBRA_MAXIMUM_STRIPE_CUSTOMER_LIST_SIZE
class Command(BaseCommand):
help = "Clear all test mode customers from your stripe account."
__test__ = False
def handle(self, *args, **options):
verbosity = int(options.get('verbosity', 1))
stripe.api_key = zoptions.STRIPE_SECRET
customer_chunk = [0]
if verbosity > 0:
print "Clearing stripe test customers:"
num_checked = 0
while len(customer_chunk) is not 0:
customer_chunk = stripe.Customer.all(count=CLEAR_CHUNK_SIZE, offset=num_checked).data
if verbosity > 1:
print "Processing records %s-%s" % (num_checked, num_checked+len(customer_chunk))
for c in customer_chunk:
if verbosity > 2:
print "Deleting %s..." % (c.description),
if not c.livemode:
c.delete()
if verbosity > 2:
print "done"
num_checked = num_checked + len(customer_chunk)
if verbosity > 0:
print "Finished clearing stripe test customers."

169
vendor/zebra/mixins.py vendored Executable file
View file

@ -0,0 +1,169 @@
import stripe
from zebra.conf import options
def _get_attr_value(instance, attr, default=None):
"""
Simple helper to get the value of an instance's attribute if it exists.
If the instance attribute is callable it will be called and the result will
be returned.
Optionally accepts a default value to return if the attribute is missing.
Defaults to `None`
>>> class Foo(object):
... bar = 'baz'
... def hi(self):
... return 'hi'
>>> f = Foo()
>>> _get_attr_value(f, 'bar')
'baz'
>>> _get_attr_value(f, 'xyz')
>>> _get_attr_value(f, 'xyz', False)
False
>>> _get_attr_value(f, 'hi')
'hi'
"""
value = default
if hasattr(instance, attr):
value = getattr(instance, attr)
if callable(value):
value = value()
return value
class StripeMixin(object):
"""
Provides a property `stripe` that returns an instance of the Stripe module.
It optionally supports the ability to set `stripe.api_key` if your class
has a `stripe_api_key` attribute (method or property), or if
settings has a `STRIPE_SECRET` attribute (method or property).
"""
def _get_stripe(self):
if hasattr(self, 'stripe_api_key'):
stripe.api_key = _get_attr_value(self, 'stripe_api_key')
elif hasattr(options, 'STRIPE_SECRET'):
stripe.api_key = _get_attr_value(options, 'STRIPE_SECRET')
return stripe
stripe = property(_get_stripe)
class StripeCustomerMixin(object):
"""
Provides a property property `stripe_customer` that returns a stripe
customer instance.
Your class must provide:
- an attribute `stripe_customer_id` (method or property)
to provide the customer id for the returned instance, and
- an attribute `stripe` (method or property) that returns an instance
of the Stripe module. StripeMixin is an easy way to get this.
"""
def _get_stripe_customer(self):
c = None
if _get_attr_value(self, 'stripe_customer_id'):
c = self.stripe.Customer.retrieve(_get_attr_value(self,
'stripe_customer_id'))
if not c and options.ZEBRA_AUTO_CREATE_STRIPE_CUSTOMERS:
c = self.stripe.Customer.create()
self.stripe_customer_id = c.id
self.save()
return c
stripe_customer = property(_get_stripe_customer)
class StripeSubscriptionMixin(object):
"""
Provides a property `stripe` that returns an instance of the Stripe module &
additionally adds a property `stripe_subscription` that returns a stripe
subscription instance.
Your class must have an attribute `stripe_customer` (method or property)
to provide a customer instance with which to lookup the subscription.
"""
def _get_stripe_subscription(self):
subscription = None
customer = _get_attr_value(self, 'stripe_customer')
if hasattr(customer, 'subscription'):
subscription = customer.subscription
return subscription
stripe_subscription = property(_get_stripe_subscription)
class StripePlanMixin(object):
"""
Provides a property `stripe` that returns an instance of the Stripe module &
additionally adds a property `stripe_plan` that returns a stripe plan
instance.
Your class must have an attribute `stripe_plan_id` (method or property)
to provide the plan id for the returned instance.
"""
def _get_stripe_plan(self):
return stripe.Plan.retrieve(_get_attr_value(self, 'stripe_plan_id'))
stripe_plan = property(_get_stripe_plan)
class StripeInvoiceMixin(object):
"""
Provides a property `stripe` that returns an instance of the Stripe module &
additionally adds a property `stripe_invoice` that returns a stripe invoice
instance.
Your class must have an attribute `stripe_invoice_id` (method or property)
to provide the invoice id for the returned instance.
"""
def _get_stripe_invoice(self):
return stripe.Invoice.retrieve(_get_attr_value(self,
'stripe_invoice_id'))
stripe_invoice = property(_get_stripe_invoice)
class StripeInvoiceItemMixin(object):
"""
Provides a property `stripe` that returns an instance of the Stripe module &
additionally adds a property `stripe_invoice_item` that returns a stripe
invoice item instance.
Your class must have an attribute `stripe_invoice_item_id` (method or
property) to provide the invoice id for the returned instance.
"""
def _get_stripe_invoice(self):
return stripe.Invoice.retrieve(_get_attr_value(self,
'stripe_invoice_item_id'))
stripe_invoice = property(_get_stripe_invoice)
class StripeChargeMixin(object):
"""
Provides a property `stripe` that returns an instance of the Stripe module &
additionally adds a property `stripe_invoice_item` that returns a stripe
invoice item instance.
Your class must have an attribute `stripe_invoice_item_id` (method or
property) to provide the invoice id for the returned instance.
"""
def _get_stripe_charge(self):
return stripe.Charge.retrieve(_get_attr_value(self, 'stripe_charge_id'))
stripe_charge = property(_get_stripe_charge)
class ZebraMixin(StripeMixin, StripeCustomerMixin, StripeSubscriptionMixin,
StripePlanMixin, StripeInvoiceMixin, StripeInvoiceItemMixin,
StripeChargeMixin):
"""
Provides all available Stripe mixins in one class.
`self.stripe`
`self.stripe_customer`
`self.stripe_subscription`
`self.stripe_plan`
"""
pass

60
vendor/zebra/models.py vendored Executable file
View file

@ -0,0 +1,60 @@
from django.db import models
from zebra import mixins
from zebra.conf import options
class StripeCustomer(models.Model, mixins.StripeMixin, mixins.StripeCustomerMixin):
stripe_customer_id = models.CharField(max_length=50, blank=True, null=True)
class Meta:
abstract = True
def __unicode__(self):
return u"%s" % self.stripe_customer_id
class StripePlan(models.Model, mixins.StripeMixin, mixins.StripePlanMixin):
stripe_plan_id = models.CharField(max_length=50, blank=True, null=True)
class Meta:
abstract = True
def __unicode__(self):
return u"%s" % self.stripe_plan_id
class StripeSubscription(models.Model, mixins.StripeMixin, mixins.StripeSubscriptionMixin):
"""
You need to provide a stripe_customer attribute. See zebra.models for an
example implimentation.
"""
class Meta:
abstract = True
# Non-abstract classes must be enabled in your project's settings.py
if options.ZEBRA_ENABLE_APP:
class DatesModelBase(models.Model):
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Customer(DatesModelBase, StripeCustomer):
pass
class Plan(DatesModelBase, StripePlan):
pass
class Subscription(DatesModelBase, StripeSubscription):
customer = models.ForeignKey(Customer)
plan = models.ForeignKey(Plan)
def __unicode__(self):
return u"%s: %s" % (self.customer, self.plan)
@property
def stripe_customer(self):
return self.customer.stripe_customer

122
vendor/zebra/signals.py vendored Executable file
View file

@ -0,0 +1,122 @@
"""
Provides the following signals:
V1
- zebra_webhook_recurring_payment_failed
- zebra_webhook_invoice_ready
- zebra_webhook_recurring_payment_succeeded
- zebra_webhook_subscription_trial_ending
- zebra_webhook_subscription_final_payment_attempt_failed
- zebra_webhook_subscription_ping_sent
v2
- zebra_webhook_charge_succeeded
- zebra_webhook_charge_failed
- zebra_webhook_charge_refunded
- zebra_webhook_charge_disputed
- zebra_webhook_customer_created
- zebra_webhook_customer_updated
- zebra_webhook_customer_deleted
- zebra_webhook_customer_subscription_created
- zebra_webhook_customer_subscription_updated
- zebra_webhook_customer_subscription_deleted
- zebra_webhook_customer_subscription_trial_will_end
- zebra_webhook_customer_discount_created
- zebra_webhook_customer_discount_updated
- zebra_webhook_customer_discount_deleted
- zebra_webhook_invoice_created
- zebra_webhook_invoice_updated
- zebra_webhook_invoice_payment_succeeded
- zebra_webhook_invoice_payment_failed
- zebra_webhook_invoiceitem_created
- zebra_webhook_invoiceitem_updated
- zebra_webhook_invoiceitem_deleted
- zebra_webhook_plan_created
- zebra_webhook_plan_updated
- zebra_webhook_plan_deleted
- zebra_webhook_coupon_created
- zebra_webhook_coupon_updated
- zebra_webhook_coupon_deleted
- zebra_webhook_transfer_created
- zebra_webhook_transfer_failed
- zebra_webhook_ping
"""
import django.dispatch
WEBHOOK_ARGS = ["customer", "full_json"]
zebra_webhook_recurring_payment_failed = django.dispatch.Signal(providing_args=WEBHOOK_ARGS)
zebra_webhook_invoice_ready = django.dispatch.Signal(providing_args=WEBHOOK_ARGS)
zebra_webhook_recurring_payment_succeeded = django.dispatch.Signal(providing_args=WEBHOOK_ARGS)
zebra_webhook_subscription_trial_ending = django.dispatch.Signal(providing_args=WEBHOOK_ARGS)
zebra_webhook_subscription_final_payment_attempt_failed = django.dispatch.Signal(providing_args=WEBHOOK_ARGS)
zebra_webhook_subscription_ping_sent = django.dispatch.Signal(providing_args=[])
# v2 webhooks
WEBHOOK2_ARGS = ["full_json"]
zebra_webhook_charge_succeeded = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_charge_failed = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_charge_refunded = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_charge_disputed = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_updated = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_deleted = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_subscription_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_subscription_updated = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_subscription_deleted = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_subscription_trial_will_end = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_discount_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_discount_updated = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_customer_discount_deleted = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_invoice_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_invoice_updated = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_invoice_payment_succeeded = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_invoice_payment_failed = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_invoiceitem_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_invoiceitem_updated = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_invoiceitem_deleted = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_plan_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_plan_updated = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_plan_deleted = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_coupon_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_coupon_updated = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_coupon_deleted = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_transfer_created = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_transfer_failed = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
zebra_webhook_ping = django.dispatch.Signal(providing_args=WEBHOOK2_ARGS)
WEBHOOK_MAP = {
'charge_succeeded': zebra_webhook_charge_succeeded,
'charge_failed': zebra_webhook_charge_failed,
'charge_refunded': zebra_webhook_charge_refunded,
'charge_disputed': zebra_webhook_charge_disputed,
'customer_created': zebra_webhook_customer_created,
'customer_updated': zebra_webhook_customer_updated,
'customer_deleted': zebra_webhook_customer_deleted,
'customer_subscription_created': zebra_webhook_customer_subscription_created,
'customer_subscription_updated': zebra_webhook_customer_subscription_updated,
'customer_subscription_deleted': zebra_webhook_customer_subscription_deleted,
'customer_subscription_trial_will_end': zebra_webhook_customer_subscription_trial_will_end,
'customer_discount_created': zebra_webhook_customer_discount_created,
'customer_discount_updated': zebra_webhook_customer_discount_updated,
'customer_discount_deleted': zebra_webhook_customer_discount_deleted,
'invoice_created': zebra_webhook_invoice_created,
'invoice_updated': zebra_webhook_invoice_updated,
'invoice_payment_succeeded': zebra_webhook_invoice_payment_succeeded,
'invoice_payment_failed': zebra_webhook_invoice_payment_failed,
'invoiceitem_created': zebra_webhook_invoiceitem_created,
'invoiceitem_updated': zebra_webhook_invoiceitem_updated,
'invoiceitem_deleted': zebra_webhook_invoiceitem_deleted,
'plan_created': zebra_webhook_plan_created,
'plan_updated': zebra_webhook_plan_updated,
'plan_deleted': zebra_webhook_plan_deleted,
'coupon_created': zebra_webhook_coupon_created,
'coupon_updated': zebra_webhook_coupon_updated,
'coupon_deleted': zebra_webhook_coupon_deleted,
'transfer_created': zebra_webhook_transfer_created,
'transfer_failed': zebra_webhook_transfer_failed,
'ping': zebra_webhook_ping,
}

9
vendor/zebra/static/zebra/card-form.css vendored Executable file
View file

@ -0,0 +1,9 @@
#id_card_number {
width: 15ex;
}
#id_card_cvv {
width: 3ex;
}
#id_card_expiry_year {
width: 10ex;
}

33
vendor/zebra/static/zebra/card-form.js vendored Executable file
View file

@ -0,0 +1,33 @@
$(function() {
$("#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) {
// console.log(status, response);
$("#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
});
});

View file

@ -0,0 +1,6 @@
<form action="" method="POST" id="payment-form">{% csrf_token %}
<noscript><h3>Note:&nbsp; this form requires Javascript to use.</h3></noscript>
<span class="payment-errors"></span>
{{ zebra_form.as_p }}
<button type="submit" class="submit-button NB-modal-submit-button NB-modal-submit-green">Submit Payment</button>
</form>

View file

@ -0,0 +1,3 @@
<script type="text/javascript">
Stripe.setPublishableKey('{{STRIPE_PUBLISHABLE}}');
</script>

0
vendor/zebra/templatetags/__init__.py vendored Executable file
View file

30
vendor/zebra/templatetags/zebra_tags.py vendored Executable file
View file

@ -0,0 +1,30 @@
from django.core.urlresolvers import reverse
from django import template
from django.template.loader import render_to_string
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from zebra.conf import options
register = template.Library()
def _set_up_zebra_form(context):
if not "zebra_form" in context:
if "form" in context:
context["zebra_form"] = context["form"]
else:
raise Exception, "Missing stripe form."
context["STRIPE_PUBLISHABLE"] = options.STRIPE_PUBLISHABLE
return context
@register.inclusion_tag('zebra/_stripe_js_and_set_stripe_key.html', takes_context=True)
def zebra_head_and_stripe_key(context):
return _set_up_zebra_form(context)
@register.inclusion_tag('zebra/_basic_card_form.html', takes_context=True)
def zebra_card_form(context):
return _set_up_zebra_form(context)

8
vendor/zebra/urls.py vendored Executable file
View file

@ -0,0 +1,8 @@
from django.conf.urls.defaults import *
from zebra import views
urlpatterns = patterns('',
url(r'webhooks/$', views.webhooks, name='webhooks'),
url(r'webhooks/v2/$', views.webhooks_v2, name='webhooks_v2'),
)

26
vendor/zebra/utils.py vendored Executable file
View file

@ -0,0 +1,26 @@
from zebra.conf import options
AUDIT_RESULTS = options.ZEBRA_AUDIT_RESULTS
def audit_customer_subscription(customer, unknown=True):
"""
Audits the provided customer's subscription against stripe and returns a pair
that contains a boolean and a result type.
Default result types can be found in zebra.conf.defaults and can be
overridden in your project's settings.
"""
if (hasattr(customer, 'suspended') and customer.suspended):
result = AUDIT_RESULTS['suspended']
else:
if hasattr(customer, 'subscription'):
try:
result = AUDIT_RESULTS[customer.subscription.status]
except KeyError, err:
# TODO should this be a more specific exception class?
raise Exception("Unable to locate a result set for \
subscription status %s in ZEBRA_AUDIT_RESULTS") % str(err)
else:
result = AUDIT_RESULTS['no_subscription']
return result

73
vendor/zebra/views.py vendored Executable file
View file

@ -0,0 +1,73 @@
from django.http import HttpResponse
from django.utils import simplejson
from django.db.models import get_model
import stripe
from zebra.conf import options
from zebra.signals import *
from django.views.decorators.csrf import csrf_exempt
import logging
log = logging.getLogger("zebra.%s" % __name__)
stripe.api_key = options.STRIPE_SECRET
def _try_to_get_customer_from_customer_id(stripe_customer_id):
if options.ZEBRA_CUSTOMER_MODEL:
m = get_model(*options.ZEBRA_CUSTOMER_MODEL.split('.'))
try:
return m.objects.get(stripe_customer_id=stripe_customer_id)
except:
pass
return None
@csrf_exempt
def webhooks(request):
"""
Handles all known webhooks from stripe, and calls signals.
Plug in as you need.
"""
if request.method != "POST":
return HttpResponse("Invalid Request.", status=400)
json = simplejson.loads(request.POST["json"])
if json["event"] == "recurring_payment_failed":
zebra_webhook_recurring_payment_failed.send(sender=None, customer=_try_to_get_customer_from_customer_id(json["customer"]), full_json=json)
elif json["event"] == "invoice_ready":
zebra_webhook_invoice_ready.send(sender=None, customer=_try_to_get_customer_from_customer_id(json["customer"]), full_json=json)
elif json["event"] == "recurring_payment_succeeded":
zebra_webhook_recurring_payment_succeeded.send(sender=None, customer=_try_to_get_customer_from_customer_id(json["customer"]), full_json=json)
elif json["event"] == "subscription_trial_ending":
zebra_webhook_subscription_trial_ending.send(sender=None, customer=_try_to_get_customer_from_customer_id(json["customer"]), full_json=json)
elif json["event"] == "subscription_final_payment_attempt_failed":
zebra_webhook_subscription_final_payment_attempt_failed.send(sender=None, customer=_try_to_get_customer_from_customer_id(json["customer"]), full_json=json)
elif json["event"] == "ping":
zebra_webhook_subscription_ping_sent.send(sender=None)
else:
return HttpResponse(status=400)
return HttpResponse(status=200)
@csrf_exempt
def webhooks_v2(request):
"""
Handles all known webhooks from stripe, and calls signals.
Plug in as you need.
"""
if request.method != "POST":
return HttpResponse("Invalid Request.", status=400)
event_json = simplejson.loads(request.raw_post_data)
event_key = event_json['type'].replace('.', '_')
if event_key in WEBHOOK_MAP:
WEBHOOK_MAP[event_key].send(sender=None, full_json=event_json)
return HttpResponse(status=200)

41
vendor/zebra/widgets.py vendored Executable file
View file

@ -0,0 +1,41 @@
from django.forms.widgets import Select, TextInput
from django.utils.safestring import mark_safe
class NoNameWidget(object):
def _update_to_noname_class_name(self, name, kwargs_dict):
if "attrs" in kwargs_dict:
if "class" in kwargs_dict["attrs"]:
kwargs_dict["attrs"]["class"] += " %s" % (name.replace("_", "-"), )
else:
kwargs_dict["attrs"].update({'class': name.replace("_", "-")})
else:
kwargs_dict["attrs"] = {'class': name.replace("_", "-")}
return kwargs_dict
def _strip_name_attr(self, widget_string, name):
return widget_string.replace("name=\"%s\"" % (name,), "")
class Media:
css = {
'all': ('zebra/card-form.css',)
}
js = ('zebra/card-form.js', 'https://js.stripe.com/v1/')
class NoNameTextInput(TextInput, NoNameWidget):
def render(self, name, *args, **kwargs):
print name, kwargs
kwargs = self._update_to_noname_class_name(name, kwargs)
return mark_safe(self._strip_name_attr(super(NoNameTextInput, self).render(name, *args, **kwargs), name))
class NoNameSelect(Select, NoNameWidget):
def render(self, name, *args, **kwargs):
kwargs = self._update_to_noname_class_name(name, kwargs)
return mark_safe(self._strip_name_attr(super(NoNameSelect, self).render(name, *args, **kwargs), name))