diff --git a/apps/rss_feeds/models.py b/apps/rss_feeds/models.py index 60dfe5d9e..3f6b911ab 100644 --- a/apps/rss_feeds/models.py +++ b/apps/rss_feeds/models.py @@ -2,6 +2,7 @@ from django.db import models from django.db import IntegrityError from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType +from django.core import serializers from django.core.cache import cache from utils import feedparser, object_manager from utils.dateutil.parser import parse as dateutil_parse @@ -157,16 +158,16 @@ class Feed(models.Model): story.save() def get_stories(self, offset=0, limit=25): - stories = cache.get('feed_stories:%s-%s-%s' % (self.id, offset, limit)) + stories = cache.get('feed_stories:%s-%s-%s' % (self.id, offset, limit), []) - if stories is None: - stories = Story.objects.filter(story_feed=self).values()[offset:offset+limit] - for story in stories: + if not stories: + stories_db = Story.objects.filter(story_feed=self).select_related('story_author')[offset:offset+limit] + for story_db in stories_db: + story = story_db.__dict__ story['short_parsed_date'] = format_story_link_date__short(story['story_date']) story['long_parsed_date'] = format_story_link_date__long(story['story_date']) - story['story_feed_title'] = self.feed_title - story['story_feed_link'] = mark_safe(self.feed_link) - story['story_permalink'] = mark_safe(story['story_permalink']) + story['story_authors'] = story_db.story_author.author_name + stories.append(story) cache.set('feed_stories:%s-%s-%s' % (self.id, offset, limit), stories) return stories @@ -238,6 +239,9 @@ class StoryAuthor(models.Model): def __unicode__(self): return '%s - %s' % (self.feed, self.author_name) + + def natural_keys(self): + return (self.author_name,) class Story(models.Model): '''A feed item''' diff --git a/media/css/reader.css b/media/css/reader.css index 5ed2e5bbc..bf23f930a 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -664,4 +664,77 @@ form.opml_import_form input { height: 376px; width: 600px; background: transparent url('../img/reader/newsblur_splash_image.png') no-repeat 0 0; +} + +/* ============== */ +/* = Classifier = */ +/* ============== */ + +.NB-classifier { + padding: 12px; +} + +.NB-classifier h2 { + margin: 0; + padding: 0; + font-size: 130%; +} + +.NB-classifier h2.NB-like { + color: #007000; +} + +.NB-classifier h2.NB-dislike { + color: #700000; +} + +.NB-classifier h5 { + border-bottom: 1px solid #A0A0A0; + padding: 16px 0 0px; + margin: 0 0 6px -24px; + color: #808080; + text-transform: uppercase; + font-weight: normal; + font-size: 70%; +} + +.NB-classifier .NB-classifier-field { + margin: 0 0 0 24px; + position: relative; +} + +.NB-classifier .NB-classifier-field input[type=text] { + width: 446px; + font-size: 14px; +} + +.NB-classifier .NB-classifier-field input[type=checkbox] { + margin: 5px 0 0 -20px; + float: left; +} + +.NB-classifier .NB-classifier-title { + font-weight: bold; +} + +.NB-classifier .NB-classifier-facet-disabled { + color: #A0A0A0; +} + +.NB-disabled { + color: #A0A0A0; +} + +.NB-classifier .NB-classifier-submit { + margin: 16px 0 0 0; + font-size: 13px; +} + +.NB-classifier .NB-classifier-submit input[type=submit] { + font-size: 14px; +} + +.NB-classifier img.feed_favicon { + margin: 0 6px 0 0; + vertical-align: middle; } \ No newline at end of file diff --git a/media/js/jquery.fieldselection.js b/media/js/jquery.fieldselection.js new file mode 100644 index 000000000..92fb65622 --- /dev/null +++ b/media/js/jquery.fieldselection.js @@ -0,0 +1,83 @@ +/* + * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16 + * (c) 2006 Alex Brem - http://blog.0xab.cd + */ + +(function() { + + var fieldSelection = { + + getSelection: function() { + + var e = this.jquery ? this[0] : this; + + return ( + + /* mozilla / dom 3.0 */ + ('selectionStart' in e && function() { + var l = e.selectionEnd - e.selectionStart; + return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) }; + }) || + + /* exploder */ + (document.selection && function() { + + e.focus(); + + var r = document.selection.createRange(); + if (r == null) { + return { start: 0, end: e.value.length, length: 0 } + } + + var re = e.createTextRange(); + var rc = re.duplicate(); + re.moveToBookmark(r.getBookmark()); + rc.setEndPoint('EndToStart', re); + + return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text }; + }) || + + /* browser not supported */ + function() { + return { start: 0, end: e.value.length, length: 0 }; + } + + )(); + + }, + + replaceSelection: function() { + + var e = this.jquery ? this[0] : this; + var text = arguments[0] || ''; + + return ( + + /* mozilla / dom 3.0 */ + ('selectionStart' in e && function() { + e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length); + return this; + }) || + + /* exploder */ + (document.selection && function() { + e.focus(); + document.selection.createRange().text = text; + return this; + }) || + + /* browser not supported */ + function() { + e.value += text; + return this; + } + + )(); + + } + + }; + + jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; }); + +})(); \ No newline at end of file diff --git a/media/js/newsblur/reader.js b/media/js/newsblur/reader.js index 53d83c284..b0bd2f60d 100644 --- a/media/js/newsblur/reader.js +++ b/media/js/newsblur/reader.js @@ -42,7 +42,7 @@ var $story_title = $.make('div', { className: 'story' + read }, [ $.make('a', { href: story.story_permalink, className: 'story_title' }, [ story.story_title, - $.make('span', { className: 'NB-storytitles-author'}, story.story_author) + $.make('span', { className: 'NB-storytitles-author'}, story.story_authors) ]), $.make('span', { className: 'story_date' }, story.short_parsed_date), $.make('span', { className: 'story_id' }, ''+story.id), @@ -388,8 +388,8 @@ var $story = $.make('li', { className: 'NB-feed-story' }, [ $.make('div', { className: 'NB-feed-story-header' }, [ - ( story.story_author && - $.make('div', { className: 'NB-feed-story-author' }, story.story_author)), + ( story.story_authors && + $.make('div', { className: 'NB-feed-story-author' }, story.story_authors)), $.make('a', { className: 'NB-feed-story-title', href: unescape(story.story_permalink) }, story.story_title), ( story.long_parsed_date && $.make('span', { className: 'NB-feed-story-date' }, story.long_parsed_date)) @@ -695,7 +695,7 @@ if (!$stories.length) { // Try slicing words off the title, from the beginning. title_words = title.match(/[^ ]+/g); - NEWSBLUR.log(['Words', title_words.length, title_words, title_words.slice(1).join(' '), title_words.slice(0, -1).join(' '), title_words.slice(1, -1).join(' ')]) + NEWSBLUR.log(['Words', title_words.length, title_words, title_words.slice(1).join(' '), title_words.slice(0, -1).join(' '), title_words.slice(1, -1).join(' ')]); if (title_words.length > 2) { for (var i=0; i < 3; i++) { if (i==0) shortened_title = title_words.slice(1).join(' '); @@ -814,16 +814,9 @@ }, mark_story_as_like: function(story_id, $button) { - var self = this; - - var callback = function() { - return; - }; - - $button.addClass('liked'); - if (NEWSBLUR.Globals.is_authenticated) { - this.model.mark_story_as_like(story_id, callback); - } + var feed_id = this.active_feed; + + var classifier = new NEWSBLUR.ReaderClassifier(story_id, feed_id); }, mark_story_as_dislike: function(story_id, $button) { diff --git a/media/js/newsblur/reader_classifier.js b/media/js/newsblur/reader_classifier.js new file mode 100644 index 000000000..d87ede0c4 --- /dev/null +++ b/media/js/newsblur/reader_classifier.js @@ -0,0 +1,137 @@ +NEWSBLUR.ReaderClassifier = function(story_id, feed_id, options) { + var defaults = {}; + + this.story_id = story_id; + this.feed_id = feed_id; + this.options = $.extend({}, defaults, options); + this.model = NEWSBLUR.AssetModel.reader(); + this.google_favicon_url = 'http://www.google.com/s2/favicons?domain_url='; + this.runner(); +}; + +NEWSBLUR.ReaderClassifier.prototype = { + + runner: function() { + this.find_story_and_feed(); + this.make_modal(); + this.handle_text_highlight(); + this.handle_select_checkboxes(); + this.handle_cancel(); + this.open_modal(); + }, + + find_story_and_feed: function() { + this.story = this.model.get_story(this.story_id); + this.feed = this.model.get_feed(this.feed_id); + }, + + make_modal: function() { + var self = this; + var story = this.story; + var feed = this.feed; + + this.$classifier = $.make('div', { className: 'NB-classifier' }, [ + $.make('h2', 'What do you like about this story?'), + $.make('form', { method: 'post' }, [ + (story.story_title && $.make('div', { className: 'NB-classifier-field' }, [ + $.make('h5', 'Story Title'), + $.make('input', { type: 'checkbox', name: 'facet', value: 'title', id: 'classifier_title' }), + $.make('input', { type: 'text', value: story.story_title, className: 'NB-classifier-title-highlight' }), + $.make('label', { 'for': 'classifier_title' }, [ + $.make('div', [ + 'Look for: ', + $.make('span', { className: 'NB-classifier-title NB-classifier-facet-disabled' }, 'Highlight phrases to look for in future stories') + ]) + ]) + ])), + (story.story_authors && $.make('div', { className: 'NB-classifier-field' }, [ + $.make('h5', 'Story Author'), + $.make('input', { type: 'checkbox', name: 'facet', value: 'author', id: 'classifier_author' }), + $.make('label', { 'for': 'classifier_author' }, [ + $.make('b', story.story_authors) + ]) + ])), + $.make('div', { className: 'NB-classifier-field' }, [ + $.make('h5', 'The Publisher'), + $.make('input', { type: 'checkbox', name: 'facet', value: 'publisher', id: 'classifier_publisher' }), + $.make('label', { 'for': 'classifier_publisher' }, [ + $.make('img', { className: 'feed_favicon', src: this.google_favicon_url + feed.feed_link }), + $.make('span', { className: 'feed_title' }, feed.feed_title) + ]) + ]), + $.make('div', { className: 'NB-classifier-submit' }, [ + $.make('input', { type: 'submit', disabled: 'true', className: 'NB-disabled', value: 'Check what you like above...' }), + ' or ', + $.make('a', { href: '#', className: 'NB-classifier-cancel' }, 'cancel') + ]) + ]).bind('submit', function(e) { + e.preventDefault(); + self.save(); + return false; + }) + ]); + }, + + open_modal: function() { + this.$classifier.modal({ + 'minWidth': 550, + 'overlayClose': true, + 'onOpen': function (dialog) { + dialog.overlay.fadeIn(200, function () { + dialog.container.fadeIn(400); + dialog.data.fadeIn(400); + }); + }, + 'onShow': function() { + $('#simplemodal-container').corners('4px') + .css({'height': 'auto', 'width': '550px'}); + } + }); + }, + + handle_text_highlight: function() { + var $title_highlight = $('.NB-classifier-title-highlight', this.$classifier); + var $title = $('.NB-classifier-title', this.$classifier); + var $title_checkbox = $('#classifier_title', this.$classifier); + + var update = function() { + var text = $.trim($(this).getSelection().text); + + if ($title.text() != text && text.length) { + $title_checkbox.attr('checked', 'checked').change(); + $title.text(text).removeClass('NB-classifier-facet-disabled'); + } + }; + + $title_highlight.keydown(update).keyup(update).mousedown(update).mouseup(update).mousemove(update); + }, + + handle_select_checkboxes: function() { + var self = this; + var $submit = $('input[type=submit]', this.$classifier); + + $('input', this.$classifier).change(function() { + var count = $('input:checked', self.$classifier).length; + + if (count) { + $submit.removeClass("NB-disabled").removeAttr('disabled').attr('value', 'Save'); + } else { + $submit.addClass("NB-disabled").attr('disabled', 'true').attr('value', 'Check what you like above...'); + } + }); + }, + + handle_cancel: function() { + var $cancel = $('.NB-classifier-cancel', this.$classifier); + + $cancel.click(function(e) { + e.preventDefault(); + $.modal.close(); + }); + }, + + save: function() { + + } + +}; \ No newline at end of file diff --git a/settings.py b/settings.py index 2b0a15d31..cbe4ce8ff 100644 --- a/settings.py +++ b/settings.py @@ -175,9 +175,11 @@ COMPRESS_JS = { 'js/jquery.color.js', 'js/jquery-ui-1.7.2.custom.min.js', 'js/jquery.layout.js', + 'js/jquery.fieldselection.js', 'js/newsblur/assetmodel.js', - 'js/newsblur/reader.js' + 'js/newsblur/reader.js', + 'js/newsblur/reader_classifier.js' ), 'output_filename': 'release/all-compressed-?.js' }