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