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

{% block resources_header %}There are a couple resources you can use if you end up loving NewsBlur:{% endblock resources_header %}

@@ -46,7 +46,7 @@

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

diff --git a/templates/profile/stripe_form.xhtml b/templates/profile/stripe_form.xhtml new file mode 100644 index 000000000..48932df5b --- /dev/null +++ b/templates/profile/stripe_form.xhtml @@ -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" %} + + + + + {% zebra_head_and_stripe_key %} +{% endblock %} + +{% block content %} + +
+
+ +
{{ user.username }}
+
+ + + + +
+ +
{% csrf_token %} + +
+ {{ zebra_form.card_number.label_tag }} + {{ zebra_form.card_number }} +
+
+ {{ zebra_form.card_cvv.label_tag }} + {{ zebra_form.card_cvv }} +
+ +
+ {{ zebra_form.card_expiry_month.label_tag }} + {{ zebra_form.card_expiry_month }} +
+ +
+ {{ zebra_form.card_expiry_year.label_tag }} + {{ zebra_form.card_expiry_year }} +
+ +
+ {{ zebra_form.email.label_tag }} + {{ zebra_form.email }} +
+ +
+ {{ zebra_form.plan.label_tag }} + {{ zebra_form.plan|safe }} +
+ + {{ zebra_form.last_4_digits }} + {{ zebra_form.stripe_token }} + + + + + + +
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/reader/feeds.xhtml b/templates/reader/feeds.xhtml index 0e95d2f5d..526a6d07e 100644 --- a/templates/reader/feeds.xhtml +++ b/templates/reader/feeds.xhtml @@ -2,15 +2,29 @@ {% load typogrify_tags recommendations_tags utils_tags statistics_tags %} +{% block extra_head_js %} + + + {% if user.is_staff %} + + {% endif %} +{% endblock %} + {% block content %} -

NewsBlur

- A visual feed reader with intelligence.

@@ -28,14 +42,6 @@ $(document).ready(function() {
Add
- {% endif %}
diff --git a/templates/reader/paypal_return.xhtml b/templates/reader/paypal_return.xhtml index da19452db..54ccf5cae 100644 --- a/templates/reader/paypal_return.xhtml +++ b/templates/reader/paypal_return.xhtml @@ -4,7 +4,7 @@ {% block extra_head_js %} {% include_stylesheets "common" %} - {% include_javascripts "paypal" %} + {% include_javascripts "payments" %} {% endblock %} {% block content %} diff --git a/urls.py b/urls.py index ad45bf856..02375722d 100644 --- a/urls.py +++ b/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: diff --git a/utils/feed_fetcher.py b/utils/feed_fetcher.py index f629b292f..c8d94ea49 100644 --- a/utils/feed_fetcher.py +++ b/utils/feed_fetcher.py @@ -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() diff --git a/utils/feedparser.py b/utils/feedparser.py index d88dfb412..5e6b9cecd 100755 --- a/utils/feedparser.py +++ b/utils/feedparser.py @@ -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: diff --git a/utils/jammit.py b/utils/jammit.py index 411d90494..351f27eb6 100644 --- a/utils/jammit.py +++ b/utils/jammit.py @@ -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': diff --git a/utils/json_functions.py b/utils/json_functions.py index fe56816f2..42eb4d6d3 100644 --- a/utils/json_functions.py +++ b/utils/json_functions.py @@ -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 diff --git a/utils/munin/newsblur_updates.py b/utils/munin/newsblur_updates.py index 0e6438938..f2fb50aa3 100755 --- a/utils/munin/newsblur_updates.py +++ b/utils/munin/newsblur_updates.py @@ -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__': diff --git a/utils/story_functions.py b/utils/story_functions.py index da0b2cf63..621114de9 100644 --- a/utils/story_functions.py +++ b/utils/story_functions.py @@ -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'] += """

diff --git a/vendor/paypal/standard/conf.py b/vendor/paypal/standard/conf.py index 1a6c11df5..db3cb2a0a 100644 --- a/vendor/paypal/standard/conf.py +++ b/vendor/paypal/standard/conf.py @@ -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" \ No newline at end of file +SUBSCRIPTION_SANDBOX_IMAGE = "https://www.sandbox.paypal.com/en_US/i/btn/btn_subscribe_LG.gif" \ No newline at end of file diff --git a/vendor/zebra/__init__.py b/vendor/zebra/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/vendor/zebra/admin.py b/vendor/zebra/admin.py new file mode 100755 index 000000000..dda9e8051 --- /dev/null +++ b/vendor/zebra/admin.py @@ -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) diff --git a/vendor/zebra/conf/__init__.py b/vendor/zebra/conf/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/vendor/zebra/conf/options.py b/vendor/zebra/conf/options.py new file mode 100755 index 000000000..b8e183a45 --- /dev/null +++ b/vendor/zebra/conf/options.py @@ -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) diff --git a/vendor/zebra/forms.py b/vendor/zebra/forms.py new file mode 100755 index 000000000..004132053 --- /dev/null +++ b/vendor/zebra/forms.py @@ -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) diff --git a/vendor/zebra/management/__init__.py b/vendor/zebra/management/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/vendor/zebra/management/commands/__init__.py b/vendor/zebra/management/commands/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/vendor/zebra/management/commands/clear_stripe_test_customers.py b/vendor/zebra/management/commands/clear_stripe_test_customers.py new file mode 100755 index 000000000..165fba8a5 --- /dev/null +++ b/vendor/zebra/management/commands/clear_stripe_test_customers.py @@ -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." diff --git a/vendor/zebra/mixins.py b/vendor/zebra/mixins.py new file mode 100755 index 000000000..633b8514e --- /dev/null +++ b/vendor/zebra/mixins.py @@ -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 diff --git a/vendor/zebra/models.py b/vendor/zebra/models.py new file mode 100755 index 000000000..deaa2a70d --- /dev/null +++ b/vendor/zebra/models.py @@ -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 \ No newline at end of file diff --git a/vendor/zebra/signals.py b/vendor/zebra/signals.py new file mode 100755 index 000000000..c2911a4ef --- /dev/null +++ b/vendor/zebra/signals.py @@ -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, +} diff --git a/vendor/zebra/static/zebra/card-form.css b/vendor/zebra/static/zebra/card-form.css new file mode 100755 index 000000000..d77015bec --- /dev/null +++ b/vendor/zebra/static/zebra/card-form.css @@ -0,0 +1,9 @@ +#id_card_number { + width: 15ex; +} +#id_card_cvv { + width: 3ex; +} +#id_card_expiry_year { + width: 10ex; +} \ No newline at end of file diff --git a/vendor/zebra/static/zebra/card-form.js b/vendor/zebra/static/zebra/card-form.js new file mode 100755 index 000000000..139d0aa52 --- /dev/null +++ b/vendor/zebra/static/zebra/card-form.js @@ -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 + + }); +}); diff --git a/vendor/zebra/templates/zebra/_basic_card_form.html b/vendor/zebra/templates/zebra/_basic_card_form.html new file mode 100755 index 000000000..32e34e760 --- /dev/null +++ b/vendor/zebra/templates/zebra/_basic_card_form.html @@ -0,0 +1,6 @@ +
{% csrf_token %} + + + {{ zebra_form.as_p }} + +
diff --git a/vendor/zebra/templates/zebra/_stripe_js_and_set_stripe_key.html b/vendor/zebra/templates/zebra/_stripe_js_and_set_stripe_key.html new file mode 100755 index 000000000..f34b39215 --- /dev/null +++ b/vendor/zebra/templates/zebra/_stripe_js_and_set_stripe_key.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/vendor/zebra/templatetags/__init__.py b/vendor/zebra/templatetags/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/vendor/zebra/templatetags/zebra_tags.py b/vendor/zebra/templatetags/zebra_tags.py new file mode 100755 index 000000000..60db3d7b4 --- /dev/null +++ b/vendor/zebra/templatetags/zebra_tags.py @@ -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) diff --git a/vendor/zebra/urls.py b/vendor/zebra/urls.py new file mode 100755 index 000000000..929b79e55 --- /dev/null +++ b/vendor/zebra/urls.py @@ -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'), +) diff --git a/vendor/zebra/utils.py b/vendor/zebra/utils.py new file mode 100755 index 000000000..07a565d6e --- /dev/null +++ b/vendor/zebra/utils.py @@ -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 \ No newline at end of file diff --git a/vendor/zebra/views.py b/vendor/zebra/views.py new file mode 100755 index 000000000..6ce4a3622 --- /dev/null +++ b/vendor/zebra/views.py @@ -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) diff --git a/vendor/zebra/widgets.py b/vendor/zebra/widgets.py new file mode 100755 index 000000000..fe9eb61e4 --- /dev/null +++ b/vendor/zebra/widgets.py @@ -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))