From db6470718933a668a5b6a7f8caff11ea27df8916 Mon Sep 17 00:00:00 2001 From: ketralnis Date: Tue, 2 Jun 2009 15:14:52 -0700 Subject: [PATCH] Add new 'recently visited' gadget --- r2/r2/config/routing.py | 2 + r2/r2/controllers/api.py | 31 ++++++- r2/r2/controllers/reddit_base.py | 30 +++---- r2/r2/controllers/validator/validator.py | 4 +- r2/r2/lib/jsontemplates.py | 2 - r2/r2/lib/pages/pages.py | 7 ++ r2/r2/models/link.py | 1 + r2/r2/public/static/css/reddit.css | 45 +++++++++- r2/r2/public/static/js/jquery.reddit.js | 43 +++++---- r2/r2/public/static/js/reddit.js | 107 +++++++++++++++-------- r2/r2/templates/clickgadget.html | 34 +++++++ r2/r2/templates/frametoolbar.html | 6 ++ r2/r2/templates/link.htmllite | 4 +- r2/r2/templates/listing.htmllite | 12 ++- r2/r2/templates/printable.html | 29 +++--- r2/r2/templates/printable.htmllite | 22 ++--- r2/r2/templates/reddit.html | 5 +- 17 files changed, 271 insertions(+), 113 deletions(-) create mode 100644 r2/r2/templates/clickgadget.html diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index 04cd87b5e..9dc6a1404 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -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') diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 5cc1c7284..bbb9d232f 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -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 ] diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index f87df1720..86e530a01 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -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 diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 159b22c96..f61db59ae 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -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): diff --git a/r2/r2/lib/jsontemplates.py b/r2/r2/lib/jsontemplates.py index 9cf5d09f6..4dc2ce04f 100644 --- a/r2/r2/lib/jsontemplates.py +++ b/r2/r2/lib/jsontemplates.py @@ -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()))) diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index f48c0c925..70381681b 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -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""" diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index fe662237a..31b4d4927 100644 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -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 diff --git a/r2/r2/public/static/css/reddit.css b/r2/r2/public/static/css/reddit.css index 0ec9cd882..33da6b7c0 100644 --- a/r2/r2/public/static/css/reddit.css +++ b/r2/r2/public/static/css/reddit.css @@ -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; } diff --git a/r2/r2/public/static/js/jquery.reddit.js b/r2/r2/public/static/js/jquery.reddit.js index 76c02f976..e7c4b83cd 100644 --- a/r2/r2/public/static/js/jquery.reddit.js +++ b/r2/r2/public/static/js/jquery.reddit.js @@ -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 diff --git a/r2/r2/public/static/js/reddit.js b/r2/r2/public/static/js/reddit.js index c40d1d8ea..293bd6a0b 100644 --- a/r2/r2/public/static/js/reddit.js +++ b/r2/r2/public/static/js/reddit.js @@ -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('