Merge branch 'master' of github.com:samuelclay/newsblur

This commit is contained in:
Samuel Clay 2010-02-14 22:51:29 -05:00
commit 478827b51e
13 changed files with 378 additions and 150 deletions

View file

@ -10,7 +10,7 @@ from django.utils.safestring import mark_safe
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from apps.rss_feeds.models import Feed, Story, Tag 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 apps.analyzer.models import ClassifierTitle, ClassifierAuthor, ClassifierFeed, ClassifierTag
from utils import json from utils import json
from utils.user_functions import get_user from utils.user_functions import get_user

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
from django.shortcuts import render_to_response, get_list_or_404, get_object_or_404 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.contrib.auth.decorators import login_required
from django.template import RequestContext from django.template import RequestContext
from django.core.cache import cache from django.core.cache import cache
from apps.rss_feeds.models import Feed from apps.rss_feeds.models import Feed
from apps.reader.models import UserSubscription, UserSubscriptionFolders from apps.reader.models import UserSubscription, UserSubscriptionFolders
from utils.json import json_encode from utils import json
import utils.opml as opml import utils.opml as opml
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponse, HttpRequest from django.http import HttpResponse, HttpRequest
@ -17,46 +19,128 @@ import codecs
def opml_upload(request): def opml_upload(request):
xml_opml = None xml_opml = None
message = "OK"
code = 1
if request.method == 'POST': if request.method == 'POST':
if 'file' in request.FILES: if 'file' in request.FILES:
file = request.FILES['file'] file = request.FILES['file']
xml_opml = file.read() 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') return HttpResponse(data, mimetype='text/plain')
def opml_import(xml_opml, user): class OPMLImporter:
context = None
outline = opml.from_string(xml_opml) def __init__(self, opml_xml, user):
feeds = [] self.user = user
message = "OK" self.opml_xml = opml_xml
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)
return data 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 = """<?xml version="1.0" encoding="UTF-8"?>
<!-- OPML generated by NetNewsWire -->
<opml version="1.1">
<head>
<title>mySubscriptions</title>
</head>
<body>
<outline text="New York" title="New York">
<outline text="Brownstoner" description="" title="Brownstoner" type="rss" version="RSS" htmlUrl="http://www.brownstoner.com/" xmlUrl="http://www.brownstoner.com/index.xml"/>
<outline text="Gothamist" description="" title="Gothamist" type="rss" version="RSS" htmlUrl="http://gothamist.com/" xmlUrl="http://gothamist.com/index.rdf"/>
<outline text="Brokelyn" description="" title="Brokelyn" type="rss" version="RSS" htmlUrl="http://www.brokelyn.com" xmlUrl="http://www.brokelyn.com/feed/"/>
<outline text="Streetsblog New York City" description="" title="Streetsblog New York City" type="rss" version="RSS" htmlUrl="http://www.streetsblog.org" xmlUrl="http://www.streetsblog.org/feed/"/>
<outline text="The Corduroy Appreciation Club" description="" title="The Corduroy Appreciation Club" type="rss" version="RSS" htmlUrl="http://corduroyclub.com" xmlUrl="http://corduroyclub.com/feed"/>
<outline text="FREEwilliamsburg" description="" title="FREEwilliamsburg" type="rss" version="RSS" htmlUrl="http://www.freewilliamsburg.com/" xmlUrl="http://www.freewilliamsburg.com/atom.xml"/>
<outline text="Scouting NY" description="" title="Scouting NY" type="rss" version="RSS" htmlUrl="http://www.scoutingny.com" xmlUrl="http://www.scoutingny.com/?feed=rss2"/>
<outline text="very small array" description="" title="very small array" type="rss" version="RSS" htmlUrl="http://www.verysmallarray.com" xmlUrl="http://www.verysmallarray.com/?feed=rss2"/>
<outline text="Frugal Traveler" description="" title="Frugal Traveler" type="rss" version="RSS" htmlUrl="http://frugaltraveler.blogs.nytimes.com/" xmlUrl="http://frugaltraveler.blogs.nytimes.com/feed/"/>
</outline>
<outline text="Tech" title="Tech">
<outline text="Joel on Software" description="" title="Joel on Software" type="rss" version="RSS" htmlUrl="http://www.joelonsoftware.com" xmlUrl="http://www.joelonsoftware.com/rss.xml"/>
<outline text="Daring Fireball" description="" title="Daring Fireball" type="rss" version="RSS" htmlUrl="http://daringfireball.net/" xmlUrl="http://daringfireball.net/index.xml"/>
<outline text="Techcrunch" description="" title="Techcrunch" type="rss" version="RSS" htmlUrl="http://techcrunch.com" xmlUrl="http://feeds.feedburner.com/Techcrunch"/>
<outline text="Coding Horror" description="" title="Coding Horror" type="rss" version="RSS" htmlUrl="http://www.codinghorror.com/blog/" xmlUrl="http://feeds.feedburner.com/codinghorror/"/>
<outline text="John Resig" description="" title="John Resig" type="rss" version="RSS" htmlUrl="http://ejohn.org" xmlUrl="http://feeds.feedburner.com/JohnResig"/>
<outline text="Ted Dziuba" description="" title="Ted Dziuba" type="rss" version="RSS" htmlUrl="http://teddziuba.com/" xmlUrl="http://teddziuba.com/atom.xml"/>
<outline text="Ajaxian » Front Page" description="" title="Ajaxian » Front Page" type="rss" version="RSS" htmlUrl="http://ajaxian.com" xmlUrl="http://ajaxian.com/index.xml"/>
<outline text="James Padolsey" description="" title="James Padolsey" type="rss" version="RSS" htmlUrl="http://james.padolsey.com" xmlUrl="http://james.padolsey.com/feed/"/>
<outline text="David Cramer's Blog" description="" title="David Cramer's Blog" type="rss" version="RSS" htmlUrl="http://www.davidcramer.net" xmlUrl="http://www.davidcramer.net/feed"/>
<outline text="MacRumors : Mac News and Rumors" description="" title="MacRumors : Mac News and Rumors" type="rss" version="RSS" htmlUrl="http://www.macrumors.com" xmlUrl="http://www.macrumors.com/macrumors.xml"/>
<outline text="wonko.com" description="" title="wonko.com" type="rss" version="RSS" htmlUrl="http://wonko.com/" xmlUrl="http://feeds.feedburner.com/wonko"/>
<outline text="Devthought" description="" title="Devthought" type="rss" version="RSS" htmlUrl="http://devthought.com" xmlUrl="http://feeds2.feedburner.com/devthought?format=xml"/>
<outline text="jQuery Blog" description="" title="jQuery Blog" type="rss" version="RSS" htmlUrl="http://blog.jquery.com" xmlUrl="http://blog.jquery.com/feed/"/>
<outline text="Hacker News - Excerpts" description="" title="Hacker News - Excerpts" type="rss" version="RSS" htmlUrl="http://news.ycombinator.com/" xmlUrl="http://andrewtrusty.appspot.com/readability/feed?url=http%3A//news.ycombinator.com/rss"/>
<outline text="Waxy.org" description="" title="Waxy.org" type="rss" version="RSS" htmlUrl="http://waxy.org/" xmlUrl="http://waxy.org/index.xml"/>
<outline text="Lazy Pythonista" description="" title="Lazy Pythonista" type="rss" version="RSS" htmlUrl="http://lazypython.blogspot.com/" xmlUrl="http://lazypython.blogspot.com/feeds/posts/default?alt=rss"/>
<outline text="cdixon.org - chris dixon's blog" description="" title="cdixon.org - chris dixon's blog" type="rss" version="RSS" htmlUrl="http://cdixon.org" xmlUrl="http://cdixon.org/feed/"/>
<outline text="Bill the Lizard" description="" title="Bill the Lizard" type="rss" version="RSS" htmlUrl="http://www.billthelizard.com/" xmlUrl="http://www.billthelizard.com/feeds/posts/default"/>
<outline text="inessential.com" description="" title="inessential.com" type="rss" version="RSS" htmlUrl="http://inessential.com/" xmlUrl="http://inessential.com/xml/rss.xml"/>
</outline>
<outline text="Blogs" title="Blogs">
<outline text="You Look Nice Today" description="" title="You Look Nice Today" type="rss" version="RSS" htmlUrl="http://youlooknicetoday.com" xmlUrl="http://youlooknicetoday.com/rss.xml"/>
<outline text="The Lens and the Eyebrow" description="" title="The Lens and the Eyebrow" type="rss" version="RSS" htmlUrl="http://blog.seemichaelsphotos.com" xmlUrl="http://blog.seemichaelsphotos.com/rss2.aspx"/>
<outline text="43 Folders" description="" title="43 Folders" type="rss" version="RSS" htmlUrl="http://www.43folders.com" xmlUrl="http://www.43folders.com/rss.xml"/>
<outline text="The Doree Chronicles" description="" title="The Doree Chronicles" type="rss" version="RSS" htmlUrl="http://doree.tumblr.com/" xmlUrl="http://doree.tumblr.com/rss"/>
<outline text="the impossible cool." description="" title="the impossible cool." type="rss" version="RSS" htmlUrl="http://theimpossiblecool.tumblr.com/" xmlUrl="http://theimpossiblecool.tumblr.com/rss"/>
<outline text="kottke.org" description="" title="kottke.org" type="rss" version="RSS" htmlUrl="http://kottke.org/" xmlUrl="http://feeds.kottke.org/main"/>
<outline text="Philip Greenspun's Weblog" description="" title="Philip Greenspun's Weblog" type="rss" version="RSS" htmlUrl="http://blogs.law.harvard.edu/philg" xmlUrl="http://blogs.law.harvard.edu/philg/feed/"/>
<outline text="Rands In Repose" description="" title="Rands In Repose" type="rss" version="RSS" htmlUrl="http://www.randsinrepose.com/" xmlUrl="http://www.randsinrepose.com/index.xml"/>
<outline text="the selby - photos in your space" description="" title="the selby - photos in your space" type="rss" version="RSS" htmlUrl="http://www.theselby.com" xmlUrl="http://feeds2.feedburner.com/Theselby-PhotosInYourSpace?format=xml"/>
<outline text="The Storybird blog" description="" title="The Storybird blog" type="rss" version="RSS" htmlUrl="http://blog.storybird.com" xmlUrl="http://feeds.feedburner.com/TheStorybirdBlog?format=xml"/>
<outline text="You are what you share" description="" title="You are what you share" type="rss" version="RSS" htmlUrl="http://markury.com/" xmlUrl="http://markury.com/rss"/>
<outline text="Bird feed" description="" title="Bird feed" type="rss" version="RSS" htmlUrl="http://storybird.tumblr.com/" xmlUrl="http://storybird.tumblr.com/rss"/>
<outline text="one day at a time" description="" title="one day at a time" type="rss" version="RSS" htmlUrl="http://abangupjob.tumblr.com/" xmlUrl="http://abangupjob.tumblr.com/rss"/>
<outline text="Pretty in the City • Karyn Bosnak" description="" title="Pretty in the City • Karyn Bosnak" type="rss" version="RSS" htmlUrl="http://www.prettyinthecity.com/blog/" xmlUrl="http://www.prettyinthecity.com/blog/rss.xml"/>
<outline text="An Entirely Other Day: Full Feed" description="" title="An Entirely Other Day: Full Feed" type="rss" version="RSS" htmlUrl="http://www.eod.com/blog/" xmlUrl="http://feeds.feedburner.com/eod_full"/>
<outline text="hipster puppies" description="" title="hipster puppies" type="rss" version="RSS" htmlUrl="http://hipsterpuppies.tumblr.com/" xmlUrl="http://hipsterpuppies.tumblr.com/rss"/>
<outline text="The Bloglets" title="The Bloglets">
<outline text="FAIL Blog: Pictures and Videos of Owned, Pwnd and Fail Moments" description="" title="FAIL Blog: Pictures and Videos of Owned, Pwnd and Fail Moments" type="rss" version="RSS" htmlUrl="http://failblog.org" xmlUrl="http://feeds.feedburner.com/failblog"/>
<outline text="Iconic Photos" description="" title="Iconic Photos" type="rss" version="RSS" htmlUrl="http://iconicphotos.wordpress.com" xmlUrl="http://iconicphotos.wordpress.com/feed/"/>
<outline text="nerdboyfriend" description="" title="nerdboyfriend" type="rss" version="RSS" htmlUrl="http://nerdboyfriend.com" xmlUrl="http://nerdboyfriend.com/?feed=rss2"/>
<outline text="The Cellar - Image of the Day" description="" title="The Cellar - Image of the Day" type="rss" version="RSS" htmlUrl="http://cellar.org" xmlUrl="http://cellar.org/external.php?type=rss2&amp;forumids=10"/>
<outline text="The Sartorialist" description="" title="The Sartorialist" type="rss" version="RSS" htmlUrl="http://thesartorialist.blogspot.com/" xmlUrl="http://thesartorialist.blogspot.com/feeds/posts/default?alt=rss"/>
</outline>
</outline>
<outline text="Cooking" title="Cooking">
<outline text="Easy Recipes For Everyday Cooking by Savory Sweet Life" description="" title="Easy Recipes For Everyday Cooking by Savory Sweet Life" type="rss" version="RSS" htmlUrl="http://savorysweetlife.com" xmlUrl="http://savorysweetlife.com/?feed=rss2"/>
<outline text="SALAD &amp; CANDY" description="" title="SALAD &amp; CANDY" type="rss" version="RSS" htmlUrl="http://saladandcandy.com/" xmlUrl="http://saladandcandy.com/rss"/>
<outline text="smitten kitchen" description="" title="smitten kitchen" type="rss" version="RSS" htmlUrl="http://smittenkitchen.com" xmlUrl="http://feeds.feedburner.com/smittenkitchen"/>
<outline text="Salt &amp; Fat" description="" title="Salt &amp; Fat" type="rss" version="RSS" htmlUrl="http://saltandfat.com/" xmlUrl="http://saltandfat.com/rss"/>
</outline>
<outline text="Samuel Clay's OfBrooklyn.com" description="" title="Samuel Clay's OfBrooklyn.com" type="rss" version="RSS" htmlUrl="http://conesus.com" xmlUrl="http://www.ofbrooklyn.com/feeds/all/"/>
</body>
</opml>"""
user = User.objects.get(username='conesus')
opml_importer = OPMLImporter(opml_string, user)
data = opml_importer.process()
print data

View file

@ -165,15 +165,11 @@ class UserStory(models.Model):
class UserSubscriptionFolders(models.Model): class UserSubscriptionFolders(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
user_sub = models.ForeignKey(UserSubscription) folders = models.TextField(default="{}")
feed = models.ForeignKey(Feed)
folder = models.CharField(max_length=255)
def __unicode__(self): def __unicode__(self):
return ('[' + self.feed.feed_title + '] ' return "[%s]: %s" % (self.user, len(self.folders),)
+ self.folder)
class Meta: class Meta:
verbose_name_plural = "folders" verbose_name_plural = "folders"
verbose_name = "folder" verbose_name = "folder"
unique_together = ("user", "user_sub")

View file

@ -57,40 +57,23 @@ def logout(request):
def load_feeds(request): def load_feeds(request):
user = get_user(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( for sub in user_subs:
user=user feeds[sub.feed.pk] = {
) 'id': sub.feed.pk,
# logging.info('UserSubs: %s' % us) 'feed_title': sub.feed.feed_title,
feeds = [] 'feed_link': sub.feed.feed_link,
folders = [] 'unread_count_positive': sub.unread_count_positive,
for sub in us: 'unread_count_neutral': sub.unread_count_neutral,
try: 'unread_count_negative': sub.unread_count_negative,
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)
# Alphabetize folders, then feeds inside folders data = dict(feeds=feeds, folders=json.decode(folders.folders))
feeds.sort(lambda x, y: cmp(x['folder'].lower(), y['folder'].lower())) return HttpResponse(json.encode(data), mimetype='application/json')
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')
def load_single_feed(request): def load_single_feed(request):
user = get_user(request) user = get_user(request)

View file

@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
from apps.rss_feeds.models import Feed, Story from apps.rss_feeds.models import Feed, Story
from django.core.cache import cache 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 from optparse import OptionParser, make_option
import os import os
import logging import logging

View file

@ -3,7 +3,7 @@ from django.core.handlers.wsgi import WSGIHandler
from apps.rss_feeds.models import Feed, Story from apps.rss_feeds.models import Feed, Story
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Q 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 optparse import OptionParser, make_option
from utils.management_functions import daemonize from utils.management_functions import daemonize
import os import os

View file

@ -164,12 +164,16 @@ class Feed(models.Model):
user_stories_count = 0 user_stories_count = 0
stories = Story.objects.filter(story_feed=self).order_by('-story_date') stories = Story.objects.filter(story_feed=self).order_by('-story_date')
print 'Found %s stories in %s. Trimming...' % (stories.count(), self) print 'Found %s stories in %s. Trimming...' % (stories.count(), self)
for story in stories[1000:]: if stories.count() > 1000:
user_stories = UserStory.objects.filter(story=story) 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_count = user_stories.count()
user_stories.delete() user_stories.delete()
story.delete() old_stories = Story.objects.filter(story_feed=self,
stories_deleted_count += 1 story_date__lte=old_story.story_date)
stories_deleted_count = old_stories.count()
old_stories.delete()
if stories_deleted_count: if stories_deleted_count:
print "Trimming %s stories from %s. %s user stories." % ( print "Trimming %s stories from %s. %s user stories." % (

View file

@ -135,6 +135,10 @@ a img {
text-transform: uppercase; text-transform: uppercase;
} }
#feed_list .feeds {
margin-left: 22px;
}
#feed_list .feed { #feed_list .feed {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@ -149,11 +153,11 @@ a img {
#feed_list img.feed_favicon { #feed_list img.feed_favicon {
position: absolute; position: absolute;
top: 2px; top: 2px;
left: 24px; left: 2px;
} }
#feed_list .feed_title { #feed_list .feed_title {
display: block; display: block;
padding: 4px 42px 2px 45px; padding: 4px 42px 2px 23px;
text-decoration: none; text-decoration: none;
color: #272727; color: #272727;
line-height: 1.3em; line-height: 1.3em;

127
media/js/jquery.tinysort.js Normal file
View file

@ -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)*(x<y?-1:(x>y?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&&i<iLow+oParent.s.length;
if (contains(aOrg,i)) bSList = true;
var mEl = (bSList?oParent.s:oParent.n)[aCnt[bSList?0:1]].e;
mEl.parent().append(mEl);
if (bSList||!oSettings.returns) aNewOrder.push(mEl.get(0));
aCnt[bSList?0:1]++;
}
}
//
return this.setArray(aNewOrder); // setArray or pushStack?
}
});
// is numeric
function isNum(n) {
var x = /^\s*?[\+-]?(\d*\.?\d*?)\s*?$/.exec(n);
return x&&x.length>0?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);

View file

@ -25,6 +25,7 @@ NEWSBLUR.AssetModel = function() {
NEWSBLUR.AssetModel.Reader = function() { NEWSBLUR.AssetModel.Reader = function() {
this.feeds = {}; this.feeds = {};
this.folders = [];
this.stories = {}; this.stories = {};
}; };
@ -135,9 +136,10 @@ NEWSBLUR.AssetModel.Reader.prototype = {
load_feeds: function(callback) { load_feeds: function(callback) {
var self = this; var self = this;
var pre_callback = function(folders) { var pre_callback = function(subscriptions) {
self.folders = folders; self.feeds = subscriptions.feeds;
callback(folders); self.folders = subscriptions.folders;
callback();
}; };
this.make_request('/reader/load_feeds', {}, pre_callback); this.make_request('/reader/load_feeds', {}, pre_callback);
@ -182,15 +184,8 @@ NEWSBLUR.AssetModel.Reader.prototype = {
get_feed: function(feed_id, callback) { get_feed: function(feed_id, callback) {
var self = this; var self = this;
for (fld in this.folders) {
var feeds = this.folders[fld].feeds; return this.feeds[feed_id];
for (f in feeds) {
if (feeds[f].id == feed_id) {
return feeds[f];
}
}
}
return null;
}, },
get_feed_tags: function() { get_feed_tags: function() {

View file

@ -334,63 +334,85 @@
load_feeds: function() { load_feeds: function() {
var self = this; 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) { 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 = // = Story Titles Pane =
// ===================== // =====================

View file

@ -108,7 +108,8 @@ elif DEV_SERVER1:
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
DEBUG = True DEBUG = True
# CACHE_BACKEND = 'locmem:///' # 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, logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(message)s', format='%(asctime)s %(levelname)s %(message)s',
filename=LOG_FILE, filename=LOG_FILE,
@ -181,6 +182,7 @@ COMPRESS_JS = {
'js/jquery.ui.core.js', 'js/jquery.ui.core.js',
'js/jquery.ui.slider.js', 'js/jquery.ui.slider.js',
'js/jquery.layout.js', 'js/jquery.layout.js',
'js/jquery.tinysort.js',
'js/jquery.fieldselection.js', 'js/jquery.fieldselection.js',
'js/newsblur/assetmodel.js', 'js/newsblur/assetmodel.js',
@ -267,5 +269,16 @@ INSTALLED_APPS = (
'apps.registration', 'apps.registration',
'apps.opml_import', 'apps.opml_import',
'apps.profile', 'apps.profile',
'devserver',
# 'debug_toolbar' # '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',
)

View file

@ -1,6 +1,6 @@
from apps.rss_feeds.models import Story from apps.rss_feeds.models import Story
from django.core.cache import cache 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 apps.rss_feeds.importer import PageImporter
from utils import feedparser, threadpool from utils import feedparser, threadpool
from django.db import transaction from django.db import transaction