diff --git a/apps/profile/migrations/0015_send_emails.py b/apps/profile/migrations/0015_send_emails.py new file mode 100644 index 000000000..a914a6688 --- /dev/null +++ b/apps/profile/migrations/0015_send_emails.py @@ -0,0 +1,77 @@ +# 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.send_emails' + db.add_column('profile_profile', 'send_emails', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Profile.send_emails' + db.delete_column('profile_profile', 'send_emails') + + + 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'}), + '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 a66b9f0f9..4e6585119 100644 --- a/apps/profile/models.py +++ b/apps/profile/models.py @@ -23,6 +23,7 @@ from vendor.paypal.standard.ipn.signals import subscription_signup class Profile(models.Model): user = models.OneToOneField(User, unique=True, related_name="profile") is_premium = models.BooleanField(default=False) + send_emails = models.BooleanField(default=True) preferences = models.TextField(default="{}") view_settings = models.TextField(default="{}") collapsed_folders = models.TextField(default="[]") @@ -49,6 +50,8 @@ class Profile(models.Model): self.is_premium = True self.save() + self.send_new_premium_email() + subs = UserSubscription.objects.filter(user=self.user) for sub in subs: sub.active = True @@ -101,8 +104,8 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()} stale_feeds = list(set([f.feed.pk for f in stale_feeds])) self.queue_new_feeds(new_feeds=stale_feeds) - def mail_new_account(self): - if not self.user.email: + def send_new_user_email(self): + if not self.user.email or not self.send_emails: return user = self.user @@ -115,8 +118,8 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()} msg.attach_alternative(html, "text/html") msg.send() - def mail_new_premium(self): - if not self.user.email: + def send_new_premium_email(self): + if not self.user.email or not self.send_emails: return user = self.user diff --git a/apps/profile/views.py b/apps/profile/views.py index c36457fd8..72469148b 100644 --- a/apps/profile/views.py +++ b/apps/profile/views.py @@ -13,7 +13,7 @@ from utils.user_functions import ajax_login_required from apps.profile.models import Profile, change_password from apps.reader.models import UserSubscription -SINGLE_FIELD_PREFS = ('timezone','feed_pane_size','tutorial_finished','hide_mobile') +SINGLE_FIELD_PREFS = ('timezone','feed_pane_size','tutorial_finished','hide_mobile','send_emails',) SPECIAL_PREFERENCES = ('old_password', 'new_password',) @ajax_login_required diff --git a/apps/reader/forms.py b/apps/reader/forms.py index 8252b9863..df4a743c4 100644 --- a/apps/reader/forms.py +++ b/apps/reader/forms.py @@ -105,6 +105,7 @@ class SignupForm(forms.Form): new_user.save() new_user = authenticate(username=self.cleaned_data['username'], password=self.cleaned_data['password']) + new_user.profile.send_new_user_email() return new_user diff --git a/apps/reader/views.py b/apps/reader/views.py index dea5360f3..689588f53 100644 --- a/apps/reader/views.py +++ b/apps/reader/views.py @@ -133,19 +133,24 @@ def logout(request): return HttpResponseRedirect(reverse('index')) def autologin(request, username, secret): + next = request.GET.get('next', '') + if not username or not secret: return HttpResponseForbidden() profile = Profile.objects.filter(user__username=username, secret_token=secret) - if profile: - user = profile[0].user - user.backend = settings.AUTHENTICATION_BACKENDS[0] - login_user(request, user) - logging.user(user, "~FG~BB~SKAuto-Login~FW") - else: + if not profile: return HttpResponseForbidden() + + user = profile[0].user + user.backend = settings.AUTHENTICATION_BACKENDS[0] + login_user(request, user) + logging.user(user, "~FG~BB~SKAuto-Login. Next stop: %s~FW" % (next if next else 'Homepage',)) - return HttpResponseRedirect(reverse('index') + request.GET.get('next', '')) + if next: + next = '?next=' + next + + return HttpResponseRedirect(reverse('index') + next) @json.json_view def load_feeds(request): diff --git a/fabfile.py b/fabfile.py index 450e2efc5..5c176eddc 100644 --- a/fabfile.py +++ b/fabfile.py @@ -89,6 +89,12 @@ def deploy_full(): run('curl -s http://www.newsblur.com/m/ > /dev/null') compress_media() +@roles('web') +def restart_gunicorn(): + with cd(env.NEWSBLUR_PATH): + with settings(warn_only=True): + run('sudo supervisorctl restart gunicorn') + @roles('web') def staging(): with cd('~/staging'): diff --git a/media/iphone/Classes/FeedDetailViewController.m b/media/iphone/Classes/FeedDetailViewController.m index 24e52acb7..519ecdd49 100644 --- a/media/iphone/Classes/FeedDetailViewController.m +++ b/media/iphone/Classes/FeedDetailViewController.m @@ -181,10 +181,12 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0] - (void)failLoadingFeed:(ASIHTTPRequest *)request { [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; - [appDelegate.navigationController - popToViewController:[appDelegate.navigationController.viewControllers - objectAtIndex:0] - animated:YES]; + if (self.feedPage <= 1) { + [appDelegate.navigationController + popToViewController:[appDelegate.navigationController.viewControllers + objectAtIndex:0] + animated:YES]; + } [NewsBlurAppDelegate informError:[request error]]; } diff --git a/media/iphone/NewsBlur.xcodeproj/project.pbxproj b/media/iphone/NewsBlur.xcodeproj/project.pbxproj index b5e19139b..a847244ce 100755 --- a/media/iphone/NewsBlur.xcodeproj/project.pbxproj +++ b/media/iphone/NewsBlur.xcodeproj/project.pbxproj @@ -538,7 +538,7 @@ ALWAYS_SEARCH_USER_PATHS = YES; CODE_SIGN_ENTITLEMENTS = Entitlements.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Samuel Clay"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -567,7 +567,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; CODE_SIGN_ENTITLEMENTS = Entitlements.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Samuel Clay"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; COPY_PHASE_STRIP = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = NewsBlur_Prefix.pch; diff --git a/media/js/newsblur/modal.js b/media/js/newsblur/modal.js index 8ed129db4..418f4800a 100644 --- a/media/js/newsblur/modal.js +++ b/media/js/newsblur/modal.js @@ -20,7 +20,11 @@ NEWSBLUR.Modal.prototype = { 'onOpen': function (dialog) { dialog.overlay.fadeIn(200, function () { dialog.container.fadeIn(200); - dialog.data.fadeIn(200); + dialog.data.fadeIn(200, function() { + if (self.options.onOpen) { + self.options.onOpen(); + } + }); setTimeout(function() { $(window).resize(); }); @@ -28,6 +32,9 @@ NEWSBLUR.Modal.prototype = { }, 'onShow': function(dialog) { $('#simplemodal-container').corner('6px'); + if (self.options.onShow) { + self.options.onShow(); + } }, 'onClose': function(dialog, callback) { dialog.data.hide().empty().remove(); diff --git a/media/js/newsblur/reader.js b/media/js/newsblur/reader.js index 1ca6951d5..106272fb8 100644 --- a/media/js/newsblur/reader.js +++ b/media/js/newsblur/reader.js @@ -104,6 +104,7 @@ this.load_recommended_feeds(); this.setup_dashboard_graphs(); this.setup_howitworks_hovers(); + this.load_email_optout(); }; NEWSBLUR.Reader.prototype = { @@ -308,6 +309,13 @@ } }, + load_email_optout: function() { + var next = $.getQueryString('next'); + if (next == 'optout') { + this.open_account_modal({'animate_email': true}); + } + }, + animate_progress_bar: function($bar, seconds, percentage) { var self = this; percentage = percentage || 0; @@ -2296,6 +2304,10 @@ mark_feed_as_read: function(feed_id) { feed_id = feed_id || this.active_feed; + if (this.flags['river_view']) { + return; + } + this.mark_feed_as_read_update_counts(feed_id); this.model.mark_feed_as_read([feed_id]); @@ -3682,8 +3694,8 @@ NEWSBLUR.preferences = new NEWSBLUR.ReaderPreferences(); }, - open_account_modal: function() { - NEWSBLUR.account = new NEWSBLUR.ReaderAccount(); + open_account_modal: function(options) { + NEWSBLUR.account = new NEWSBLUR.ReaderAccount(options); }, open_feedchooser_modal: function() { @@ -6162,6 +6174,10 @@ e.preventDefault(); self.show_next_feed(-1); }); + $document.bind('keydown', 'shift+a', function(e) { + e.preventDefault(); + self.mark_feed_as_read(); + }); $document.bind('keydown', 'left', function(e) { e.preventDefault(); self.switch_taskbar_view_direction(-1); diff --git a/media/js/newsblur/reader_account.js b/media/js/newsblur/reader_account.js index 0b2cb2831..77dd4beec 100644 --- a/media/js/newsblur/reader_account.js +++ b/media/js/newsblur/reader_account.js @@ -1,5 +1,10 @@ NEWSBLUR.ReaderAccount = function(options) { - var defaults = {}; + var defaults = { + 'animate_email': false, + 'onOpen': _.bind(function() { + this.animate_email(); + }, this) + }; this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.AssetModel.reader(); @@ -18,6 +23,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { this.$modal.bind('click', $.rescope(this.handle_click, this)); this.handle_change(); + this.select_preferences(); }, make_modal: function() { @@ -89,6 +95,25 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-preference-sublabel' }, 'Download this XML file as a backup') ]) ]), + $.make('div', { className: 'NB-preference NB-preference-emails' }, [ + $.make('div', { className: 'NB-preference-options' }, [ + $.make('div', [ + $.make('input', { id: 'NB-preference-emails-1', type: 'radio', name: 'send_emails', value: 'true' }), + $.make('label', { 'for': 'NB-preference-emails-1' }, [ + 'Mail me the infrequent email' + ]) + ]), + $.make('div', [ + $.make('input', { id: 'NB-preference-emails-2', type: 'radio', name: 'send_emails', value: 'false' }), + $.make('label', { 'for': 'NB-preference-emails-2' }, [ + 'Never ever send me an email' + ]) + ]) + ]), + $.make('div', { className: 'NB-preference-label'}, [ + 'Emails' + ]) + ]), $.make('div', { className: 'NB-modal-submit' }, [ $.make('input', { type: 'submit', disabled: 'true', className: 'NB-modal-submit-green NB-disabled', value: 'Change what you like above...' }), ' or ', @@ -102,6 +127,31 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { ]); }, + animate_email: function() { + if (this.options.animate_email) { + _.delay(_.bind(function() { + var $emails = $('.NB-preference-emails', this.$modal); + var bgcolor = $emails.css('backgroundColor'); + $emails.css('backgroundColor', bgcolor).animate({ + 'backgroundColor': 'orange' + }, { + 'queue': false, + 'duration': 1200, + 'easing': 'easeInQuad', + 'complete': function() { + $emails.animate({ + 'backgroundColor': bgcolor + }, { + 'queue': false, + 'duration': 650, + 'easing': 'easeOutQuad' + }); + } + }); + }, this), 200); + } + }, + close_and_load_preferences: function() { this.close(function() { NEWSBLUR.reader.open_preferences_modal(); @@ -122,11 +172,22 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.modal.close(); }); }, + + select_preferences: function() { + var pref = this.model.preference; + $('input[name=send_emails]', this.$modal).each(function() { + console.log(["pref", pref('send_emails'), $(this).val()]); + if ($(this).val() == ""+pref('send_emails')) { + $(this).attr('checked', true); + return false; + } + }); + }, serialize_preferences: function() { var preferences = {}; - $('input[type=radio]:checked, select, input', this.$modal).each(function() { + $('input[type=radio]:checked, select, input[type=text], input[type=password]', this.$modal).each(function() { var name = $(this).attr('name'); var preference = preferences[name] = $(this).val(); if (preference == 'true') preferences[name] = true; @@ -145,6 +206,8 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $('.NB-preference-error', this.$modal).text(''); $('input[type=submit]', this.$modal).val('Saving...').attr('disabled', true).addClass('NB-disabled'); + console.log(["form['send_emails']", form['send_emails']]); + this.model.preference('send_emails', form['send_emails']); this.model.save_account_settings(form, function(data) { if (data.code == -1) { $('.NB-preference-username .NB-preference-error', this.$modal).text(data.message); diff --git a/templates/base.html b/templates/base.html index 5b546229a..64d4fb0af 100644 --- a/templates/base.html +++ b/templates/base.html @@ -36,6 +36,7 @@ 'hide_story_changes' : 1, 'feed_view_single_story' : 0, 'animations' : true, + 'send_emails' : {{ user_profile.send_emails|yesno:"true,false" }}, 'view_settings' : {}, 'collapsed_folders' : [], 'story_styling' : 'sans-serif', diff --git a/templates/mail/email_base.txt b/templates/mail/email_base.txt index fe491a7b6..16800daa8 100644 --- a/templates/mail/email_base.txt +++ b/templates/mail/email_base.txt @@ -10,12 +10,12 @@ Stay up to date and in touch with me, yr. developer, in a few different ways: * Follow @newsblur on Twitter: http://twitter.com/newsblur * Follow the constantly evolving source code on GitHub: http://github.com/samuelclay -{% block resources_header %}There are a few resources that would make sense to follow if you end up loving NewsBlur:{% endblock resources_header %} +{% block resources_header %}There are a few resources you can use if you end up loving NewsBlur:{% endblock resources_header %} * Read the NewsBlur Blog: http://blog.newsblur.com * Get support on NewsBlur's Get Satisfaction: http://getsatisfaction.com/newsblur - * Download the free NewsBlur iPhone app: http://itunes.com/newsblur + * Download the free NewsBlur iPhone app: [Coming soon -- it's in review on the App Store] ----------------------------------------------------------------------------- -Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur: http://www.newsblur.com{{ user.profile.autologin_url }}?optout=1. \ No newline at end of file +Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur: http://www.newsblur.com{{ user.profile.autologin_url }}?next=optout \ No newline at end of file diff --git a/templates/mail/email_base.xhtml b/templates/mail/email_base.xhtml index 63d90e4c9..68f2917c5 100644 --- a/templates/mail/email_base.xhtml +++ b/templates/mail/email_base.xhtml @@ -31,17 +31,17 @@
Stay up to date and in touch with me, yr. developer, in a few different ways:
{% block resources_header %}There are a few resources that would make sense to follow if you end up loving NewsBlur:{% endblock resources_header %}
+{% block resources_header %}There are a few resources you can use if you end up loving NewsBlur:{% endblock resources_header %}
- Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur. + Don't want to be notified about anything NewsBlur related? Opt-out of emails from NewsBlur.
Welcome to NewsBlur, {{ user.username }}.
OK, firstly, thank you for trying out new software. It's not quite what you're used to, but you might find NewsBlur to be a perfect fit. There's a lot packed into a minimal interface.
Spend a few days trying out NewsBlur. I hope you end up loving it.
+If you really do love using NewsBlur you can purchase a fancy new premium account for only $1/month. You get to support an independent developer, help feed his dog, and contribute to the long-term health of NewsBlur.
+(Plus, among other cool premium-only features, all of your sites will update more often. Going premium supports 100% genuine hard-work and you'll make me a very happy developer.)
{% endblock %} - -{% block resources_header %}There are a few resources that would make sense to follow if you end up loving NewsBlur:{% endblock resources_header %} \ No newline at end of file