mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-31 21:41:33 +00:00
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:
commit
a16c25fce9
62 changed files with 1654 additions and 379 deletions
33
apps/profile/forms.py
Normal file
33
apps/profile/forms.py
Normal 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')
|
85
apps/profile/migrations/0016_profile_stripe.py
Normal file
85
apps/profile/migrations/0016_profile_stripe.py
Normal 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']
|
|
@ -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:
|
||||
|
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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):
|
||||
|
|
|
@ -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,))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
22
fabfile.py
vendored
|
@ -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')
|
||||
|
|
|
@ -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
154
media/css/payments.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
BIN
media/img/reader/logo-paypal.png
Normal file
BIN
media/img/reader/logo-paypal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -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 + '/';
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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() {});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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' }, [
|
||||
|
|
117
media/js/newsblur/stripe_form.js
Normal file
117
media/js/newsblur/stripe_form.js
Normal 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();
|
||||
}
|
||||
});
|
165
settings.py
165
settings.py
|
@ -39,10 +39,12 @@ if '/utils' not in ' '.join(sys.path):
|
|||
sys.path.append(UTILS_ROOT)
|
||||
if '/vendor' not in ' '.join(sys.path):
|
||||
sys.path.append(VENDOR_ROOT)
|
||||
|
||||
# ===================
|
||||
# = Global Settings =
|
||||
# ===================
|
||||
|
||||
DEBUG = False
|
||||
TEST_DEBUG = False
|
||||
SEND_BROKEN_LINK_EMAILS = False
|
||||
MANAGERS = ADMINS
|
||||
|
@ -60,7 +62,7 @@ ADMIN_MEDIA_PREFIX = '/media/admin/'
|
|||
SECRET_KEY = 'YOUR_SECRET_KEY'
|
||||
EMAIL_BACKEND = 'django_ses.SESBackend'
|
||||
CIPHER_USERNAMES = False
|
||||
|
||||
DEBUG_ASSETS = DEBUG
|
||||
|
||||
# ===============
|
||||
# = Enviornment =
|
||||
|
@ -160,143 +162,6 @@ LOGGING = {
|
|||
}
|
||||
}
|
||||
|
||||
# =====================
|
||||
# = Media Compression =
|
||||
# =====================
|
||||
|
||||
COMPRESS_JS = {
|
||||
'all': {
|
||||
'source_filenames': (
|
||||
'js/jquery-1.7.1.js',
|
||||
'js/inflector.js',
|
||||
'js/jquery.json.js',
|
||||
'js/jquery.easing.js',
|
||||
'js/jquery.newsblur.js',
|
||||
'js/jquery.scrollTo.js',
|
||||
'js/jquery.corners.js',
|
||||
'js/jquery.hotkeys.js',
|
||||
'js/jquery.ajaxupload.js',
|
||||
'js/jquery.ajaxmanager.3.js',
|
||||
'js/jquery.simplemodal-1.3.js',
|
||||
'js/jquery.color.js',
|
||||
'js/jquery.rightclick.js',
|
||||
'js/jquery.ui.core.js',
|
||||
'js/jquery.ui.widget.js',
|
||||
'js/jquery.ui.mouse.js',
|
||||
'js/jquery.ui.position.js',
|
||||
'js/jquery.ui.draggable.js',
|
||||
'js/jquery.ui.sortable.js',
|
||||
'js/jquery.ui.slider.js',
|
||||
'js/jquery.ui.autocomplete.js',
|
||||
'js/jquery.ui.progressbar.js',
|
||||
'js/jquery.layout.js',
|
||||
'js/jquery.tinysort.js',
|
||||
'js/jquery.fieldselection.js',
|
||||
'js/jquery.flot.js',
|
||||
'js/jquery.tipsy.js',
|
||||
# 'js/socket.io-client.0.8.7.js',
|
||||
'js/underscore.js',
|
||||
'js/underscore.string.js',
|
||||
'js/newsblur/reader_utils.js',
|
||||
'js/newsblur/assetmodel.js',
|
||||
'js/newsblur/reader.js',
|
||||
'js/newsblur/generate_bookmarklet.js',
|
||||
'js/newsblur/modal.js',
|
||||
'js/newsblur/reader_classifier.js',
|
||||
'js/newsblur/reader_add_feed.js',
|
||||
'js/newsblur/reader_mark_read.js',
|
||||
'js/newsblur/reader_goodies.js',
|
||||
'js/newsblur/reader_preferences.js',
|
||||
'js/newsblur/reader_account.js',
|
||||
'js/newsblur/reader_feedchooser.js',
|
||||
'js/newsblur/reader_statistics.js',
|
||||
'js/newsblur/reader_feed_exception.js',
|
||||
'js/newsblur/reader_keyboard.js',
|
||||
'js/newsblur/reader_recommend_feed.js',
|
||||
'js/newsblur/reader_send_email.js',
|
||||
'js/newsblur/reader_tutorial.js',
|
||||
'js/newsblur/about.js',
|
||||
'js/newsblur/faq.js',
|
||||
),
|
||||
'output_filename': 'js/all-compressed-?.js'
|
||||
},
|
||||
'mobile': {
|
||||
'source_filenames': (
|
||||
'js/jquery-1.7.1.js',
|
||||
'js/mobile/jquery.mobile-1.0b1.js',
|
||||
'js/jquery.ajaxmanager.3.js',
|
||||
'js/underscore.js',
|
||||
'js/underscore.string.js',
|
||||
'js/inflector.js',
|
||||
'js/jquery.json.js',
|
||||
'js/jquery.easing.js',
|
||||
'js/jquery.newsblur.js',
|
||||
'js/newsblur/reader_utils.js',
|
||||
'js/newsblur/assetmodel.js',
|
||||
'js/mobile/newsblur/mobile_workspace.js',
|
||||
),
|
||||
'output_filename': 'js/mobile-compressed-?.js',
|
||||
},
|
||||
'paypal': {
|
||||
'source_filenames': (
|
||||
'js/newsblur/paypal_return.js',
|
||||
),
|
||||
'output_filename': 'js/paypal-compressed-?.js',
|
||||
},
|
||||
'bookmarklet': {
|
||||
'source_filenames': (
|
||||
'js/jquery-1.5.1.min.js',
|
||||
'js/jquery.noConflict.js',
|
||||
'js/jquery.newsblur.js',
|
||||
'js/jquery.tinysort.js',
|
||||
'js/jquery.simplemodal-1.3.js',
|
||||
'js/jquery.corners.js',
|
||||
),
|
||||
'output_filename': 'js/bookmarklet-compressed-?.js',
|
||||
},
|
||||
}
|
||||
|
||||
COMPRESS_CSS = {
|
||||
'all': {
|
||||
'source_filenames': (
|
||||
'css/reader.css',
|
||||
'css/modals.css',
|
||||
'css/status.css',
|
||||
'css/jquery-ui/jquery.theme.css',
|
||||
'css/jquery.tipsy.css',
|
||||
),
|
||||
'output_filename': 'css/all-compressed-?.css'
|
||||
},
|
||||
'mobile': {
|
||||
'source_filenames': (
|
||||
'css/mobile/jquery.mobile-1.0b1.css',
|
||||
'css/mobile/mobile.css',
|
||||
),
|
||||
'output_filename': 'css/mobile/mobile-compressed-?.css',
|
||||
},
|
||||
'paypal': {
|
||||
'source_filenames': (
|
||||
'css/paypal_return.css',
|
||||
),
|
||||
'output_filename': 'css/paypal-compressed-?.css',
|
||||
},
|
||||
'bookmarklet': {
|
||||
'source_filenames': (
|
||||
'css/reset.css',
|
||||
'css/modals.css',
|
||||
),
|
||||
'output_filename': 'css/paypal-compressed-?.css',
|
||||
},
|
||||
}
|
||||
|
||||
COMPRESS_VERSION = True
|
||||
COMPRESS_JS_FILTERS = ['compress.filters.jsmin.JSMinFilter']
|
||||
COMPRESS_CSS_FILTERS = []
|
||||
|
||||
# YUI_DIR = ''.join([UTILS_ROOT, '/yuicompressor-2.4.2/build/yuicompressor-2.4.2.jar'])
|
||||
# COMPRESS_YUI_BINARY = 'java -jar ' + YUI_DIR
|
||||
# COMPRESS_YUI_JS_ARGUMENTS = '--preserve-semi --nomunge --disable-optimizations'
|
||||
|
||||
# ==========================
|
||||
# = Miscellaneous Settings =
|
||||
# ==========================
|
||||
|
@ -336,7 +201,6 @@ INSTALLED_APPS = (
|
|||
'django.contrib.admin',
|
||||
'django_extensions',
|
||||
'djcelery',
|
||||
# 'seacucumber',
|
||||
'django_ses',
|
||||
'apps.rss_feeds',
|
||||
'apps.reader',
|
||||
|
@ -352,12 +216,21 @@ INSTALLED_APPS = (
|
|||
'vendor',
|
||||
'vendor.typogrify',
|
||||
'vendor.paypal.standard.ipn',
|
||||
'vendor.zebra',
|
||||
)
|
||||
|
||||
if not DEVELOPMENT:
|
||||
INSTALLED_APPS += (
|
||||
'gunicorn',
|
||||
)
|
||||
|
||||
# ==========
|
||||
# = Stripe =
|
||||
# ==========
|
||||
|
||||
STRIPE_SECRET = "YOUR-SECRET-API-KEY"
|
||||
STRIPE_PUBLISHABLE = "YOUR-PUBLISHABLE-API-KEY"
|
||||
ZEBRA_ENABLE_APP = True
|
||||
|
||||
# ==========
|
||||
# = Celery =
|
||||
|
@ -388,21 +261,17 @@ CELERY_QUEUES = {
|
|||
},
|
||||
}
|
||||
CELERY_DEFAULT_QUEUE = "update_feeds"
|
||||
BROKER_BACKEND = "amqplib"
|
||||
BROKER_HOST = "db01.newsblur.com"
|
||||
BROKER_PORT = 5672
|
||||
BROKER_USER = "newsblur"
|
||||
BROKER_PASSWORD = "newsblur"
|
||||
BROKER_VHOST = "newsblurvhost"
|
||||
BROKER_BACKEND = "redis"
|
||||
BROKER_URL = "redis://db01:6379/0"
|
||||
CELERY_REDIS_HOST = "db01"
|
||||
|
||||
CELERY_RESULT_BACKEND = "amqp"
|
||||
CELERYD_LOG_LEVEL = 'ERROR'
|
||||
CELERYD_PREFETCH_MULTIPLIER = 1
|
||||
CELERY_IMPORTS = ("apps.rss_feeds.tasks", )
|
||||
CELERYD_CONCURRENCY = 4
|
||||
CELERY_IGNORE_RESULT = True
|
||||
CELERY_ACKS_LATE = True # Retry if task fails
|
||||
CELERYD_MAX_TASKS_PER_CHILD = 10
|
||||
# CELERYD_TASK_TIME_LIMIT = 12 * 30
|
||||
CELERYD_TASK_TIME_LIMIT = 12 * 30
|
||||
CELERY_DISABLE_RATE_LIMITS = True
|
||||
|
||||
# ====================
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>. ← 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>
|
||||
|
|
73
templates/profile/stripe_form.xhtml
Normal file
73
templates/profile/stripe_form.xhtml
Normal 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: 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 %}
|
|
@ -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>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block extra_head_js %}
|
||||
{% include_stylesheets "common" %}
|
||||
{% include_javascripts "paypal" %}
|
||||
{% include_javascripts "payments" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
1
urls.py
1
urls.py
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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>
|
||||
|
|
4
vendor/paypal/standard/conf.py
vendored
4
vendor/paypal/standard/conf.py
vendored
|
@ -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
0
vendor/zebra/__init__.py
vendored
Executable file
10
vendor/zebra/admin.py
vendored
Executable file
10
vendor/zebra/admin.py
vendored
Executable 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
0
vendor/zebra/conf/__init__.py
vendored
Executable file
59
vendor/zebra/conf/options.py
vendored
Executable file
59
vendor/zebra/conf/options.py
vendored
Executable 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
36
vendor/zebra/forms.py
vendored
Executable 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
0
vendor/zebra/management/__init__.py
vendored
Executable file
0
vendor/zebra/management/commands/__init__.py
vendored
Executable file
0
vendor/zebra/management/commands/__init__.py
vendored
Executable file
42
vendor/zebra/management/commands/clear_stripe_test_customers.py
vendored
Executable file
42
vendor/zebra/management/commands/clear_stripe_test_customers.py
vendored
Executable 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
169
vendor/zebra/mixins.py
vendored
Executable 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
60
vendor/zebra/models.py
vendored
Executable 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
122
vendor/zebra/signals.py
vendored
Executable 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
9
vendor/zebra/static/zebra/card-form.css
vendored
Executable 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
33
vendor/zebra/static/zebra/card-form.js
vendored
Executable 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
|
||||
|
||||
});
|
||||
});
|
6
vendor/zebra/templates/zebra/_basic_card_form.html
vendored
Executable file
6
vendor/zebra/templates/zebra/_basic_card_form.html
vendored
Executable file
|
@ -0,0 +1,6 @@
|
|||
<form action="" method="POST" id="payment-form">{% csrf_token %}
|
||||
<noscript><h3>Note: 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>
|
3
vendor/zebra/templates/zebra/_stripe_js_and_set_stripe_key.html
vendored
Executable file
3
vendor/zebra/templates/zebra/_stripe_js_and_set_stripe_key.html
vendored
Executable file
|
@ -0,0 +1,3 @@
|
|||
<script type="text/javascript">
|
||||
Stripe.setPublishableKey('{{STRIPE_PUBLISHABLE}}');
|
||||
</script>
|
0
vendor/zebra/templatetags/__init__.py
vendored
Executable file
0
vendor/zebra/templatetags/__init__.py
vendored
Executable file
30
vendor/zebra/templatetags/zebra_tags.py
vendored
Executable file
30
vendor/zebra/templatetags/zebra_tags.py
vendored
Executable 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
8
vendor/zebra/urls.py
vendored
Executable 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
26
vendor/zebra/utils.py
vendored
Executable 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
73
vendor/zebra/views.py
vendored
Executable 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
41
vendor/zebra/widgets.py
vendored
Executable 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))
|
Loading…
Add table
Reference in a new issue