diff --git a/apps/analyzer/views.py b/apps/analyzer/views.py
index f72ac0091..df8ae1c40 100644
--- a/apps/analyzer/views.py
+++ b/apps/analyzer/views.py
@@ -10,7 +10,7 @@ from django.utils.safestring import mark_safe
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_POST
from apps.rss_feeds.models import Feed, Story, Tag
-from apps.reader.models import UserSubscription, UserSubscriptionFolders, UserStory
+from apps.reader.models import UserSubscription, UserStory
from apps.analyzer.models import ClassifierTitle, ClassifierAuthor, ClassifierFeed, ClassifierTag
from utils import json
from utils.user_functions import get_user
diff --git a/apps/opml_import/views.py b/apps/opml_import/views.py
index 41d161944..b438b4b53 100644
--- a/apps/opml_import/views.py
+++ b/apps/opml_import/views.py
@@ -1,10 +1,12 @@
+# -*- coding: utf-8 -*-
+
from django.shortcuts import render_to_response, get_list_or_404, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.template import RequestContext
from django.core.cache import cache
from apps.rss_feeds.models import Feed
from apps.reader.models import UserSubscription, UserSubscriptionFolders
-from utils.json import json_encode
+from utils import json
import utils.opml as opml
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpRequest
@@ -17,46 +19,128 @@ import codecs
def opml_upload(request):
xml_opml = None
+ message = "OK"
+ code = 1
if request.method == 'POST':
if 'file' in request.FILES:
file = request.FILES['file']
xml_opml = file.read()
- data = opml_import(xml_opml, request.user).encode('utf-8')
+ opml_importer = OPMLImporter(xml_opml, request.user)
+ folders = opml_importer.process()
+
+ feeds = UserSubscription.objects.filter(user=user).values()
+ data = json.encode(dict(message=message, code=code, payload=dict(folders=folders, feeds=feeds)))
+
return HttpResponse(data, mimetype='text/plain')
-def opml_import(xml_opml, user):
- context = None
- outline = opml.from_string(xml_opml)
- feeds = []
- message = "OK"
- code = 1
- for folder in outline:
- print folder.text
- for feed in folder:
- print '\t%s - %s - %s' % (feed.title, type(feed.title), type(feed.title.decode('utf-8')))
- feed_data = dict(feed_address=feed.xmlUrl, feed_link=feed.htmlUrl, feed_title=feed.title)
- feeds.append(feed_data)
- new_feed = Feed(**feed_data)
- try:
- new_feed.save()
- except IntegrityError:
- new_feed = Feed.objects.get(feed_address=feed.xmlUrl)
- us = UserSubscription(feed=new_feed, user=user)
- try:
- us.save()
- except IntegrityError:
- us = UserSubscription.objects.get(feed=new_feed, user=user)
- user_sub_folder = UserSubscriptionFolders(user=user, feed=new_feed, user_sub=us, folder=folder.text)
- try:
- user_sub_folder.save()
- except IntegrityError:
- print 'Can\'t save user_sub_folder'
- # new_feed, _ = Feed.objects.get_or_create(feed_address=feed.xmlUrl, defaults=feed_data)
- # us, _ = UserSubscription.objects.get_or_create(feed=new_feed, user=user)
- # user_sub_folder, _ = UserSubscriptionFolders.objects.get_or_create(user=user, feed=new_feed, user_sub=us, defaults=dict(folder=folder.text))
- data = json_encode(dict(message=message, code=code, payload=dict(feeds=feeds, feed_count=len(feeds))))
- cache.delete('usersub:%s' % user)
+class OPMLImporter:
+
+ def __init__(self, opml_xml, user):
+ self.user = user
+ self.opml_xml = opml_xml
- return data
\ No newline at end of file
+ def process(self):
+ outline = opml.from_string(self.opml_xml)
+ folders = self.process_outline(outline)
+ UserSubscriptionFolders.objects.create(user=self.user, folders=json.encode(folders))
+
+ return folders
+
+ def process_outline(self, outline):
+ folders = []
+
+ for item in outline:
+ if not hasattr(item, 'xmlUrl'):
+ folder = item
+ print 'New Folder: %s' % folder.text
+ folders.append({folder.text: self.process_outline(folder)})
+ elif hasattr(item, 'xmlUrl'):
+ feed = item
+ print '\t%s - %s - %s' % (feed.title, feed.htmlUrl, feed.xmlUrl,)
+ feed_data = dict(feed_address=feed.xmlUrl, feed_link=feed.htmlUrl, feed_title=feed.title)
+ # feeds.append(feed_data)
+ feed_db, _ = Feed.objects.get_or_create(feed_address=feed.xmlUrl, defaults=dict(**feed_data))
+ us, _ = UserSubscription.objects.get_or_create(feed=feed_db, user=self.user)
+ folders.append(feed_db.pk)
+ return folders
+
+if __name__ == '__main__':
+ opml_string = """
+
+
+
+ mySubscriptions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ user = User.objects.get(username='conesus')
+ opml_importer = OPMLImporter(opml_string, user)
+ data = opml_importer.process()
+ print data
\ No newline at end of file
diff --git a/apps/reader/models.py b/apps/reader/models.py
index 534258bb5..ab41e413b 100644
--- a/apps/reader/models.py
+++ b/apps/reader/models.py
@@ -165,15 +165,11 @@ class UserStory(models.Model):
class UserSubscriptionFolders(models.Model):
user = models.ForeignKey(User)
- user_sub = models.ForeignKey(UserSubscription)
- feed = models.ForeignKey(Feed)
- folder = models.CharField(max_length=255)
+ folders = models.TextField(default="{}")
def __unicode__(self):
- return ('[' + self.feed.feed_title + '] '
- + self.folder)
+ return "[%s]: %s" % (self.user, len(self.folders),)
class Meta:
verbose_name_plural = "folders"
- verbose_name = "folder"
- unique_together = ("user", "user_sub")
\ No newline at end of file
+ verbose_name = "folder"
\ No newline at end of file
diff --git a/apps/reader/views.py b/apps/reader/views.py
index 82bb3736b..51522e8ac 100644
--- a/apps/reader/views.py
+++ b/apps/reader/views.py
@@ -57,40 +57,23 @@ def logout(request):
def load_feeds(request):
user = get_user(request)
+ feeds = {}
+
+ folders = UserSubscriptionFolders.objects.get(user=user)
+ user_subs = UserSubscription.objects.select_related('feed').filter(user=user)
- us = UserSubscriptionFolders.objects.select_related('feed', 'user_sub').filter(
- user=user
- )
- # logging.info('UserSubs: %s' % us)
- feeds = []
- folders = []
- for sub in us:
- try:
- sub.feed.unread_count_positive = sub.user_sub.unread_count_positive
- sub.feed.unread_count_neutral = sub.user_sub.unread_count_neutral
- sub.feed.unread_count_negative = sub.user_sub.unread_count_negative
- except:
- logging.warn("Subscription %s does not exist outside of Folder." % (sub.feed))
- sub.delete()
- else:
- if sub.folder not in folders:
- folders.append(sub.folder)
- feeds.append({'folder': sub.folder, 'feeds': []})
- for folder in feeds:
- if folder['folder'] == sub.folder:
- folder['feeds'].append(sub.feed)
+ for sub in user_subs:
+ feeds[sub.feed.pk] = {
+ 'id': sub.feed.pk,
+ 'feed_title': sub.feed.feed_title,
+ 'feed_link': sub.feed.feed_link,
+ 'unread_count_positive': sub.unread_count_positive,
+ 'unread_count_neutral': sub.unread_count_neutral,
+ 'unread_count_negative': sub.unread_count_negative,
+ }
- # Alphabetize folders, then feeds inside folders
- feeds.sort(lambda x, y: cmp(x['folder'].lower(), y['folder'].lower()))
- for feed in feeds:
- feed['feeds'].sort(lambda x, y: cmp(x.feed_title.lower(), y.feed_title.lower()))
- for f in feed['feeds']:
- f.feed_address = mark_safe(f.feed_address)
- f.page_data = None
-
-
- data = json.encode(feeds)
- return HttpResponse(data, mimetype='application/json')
+ data = dict(feeds=feeds, folders=json.decode(folders.folders))
+ return HttpResponse(json.encode(data), mimetype='application/json')
def load_single_feed(request):
user = get_user(request)
diff --git a/apps/rss_feeds/management/commands/calculate_scores.py b/apps/rss_feeds/management/commands/calculate_scores.py
index f4ec71b22..facc8c70f 100644
--- a/apps/rss_feeds/management/commands/calculate_scores.py
+++ b/apps/rss_feeds/management/commands/calculate_scores.py
@@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand
from django.core.handlers.wsgi import WSGIHandler
from apps.rss_feeds.models import Feed, Story
from django.core.cache import cache
-from apps.reader.models import UserSubscription, UserSubscriptionFolders, UserStory
+from apps.reader.models import UserSubscription, UserStory
from optparse import OptionParser, make_option
import os
import logging
diff --git a/apps/rss_feeds/management/commands/refresh_feed.py b/apps/rss_feeds/management/commands/refresh_feed.py
index 729969958..e86f8a44e 100644
--- a/apps/rss_feeds/management/commands/refresh_feed.py
+++ b/apps/rss_feeds/management/commands/refresh_feed.py
@@ -3,7 +3,7 @@ from django.core.handlers.wsgi import WSGIHandler
from apps.rss_feeds.models import Feed, Story
from django.core.cache import cache
from django.db.models import Q
-from apps.reader.models import UserSubscription, UserSubscriptionFolders, UserStory
+from apps.reader.models import UserSubscription, UserStory
from optparse import OptionParser, make_option
from utils.management_functions import daemonize
import os
diff --git a/apps/rss_feeds/models.py b/apps/rss_feeds/models.py
index 520a59b1a..daec803bc 100644
--- a/apps/rss_feeds/models.py
+++ b/apps/rss_feeds/models.py
@@ -164,12 +164,16 @@ class Feed(models.Model):
user_stories_count = 0
stories = Story.objects.filter(story_feed=self).order_by('-story_date')
print 'Found %s stories in %s. Trimming...' % (stories.count(), self)
- for story in stories[1000:]:
- user_stories = UserStory.objects.filter(story=story)
+ if stories.count() > 1000:
+ old_story = stories[1000]
+ user_stories = UserStory.objects.filter(feed=self,
+ read_date__lte=old_story.story_date)
user_stories_count = user_stories.count()
user_stories.delete()
- story.delete()
- stories_deleted_count += 1
+ old_stories = Story.objects.filter(story_feed=self,
+ story_date__lte=old_story.story_date)
+ stories_deleted_count = old_stories.count()
+ old_stories.delete()
if stories_deleted_count:
print "Trimming %s stories from %s. %s user stories." % (
diff --git a/media/css/reader.css b/media/css/reader.css
index c58388c63..e06e8e7ab 100644
--- a/media/css/reader.css
+++ b/media/css/reader.css
@@ -135,6 +135,10 @@ a img {
text-transform: uppercase;
}
+#feed_list .feeds {
+ margin-left: 22px;
+}
+
#feed_list .feed {
position: relative;
cursor: pointer;
@@ -149,11 +153,11 @@ a img {
#feed_list img.feed_favicon {
position: absolute;
top: 2px;
- left: 24px;
+ left: 2px;
}
#feed_list .feed_title {
display: block;
- padding: 4px 42px 2px 45px;
+ padding: 4px 42px 2px 23px;
text-decoration: none;
color: #272727;
line-height: 1.3em;
diff --git a/media/js/jquery.tinysort.js b/media/js/jquery.tinysort.js
new file mode 100644
index 000000000..2a58e580a
--- /dev/null
+++ b/media/js/jquery.tinysort.js
@@ -0,0 +1,127 @@
+/*
+* jQuery TinySort - A plugin to sort child nodes by (sub) contents or attributes.
+*
+* Version: 1.0.3
+*
+* Copyright (c) 2008 Ron Valstar
+*
+* Dual licensed under the MIT and GPL licenses:
+* http://www.opensource.org/licenses/mit-license.php
+* http://www.gnu.org/licenses/gpl.html
+*
+* description
+* - A plugin to sort child nodes by (sub) contents or attributes.
+*
+* Usage:
+* $("ul#people>li").tsort();
+* $("ul#people>li").tsort("span.surname");
+* $("ul#people>li").tsort("span.surname",{order:"desc"});
+* $("ul#people>li").tsort({place:"end"});
+*
+* Change default like so:
+* $.tinysort.defaults.order = "desc";
+*
+* in this update:
+* - tested with jQuery 1.4.1
+* - correct isNum return
+*
+* in last update:
+* - matching numerics did not work for trailing zero's, replaced with regexp (which should now work for + and - signs as well)
+*
+* Todos
+* - fix mixed literal/numeral values
+* - determine if I have to use setArray or pushStack
+*
+*/
+;(function($) {
+ // default settings
+ $.tinysort = {
+ id: "TinySort"
+ ,version: "1.0.3"
+ ,defaults: {
+ order: "asc" // order: asc, desc or rand
+ ,attr: "" // order by attribute value
+ ,place: "start" // place ordered elements at position: start, end, org (original position), first
+ ,returns: false // return all elements or only the sorted ones (true/false)
+ }
+ };
+ $.fn.extend({
+ tinysort: function(_find,_settings) {
+ if (_find&&typeof(_find)!="string") {
+ _settings = _find;
+ _find = null;
+ }
+
+ var oSettings = $.extend({}, $.tinysort.defaults, _settings);
+
+ var oElements = {}; // contains sortable- and non-sortable list per parent
+ this.each(function(i) {
+ // element or sub selection
+ var mElm = (!_find||_find=="")?$(this):$(this).find(_find);
+ // text or attribute value
+ var sSort = oSettings.order=="rand"?""+Math.random():(oSettings.attr==""?mElm.text():mElm.attr(oSettings.attr));
+ // to sort or not to sort
+ var mParent = $(this).parent();
+ if (!oElements[mParent]) oElements[mParent] = {s:[],n:[]}; // s: sort, n: not sort
+ if (mElm.length>0) oElements[mParent].s.push({s:sSort,e:$(this),n:i}); // s:string, e:element, n:number
+ else oElements[mParent].n.push({e:$(this),n:i});
+ });
+ //
+ // sort
+ for (var sParent in oElements) {
+ var oParent = oElements[sParent];
+ oParent.s.sort(
+ function zeSort(a,b) {
+ var x = a.s.toLowerCase?a.s.toLowerCase():a.s;
+ var y = b.s.toLowerCase?b.s.toLowerCase():b.s;
+ if (isNum(a.s)&&isNum(b.s)) {
+ x = parseFloat(a.s);
+ y = parseFloat(b.s);
+ }
+ return (oSettings.order=="asc"?1:-1)*(xy?1:0));
+ }
+ );
+ }
+ //
+ // order elements and fill new order
+ var aNewOrder = [];
+ for (var sParent in oElements) {
+ var oParent = oElements[sParent];
+ var aOrg = []; // list for original position
+ var iLow = $(this).length;
+ switch (oSettings.place) {
+ case "first": $.each(oParent.s,function(i,obj) { iLow = Math.min(iLow,obj.n) }); break;
+ case "org": $.each(oParent.s,function(i,obj) { aOrg.push(obj.n) }); break;
+ case "end": iLow = oParent.n.length; break;
+ default: iLow = 0;
+ }
+ var aCnt = [0,0]; // count how much we've sorted for retreival from either the sort list or the non-sort list (oParent.s/oParent.n)
+ for (var i=0;i<$(this).length;i++) {
+ var bSList = i>=iLow&&i0?x[1]:false;
+ };
+ // array contains
+ function contains(a,n) {
+ var bInside = false;
+ $.each(a,function(i,m) {
+ if (!bInside) bInside = m==n;
+ });
+ return bInside;
+ };
+ // set functions
+ $.fn.TinySort = $.fn.Tinysort = $.fn.tsort = $.fn.tinysort;
+})(jQuery);
\ No newline at end of file
diff --git a/media/js/newsblur/assetmodel.js b/media/js/newsblur/assetmodel.js
index 063c37558..d5e2b6fdb 100644
--- a/media/js/newsblur/assetmodel.js
+++ b/media/js/newsblur/assetmodel.js
@@ -25,6 +25,7 @@ NEWSBLUR.AssetModel = function() {
NEWSBLUR.AssetModel.Reader = function() {
this.feeds = {};
+ this.folders = [];
this.stories = {};
};
@@ -135,9 +136,10 @@ NEWSBLUR.AssetModel.Reader.prototype = {
load_feeds: function(callback) {
var self = this;
- var pre_callback = function(folders) {
- self.folders = folders;
- callback(folders);
+ var pre_callback = function(subscriptions) {
+ self.feeds = subscriptions.feeds;
+ self.folders = subscriptions.folders;
+ callback();
};
this.make_request('/reader/load_feeds', {}, pre_callback);
@@ -182,15 +184,8 @@ NEWSBLUR.AssetModel.Reader.prototype = {
get_feed: function(feed_id, callback) {
var self = this;
- for (fld in this.folders) {
- var feeds = this.folders[fld].feeds;
- for (f in feeds) {
- if (feeds[f].id == feed_id) {
- return feeds[f];
- }
- }
- }
- return null;
+
+ return this.feeds[feed_id];
},
get_feed_tags: function() {
diff --git a/media/js/newsblur/reader.js b/media/js/newsblur/reader.js
index 0ee0b49c9..f49a153fa 100644
--- a/media/js/newsblur/reader.js
+++ b/media/js/newsblur/reader.js
@@ -334,63 +334,85 @@
load_feeds: function() {
var self = this;
- var callback = function() {
- var $feed_list = self.$feed_list.empty();
- var folders = self.model.folders;
-
- $('#story_taskbar').css({'display': 'block'});
- // NEWSBLUR.log(['Subscriptions', {'folders':folders}]);
- for (fo in folders) {
- var feeds = folders[fo].feeds;
- var $folder = $.make('div', { className: 'folder' }, [
- $.make('span', { className: 'folder_title' }, folders[fo].folder),
- $.make('div', { className: 'feeds' })
- ]);
- for (f in feeds) {
- var unread_class = '';
- if (feeds[f].unread_count_positive) {
- unread_class += ' unread_positive';
- }
- if (feeds[f].unread_count_neutral) {
- unread_class += ' unread_neutral';
- }
- if (feeds[f].unread_count_negative) {
- unread_class += ' unread_negative';
- }
- var $feed = $.make('div', { className: 'feed ' + unread_class }, [
- $.make('span', {
- className: 'unread_count unread_count_positive '
- + (feeds[f].unread_count_positive
- ? "unread_count_full"
- : "unread_count_empty")
- }, ''+feeds[f].unread_count_positive),
- $.make('span', {
- className: 'unread_count unread_count_neutral '
- + (feeds[f].unread_count_neutral
- ? "unread_count_full"
- : "unread_count_empty")
- }, ''+feeds[f].unread_count_neutral),
- $.make('span', {
- className: 'unread_count unread_count_negative '
- + (feeds[f].unread_count_negative
- ? "unread_count_full"
- : "unread_count_empty")
- }, ''+feeds[f].unread_count_negative),
- $.make('img', { className: 'feed_favicon', src: self.google_favicon_url + feeds[f].feed_link }),
- $.make('span', { className: 'feed_title' }, feeds[f].feed_title)
- ]).data('feed_id', feeds[f].id);
- $('.feeds', $folder).append($feed);
- }
- $feed_list.append($folder);
- }
- $('.unread_count', $feed_list).corners('4px');
- };
-
if ($('#feed_list').length) {
- this.model.load_feeds(callback);
+ this.model.load_feeds($.rescope(this.make_feeds, this));
}
},
+ make_feeds: function() {
+ var $feed_list = this.$feed_list.empty();
+ var folders = this.model.folders;
+ var feeds = this.model.feeds;
+ NEWSBLUR.log(['Making feeds', {'folders': folders, 'feeds': feeds}]);
+
+ $('#story_taskbar').css({'display': 'block'});
+ // NEWSBLUR.log(['Subscriptions', {'folders':folders}]);
+ var $folder = this.make_feeds_folder(folders);
+ $feed_list.append($folder);
+ $('.unread_count', $feed_list).corners('4px');
+ },
+
+ make_feeds_folder: function(items) {
+ var $feeds = $.make('div');
+
+ for (var i in items) {
+ var item = items[i];
+
+ if (typeof item == "number") {
+ var feed = this.model.feeds[item];
+
+ var unread_class = '';
+ if (feed.unread_count_positive) {
+ unread_class += ' unread_positive';
+ }
+ if (feed.unread_count_neutral) {
+ unread_class += ' unread_neutral';
+ }
+ if (feed.unread_count_negative) {
+ unread_class += ' unread_negative';
+ }
+ var $feed = $.make('div', { className: 'feed ' + unread_class }, [
+ $.make('span', {
+ className: 'unread_count unread_count_positive '
+ + (feed.unread_count_positive
+ ? "unread_count_full"
+ : "unread_count_empty")
+ }, ''+feed.unread_count_positive),
+ $.make('span', {
+ className: 'unread_count unread_count_neutral '
+ + (feed.unread_count_neutral
+ ? "unread_count_full"
+ : "unread_count_empty")
+ }, ''+feed.unread_count_neutral),
+ $.make('span', {
+ className: 'unread_count unread_count_negative '
+ + (feed.unread_count_negative
+ ? "unread_count_full"
+ : "unread_count_empty")
+ }, ''+feed.unread_count_negative),
+ $.make('img', { className: 'feed_favicon', src: this.google_favicon_url + feed.feed_link }),
+ $.make('span', { className: 'feed_title' }, feed.feed_title)
+ ]).data('feed_id', feed.id);
+
+ $feeds.append($feed);
+ } else if (typeof item == "object") {
+ for (var o in item) {
+ var folder = item[o];
+ var $folder = $.make('div', { className: 'folder' }, [
+ $.make('span', { className: 'folder_title' }, o),
+ $.make('div', { className: 'feeds' }, this.make_feeds_folder(folder))
+ ]);
+ $feeds.append($folder);
+ }
+ }
+ }
+
+ $('.feed', $feeds).tsort('.feed_title');
+ $('.folder', $feeds).tsort('.folder_title');
+
+ return $feeds;
+ },
+
// =====================
// = Story Titles Pane =
// =====================
diff --git a/settings.py b/settings.py
index 6ea29d062..53f6e46c0 100644
--- a/settings.py
+++ b/settings.py
@@ -108,7 +108,8 @@ elif DEV_SERVER1:
MEDIA_URL = '/media/'
DEBUG = True
# CACHE_BACKEND = 'locmem:///'
- CACHE_BACKEND = 'memcached://127.0.0.1:11211'
+ # CACHE_BACKEND = 'memcached://127.0.0.1:11211'
+ CACHE_BACKEND = 'dummy:///'
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(message)s',
filename=LOG_FILE,
@@ -181,6 +182,7 @@ COMPRESS_JS = {
'js/jquery.ui.core.js',
'js/jquery.ui.slider.js',
'js/jquery.layout.js',
+ 'js/jquery.tinysort.js',
'js/jquery.fieldselection.js',
'js/newsblur/assetmodel.js',
@@ -267,5 +269,16 @@ INSTALLED_APPS = (
'apps.registration',
'apps.opml_import',
'apps.profile',
+ 'devserver',
# 'debug_toolbar'
)
+
+DEVSERVER_MODULES = (
+ 'devserver.modules.sql.SQLRealTimeModule',
+ 'devserver.modules.sql.SQLSummaryModule',
+ 'devserver.modules.profile.ProfileSummaryModule',
+
+ # Modules not enabled by default
+ 'devserver.modules.profile.MemoryUseModule',
+ 'devserver.modules.cache.CacheSummaryModule',
+)
diff --git a/utils/feed_fetcher.py b/utils/feed_fetcher.py
index 6eaf41fdc..832ef697b 100644
--- a/utils/feed_fetcher.py
+++ b/utils/feed_fetcher.py
@@ -1,6 +1,6 @@
from apps.rss_feeds.models import Story
from django.core.cache import cache
-from apps.reader.models import UserSubscription, UserSubscriptionFolders, UserStory
+from apps.reader.models import UserSubscription, UserStory
from apps.rss_feeds.importer import PageImporter
from utils import feedparser, threadpool
from django.db import transaction