diff --git a/apps/recommendations/migrations/0003_decline_and_twitter.py b/apps/recommendations/migrations/0003_decline_and_twitter.py new file mode 100644 index 000000000..47ca58255 --- /dev/null +++ b/apps/recommendations/migrations/0003_decline_and_twitter.py @@ -0,0 +1,112 @@ +# 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 'RecommendedFeed.declined_date' + db.add_column('recommendations_recommendedfeed', 'declined_date', self.gf('django.db.models.fields.DateField')(null=True), keep_default=False) + + # Adding field 'RecommendedFeed.twitter' + db.add_column('recommendations_recommendedfeed', 'twitter', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'RecommendedFeed.declined_date' + db.delete_column('recommendations_recommendedfeed', 'declined_date') + + # Deleting field 'RecommendedFeed.twitter' + db.delete_column('recommendations_recommendedfeed', 'twitter') + + + 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'}) + }, + 'recommendations.recommendedfeed': { + 'Meta': {'ordering': "['-approved_date']", 'object_name': 'RecommendedFeed'}, + 'approved_date': ('django.db.models.fields.DateField', [], {'null': 'True'}), + 'created_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'declined_date': ('django.db.models.fields.DateField', [], {'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'feed': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recommendations'", 'to': "orm['rss_feeds.Feed']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'twitter': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recommendations'", 'to': "orm['auth.User']"}) + }, + 'recommendations.recommendedfeeduserfeedback': { + 'Meta': {'object_name': 'RecommendedFeedUserFeedback'}, + 'created_date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'feedback'", 'to': "orm['recommendations.RecommendedFeed']"}), + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'feed_feedback'", 'to': "orm['auth.User']"}) + }, + 'rss_feeds.feed': { + 'Meta': {'ordering': "['feed_title']", 'object_name': 'Feed', 'db_table': "'feeds'"}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'active_subscribers': ('django.db.models.fields.IntegerField', [], {'default': '-1', 'db_index': 'True'}), + 'average_stories_per_month': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'creation': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'days_to_trim': ('django.db.models.fields.IntegerField', [], {'default': '90'}), + 'etag': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'exception_code': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'feed_address': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '255'}), + 'feed_link': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '1000', 'null': 'True', 'blank': 'True'}), + 'feed_title': ('django.db.models.fields.CharField', [], {'default': "'[Untitled]'", 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'fetched_once': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_feed_exception': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'has_page_exception': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_load_time': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'min_to_decay': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'next_scheduled_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'num_subscribers': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), + 'premium_subscribers': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), + 'queued_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'stories_last_month': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + } + } + + complete_apps = ['recommendations'] diff --git a/apps/recommendations/models.py b/apps/recommendations/models.py index 3bbc757bb..270ab51cc 100644 --- a/apps/recommendations/models.py +++ b/apps/recommendations/models.py @@ -9,6 +9,8 @@ class RecommendedFeed(models.Model): is_public = models.BooleanField(default=False) created_date = models.DateField(auto_now_add=True) approved_date = models.DateField(null=True) + declined_date = models.DateField(null=True) + twitter = models.CharField(max_length=50, null=True, blank=True) def __unicode__(self): return "%s (%s)" % (self.feed, self.approved_date or self.created_date) diff --git a/apps/recommendations/urls.py b/apps/recommendations/urls.py index 753fad988..ab3b1d0b2 100644 --- a/apps/recommendations/urls.py +++ b/apps/recommendations/urls.py @@ -3,4 +3,6 @@ from apps.recommendations import views urlpatterns = patterns('', url(r'^load_recommended_feed', views.load_recommended_feed, name='load-recommended-feed'), + url(r'^save_recommended_feed', views.save_recommended_feed, name='save-recommended-feed'), + url(r'^load_feed_info', views.load_feed_info, name='load-recommended-feed-info'), ) diff --git a/apps/recommendations/views.py b/apps/recommendations/views.py index e23411564..32f25a487 100644 --- a/apps/recommendations/views.py +++ b/apps/recommendations/views.py @@ -1,11 +1,12 @@ from utils import log as logging from django.http import HttpResponse from django.template import RequestContext -from django.shortcuts import render_to_response +from django.shortcuts import render_to_response, get_object_or_404 from apps.recommendations.models import RecommendedFeed from apps.reader.models import UserSubscription -# from utils import json_functions as json -from utils.user_functions import get_user +from apps.rss_feeds.models import Feed +from utils import json_functions as json +from utils.user_functions import get_user, ajax_login_required def load_recommended_feed(request): @@ -30,4 +31,38 @@ def load_recommended_feed(request): 'has_previous_page' : page != 0, }, context_instance=RequestContext(request)) else: - return HttpResponse("") \ No newline at end of file + return HttpResponse("") + +@json.json_view +def load_feed_info(request): + feed_id = request.GET['feed_id'] + feed = get_object_or_404(Feed, pk=feed_id) + previous_recommendation = None + recommended_feed = RecommendedFeed.objects.filter(user=request.user, feed=feed) + if recommended_feed: + previous_recommendation = recommended_feed[0].created_date + + return { + 'tagline': feed.data.feed_tagline, + 'previous_recommendation': previous_recommendation + } + +@ajax_login_required +@json.json_view +def save_recommended_feed(request): + feed_id = request.POST['feed_id'] + feed = get_object_or_404(Feed, pk=int(feed_id)) + tagline = request.POST['tagline'] + twitter = request.POST.get('twitter') + code = 1 + + recommended_feed, created = RecommendedFeed.objects.get_or_create( + feed=feed, + user=request.user, + defaults=dict( + description=tagline, + twitter=twitter + ) + ) + + return dict(code=code if created else -1) \ No newline at end of file diff --git a/apps/rss_feeds/urls.py b/apps/rss_feeds/urls.py index 08b7dde21..d2ff52c31 100644 --- a/apps/rss_feeds/urls.py +++ b/apps/rss_feeds/urls.py @@ -3,7 +3,7 @@ from apps.rss_feeds import views urlpatterns = patterns('', url(r'^feed_autocomplete', views.feed_autocomplete, name='feed-autocomplete'), - url(r'^statistics', views.load_feed_statistics, name='statistics'), + url(r'^statistics', views.load_feed_statistics, name='feed-statistics'), url(r'^exception_retry', views.exception_retry, name='exception-retry'), url(r'^exception_change_feed_address', views.exception_change_feed_address, name='exception-change-feed-address'), url(r'^exception_change_feed_link', views.exception_change_feed_link, name='exception-change-feed-link'), diff --git a/apps/rss_feeds/views.py b/apps/rss_feeds/views.py index 71fda63ba..bbcab21b1 100644 --- a/apps/rss_feeds/views.py +++ b/apps/rss_feeds/views.py @@ -88,7 +88,7 @@ def load_feed_statistics(request): logging.user(request.user, "~FBStatistics: ~SB%s ~FG(%s/%s/%s subs)" % (feed, feed.num_subscribers, feed.active_subscribers, feed.premium_subscribers,)) return stats - + @ajax_login_required @json.json_view def exception_retry(request): diff --git a/media/css/reader.css b/media/css/reader.css index 3330b8ee7..e1332ebca 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -52,6 +52,15 @@ body.NB-theme-serif #story_pane .NB-feed-story-content { line-height: 20px; } +.NB-input { + font-size: 14px; + padding: 2px; + margin: 2px 4px 2px; + border: 1px solid #606060; + -moz-box-shadow:2px 2px 0 #D0D0D0; + -webkit-box-shadow:2px 2px 0 #D0D0D0; + box-shadow:2px 2px 0 #D0D0D0; +} /* =================== */ /* = Resize Controls = */ /* =================== */ @@ -2844,13 +2853,6 @@ a.NB-splash-link:hover { .NB-add input[type=text] { width: 340px; - font-size: 14px; - padding: 2px; - margin: 2px 4px 2px; - border: 1px solid #606060; - -moz-box-shadow:2px 2px 0 #D0D0D0; - -webkit-box-shadow:2px 2px 0 #D0D0D0; - box-shadow:2px 2px 0 #D0D0D0; } .NB-add .NB-folder-icon { @@ -4166,6 +4168,52 @@ background: transparent; display: none; } +/* ============================= */ +/* = Feed Recommendation Modal = */ +/* ============================= */ + +.NB-modal.NB-modal-recommend .NB-modal-feed-chooser-container { + margin: 3px 0 12px; +} +.NB-modal.NB-modal-recommend .NB-modal-feed-chooser-container .NB-modal-feed-chooser { + margin: 0 0 12px; +} +.NB-modal-recommend .NB-modal-loading { + margin: 6px 8px 0; +} +.NB-modal.NB-modal-recommend .NB-modal-recommend-tagline-container { + padding: 6px 0; + margin: 4px 0; + border-top: 1px solid #C0C0C0; + border-bottom: 1px solid #C0C0C0; +} +.NB-modal-recommend .NB-modal-recommend-tagline { + width: 558px; + height: 80px; + font-size: 14px; + color: #404040; + line-height: 20px; + padding: 8px; + margin: 0; + border: 1px solid #E0E0E0; + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; +} +.NB-modal-recommend .NB-modal-recommend-credit { + color: #505050; + font-size: 12px; + margin: 4px 0; +} +.NB-modal-recommend .NB-modal-recommend-twitter { + +} +.NB-modal-recommend .NB-modal-recommend-explanation { + clear: both; + color: #505050; + font-size: 12px; + margin: 12px 0 14px; + padding: 0 1px; +} + /* ========================= */ /* = Feed Exceptions Modal = */ diff --git a/media/js/newsblur/assetmodel.js b/media/js/newsblur/assetmodel.js index 318a9d994..9306b9754 100644 --- a/media/js/newsblur/assetmodel.js +++ b/media/js/newsblur/assetmodel.js @@ -710,10 +710,26 @@ NEWSBLUR.AssetModel.Reader.prototype = { }); }, + get_feed_recommendation_info: function(feed_id, callback) { + this.make_request('/recommendations/load_feed_info', { + 'feed_id': feed_id + }, callback, callback, { + 'ajax_group': 'statistics' + }); + }, + start_import_from_google_reader: function(callback) { this.make_request('/import/import_from_google_reader/', {}, callback); }, - + + save_recommended_site: function(data, callback) { + if (NEWSBLUR.Globals.is_authenticated) { + this.make_request('/recommendations/save_recommended_feed', data, callback); + } else { + if ($.isFunction(callback)) callback(); + } + }, + save_exception_retry: function(feed_id, callback) { var self = this; diff --git a/media/js/newsblur/reader.js b/media/js/newsblur/reader.js index f46623ae1..1ffcb0f08 100644 --- a/media/js/newsblur/reader.js +++ b/media/js/newsblur/reader.js @@ -2966,7 +2966,7 @@ }, open_recommend_modal: function(feed_id) { - NEWSBLUR.recommend_feed = new NEWSBLUR.ReaderRecommendFeed({'feed_id': feed_id}); + NEWSBLUR.recommend_feed = new NEWSBLUR.ReaderRecommendFeed(feed_id); }, // ========================== diff --git a/media/js/newsblur/reader_add_feed.js b/media/js/newsblur/reader_add_feed.js index 39415e5e3..317e25c4e 100644 --- a/media/js/newsblur/reader_add_feed.js +++ b/media/js/newsblur/reader_add_feed.js @@ -34,7 +34,7 @@ NEWSBLUR.ReaderAddFeed.prototype = { $.make('div', [ $.make('div', { className: 'NB-loading' }), $.make('label', { 'for': 'NB-add-url' }, 'RSS or URL: '), - $.make('input', { type: 'text', id: 'NB-add-url', className: 'NB-add-url', name: 'url', value: self.options.url }), + $.make('input', { type: 'text', id: 'NB-add-url', className: 'NB-input NB-add-url', name: 'url', value: self.options.url }), $.make('input', { type: 'submit', value: 'Add it', className: 'NB-modal-submit-green NB-add-url-submit' }), $.make('div', { className: 'NB-error' }) ]) @@ -51,7 +51,7 @@ NEWSBLUR.ReaderAddFeed.prototype = { $.make('label', { 'for': 'NB-add-folder' }, [ $.make('div', { className: 'NB-folder-icon' }) ]), - $.make('input', { type: 'text', id: 'NB-add-folder', className: 'NB-add-folder', name: 'url' }), + $.make('input', { type: 'text', id: 'NB-add-folder', className: 'NB-input NB-add-folder', name: 'url' }), $.make('input', { type: 'submit', value: 'Add folder', className: 'NB-add-folder-submit NB-modal-submit-green' }), $.make('div', { className: 'NB-error' }) ]) diff --git a/media/js/newsblur/reader_recommend_feed.js b/media/js/newsblur/reader_recommend_feed.js index 13d975702..3adb8d189 100644 --- a/media/js/newsblur/reader_recommend_feed.js +++ b/media/js/newsblur/reader_recommend_feed.js @@ -1,8 +1,12 @@ -NEWSBLUR.ReaderRecommendFeed = function(options) { +NEWSBLUR.ReaderRecommendFeed = function(feed_id, options) { var defaults = {}; this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.AssetModel.reader(); + this.feed_id = feed_id; + this.feed = this.model.get_feed(feed_id); + this.feeds = this.model.get_feeds(); + this.first_load = true; this.runner(); }; @@ -11,15 +15,37 @@ NEWSBLUR.ReaderRecommendFeed.prototype = { runner: function() { this.make_modal(); this.open_modal(); + _.delay(_.bind(function() { + this.get_tagline(); + }, this), 50); this.$modal.bind('click', $.rescope(this.handle_click, this)); + this.$modal.bind('change', $.rescope(this.handle_change, this)); }, make_modal: function() { var self = this; this.$modal = $.make('div', { className: 'NB-modal-recommend NB-modal' }, [ + $.make('div', { className: 'NB-modal-feed-chooser-container'}, [ + this.make_feed_chooser() + ]), + $.make('div', { className: 'NB-modal-loading' }), $.make('h2', { className: 'NB-modal-title' }, 'Recommend this Site'), + $.make('h2', { className: 'NB-modal-subtitle' }, [ + $.make('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(this.feed.favicon) }), + $.make('div', { className: 'NB-modal-feed-title' }, this.feed.feed_title) + ]), + $.make('div', { className: 'NB-modal-recommend-explanation' }, [ + "Spruce up the site's tagline. If chosen, this site will enjoy a week on the NewsBlur dashboard." + ]), + $.make('div', { className: 'NB-modal-recommend-tagline-container' }, [ + $.make('textarea', { className: 'NB-modal-recommend-tagline' }) + ]), + $.make('div', { className: 'NB-modal-recommend-credit' }, [ + '» Want credit? Enter your Twitter username: ', + $.make('input', { className: 'NB-input NB-modal-recommend-twitter' }) + ]), $.make('form', { className: 'NB-recommend-form' }, [ $.make('div', { className: 'NB-modal-submit' }, [ $.make('input', { type: 'submit', className: 'NB-modal-submit-save NB-modal-submit-green', value: 'Recommend Site' }), @@ -30,6 +56,52 @@ NEWSBLUR.ReaderRecommendFeed.prototype = { ]); }, + make_feed_chooser: function() { + var $chooser = $.make('select', { name: 'feed', className: 'NB-modal-feed-chooser' }); + + for (var f in this.feeds) { + var feed = this.feeds[f]; + var $option = $.make('option', { value: feed.id }, feed.feed_title); + $option.appendTo($chooser); + + if (feed.id == this.feed_id) { + $option.attr('selected', true); + } + } + + $('option', $chooser).tsort(); + return $chooser; + }, + + get_tagline: function() { + var $loading = $('.NB-modal-loading', this.$modal); + $loading.addClass('NB-active'); + + this.model.get_feed_recommendation_info(this.feed_id, _.bind(this.populate_tagline, this)); + }, + + populate_tagline: function(data) { + var $submit = $('.NB-modal-submit-save', this.$modal); + var $loading = $('.NB-modal-loading', this.$modal); + $loading.removeClass('NB-active'); + + $('.NB-modal-recommend-tagline', this.$modal).val(data.tagline); + + if (data.previous_recommendation) { + $submit.addClass('NB-disabled').val('Previously Recommended on: ' + data.previous_recommendation); + } else { + $submit.removeClass('NB-disabled').val('Recommend Site'); + } + }, + + initialize_feed: function(feed_id) { + this.feed_id = feed_id; + this.feed = this.model.get_feed(feed_id); + + $('.NB-modal-subtitle .NB-modal-feed-image', this.$modal).attr('src', $.favicon(this.feed.favicon)); + $('.NB-modal-subtitle .NB-modal-feed-title', this.$modal).html(this.feed['feed_title']); + }, + open_modal: function() { var self = this; @@ -60,7 +132,20 @@ NEWSBLUR.ReaderRecommendFeed.prototype = { } }); }, - + + save : function() { + var $submit = $('.NB-modal-submit-save', this.$modal); + $submit.addClass('NB-disabled').val('Saving...'); + + this.model.save_recommended_site({ + feed_id : this.feed_id, + tagline : $('.NB-modal-recommend-tagline').val(), + twitter : $('.NB-modal-recommend-twitter').val() + }, function() { + $.modal.close(); + }); + }, + // =========== // = Actions = // =========== @@ -72,6 +157,24 @@ NEWSBLUR.ReaderRecommendFeed.prototype = { e.preventDefault(); $.modal.close(); }); + + $.targetIs(e, { tagSelector: '.NB-modal-submit-save' }, function($t, $p) { + e.preventDefault(); + if (!$t.hasClass('NB-disabled')) { + self.save(); + } + }); + }, + + handle_change: function(elem, e) { + var self = this; + + $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function($t, $p){ + var feed_id = $t.val(); + self.first_load = false; + self.initialize_feed(feed_id); + self.get_tagline(); + }); } }; \ No newline at end of file