Adding oldest/newer order to feeds and river. Also adding unread/all read filter to both as well. Including stubbed segmented control for switching between modes on a per-feed basis. Still needs global preference and glue to hook them up.

This commit is contained in:
Samuel Clay 2012-07-18 18:34:19 -07:00
parent be3c34aa87
commit a4414eebde
7 changed files with 151 additions and 60 deletions

View file

@ -84,9 +84,10 @@ class UserSubscription(models.Model):
break
else:
self.delete()
def unread_stories(self, withscores=False):
def get_stories(self, offset=0, limit=6, order='newest', read_filter='all', withscores=False):
r = redis.Redis(connection_pool=settings.REDIS_STORY_POOL)
ignore_user_stories = False
stories_key = 'F:%s' % (self.feed_id)
read_stories_key = 'RS:%s:%s' % (self.user_id, self.feed_id)
@ -94,48 +95,64 @@ class UserSubscription(models.Model):
if not r.exists(stories_key):
return []
elif not r.exists(read_stories_key):
elif read_filter != 'unread' or not r.exists(read_stories_key):
ignore_user_stories = True
unread_stories_key = stories_key
else:
r.sdiffstore(unread_stories_key, stories_key, read_stories_key)
sorted_stories_key = 'zF:%s' % (self.feed_id)
unread_ranked_stories_key = 'zU:%s:%s' % (self.user_id, self.feed_id)
r.zinterstore(unread_ranked_stories_key, [sorted_stories_key, unread_stories_key])
current_time = int(time.time())
mark_read_time = time.mktime(self.mark_read_date.timetuple())
story_guids = r.zrevrangebyscore(unread_ranked_stories_key, current_time,
mark_read_time, withscores=withscores)
if order == 'oldest':
byscorefunc = r.zrangebyscore
min_score = mark_read_time
max_score = current_time
else:
byscorefunc = r.zrevrangebyscore
min_score = current_time
max_score = mark_read_time
story_ids = byscorefunc(unread_ranked_stories_key, min_score,
max_score, start=offset, num=limit,
withscores=withscores)
r.delete(unread_ranked_stories_key)
if r.exists(read_stories_key):
r.expire(unread_ranked_stories_key, 24*60*60)
if not ignore_user_stories:
r.delete(unread_stories_key)
return story_guids
return story_ids
@classmethod
def unread_feed_stories(cls, user_id, feed_ids, offset=0, limit=6):
def feed_stories(cls, user_id, feed_ids, offset=0, limit=6, order='newest', read_filter='all'):
r = redis.Redis(connection_pool=settings.REDIS_STORY_POOL)
if order == 'oldest':
range_func = r.zrange
else:
range_func = r.zrevrange
if not isinstance(feed_ids, list):
feed_ids = [feed_ids]
unread_ranked_stories_keys = 'zU:%s' % (user_id)
if offset and r.exists(unread_ranked_stories_keys):
story_guids = r.zrevrange(unread_ranked_stories_keys, offset, limit)
story_guids = range_func(unread_ranked_stories_keys, offset, limit)
return story_guids
else:
r.delete(unread_ranked_stories_keys)
for feed_id in feed_ids:
us = cls.objects.get(user=user_id, feed=feed_id)
story_guids = us.unread_stories(withscores=True)
story_guids = us.get_stories(offset=offset, limit=limit,
order=order, read_filter=read_filter,
withscores=True)
if story_guids:
r.zadd(unread_ranked_stories_keys, **dict(story_guids))
story_guids = r.zrevrange(unread_ranked_stories_keys, offset, limit)
story_guids = range_func(unread_ranked_stories_keys, offset, limit)
r.expire(unread_ranked_stories_keys, 24*60*60)
return story_guids
@ -257,8 +274,10 @@ class UserSubscription(models.Model):
self.unread_count_updated = now
self.oldest_unread_story_date = now
self.needs_unread_recalc = False
MUserStory.delete_old_stories(self.user_id, self.feed_id)
# No longer removing old user read stories, since they're needed for social,
# and they get cleaned up automatically when new stories come in.
# MUserStory.delete_old_stories(self.user_id, self.feed_id)
self.save()

View file

@ -394,6 +394,8 @@ def load_single_feed(request, feed_id):
offset = int(request.REQUEST.get('offset', 0))
limit = int(request.REQUEST.get('limit', 6))
page = int(request.REQUEST.get('page', 1))
order = request.REQUEST.get('order', 'newest')
read_filter = request.REQUEST.get('read_filter', 'all')
dupe_feed_id = None
userstories_db = None
user_profiles = {}
@ -401,19 +403,21 @@ def load_single_feed(request, feed_id):
if page: offset = limit * (page-1)
if not feed_id: raise Http404
try:
feed = Feed.objects.get(id=feed_id)
except Feed.DoesNotExist:
feed_address = request.REQUEST.get('feed_address')
dupe_feed = DuplicateFeed.objects.filter(duplicate_address=feed_address)
if dupe_feed:
feed = dupe_feed[0].feed
dupe_feed_id = feed_id
else:
raise Http404
stories = feed.get_stories(offset, limit)
feed_address = request.REQUEST.get('feed_address')
feed = Feed.get_by_id(feed_id, feed_address=feed_address)
if not feed:
raise Http404
usersub = UserSubscription.objects.get(user=user, feed=feed)
if read_filter == 'unread' or order == 'oldest':
story_ids = usersub.get_stories(order=order, read_filter=read_filter, offset=offset, limit=limit)
story_date_order = "%sstory_date" % ('' if order == 'oldest' else '-')
mstories = MStory.objects(id__in=story_ids).order_by(story_date_order)
stories = Feed.format_stories(mstories)
else:
stories = feed.get_stories(offset, limit)
try:
stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, user.pk)
except redis.ConnectionError:
@ -433,7 +437,6 @@ def load_single_feed(request, feed_id):
checkpoint1 = time.time()
usersub = UserSubscription.objects.get(user=user, feed=feed)
userstories = []
if usersub and stories:
story_ids = [story['id'] for story in stories]
@ -574,6 +577,8 @@ def load_river_stories(request):
feed_ids = [int(feed_id) for feed_id in request.REQUEST.getlist('feeds') if feed_id]
original_feed_ids = list(feed_ids)
page = int(request.REQUEST.get('page', 1))
order = request.REQUEST.get('order', 'newest')
read_filter = request.REQUEST.get('read_filter', 'unread')
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
if not feed_ids:
@ -583,8 +588,10 @@ def load_river_stories(request):
offset = (page-1) * limit
limit = page * limit - 1
story_ids = UserSubscription.unread_feed_stories(user.pk, feed_ids, offset=offset, limit=limit)
mstories = MStory.objects(id__in=story_ids)
story_ids = UserSubscription.feed_stories(user.pk, feed_ids, offset=offset, limit=limit,
order=order, read_filter=read_filter)
story_date_order = "%sstory_date" % ('' if order == 'oldest' else '-')
mstories = MStory.objects(id__in=story_ids).order_by(story_date_order)
stories = Feed.format_stories(mstories)
found_feed_ids = list(set([story['story_feed_id'] for story in stories]))

View file

@ -704,18 +704,19 @@ class Feed(models.Model):
return feed
@classmethod
def get_by_id(cls, feed_id):
feed = None
def get_by_id(cls, feed_id, feed_address=None):
try:
feed = Feed.objects.get(pk=feed_id)
return feed
except Feed.DoesNotExist:
# Feed has been merged after updating. Find the right feed.
duplicate_feeds = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id)
if duplicate_feeds:
feed = duplicate_feeds[0].feed
return feed
return duplicate_feeds[0].feed
if feed_address:
duplicate_feeds = DuplicateFeed.objects.filter(duplicate_address=feed_address)
if duplicate_feeds:
return duplicate_feeds[0].feed
def add_update_stories(self, stories, existing_stories, verbose=False):
ret_values = {

View file

@ -995,11 +995,17 @@ background: transparent;
margin-left: 24px;
}
#story_titles .NB-feedbar .NB-feedbar-settings {
background: transparent url("/media/embed/icons/silk/cog.png") no-repeat center center;
width: 16px;
height: 16px;
cursor: pointer;
padding: 0 24px 0 12px;
}
#story_titles .NB-feedbar .NB-feedbar-train-feed {
background: transparent url("/media/embed/icons/silk/bricks.png") no-repeat center center;
width: 16px;
height: 16px;
/* display: none;*/
cursor: pointer;
padding: 0 0 0 38px;
}
@ -1007,7 +1013,6 @@ background: transparent;
background: transparent url('/media/embed/icons/silk/chart_curve.png') no-repeat center center;
width: 16px;
height: 16px;
/* display: none;*/
cursor: pointer;
margin:3px 0 0 -6px;
padding:0 24px 0 6px;
@ -5307,6 +5312,37 @@ form.opml_import_form input {
display: none;
}
.NB-menu-manage .NB-menu-manage-controls {
overflow: hidden;
background: none;
padding: 2px 0 4px;
}
.NB-menu-manage li.NB-menu-manage-controls:hover {
overflow: hidden;
background: none;
cursor: default;
}
.NB-menu-manage .NB-menu-manage-controls .segmented-control {
display: inline-block;
margin: 4px 0 0 36px;
height: 14px;
width: auto;
color: white;
overflow: hidden;
text-align: center;
}
.NB-menu-manage .NB-menu-manage-controls .segmented-control:last-child {
}
.NB-menu-manage .NB-menu-manage-controls .segmented-control li {
clear: none;
padding: 1px 12px 0;
font-size: 10px;
color: #E0E0E0;
}
.NB-menu-manage .NB-menu-manage-controls .segmented-control li.NB-active {
color: white;
}
/* ==================== */
/* = Statistics Modal = */
/* ==================== */

View file

@ -56,14 +56,20 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
NEWSBLUR.assets.rename_feed(this.id, new_title);
},
get_view: function($feed) {
return _.detect(this.views, function(view) {
get_view: function($feed, fallback) {
var found_view = _.detect(this.views, function(view) {
if ($feed) {
return view.el == $feed.get(0);
} else {
return true;
}
});
if (!found_view && fallback && this.views.length) {
found_view = this.views[0];
}
return found_view;
},
is_social: function() {

View file

@ -2308,9 +2308,16 @@
$.make('div', { className: 'NB-menu-manage-image' }),
$.make('div', { className: 'NB-menu-manage-title' }, 'Statistics')
]),
$.make('li', { className: 'NB-menu-manage-feed NB-menu-manage-feed-settings' }, [
$.make('div', { className: 'NB-menu-manage-image' }),
$.make('div', { className: 'NB-menu-manage-title' }, 'Site settings')
$.make('li', { className: 'NB-menu-separator' }),
$.make('li', { className: 'NB-menu-manage-feed NB-menu-manage-controls' }, [
$.make('ul', { className: 'segmented-control NB-menu-manage-feed-order' }, [
$.make('li', { className: 'NB-feed-order-oldest' }, 'Oldest'),
$.make('li', { className: 'NB-feed-order-newest NB-active' }, 'Newest first')
]),
$.make('ul', { className: 'segmented-control NB-menu-manage-feed-unreads' }, [
$.make('li', { className: 'NB-feed-unreads-all NB-active' }, 'All stories'),
$.make('li', { className: 'NB-feed-unreads-only' }, 'Unread only')
])
]),
$.make('li', { className: 'NB-menu-separator' }),
$.make('li', { className: 'NB-menu-manage-feed NB-menu-manage-feed-train' }, [
@ -2319,11 +2326,15 @@
$.make('div', { className: 'NB-menu-manage-subtitle' }, 'What you like and dislike.')
]),
$.make('li', { className: 'NB-menu-separator' }),
$.make('li', { className: 'NB-menu-manage-feed NB-menu-manage-feed-recommend' }, [
// $.make('li', { className: 'NB-menu-manage-feed NB-menu-manage-feed-recommend' }, [
// $.make('div', { className: 'NB-menu-manage-image' }),
// $.make('div', { className: 'NB-menu-manage-title' }, 'Recommend this site')
// ]),
// $.make('li', { className: 'NB-menu-separator' }),
$.make('li', { className: 'NB-menu-manage-feed NB-menu-manage-feed-settings' }, [
$.make('div', { className: 'NB-menu-manage-image' }),
$.make('div', { className: 'NB-menu-manage-title' }, 'Recommend this site')
$.make('div', { className: 'NB-menu-manage-title' }, 'Site settings')
]),
$.make('li', { className: 'NB-menu-separator' }),
$.make('li', { className: 'NB-menu-manage-feed NB-menu-manage-move NB-menu-manage-feed-move' }, [
$.make('div', { className: 'NB-menu-manage-image' }),
$.make('div', { className: 'NB-menu-manage-title' }, 'Move to folder')
@ -2907,7 +2918,7 @@
var $select = $('select', $confirm);
if (_.isNumber(feed_id)) {
var feed = this.model.get_feed(feed_id);
var feed_view = feed.get_view($feed);
var feed_view = feed.get_view($feed, true);
var in_folder = feed_view.options.folder_title;
} else {
folder_view = NEWSBLUR.assets.folders.get_view($feed);
@ -4173,15 +4184,6 @@
self.mark_feed_as_read(feed_id, $t);
$t.fadeOut(400);
});
$.targetIs(e, { tagSelector: '.NB-feedbar-statistics' }, function($t, $p){
self.open_feed_statistics_modal();
});
$.targetIs(e, { tagSelector: '.NB-feedbar-train-feed' }, function($t, $p){
e.preventDefault();
if (!$('.NB-task-manage').hasClass('NB-disabled')) {
self.open_feed_intelligence_modal(1, self.active_feed, !self.flags.social_view);
}
});
$.targetIs(e, { tagSelector: '.NB-story-title-indicator' }, function($t, $p){
e.preventDefault();
self.show_hidden_story_titles();
@ -4363,6 +4365,10 @@
e.preventDefault();
e.stopPropagation();
});
$.targetIs(e, { tagSelector: '.NB-menu-manage-controls' }, function($t, $p){
e.preventDefault();
e.stopPropagation();
});
$.targetIs(e, { tagSelector: '.NB-menu-manage-rename' }, function($t, $p){
e.preventDefault();
e.stopPropagation();

View file

@ -6,6 +6,9 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
},
events: {
"click .NB-feedbar-train-feed" : "open_trainer",
"click .NB-feedbar-statistics" : "open_statistics",
"click .NB-feedbar-settings" : "open_settings",
"contextmenu" : "show_manage_menu",
"click .NB-feedlist-manage-icon" : "show_manage_menu",
"dblclick" : "open_feed_link",
@ -71,8 +74,7 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
<% } %>\
<%= feed.get("feed_title") %>\
<% if (type == "story") { %>\
<span class="NB-feedbar-train-feed" title="Train Intelligence"></span>\
<span class="NB-feedbar-statistics" title="Statistics"></span>\
<span class="NB-feedbar-settings" title="Site settings"></span>\
<% } %>\
</span>\
<% if (type == "story") { %>\
@ -272,6 +274,20 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
remove_hover_inverse: function() {
this.$el.removeClass('NB-hover-inverse');
},
open_trainer: function() {
if (!$('.NB-task-manage').hasClass('NB-disabled')) {
NEWSBLUR.reader.open_feed_intelligence_modal(1, null, !NEWSBLUR.reader.flags.social_view);
}
},
open_statistics: function() {
NEWSBLUR.reader.open_feed_statistics_modal();
},
open_settings: function(e) {
this.show_manage_menu(e);
}
});