Double duty: Adding /rss_feeds/search_feed endpoint for discovering feeds through the API. Also adding in a new preference for the sort order of feeds. A bit broken, but it'll do for now. Thanks to @heliostatic for the idea.

This commit is contained in:
Samuel Clay 2011-04-25 20:53:29 -04:00
parent 8cf598fcbe
commit cc94f3fd12
9 changed files with 156 additions and 29 deletions

View file

@ -49,6 +49,7 @@ class UserSubscription(models.Model):
feed['nt'] = self.unread_count_neutral
feed['ng'] = self.unread_count_negative
feed['active'] = self.active
feed['feed_opens'] = self.feed_opens
if not self.active and self.user.profile.is_premium:
feed['active'] = True
self.active = True

View file

@ -13,6 +13,7 @@ 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
from mongoengine.base import ValidationError
from apps.rss_feeds.tasks import UpdateFeeds
@ -118,37 +119,54 @@ class Feed(models.Model):
pass
@classmethod
def get_feed_from_url(cls, url):
def get_feed_from_url(cls, url, create=True, aggressive=False, offset=0):
feed = None
def criteria(key, value):
if aggressive:
return {'%s__icontains' % key: value}
else:
return {'%s' % key: value}
def by_url(address):
feed = cls.objects.filter(feed_address=address)
feed = cls.objects.filter(**criteria('feed_address', address))
if not feed:
duplicate_feed = DuplicateFeed.objects.filter(duplicate_address=address).order_by('pk')
if duplicate_feed:
feed = [duplicate_feed[0].feed]
duplicate_feed = DuplicateFeed.objects.filter(**criteria('duplicate_address', address)).order_by('pk')
if duplicate_feed and len(duplicate_feed) > offset:
feed = [duplicate_feed[offset].feed]
if not feed:
feed = cls.objects.filter(**criteria('feed_link', address))
return feed
url = urlnorm.normalize(url)
# Normalize and check for feed_address, dupes, and feed_link
if not aggressive:
url = urlnorm.normalize(url)
feed = by_url(url)
if feed:
feed = feed[0]
else:
# Create if it looks good
if feed and len(feed) > offset:
feed = feed[offset]
elif create:
if feedfinder.isFeed(url):
feed = cls.objects.create(feed_address=url)
feed = feed.update()
else:
feed_finder_url = feedfinder.feed(url)
if feed_finder_url:
feed = by_url(feed_finder_url)
if not feed:
feed = cls.objects.create(feed_address=feed_finder_url)
feed = feed.update()
else:
feed = feed[0]
# Still nothing? Maybe the URL has some clues.
if not feed:
feed_finder_url = feedfinder.feed(url)
if feed_finder_url:
feed = by_url(feed_finder_url)
if not feed and create:
feed = cls.objects.create(feed_address=feed_finder_url)
feed = feed.update()
elif feed and len(feed) > offset:
feed = feed[offset]
# Not created and not within bounds, so toss results.
if isinstance(feed, QuerySet):
return
return feed
@classmethod

View file

@ -3,6 +3,7 @@ from apps.rss_feeds import views
urlpatterns = patterns('',
url(r'^feed_autocomplete', views.feed_autocomplete, name='feed-autocomplete'),
url(r'^search_feed', views.search_feed, name='search-feed'),
url(r'^statistics/(?P<feed_id>\d+)', views.load_feed_statistics, name='feed-statistics'),
url(r'^feed/(?P<feed_id>\d+)', views.load_single_feed, name='feed-info'),
url(r'^exception_retry', views.exception_retry, name='exception-retry'),

View file

@ -15,6 +15,18 @@ from utils import json_functions as json, feedfinder
from utils.feed_functions import relative_timeuntil, relative_timesince
from utils.user_functions import get_user
@json.json_view
def search_feed(request):
address = request.REQUEST['address']
offset = int(request.REQUEST.get('offset', 0))
feed = Feed.get_feed_from_url(address, create=False, aggressive=True, offset=offset)
if feed:
return feed.canonical()
else:
return dict(code=-1, message="No feed found matching that XML or website address.")
@json.json_view
def load_single_feed(request, feed_id):
user = get_user(request)

View file

@ -4957,6 +4957,10 @@ background: transparent;
vertical-align: middle;
margin: -1px 6px 0 2px;
}
.NB-modal-preferences .NB-preference-feedorder label img {
vertical-align: middle;
margin: -3px 6px 0 2px;
}
.NB-modal-preferences .NB-preference-password .NB-preference-option {
float: left;
margin: 0 12px 0 0;

View file

@ -852,7 +852,7 @@
'opacity': 0
});
$feed_list.html($feeds);
this.sort_feeds($feed_list);
// this.sort_feeds($feed_list);
this.count_collapsed_unread_stories();
$feed_list.animate({'opacity': 1}, {'duration': 700});
this.hover_over_feed_titles($feed_list);
@ -903,6 +903,49 @@
}
},
sort_items: function(items) {
var self = this;
var sort_order = this.model.preference('feed_order');
if (sort_order == 'ALPHABETICAL' || !sort_order) {
return items.sort(function(a, b) {
var feedA, feedB;
if (_.isNumber(a)) feedA = self.model.get_feed(a);
if (_.isNumber(b)) feedB = self.model.get_feed(b);
if (feedA && feedB) {
return feedA.feed_title > feedB.feed_title ? 1 : -1;
} else if (feedA && !feedB) {
return -1;
} else if (!feedA && feedB) {
return 1;
} else if (!feedA && !feedB) {
var folderA = _.keys(a)[0];
var folderB = _.keys(b)[0];
return folderA > folderB ? 1 : -1;
}
});
} else if (sort_order == 'MOSTUSED') {
return items.sort(function(a, b) {
var feedA, feedB;
if (_.isNumber(a)) feedA = self.model.get_feed(a);
if (_.isNumber(b)) feedB = self.model.get_feed(b);
if (feedA && feedB) {
return feedA.feed_opens < feedB.feed_opens ? 1 :
(feedA.feed_opens > feedB.feed_opens ? -1 :
(feedA.feed_title > feedB.feed_title));
} else if (feedA && !feedB) {
return -1;
} else if (!feedA && feedB) {
return 1;
} else if (!feedA && !feedB) {
var folderA = _.keys(a)[0];
var folderB = _.keys(b)[0];
return folderA > folderB ? 1 : -1;
}
});
}
},
sort_feeds: function($feeds) {
$('.feed', $feeds).tsort('.feed_title');
$('.folder', $feeds).tsort('.folder_title_text');
@ -920,6 +963,8 @@
var self = this;
var $feeds = "";
items = this.sort_items(items);
for (var i in items) {
var item = items[i];
@ -1093,8 +1138,8 @@
}
},
change: function(e, ui) {
$('.feed', ui.placeholder.closest('ul.folder')).tsort('.feed_title');
$('li.folder', ui.placeholder.closest('ul.folder')).tsort('.folder_title_text');
var $feeds = ui.placeholder.closest('ul.folder');
self.sort_feeds($feeds);
},
stop: function(e, ui) {
setTimeout(function() {
@ -1102,8 +1147,7 @@
}, 100);
ui.item.removeClass('NB-feed-sorting');
self.$s.$feed_list.removeClass('NB-feed-sorting');
$('.feed', e.target).tsort('.feed_title');
$('li.folder', e.target).tsort('.folder_title_text');
self.sort_feeds(e.target);
self.save_feed_order();
ui.item.css({'backgroundColor': '#D7DDE6'})
.animate({'backgroundColor': '#F0F076'}, {'duration': 800})

View file

@ -18,6 +18,7 @@ NEWSBLUR.ReaderPreferences.prototype = {
this.handle_cancel();
this.handle_change();
this.open_modal();
this.original_preferences = this.serialize_preferences();
this.$modal.bind('click', $.rescope(this.handle_click, this));
},
@ -198,6 +199,27 @@ NEWSBLUR.ReaderPreferences.prototype = {
$.make('div', { className: 'NB-preference-sublabel' }, this.make_site_sidebar_count())
])
]),
$.make('div', { className: 'NB-preference NB-preference-feedorder' }, [
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
$.make('input', { id: 'NB-preference-feedorder-1', type: 'radio', name: 'feed_order', value: 'ALPHABETICAL' }),
$.make('label', { 'for': 'NB-preference-feedorder-1' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/pilcrow.png' }),
'Alphabetical'
])
]),
$.make('div', [
$.make('input', { id: 'NB-preference-feedorder-2', type: 'radio', name: 'feed_order', value: 'MOSTUSED' }),
$.make('label', { 'for': 'NB-preference-feedorder-2' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/report_user.png' }),
'Most used at top, then alphabetical'
])
])
]),
$.make('div', { className: 'NB-preference-label'}, [
'Site sidebar order'
])
]),
$.make('div', { className: 'NB-preference NB-preference-hidestorychanges' }, [
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
@ -290,7 +312,7 @@ NEWSBLUR.ReaderPreferences.prototype = {
$.make('a', { className: 'NB-splash-link', href: NEWSBLUR.URLs['opml-export'] }, 'Download OPML')
]),
$.make('div', { className: 'NB-preference-label'}, [
'Backup Your Sites',
'Backup your sites',
$.make('div', { className: 'NB-preference-sublabel' }, 'Download this XML file as a backup.')
])
]),
@ -306,7 +328,7 @@ NEWSBLUR.ReaderPreferences.prototype = {
])
]),
$.make('div', { className: 'NB-preference-label'}, [
'Change Password',
'Change password',
$.make('div', { className: 'NB-preference-error'})
])
]),
@ -379,6 +401,12 @@ NEWSBLUR.ReaderPreferences.prototype = {
return false;
}
});
$('input[name=feed_order]', this.$modal).each(function() {
if ($(this).val() == NEWSBLUR.Preferences.feed_order) {
$(this).attr('checked', true);
return false;
}
});
$('input[name=hide_story_changes]', this.$modal).each(function() {
if ($(this).val() == NEWSBLUR.Preferences.hide_story_changes) {
$(this).attr('checked', true);
@ -443,6 +471,9 @@ NEWSBLUR.ReaderPreferences.prototype = {
NEWSBLUR.reader.switch_feed_view_unread_view();
NEWSBLUR.reader.apply_story_styling(true);
NEWSBLUR.reader.show_stories_preference_in_feed_view();
if (self.original_preferences['feed_order'] != form['feed_order']) {
NEWSBLUR.reader.make_feeds();
}
$.modal.close();
});
},

View file

@ -28,6 +28,7 @@
'new_window' : 1,
'default_view' : 'page',
'hide_read_feeds' : 0,
'feed_order' : 'ALPHABETICAL',
'hide_story_changes' : 0,
'feed_view_single_story' : 0,
'view_settings' : {},

View file

@ -42,6 +42,21 @@
example: "samuel@ofbrooklyn.com"
- feeds:
- url: /rss_feeds/search_feed
method: GET
short_desc: "Information about a feed from its address."
long_desc:
- "Retrieve information about a feed from its website or RSS address."
params:
- key: address
desc: "Searches the RSS and website address and returns a feed."
required: true
example: 'techcrunch.com'
- key: offset
desc: "Try paging through feeds found by using the offset."
optional: true
example: 1
- url: /reader/feeds
method: GET
short_desc: "User's feeds, with unread counts, meta data, and optional favicons."