Add new 'recently visited' gadget

This commit is contained in:
ketralnis
2009-06-02 15:14:52 -07:00
parent e7eb9e63d7
commit db64707189
17 changed files with 271 additions and 113 deletions

View File

@@ -157,6 +157,8 @@ def make_map(global_conf={}, app_conf={}):
mc('/api/:action/:url_user', controller='api',
requirements=dict(action="login|register"))
mc('/api/gadget/click/:ids', controller = 'api', action='gadget', type='click')
mc('/api/gadget/:type', controller = 'api', action='gadget')
mc('/api/:action', controller='api')
mc('/captcha/:iden', controller='captcha', action='captchaimg')

View File

@@ -19,7 +19,7 @@
# All portions of the code written by CondeNet are Copyright (c) 2006-2009
# CondeNet, Inc. All Rights Reserved.
################################################################################
from reddit_base import RedditController
from reddit_base import RedditController, set_user_cookie
from pylons.i18n import _
from pylons import c, request
@@ -42,7 +42,7 @@ from r2.lib.menus import CommentSortMenu
from r2.lib.normalized_hot import expire_hot
from r2.lib.captcha import get_iden
from r2.lib.strings import strings
from r2.lib.filters import _force_unicode, websafe_json
from r2.lib.filters import _force_unicode, websafe_json, spaceCompress
from r2.lib.db import queries
from r2.lib.media import force_thumbnail, thumbnail_url
from r2.lib.comment_tree import add_comment, delete_comment
@@ -1359,6 +1359,30 @@ class ApiController(RedditController):
return UploadedImage(_('saved'), thumbnail_url(link), "",
errors = errors).render()
@validatedForm(type = VOneOf('type', ('click'), default = 'click'),
links = VByName('ids', thing_cls = Link, multiple = True))
def GET_gadget(self, form, jquery, type, links):
if not links and type == 'click':
# malformed cookie, clear it out
set_user_cookie('click', '')
if not links:
return
def wrapper(link):
link.embed_voting_style = 'votable'
return Wrapped(link)
#this will disable the hardcoded widget styles
request.get.style = "off"
c.render_style = 'htmllite'
builder = IDBuilder([ link._fullname for link in links ],
wrap = wrapper)
listing = LinkListing(builder, nextprev=False, show_nums=False).listing()
jquery('.gadget').show().find('.click-gadget').html(
spaceCompress(listing.render()))
@noresponse()
def POST_tb_commentspanel_show(self):
# this preference is allowed for non-logged-in users
@@ -1373,6 +1397,9 @@ class ApiController(RedditController):
@validatedForm(promoted = VByName('ids', thing_cls = Link, multiple = True))
def POST_onload(self, form, jquery, promoted, *a, **kw):
if not promoted:
return
# make sure that they are really promoted
promoted = [ l for l in promoted if l.promoted ]

View File

@@ -26,7 +26,7 @@ from pylons.i18n import _
from pylons.i18n.translation import LanguageError
from r2.lib.base import BaseController, proxyurl
from r2.lib import pages, utils, filters
from r2.lib.utils import http_utils
from r2.lib.utils import http_utils, UniqueIterator
from r2.lib.cache import LocalCache
import random as rand
from r2.models.account import valid_cookie, FakeAccount
@@ -140,21 +140,22 @@ def set_user_cookie(name, val):
uname = c.user.name if c.user_is_loggedin else ""
c.cookies[uname + '_' + name] = Cookie(value = val)
valid_click_cookie = re.compile(r'(t[0-9]_[a-zA-Z0-9]+:)+').match
valid_click_cookie = re.compile(r'(:?t[0-9]+_[a-zA-Z0-9]+)+').match
def read_click_cookie():
if c.user_is_loggedin:
click_cookie = read_user_cookie('click')
if click_cookie and valid_click_cookie(click_cookie):
ids = [s for s in click_cookie.split(':') if s]
things = Thing._by_fullname(ids, return_dict = False)
for t in things:
def foo(t1, user):
return lambda: t1._click(user)
#don't record clicks for the time being
#utils.worker.do(foo(t, c.user))
set_user_cookie('click', '')
# not used at the moment, if you start using this, you should also
# test it
click_cookie = read_user_cookie('click')
if click_cookie:
if valid_click_cookie(click_cookie):
fullnames = [ x for x in UniqueIterator(click_cookie.split(':')) if x ]
if len(click_cookie) > 1000:
fullnames = fullnames[:20]
set_user_cookie('click', ':'.join(fullnames))
return fullnames
else:
set_user_cookie('click', '')
def read_mod_cookie():
cook = [s.split('=')[0:2] for s in read_user_cookie('mod').split(':') if s]
if cook:
@@ -481,7 +482,6 @@ class RedditController(BaseController):
c.user._load()
c.modhash = c.user.modhash()
if request.method.lower() == 'get':
read_click_cookie()
read_mod_cookie()
if hasattr(c.user, 'msgtime') and c.user.msgtime:
c.have_messages = c.user.msgtime

View File

@@ -370,15 +370,13 @@ class VByName(Validator):
def run(self, items):
if items and self.re.match(items):
if self.multiple:
items = self.splitter.split(items)
items = filter(None, self.splitter.split(items))
try:
return Thing._by_fullname(items, return_dict = False,
data=True)
except NotFound:
pass
return self.set_error(self._error)
class VByNameIfAuthor(VByName):
def run(self, fullname):

View File

@@ -125,14 +125,12 @@ class ThingJsonTemplate(JsonTemplate):
when sent out). The elements are:
* id : Thing _fullname of thing.
* vl : triplet of scores (up, none, down) from self.score
* content : rendered representation of the thing by
calling replace_render on it using the style of get_api_subtype().
"""
from r2.lib.template_helpers import replace_render
listing = thing.listing if hasattr(thing, "listing") else None
return dict(id = thing._fullname,
#vl = self.points(thing),
content = spaceCompress(
replace_render(listing, thing,
style=get_api_subtype())))

View File

@@ -140,6 +140,11 @@ class Reddit(Wrapped):
'/reddits/create', 'create',
subtitles = rand_strings.get("create_reddit", 2),
show_cover = True, nocname=True))
#we should do this here, but unless we move the ads into a
#template of its own, it will render above the ad
#ps.append(ClickGadget())
return ps
def render(self, *a, **kw):
@@ -303,6 +308,8 @@ class Reddit(Wrapped):
"""returns a Wrapped (or renderable) item for the main content div."""
return self.content_stack(self.infobar, self.nav_menu, self._content)
class ClickGadget(Wrapped): pass
class RedditMin(Reddit):
"""a version of Reddit that has no sidebar, toolbar, footer,
etc"""

View File

@@ -231,6 +231,7 @@ class Link(Thing, Printable):
s += ''.join(map(str, [request.get.has_key('style'),
request.get.has_key('expanded'),
request.get.has_key('twocolumn'),
getattr(wrapped, 'embed_voting_style', None),
c.bgcolor,
c.bordercolor]))
return s

View File

@@ -708,9 +708,17 @@ a.star { text-decoration: none; color: #ff8b60 }
text-align: right;
}
/* display the right score based on whether they've voted */
.score.likes, .score.dislikes {display: none;}
.likes .score, .dislikes .score {display: none;}
.likes .score.likes {display: inline;}
.dislikes .score.dislikes {display: inline;}
.likes div.score.likes {display: block;}
.dislikes div.score.dislikes {display: block;}
/* compressed links */
.linkcompressed { margin: 4px 0; overflow: hidden; margin-top: 6px; }
.linkcompressed .title {margin-bottom: 1px; font-size:medium; font-weight: normal;}
.linkcompressed { margin: 4px 0; overflow: hidden; margin-top: 7px; }
.linkcompressed .title {margin-bottom: 2px; font-size:medium; font-weight: normal;}
.linkcompressed .child h3 {
margin: 15px;
text-transform: none;
@@ -739,7 +747,7 @@ a.star { text-decoration: none; color: #ff8b60 }
.linkcompressed .entry .buttons li.first {padding-left: .5em;}
.linkcompressed .entry .buttons li a {
padding: 0 2px;
background-color: #f7f7f7;
background-color: #f8f8f8;
font-weight: bold
}
@@ -752,6 +760,35 @@ a.star { text-decoration: none; color: #ff8b60 }
.cool-entry .rank { color: #A5ABFB; }
.cold-entry .rank { color: #4959F7; }
/* widget styling */
.gadget {
font-size: x-small;
border: 1px solid gray;
padding: 5px;
}
.gadget h2 {
padding-left: 49px;
font-size: 150%;
margin-bottom: 5px;
}
.gadget .midcol {
width: 15px;
margin: 0;
}
.gadget .reddit-link-end {
clear: left;
padding-top: 10px;
}
.gadget .click-gadget {font-size: small;}
.gadget small {color: gray;}
.gadget .reddit-entry {margin-left: 20px;}
.gadget .right {text-align: right;}
/* comments */
.comment { margin-left: 10px; }
@@ -1095,7 +1132,7 @@ textarea.gray { color: gray; }
border: 0px;
overflow: hidden;
width: 300px;
height: 300px;
height: 280px;
}

View File

@@ -69,6 +69,20 @@ $.unsafe = function(text) {
return (text || "");
};
$.uniq = function(list, max) {
/* $.unique only works on arrays of DOM elements */
var ret = [];
var seen = {};
var num = max ? max : list.length;
for(var i = 0; i < list.length && ret.length < num; i++) {
if(!seen[list[i]]) {
seen[list[i]] = true;
ret.push(list[i]);
}
}
return ret;
};
/* upgrade show and hide to trigger onshow/onhide events when fired. */
(function(show, hide) {
$.fn.show = function(speed, callback) {
@@ -123,7 +137,7 @@ function handleResponse(action) {
};
var api_loc = '/api/';
$.request = function(op, parameters, worker_in, block) {
$.request = function(op, parameters, worker_in, block, get_only) {
/*
Uniquitous reddit AJAX poster. Automatically addes
handleResponse(action) worker to deal with the API result. The
@@ -146,6 +160,7 @@ $.request = function(op, parameters, worker_in, block) {
return worker_in(r);
};
get_only = $.with_default(get_only, false);
/* set the subreddit name if there is one */
if (reddit.post_site)
@@ -163,7 +178,11 @@ $.request = function(op, parameters, worker_in, block) {
op = api_loc + op;
/*if( document.location.host == reddit.ajax_domain )
/* normal AJAX post */
$.post(op, parameters, worker, "json");
if(get_only) {
$.get(op, parameters, worker, "json");
} else {
$.post(op, parameters, worker, "json");
}
/*else { /* cross domain it is... * /
op = "http://" + reddit.ajax_domain + op + "?callback=?";
$.getJSON(op, parameters, worker);
@@ -196,21 +215,13 @@ $.fn.vote = function(vh, callback) {
/* let the user vote only if they are logged in */
if(reddit.logged) {
/* set the score and update the class */
things.each(function() {
var score = $(this).children().not(".child").find(".score");
var to_update = score.add($(this));
var label = reddit && reddit.vl &&
reddit.vl[$(this).thing_id()];
if(label)
score.html(label[dir+1]);
if(dir > 0)
to_update.addClass('likes').removeClass('dislikes');
$(this).addClass('likes').removeClass('dislikes');
else if(dir < 0)
to_update.removeClass('likes').addClass('dislikes');
$(this).removeClass('likes').addClass('dislikes');
else
to_update.removeClass('likes').removeClass('dislikes');
$(this).removeClass('likes').removeClass('dislikes');
});
$.request("vote", {id: things.filter(":first").thing_id(),
@@ -364,10 +375,6 @@ $.replace_things = function(things, keep_children, reveal, stubs) {
new_thing.find(".midcol").css("width", midcol).end()
.find(".rank").css("width", midcol);
/* update the score lookups */
if(data.vl)
reddit.vl[data.id] = data.vl;
if(keep_children) {
/* show the new thing */
new_thing.show()
@@ -417,8 +424,6 @@ $.insert_things = function(things, append) {
var midcol = $(".midcol:visible:first").css("width");
var numcol = $(".rank:visible:first").css("width");
var s = $.listing(data.parent);
if(data.vl)
reddit.vl[data.id] = data.vl;
if(append)
s = s.append($.unsafe(data.content)).children(".thing:last");
else

View File

@@ -448,21 +448,57 @@ function update_reddit_count(site) {
function add_thing_to_cookie(thing, cookie_name) {
var id = $(thing).thing_id();
var cookie = $.cookie_read(cookie_name);
cookie.data += ":" + id;
/* enforce a cookie max size of 1000 characters */
while(cookie.data.length > 1000) {
var i = cookie.data.indexOf(":");
/* break on bad data in the cookie and whipe out the contents */
if (i < 0) {
cookie.data = "";
break;
}
cookie.data = cookie.data.slice(i+1);
if(id && id.length) {
return add_thing_id_to_cookie(id, cookie_name);
}
}
function add_thing_id_to_cookie(id, cookie_name) {
var cookie = $.cookie_read(cookie_name);
if(!cookie.data) {
cookie.data = "";
}
/* avoid adding consecutive duplicates */
if(cookie.data.substring(0, id.length) == id) {
return;
}
cookie.data = id + ':' + cookie.data;
if(cookie.data.length > 1000) {
var fullnames = cookie.data.split(':');
fullnames = $.uniq(fullnames, 20);
cookie.data = fullnames.join(':');
}
$.cookie_write(cookie);
};
function clicked_items() {
var cookie = $.cookie_read('click');
if(cookie && cookie.data) {
var fullnames = cookie.data.split(":");
/* don't return empty ones */
for(var i=fullnames.length-1; i >= 0; i--) {
if(!fullnames[i] || !fullnames[i].length) {
fullnames.splice(i,1);
}
}
return fullnames;
} else {
return [];
}
}
function clear_clicked_items() {
var cookie = $.cookie_read('click');
cookie.data = '';
$.cookie_write(cookie);
$('.gadget').remove();
}
function updateEventHandlers(thing) {
/* this function serves as a default callback every time a new
* Thing is inserted into the DOM. It serves to rewrite a Thing's
@@ -563,38 +599,22 @@ function last_click(thing, organic) {
olisting.find('.thing:visible').hide();
thing.show();
} else {
/* we're going to have to put it into the organic box
somehow */
var thingelsewhere = $.things(current.what).filter(':not(.stub):first');
if(thingelsewhere.length > 0) {
/* if it's available on the page somewhere else, we can
clone it up into the organic box rather than go to
the server for it */
/* if there was a stub before, remove it */
thing.remove();
var othercopy = thingelsewhere.clone();
olisting.find('.thing:visible').before(othercopy).hide();
othercopy.show();
} else {
/* either it's available in the organic box, but the
data there is a stub, or it's not available at
all. either way, we need a server round-trip */
/* remove the stub if it's there */
thing.remove();
/* and add a new stub */
/* add a new stub */
olisting.find('.thing:visible')
.before('<div class="thing id-'+current.what+' stub" style="display: none"></div');
.before('<div class="thing id-'+current.what+' stub" style="display: none"></div');
/* and ask the server to fill in that stub */
/* and ask the server to fill it in */
$.request('fetch_links',
{links: [current.what],
show: current.what,
listing: olisting.attr('id')});
}
show: current.what,
listing: olisting.attr('id')});
}
}
}
@@ -621,6 +641,20 @@ function register(elem) {
return post_user(this, "register");
};
function populate_click_gadget() {
/* if we can find the click-gadget, populate it */
if($('.click-gadget').length) {
var clicked = clicked_items();
if(clicked && clicked.length) {
clicked = $.uniq(clicked, 5);
clicked.sort();
$.request('gadget/click/' + clicked.join(','), undefined, undefined, undefined, true);
}
}
}
var toolbar_p = function(expanded_size, collapsed_size) {
/* namespace for functions related to the reddit toolbar frame */
@@ -758,8 +792,7 @@ $(function() {
/* visually mark the last-clicked entry */
last_click();
populate_click_gadget();
});

View File

@@ -0,0 +1,34 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be consistent
## with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is Reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2009
## CondeNet, Inc. All Rights Reserved.
################################################################################
<%namespace file="printable.html" import="simple_button" />
<div class="gadget" style="display: none;">
<h2>${_("Recently viewed links")}</h2>
<div class="click-gadget">
<!-- populated by the populate_click_gadget function in reddit.js -->
</div>
<div class="right">
${simple_button(_("clear"), "clear_clicked_items")}
</div>
</div>

View File

@@ -129,6 +129,12 @@
<%def name="withlink()">
<span class="${thing_css_class(thing.link)}">
## add us to the click cookie
<script type="text/javascript">
$(function() {
add_thing_id_to_cookie('${thing.link._fullname}', 'click');
});
</script>
<span>
${score(thing.link, thing.link.likes, score_fmt = Score.safepoints, tag='b')}
</span>

View File

@@ -52,10 +52,10 @@
%if expanded:
${optionalstyle("margin-left: 58px;")}
%else:
${optionalstyle("margin-left: 20px; min-height:32px;")}
${optionalstyle("margin-left: 28px; min-height:32px;")}
%endif
>
<a href="${thing.url}" class="reddit-link-title"
<a href="${thing.click_url}" class="reddit-link-title"
${optionalstyle("text-decoration:none;color:#336699;font-size:small;")}>
${thing.title}
</a>

View File

@@ -19,10 +19,12 @@
## All portions of the code written by CondeNet are Copyright (c) 2006-2009
## CondeNet, Inc. All Rights Reserved.
################################################################################
<%! from r2.lib.template_helpers import replace_render %>
<%namespace file="utils.html" import="optionalstyle"/>
<%namespace file="printable.html" import="thing_css_class"/>
<div class="reddit-listing"
${optionalstyle("margin-left:5px;margin-top:7px;")}>
<div ${optionalstyle("margin-left:5px;margin-top:7px;")}>
<%
t = thing.things
l = len(t)
@@ -46,7 +48,11 @@
%endif
<div class="${cls} ${thing_css_class(a)}">
${a.render()}
%if getattr(a, 'embed_voting_style', None) == 'votable':
${unsafe(replace_render(thing,a))}
%else:
${a.render()}
%endif
</div>
%if two_col and i == l - 1:
</div>

View File

@@ -49,6 +49,13 @@ ${self.RenderPrintable()}
<%def name="thing_css_class(what)" buffered="True">
thing id-${what._fullname}
%if getattr(what, "likes", None):
likes
%elif getattr(what, "likes", None) is False:
dislikes
%elif getattr(what, "like", False) is None:
unvoted
%endif
</%def>
<%def name="RenderPrintable()">
@@ -193,25 +200,19 @@ thing id-${what._fullname}
<%def name="score(this, likes=None, tag='span', score_fmt = None)">
<%
_class = "" if likes is None else "likes" if likes else "dislikes"
# figure out alterna-points
score = this.score
base_score = score - 1 if likes else score if likes is None else score + 1
base_score = [base_score + x for x in range(-1, 2)];
if score_fmt is None:
score_fmt = thing.score_fmt
%>
<${tag} class="score ${_class}">
${score_fmt(this.score)}
<${tag} class="score dislikes">
${score_fmt(score - 1)}
</${tag}>
<${tag} class="score">
${score_fmt(score)}
</${tag}>
<${tag} class="score likes">
${score_fmt(score + 1)}
</${tag}>
<script type="text/javascript">
if(reddit)
reddit.vl['${this._fullname}'] = ['${score_fmt(base_score[0])}',
'${score_fmt(base_score[1])}',
'${score_fmt(base_score[2])}' ];
</script>
</%def>

View File

@@ -82,19 +82,19 @@ ${self.Child()}
</%def>
<%def name="real_arrows(thing)">
<div class="midcol">
${arrow(thing, 1, thing.likes)}
${arrow(thing, 0, thing.likes == False)}
</div>
<div class="midcol" ${optionalstyle("width: 15px")}>
${arrow(thing, 1, thing.likes)}
${arrow(thing, 0, thing.likes == False)}
</div>
</%def>
<%def name="arrows(thing)">
%if request.get.get("votable"):
${self.real_arrows(thing)}
%elif request.get.get("expanded"):
${self.iframe_arrows(thing)}
%else:
${self.static_arrows(thing)}
%endif
%if getattr(thing, 'embed_voting_style',None) == 'votable':
${self.real_arrows(thing)}
%elif request.get.get("expanded") or getattr(thing, 'embed_voting_style',None) == 'expanded':
${self.iframe_arrows(thing)}
%else:
${self.static_arrows(thing)}
%endif
</%def>

View File

@@ -22,7 +22,7 @@
<%!
from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, path_info
from r2.lib.pages import SearchForm
from r2.lib.pages import SearchForm, ClickGadget
from pylons import request
%>
<%namespace file="framebuster.html" import="framebuster"/>
@@ -133,6 +133,9 @@
%else:
<%include file="ads.html"/>
%endif
##cheating... we should move ads into a template of its own
${ClickGadget().render()}
</%def>