Crazy refactor and optimization for choosing and removing classifiers. Now feeds and stories use the same modal, and everything uses the same classifier save on the back-end.

This commit is contained in:
Samuel Clay 2010-04-03 20:32:40 -04:00
parent b34972a1bd
commit b6495eeab4
4 changed files with 258 additions and 208 deletions

View file

@ -3,7 +3,7 @@ from apps.analyzer import views
urlpatterns = patterns('',
(r'^$', views.index),
(r'^save/story/?', views.save_classifier_story),
(r'^save/publisher/?', views.save_classifier_publisher),
(r'^save/story/?', views.save_classifier),
(r'^save/publisher/?', views.save_classifier),
(r'^get/publisher/?', views.get_classifiers_feed),
)

View file

@ -23,86 +23,20 @@ def index(requst):
@require_POST
@json.json_view
def save_classifier_story(request):
post = request.POST
facets = post.getlist('facet')
code = 0
message = 'OK'
payload = {}
feed = Feed.objects.get(pk=post['feed_id'])
story = Story.objects.get(pk=post['story_id'])
score = int(post['score'])
if 'title' in post and 'title' in facets:
classifier_title, created = ClassifierTitle.objects.get_or_create(
user=request.user,
title=post['title'],
feed=feed,
defaults={
'original_story': story,
'score': score,
})
if not created:
classifier_title.score = score
classifier_title.save()
if 'author' in facets:
author = story.story_author
classifier_author, created = ClassifierAuthor.objects.get_or_create(
user=request.user,
author=author,
feed=feed,
defaults={
'original_story': story,
'score': score,
})
if not created:
classifier_author.score = score
classifier_author.save()
if 'publisher' in facets:
classifier_feed, created = ClassifierFeed.objects.get_or_create(
user=request.user,
feed=feed,
defaults={
'original_story': story,
'score': score,
})
if not created:
classifier_feed.score = score
classifier_feed.save()
if 'tag' in post:
tags = post.getlist('tag')
for tag_name in tags:
tag = Tag.objects.get(name=tag_name, feed=feed)
classifier_tag, created = ClassifierTag.objects.get_or_create(
user=request.user,
tag=tag,
feed=feed,
defaults={
'original_story': story,
'score': score,
})
print classifier_tag, classifier_tag.score, created, score
if not created:
classifier_tag.score = score
classifier_tag.save()
response = dict(code=code, message=message, payload=payload)
return response
@require_POST
@json.json_view
def save_classifier_publisher(request):
def save_classifier(request):
post = request.POST
feed = Feed.objects.get(pk=post['feed_id'])
story = None
if 'story_id' in post and post['story_id']:
story_id = int(post['story_id'])
if story_id:
story = Story.objects.get(pk=story_id)
code = 0
message = 'OK'
payload = {}
def _save_classifier(ClassifierCls, content_type, ContentCls=None, post_content_field=None):
for opinion, score in {'like_'+content_type: 1, 'dislike_'+content_type: -2}.items():
for opinion, score in {'like_'+content_type: 1, 'dislike_'+content_type: -1}.items():
if opinion in post:
post_contents = post.getlist(opinion)
for post_content in post_contents:
@ -113,6 +47,8 @@ def save_classifier_publisher(request):
'score': score
}
}
if story:
classifier_dict['defaults'].update(original_story=story)
if content_type in ('author', 'tag'):
# Use content to lookup object. Authors, Tags.
content_dict = {
@ -125,7 +61,7 @@ def save_classifier_publisher(request):
# Skip content lookup and just use content directly. Titles.
classifier_dict.update({content_type: post_content})
classifier, _ = ClassifierCls.objects.get_or_create(**classifier_dict)
classifier, created = ClassifierCls.objects.get_or_create(**classifier_dict)
if classifier.score != score:
classifier.score = score
classifier.save()

View file

@ -1064,6 +1064,15 @@ form.opml_import_form input {
color: #A0A0A0;
}
.NB-classifiers .NB-classifier.NB-classifier-title.NB-classifier-facet-disabled label {
color: #A0A0A0;
text-shadow: none;
}
.NB-classifier .NB-classifier-title-highlight {
margin-bottom: 6px;
}
.NB-classifier-title-display {
margin: 6px 0 0 0;
}
@ -1073,7 +1082,9 @@ form.opml_import_form input {
.NB-classifier .NB-publisher .NB-classifier-authors .NB-classifier-author {
float: left;
margin: 4px 16px 0 0;
white-space: nowrap;
margin: 2px 6px 6px 0;
display: block;
}
.NB-classifier .NB-publisher .NB-classifier-authors input {

View file

@ -56,64 +56,30 @@ var classifier = {
make_modal_feed: function() {
var self = this;
var feed = this.feed;
var $feed_authors = [];
var $feed_tags = [];
if (this.feed_authors) {
for (var fa in this.feed_authors) {
var feed_author = this.feed_authors[fa];
if (feed_author[0]) {
var $author = $.make('span', { className: 'NB-classifier-author NB-classifier' }, [
$.make('input', { type: 'checkbox', name: 'author', value: feed_author[0], id: 'classifier_author_'+fa }),
$.make('label', { 'for': 'classifier_author_'+fa }, feed_author[0])
]);
$feed_authors.push($author);
}
}
}
if (this.feed_tags) {
for (var t in this.feed_tags) {
var tag = this.feed_tags[t];
var checked = (tag[0] in this.model.classifiers.tags) ? 'checked' : 'false';
var $tag = $.make('span', { className: 'NB-classifier-tag-container' }, [
$.make('span', { className: 'NB-classifier-tag NB-classifier' }, [
$.make('input', { type: 'checkbox', name: 'tag', value: tag[0], id: 'classifier_tag_'+t, checked: checked }),
$.make('label', { 'for': 'classifier_tag_'+t }, [
tag[0]
])
]),
$.make('span', { className: 'NB-classifier-tag-count' }, [
'× ',
tag[1]
])
]);
$feed_tags.push($tag);
}
}
var opinion = (this.score == 1 ? 'like_' : 'dislike_');
NEWSBLUR.log(['Make feed', feed, this.feed_authors, this.feed_tags]);
this.$classifier = $.make('div', { className: 'NB-classifier NB-modal' }, [
$.make('h2', { className: 'NB-modal-title' }),
$.make('form', { method: 'post', className: 'NB-publisher' }, [
($feed_authors.length && $.make('div', { className: 'NB-modal-field NB-classifiers' }, [
(this.feed_authors.length && $.make('div', { className: 'NB-modal-field NB-classifiers' }, [
$.make('h5', 'Authors'),
$.make('div', { className: 'NB-classifier-authors NB-classifiers' }, $feed_authors)
$.make('div', { className: 'NB-classifier-authors NB-classifiers' },
this.make_authors(this.feed_authors, opinion)
)
])),
($feed_tags.length && $.make('div', { className: 'NB-modal-field' }, [
(this.feed_tags.length && $.make('div', { className: 'NB-modal-field' }, [
$.make('h5', 'Categories & Tags'),
$.make('div', { className: 'NB-classifier-tags NB-classifiers' }, $feed_tags)
$.make('div', { className: 'NB-classifier-tags NB-classifiers' },
this.make_tags(this.feed_tags, opinion)
)
])),
$.make('div', { className: 'NB-modal-field NB-classifiers' }, [
$.make('h5', 'Everything by This Publisher'),
$.make('div', { className: 'NB-classifiers' }, [
$.make('div', { className: 'NB-classifier NB-classifier-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-classifiers' },
this.make_publisher(feed, opinion)
)
]),
$.make('div', { className: 'NB-modal-submit' }, [
$.make('input', { name: 'score', value: this.score, type: 'hidden' }),
@ -142,35 +108,12 @@ var classifier = {
var self = this;
var story = this.story;
var feed = this.feed;
var $story_tags = [];
var $story_author;
var opinion = (this.score == 1 ? 'like_' : 'dislike_');
NEWSBLUR.log(['Make Story', story, feed]);
// HTML entities decoding.
story.story_title = $('<div/>').html(story.story_title).text();
for (var t in story.story_tags) {
var tag = story.story_tags[t];
var input_attrs = { type: 'checkbox', name: 'tag', value: tag, id: 'classifier_tag_'+t };
NEWSBLUR.log(['input_attrs', input_attrs, tag, this.model.classifiers.tags]);
if (tag in this.model.classifiers.tags && this.model.classifiers.tags[tag] == this.score) {
input_attrs['checked'] = 'checked';
}
var $tag = $.make('span', { className: 'NB-classifier-tag-container NB-classifier NB-classifier-tag' }, [
$.make('input', input_attrs),
$.make('label', { 'for': 'classifier_tag_'+t }, [
tag
])
]);
$story_tags.push($tag);
}
if (story.story_authors) {
var input_attrs = { type: 'checkbox', name: 'author', value: story.story_authors, id: 'classifier_author' };
if (story.story_authors in this.model.classifiers.authors && this.model.classifiers.authors[story.story_authors] == this.score) {
input_attrs['checked'] = 'checked';
}
$story_author = $.make('input', input_attrs);
}
this.$classifier = $.make('div', { className: 'NB-classifier NB-modal' }, [
$.make('h2', { className: 'NB-modal-title' }),
@ -178,43 +121,33 @@ var classifier = {
(story.story_title && $.make('div', { className: 'NB-modal-field' }, [
$.make('h5', 'Story Title'),
$.make('div', { className: 'NB-classifiers' }, [
$.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', { className: 'NB-classifier-title-display' }, [
'Look for: ',
$.make('span', { className: 'NB-classifier-title NB-classifier-facet-disabled' }, 'Highlight phrases to look for in future stories'),
$.make('input', { name: 'title', value: '', type: 'hidden', className: 'NB-classifier-title-hidden' })
$.make('div', { className: 'NB-classifier NB-classifier-title NB-classifier-facet-disabled' }, [
$.make('input', { type: 'checkbox', name: opinion+'title', value: '', id: 'classifier_title' }),
$.make('label', { 'for': 'classifier_title' }, [
$.make('b', 'Look for: '),
$.make('span', { className: 'NB-classifier-title-text' }, 'Highlight phrases to look for in future stories')
])
])
])
])),
(story.story_authors && $.make('div', { className: 'NB-modal-field' }, [
$.make('h5', 'Story Author'),
$.make('div', { className: 'NB-classifiers' }, [
$.make('div', { className: 'NB-classifier NB-classifier-author' }, [
$story_author,
$.make('label', { 'for': 'classifier_author' }, [
story.story_authors
])
])
])
$.make('div', { className: 'NB-classifiers' },
this.make_authors([story.story_authors], opinion)
)
])),
($story_tags.length && $.make('div', { className: 'NB-modal-field' }, [
(story.story_tags.length && $.make('div', { className: 'NB-modal-field' }, [
$.make('h5', 'Story Categories &amp; Tags'),
$.make('div', { className: 'NB-classifier-tags NB-classifiers' }, $story_tags)
$.make('div', { className: 'NB-classifier-tags NB-classifiers' },
this.make_tags(story.story_tags, opinion)
)
])),
$.make('div', { className: 'NB-modal-field' }, [
$.make('h5', 'Everything by This Publisher'),
$.make('div', { className: 'NB-classifiers' }, [
$.make('div', { className: 'NB-classifier NB-classifier-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-classifiers' },
this.make_publisher(feed, opinion)
)
]),
$.make('div', { className: 'NB-modal-submit' }, [
$.make('input', { name: 'score', value: this.score, type: 'hidden' }),
@ -239,10 +172,140 @@ var classifier = {
}
},
make_authors: function(authors, opinion) {
var $authors = [];
for (var a in authors) {
var author_obj = authors[a];
if (typeof author_obj == 'string') {
var author = author_obj;
var author_count;
} else {
var author = author_obj[0];
var author_count = author_obj[1];
}
if (!author) continue;
var input_attrs = {
type: 'checkbox',
name: opinion+'author',
value: author,
id: 'classifier_author_'+a
};
if (author in this.model.classifiers.authors
&& this.model.classifiers.authors[author] == this.score) {
input_attrs['checked'] = 'checked';
}
var $author = $.make('span', { className: 'NB-classifier-author-container' }, [
$.make('span', { className: 'NB-classifier NB-classifier-author' }, [
$.make('input', input_attrs),
$.make('label', { 'for': 'classifier_author_'+a }, [
$.make('b', 'Author: '),
$.make('span', author)
])
]),
(author_count && $.make('span', { className: 'NB-classifier-tag-count' }, [
'&times;&nbsp;',
author_count
]))
]);
$authors.push($author);
}
return $authors;
},
make_tags: function(tags, opinion) {
var $tags = [];
for (var t in tags) {
var tag_obj = tags[t];
if (typeof tag_obj == 'string') {
var tag = tag_obj;
var tag_count;
} else {
var tag = tag_obj[0];
var tag_count = tag_obj[1];
}
if (!tag) continue;
var input_attrs = {
type: 'checkbox',
name: opinion+'tag',
value: tag,
id: 'classifier_tag_'+t
};
if (tag in this.model.classifiers.tags && this.model.classifiers.tags[tag] == this.score) {
input_attrs['checked'] = 'checked';
}
var $tag = $.make('span', { className: 'NB-classifier-tag-container' }, [
$.make('span', { className: 'NB-classifier NB-classifier-tag' }, [
$.make('input', input_attrs),
$.make('label', { 'for': 'classifier_tag_'+t }, [
$.make('b', 'Tag: '),
$.make('span', tag)
])
]),
(tag_count && $.make('span', { className: 'NB-classifier-tag-count' }, [
'&times;&nbsp;',
tag_count
]))
]);
$tags.push($tag);
}
return $tags;
},
make_publisher: function(publisher, opinion) {
var input_attrs = {
type: 'checkbox',
name: opinion+'publisher',
value: this.feed_id,
id: 'classifier_publisher',
checked: false
};
if (this.feed.feed_link in this.model.classifiers.feeds
&& this.model.classifiers.feeds[this.feed.feed_link].score == this.score) {
input_attrs['checked'] = true;
}
var $publisher = $.make('div', { className: 'NB-classifier NB-classifier-publisher' }, [
$.make('input', input_attrs),
$.make('label', { 'for': 'classifier_publisher' }, [
$.make('img', { className: 'feed_favicon', src: this.google_favicon_url + publisher.feed_link }),
$.make('span', { className: 'feed_title' }, [
$.make('b', 'Publisher: '),
$.make('span', publisher.feed_title)
])
])
]);
return $publisher;
},
make_title: function(title, t, opinion) {
var $title = $.make('div', { className: 'NB-classifier NB-classifier-title' }, [
$.make('input', { type: 'checkbox', name: opinion+'title', value: title, id: 'classifier_title_'+t, checked: 'checked' }),
$.make('label', { 'for': 'classifier_title_'+t }, [
$.make('b', 'Title: '),
$.make('span', title)
])
]);
return $title;
},
open_modal: function() {
var self = this;
var $holder = $.make('div', { className: 'NB-modal-holder' }).append(this.$classifier).appendTo('body').css({'visibility': 'hidden', 'display': 'block', 'width': 600});
var $holder = $.make('div', { className: 'NB-modal-holder' })
.append(this.$classifier)
.appendTo('body')
.css({'visibility': 'hidden', 'display': 'block', 'width': 600});
var height = $('.NB-classifier', $holder).outerHeight(true);
$holder.css({'visibility': 'visible', 'display': 'none'});
@ -274,8 +337,7 @@ var classifier = {
handle_text_highlight: function() {
var $title_highlight = $('.NB-classifier-title-highlight', this.$classifier);
var $title = $('.NB-classifier-title', this.$classifier);
var $title_hidden = $('.NB-classifier-title-hidden', this.$classifier);
var $title = $('.NB-classifier-title-text', this.$classifier);
var $title_checkbox = $('#classifier_title', this.$classifier);
var update = function() {
@ -283,25 +345,30 @@ var classifier = {
if ($title.text() != text && text.length) {
$title_checkbox.attr('checked', 'checked').change();
$title.text(text).removeClass('NB-classifier-facet-disabled');
$title_hidden.val(text);
$title.text(text);
$title_checkbox.parents('.NB-classifier-facet-disabled')
.removeClass('NB-classifier-facet-disabled');
$title_checkbox.val(text);
}
};
$title_highlight.keydown(update).keyup(update).mousedown(update).mouseup(update).mousemove(update);
$title_highlight
.keydown(update).keyup(update)
.mousedown(update).mouseup(update).mousemove(update);
},
handle_select_title: function() {
var $title_checkbox = $('#classifier_title', this.$classifier);
var $title = $('.NB-classifier-title', this.$classifier);
var $title_hidden = $('.NB-classifier-title-hidden', this.$classifier);
var $title = $('.NB-classifier-title-text', this.$classifier);
var $title_highlight = $('.NB-classifier-title-highlight', this.$classifier);
$title_checkbox.change(function() {;
if ($title.hasClass('NB-classifier-facet-disabled')) {
if ($title.parents('.NB-classifier-facet-disabled').length) {
var text = $title_highlight.val();
$title.text(text).removeClass('NB-classifier-facet-disabled');
$title_hidden.val(text);
$title.text(text);
$title_checkbox.parents('.NB-classifier-facet-disabled')
.removeClass('NB-classifier-facet-disabled');
$title_checkbox.val(text);
}
});
},
@ -331,22 +398,31 @@ var classifier = {
},
serialize_classifier: function() {
var data = $('.NB-classifier form input').serialize();
var checked_data = $('input', this.$classifier).serialize();
var $unchecked = $('input[type=checkbox]:not(:checked)', this.$classifier);
$unchecked.attr('checked', true);
$unchecked.each(function() {
$(this).attr('name', 'remove_' + $(this).attr('name'));
});
var unchecked_data = $unchecked.serialize();
$unchecked.each(function() {
$(this).attr('name', $(this).attr('name').replace(/^remove_/, ''));
});
$unchecked.attr('checked', false);
var data = [checked_data, unchecked_data].join('&');
return data;
},
serialize_classifier_array: function() {
var data = $('.NB-classifier form input').serializeArray();
return data;
},
save_publisher: function() {
var $save = $('.NB-classifier input[type=submit]');
var story_id = this.story_id;
var data = this.serialize_classifier();
this.update_opinions();
$save.text('Saving...').addClass('NB-disabled').attr('disabled', true);
this.model.save_classifier_publisher(data, function() {
$.modal.close();
@ -357,26 +433,53 @@ var classifier = {
var $save = $('.NB-classifier input[type=submit]');
var story_id = this.story_id;
var data = this.serialize_classifier();
var classifiers = this.serialize_classifier_array();
for (var c in classifiers) {
var classifier = classifiers[c];
if (!classifier) continue;
if (classifier['name'] == 'tag') {
this.model.classifiers.tags[classifier['value']] = this.score;
} else if (classifier['name'] == 'title') {
this.model.classifiers.titles[classifier['value']] = this.score;
} else if (classifier['name'] == 'author') {
this.model.classifiers.authors[classifier['value']] = this.score;
} else if (classifier['name'] == 'facet' && classifier['value'] == 'publisher') {
this.model.classifiers.feeds[this.feed_id] = this.score;
}
}
this.update_opinions();
$save.text('Saving...').addClass('NB-disabled').attr('disabled', true);
this.model.save_classifier_story(story_id, data, function() {
$.modal.close();
});
},
update_opinions: function() {
var self = this;
$('input[type=checkbox]', this.$classifier).each(function() {
var name = $(this).attr('name').replace(/^(dis)?like_/, '');
var value = $(this).val();
var checked = $(this).attr('checked');
NEWSBLUR.log(['update_opinions', name, value, checked]);
if (checked) {
if (name == 'tag') {
self.model.classifiers.tags[value] = self.score;
} else if (name == 'title') {
self.model.classifiers.titles[value] = self.score;
} else if (name == 'author') {
self.model.classifiers.authors[value] = self.score;
} else if (name == 'publisher') {
self.model.classifiers.feeds[self.feed.feed_link] = {
'feed_link': self.feed.feed_link,
'feed_title': self.feed.feed_title,
'score': self.score
};
}
} else {
if (name == 'tag' && self.model.classifiers.tags[value] == self.score) {
delete self.model.classifiers.tags[value];
} else if (name == 'title' && self.model.classifiers.titles[value] == self.score) {
delete self.model.classifiers.titles[value];
} else if (name == 'author' && self.model.classifiers.authors[value] == self.score) {
delete self.model.classifiers.authors[value];
} else if (name == 'publisher'
&& self.model.classifiers.feeds[self.feed.feed_link]
&& self.model.classifiers.feeds[self.feed.feed_link].score == self.score) {
delete self.model.classifiers.feeds[self.feed.feed_link];
}
}
});
}
};