diff --git a/apps/reader/urls.py b/apps/reader/urls.py index 71195258d..55a235569 100644 --- a/apps/reader/urls.py +++ b/apps/reader/urls.py @@ -19,6 +19,7 @@ urlpatterns = patterns('', url(r'^feed_unread_count', views.feed_unread_count, name='feed-unread-count'), url(r'^starred_stories', views.load_starred_stories, name='load-starred-stories'), url(r'^starred_story_hashes', views.starred_story_hashes, name='starred-story-hashes'), + url(r'^starred_rss/(?P\d+)/(?P\w+)/(?P[-\w]+)?/?$', views.starred_stories_rss_feed, name='load-starred-rss'), url(r'^unread_story_hashes', views.unread_story_hashes, name='unread-story-hashes'), url(r'^mark_all_as_read', views.mark_all_as_read, name='mark-all-as-read'), url(r'^mark_story_as_read', views.mark_story_as_read, name='mark-story-as-read'), diff --git a/apps/reader/views.py b/apps/reader/views.py index e67d83a7f..ae33514c8 100644 --- a/apps/reader/views.py +++ b/apps/reader/views.py @@ -849,6 +849,67 @@ def starred_story_hashes(request): return dict(starred_story_hashes=story_hashes) +def starred_stories_rss_feed(request, user_id, secret_token, tag_slug): + try: + user = User.objects.get(pk=user_id) + except User.DoesNotExist: + raise Http404 + + username = username and username.lower() + profile = MSocialProfile.get_user(user.pk) + params = {'username': profile.username_slug, 'user_id': user.pk} + if not username or profile.username_slug.lower() != username: + return HttpResponseRedirect(reverse('shared-stories-rss-feed', kwargs=params)) + + social_profile = MSocialProfile.get_user(user_id) + current_site = Site.objects.get_current() + current_site = current_site and current_site.domain + + if social_profile.private: + return HttpResponseForbidden() + + data = {} + data['title'] = social_profile.title + data['link'] = social_profile.blurblog_url + data['description'] = "Stories shared by %s on NewsBlur." % user.username + data['lastBuildDate'] = datetime.datetime.utcnow() + data['generator'] = 'NewsBlur - http://www.newsblur.com' + data['docs'] = None + data['author_name'] = user.username + data['feed_url'] = "http://%s%s" % ( + current_site, + reverse('shared-stories-rss-feed', kwargs=params), + ) + rss = feedgenerator.Atom1Feed(**data) + + shared_stories = MSharedStory.objects.filter(user_id=user.pk).order_by('-shared_date')[:25] + for shared_story in shared_stories: + feed = Feed.get_by_id(shared_story.story_feed_id) + content = render_to_string('social/rss_story.xhtml', { + 'feed': feed, + 'user': user, + 'social_profile': social_profile, + 'shared_story': shared_story, + 'content': (shared_story.story_content_z and + zlib.decompress(shared_story.story_content_z)) + }) + story_data = { + 'title': shared_story.story_title, + 'link': shared_story.story_permalink, + 'description': content, + 'author_name': shared_story.story_author_name, + 'categories': shared_story.story_tags, + 'unique_id': shared_story.story_guid, + 'pubdate': shared_story.shared_date, + } + rss.add_item(**story_data) + + logging.user(request, "~FBGenerating ~SB%s~SN's RSS feed: ~FM%s" % ( + user.username, + request.META.get('HTTP_USER_AGENT', "")[:24] + )) + return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml') + @json.json_view def load_river_stories__redis(request): limit = 12 diff --git a/apps/rss_feeds/models.py b/apps/rss_feeds/models.py index 807ef6731..d3d875561 100644 --- a/apps/rss_feeds/models.py +++ b/apps/rss_feeds/models.py @@ -2062,6 +2062,7 @@ class MStarredStory(mongo.Document): class MStarredStoryCounts(mongo.Document): user_id = mongo.IntField() tag = mongo.StringField(max_length=128, unique_with=['user_id']) + slug = mongo.StringField(max_length=128) count = mongo.IntField() meta = { @@ -2070,15 +2071,25 @@ class MStarredStoryCounts(mongo.Document): 'ordering': ['tag'], 'allow_inheritance': False, } + + @property + def rss_url(self, secret_token=None): + if not secret_token: + user = User.objects.select_related('profile').get(pk=self.user_id) + secret_token = user.profile.secret_token + + return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id, + secret_token, self.slug) @classmethod def user_counts(cls, user_id, include_total=False, try_counting=True): - counts = cls.objects.filter(user_id=user_id).only('tag', 'count') - counts = [{'tag': c.tag, 'count': c.count} for c in counts] + counts = cls.objects.filter(user_id=user_id) + counts = [{'tag': c.tag, 'count': c.count, 'feed_address': c.rss_url} for c in counts] if counts == [] and try_counting: cls.count_tags_for_user(user_id) - return cls.user_counts(user_id, include_total=include_total, try_counting=False) + return cls.user_counts(user_id, include_total=include_total, + try_counting=False) if include_total: for c in counts: @@ -2098,13 +2109,13 @@ class MStarredStoryCounts(mongo.Document): cls.objects(user_id=user_id).delete() for tag, count in dict(user_tags).items(): - cls.objects.create(user_id=user_id, tag=tag, count=count) + cls.objects.create(user_id=user_id, tag=tag, slug=slugify(tag), count=count) total_stories_count = MStarredStory.objects(user_id=user_id).count() cls.objects.create(user_id=user_id, tag="", count=total_stories_count) return dict(total=total_stories_count, tags=user_tags) - + class MFetchHistory(mongo.Document): feed_id = mongo.IntField(unique=True) feed_fetch_history = mongo.DynamicField() diff --git a/media/css/reader.css b/media/css/reader.css index c186255eb..f1b00439d 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -3096,6 +3096,7 @@ body { .NB-sideoption-save-wrapper.NB-active { display: block; + height: auto; } .NB-narrow-content .NB-sideoption-save-wrapper { @@ -7583,11 +7584,11 @@ form.opml_import_form input { } .NB-modal-feed-settings .NB-preference-options div { float: left; - margin: 0 12px; + margin: 0 6px 0 0; } .NB-modal-feed-settings .NB-preference-options input[type=radio] { float: left; - margin: 10px 4px; + margin: 2px 6px 0 0px; } .NB-modal-feed-settings .NB-preference-options label { @@ -7595,6 +7596,9 @@ form.opml_import_form input { margin: 0 0 4px 0; float: left; cursor: pointer; + text-transform: uppercase; + font-size: 12px; + color: #303030; } .NB-modal-feed-settings .NB-preference-options img { height: 31px; @@ -7605,6 +7609,23 @@ form.opml_import_form input { text-transform: uppercase; opacity: 0; } +.NB-modal-feed-settings .NB-view-settings label { + display: block; + padding: 4px 6px; + border: 1px solid rgba(0,0,0,.2); + border-radius: 3px; +} +.NB-modal-feed-settings .NB-view-settings img { + float: left; + width: 18px; + height: 15px; + padding: 1px 0 0 0; + margin: 0 4px 0 0; +} +.NB-modal-feed-settings .NB-view-title { + margin: 0; + padding: 1px 0 0 0; +} /* ===================== */ /* = Feedchooser Modal = */ diff --git a/media/js/newsblur/common/assetmodel.js b/media/js/newsblur/common/assetmodel.js index 52f625b6a..92d9935de 100644 --- a/media/js/newsblur/common/assetmodel.js +++ b/media/js/newsblur/common/assetmodel.js @@ -874,8 +874,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ get_feed: function(feed_id) { var self = this; - if (_.string.include(feed_id, 'social:')) { + if (_.string.startsWith(feed_id, 'social:')) { return this.social_feeds.get(feed_id); + } else if (_.string.startsWith(feed_id, 'starred:')) { + return this.starred_feeds.get(feed_id); } else { return this.feeds.get(feed_id); } @@ -896,6 +898,18 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ return this.feeds; }, + get_social_feeds: function() { + var self = this; + + return this.social_feeds; + }, + + get_starred_feeds: function() { + var self = this; + + return this.starred_feeds; + }, + get_folders: function() { var self = this; diff --git a/media/js/newsblur/common/modal.js b/media/js/newsblur/common/modal.js index 2213d75ea..3cce29884 100644 --- a/media/js/newsblur/common/modal.js +++ b/media/js/newsblur/common/modal.js @@ -69,21 +69,52 @@ NEWSBLUR.Modal.prototype = { $.modal.close(callback); }, - make_feed_chooser: function() { + make_feed_chooser: function(options) { + options = options || {}; var $chooser = $.make('select', { name: 'feed', className: 'NB-modal-feed-chooser' }); + var $feeds_optgroup = $.make('optgroup', { label: "Sites" }); + var $social_feeds_optgroup = $.make('optgroup', { label: "Blurblogs" }); + var $starred_feeds_optgroup = $.make('optgroup', { label: "Saved Tags" }); var current_feed_id = this.feed_id; - this.feeds = this.model.get_feeds(); - this.feeds.each(function(feed) { + var make_feed_option = function(feed) { + if (!feed.get('feed_title')) return; + var $option = $.make('option', { value: feed.id }, feed.get('feed_title')); - $option.appendTo($chooser); + $option.appendTo(feed.is_starred() ? $starred_feeds_optgroup : + feed.is_social() ? $social_feeds_optgroup : + $feeds_optgroup); if (feed.id == current_feed_id) { $option.attr('selected', true); } - }); + }; + + this.feeds = this.model.get_feeds(); + this.feeds.each(make_feed_option); + + if (!options.skip_social) { + this.social_feeds = this.model.get_social_feeds(); + this.social_feeds.each(make_feed_option); + } + + if (!options.skip_starred) { + this.starred_feeds = this.model.get_starred_feeds(); + this.starred_feeds.each(make_feed_option); + } + + $('option', $feeds_optgroup).tsort(); + $('option', $social_feeds_optgroup).tsort(); + $('option', $starred_feeds_optgroup).tsort(); + + $chooser.append($feeds_optgroup); + if (!options.skip_social) { + $chooser.append($social_feeds_optgroup); + } + if (!options.skip_starred) { + $chooser.append($starred_feeds_optgroup); + } - $('option', $chooser).tsort(); return $chooser; }, diff --git a/media/js/newsblur/reader/reader.js b/media/js/newsblur/reader/reader.js index b3526064c..314bdecc2 100644 --- a/media/js/newsblur/reader/reader.js +++ b/media/js/newsblur/reader/reader.js @@ -2973,6 +2973,16 @@ if (feed_id && unread_count == 0) { $('.NB-menu-manage-feed-mark-read', $manage_menu).addClass('NB-disabled'); } + } else if (type == 'starred') { + $manage_menu = $.make('ul', { className: 'NB-menu-manage NB-menu-manage-feed' }, [ + $.make('li', { className: 'NB-menu-separator-inverse' }), + $.make('li', { className: 'NB-menu-item NB-menu-manage-feed-settings' }, [ + $.make('div', { className: 'NB-menu-manage-image' }), + $.make('div', { className: 'NB-menu-manage-title' }, 'Tag settings') + ]) + ]); + $manage_menu.data('feed_id', feed_id); + $manage_menu.data('$feed', $item); } else if (type == 'folder') { $manage_menu = $.make('ul', { className: 'NB-menu-manage NB-menu-manage-folder' }, [ $.make('li', { className: 'NB-menu-separator-inverse' }), @@ -3235,6 +3245,9 @@ } else if (type == 'socialfeed') { feed_id = options.feed_id; inverse = options.inverse || $item.hasClass("NB-hover-inverse"); + } else if (type == 'starred') { + feed_id = options.feed_id; + inverse = options.inverse || $item.hasClass("NB-hover-inverse"); } else if (type == 'story') { story_id = options.story_id; if ($item.hasClass('NB-hover-inverse')) inverse = true; @@ -3281,15 +3294,16 @@ $manage_menu_container.css('z-index', $("#simplemodal-container").css('z-index')); } $('.NB-task-manage').addClass('NB-hover'); - } else if (type == 'feed' || type == 'folder' || type == 'story' || type == 'socialfeed') { + } else if (type == 'feed' || type == 'folder' || type == 'story' || + type == 'socialfeed' || type == 'starred') { var left, top; - NEWSBLUR.log(['menu open', $item, inverse, toplevel, type]); + // NEWSBLUR.log(['menu open', $item, inverse, toplevel, type]); if (inverse) { var $align = $item; if (type == 'feed') { left = toplevel ? 2 : -22; top = toplevel ? 1 : 3; - } else if (type == 'socialfeed') { + } else if (type == 'socialfeed' || type == 'starred') { left = 2; top = 2; } else if (type == 'folder') { @@ -3319,7 +3333,7 @@ left = toplevel ? 0 : -2; top = toplevel ? 20 : 19; $align = $('.NB-feedlist-manage-icon', $item); - } else if (type == 'socialfeed') { + } else if (type == 'socialfeed' || type == 'starred') { left = toplevel ? 0 : -18; top = toplevel ? 20 : 21; $align = $('.NB-feedlist-manage-icon', $item); @@ -3344,7 +3358,8 @@ $manage_menu_container.stop().css({'display': 'block', 'opacity': 1}); // Create and position the arrow tab - if (type == 'feed' || type == 'folder' || type == 'story' || type == 'socialfeed') { + if (type == 'feed' || type == 'folder' || type == 'story' || + type == 'socialfeed' || type == 'starred') { var $arrow = $.make('div', { className: 'NB-menu-manage-arrow' }, [ $.make('div', { className: 'NB-icon' }) ]); @@ -3408,7 +3423,7 @@ // Hide menu on scroll. var $scroll; this.flags['feed_list_showing_manage_menu'] = true; - if (type == 'feed' || type == 'socialfeed') { + if (type == 'feed' || type == 'socialfeed' || type == 'starred') { $scroll = this.$s.$feed_list.parent(); } else if (type == 'story') { $scroll = this.$s.$story_titles.add(this.$s.$feed_scroll); diff --git a/media/js/newsblur/reader/reader_feed_exception.js b/media/js/newsblur/reader/reader_feed_exception.js index bde4c4bfc..4f13a2586 100644 --- a/media/js/newsblur/reader/reader_feed_exception.js +++ b/media/js/newsblur/reader/reader_feed_exception.js @@ -45,7 +45,9 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { return false; } }); - + + $(".NB-exception-option-page", this.$modal).toggle(this.feed.is_feed() || this.feed.is_social()); + $(".NB-view-setting-original", this.$modal).toggle(this.feed.is_feed() || this.feed.is_social()); if (this.feed.get('exception_type')) { this.$modal.removeClass('NB-modal-feed-settings'); } else { @@ -56,10 +58,13 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { }, get_feed_settings: function() { + if (this.feed.is_starred()) return; + var $loading = $('.NB-modal-loading', this.$modal); $loading.addClass('NB-active'); - var settings_fn = this.options.social_feed ? this.model.get_social_settings : this.model.get_feed_settings; + var settings_fn = this.options.social_feed ? this.model.get_social_settings : + this.model.get_feed_settings; settings_fn.call(this.model, this.feed_id, _.bind(this.populate_settings, this)); }, @@ -90,7 +95,7 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { $.make('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(this.feed) }), $.make('div', { className: 'NB-modal-feed-heading' }, [ $.make('span', { className: 'NB-modal-feed-title' }, this.feed.get('feed_title')), - $.make('span', { className: 'NB-modal-feed-subscribers' },Inflector.pluralize(' subscriber', this.feed.get('num_subscribers'), true)) + (this.feed.get('num_subscribers') && $.make('span', { className: 'NB-modal-feed-subscribers' },Inflector.pluralize(' subscriber', this.feed.get('num_subscribers'), true))) ]) ]), $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-view NB-modal-submit NB-settings-only' }, [ @@ -104,23 +109,33 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { $.make('div', { className: 'NB-preference-label'}, [ 'Reading view' ]), - $.make('div', { className: 'NB-preference-options' }, [ - $.make('div', [ - $.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'view_settings', value: 'page' }), + $.make('div', { className: 'NB-preference-options NB-view-settings' }, [ + $.make('div', { className: "NB-view-setting-original" }, [ $.make('label', { 'for': 'NB-preference-view-1' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_original.png' }) + $.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'view_settings', value: 'page' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_original_active.png' }), + $.make("div", { className: "NB-view-title" }, "Original") ]) ]), $.make('div', [ - $.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'view_settings', value: 'feed' }), $.make('label', { 'for': 'NB-preference-view-2' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_feed.png' }) + $.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'view_settings', value: 'feed' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_feed_active.png' }), + $.make("div", { className: "NB-view-title" }, "Feed") ]) ]), $.make('div', [ - $.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'view_settings', value: 'story' }), $.make('label', { 'for': 'NB-preference-view-3' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/preferences_view_story.png' }) + $.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'view_settings', value: 'text' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_text_active.png' }), + $.make("div", { className: "NB-view-title" }, "Text") + ]) + ]), + $.make('div', [ + $.make('label', { 'for': 'NB-preference-view-4' }, [ + $.make('input', { id: 'NB-preference-view-4', type: 'radio', name: 'view_settings', value: 'story' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_story_active.png' }), + $.make("div", { className: "NB-view-title" }, "Story") ]) ]) ]) @@ -156,14 +171,14 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { ]), $.make('input', { type: 'text', id: 'NB-exception-input-address', className: 'NB-exception-input-address NB-input', name: 'feed_address', value: this.feed.get('feed_address') }) ]), - (!this.options.social_feed && $.make('div', { className: 'NB-exception-submit-wrapper' }, [ + (this.feed.is_feed() && $.make('div', { className: 'NB-exception-submit-wrapper' }, [ $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-address' }, 'Parse this RSS/XML Feed'), $.make('div', { className: 'NB-error' }), $.make('div', { className: 'NB-exception-feed-history' }) ])) ]) ]), - $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-page NB-modal-submit' }, [ + ($.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-page NB-modal-submit' }, [ $.make('h5', [ $.make('div', { className: 'NB-exception-option-meta' }), $.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 3:'), @@ -178,13 +193,13 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { ]), $.make('input', { type: 'text', id: 'NB-exception-input-link', className: 'NB-exception-input-link NB-input', name: 'feed_link', value: this.feed.get('feed_link') }) ]), - (!this.options.social_feed && $.make('div', { className: 'NB-exception-submit-wrapper' }, [ + (this.feed.is_feed() && $.make('div', { className: 'NB-exception-submit-wrapper' }, [ $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-link' }, 'Fetch Feed From Website'), $.make('div', { className: 'NB-error' }), $.make('div', { className: 'NB-exception-page-history' }) ])) ]) - ]), + ])), $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-delete NB-exception-block-only NB-modal-submit' }, [ $.make('h5', [ $.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 4:'), diff --git a/media/js/newsblur/views/feed_list_view.js b/media/js/newsblur/views/feed_list_view.js index 3a8326f45..a7e8ead1e 100644 --- a/media/js/newsblur/views/feed_list_view.js +++ b/media/js/newsblur/views/feed_list_view.js @@ -169,8 +169,7 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ model: feed, type: 'feed', depth: 0, - starred_tag: true, - disable_hover: true + starred_tag: true }).render(); feed.views.push(feed_view); return feed_view.el; diff --git a/media/js/newsblur/views/feed_title_view.js b/media/js/newsblur/views/feed_title_view.js index f4f40f164..a6352a5f0 100644 --- a/media/js/newsblur/views/feed_title_view.js +++ b/media/js/newsblur/views/feed_title_view.js @@ -360,11 +360,14 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ show_manage_menu: function(e) { if (this.options.feed_chooser) return; - + + var feed_type = this.model.is_social() ? 'socialfeed' : + this.model.is_starred() ? 'starred' : + 'feed'; e.preventDefault(); e.stopPropagation(); - NEWSBLUR.log(["showing manage menu", this.model.is_social() ? 'socialfeed' : 'feed', $(this.el), this, e.which, e.button]); - NEWSBLUR.reader.show_manage_menu(this.model.is_social() ? 'socialfeed' : 'feed', this.$el, { + + NEWSBLUR.reader.show_manage_menu(feed_type, this.$el, { feed_id: this.model.id, toplevel: this.options.depth == 0, rightclick: e.which >= 2 diff --git a/media/js/newsblur/views/story_detail_view.js b/media/js/newsblur/views/story_detail_view.js index a83e9ceef..25a649228 100644 --- a/media/js/newsblur/views/story_detail_view.js +++ b/media/js/newsblur/views/story_detail_view.js @@ -103,7 +103,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ resize_starred_tags: function() { if (this.model.get('starred')) { - this.save_view.reset_height(); + this.save_view.reset_height({immediate: true}); } }, diff --git a/media/js/newsblur/views/story_save_view.js b/media/js/newsblur/views/story_save_view.js index 5db5df7bb..f2feff5df 100644 --- a/media/js/newsblur/views/story_save_view.js +++ b/media/js/newsblur/views/story_save_view.js @@ -22,7 +22,7 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ }, template: _.template('\ -
\ +
NB-active<% } %>">\
\ <% if (story_tags.length) { %>\
\