diff --git a/r2/Makefile b/r2/Makefile index 8ad089956..4e1d96842 100644 --- a/r2/Makefile +++ b/r2/Makefile @@ -23,7 +23,7 @@ # Jacascript files to be compressified js_targets = jquery.js jquery.json.js jquery.reddit.js reddit.js # CSS targets -css_targets = reddit.css reddit-ie6-hax.css reddit-ie7-hax.css +css_targets = reddit.css reddit-ie6-hax.css reddit-ie7-hax.css mobile.css SED=sed diff --git a/r2/r2/config/environment.py b/r2/r2/config/environment.py index 1001ce881..8c7620b55 100644 --- a/r2/r2/config/environment.py +++ b/r2/r2/config/environment.py @@ -24,6 +24,9 @@ import os #import pylons.config from pylons import config +import mimetypes +mimetypes.init() + import webhelpers from r2.config.routing import make_map diff --git a/r2/r2/config/middleware.py b/r2/r2/config/middleware.py index 89643d8c6..54195f3bc 100644 --- a/r2/r2/config/middleware.py +++ b/r2/r2/config/middleware.py @@ -39,8 +39,7 @@ from r2.lib.jsontemplates import api_type #middleware stuff from r2.lib.html_source import HTMLValidationParser from cStringIO import StringIO -import sys, tempfile, urllib, re, os, sha - +import sys, tempfile, urllib, re, os, sha, subprocess #from pylons.middleware import error_mapper def error_mapper(code, message, environ, global_conf=None, **kw): @@ -94,17 +93,70 @@ class DebugMiddleware(object): if debug and self.keyword in args.keys(): prof_arg = args.get(self.keyword) prof_arg = urllib.unquote(prof_arg) if prof_arg else None - return self.filter(foo, prof_arg = prof_arg) + r = self.filter(foo, prof_arg = prof_arg) + if isinstance(r, Response): + return r(environ, start_response) + return r return self.app(environ, start_response) def filter(self, execution_func, prof_arg = None): pass + +class ProfileGraphMiddleware(DebugMiddleware): + def __init__(self, app): + DebugMiddleware.__init__(self, app, 'profile-graph') + + def filter(self, execution_func, prof_arg = None): + # put thie imports here so the app doesn't choke if profiling + # is not present (this is a debug-only feature anyway) + import cProfile as profile + from pstats import Stats + from r2.lib.contrib import gprof2dot + # profiling needs an actual file to dump to. Everything else + # can be mitigated with streams + tmpfile = tempfile.NamedTemporaryFile() + dotfile = StringIO() + # simple cutoff validation + try: + cutoff = .01 if prof_arg is None else float(prof_arg)/100 + except ValueError: + cutoff = .01 + try: + # profile the code in the current context + profile.runctx('execution_func()', + globals(), locals(), tmpfile.name) + # parse the data + parser = gprof2dot.PstatsParser(tmpfile.name) + prof = parser.parse() + # remove nodes and edges with less than cutoff work + prof.prune(cutoff, cutoff) + # make the dotfile + dot = gprof2dot.DotWriter(dotfile) + dot.graph(prof, gprof2dot.TEMPERATURE_COLORMAP) + # convert the dotfile to PNG in local stdout + proc = subprocess.Popen("dot -Tpng", + shell = True, + stdin =subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, error = proc.communicate(input = dotfile.getvalue()) + # generate the response + r = Response() + r.status_code = 200 + r.headers['content-type'] = "image/png" + r.content = out + return r + finally: + tmpfile.close() + class ProfilingMiddleware(DebugMiddleware): def __init__(self, app): DebugMiddleware.__init__(self, app, 'profile') def filter(self, execution_func, prof_arg = None): + # put thie imports here so the app doesn't choke if profiling + # is not present (this is a debug-only feature anyway) import cProfile as profile from pstats import Stats @@ -383,6 +435,10 @@ class RequestLogMiddleware(object): return r class LimitUploadSize(object): + """ + Middleware for restricting the size of uploaded files (such as + image files for the CSS editing capability). + """ def __init__(self, app, max_size=1024*500): self.app = app self.max_size = max_size @@ -399,6 +455,29 @@ class LimitUploadSize(object): return self.app(environ, start_response) + +class CleanupMiddleware(object): + """ + Put anything here that should be called after every other bit of + middleware. This currently includes the code for removing + duplicate headers (such as multiple cookie setting). The behavior + here is to disregard all but the last record. + """ + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + def custom_start_response(status, headers, exc_info = None): + fixed = [] + seen = set() + for head, val in reversed(headers): + head = head.lower() + if head not in seen: + fixed.insert(0, (head, val)) + seen.add(head) + return start_response(status, fixed, exc_info) + return self.app(environ, custom_start_response) + #god this shit is disorganized and confusing class RedditApp(PylonsBaseWSGIApp): def find_controller(self, controller): @@ -439,7 +518,11 @@ def make_app(global_conf, full_stack=True, **app_conf): # CUSTOM MIDDLEWARE HERE (filtered by the error handling middlewares) + # last thing first from here down + app = CleanupMiddleware(app) + app = LimitUploadSize(app) + app = ProfileGraphMiddleware(app) app = ProfilingMiddleware(app) app = SourceViewMiddleware(app) diff --git a/r2/r2/config/templates.py b/r2/r2/config/templates.py index 0b7ff7f90..a5a2c8d0f 100644 --- a/r2/r2/config/templates.py +++ b/r2/r2/config/templates.py @@ -29,7 +29,7 @@ def api(type, cls): tpm.add_handler(type, 'api-html', cls()) # blanket fallback rule -api('wrapped', NullJsonTemplate) +api('templated', NullJsonTemplate) # class specific overrides api('link', LinkJsonTemplate) diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 8d572b7ed..ffd57f878 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -30,14 +30,13 @@ from r2.models import * from r2.models.subreddit import Default as DefaultSR import r2.models.thing_changes as tc -from r2.controllers import ListingController - from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified from r2.lib.utils import query_string, to36, timefromnow, link_from_url from r2.lib.wrapped import Wrapped from r2.lib.pages import FriendList, ContributorList, ModList, \ BannedList, BoringPage, FormPage, NewLink, CssError, UploadedImage, \ ClickGadget +from r2.lib.pages.things import wrap_links, default_thing_wrapper from r2.lib.menus import CommentSortMenu from r2.lib.normalized_hot import expire_hot @@ -84,8 +83,7 @@ class ApiController(RedditController): return abort(404, 'not found') links = link_from_url(request.params.get('url'), filter_spam = False) - builder = IDBuilder([link._fullname for link in links], num = count) - listing = LinkListing(builder, nextprev = False).listing() + listing = wrap_links(links, num = count) return BoringPage(_("API"), content = listing).render() @validatedForm(VCaptcha(), @@ -574,8 +572,7 @@ class ApiController(RedditController): if kind == 'link': set_last_modified(item, 'comments') - wrapper = make_wrapper(ListingController.builder_wrapper, - expand_children = True) + wrapper = default_thing_wrapper(expand_children = True) jquery(".content").replace_things(item, True, True, wrap = wrapper) @validatedForm(VUser(), @@ -1116,7 +1113,8 @@ class ApiController(RedditController): if children: builder = CommentBuilder(link, CommentSortMenu.operator(sort), children) - items = builder.get_items(starting_depth = depth, num = 20) + listing = Listing(builder, nextprev = False) + items = listing.get_items(starting_depth = depth, num = 20) def _children(cur_items): items = [] for cm in cur_items: @@ -1286,12 +1284,8 @@ class ApiController(RedditController): @validatedForm(links = VByName('links', thing_cls = Link, multiple = True), show = VByName('show', thing_cls = Link, multiple = False)) def POST_fetch_links(self, form, jquery, links, show): - b = IDBuilder([l._fullname for l in links], - wrap = ListingController.builder_wrapper) - l = OrganicListing(b) - l.num_margin = 0 - l.mid_margin = 0 - + l = wrap_links(links, listing_cls = OrganicListing, + num_margin = 0, mid_margin = 0) jquery(".content").replace_things(l, stubs = True) if show: @@ -1473,5 +1467,6 @@ class ApiController(RedditController): if not link: abort(404, 'not found') - wrapped = IDBuilder([link._fullname]).get_items()[0][0] + wrapped = wrap_links(link) + wrapped = list(wrapped)[0] return spaceCompress(websafe(wrapped.link_child.content())) diff --git a/r2/r2/controllers/buttons.py b/r2/r2/controllers/buttons.py index 25fbc7b2b..567264857 100644 --- a/r2/r2/controllers/buttons.py +++ b/r2/r2/controllers/buttons.py @@ -22,8 +22,8 @@ from reddit_base import RedditController from r2.lib.pages import Button, ButtonNoBody, ButtonEmbed, ButtonLite, \ ButtonDemoPanel, WidgetDemoPanel, Bookmarklets, BoringPage, Socialite +from r2.lib.pages.things import wrap_links from r2.models import * -from r2.lib.strings import Score from r2.lib.utils import tup, query_string from pylons import c, request from validator import * @@ -39,7 +39,7 @@ class ButtonsController(RedditController): except ValueError: return 1 - def get_wrapped_link(self, url, link = None): + def get_wrapped_link(self, url, link = None, wrapper = None): try: if link: links = [link] @@ -51,24 +51,20 @@ class ButtonsController(RedditController): links = [] if links: - # cache the render style and reset it to html - rs = c.render_style - c.render_style = 'html' - link_builder = IDBuilder([l._fullname for l in links], - wrap=ListingController.builder_wrapper) - - # link_listing will be the one-element listing at the top - link_listing = LinkListing(link_builder, - nextprev=False).listing() - - # reset the render style - c.render_style = rs - links = link_listing.things + kw = {} + if wrapper: + links = wrap_links(links, wrapper = wrapper) + else: + links = wrap_links(links) + links = list(links) + links = max(links, key = lambda x: x._score) if links else None + if not links and wrapper: + return wrapper(None) + return links # note: even if _by_url successed or a link was passed in, # it is possible link_listing.things is empty if the # link(s) is/are members of a private reddit # return the link with the highest score (if more than 1) - return max(links, key = lambda x: x._score) if links else None except: #we don't want to return 500s in other people's pages. import traceback @@ -84,43 +80,31 @@ class ButtonsController(RedditController): width = VInt('width', 0, 800), link = VByName('id')) def GET_button_content(self, url, title, css, vote, newwindow, width, link): - + + # no buttons on domain listings if isinstance(c.site, DomainSR): c.site = Default return self.redirect(request.path + query_string(request.GET)) - l = self.get_wrapped_link(url, link) - if l: url = l.url - #disable css hack if (css != 'http://blog.wired.com/css/redditsocial.css' and css != 'http://www.wired.com/css/redditsocial.css'): css = None - bt = self.buttontype() - if bt == 1: - score_fmt = Score.safepoints - else: - score_fmt = Score.number_only - - page_handler = Button - if not vote: - page_handler = ButtonNoBody + wrapper = make_wrapper(Button if vote else ButtonNoBody, + url = url, + target = "_new" if newwindow else "_parent", + title = title, vote = vote, bgcolor = c.bgcolor, + width = width, css = css, + button = self.buttontype()) - if newwindow: - target = "_new" - else: - target = "_parent" - - res = page_handler(button=bt, css=css, - score_fmt = score_fmt, link = l, - url=url, title=title, - vote = vote, target = target, - bgcolor=c.bgcolor, width=width).render() + l = self.get_wrapped_link(url, link, wrapper) + res = l.render() c.response.content = spaceCompress(res) return c.response + @validate(buttontype = VInt('t', 1, 5), url = VSanitizedUrl("url"), @@ -138,7 +122,8 @@ class ButtonsController(RedditController): return c.response buttontype = buttontype or 1 - width, height = ((120, 22), (51, 69), (69, 52), (51, 52), (600, 52))[min(buttontype - 1, 4)] + width, height = ((120, 22), (51, 69), (69, 52), + (51, 52), (600, 52))[min(buttontype - 1, 4)] if _width: width = _width if _height: height = _height @@ -147,32 +132,36 @@ class ButtonsController(RedditController): height=height, url = url, referer = request.referer).render() - # we doing want the JS to be cached! + # we doing want the JS to be cached (it is referer dependent) c.used_cache = True return self.sendjs(bjs, callback='', escape=False) @validate(buttonimage = VInt('i', 0, 14), + title = nop('title'), url = VSanitizedUrl('url'), newwindow = VBoolean('newwindow', default = False), styled = VBoolean('styled', default=True)) - def GET_button_lite(self, buttonimage, url, styled, newwindow): + def GET_button_lite(self, buttonimage, title, url, styled, newwindow): c.render_style = 'js' c.response_content_type = 'text/javascript; charset=UTF-8' + if not url: url = request.referer - if newwindow: - target = "_new" - else: - target = "_parent" + # we don't want the JS to be cached if the referer was involved. + c.used_cache = True - l = self.get_wrapped_link(url) - image = 1 if buttonimage is None else buttonimage + def builder_wrapper(thing = None): + kw = {} + if not thing: + kw['url'] = url + kw['title'] = title + return ButtonLite(thing, + image = 1 if buttonimage is None else buttonimage, + target = "_new" if newwindow else "_parent", + styled = styled, **kw) - bjs = ButtonLite(image = image, link = l, url = l.url if l else url, - target = target, styled = styled).render() - # we don't want the JS to be cached! - c.used_cache = True - return self.sendjs(bjs, callback='', escape=False) + bjs = self.get_wrapped_link(url, wrapper = builder_wrapper) + return self.sendjs(bjs.render(), callback='', escape=False) diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index 67fd539b1..6a62d5ca9 100644 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -25,6 +25,7 @@ from reddit_base import RedditController, base_listing from r2 import config from r2.models import * from r2.lib.pages import * +from r2.lib.pages.things import wrap_links from r2.lib.menus import * from r2.lib.utils import to36, sanitize_url, check_cheating, title_to_url from r2.lib.utils import query_string, UrlParser, link_from_url, link_duplicates @@ -513,12 +514,10 @@ class FrontController(RedditController): if links and len(links) == 1: return self.redirect(links[0].already_submitted_link) elif links: - builder = IDBuilder([link._fullname for link in links]) - listing = LinkListing(builder, nextprev=False).listing() infotext = (strings.multiple_submitted % links[0].resubmit_link()) res = BoringPage(_("seen it"), - content = listing, + content = wrap_links(links), infotext = infotext).render() return res diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index ebe028eee..ca6dae3a4 100644 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -24,6 +24,7 @@ from validator import * from r2.models import * from r2.lib.pages import * +from r2.lib.pages.things import wrap_links from r2.lib.menus import NewMenu, TimeMenu, SortMenu, RecSortMenu, ControversyTimeMenu from r2.lib.rising import get_rising from r2.lib.wrapped import Wrapped @@ -148,17 +149,7 @@ class ListingController(RedditController): """Contents of the right box when rendering""" pass - @staticmethod - def builder_wrapper(thing): - w = Wrapped(thing) - - if isinstance(thing, Link): - if thing.promoted: - w = Wrapped(thing) - w.render_class = PromotedLink - w.rowstyle = 'promoted link' - - return w + builder_wrapper = staticmethod(default_thing_wrapper()) def GET_listing(self, **env): return self.build_listing(**env) diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py index 83cce1b37..02d8be370 100644 --- a/r2/r2/controllers/promotecontroller.py +++ b/r2/r2/controllers/promotecontroller.py @@ -23,6 +23,7 @@ from validator import * from pylons.i18n import _ from r2.models import * from r2.lib.pages import * +from r2.lib.pages.things import wrap_links from r2.lib.menus import * from r2.controllers import ListingController @@ -40,16 +41,10 @@ class PromoteController(RedditController): @validate(VSponsor()) def GET_current_promos(self): - current_list = get_promoted() - - b = IDBuilder(current_list) - - render_list = b.get_items()[0] - + render_list = list(wrap_links(get_promoted())) for x in render_list: if x.promote_until: x.promote_expires = timetext(datetime.now(g.tz) - x.promote_until) - page = PromotePage('current_promos', content = PromotedLinks(render_list)) @@ -65,12 +60,8 @@ class PromoteController(RedditController): link = VLink('link')) def GET_edit_promo(self, link): sr = Subreddit._byID(link.sr_id) - - names = [link._fullname] - builder = IDBuilder(names, wrap = ListingController.builder_wrapper) - listing = LinkListing(builder, - show_nums = False, nextprev = False) - rendered = listing.listing().render() + listing = wrap_links(link) + rendered = listing.render() timedeltatext = '' if link.promote_until: diff --git a/r2/r2/controllers/toolbar.py b/r2/r2/controllers/toolbar.py index c8a116fdd..beb711115 100644 --- a/r2/r2/controllers/toolbar.py +++ b/r2/r2/controllers/toolbar.py @@ -22,6 +22,7 @@ from reddit_base import RedditController from r2.lib.pages import * from r2.models import * +from r2.lib.pages.things import wrap_links from r2.lib.menus import CommentSortMenu from r2.lib.filters import spaceCompress, safemarkdown from r2.lib.memoize import memoize @@ -151,26 +152,16 @@ class ToolbarController(RedditController): if not link.subreddit_slow.can_view(c.user): abort(403, 'forbidden') - def builder_wrapper(cm): - w = Wrapped(cm) - w.render_class = StarkComment - w.target = "_top" - return w - - link_builder = IDBuilder((link._fullname,)) - link_listing = LinkListing(link_builder, nextprev=False).listing() - links = link_listing.things[0], + links = list(wrap_links(link)) if not links: # they aren't allowed to see this link return self.abort(403, 'forbidden') link = links[0] - res = FrameToolbar(link = link, - title = link.title, - url = link.url) - + wrapper = make_wrapper(render_class = StarkComment, + target = "_top") b = TopCommentBuilder(link, CommentSortMenu.operator('top'), - wrap = builder_wrapper) + wrap = wrapper) listing = NestedListing(b, num = 10, # TODO: add config var parent_name = link._fullname) @@ -180,7 +171,8 @@ class ToolbarController(RedditController): md_bar = safemarkdown(raw_bar, target="_top") - res = RedditMin(content=CommentsPanel(link=link, listing=listing.listing(), + res = RedditMin(content=CommentsPanel(link=link, + listing=listing.listing(), expanded=auto_expand_panel(link), infobar=md_bar)) @@ -194,15 +186,9 @@ class ToolbarController(RedditController): link = utils.link_from_url(url, multiple = False) if link: - link_builder = IDBuilder((link._fullname,)) - - res = FrameToolbar(link = link_builder.get_items()[0][0], - title = link.title, - url = link.url, - domain = None - if link.is_self - else domain(link.url), - expanded = auto_expand_panel(link)) + link = list(wrap_links(link, wrapper = FrameToolbar)) + if link: + res = link[0] else: res = FrameToolbar(link = None, title = None, diff --git a/r2/r2/lib/cssfilter.py b/r2/r2/lib/cssfilter.py index 0f08a6c65..d79c9d5c8 100644 --- a/r2/r2/lib/cssfilter.py +++ b/r2/r2/lib/cssfilter.py @@ -24,6 +24,7 @@ from __future__ import with_statement from r2.models import * from r2.lib.utils import sanitize_url, domain, randstr from r2.lib.strings import string_dict +from r2.lib.pages.things import wrap_links from pylons import g, c from pylons.i18n import _ @@ -332,38 +333,14 @@ def find_preview_links(sr): return links def rendered_link(links, media, compress): - from pylons.controllers.util import abort - from r2.controllers import ListingController - - try: - render_style = c.render_style - - c.render_style = 'html' - - with c.user.safe_set_attr: - c.user.pref_compress = compress - c.user.pref_media = media - - b = IDBuilder([l._fullname for l in links], - num = 1, wrap = ListingController.builder_wrapper) - return LinkListing(b, nextprev=False, - show_nums=True).listing().render(style='html') - finally: - c.render_style = render_style + with c.user.safe_set_attr: + c.user.pref_compress = compress + c.user.pref_media = media + links = wrap_links(links, show_nums = True, num = 1) + return links.render(style = "html") def rendered_comment(comments): - try: - render_style = c.render_style - - c.render_style = 'html' - - b = IDBuilder([x._fullname for x in comments], - num = 1) - return LinkListing(b, nextprev=False, - show_nums=False).listing().render(style='html') - - finally: - c.render_style = render_style + return wrap_links(comments, num = 1).render(style = "html") class BadImage(Exception): pass diff --git a/r2/r2/lib/filters.py b/r2/r2/lib/filters.py index 5453486d3..19eebc171 100644 --- a/r2/r2/lib/filters.py +++ b/r2/r2/lib/filters.py @@ -24,6 +24,7 @@ from pylons import c import cgi import urllib import re +from wrapped import Templated, CacheStub SC_OFF = "" SC_ON = "" @@ -75,6 +76,8 @@ class _Unsafe(unicode): pass def _force_unicode(text): try: text = unicode(text, 'utf-8') + except UnicodeDecodeError: + text = unicode(text, 'latin1') except TypeError: text = unicode(text) return text @@ -91,6 +94,12 @@ def websafe_json(text=""): def mako_websafe(text = ''): if text.__class__ == _Unsafe: return text + elif isinstance(text, Templated): + return _Unsafe(text.render()) + elif isinstance(text, CacheStub): + return _Unsafe(text) + elif text is None: + return "" elif text.__class__ != unicode: text = _force_unicode(text) return c_websafe(text) diff --git a/r2/r2/lib/jsonresponse.py b/r2/r2/lib/jsonresponse.py index b67ecd434..5b60db4d7 100644 --- a/r2/r2/lib/jsonresponse.py +++ b/r2/r2/lib/jsonresponse.py @@ -21,11 +21,11 @@ ################################################################################ from r2.lib.utils import tup from r2.lib.captcha import get_iden -from r2.lib.wrapped import Wrapped +from r2.lib.wrapped import Wrapped, StringTemplate from r2.lib.filters import websafe_json -from r2.lib.template_helpers import replace_render from r2.lib.jsontemplates import get_api_subtype from r2.lib.base import BaseController +from r2.lib.pages.things import wrap_links from r2.models import IDBuilder, Listing import simplejson @@ -88,16 +88,11 @@ class JsonResponse(object): """ function for inserting/replacing things in listings. """ - listing = None - if isinstance(things, Listing): - listing = things.listing() - things = listing.things things = tup(things) if not all(isinstance(t, Wrapped) for t in things): wrap = kw.pop('wrap', Wrapped) - b = IDBuilder([t._fullname for t in things], wrap) - things = b.get_items()[0] - data = [replace_render(listing, t) for t in things] + things = wrap_links(things, wrapper = wrap) + data = [t.render() for t in things] if kw: for d in data: diff --git a/r2/r2/lib/jsontemplates.py b/r2/r2/lib/jsontemplates.py index 9f09f168e..674ae3b8a 100644 --- a/r2/r2/lib/jsontemplates.py +++ b/r2/r2/lib/jsontemplates.py @@ -20,7 +20,7 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from utils import to36, tup, iters -from wrapped import Wrapped +from wrapped import Wrapped, StringTemplate, CacheStub, CachedVariable from mako.template import Template from r2.lib.filters import spaceCompress, safemarkdown import time, pytz @@ -42,11 +42,34 @@ def make_typename(typ): def make_fullname(typ, _id): return '%s_%s' % (make_typename(typ), to36(_id)) + +class ObjectTemplate(StringTemplate): + def __init__(self, d): + self.d = d + + def update(self, kw): + def _update(obj): + if isinstance(obj, (str, unicode)): + return spaceCompress(StringTemplate(obj).finalize(kw)) + elif isinstance(obj, dict): + return dict((k, _update(v)) for k, v in obj.iteritems()) + elif isinstance(obj, (list, tuple)): + return map(_update, obj) + elif isinstance(obj, CacheStub) and kw.has_key(obj.name): + return kw[obj.name] + else: + return obj + res = _update(self.d) + return ObjectTemplate(res) + + def finalize(self, kw = {}): + return self.update(kw).d + class JsonTemplate(Template): def __init__(self): pass def render(self, thing = None, *a, **kw): - return {} + return ObjectTemplate({}) class TableRowTemplate(JsonTemplate): def cells(self, thing): @@ -59,15 +82,15 @@ class TableRowTemplate(JsonTemplate): return "" def render(self, thing = None, *a, **kw): - return {"id": self.css_id(thing), - "css_class": self.css_class(thing), - "cells": self.cells(thing)} + return ObjectTemplate(dict(id = self.css_id(thing), + css_class = self.css_class(thing), + cells = self.cells(thing))) class UserItemJsonTemplate(TableRowTemplate): def cells(self, thing): cells = [] for cell in thing.cells: - r = Wrapped.part_render(thing, 'cell_type', cell) + r = Templated.part_render(thing, 'cell_type', cell) cells.append(spaceCompress(r)) return cells @@ -90,18 +113,6 @@ class ThingJsonTemplate(JsonTemplate): d.update(kw) return d - def points(self, wrapped): - """ - Generates the JS-style point triplet for votable elements - (stored on the vl var on the JS side). - """ - score = wrapped.score - likes = wrapped.likes - 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)] - return [wrapped.score_fmt(s) for s in base_score] - - def kind(self, wrapped): """ Returns a string literal which identifies the type of this @@ -122,31 +133,18 @@ class ThingJsonTemplate(JsonTemplate): * id : Thing _fullname of thing. * content : rendered representation of the thing by - calling replace_render on it using the style of get_api_subtype(). + calling 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, - content = spaceCompress( - replace_render(listing, thing, - style=get_api_subtype()))) - + res = dict(id = thing._fullname, + content = thing.render(style=get_api_subtype())) + return res + def raw_data(self, thing): """ Complement to rendered_data. Called when a dictionary of thing data attributes is to be sent across the wire. """ - def strip_data(x): - if isinstance(x, dict): - return dict((k, strip_data(v)) for k, v in x.iteritems()) - elif isinstance(x, iters): - return [strip_data(y) for y in x] - elif isinstance(x, Wrapped): - return x.render() - else: - return x - - return dict((k, strip_data(self.thing_attr(thing, v))) + return dict((k, self.thing_attr(thing, v)) for k, v in self._data_attrs_.iteritems()) def thing_attr(self, thing, attr): @@ -162,7 +160,9 @@ class ThingJsonTemplate(JsonTemplate): return time.mktime(thing._date.timetuple()) elif attr == "created_utc": return time.mktime(thing._date.astimezone(pytz.UTC).timetuple()) - return getattr(thing, attr) if hasattr(thing, attr) else None + elif attr == "child": + return CachedVariable("childlisting") + return getattr(thing, attr, None) def data(self, thing): if get_api_subtype(): @@ -171,7 +171,8 @@ class ThingJsonTemplate(JsonTemplate): return self.raw_data(thing) def render(self, thing = None, action = None, *a, **kw): - return dict(kind = self.kind(thing), data = self.data(thing)) + return ObjectTemplate(dict(kind = self.kind(thing), + data = self.data(thing))) class SubredditJsonTemplate(ThingJsonTemplate): _data_attrs_ = ThingJsonTemplate.data_attrs(subscribers = "score", @@ -244,26 +245,17 @@ class CommentJsonTemplate(ThingJsonTemplate): return make_typename(Comment) def rendered_data(self, wrapped): - from r2.models import Comment, Link - try: - parent_id = wrapped.parent_id - except AttributeError: - parent_id = make_fullname(Link, wrapped.link_id) - else: - parent_id = make_fullname(Comment, parent_id) d = ThingJsonTemplate.rendered_data(self, wrapped) + d['replies'] = self.thing_attr(wrapped, 'child') d['contentText'] = self.thing_attr(wrapped, 'body') d['contentHTML'] = self.thing_attr(wrapped, 'body_html') - d['parent'] = parent_id - d['link'] = make_fullname(Link, wrapped.link_id) + d['link'] = self.thing_attr(wrapped, 'link_id') + d['parent'] = self.thing_attr(wrapped, 'parent_id') return d class MoreCommentJsonTemplate(CommentJsonTemplate): _data_attrs_ = dict(id = "_id36", name = "_fullname") - def points(self, wrapped): - return [] - def kind(self, wrapped): return "more" @@ -303,12 +295,14 @@ class MessageJsonTemplate(ThingJsonTemplate): parent_id = make_fullname(Message, parent_id) d = ThingJsonTemplate.rendered_data(self, wrapped) d['parent'] = parent_id + d['contentText'] = self.thing_attr(wrapped, 'body') + d['contentHTML'] = self.thing_attr(wrapped, 'body_html') return d class RedditJsonTemplate(JsonTemplate): def render(self, thing = None, *a, **kw): - return thing.content().render() if thing else {} + return ObjectTemplate(thing.content().render() if thing else {}) class PanestackJsonTemplate(JsonTemplate): def render(self, thing = None, *a, **kw): @@ -316,36 +310,34 @@ class PanestackJsonTemplate(JsonTemplate): res = [x for x in res if x] if not res: return {} - return res if len(res) > 1 else res[0] + return ObjectTemplate(res if len(res) > 1 else res[0] ) class NullJsonTemplate(JsonTemplate): def render(self, thing = None, *a, **kw): - return None + return "" class ListingJsonTemplate(ThingJsonTemplate): _data_attrs_ = dict(children = "things") - def points(self, w): - return [] + def thing_attr(self, thing, attr): + if attr == "things": + res = [] + for a in thing.things: + a.childlisting = False + r = a.render() + if isinstance(r, str): + r = spaceCompress(r) + res.append(r) + return res + return ThingJsonTemplate.thing_attr(self, thing, attr) + def rendered_data(self, thing): - from r2.lib.template_helpers import replace_render - res = [] - for a in thing.things: - a.listing = thing - r = replace_render(thing, a, style = 'api') - if isinstance(r, str): - r = spaceCompress(r) - res.append(r) - return res + return self.thing_attr(thing, "things") def kind(self, wrapped): return "Listing" - def render(self, *a, **kw): - res = ThingJsonTemplate.render(self, *a, **kw) - return res - class OrganicListingJsonTemplate(ListingJsonTemplate): def kind(self, wrapped): return "OrganicListing" @@ -357,4 +349,4 @@ class TrafficJsonTemplate(JsonTemplate): if hasattr(thing, ival + "_data"): res[ival] = [[time.mktime(date.timetuple())] + list(data) for date, data in getattr(thing, ival+"_data")] - return res + return ObjectTemplate(res) diff --git a/r2/r2/lib/manager/tp_manager.py b/r2/r2/lib/manager/tp_manager.py index 6ac4b1d67..7c943f29c 100644 --- a/r2/r2/lib/manager/tp_manager.py +++ b/r2/r2/lib/manager/tp_manager.py @@ -19,7 +19,7 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ -import pylons +import pylons, sha from mako.template import Template as mTemplate from mako.exceptions import TemplateLookupException from r2.lib.filters import websafe, unsafe @@ -67,6 +67,11 @@ class tp_manager: template = _loader.load_template(self.templates[key]) if cache: self.templates[key] = template + # also store a hash for the template + if (not hasattr(template, "hash") and + hasattr(template, "filename")): + with open(template.filename, 'r') as handle: + template.hash = sha.new(handle.read()).hexdigest() # cache also for the base class so # introspection is not required on subsequent passes if key != top_key: @@ -78,3 +83,4 @@ class tp_manager: if not template or not isinstance(template, self.Template): raise AttributeError, ("template doesn't exist for %s" % str(top_key)) return template + diff --git a/r2/r2/lib/menus.py b/r2/r2/lib/menus.py index fe5547422..2c747ce55 100644 --- a/r2/r2/lib/menus.py +++ b/r2/r2/lib/menus.py @@ -20,7 +20,7 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ -from wrapped import Wrapped, Styled +from wrapped import CachedTemplate, Styled from pylons import c, request, g from utils import query_string, timeago from strings import StringHandler, plurals @@ -29,6 +29,7 @@ from r2.lib.filters import _force_unicode from pylons.i18n import _ + class MenuHandler(StringHandler): """Bastard child of StringHandler and plurals. Menus are typically a single word (and in some cases, a single plural word @@ -172,7 +173,7 @@ class NavMenu(Styled): 'style' parameter sets what template/layout to use to differentiate, say, a dropdown from a flatlist, while the optional _class, and _id attributes can be used to set individualized CSS.""" - + def __init__(self, options, default = None, title = '', type = "dropdown", base_path = '', separator = '|', **kw): self.options = options @@ -208,9 +209,6 @@ class NavMenu(Styled): if opt.dest == self.default: return opt - def __repr__(self): - return "" - def __iter__(self): for opt in self.options: yield opt @@ -223,14 +221,17 @@ class NavButton(Styled): def __init__(self, title, dest, sr_path = True, nocname=False, opt = '', aliases = [], target = "", style = "plain", **kw): - # keep original dest to check against c.location when rendering - self.aliases = set(a.rstrip('/') for a in aliases) - self.aliases.add(dest.rstrip('/')) - self.dest = dest + aliases = set(a.rstrip('/') for a in aliases) + aliases.add(dest.rstrip('/')) + + self.request_params = dict(request.params) + self.stripped_path = request.path.rstrip('/').lower() Styled.__init__(self, style = style, sr_path = sr_path, - nocname = nocname, target = target, + nocname = nocname, target = target, + aliases = aliases, dest = dest, + selected = False, title = title, opt = opt, **kw) def build(self, base_path = ''): @@ -239,7 +240,7 @@ class NavButton(Styled): # append to the path or update the get params dependent on presence # of opt if self.opt: - p = request.get.copy() + p = self.request_params.copy() p[self.opt] = self.dest else: p = {} @@ -258,13 +259,11 @@ class NavButton(Styled): def is_selected(self): """Given the current request path, would the button be selected.""" if self.opt: - return request.params.get(self.opt, '') in self.aliases + return self.request_params.get(self.opt, '') in self.aliases else: - stripped_path = request.path.rstrip('/').lower() - ustripped_path = _force_unicode(stripped_path) - if stripped_path == self.bare_path: + if self.stripped_path == self.bare_path: return True - if stripped_path in self.aliases: + if self.stripped_path in self.aliases: return True def selected_title(self): @@ -277,17 +276,24 @@ class OffsiteButton(NavButton): self.sr_path = False self.path = self.bare_path = self.dest + def cachable_attrs(self): + return [('path', self.path), ('title', self.title)] + class SubredditButton(NavButton): def __init__(self, sr): - self.sr = sr - NavButton.__init__(self, sr.name, sr.path, False) + self.path = sr.path + NavButton.__init__(self, sr.name, sr.path, False, + isselected = (c.site == sr)) def build(self, base_path = ''): - self.path = self.sr.path + pass def is_selected(self): - return c.site == self.sr + return self.isselected + def cachable_attrs(self): + return [('path', self.path), ('title', self.title), + ('isselected', self.isselected)] class NamedButton(NavButton): """Convenience class for handling the majority of NavButtons @@ -345,13 +351,11 @@ class SimpleGetMenu(NavMenu): type = 'lightdrop' def __init__(self, **kw): - kw['default'] = kw.get('default', self.default) - kw['base_path'] = kw.get('base_path') or request.path buttons = [NavButton(self.make_title(n), n, opt = self.get_param) for n in self.options] + kw['default'] = kw.get('default', self.default) + kw['base_path'] = kw.get('base_path') or request.path NavMenu.__init__(self, buttons, type = self.type, **kw) - #if kw.get('default'): - # self.selected = kw['default'] def make_title(self, attr): return menu[attr] @@ -467,29 +471,29 @@ class NumCommentsMenu(SimpleGetMenu): def __init__(self, num_comments, **context): self.num_comments = num_comments + self.max_comments = g.max_comments + self.user_num = c.user.pref_num_comments SimpleGetMenu.__init__(self, **context) def make_title(self, attr): - user_num = c.user.pref_num_comments - if user_num > self.num_comments: + if self.user_num > self.num_comments: # no menus needed if the number of comments is smaller # than any of the limits return "" - elif self.num_comments > g.max_comments: + elif self.num_comments > self.max_comments: # if the number present is larger than the global max, # label the menu as the user pref and the max number - return dict(true=str(g.max_comments), - false=str(user_num))[attr] + return dict(true=str(self.max_comments), + false=str(self.user_num))[attr] else: # if the number is less than the global max, display "all" # instead for the upper bound. return dict(true=_("all"), - false=str(user_num))[attr] + false=str(self.user_num))[attr] def render(self, **kw): - user_num = c.user.pref_num_comments - if user_num > self.num_comments: + if self.user_num > self.num_comments: return "" return SimpleGetMenu.render(self, **kw) diff --git a/r2/r2/lib/pages/admin_pages.py b/r2/r2/lib/pages/admin_pages.py index 78fe4aff2..6d0c6e396 100644 --- a/r2/r2/lib/pages/admin_pages.py +++ b/r2/r2/lib/pages/admin_pages.py @@ -20,18 +20,18 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from pylons import c, g -from r2.lib.wrapped import Wrapped +from r2.lib.wrapped import Templated from pages import Reddit from r2.lib.menus import NamedButton, NavButton, menu, NavMenu -class AdminSidebar(Wrapped): +class AdminSidebar(Templated): def __init__(self, user): self.user = user -class Details(Wrapped): +class Details(Templated): def __init__(self, link): - Wrapped.__init__(self) + Templated.__init__(self) self.link = link diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 252507f44..fb3a01cff 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -19,11 +19,11 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ -from r2.lib.wrapped import Wrapped, NoTemplateFound, Styled -from r2.models import IDBuilder, LinkListing, Account, Default +from r2.lib.wrapped import Wrapped, Templated, NoTemplateFound, CachedTemplate +from r2.models import Account, Default from r2.models import FakeSubreddit, Subreddit from r2.models import Friends, All, Sub, NotFound, DomainSR -from r2.models import make_wrapper +from r2.models import Link, Printable from r2.config import cache from r2.lib.jsonresponse import json_respond from r2.lib.jsontemplates import is_api @@ -48,13 +48,15 @@ import graph from itertools import chain from urllib import quote +from things import wrap_links, default_thing_wrapper + datefmt = _force_utf8(_('%d %b %Y')) def get_captcha(): if not c.user_is_loggedin or c.user.needs_captcha(): return get_iden() -class Reddit(Wrapped): +class Reddit(Templated): '''Base class for rendering a page on reddit. Handles toolbar creation, content of the footers, and content of the corner buttons. @@ -90,15 +92,16 @@ class Reddit(Wrapped): def __init__(self, space_compress = True, nav_menus = None, loginbox = True, infotext = '', content = None, title = '', robots = None, show_sidebar = True, footer = True, **context): - Wrapped.__init__(self, **context) + Templated.__init__(self, **context) self.title = title self.robots = robots self.infotext = infotext self.loginbox = True self.show_sidebar = show_sidebar - self.footer = footer self.space_compress = space_compress - + # instantiate a footer + self.footer = RedditFooter() if footer else None + #put the sort menus at the top self.nav_menu = MenuArea(menus = nav_menus) if nav_menus else None @@ -155,14 +158,14 @@ class Reddit(Wrapped): return ps def render(self, *a, **kw): - """Overrides default Wrapped.render with two additions + """Overrides default Templated.render with two additions * support for rendering API requests with proper wrapping * support for space compression of the result - In adition, unlike Wrapped.render, the result is in the form of a pylons + In adition, unlike Templated.render, the result is in the form of a pylons Response object with it's content set. """ try: - res = Wrapped.render(self, *a, **kw) + res = Templated.render(self, *a, **kw) if is_api(): res = json_respond(res) elif self.space_compress: @@ -200,73 +203,6 @@ class Reddit(Wrapped): css_class = "pref-lang")] return NavMenu(buttons, base_path = "/", type = "flatlist") - def footer_nav(self): - """navigation buttons in the footer.""" - return [NavMenu([NamedButton("toplinks", False), - NamedButton("mobile", False, nocname=True), - OffsiteButton("rss", dest = '/.rss'), - NamedButton("store", False, nocname=True), - NamedButton("stats", False, nocname=True), - NamedButton('random', False, nocname=False), - NamedButton("feedback", False),], - title = _('site links'), type = 'flat_vert', - separator = ''), - - NavMenu([NamedButton("help", False, nocname=True), - OffsiteButton(_("FAQ"), dest = '/help/faq', - nocname=True), - OffsiteButton(_("reddiquette"), nocname=True, - dest = '/help/reddiquette')], - title = _('help'), type = 'flat_vert', - separator = ''), - - NavMenu([NamedButton("bookmarklets", False), - NamedButton("buttons", True), - NamedButton("code", False, nocname=True), - NamedButton("socialite", False), - NamedButton("widget", True), - NamedButton("iphone", False),], - title = _('reddit tools'), type = 'flat_vert', - separator = ''), - - NavMenu([NamedButton("blog", False, nocname=True), - NamedButton("ad_inq", False, nocname=True), - OffsiteButton('reddit.tv', "http://www.reddit.tv"), - OffsiteButton('redditall', "http://www.redditall.com"), - OffsiteButton(_('job board'), - "http://www.redditjobs.com")], - title = _('about us'), type = 'flat_vert', - separator = ''), - NavMenu([OffsiteButton('BaconBuzz', - "http://www.baconbuzz.com"), - OffsiteButton('Destructoid reddit', - "http://reddit.destructoid.com"), - OffsiteButton('TheCuteList', - "http://www.thecutelist.com"), - OffsiteButton('The Independent reddit', - "http://reddit.independent.co.uk"), - OffsiteButton('redditGadgetGuide', - "http://www.redditgadgetguide.com"), - OffsiteButton('WeHeartGossip', - "http://www.weheartgossip.com"), - OffsiteButton('idealistNews', - "http://www.idealistnews.com"),], - title = _('brothers'), type = 'flat_vert', - separator = ''), - NavMenu([OffsiteButton('Wired.com', - "http://www.wired.com"), - OffsiteButton('Ars Technica', - "http://www.arstechnica.com"), - OffsiteButton('Style.com', - "http://www.style.com"), - OffsiteButton('Epicurious.com', - "http://www.epicurious.com"), - OffsiteButton('Concierge.com', - "http://www.concierge.com")], - title = _('sisters'), type = 'flat_vert', - separator = '') - ] - def build_toolbars(self): """Sets the layout of the navigation topbar on a Reddit. The result is a list of menus which will be rendered in order and @@ -275,13 +211,12 @@ class Reddit(Wrapped): NamedButton('new'), NamedButton('controversial'), NamedButton('top'), + NamedButton('saved', False) ] more_buttons = [] if c.user_is_loggedin: - more_buttons.append(NamedButton('saved', False)) - if c.user_is_admin: more_buttons.append(NamedButton('admin')) elif c.site.is_moderator(c.user): @@ -319,35 +254,97 @@ 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): +class RedditHeader(Templated): + def __init__(self): + pass + +class RedditFooter(CachedTemplate): + def cachable_attrs(self): + return [('path', request.path)] + + def nav(self): + return [NavMenu([NamedButton("toplinks", False), + NamedButton("mobile", False, nocname=True), + OffsiteButton("rss", dest = '/.rss'), + NamedButton("store", False, nocname=True), + NamedButton("stats", False, nocname=True), + NamedButton('random', False, nocname=False), + NamedButton("feedback", False),], + title = _('site links'), type = 'flat_vert', + separator = ''), + + NavMenu([NamedButton("help", False, nocname=True), + OffsiteButton(_("FAQ"), dest = '/help/faq', + nocname=True), + OffsiteButton(_("reddiquette"), nocname=True, + dest = '/help/reddiquette')], + title = _('help'), type = 'flat_vert', + separator = ''), + + NavMenu([NamedButton("bookmarklets", False), + NamedButton("buttons", True), + NamedButton("code", False, nocname=True), + NamedButton("socialite", False), + NamedButton("widget", True), + NamedButton("iphone", False),], + title = _('reddit tools'), type = 'flat_vert', + separator = ''), + + NavMenu([NamedButton("blog", False, nocname=True), + NamedButton("ad_inq", False, nocname=True), + OffsiteButton('reddit.tv', "http://www.reddit.tv"), + OffsiteButton('redditall', "http://www.redditall.com"), + OffsiteButton(_('job board'), + "http://www.redditjobs.com")], + title = _('about us'), type = 'flat_vert', + separator = ''), + NavMenu([OffsiteButton('BaconBuzz', + "http://www.baconbuzz.com"), + OffsiteButton('Destructoid reddit', + "http://reddit.destructoid.com"), + OffsiteButton('TheCuteList', + "http://www.thecutelist.com"), + OffsiteButton('The Independent reddit', + "http://reddit.independent.co.uk"), + OffsiteButton('redditGadgetGuide', + "http://www.redditgadgetguide.com"), + OffsiteButton('WeHeartGossip', + "http://www.weheartgossip.com"), + OffsiteButton('idealistNews', + "http://www.idealistnews.com"),], + title = _('brothers'), type = 'flat_vert', + separator = ''), + NavMenu([OffsiteButton('Wired.com', + "http://www.wired.com"), + OffsiteButton('Ars Technica', + "http://www.arstechnica.com"), + OffsiteButton('Style.com', + "http://www.style.com"), + OffsiteButton('Epicurious.com', + "http://www.epicurious.com"), + OffsiteButton('Concierge.com', + "http://www.concierge.com")], + title = _('sisters'), type = 'flat_vert', + separator = '') + ] + + +class ClickGadget(Templated): def __init__(self, links, *a, **kw): self.links = links self.content = '' if c.user_is_loggedin and self.links: self.content = self.make_content() - Wrapped.__init__(self, *a, **kw) + Templated.__init__(self, *a, **kw) def make_content(self): - def wrapper(link): - link.embed_voting_style = 'votable' - return Wrapped(link) - - #temporarily change the render style - orig_render_style = c.render_style - c.render_style = 'htmllite' - #this will disable the hardcoded widget styles request.get.style = "off" + wrapper = default_thing_wrapper(embed_voting_style = 'votable', + style = "htmllite") + content = wrap_links(self.links, wrapper = wrapper) - builder = IDBuilder([link._fullname for link in self.links], - wrap = wrapper) - listing = LinkListing(builder, nextprev=False, show_nums=False).listing() - content = listing.render() - - #restore render style - c.render_style = orig_render_style - - return content + return content.render(style = "htmllite") class RedditMin(Reddit): @@ -357,37 +354,62 @@ class RedditMin(Reddit): show_sidebar = False show_firsttext = False -class LoginFormWide(Wrapped): +class LoginFormWide(CachedTemplate): """generates a login form suitable for the 300px rightbox.""" - pass + def __init__(self): + self.cname = c.cname + self.auth_cname = not c.frameless_cname or c.authorized_cname + CachedTemplate.__init__(self) -class SubredditInfoBar(Wrapped): +class SubredditInfoBar(CachedTemplate): """When not on Default, renders a sidebox which gives info about the current reddit, including links to the moderator and contributor pages, as well as links to the banning page if the current user is a moderator.""" + def __init__(self, site = None): + site = site or c.site + self.spam = site._spam + self.name = site.name + self.type = site.type + self.is_fake = isinstance(site, FakeSubreddit) + self.is_loggedin = c.user_is_loggedin + self.is_admin = c.user_is_admin + self.fullname = site._fullname + self.is_subscriber = bool(c.user_is_loggedin and \ + site.is_subscriber_defaults(c.user)) + self.is_moderator = bool(c.user_is_loggedin and \ + site.is_moderator(c.user)) + self.is_contributor = bool(site.type in ("private", "restricted") and \ + c.user_is_loggedin and \ + site.is_contributor(c.user)) + self.subscribers = site._ups + self.date = site._date + self.spam = site._spam + self.banner = getattr(site, "banner", None) + CachedTemplate.__init__(self) + def nav(self): - is_moderator = c.user_is_loggedin and \ - c.site.is_moderator(c.user) or c.user_is_admin - buttons = [NavButton(plurals.moderators, 'moderators')] - if c.site.type != 'public': + if self.type != 'public': buttons.append(NavButton(plurals.contributors, 'contributors')) - if is_moderator: + if self.is_moderator: buttons.append(NamedButton('edit')) buttons.extend([NavButton(menu.banusers, 'banned'), NamedButton('spam')]) buttons.append(NamedButton('traffic')) return [NavMenu(buttons, type = "flatlist", base_path = "/about/")] -class SideBox(Wrapped): - """Generic sidebox used to generate the 'submit' and 'create a reddit' boxes.""" +class SideBox(CachedTemplate): + """ + Generic sidebox used to generate the 'submit' and 'create a reddit' boxes. + """ def __init__(self, title, link, css_class='', subtitles = [], show_cover = False, nocname=False, sr_path = False): - Wrapped.__init__(self, link = link, target = '_top', - title = title, css_class = css_class, sr_path = sr_path, - subtitles = subtitles, show_cover = show_cover, nocname=nocname) + Templated.__init__(self, link = link, target = '_top', + title = title, css_class = css_class, + sr_path = sr_path, subtitles = subtitles, + show_cover = show_cover, nocname=nocname) class PrefsPage(Reddit): @@ -408,16 +430,16 @@ class PrefsPage(Reddit): return [PageNameNav('nomenu', title = _("preferences")), NavMenu(buttons, base_path = "/prefs", type="tabmenu")] -class PrefOptions(Wrapped): +class PrefOptions(Templated): """Preference form for updating language and display options""" def __init__(self, done = False): - Wrapped.__init__(self, done = done) + Templated.__init__(self, done = done) -class PrefUpdate(Wrapped): +class PrefUpdate(Templated): """Preference form for updating email address and passwords""" pass -class PrefDelete(Wrapped): +class PrefDelete(Templated): """preference form for deleting a user's own account.""" pass @@ -445,11 +467,11 @@ class MessagePage(Reddit): return [PageNameNav('nomenu', title = _("message")), NavMenu(buttons, base_path = "/message", type="tabmenu")] -class MessageCompose(Wrapped): +class MessageCompose(Templated): """Compose message form.""" def __init__(self,to='', subject='', message='', success='', captcha = None): - Wrapped.__init__(self, to = to, subject = subject, + Templated.__init__(self, to = to, subject = subject, message = message, success = success, captcha = captcha) @@ -498,15 +520,11 @@ class LoginPage(BoringPage): kw[x] = getattr(self, x) if hasattr(self, x) else '' return Login(dest = self.dest, **kw) -class Login(Wrapped): +class Login(Templated): """The two-unit login and register form.""" def __init__(self, user_reg = '', user_login = '', dest=''): - Wrapped.__init__(self, - user_reg = user_reg, - user_login = user_login, - dest = dest, - captcha = Captcha()) - + Templated.__init__(self, user_reg = user_reg, user_login = user_login, + dest = dest, captcha = Captcha()) class SearchPage(BoringPage): """Search results page""" @@ -522,7 +540,7 @@ class SearchPage(BoringPage): return self.content_stack((self.searchbar, self.infobar, self.nav_menu, self._content)) -class CommentsPanel(Wrapped): +class CommentsPanel(Templated): """the side-panel on the reddit toolbar frame that shows the top comments of a link""" @@ -531,7 +549,7 @@ class CommentsPanel(Wrapped): self.listing = listing self.expanded = expanded - Wrapped.__init__(self, *a, **kw) + Templated.__init__(self, *a, **kw) class LinkInfoPage(Reddit): """Renders the varied /info pages for a link. The Link object is @@ -549,14 +567,10 @@ class LinkInfoPage(Reddit): def __init__(self, link = None, comment = None, link_title = '', subtitle = None, duplicates = None, *a, **kw): - from r2.controllers.listingcontroller import ListingController - wrapper = make_wrapper(ListingController.builder_wrapper, - expand_children = True) - link_builder = IDBuilder(link._fullname, - wrap = wrapper) + wrapper = default_thing_wrapper(expand_children = True) # link_listing will be the one-element listing at the top - self.link_listing = LinkListing(link_builder, nextprev=False).listing() + self.link_listing = wrap_links(link, wrapper = wrapper) # link is a wrapped Link object self.link = self.link_listing.things[0] @@ -624,10 +638,12 @@ class LinkInfoPage(Reddit): rb.insert(1, LinkInfoBar(a = self.link)) return rb -class LinkInfoBar(Wrapped): +class LinkInfoBar(Templated): """Right box for providing info about a link.""" def __init__(self, a = None): - Wrapped.__init__(self, a = a, datefmt = datefmt) + if a: + a = Wrapped(a) + Templated.__init__(self, a = a, datefmt = datefmt) class EditReddit(Reddit): """Container for the about page for a reddit""" @@ -753,23 +769,23 @@ class ProfilePage(Reddit): rb.append(AdminSidebar(self.user)) return rb -class ProfileBar(Wrapped): +class ProfileBar(Templated): """Draws a right box for info about the user (karma, etc)""" def __init__(self, user): - Wrapped.__init__(self, user = user) + Templated.__init__(self, user = user) self.isFriend = self.user._id in c.user.friends \ if c.user_is_loggedin else False self.isMe = (self.user == c.user) -class MenuArea(Wrapped): +class MenuArea(Templated): """Draws the gray box at the top of a page for sort menus""" def __init__(self, menus = []): - Wrapped.__init__(self, menus = menus) + Templated.__init__(self, menus = menus) -class InfoBar(Wrapped): +class InfoBar(Templated): """Draws the yellow box at the top of a page for info""" def __init__(self, message = ''): - Wrapped.__init__(self, message = message) + Templated.__init__(self, message = message) class RedditError(BoringPage): @@ -789,126 +805,126 @@ class Reddit404(BoringPage): show_sidebar = False, content=UnfoundPage(ch)) -class UnfoundPage(Wrapped): +class UnfoundPage(Templated): """Wrapper for the 404 page""" def __init__(self, choice): - Wrapped.__init__(self, choice = choice) + Templated.__init__(self, choice = choice) -class ErrorPage(Wrapped): +class ErrorPage(Templated): """Wrapper for an error message""" def __init__(self, message = _("you aren't allowed to do that.")): - Wrapped.__init__(self, message = message) + Templated.__init__(self, message = message) -class Profiling(Wrapped): +class Profiling(Templated): """Debugging template for code profiling using built in python library (only used in middleware)""" def __init__(self, header = '', table = [], caller = [], callee = [], path = ''): - Wrapped.__init__(self, header = header, table = table, caller = caller, + Templated.__init__(self, header = header, table = table, caller = caller, callee = callee, path = path) -class Over18(Wrapped): +class Over18(Templated): """The creepy 'over 18' check page for nsfw content.""" pass -class SubredditTopBar(Wrapped): +class SubredditTopBar(Templated): """The horizontal strip at the top of most pages for navigating user-created reddits.""" def __init__(self): - Wrapped.__init__(self) + Templated.__init__(self) - my_reddits = Subreddit.user_subreddits(c.user, ids = False) - my_reddits.sort(key = lambda sr: sr.name.lower()) + self.my_reddits = Subreddit.user_subreddits(c.user, ids = False) + self.my_reddits.sort(key = lambda sr: sr.name.lower()) - drop_down_buttons = [] - for sr in my_reddits: - drop_down_buttons.append(SubredditButton(sr)) - - #leaving the 'home' option out for now - #drop_down_buttons.insert(0, NamedButton('home', sr_path = False, - # css_class = 'top-option', - # dest = '/')) - drop_down_buttons.append(NamedButton('edit', sr_path = False, - css_class = 'bottom-option', - dest = '/reddits/')) - self.sr_dropdown = SubredditMenu(drop_down_buttons, - title = _('my reddits'), - type = 'srdrop') pop_reddits = Subreddit.default_subreddits(ids = False, limit = Subreddit.sr_limit) - buttons = [SubredditButton(sr) for sr in c.recent_reddits] + self.reddits = c.recent_reddits for sr in pop_reddits: if sr not in c.recent_reddits: - buttons.append(SubredditButton(sr)) + self.reddits.append(sr) + + def my_reddits_dropdown(self): + drop_down_buttons = [] + for sr in self.my_reddits: + drop_down_buttons.append(SubredditButton(sr)) + drop_down_buttons.append(NamedButton('edit', sr_path = False, + css_class = 'bottom-option', + dest = '/reddits/')) + return SubredditMenu(drop_down_buttons, + title = _('my reddits'), + type = 'srdrop') + - self.sr_bar = NavMenu(buttons, type='flatlist', separator = '-', - _id = 'sr-bar') - -class SubscriptionBox(Wrapped): + def recent_reddits(self): + return NavMenu([SubredditButton(sr) for sr in self.reddits], + type='flatlist', separator = '-', + _id = 'sr-bar') + +class SubscriptionBox(Templated): """The list of reddits a user is currently subscribed to to go in the right pane.""" def __init__(self): srs = Subreddit.user_subreddits(c.user, ids = False) srs.sort(key = lambda sr: sr.name.lower()) - b = IDBuilder([sr._fullname for sr in srs]) - self.reddits = LinkListing(b).listing().things + self.reddits = wrap_links(srs) + Templated.__init__(self) -class CreateSubreddit(Wrapped): +class CreateSubreddit(Templated): """reddit creation form.""" def __init__(self, site = None, name = ''): - Wrapped.__init__(self, site = site, name = name) + Templated.__init__(self, site = site, name = name) -class SubredditStylesheet(Wrapped): +class SubredditStylesheet(Templated): """form for editing or creating subreddit stylesheets""" def __init__(self, site = None, stylesheet_contents = ''): - Wrapped.__init__(self, site = site, + Templated.__init__(self, site = site, stylesheet_contents = stylesheet_contents) -class CssError(Wrapped): +class CssError(Templated): """Rendered error returned to the stylesheet editing page via ajax""" def __init__(self, error): # error is an instance of cssutils.py:ValidationError - Wrapped.__init__(self, error = error) + Templated.__init__(self, error = error) -class UploadedImage(Wrapped): +class UploadedImage(Templated): "The page rendered in the iframe during an upload of a header image" def __init__(self,status,img_src, name="", errors = {}): self.errors = list(errors.iteritems()) - Wrapped.__init__(self, status=status, img_src=img_src, name = name) + Templated.__init__(self, status=status, img_src=img_src, name = name) -class Password(Wrapped): +class Password(Templated): """Form encountered when 'recover password' is clicked in the LoginFormWide.""" def __init__(self, success=False): - Wrapped.__init__(self, success = success) + Templated.__init__(self, success = success) -class PasswordReset(Wrapped): +class PasswordReset(Templated): """Template for generating an email to the user who wishes to reset their password (step 2 of password recovery, after they have entered their user name in Password.)""" pass -class ResetPassword(Wrapped): +class ResetPassword(Templated): """Form for actually resetting a lost password, after the user has clicked on the link provided to them in the Password_Reset email (step 3 of password recovery.)""" pass -class Captcha(Wrapped): +class Captcha(Templated): """Container for rendering robot detection device.""" def __init__(self, error=None): self.error = _('try entering those letters again') if error else "" self.iden = get_captcha() - Wrapped.__init__(self) + Templated.__init__(self) -class PermalinkMessage(Wrapped): +class PermalinkMessage(Templated): """renders the box on comment pages that state 'you are viewing a single comment's thread'""" def __init__(self, comments_url): - Wrapped.__init__(self, comments_url = comments_url) + Templated.__init__(self, comments_url = comments_url) -class PaneStack(Wrapped): +class PaneStack(Templated): """Utility class for storing and rendering a list of block elements.""" def __init__(self, panes=[], div_id = None, css_class=None, div=False, @@ -919,7 +935,7 @@ class PaneStack(Wrapped): self.div = div self.stack = list(panes) self.title = title - Wrapped.__init__(self) + Templated.__init__(self) def append(self, item): """Appends an element to the end of the current stack""" @@ -934,14 +950,14 @@ class PaneStack(Wrapped): return self.stack.insert(*a) -class SearchForm(Wrapped): +class SearchForm(Templated): """The simple search form in the header of the page. prev_search is the previous search.""" def __init__(self, prev_search = ''): - Wrapped.__init__(self, prev_search = prev_search) + Templated.__init__(self, prev_search = prev_search) -class SearchBar(Wrapped): +class SearchBar(Templated): """More detailed search box for /search and /reddits pages. Displays the previous search as well as info of the elapsed_time and num_results if any.""" @@ -959,10 +975,10 @@ class SearchBar(Wrapped): else: self.num_results = num_results - Wrapped.__init__(self) + Templated.__init__(self) -class Frame(Wrapped): +class Frame(Templated): """Frameset for the FrameToolbar used when a user hits /tb/. The top 30px of the page are dedicated to the toolbar, while the rest of the page will show the results of following the link.""" @@ -973,60 +989,60 @@ class Frame(Wrapped): domain = g.domain)) else: title = g.domain - - Wrapped.__init__(self, url = url, title = title, fullname = fullname) + Templated.__init__(self, url = url, title = title, fullname = fullname) dorks_re = re.compile(r"https?://?([-\w.]*\.)?digg\.com/\w+\.\w+(/|$)") class FrameToolbar(Wrapped): """The reddit voting toolbar used together with Frame.""" - def __init__(self, link = None, title = None, url = None, expanded = False, **kw): - self.title = title - self.url = url - self.expanded = expanded - self.link = link - - self.dorks = dorks_re.match(link.url if link else url) - - if link: - self.tblink = add_sr("/tb/"+link._id36) - - likes = link.likes - self.upstyle = "mod" if likes else "" - self.downstyle = "mod" if likes is False else "" - if c.user_is_loggedin: - self.vh = vote_hash(c.user, link, 'valid') - score = link.score - - if not link.num_comments: - # generates "comment" the imperative verb - self.com_label = _("comment {verb}") - else: - # generates "XX comments" as a noun - com_label = ungettext("comment", "comments", - link.num_comments) - self.com_label = strings.number_label % dict( - num = link.num_comments, thing = com_label) - - - # generates "XX points" as a noun - self.score_label = Score.safepoints(score) - - else: - self.tblink = add_sr("/s/"+quote(url)) - submit_url_options = dict(url = _force_unicode(url), - then = 'tb') - if title: - submit_url_options['title'] = _force_unicode(title) - self.submit_url = add_sr('/submit' + query_string(submit_url_options)) - - if not c.user_is_loggedin: - self.loginurl = add_sr("/login?dest="+quote(self.tblink)) - - Wrapped.__init__(self, **kw) + cachable = True extension_handling = False + cache_ignore = Link.cache_ignore + def __init__(self, link, title = None, url = None, expanded = False, **kw): + if link: + self.title = link.title + self.url = link.url + else: + self.title = title + self.url = url -class NewLink(Wrapped): + self.expanded = expanded + + self.dorks = dorks_re.match(self.url) + Wrapped.__init__(self, link) + if link is None: + self.add_props(c.user, [self]) + + @classmethod + def add_props(cls, user, wrapped): + # unlike most wrappers we can guarantee that there is a link + # that this wrapper is wrapping. + nonempty = [w for w in wrapped if hasattr(w, "_fullname")] + Link.add_props(user, nonempty) + for w in wrapped: + w.score_fmt = Score.points + if not hasattr(w, '_fullname'): + w._fullname = None + w.tblink = add_sr("/s/"+quote(w.url)) + submit_url_options = dict(url = _force_unicode(w.url), + then = 'tb') + if w.title: + submit_url_options['title'] = _force_unicode(w.title) + w.submit_url = add_sr('/submit' + + query_string(submit_url_options)) + else: + w.tblink = add_sr("/tb/"+w._id36) + + w.upstyle = "mod" if w.likes else "" + w.downstyle = "mod" if w.likes is False else "" + if not c.user_is_loggedin: + w.loginurl = add_sr("/login?dest="+quote(w.tblink)) + # run to set scores with current score format (for example) + Printable.add_props(user, nonempty) + + + +class NewLink(Templated): """Render the link submission form""" def __init__(self, captcha = None, url = '', title= '', subreddits = (), then = 'comments'): @@ -1059,34 +1075,37 @@ class NewLink(Wrapped): else: self.default_sr = c.site.name - Wrapped.__init__(self, captcha = captcha, url = url, + Templated.__init__(self, captcha = captcha, url = url, title = title, subreddits = subreddits, then = then) -class ShareLink(Wrapped): +class ShareLink(CachedTemplate): def __init__(self, link_name = "", emails = None): - captcha = Captcha() if c.user.needs_captcha() else None - Wrapped.__init__(self, link_name = link_name, - emails = c.user.recent_share_emails(), - captcha = captcha) + self.captcha = c.user.needs_captcha() + self.email = getattr(c.user, 'email', "") + self.username = c.user.name + Templated.__init__(self, link_name = link_name, + emails = c.user.recent_share_emails()) -class Share(Wrapped): + + +class Share(Templated): pass -class Mail_Opt(Wrapped): +class Mail_Opt(Templated): pass -class OptOut(Wrapped): +class OptOut(Templated): pass -class OptIn(Wrapped): +class OptIn(Templated): pass -class UserStats(Wrapped): +class UserStats(Templated): """For drawing the stats page, which is fetched from the cache.""" def __init__(self): - Wrapped.__init__(self) + Templated.__init__(self) cache_stats = cache.get('stats') if cache_stats: top_users, top_day, top_week = cache_stats @@ -1105,38 +1124,46 @@ class UserStats(Wrapped): self.top_users = self.top_day = self.top_week = () -class ButtonEmbed(Wrapped): +class ButtonEmbed(Templated): """Generates the JS wrapper around the buttons for embedding.""" - def __init__(self, button = None, width = 100, height=100, referer = "", url = ""): - Wrapped.__init__(self, button = button, width = width, height = height, - referer=referer, url = url) + def __init__(self, button = None, width = 100, + height=100, referer = "", url = "", **kw): + Templated.__init__(self, button = button, + width = width, height = height, + referer=referer, url = url, **kw) -class ButtonLite(Wrapped): - """Generates the JS wrapper around the buttons for embedding.""" - def __init__(self, image = None, link = None, url = "", styled = True, target = '_top'): - Wrapped.__init__(self, image = image, link = link, url = url, styled = styled, target = target) - class Button(Wrapped): - """the voting buttons, embedded with the ButtonEmbed wrapper, shown on /buttons""" + cachable = True extension_handling = False - def __init__(self, link = None, button = None, css=None, - url = None, title = '', score_fmt = None, vote = True, target = "_parent", - bgcolor = None, width = 100): - Wrapped.__init__(self, link = link, score_fmt = score_fmt, - likes = link.likes if link else None, - button = button, css = css, url = url, title = title, - vote = vote, target = target, bgcolor=bgcolor, width=width) + def __init__(self, link, **kw): + Wrapped.__init__(self, link, **kw) + if link is None: + self.add_props(c.user, [self]) + + + @classmethod + def add_props(cls, user, wrapped): + # unlike most wrappers we can guarantee that there is a link + # that this wrapper is wrapping. + Link.add_props(user, [w for w in wrapped if hasattr(w, "_fullname")]) + for w in wrapped: + if not hasattr(w, '_fullname'): + w._fullname = None + +class ButtonLite(Button): + pass + class ButtonNoBody(Button): """A button page that just returns the raw button for direct embeding""" pass -class ButtonDemoPanel(Wrapped): +class ButtonDemoPanel(Templated): """The page for showing the different styles of embedable voting buttons""" pass -class Feedback(Wrapped): +class Feedback(Templated): """The feedback and ad inquery form(s)""" def __init__(self, title, action): email = name = '' @@ -1148,7 +1175,7 @@ class Feedback(Wrapped): if not c.user_is_loggedin or c.user.needs_captcha(): captcha = Captcha() - Wrapped.__init__(self, + Templated.__init__(self, captcha = captcha, title = title, action = action, @@ -1156,15 +1183,15 @@ class Feedback(Wrapped): name = name) -class WidgetDemoPanel(Wrapped): +class WidgetDemoPanel(Templated): """Demo page for the .embed widget.""" pass -class Socialite(Wrapped): +class Socialite(Templated): """Demo page for the socialite Firefox extension""" pass -class Bookmarklets(Wrapped): +class Bookmarklets(Templated): """The bookmarklets page.""" def __init__(self, buttons=None): if buttons is None: @@ -1173,35 +1200,35 @@ class Bookmarklets(Wrapped): # unathorised cname. See toolbar.py:GET_s for discussion if not (c.cname and c.site.domain not in g.authorized_cnames): buttons.insert(0, "reddit toolbar") - Wrapped.__init__(self, buttons = buttons) + Templated.__init__(self, buttons = buttons) -class AdminTranslations(Wrapped): +class AdminTranslations(Templated): """The translator control interface, used for determining which user is allowed to edit which translation file and for providing a summary of what translation files are done and/or in use.""" def __init__(self): from r2.lib.translation import list_translations - Wrapped.__init__(self) + Templated.__init__(self) self.translations = list_translations() -class Embed(Wrapped): +class Embed(Templated): """wrapper for embedding /help into reddit as if it were not on a separate wiki.""" def __init__(self,content = ''): - Wrapped.__init__(self, content = content) + Templated.__init__(self, content = content) -class Page_down(Wrapped): +class Page_down(Templated): def __init__(self, **kw): message = kw.get('message', _("This feature is currently unavailable. Sorry")) - Wrapped.__init__(self, message = message) + Templated.__init__(self, message = message) # Classes for dealing with friend/moderator/contributor/banned lists -class UserTableItem(Wrapped): +class UserTableItem(Templated): """A single row in a UserList of type 'type' and of name 'container_name' for a given user. The provided list of 'cells' will determine what order the different columns are rendered in. @@ -1211,12 +1238,12 @@ class UserTableItem(Wrapped): self.user, self.type, self.cells = user, type, cellnames self.container_name = container_name self.editable = editable - Wrapped.__init__(self) + Templated.__init__(self) def __repr__(self): return '' % self.user.name -class UserList(Wrapped): +class UserList(Templated): """base class for generating a list of users""" form_title = '' table_title = '' @@ -1227,7 +1254,7 @@ class UserList(Wrapped): def __init__(self, editable = True): self.editable = editable - Wrapped.__init__(self) + Templated.__init__(self) def user_row(self, user): """Convenience method for constructing a UserTableItem @@ -1329,10 +1356,10 @@ class DetailsPage(LinkInfoPage): from admin_pages import Details return self.content_stack((self.link_listing, Details(link = self.link))) -class Cnameframe(Wrapped): +class Cnameframe(Templated): """The frame page.""" def __init__(self, original_path, subreddit, sub_domain): - Wrapped.__init__(self, original_path=original_path) + Templated.__init__(self, original_path=original_path) if sub_domain and subreddit and original_path: self.title = "%s - %s" % (subreddit.title, sub_domain) u = UrlParser(subreddit.path + original_path) @@ -1344,7 +1371,7 @@ class Cnameframe(Wrapped): self.title = "" self.frame_target = None -class FrameBuster(Wrapped): +class FrameBuster(Templated): pass class PromotePage(Reddit): @@ -1366,37 +1393,31 @@ class PromotePage(Reddit): Reddit.__init__(self, title, nav_menus = nav_menus, *a, **kw) -class PromotedLinks(Wrapped): +class PromotedLinks(Templated): def __init__(self, current_list, *a, **kw): self.things = current_list self.recent = dict(load_summary("thing")) if self.recent: - from r2.models import Link - # TODO: temp hack until we find place for builder_wrapper - from r2.controllers.listingcontroller import ListingController - builder = IDBuilder(self.recent.keys(), - wrap = ListingController.builder_wrapper) - link_listing = LinkListing(builder, nextprev=False).listing() - - for t in link_listing.things: + link_listing = wrap_links(self.recent.keys()) + for t in link_listing: self.recent[t._fullname].insert(0, t) self.recent = self.recent.values() self.recent.sort(key = lambda x: x[0]._date) - Wrapped.__init__(self, datefmt = datefmt, *a, **kw) + Templated.__init__(self, datefmt = datefmt, *a, **kw) -class PromoteLinkForm(Wrapped): +class PromoteLinkForm(Templated): def __init__(self, sr = None, link = None, listing = '', timedeltatext = '', *a, **kw): - Wrapped.__init__(self, sr = sr, link = link, + Templated.__init__(self, sr = sr, link = link, datefmt = datefmt, timedeltatext = timedeltatext, listing = listing, *a, **kw) -class TabbedPane(Wrapped): +class TabbedPane(Templated): def __init__(self, tabs): """Renders as tabbed area where you can choose which tab to render. Tabs is a list of tuples (tab_name, tab_pane).""" @@ -1407,15 +1428,14 @@ class TabbedPane(Wrapped): self.tabmenu = JsNavMenu(buttons, type = 'tabpane') self.tabs = tabs - Wrapped.__init__(self) + Templated.__init__(self) -class LinkChild(Wrapped): +class LinkChild(object): def __init__(self, link, load = False, expand = False, nofollow = False): self.link = link self.expand = expand self.load = load or expand self.nofollow = nofollow - Wrapped.__init__(self) def content(self): return '' @@ -1431,15 +1451,13 @@ class SelfTextChild(LinkChild): u = UserText(self.link, self.link.selftext, editable = c.user == self.link.author, nofollow = self.nofollow) - #have to force the render style to html for now cause of some - #c.render_style weirdness - return u.render(style = 'html') + return u.render() -class SelfText(Wrapped): +class SelfText(Templated): def __init__(self, link): - Wrapped.__init__(self, link = link) + Templated.__init__(self, link = link) -class UserText(Wrapped): +class UserText(CachedTemplate): def __init__(self, item, text = '', @@ -1459,23 +1477,20 @@ class UserText(Wrapped): if extra_css: css_class += " " + extra_css - Wrapped.__init__(self, - item = item, - text = text, - have_form = have_form, - editable = editable, - creating = creating, - nofollow = nofollow, - target = target, - display = display, - post_form = post_form, - cloneable = cloneable, - css_class = css_class) + CachedTemplate.__init__(self, + fullname = item._fullname if item else "", + text = text, + have_form = have_form, + editable = editable, + creating = creating, + nofollow = nofollow, + target = target, + display = display, + post_form = post_form, + cloneable = cloneable, + css_class = css_class) - def button(self): - pass - -class Traffic(Wrapped): +class Traffic(Templated): @staticmethod def slice_traffic(traffic, *indices): return [[a] + [b[i] for i in indices] for a, b in traffic] @@ -1525,7 +1540,7 @@ class PromotedTraffic(Traffic): cli_total)) else: self.imp_graph = self.cli_graph = None - Wrapped.__init__(self) + Templated.__init__(self) class RedditTraffic(Traffic): """ @@ -1579,7 +1594,7 @@ class RedditTraffic(Traffic): uni_by_day[d.weekday()].append(float(uniques[i])) self.uniques_by_dow = [sum(x)/len(x) for x in uni_by_day] self.impressions_by_dow = [sum(x)/len(x) for x in imp_by_day] - Wrapped.__init__(self) + Templated.__init__(self) def reddits_summary(self): if c.default_sr: @@ -1633,7 +1648,6 @@ class RedditTraffic(Traffic): "%5.2f%%" % f)) return res -class InnerToolbarFrame(Wrapped): +class InnerToolbarFrame(Templated): def __init__(self, link, expanded = False): - Wrapped.__init__(self, link = link, expanded = expanded) - + Templated.__init__(self, link = link, expanded = expanded) diff --git a/r2/r2/lib/services.py b/r2/r2/lib/services.py index 0a06b6dc7..dfb034106 100644 --- a/r2/r2/lib/services.py +++ b/r2/r2/lib/services.py @@ -23,7 +23,7 @@ from __future__ import with_statement import os, re, sys, socket, time, random, time, signal from itertools import chain -from wrapped import Wrapped +from wrapped import Templated from datetime import datetime, timedelta from pylons import g from r2.lib.utils import tup @@ -58,7 +58,7 @@ class ShellProcess(object): return self.output -class AppServiceMonitor(Wrapped): +class AppServiceMonitor(Templated): cache_key = "service_datalogger_data_" cache_key_small = "service_datalogger_db_summary_" cache_lifetime = "memcached_lifetime" @@ -99,7 +99,7 @@ class AppServiceMonitor(Wrapped): self._db_info = db_info self.hostlogs = [] - Wrapped.__init__(self) + Templated.__init__(self) @classmethod def set_cache_lifetime(cls, data): @@ -145,7 +145,7 @@ class AppServiceMonitor(Wrapped): def render(self, *a, **kw): self.hostlogs = list(self) - return Wrapped.render(self, *a, **kw) + return Templated.render(self, *a, **kw) def monitor(self, srvname, loop = True, loop_time = 5, *a, **kw): diff --git a/r2/r2/lib/strings.py b/r2/r2/lib/strings.py index cbe8875be..908d64240 100644 --- a/r2/r2/lib/strings.py +++ b/r2/r2/lib/strings.py @@ -213,7 +213,7 @@ class Score(object): fasion, used primarily by the score() method in printable.html""" @staticmethod def number_only(x): - return max(x, 0) + return str(max(x, 0)) @staticmethod def points(x): diff --git a/r2/r2/lib/template_helpers.py b/r2/r2/lib/template_helpers.py index 8c92cb184..12388ea73 100644 --- a/r2/r2/lib/template_helpers.py +++ b/r2/r2/lib/template_helpers.py @@ -20,16 +20,16 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from r2.models import * -from r2.lib.jsontemplates import is_api from filters import unsafe, websafe -from r2.lib.utils import vote_hash, UrlParser +from r2.lib.utils import vote_hash, UrlParser, timesince from mako.filters import url_escape import simplejson import os.path from copy import copy import random -from pylons import i18n, g, c +from pylons import g, c +from pylons.i18n import _, ungettext def static(file): """ @@ -76,7 +76,6 @@ def class_dict(): res = ', '.join(classes) return unsafe('{ %s }' % res) - def path_info(): loc = dict(path = request.path, params = dict(request.get)) @@ -84,82 +83,86 @@ def path_info(): return unsafe(simplejson.dumps(loc)) -def replace_render(listing, item, style = None, display = True): - style = style or c.render_style or 'html' - rendered_item = item.render(style = style) - # for string rendered items - def string_replace(x, y): - return rendered_item.replace(x, y) - - # for JSON responses - def dict_replace(x, y): - try: - res = rendered_item['data']['content'] - rendered_item['data']['content'] = res.replace(x, y) - except AttributeError: - pass - except TypeError: - pass - return rendered_item - - if is_api(): - child_txt = "" - else: - child_txt = ( hasattr(item, "child") and item.child )\ - and item.child.render(style = style) or "" - - # handle API calls differently from normal request: dicts not strings are passed around - if isinstance(rendered_item, dict): - replace_fn = dict_replace - try: - rendered_item['data']['child'] = child_txt - except AttributeError: - pass - except TypeError: - pass - else: - replace_fn = string_replace - rendered_item = replace_fn(u"$child", child_txt) - - #only LinkListing has a show_nums attribute - if listing: - if hasattr(listing, "show_nums"): - if listing.show_nums: - num_str = str(item.num) - if hasattr(listing, "num_margin"): - num_margin = listing.num_margin - else: - num_margin = "%.2fex" % (len(str(listing.max_num))*1.1) - else: - num_str = '' - num_margin = "0px" +def replace_render(listing, item, render_func): + def _replace_render(style = None, display = True): + """ + A helper function for listings to set uncachable attributes on a + rendered thing (item) to its proper display values for the current + context. + """ + style = style or c.render_style or 'html' + replacements = {} - rendered_item = replace_fn(u"$numcolmargin", num_margin) - rendered_item = replace_fn(u"$num", num_str) - - if hasattr(listing, "max_score"): - mid_margin = len(str(listing.max_score)) - if hasattr(listing, "mid_margin"): - mid_margin = listing.mid_margin - elif mid_margin == 1: - mid_margin = "15px" + child_txt = ( hasattr(item, "child") and item.child )\ + and item.child.render(style = style) or "" + replacements["childlisting"] = child_txt + + + #only LinkListing has a show_nums attribute + if listing: + if hasattr(listing, "show_nums"): + if listing.show_nums: + num_str = str(item.num) + if hasattr(listing, "num_margin"): + num_margin = str(listing.num_margin) + else: + num_margin = "%.2fex" % (len(str(listing.max_num))*1.1) + else: + num_str = '' + num_margin = "0px" + + replacements["numcolmargin"] = num_margin + replacements["num"] = num_str + + if hasattr(listing, "max_score"): + mid_margin = len(str(listing.max_score)) + if hasattr(listing, "mid_margin"): + mid_margin = str(listing.mid_margin) + elif mid_margin == 1: + mid_margin = "15px" + else: + mid_margin = "%dex" % (mid_margin+1) + + replacements["midcolmargin"] = mid_margin + + #$votehash is only present when voting arrows are present + if c.user_is_loggedin: + replacements['votehash'] = vote_hash(c.user, item, + listing.vote_hash_type) + if hasattr(item, "num_comments"): + if not item.num_comments: + # generates "comment" the imperative verb + com_label = _("comment {verb}") + com_cls = 'comments empty' else: - mid_margin = "%dex" % (mid_margin+1) + # generates "XX comments" as a noun + com_label = ungettext("comment", "comments", item.num_comments) + com_label = strings.number_label % dict(num=item.num_comments, + thing=com_label) + com_cls = 'comments' + replacements['numcomments'] = com_label + replacements['commentcls'] = com_cls + + replacements['display'] = "" if display else "style='display:none'" + + if hasattr(item, "render_score"): + # replace the score stub + (replacements['scoredislikes'], + replacements['scoreunvoted'], + replacements['scorelikes']) = item.render_score + + # compute the timesince here so we don't end up caching it + if hasattr(item, "_date"): + replacements['timesince'] = timesince(item._date) - rendered_item = replace_fn(u"$midcolmargin", mid_margin) - - # TODO: one of these things is not like the other. We should & -> - # $ elsewhere as it plays nicer with the websafe filter. - rendered_item = replace_fn(u"$ListClass", listing._js_cls) - - #$votehash is only present when voting arrows are present - if c.user_is_loggedin and u'$votehash' in rendered_item: - hash = vote_hash(c.user, item, listing.vote_hash_type) - rendered_item = replace_fn(u'$votehash', hash) - - rendered_item = replace_fn(u"$display", "" if display else "style='display:none'") - return rendered_item + renderer = render_func or item.render + res = renderer(style = style, **replacements) + if isinstance(res, (str, unicode)): + return unsafe(res) + return res + + return _replace_render def get_domain(cname = False, subreddit = True, no_www = False): """ @@ -181,15 +184,19 @@ def get_domain(cname = False, subreddit = True, no_www = False): the trailing path). """ + # locally cache these lookups as this gets run in a loop in add_props domain = g.domain - if not no_www and g.domain_prefix: - domain = g.domain_prefix + "." + g.domain - if cname and c.cname and c.site.domain: - domain = c.site.domain + domain_prefix = g.domain_prefix + site = c.site + ccname = c.cname + if not no_www and domain_prefix: + domain = domain_prefix + "." + domain + if cname and ccname and site.domain: + domain = site.domain if hasattr(request, "port") and request.port: domain += ":" + str(request.port) - if (not c.cname or not cname) and subreddit: - domain += c.site.path.rstrip('/') + if (not ccname or not cname) and subreddit: + domain += site.path.rstrip('/') return domain def dockletStr(context, type, browser): @@ -241,6 +248,9 @@ def add_sr(path, sr_path = True, nocname=False, force_hostname = False): * sr_path: if a cname is not used for the domain, updates the path to include c.site.path. + + For caching purposes: note that this function uses: + c.cname, c.render_style, c.site.name """ # don't do anything if it is just an anchor if path.startswith('#') or path.startswith('javascript:'): @@ -289,7 +299,7 @@ def choose_width(link, width): if width: return width - 5 else: - if link: + if hasattr(link, "_ups"): return 100 + (10 * (len(str(link._ups - link._downs)))) else: return 110 diff --git a/r2/r2/lib/translation.py b/r2/r2/lib/translation.py index f942eaee7..5d104d51f 100644 --- a/r2/r2/lib/translation.py +++ b/r2/r2/lib/translation.py @@ -27,7 +27,7 @@ from pylons.i18n import _ from babel import Locale import os, re import cPickle as pickle -from wrapped import Wrapped +from wrapped import Templated from utils import Storage from md5 import md5 @@ -98,7 +98,7 @@ def hax(string): return hax_dict.get(string, string) -class TranslatedString(Wrapped): +class TranslatedString(Templated): class _SubstString: def __init__(self, string, enabled = True): self.str = hax(string) @@ -157,7 +157,7 @@ class TranslatedString(Wrapped): def __init__(self, translator, sing, plural = '', message = '', enabled = True, locale = '', tip = '', index = 0): - Wrapped.__init__(self) + Templated.__init__(self) self.translator = translator self.message = message diff --git a/r2/r2/lib/wrapped.py b/r2/r2/lib/wrapped.py index 5191fbebe..79553b5d4 100644 --- a/r2/r2/lib/wrapped.py +++ b/r2/r2/lib/wrapped.py @@ -19,29 +19,428 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ -from filters import unsafe -from utils import storage - from itertools import chain -import sys - -sys.setrecursionlimit(500) +from datetime import datetime +import re, types class NoTemplateFound(Exception): pass -class Wrapped(object): +class StringTemplate(object): + """ + Simple-minded string templating, where variables of the for $____ + in a strinf are replaced with values based on a dictionary. + + Unline the built-in Template class, this supports an update method + + We could use the built in python Template class for this, but + unfortunately it doesn't handle unicode as gracefully as we'd + like. + """ + start_delim = "<$>" + end_delim = "" + pattern2 = r"[_a-z][_a-z0-9]*" + pattern2 = r"%(start_delim)s(?:(?P%(pattern)s))%(end_delim)s" % \ + dict(pattern = pattern2, + start_delim = re.escape(start_delim), + end_delim = re.escape(end_delim), + ) + pattern2 = re.compile(pattern2, re.UNICODE) + + def __init__(self, template): + # for the nth time, we have to transform the string into + # unicode. Otherwise, re.sub will choke on non-ascii + # characters. + try: + self.template = unicode(template) + except UnicodeDecodeError: + self.template = unicode(template, "utf8") - def __init__(self, *lookups, **context): - self.lookups = lookups + def update(self, d): + """ + Given a dictionary of replacement rules for the Template, + replace variables in the template (once!) and return an + updated Template. + """ + if d: + def convert(m): + name = m.group("named") + return d.get(name, self.start_delim + name + self.end_delim) + return self.__class__(self.pattern2.sub(convert, self.template)) + return self + + def finalize(self, d = {}): + """ + The same as update, except the dictionary is optional and the + object returned will be a unicode object. + """ + return self.update(d).template + + +class CacheStub(object): + """ + When using cached renderings, this class generates a stub based on + the hash of the Templated item passed into init for the style + specified. + + This class is suitable as a stub object (in the case of API calls) + and wil render in a string form suitable for replacement with + StringTemplate in the case of normal rendering. + """ + def __init__(self, item, style): + self.name = "h%s%s" % (id(item), str(style).replace('-', '_')) + + def __str__(self): + return StringTemplate.start_delim + self.name + \ + StringTemplate.end_delim + + def __repr__(self): + return "<%s: %s>" % (self.__class__.__name__, self.name) + +class CachedVariable(CacheStub): + """ + Same use as CacheStubs in normal templates, except it can be + applied to where we would normally put a '$____' variable by hand + in a template (file). + """ + def __init__(self, name): + self.name = name + + +class Templated(object): + """ + Replaces the Wrapped class (which has now a subclass and which + takes an thing to be wrapped). + + Templated objects are suitable for rendering and caching, with a + render loop desgined to fetch other cached templates and insert + them into the current template. + + """ + + # is this template cachable (see CachedTemplate) + cachable = False + # attributes that will not be made into the cache key + cache_ignore = set() + + def __repr__(self): + return "" % self.__class__.__name__ + + def __init__(self, **context): + """ + uses context to init __dict__ (making this object a bit like a storage) + """ for k, v in context.iteritems(): setattr(self, k, v) + if not hasattr(self, "render_class"): + self.render_class = self.__class__ + def template(self, style = 'html'): + """ + Fetches template from the template manager + """ + from r2.config.templates import tpm + from pylons import g + debug = g.template_debug + template = None + try: + template = tpm.get(self.render_class, + style, cache = not debug) + except AttributeError: + raise NoTemplateFound, (repr(self), style) + + return template + + def cache_key(self, *a): + """ + if cachable, this function is used to generate the cache key. + """ + raise NotImplementedError + + def render_nocache(self, attr, style): + """ + No-frills (or caching) rendering of the template. The + template is returned as a subclass of StringTemplate and + therefore finalize() must be called on it to turn it into its + final form + """ + from filters import unsafe + from pylons import c + # the style has to default to the global render style + # fetch template + template = self.template(style) + if template: + # store the global render style (since child templates + render_style = c.render_style + c.render_style = style + # are we doing a partial render? + if attr: + template = template.get_def(attr) + # render the template + res = template.render(thing = self) + if not isinstance(res, StringTemplate): + res = StringTemplate(res) + # reset the global render style + c.render_style = render_style + return res + else: + raise NoTemplateFound, repr(self) + + def _render(self, attr, style, **kwargs): + """ + Renders the current template with the current style, possibly + doing a part_render if attr is not None. + + if this is the first template to be rendered, it is will track + cachable templates, insert stubs for them in the output, + get_multi from the cache, and render the uncached templates. + Uncached but cachable templates are inserted back into the + cache with a set_multi. + + NOTE: one of the interesting issues with this function is that + on each newly rendered thing, it is possible that that + rendering has in turn cause more cachable things to be + fetched. Thus the first template to be rendered runs a loop + and keeps rendering until there is nothing left to render. + Then it updates the master template until it doesn't change. + + NOTE 2: anything passed in as a kw to render (and thus + _render) will not be part of the cached version of the object, + and will substituted last. + """ + from pylons import c, g + style = style or c.render_style or 'html' + + # prepare (and store) the list of cachable items. + primary = False + if not isinstance(c.render_tracker, dict): + primary = True + c.render_tracker = {} + + # insert a stub for cachable non-primary templates + if self.cachable: + res = CacheStub(self, style) + cache_key = self.cache_key(attr, style) + # in the tracker, we need to store: + # The render cache key (res.name) + # The memcached cache key(cache_key) + # who I am (self) and what am I doing (attr, style) with what + # (kwargs) + c.render_tracker[res.name] = (cache_key, (self, + (attr, style, kwargs))) + else: + # either a primary template or not cachable, so render it + res = self.render_nocache(attr, style) + + # if this is the primary template, let the caching games begin + if primary: + # updates will be the (self-updated) list of all of + # the cached templates that have been cached or + # rendered. + updates = {} + # to_cache is just the keys of the cached templates + # that were not in the cache. + to_cache = set([]) + while c.render_tracker: + # copy and wipe the tracker. It'll get repopulated if + # any of the subsequent render()s call cached objects. + current = c.render_tracker + c.render_tracker = {} + + # do a multi-get. NOTE: cache keys are the first item + # in the tuple that is the current dict's values. + # This dict cast will generate a new dict of cache_key + # to value + cached = g.rendercache.get_multi(dict(current.values())) + # replacements will be a map of key -> rendered content + # for updateing the current set of updates + replacements = {} + + new_updates = {} + # render items that didn't make it into the cached list + for key, (cache_key, others) in current.iteritems(): + # unbundle the remaining args + item, (attr, style, kw) = others + if cache_key not in cached: + # this had to be rendered, so cache it later + to_cache.add(cache_key) + # render the item and apply the stored kw args + r = item.render_nocache(attr, style) + else: + r = cached[cache_key] + # store the unevaluated templates in + # cached for caching + replacements[key] = r.finalize(kw) + new_updates[key] = (cache_key, (r, kw)) + + # update the updates so that when we can do the + # replacement in one pass. + + # NOTE: keep kw, but don't update based on them. + # We might have to cache these later, and we want + # to have things like $child present. + for k in updates.keys(): + cache_key, (value, kw) = updates[k] + value = value.update(replacements) + updates[k] = cache_key, (value, kw) + + updates.update(new_updates) + + # at this point, we haven't touched res, but updates now + # has the list of all the updates we could conceivably + # want to make, and to_cache is the list of cache keys + # that we didn't find in the cache. + + # cache content that was newly rendered + g.rendercache.set_multi(dict((k, v) + for k, (v, kw) in updates.values() + if k in to_cache)) + + # edge case: this may be the primary tempalte and cachable + if isinstance(res, CacheStub): + res = updates[res.name][1][0] + + # now we can update the updates to make use of their kw args. + updates = dict((k, v.finalize(kw)) + for k, (foo, (v, kw)) in updates.iteritems()) + + # update the response to use these values + # replace till we can't replace any more. + npasses = 0 + while True: + npasses += 1 + r = res + res = res.update(kwargs).update(updates) + semi_final = res.finalize() + if r.finalize() == res.finalize(): + res = semi_final + break + + # wipe out the render tracker object + c.render_tracker = None + elif not isinstance(res, CacheStub): + # we're done. Update the template based on the args passed in + res = res.finalize(kwargs) + + return res + + + def render(self, style = None, **kw): + from r2.lib.filters import unsafe + res = self._render(None, style, **kw) + return unsafe(res) if isinstance(res, str) else res + + def part_render(self, attr, **kw): + style = kw.get('style') + if style: del kw['style'] + return self._render(attr, style, **kw) + + +class Uncachable(Exception): pass + +_easy_cache_cls = set([bool, int, long, float, unicode, str, types.NoneType, + datetime]) +def make_cachable(v, *a): + """ + Given an arbitrary object, + """ + if v.__class__ in _easy_cache_cls or isinstance(v, type): + try: + return unicode(v) + except UnicodeDecodeError: + try: + return unicode(v, "utf8") + except (TypeError, UnicodeDecodeError): + return repr(v) + elif isinstance(v, (types.MethodType, CachedVariable) ): + return + elif isinstance(v, (tuple, list, set)): + return repr([make_cachable(x, *a) for x in v]) + elif isinstance(v, dict): + return repr(dict((k, make_cachable(v[k], *a)) + for k in sorted(v.iterkeys()))) + elif hasattr(v, "cache_key"): + return v.cache_key(*a) + else: + raise Uncachable, type(v) + +class CachedTemplate(Templated): + cachable = True + + def cachable_attrs(self): + """ + Generates an iterator of attr names and their values for every + attr on this element that should be used in generating the cache key. + """ + return ((k, self.__dict__[k]) for k in sorted(self.__dict__) + if (k not in self.cache_ignore and not k.startswith('_'))) + + def cache_key(self, attr, style, *a): + from pylons import c + + # if template debugging is on, there will be no hash and we + # can make the caching process-local. + template_hash = getattr(self.template(style), "hash", + id(self.__class__)) + + # these values are needed to render any link on the site, and + # a menu is just a set of links, so we best cache against + # them. + keys = [c.user_is_loggedin, c.use_is_admin, + c.render_style, c.cname, c.lang, c.site.name, + template_hash] + keys = [make_cachable(x, *a) for x in keys] + + # add all parameters sent into __init__, using their current value + auto_keys = [(k, make_cachable(v, attr, style, *a)) + for k, v in self.cachable_attrs()] + + # lastly, add anything else that was passed in. + keys.append(repr(auto_keys)) + keys.extend(make_cachable(x) for x in a) + + return "<%s:[%s]>" % (self.__class__.__name__, u''.join(keys)) + + +class Wrapped(CachedTemplate): + # default to false, evaluate + cachable = False + cache_ignore = set(['lookups']) + + def cache_key(self, attr, style): + if self.cachable: + for i, l in enumerate(self.lookups): + if hasattr(l, "wrapped_cache_key"): + # setattr will force a __dict__ entry + setattr(self, "_lookup%d_cache_key" % i, + ''.join(map(repr, + l.wrapped_cache_key(self, style)))) + return CachedTemplate.cache_key(self, attr, style) + + def __init__(self, *lookups, **context): + self.lookups = lookups + # set the default render class to be based on the lookup if self.__class__ == Wrapped and lookups: self.render_class = lookups[0].__class__ + else: + self.render_class = self.__class__ + # this shouldn't be too surprising + self.cache_ignore = self.cache_ignore.union( + set(['cachable', 'render', 'cache_ignore', 'lookups'])) + if (not any(hasattr(l, "cachable") for l in lookups) and + any(hasattr(l, "wrapped_cache_key") for l in lookups)): + self.cachable = True + if self.cachable: + for l in lookups: + if hasattr(l, "cache_ignore"): + self.cache_ignore = self.cache_ignore.union(l.cache_ignore) + + Templated.__init__(self, **context) + + def __repr__(self): + return "" % (self.__class__.__name__, + self.lookups) def __getattr__(self, attr): - #print "GETATTR: " + str(attr) - #one would think this would never happen if attr == 'lookups': raise AttributeError, attr @@ -61,59 +460,12 @@ class Wrapped(object): setattr(self, attr, res) return res + def __iter__(self): + if self.lookups and hasattr(self.lookups[0], "__iter__"): + return self.lookups[0].__iter__() + raise NotImplementedError - def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, self.lookups) - - def template(self, style = 'html'): - from r2.config.templates import tpm - from pylons import g - debug = g.template_debug - template = None - if self.__class__ == Wrapped: - for lookup in chain(self.lookups, (self.render_class,)): - try: - template = tpm.get(lookup, style, cache = not debug) - except AttributeError: - continue - else: - try: - template = tpm.get(self, style, cache = not debug) - except AttributeError: - raise NoTemplateFound, (repr(self), style) - - return template - - #TODO is this the best way to override style? - def render(self, style = None): - """Renders the template corresponding to this class in the given style.""" - from pylons import c - style = style or c.render_style or 'html' - template = self.template(style) - if template: - res = template.render(thing = self) - return res if (style and style.startswith('api')) else unsafe(res) - else: - raise NoTemplateFound, repr(self) - - def part_render(self, attr, *a, **kw): - """Renders the part of a template associated with the %def - whose name is 'attr'. This is used primarily by - r2.lib.menus.Styled""" - style = kw.get('style', 'html') - template = self.template(style) - dt = template.get_def(attr) - return unsafe(dt.render(thing = self, *a, **kw)) - - -def SimpleWrapped(**kw): - class _SimpleWrapped(Wrapped): - def __init__(self, *a, **kw1): - kw.update(kw1) - Wrapped.__init__(self, *a, **kw) - return _SimpleWrapped - -class Styled(Wrapped): +class Styled(CachedTemplate): """Rather than creating a separate template for every possible menu/button style we might want to use, this class overrides the render function to render only the <%def> in the template whose @@ -123,15 +475,16 @@ class Styled(Wrapped): are intended to be used in the outermost container's id and class tag. """ + def __init__(self, style, _id = '', css_class = '', **kw): self._id = _id self.css_class = css_class self.style = style - Wrapped.__init__(self, **kw) + CachedTemplate.__init__(self, **kw) def render(self, **kw): """Using the canonical template file, only renders the <%def> in the template whose name is given by self.style""" - from pylons import c - style = kw.get('style', c.render_style or 'html') - return Wrapped.part_render(self, self.style, style = style, **kw) + return CachedTemplate.part_render(self, self.style, **kw) + + diff --git a/r2/r2/models/builder.py b/r2/r2/models/builder.py index 2b9d51754..92fdc6acc 100644 --- a/r2/r2/models/builder.py +++ b/r2/r2/models/builder.py @@ -101,7 +101,9 @@ class Builder(object): for item in items: w = self.wrap(item) wrapped.append(w) - + # add for caching (plus it should be bad form to use _ + # variables in templates) + w.fullname = item._fullname types.setdefault(w.render_class, []).append(w) #TODO pull the author stuff into add_props for links and @@ -124,16 +126,24 @@ class Builder(object): else: w.likes = None - #definite - w.timesince = utils.timesince(item._date) # update vote tallies compute_votes(w, item) w.score = w.upvotes - w.downvotes + if w.likes: + base_score = w.score - 1 + elif w.likes is None: + base_score = w.score + else: + base_score = w.score + 1 + # store the set of available scores based on the vote + # for ease of i18n when there is a label + w.voting_score = [max(base_score + x - 1, 0) for x in range(3)] + w.deleted = item._deleted - w.rowstyle = w.rowstyle if hasattr(w,'rowstyle') else '' + w.rowstyle = getattr(w, 'rowstyle', "") w.rowstyle += ' ' + ('even' if (count % 2) else 'odd') count += 1 @@ -157,15 +167,16 @@ class Builder(object): w.can_ban = True if item._spam: w.show_spam = True - if not hasattr(item,'moderator_banned'): - w.moderator_banned = False - + w.moderator_banned = getattr(item,'moderator_banned', False) w.autobanned, w.banner = ban_info.get(item._fullname, (False, None)) elif hasattr(item,'reported') and item.reported > 0: w.show_reports = True + # recache the user object: it may be None if user is not logged in, + # whereas now we are happy to have the UnloggedUser object + user = c.user for cls in types.keys(): cls.add_props(user, types[cls]) diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index 7d7f50890..c5731a9e6 100644 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -200,47 +200,16 @@ class Link(Thing, Printable): return True + # none of these things will change over a link's lifetime + cache_ignore = set(['subreddit', 'num_comments', 'link_child'] + ).union(Printable.cache_ignore) @staticmethod - def cache_key(wrapped): - if c.user_is_admin: - return False - - link_child = wrapped.link_child - s = (str(i) for i in (wrapped._fullname, - bool(c.user_is_sponsor), - bool(c.user_is_loggedin), - wrapped.subreddit == c.site, - c.user.pref_newwindow, - c.user.pref_frame, - c.user.pref_compress, - c.user.pref_media, - request.host, - c.cname, - wrapped.author == c.user, - wrapped.likes, - wrapped.saved, - wrapped.clicked, - wrapped.hidden, - wrapped.friend, - wrapped.show_spam, - wrapped.show_reports, - wrapped.can_ban, - wrapped.thumbnail, - wrapped.moderator_banned, - #link child stuff - bool(link_child), - bool(link_child) and link_child.load, - bool(link_child) and link_child.expand - )) - # htmllite depends on other get params - s = ''.join(s) - if c.render_style == "htmllite": - 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])) + def wrapped_cache_key(wrapped, style): + s = Printable.wrapped_cache_key(wrapped, style) + if style == "htmllite": + s.append(request.get.has_key('twocolumn')) + elif style == "xml": + s.append(request.GET.has_key("nothumbs")) return s def make_permalink(self, sr, force_domain = False): @@ -266,23 +235,39 @@ class Link(Thing, Printable): from r2.lib.media import thumbnail_url from r2.lib.utils import timeago from r2.lib.template_helpers import get_domain + from r2.models.subreddit import FakeSubreddit + from r2.lib.wrapped import CachedVariable - saved = Link._saved(user, wrapped) if user else {} - hidden = Link._hidden(user, wrapped) if user else {} + # referencing c's getattr is cheap, but not as cheap when it + # is in a loop that calls it 30 times on 25-200 things. + user_is_admin = c.user_is_admin + user_is_loggedin = c.user_is_loggedin + pref_media = user.pref_media + pref_frame = user.pref_frame + pref_newwindow = user.pref_newwindow + cname = c.cname + site = c.site + + saved = Link._saved(user, wrapped) if user_is_loggedin else {} + hidden = Link._hidden(user, wrapped) if user_is_loggedin else {} #clicked = Link._clicked(user, wrapped) if user else {} clicked = {} for item in wrapped: show_media = False - if c.user.pref_compress: - pass - elif c.user.pref_media == 'on': + if not hasattr(item, "score_fmt"): + item.score_fmt = Score.number_only + item.pref_compress = user.pref_compress + if user.pref_compress: + item.render_css_class = "compressed link" + item.score_fmt = Score.points + elif pref_media == 'on': show_media = True - elif c.user.pref_media == 'subreddit' and item.subreddit.show_media: + elif pref_media == 'subreddit' and item.subreddit.show_media: show_media = True elif (item.promoted and item.has_thumbnail - and c.user.pref_media != 'off'): + and pref_media != 'off'): show_media = True if not show_media: @@ -292,7 +277,6 @@ class Link(Thing, Printable): else: item.thumbnail = g.default_thumb - item.score = max(0, item.score) item.domain = (domain(item.url) if not item.is_self @@ -304,23 +288,30 @@ class Link(Thing, Printable): item.hidden = bool(hidden.get((user, item, 'hide'))) item.clicked = bool(clicked.get((user, item, 'click'))) item.num = None - item.score_fmt = Score.number_only item.permalink = item.make_permalink(item.subreddit) if item.is_self: item.url = item.make_permalink(item.subreddit, force_domain = True) - if c.user_is_admin: + # do we hide the score? + if user_is_admin: item.hide_score = False elif item.promoted: item.hide_score = True - elif c.user == item.author: + elif user == item.author: item.hide_score = False elif item._date > timeago("2 hours"): item.hide_score = True else: item.hide_score = False - if c.user_is_loggedin and item.author._id == c.user._id: + # store user preferences locally for caching + item.pref_frame = pref_frame + item.newwindow = pref_newwindow + # is this link a member of a different (non-c.site) subreddit? + item.different_sr = (isinstance(site, FakeSubreddit) or + site.name != item.subreddit.name) + + if user_is_loggedin and item.author._id == user._id: item.nofollow = False elif item.score <= 1 or item._spam or item.author._spam: item.nofollow = True @@ -328,11 +319,11 @@ class Link(Thing, Printable): item.nofollow = False item.subreddit_path = item.subreddit.path - if c.cname: + if cname: item.subreddit_path = ("http://" + - get_domain(cname = (c.site == item.subreddit), + get_domain(cname = (site == item.subreddit), subreddit = False)) - if c.site != item.subreddit: + if site != item.subreddit: item.subreddit_path += item.subreddit.path item.domain_path = "/domain/%s" % item.domain if item.is_self: @@ -353,7 +344,7 @@ class Link(Thing, Printable): item.editable = expand and item.author == c.user item.tblink = "http://%s/tb/%s" % ( - get_domain(cname = c.cname, subreddit=False), + get_domain(cname = cname, subreddit=False), item._id36) if item.is_self: @@ -361,7 +352,7 @@ class Link(Thing, Printable): else: item.href_url = item.url - if c.user.pref_frame and not item.is_self: + if pref_frame and not item.is_self: item.mousedown_url = item.tblink else: item.mousedown_url = None @@ -373,9 +364,20 @@ class Link(Thing, Printable): item._deleted, item._spam)) - if c.user_is_loggedin: + # bits that we will render stubs (to make the cached + # version more flexible) + item.num = CachedVariable("num") + item.numcolmargin = CachedVariable("numcolmargin") + item.commentcls = CachedVariable("commentcls") + item.midcolmargin = CachedVariable("midcolmargin") + item.comment_label = CachedVariable("numcomments") + + if user_is_loggedin: incr_counts(wrapped) + # Run this last + Printable.add_props(user, wrapped) + @property def subreddit_slow(self): from subreddit import Subreddit @@ -395,9 +397,9 @@ class PromotedLink(Link): @classmethod def add_props(cls, user, wrapped): Link.add_props(user, wrapped) - + user_is_sponsor = c.user_is_sponsor try: - if c.user_is_sponsor: + if user_is_sponsor: promoted_by_ids = set(x.promoted_by for x in wrapped if hasattr(x,'promoted_by')) @@ -414,11 +416,14 @@ class PromotedLink(Link): for item in wrapped: # these are potentially paid for placement item.nofollow = True + item.user_is_sponsor = user_is_sponsor if item.promoted_by in promoted_by_accounts: item.promoted_by_name = promoted_by_accounts[item.promoted_by].name else: # keep the template from trying to read it item.promoted_by = None + # Run this last + Printable.add_props(user, wrapped) class Comment(Thing, Printable): _data_int_props = Thing._data_int_props + ('reported',) @@ -478,33 +483,12 @@ class Comment(Thing, Printable): def keep_item(self, wrapped): return True + cache_ignore = set(["subreddit", "link", "to"] + ).union(Printable.cache_ignore) @staticmethod - def cache_key(wrapped): - if c.user_is_admin: - return False - - s = (str(i) for i in (c.profilepage, - wrapped._fullname, - bool(c.user_is_loggedin), - c.focal_comment == wrapped._id36, - request.host, - c.cname, - wrapped.author == c.user, - wrapped.editted, - wrapped.likes, - wrapped.friend, - wrapped.collapsed, - wrapped.nofollow, - wrapped.show_spam, - wrapped.show_reports, - wrapped.target, - wrapped.can_ban, - wrapped.moderator_banned, - wrapped.can_reply, - wrapped.deleted, - wrapped.render_class, - )) - s = ''.join(s) + def wrapped_cache_key(wrapped, style): + s = Printable.wrapped_cache_key(wrapped, style) + s.extend([wrapped.body]) return s def make_permalink(self, link, sr=None): @@ -517,6 +501,7 @@ class Comment(Thing, Printable): @classmethod def add_props(cls, user, wrapped): #fetch parent links + links = Link._byID(set(l.link_id for l in wrapped), data = True, return_dict = True) @@ -527,13 +512,21 @@ class Comment(Thing, Printable): subreddits = Subreddit._byID(set(cm.sr_id for cm in wrapped), data=True,return_dict=False) - can_reply_srs = set(s._id for s in subreddits if s.can_comment(user)) + can_reply_srs = set(s._id for s in subreddits if s.can_comment(user)) \ + if c.user_is_loggedin else set() - min_score = c.user.pref_min_comment_score + min_score = user.pref_min_comment_score cids = dict((w._id, w) for w in wrapped) + profilepage = c.profilepage + user_is_admin = c.user_is_admin + user_is_loggedin = c.user_is_loggedin + focal_comment = c.focal_comment + for item in wrapped: + # for caching: + item.profilepage = c.profilepage item.link = links.get(item.link_id) if not hasattr(item, 'subreddit'): @@ -554,32 +547,31 @@ class Comment(Thing, Printable): # not deleted on profile pages, # deleted if spam and not author or admin - item.deleted = (not c.profilepage and + item.deleted = (not profilepage and (item._deleted or (item._spam and - item.author != c.user and + item.author != user and not item.show_spam))) extra_css = '' if item._deleted: extra_css += "grayed" - if not c.user_is_admin: + if not user_is_admin: item.author = DeletedUser() item.body = '[deleted]' - if c.focal_comment == item._id36: + if focal_comment == item._id36: extra_css += 'border' # don't collapse for admins, on profile pages, or if deleted item.collapsed = ((item.score < min_score) and - not (c.profilepage or + not (profilepage or item.deleted or - c.user_is_admin)) + user_is_admin)) - if not hasattr(item,'editted'): - item.editted = False + item.editted = getattr(item, "editted", False) #score less than 3, nofollow the links item.nofollow = item._score < 3 @@ -589,32 +581,30 @@ class Comment(Thing, Printable): item.score_fmt = Score.points item.permalink = item.make_permalink(item.link, item.subreddit) + item.is_author = (user == item.author) + item.is_focal = (focal_comment == item._id36) + #will seem less horrible when add_props is in pages.py from r2.lib.pages import UserText item.usertext = UserText(item, item.body, - editable = item.author == c.user, + editable = item.author == user, nofollow = item.nofollow, target = item.target, extra_css = extra_css) + # Run this last + Printable.add_props(user, wrapped) + class StarkComment(Comment): """Render class for the comments in the top-comments display in the reddit toolbar""" _nodb = True -class MoreComments(object): - show_spam = False - show_reports = False - is_special = False - can_ban = False - deleted = False - rowstyle = 'even' - reported = False - collapsed = False - author = None - margin = 0 - +class MoreComments(Printable): + cachable = False + display = "" + @staticmethod - def cache_key(item): + def wrapped_cache_key(item, style): return False def __init__(self, link, depth, parent=None): @@ -647,7 +637,8 @@ class MoreChildren(MoreComments): class Message(Thing, Printable): _defaults = dict(reported = 0,) _data_int_props = Thing._data_int_props + ('reported', ) - + cache_ignore = set(["to"]).union(Printable.cache_ignore) + @classmethod def _new(cls, author, to, subject, body, ip, spam = False): m = Message(subject = subject, @@ -685,13 +676,15 @@ class Message(Thing, Printable): else: item.new = False item.score_fmt = Score.none + # Run this last + Printable.add_props(user, wrapped) - @staticmethod - def cache_key(wrapped): - #warning: inbox/sent messages - #comments as messages - return False + def wrapped_cache_key(wrapped, style): + s = Printable.wrapped_cache_key(wrapped, style) + s.extend([c.msg_location]) + return s + def keep_item(self, wrapped): return True diff --git a/r2/r2/models/listing.py b/r2/r2/models/listing.py index abe1dc509..353e78581 100644 --- a/r2/r2/models/listing.py +++ b/r2/r2/models/listing.py @@ -54,38 +54,11 @@ class Listing(object): def get_items(self, *a, **kw): """Wrapper around builder's get_items that caches the rendering.""" + from r2.lib.template_helpers import replace_render builder_items = self.builder.get_items(*a, **kw) - - #render cache - #fn to render non-boring items - fullnames = {} - for i in self.builder.item_iter(builder_items): - rs = c.render_style - key = i.render_class.cache_key(i) - if key: - fullnames[key + rs + c.lang] = i - - def render_items(names): - r = {} - for i in names: - item = fullnames[i] - r[i] = item.render() - return r - - rendered_items = sgm(g.rendercache, fullnames, render_items, 'render_', - time = g.page_cache_time) - #replace the render function - for k, v in rendered_items.iteritems(): - def make_fn(v): - default = c.render_style - default_render = fullnames[k].render - def r(style = default): - if style != c.render_style: - return default_render(style = style) - return v - return r - fullnames[k].render = make_fn(v) - + for item in self.builder.item_iter(builder_items): + # rewrite the render method + item.render = replace_render(self, item, item.render) return builder_items def listing(self): @@ -104,6 +77,9 @@ class Listing(object): #TODO: need name for template -- must be better way return Wrapped(self) + def __iter__(self): + return iter(self.things) + class LinkListing(Listing): def __init__(self, *a, **kw): Listing.__init__(self, *a, **kw) diff --git a/r2/r2/models/printable.py b/r2/r2/models/printable.py index fa2e06c2f..b42903b4a 100644 --- a/r2/r2/models/printable.py +++ b/r2/r2/models/printable.py @@ -19,10 +19,52 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ +from pylons import c, request +from r2.lib.strings import Score class Printable(object): + show_spam = False + show_reports = False + is_special = False + can_ban = False + deleted = False + rowstyle = 'even' + reported = False + collapsed = False + author = None + margin = 0 + is_focal = False + childlisting = None + cache_ignore = set(['author', 'score_fmt', 'child', + # displayed score is cachable, so remove score + # related fields. + 'voting_score', 'display_score', + 'render_score', 'score', '_score', + 'upvotes', '_ups', + 'downvotes', '_downs', + 'subreddit_slow', + 'cachable', 'make_permalink', 'permalink', + 'timesince', 'votehash' + ]) + @classmethod - def add_props(cls, listing, wrapped): - pass + def add_props(cls, user, wrapped): + from r2.lib.wrapped import CachedVariable + for item in wrapped: + # insert replacement variable for timesince to allow for + # caching of thing templates + item.display = CachedVariable("display") + item.timesince = CachedVariable("timesince") + item.votehash = CachedVariable("votehash") + item.childlisting = CachedVariable("childlisting") + + score_fmt = getattr(item, "score_fmt", Score.number_only) + item.display_score = map(score_fmt, item.voting_score) + + if item.cachable: + item.render_score = item.display_score + item.display_score = map(CachedVariable, + ["scoredislikes", "scoreunvoted", + "scorelikes"]) @property def permalink(self, *a, **kw): @@ -30,3 +72,14 @@ class Printable(object): def keep_item(self, wrapped): return True + + @staticmethod + def wrapped_cache_key(wrapped, style): + s = [wrapped._fullname, wrapped._spam] + + if style == 'htmllite': + s.extend([c.bgcolor, c.bordercolor, + request.get.has_key('style'), + request.get.get("expanded"), + getattr(wrapped, 'embed_voting_style', None)]) + return s diff --git a/r2/r2/models/subreddit.py b/r2/r2/models/subreddit.py index 4ae2c5648..aa619519c 100644 --- a/r2/r2/models/subreddit.py +++ b/r2/r2/models/subreddit.py @@ -248,33 +248,21 @@ class Subreddit(Thing, Printable): if not user or not user.has_subscribed: item.subscriber = item._id in defaults else: - item.subscriber = rels.get((item, user, 'subscriber')) - item.moderator = rels.get((item, user, 'moderator')) - item.contributor = item.moderator or \ - rels.get((item, user, 'contributor')) + item.subscriber = bool(rels.get((item, user, 'subscriber'))) + item.moderator = bool(rels.get((item, user, 'moderator'))) + item.contributor = bool(item.moderator or \ + rels.get((item, user, 'contributor'))) item.score = item._ups item.score_fmt = Score.subscribers - + Printable.add_props(user, wrapped) #TODO: make this work + cache_ignore = set(["subscribers"]).union(Printable.cache_ignore) @staticmethod - def cache_key(wrapped): - if c.user_is_admin: - return False - - s = (str(i) for i in (wrapped._fullname, - bool(c.user_is_loggedin), - wrapped.subscriber, - wrapped.moderator, - wrapped.contributor, - wrapped._spam)) - s = ''.join(s) + def wrapped_cache_key(wrapped, style): + s = Printable.wrapped_cache_key(wrapped, style) + s.extend([wrapped._spam]) return s - #TODO: make this work - #@property - #def author_id(self): - #return 1 - @classmethod def top_lang_srs(cls, lang, limit): """Returns the default list of subreddits for a given language, sorted diff --git a/r2/r2/public/static/css/reddit.css b/r2/r2/public/static/css/reddit.css index bddfcdbcd..f11165c77 100644 --- a/r2/r2/public/static/css/reddit.css +++ b/r2/r2/public/static/css/reddit.css @@ -740,7 +740,6 @@ a.star { text-decoration: none; color: #ff8b60 } .likes div.score.likes {display: block;} .dislikes div.score.dislikes {display: block;} - .warm-entry .rank { color: #EDA179; } .hot-entry .rank { color: #E47234; } .cool-entry .rank { color: #A5ABFB; } diff --git a/r2/r2/public/static/js/reddit.js b/r2/r2/public/static/js/reddit.js index 89d8f147d..8c035d3d1 100644 --- a/r2/r2/public/static/js/reddit.js +++ b/r2/r2/public/static/js/reddit.js @@ -281,6 +281,7 @@ function share(elem) { $(elem).new_thing_child($(".sharelink:first").clone(true) .attr("id", "sharelink_" + $(elem).thing_id()), false); + $.request("new_captcha"); }; function cancelShare(elem) { @@ -888,7 +889,6 @@ function reply(elem) { form.find(".cancel").get(0).onclick = function() {form.hide()}; } - function populate_click_gadget() { /* if we can find the click-gadget, populate it */ if($('.click-gadget').length) { diff --git a/r2/r2/public/static/mobile.css b/r2/r2/public/static/mobile.css deleted file mode 100644 index 80cabe465..000000000 --- a/r2/r2/public/static/mobile.css +++ /dev/null @@ -1,135 +0,0 @@ -body { - font-family: verdana,arial,helvetica,sans-serif; - margin: 0; - padding: 0; - font-size: x-small; - color: #888; -} - -a { - text-decoration: none; - color: #369; - } - -p { - margin: 0px; - padding: 0px; - } - -ul { - margin: 0px; - padding: 0px; - list-style: none; - } - -.link { - margin-left: 2px; - } - -.title { - color: blue; - font-size: small; - margin-right: .5em; - } - -.byline { - margin: 0px 0px .5em 2px; - } - -.description { - margin-bottom: .5em; - } - -.domain { - color: #369; - } - -.buttons { - font-weight: bold; - } - -.child { - margin-left: 2em; - } - -.headerbar { - background:lightgray none repeat scroll 0%; - margin: 5px 0px 5px 2px; -} - -.headerbar span { - background-color: white; - color: gray; - font-size: x-small; - font-weight: bold; - margin-left: 15px; - padding: 0px 3px; -} - -.score { - margin: 0px .5em 0px .5em; -} - -.error { - color: red; - margin: 5px; - } - -.nextprev { - margin: 10px; - } - -.tabmenu { - list-style-type: none; -} - -.tabmenu li { - display: inline; - margin-right: .25em; - padding-left: .25em; - border-left: thin solid #000; - } - -.redditname { - font-weight: bold; - margin: 0px 3px 0px 3px; - } - -.selected { - font-weight: bold; - } - -.pagename { - font-weight: bold; - margin-right: 1ex; - color: black; - } - -.or { - border-left:thin solid #000000; - border-right:thin solid #000000; - padding: 0px .25em 0px .25em; - } - -#header { - background-color: #CEE3F8; - } - -#header .redditname a{ - color: black; - } - -#header img { - margin-right: 3px; - } - -/* markdown */ -.md { max-width: 60em; overflow: auto; } -.md p, .md h1 { margin-bottom: .5em;} -.md h1 { font-weight: normal; font-size: 100%; } -.md > * { margin-bottom: 0px } -.md strong { font-weight: bold; } -.md em { font-style: italic; } -.md img { display: none } -.md ol, .md ul { margin: 10px 2em; } -.md pre { margin: 10px; } diff --git a/r2/r2/templates/admin_rightbox.html b/r2/r2/templates/admin_rightbox.html index 0597dad8e..fb953b8f5 100644 --- a/r2/r2/templates/admin_rightbox.html +++ b/r2/r2/templates/admin_rightbox.html @@ -25,5 +25,5 @@ %> %if c.user_is_admin: - ${AppServiceMonitor().render()} + ${AppServiceMonitor()} %endif diff --git a/r2/r2/templates/admintranslations.html b/r2/r2/templates/admintranslations.html index 2f130419a..32e43b3e9 100644 --- a/r2/r2/templates/admintranslations.html +++ b/r2/r2/templates/admintranslations.html @@ -21,7 +21,7 @@ ################################################################################ <%namespace file="translation.html" import="statusbar"/> -<%namespace file="printable.html" import="ynbutton, state_button" /> +<%namespace file="printablebuttons.html" import="ynbutton, state_button" /> <% from r2.lib.translation import Translator %> diff --git a/r2/r2/templates/button.html b/r2/r2/templates/button.html index 8d0412dc2..1bbe3bf4d 100644 --- a/r2/r2/templates/button.html +++ b/r2/r2/templates/button.html @@ -45,7 +45,7 @@ <%def name="bodyHTML()"> - + <%def name="button3(thing)"> -${class_def(3)} +<%self:class_def class_number="3">
- %if thing.link: + %if thing._fullname: %if thing.vote: - ${arrow(thing.link, 1, thing.likes)} - ${score(thing.link, thing.likes, tag='div')} - ${arrow(thing.link, 0, thing.likes == False)} + ${arrow(thing, 1, thing.likes)} + ${score(thing, thing.likes, tag='div')} + ${arrow(thing, 0, thing.likes == False)} %else:  
- ${thing.link.score} + ${thing.score}  
%endif %else: @@ -120,47 +126,48 @@ ${class_def(3)} ${img_link('submit', '/static/blog_snoo.gif', capture(submiturl, thing.url, thing.title), target=thing.target)}
- + <%def name="button4(thing)"> - ${class_def(2)} - %if thing.link: +<%self:class_def class_number="2"> + %if thing._fullname: %if thing.vote: - ${arrow(thing.link, 1, thing.likes)} - ${score(thing.link, thing.likes, tag='div')} - ${arrow(thing.link, 0, thing.likes == False)} + ${arrow(thing, 1, thing.likes)} + ${score(thing, thing.likes, tag='div')} + ${arrow(thing, 0, thing.likes == False)} %else:  
- ${thing.link.score} + ${thing.score}  
%endif %else: ${submitlink(thing.url, thing.title, 'submit to')} %endif - + <%def name="button5(thing)"> - ${class_def(5, width=choose_width(thing.link, thing.width))} +<%self:class_def class_number="5" + width="${choose_width(thing, thing.width)}"> ${img_link('submit', '/static/blog_snoo.gif', capture(submiturl, thing.url, thing.title), _class="left")} <% # this button style submits to the cname as a default c.cname = hasattr(c.site, "domain") and bool(c.site.domain) submitlink = capture(submiturl, thing.url, thing.title) - if thing.link: - fullname = thing.link._fullname - ups = thing.link.upvotes - downs = thing.link.downvotes + if thing._fullname: + fullname = thing._fullname + ups = thing.upvotes + downs = thing.downvotes link = "http://%s%s" % (get_domain(cname = c.cname, subreddit = False), - thing.link.make_permalink_slow()) + thing.make_permalink_slow()) else: fullname = "" ups = 0 downs = 0 link = submitlink - if c.user_is_loggedin: + if c.user_is_loggedin and thing._fullname: dir = 1 if thing.likes else 0 if thing.likes is None else -1 ups = ups - (1 if dir > 0 else 0) downs = downs - (1 if dir < 0 else 0) @@ -206,7 +213,7 @@ ${class_def(3)}
  • - %if thing.link: + %if thing._fullname: ${_("Discuss at the %(name)s reddit") % dict(name = c.site.name)} %else: ${_("Submit to the %(name)s reddit") % dict(name = c.site.name)} @@ -214,7 +221,7 @@ ${class_def(3)}
  • -
    - <%utils:round_field title="${_('message')}"> - ${UserText(None, have_form = False, creating = True).render()} + <%utils:round_field title="message"> + ${UserText(None, have_form = False, creating = True)}
    - %if thing.captcha: - ${thing.captcha.render()} - %endif + ${thing.captcha}
    diff --git a/r2/r2/templates/frametoolbar.html b/r2/r2/templates/frametoolbar.html index aae910e2f..0077de110 100644 --- a/r2/r2/templates/frametoolbar.html +++ b/r2/r2/templates/frametoolbar.html @@ -26,7 +26,8 @@ <%inherit file="reddit.html"/> <%namespace file="utils.html" import="plain_link, logout"/> -<%namespace file="printable.html" import="state_button, comment_button, thing_css_class, score" /> +<%namespace file="printablebuttons.html" import="state_button, comment_button"/> +<%namespace file="printable.html" import="thing_css_class, score" /> <%def name="javascript_run()"> ${parent.javascript_run()} @@ -56,7 +57,7 @@ %endif - %if thing.link: + %if thing._fullname: ${withlink()} %endif @@ -89,7 +90,7 @@ ${c.user.name} - %elif thing.link: + %elif thing._fullname: ${_("login / register")} @@ -118,14 +119,14 @@
    - %if thing.link: + %if thing._fullname: - ${thing.link.title} + ${thing.title} %if thing.domain: (${thing.domain}) @@ -156,15 +157,15 @@ <%def name="withlink()"> - + ## add us to the click cookie - - ${score(thing.link, thing.link.likes, score_fmt = Score.safepoints, tag='b')} + + ${score(thing, thing.likes, tag='b')} <%def name="arrow(direction, style, message)"> @@ -172,7 +173,7 @@ title="${_('vote %(direction)s') % dict(direction=direction)}" %if c.user_is_loggedin: href="javascript:void(0);" - onclick="$(this).vote('${thing.vh}')" + onclick="$(this).vote('${thing.votehash}')" %else: href="${thing.loginurl}" target="_top" @@ -189,7 +190,7 @@ %if c.user_is_loggedin: - %if thing.link.saved: + %if thing.saved: ${state_button("unsave", _("unsave"), "return change_state(this, 'unsave');", "%s" % _("unsaved"), a_class="clickable")} @@ -200,13 +201,13 @@ %endif %endif - ${thing.com_label} + class="comments comments-button">${thing.comment_label} - %if thing.link and thing.expanded: + %if thing._fullname and thing.expanded: diff --git a/r2/r2/templates/organiclisting.html b/r2/r2/templates/organiclisting.html index 436407c4c..ee50bd1ba 100644 --- a/r2/r2/templates/organiclisting.html +++ b/r2/r2/templates/organiclisting.html @@ -20,9 +20,9 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ -<%namespace file="printable.html" import="ynbutton"/> +<%namespace file="printablebuttons.html" import="ynbutton"/> <% - from r2.lib.template_helpers import replace_render, static + from r2.lib.template_helpers import static from r2.lib.promote import get_promoted %> @@ -43,8 +43,7 @@ <% pass %> %elif lookup.has_key(name): <% seen.add(name) %> - ${unsafe(replace_render(thing, lookup[name], - display = (thing.visible_link == name)))} + ${unsafe(lookup[name].render(display = (thing.visible_link == name)))} %else: %endif diff --git a/r2/r2/templates/pane.htmllite b/r2/r2/templates/pane.htmllite deleted file mode 100644 index 0a8cc9742..000000000 --- a/r2/r2/templates/pane.htmllite +++ /dev/null @@ -1,23 +0,0 @@ -## 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. -################################################################################ - -${thing.content and thing.content.render() or ''} diff --git a/r2/r2/templates/pane.xml b/r2/r2/templates/pane.xml deleted file mode 100644 index 0a8cc9742..000000000 --- a/r2/r2/templates/pane.xml +++ /dev/null @@ -1,23 +0,0 @@ -## 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. -################################################################################ - -${thing.content and thing.content.render() or ''} diff --git a/r2/r2/templates/panestack.html b/r2/r2/templates/panestack.html index e5101dc1b..12edc278c 100644 --- a/r2/r2/templates/panestack.html +++ b/r2/r2/templates/panestack.html @@ -30,7 +30,7 @@

    ${thing.title}

    %endif - ${t.render() if t else ''} + ${t} %if thing.div:
    diff --git a/r2/r2/templates/panestack.htmllite b/r2/r2/templates/panestack.htmllite index 88b39e295..e5ebbad69 100644 --- a/r2/r2/templates/panestack.htmllite +++ b/r2/r2/templates/panestack.htmllite @@ -21,5 +21,5 @@ ################################################################################ %for t in thing.stack: -${t.render()} + ${t} %endfor diff --git a/r2/r2/templates/panestack.mobile b/r2/r2/templates/panestack.mobile index 87f5fe3b7..e5ebbad69 100644 --- a/r2/r2/templates/panestack.mobile +++ b/r2/r2/templates/panestack.mobile @@ -21,5 +21,5 @@ ################################################################################ %for t in thing.stack: -${t.render() if t else ""} + ${t} %endfor diff --git a/r2/r2/templates/panestack.xml b/r2/r2/templates/panestack.xml index 230139b5a..e5ebbad69 100644 --- a/r2/r2/templates/panestack.xml +++ b/r2/r2/templates/panestack.xml @@ -21,5 +21,5 @@ ################################################################################ %for t in thing.stack: -${t.render() if t else ''} + ${t} %endfor diff --git a/r2/r2/templates/printable.html b/r2/r2/templates/printable.html index 6e0a88e96..389b1cb48 100644 --- a/r2/r2/templates/printable.html +++ b/r2/r2/templates/printable.html @@ -23,6 +23,7 @@ <%! from r2.lib.template_helpers import add_sr from r2.lib.strings import strings + from r2.lib.pages.things import BanButtons %> <%namespace file="utils.html" import="plain_link" /> @@ -61,9 +62,6 @@ thing id-${what._fullname} else: cls = thing.lookups[0].__class__.__name__.lower() - if cls == 'link' and c.user.pref_compress: - cls = 'compressed link' - if thing.show_spam: rowclass = thing.rowstyle + " spam" elif thing.show_reports: @@ -75,7 +73,7 @@ thing id-${what._fullname} if hasattr(thing, "hidden") and thing.hidden: rowclass += " hidden" %> -
    +

    ${self.ParentDiv()}

    @@ -98,43 +96,9 @@ thing id-${what._fullname} <%def name="buttons(ban=True)"> -%if thing.can_ban and ban: - %if thing.show_spam: -
  • - ${self.state_button("unban", _("unban"), - "return change_state(this, 'unban');", _("unbanned"))} -
  • - %else: -
  • - ${self.state_button("ban", _("ban"), - "return change_state(this, 'ban');", _("banned"))} -
  • - %endif - %if thing.show_reports: -
  • - ${self.state_button("ignore", _("ignore"), \ - "change_state(this, 'ignore');", _("ignored"))} -
  • - %endif -%endif + ${BanButtons(thing, show_ban = ban)} -<%def name="delete_or_report_buttons(delete=True, report=True)"> -%if c.user_is_loggedin: - %if (not thing.author or thing.author.name != c.user.name) and report: -
  • - ${ynbutton(_("report"), _("reported"), "report", "hide_thing")} -
  • - %endif - %if (not thing.author or thing.author.name == c.user.name) and delete and not thing._deleted: -
  • - ${ynbutton(_("delete"), _("deleted"), "del", "hide_thing")} -
  • - %endif -%endif - - - <%def name="ParentDiv()"> @@ -144,11 +108,9 @@ thing id-${what._fullname} <%def name="entry()"> -<%def name="Child(display=True, render_hook=True)"> +<%def name="Child(display=True)">
    - %if render_hook: - $child - %endif + ${thing.childlisting}
    @@ -193,7 +155,7 @@ thing id-${what._fullname} %>
    -<%def name="score(this, likes=None, tag='span', score_fmt = None)"> -<% - 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 dislikes"> - ${score_fmt(base_score[0])} - - <${tag} class="score unvoted"> - ${score_fmt(base_score[1])} - - <${tag} class="score likes"> - ${score_fmt(base_score[2])} - +<%def name="score(this, likes=None, scores = None, tag='span')"> + <% + if scores is None: + scores = this.display_score + %> + %for cls, score in zip(["dislikes", "unvoted", "likes"], scores): + <${tag} class="score ${cls}"> + ${score} + + %endfor @@ -228,151 +183,3 @@ thing id-${what._fullname} ${self.arrow(thing, 0, thing.likes == False)}
    - -## -## -## -### originally in statebuttons -<%def name="state_button(name, title, onclick, executed, clicked=False, a_class = '', fmt=None, fmt_param = '', hidden_data = {})"> - <%def name="_link()" buffered="True"> - ${title} - - <% - link = _link() - if fmt: - link = fmt % {fmt_param: link} - ## preserve spaces before and after < & > for space compression - link = link.replace(" <", " <").replace("> ", "> ") - %> - - %if clicked: - ${executed} - %else: -
    - - %for key, value in hidden_data.iteritems(): - - %endfor - - ${unsafe(link)} - -
    - %endif - - - -<%def name="ynbutton(title, executed, op, callback = 'null', - question = None, - format = '%(link)s', - format_arg = 'link', - hidden_data = {})"> - <% - if question is None: - question = _("are you sure?") - link = ('' - + title + '') - link = format % {format_arg : link} - link = unsafe(link.replace(" <", " <").replace("> ", "> ")) - %> -
    - - %for k, v in hidden_data.iteritems(): - - %endfor - - ${link} - - - ${question} - - ${_("yes")} - / - ${_("no")} - -
    - - -<%def name="simple_button(title, nameFunc)"> - ${title} - - -<%def name="toggle_button(class_name, title, alt_title, - callback, cancelback, - css_class = '', alt_css_class = '', - reverse = False, - login_required = False, - style = '',)"> -<% - if reverse: - callback, cancelback = cancelback, callback - title, alt_title = alt_title, title - css_class, alt_css_class = alt_css_class, css_class - %> - - - ${title} - - ${alt_title} - - - -<%def name="toggleable_label(class_name, - title, alt_title, - callback, cancelback, - reverse = False)"> - <% - if reverse: - callback, cancelback = cancelback, callback - title, alt_title = alt_title, title - %> - - ${title} - ${alt_title} - ( - - ${_("toggle")} - - ) - - - -<%def name="tags(**kw)"> - %for k, v in kw.iteritems(): - %if v is not None: - ${k.strip('_')}="${v}" \ - %endif - %endfor - - - -### originally in commentbutton -<%def name="comment_button(name, link_text, num, link,\ - a_class='', title='', newwindow = False)"> - <% - cls = "comments empty" if num == 0 else "comments" - - if num > 0: - link_text = strings.number_label % dict(num=num, thing=link_text) - - target = '_blank' if newwindow else '_parent' - %> - ${plain_link(link_text, link, - _class="%s %s" % (a_class, cls), title=title, target=target)} - - diff --git a/r2/r2/templates/printable.htmllite b/r2/r2/templates/printable.htmllite index a2a775d4e..26c4a3ca2 100644 --- a/r2/r2/templates/printable.htmllite +++ b/r2/r2/templates/printable.htmllite @@ -39,7 +39,7 @@ ${self.Child()} <%def name="Child()"> - ${hasattr(thing, "child") and thing.child.render() or ''} + ${getattr(thing, "child", '')} <%def name="entry()"> diff --git a/r2/r2/templates/printable.mobile b/r2/r2/templates/printable.mobile index 025c32999..8885aa224 100644 --- a/r2/r2/templates/printable.mobile +++ b/r2/r2/templates/printable.mobile @@ -34,7 +34,7 @@ <%def name="Child()"> %if hasattr(thing, "child"):
    - ${hasattr(thing, "child") and thing.child.render() or ''} + ${thing.child}
    %endif diff --git a/r2/r2/templates/profilebar.html b/r2/r2/templates/profilebar.html index 97000e59c..1fa304cb0 100644 --- a/r2/r2/templates/profilebar.html +++ b/r2/r2/templates/profilebar.html @@ -25,7 +25,7 @@ from r2.lib.utils import timesince %> <%namespace file="utils.html" import="submit_form, plain_link"/> -<%namespace file="printable.html" import="toggle_button"/> +<%namespace file="printablebuttons.html" import="toggle_button"/> <% user = thing.user %> %if thing.user:
    diff --git a/r2/r2/templates/promotedlink.html b/r2/r2/templates/promotedlink.html index fdaac6e48..38b0bf442 100644 --- a/r2/r2/templates/promotedlink.html +++ b/r2/r2/templates/promotedlink.html @@ -24,6 +24,7 @@ %> <%inherit file="link.html"/> +<%namespace file="printablebuttons.html" import="ynbutton" /> <%namespace file="utils.html" import="plain_link" /> <%def name="tagline()"> @@ -36,7 +37,7 @@ _sr_path = False)}
  • - ${self.ynbutton(_("unpromote"), _("unpromoted"), "unpromote")} + ${ynbutton(_("unpromote"), _("unpromoted"), "unpromote")}
  • %endif diff --git a/r2/r2/templates/promotedlinks.html b/r2/r2/templates/promotedlinks.html index 175d062a7..3253928c5 100644 --- a/r2/r2/templates/promotedlinks.html +++ b/r2/r2/templates/promotedlinks.html @@ -23,7 +23,7 @@ from r2.lib.utils import to36 %> <%namespace file="utils.html" import="plain_link"/> -<%namespace file="printable.html" import="ynbutton"/> +<%namespace file="printablebuttons.html" import="ynbutton"/>
    diff --git a/r2/r2/templates/promotelinkform.html b/r2/r2/templates/promotelinkform.html index 3bc5df96b..03841452c 100644 --- a/r2/r2/templates/promotelinkform.html +++ b/r2/r2/templates/promotelinkform.html @@ -24,7 +24,7 @@ from r2.lib.media import thumbnail_url %> <%namespace file="utils.html" import="error_field, checkbox, plain_link, image_upload" /> -<%namespace file="printable.html" import="ynbutton"/> +<%namespace file="printablebuttons.html" import="ynbutton"/> <%namespace file="utils.html" import="error_field, checkbox, image_upload" /> %if thing.link: diff --git a/r2/r2/templates/reddit.html b/r2/r2/templates/reddit.html index 4b55bb0c2..8d87cb1d7 100644 --- a/r2/r2/templates/reddit.html +++ b/r2/r2/templates/reddit.html @@ -21,10 +21,13 @@ ################################################################################ <%! - from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, path_info + from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, path_info, get_domain from r2.lib.pages import SearchForm, ClickGadget + from r2.lib import tracking from pylons import request + from r2.lib.strings import strings %> +<%namespace file="login.html" import="login_panel, login_form"/> <%namespace file="framebuster.html" import="framebuster"/> <%inherit file="base.html"/> @@ -37,8 +40,6 @@ <%def name="stylesheet()"> - <% from r2.lib.template_helpers import static, get_domain %> - %if c.lang_rtl: @@ -112,21 +113,52 @@ ##
    ##
    - ${thing.content().render()} + ${thing.content()}
    %endif %if thing.footer: - <%include file="redditfooter.html"/> - %endif + + ${thing.footer} + + %if not c.user_is_loggedin: + %if thing.enable_login_cover: + + %endif + + %endif + %if g.tracker_url and thing.site_tracking: + + %endif + %endif ${framebuster()} - <%def name="sidebar(content=None)"> - ${content.render() if content else ''} + ${content} %if c.user_is_admin: <%include file="admin_rightbox.html"/> @@ -135,7 +167,7 @@ %endif ##cheating... we should move ads into a template of its own - ${ClickGadget(c.recent_clicks).render()} + ${ClickGadget(c.recent_clicks)} diff --git a/r2/r2/templates/reddit.htmllite b/r2/r2/templates/reddit.htmllite index 3e87fbc61..55ce38545 100644 --- a/r2/r2/templates/reddit.htmllite +++ b/r2/r2/templates/reddit.htmllite @@ -22,4 +22,4 @@ <%inherit file="base.htmllite"/> -${thing.content and thing.content().render() or ''} +${thing.content and thing.content() or ''} diff --git a/r2/r2/templates/reddit.mobile b/r2/r2/templates/reddit.mobile index 7a752f564..429f3419f 100644 --- a/r2/r2/templates/reddit.mobile +++ b/r2/r2/templates/reddit.mobile @@ -24,7 +24,7 @@ <%include file="redditheader.mobile"/> -${thing.content and thing.content().render() or ''} +${thing.content and thing.content() or ''} <%def name="Title()"> %if thing.title: diff --git a/r2/r2/templates/reddit.xml b/r2/r2/templates/reddit.xml index fc78bf03c..904f40975 100644 --- a/r2/r2/templates/reddit.xml +++ b/r2/r2/templates/reddit.xml @@ -22,4 +22,4 @@ <%inherit file="base.xml"/> -${thing.content().render()} +${thing.content()} diff --git a/r2/r2/templates/redditfooter.html b/r2/r2/templates/redditfooter.html index 9bb960801..d435aeb6a 100644 --- a/r2/r2/templates/redditfooter.html +++ b/r2/r2/templates/redditfooter.html @@ -21,65 +21,29 @@ ################################################################################ <%! - from r2.lib.template_helpers import get_domain, static, UrlParser from r2.lib.strings import strings - from r2.lib import tracking - import random, datetime + import datetime %> -<%namespace file="login.html" import="login_panel, login_form"/> -<%namespace file="utils.html" import="text_with_links, plain_link"/> +<%namespace file="utils.html" import="text_with_links"/> -%if not c.user_is_loggedin: - %if thing.enable_login_cover: - ${self.loginpopup(login_panel, login_form)} - %endif - ${self.langpopup()} -%endif - -<%def name="loginpopup(lp, lf)"> - - - - -<%def name="langpopup()"> - - diff --git a/r2/r2/templates/redditheader.html b/r2/r2/templates/redditheader.html index d7bcb1aca..c99dae1ae 100644 --- a/r2/r2/templates/redditheader.html +++ b/r2/r2/templates/redditheader.html @@ -29,10 +29,7 @@ <%namespace file="utils.html" import="plain_link, text_with_js, img_link, separator, logout"/>