diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 137c38411..373932b93 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -79,7 +79,7 @@ class ApiController(RedditController): Controller which deals with almost all AJAX site interaction. """ - def response_func(self, **kw): + def response_func(self, kw): data = dumps(kw) if request.method == "GET" and request.GET.get("callback"): return "%s(%s)" % (websafe_json(request.GET.get("callback")), @@ -97,10 +97,8 @@ class ApiController(RedditController): """ Get's a listing of links which have the provided url. """ - listing = None - if link and errors.ALREADY_SUB in c.errors: - listing = link_listing_by_url(request.params.get('url'), - count = count) + listing = link_listing_by_url(request.params.get('url'), + count = count) return BoringPage(_("API"), content = listing).render() @validatedForm(VCaptcha(), @@ -261,6 +259,8 @@ class ApiController(RedditController): user cookie and send back a redirect. """ self.login(user, rem = rem) + form._send_data(modhash = user.modhash()) + form._send_data(cookie = user.make_cookie()) dest = dest or request.referer or '/' form.redirect(dest) diff --git a/r2/r2/controllers/errors.py b/r2/r2/controllers/errors.py index 849fabe99..712ddd5f6 100644 --- a/r2/r2/controllers/errors.py +++ b/r2/r2/controllers/errors.py @@ -24,6 +24,7 @@ from pylons.i18n import _ from copy import copy error_list = dict(( + ('USER_REQUIRED', _("please login to do that")), ('NO_URL', _('url required')), ('BAD_URL', _('you should check that url')), ('NO_TITLE', _('title required')), diff --git a/r2/r2/controllers/post.py b/r2/r2/controllers/post.py index 2fe73ac19..6fa4c42ac 100644 --- a/r2/r2/controllers/post.py +++ b/r2/r2/controllers/post.py @@ -37,7 +37,7 @@ def to_referer(func, **params): class PostController(ApiController): - def response_func(self, **kw): + def response_func(self, kw): return Storage(**kw) #TODO: feature disabled for now diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 52063a70c..d33a04e7c 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -26,7 +26,7 @@ from r2.lib import utils, captcha from r2.lib.filters import unkeep_space, websafe, _force_unicode from r2.lib.db.operators import asc, desc from r2.lib.template_helpers import add_sr -from r2.lib.jsonresponse import json_respond, JQueryResponse +from r2.lib.jsonresponse import json_respond, JQueryResponse, JsonResponse from r2.lib.jsontemplates import api_type from r2.models import * @@ -110,68 +110,64 @@ def validate(*simple_vals, **param_vals): return newfn return val -def noresponse(*simple_vals, **param_vals): + +def api_validate(response_function): """ - AJAXy decorator which takes the place of validate when no response - is expected from the controller method. + Factory for making validators for API calls, since API calls come + in two flavors: responsive and unresponsive. The machinary + associated with both is similar, and the error handling identical, + so this function abstracts away the kw validation and creation of + a Json-y responder object. """ - def val(fn): - def newfn(self, *a, **env): - c.render_style = api_type('html') - c.response_content_type = 'application/json; charset=UTF-8' - jquery = JQueryResponse() + def _api_validate(*simple_vals, **param_vals): + def val(fn): + def newfn(self, *a, **env): + c.render_style = api_type('html') + c.response_content_type = 'application/json; charset=UTF-8' + # generate a response object + if request.params.get('api_type') == "json": + responder = JsonResponse() + else: + responder = JQueryResponse() + try: + kw = _make_validated_kw(fn, simple_vals, param_vals, env) + return response_function(self, fn, responder, + simple_vals, param_vals, *a, **kw) + except UserRequiredException: + responder.send_failure(errors.USER_REQUIRED) + return self.response_func(dict(iter(responder))) + return newfn + return val + return _api_validate + - try: - kw = _make_validated_kw(fn, simple_vals, param_vals, env) - fn(self, *a, **kw) - return '' - except UserRequiredException: - jquery = JQueryResponse() - jquery.refresh() - return self.response_func(**dict(list(jquery))) - return newfn - return val +@api_validate +def noresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw): + self_method(self, *a, **kw) + return self.response_func({}) -def validatedForm(*simple_vals, **param_vals): - """ - AJAX response validator for general form handling. In addition to - validating simple_vals and param_vals in the same way as validate, - a jquery object and a jquery form object are allocated and passed - into the method which is decorated. - """ - def val(fn): - def newfn(self, *a, **env): - # set the content type for the response - c.render_style = api_type('html') - c.response_content_type = 'application/json; charset=UTF-8' +@api_validate +def validatedForm(self, self_method, responder, simple_vals, param_vals, + *a, **kw): + # generate a form object + form = responder(request.POST.get('id', "body")) - # generate a response object - jquery = JQueryResponse() - # generate a form object - form = jquery(request.POST.get('id', "body")) - # clear out the status line as a courtesy - form.set_html(".status", "") + # clear out the status line as a courtesy + form.set_html(".status", "") - try: + # do the actual work + val = self_method(self, form, responder, *a, **kw) - kw = _make_validated_kw(fn, simple_vals, param_vals, env) - val = fn(self, form, jquery, *a, **kw) + # auto-refresh the captcha if there are errors. + if (c.errors.errors and + any(isinstance(v, VCaptcha) for v in simple_vals)): + form.new_captcha() + + if val: return val + return self.response_func(dict(iter(responder))) - # auto-refresh the captcha if there are errors. - if (c.errors.errors and - any(isinstance(v, VCaptcha) for v in simple_vals)): - form.new_captcha() - - if val: return val - return self.response_func(**dict(list(jquery))) - except UserRequiredException: - jquery = JQueryResponse() - jquery.refresh() - return self.response_func(**dict(list(jquery))) - return newfn - return val #### validators #### class nop(Validator): diff --git a/r2/r2/lib/jsonresponse.py b/r2/r2/lib/jsonresponse.py index 4f3cc5a89..ee1bcae2b 100644 --- a/r2/r2/lib/jsonresponse.py +++ b/r2/r2/lib/jsonresponse.py @@ -27,63 +27,76 @@ from r2.lib.template_helpers import replace_render from r2.lib.jsontemplates import get_api_subtype from r2.lib.base import BaseController import simplejson +from pylons import c def json_respond(x): - from pylons import c - if get_api_subtype(): - res = JsonResponse() - res.object = tup(x) - res = dict(res) - else: - res = x or '' - return websafe_json(simplejson.dumps(res)) + return websafe_json(simplejson.dumps(x or '')) -class JQueryResponse(object): +class JsonResponse(object): """ - class which mimics the jQuery in javascript for allowing Dom - manipulations on the client side. - - An instantiated JQueryResponse acts just like the "$" function on - the JS layer with the exception of the ability to run arbitrary - code on the client. Selectors and method functions evaluate to - new JQueryResponse objects, and the transformations are cataloged - by the original object which can be iterated and sent across the - wire. + Simple Api response handler, returning a list of errors generated + in the api func's validators, as well as blobs of data set by the + api func. """ - def __init__(self, factory = None): + def __init__(self): + self._clear() + + def _clear(self): self._has_errors = set([]) self._new_captcha = False - if factory: - self.factory = factory - self.ops = None - self.objs = None - else: - self.factory = self - self.objs = {self: 0} - self.ops = [] + self._data = {} + def send_failure(self, error): + c.errors.add(error) + self._clear() + self._has_errors.add(error) + def __call__(self, *a): - return self.factory.transform(self, "call", a) + return self def __getattr__(self, key): - if not key.startswith("__"): - return self.factory.transform(self, "attr", key) - - def transform(self, obj, op, args): - new = self.__class__(self) - newi = self.objs[new] = len(self.objs) - self.ops.append([self.objs[obj], newi, op, args]) - return new + return self def __iter__(self): - yield ("jquery", self.ops) + res = {} + if self._data: + res['data'] = self._data + res['errors'] = [(e, c.errors[e].message) for e in self._has_errors] + yield ("json", res) + + def _mark_error(self, e): + pass + + def _unmark_error(self, e): + pass def has_error(self): return bool(self._has_errors) - # thing methods - #-------------- - + def has_errors(self, input, *errors, **kw): + rval = False + for e in errors: + if e in c.errors: + # get list of params checked to generate this error + # if they exist, make sure they match input checked + fields = c.errors[e].fields + if not input or not fields or input in fields: + self._has_errors.add(e) + rval = True + self._mark_error(e) + else: + self._unmark_error(e) + + if rval and input: + self.focus_input(input) + return rval + + def clear_errors(self, *errors): + for e in errors: + if e in self._has_errors: + self._has_errors.remove(e) + self._unmark_error(e) + def _things(self, things, action, *a, **kw): """ function for inserting/replacing things in listings. @@ -104,8 +117,8 @@ class JQueryResponse(object): if d.has_key('data'): d['data'].update(kw) - new = self.__getattr__(action) - return new(data, *a) + self._data['things'] = data + return data def insert_things(self, things, append = False, **kw): return self._things(things, "insert_things", append, **kw) @@ -115,6 +128,67 @@ class JQueryResponse(object): return self._things(things, "replace_things", keep_children, reveal, stubs, **kw) + def _send_data(self, **kw): + self._data.update(kw) + + +class JQueryResponse(JsonResponse): + """ + class which mimics the jQuery in javascript for allowing Dom + manipulations on the client side. + + An instantiated JQueryResponse acts just like the "$" function on + the JS layer with the exception of the ability to run arbitrary + code on the client. Selectors and method functions evaluate to + new JQueryResponse objects, and the transformations are cataloged + by the original object which can be iterated and sent across the + wire. + """ + def __init__(self, top_node = None): + if top_node: + self.top_node = top_node + else: + self.top_node = self + JsonResponse.__init__(self) + self._clear() + + def _clear(self): + if self.top_node == self: + self.objs = {self: 0} + self.ops = [] + else: + self.objs = None + self.ops = None + JsonResponse._clear(self) + + def send_failure(self, error): + JsonResponse.send_failure(self, error) + self.refresh() + + def __call__(self, *a): + return self.top_node.transform(self, "call", a) + + def __getattr__(self, key): + if not key.startswith("__"): + return self.top_node.transform(self, "attr", key) + + def transform(self, obj, op, args): + new = self.__class__(self) + newi = self.objs[new] = len(self.objs) + self.ops.append([self.objs[obj], newi, op, args]) + return new + + def __iter__(self): + yield ("jquery", self.ops) + + # thing methods + #-------------- + + def _things(self, things, action, *a, **kw): + data = JsonResponse._things(self, things, action, *a, **kw) + new = self.__getattr__(action) + return new(data, *a) + def insert_table_rows(self, rows, index = -1): new = self.__getattr__("insert_table_rows") return new([row.render() for row in tup(rows)], index) @@ -122,31 +196,11 @@ class JQueryResponse(object): # convenience methods: # -------------------- - def has_errors(self, input, *errors, **kw): - from pylons import c - rval = False - for e in errors: - if e in c.errors: - # get list of params checked to generate this error - # if they exist, make sure they match input checked - fields = c.errors[e].fields - if input and fields and input not in fields: - continue - self._has_errors.add(e) - rval = True - self.find("." + e).show().html(c.errors[e].message).end() - else: - self.find("." + e).html("").end() - if rval and input: - self.focus_input(input) - return rval + def _mark_error(self, e): + self.find("." + e).show().html(c.errors[e].message).end() - def clear_errors(self, *errors): - from pylons import c - for e in errors: - if e in self._has_errors: - self._has_errors.remove(e) - self.find("." + e).hide().html("").end() + def _unmark_error(self, e): + self.find("." + e).html("").end() def new_captcha(self): if not self._new_captcha: diff --git a/r2/r2/lib/jsontemplates.py b/r2/r2/lib/jsontemplates.py index ee04a4a4a..a19be4c8c 100644 --- a/r2/r2/lib/jsontemplates.py +++ b/r2/r2/lib/jsontemplates.py @@ -22,16 +22,17 @@ from utils import to36, tup, iters from wrapped import Wrapped from mako.template import Template +from r2.lib.filters import spaceCompress, safemarkdown +import time, pytz +from pylons import c def api_type(subtype = ''): return 'api-' + subtype if subtype else 'api' def is_api(subtype = ''): - from pylons import c return c.render_style and c.render_style.startswith(api_type(subtype)) def get_api_subtype(): - from pylons import c if is_api() and c.render_style.startswith('api-'): return c.render_style[4:] @@ -42,7 +43,6 @@ def make_fullname(typ, _id): return '%s_%s' % (make_typename(typ), to36(_id)) def mass_part_render(thing, **kw): - from r2.lib.filters import spaceCompress return dict([(k, spaceCompress(thing.part_render(v)).strip(' ')) \ for k, v in kw.iteritems()]) @@ -69,7 +69,6 @@ class TableRowTemplate(JsonTemplate): class UserItemJsonTemplate(TableRowTemplate): def cells(self, thing): - from r2.lib.filters import spaceCompress cells = [] for cell in thing.cells: r = Wrapped.part_render(thing, 'cell_type', cell) @@ -84,7 +83,16 @@ class UserItemJsonTemplate(TableRowTemplate): class ThingJsonTemplate(JsonTemplate): - __data_attrs__ = dict() + _data_attrs_ = dict(id = "_id36", + name = "_fullname", + created = "created", + created_utc = "created_utc") + + @classmethod + def data_attrs(cls, **kw): + d = cls._data_attrs_.copy() + d.update(kw) + return d def points(self, wrapped): """ @@ -121,9 +129,7 @@ class ThingJsonTemplate(JsonTemplate): * content : rendered representation of the thing by calling replace_render on it using the style of get_api_subtype(). """ - from r2.lib.filters import spaceCompress from r2.lib.template_helpers import replace_render - from pylons import c listing = thing.listing if hasattr(thing, "listing") else None return dict(id = thing._fullname, #vl = self.points(thing), @@ -147,7 +153,7 @@ class ThingJsonTemplate(JsonTemplate): return x return dict((k, strip_data(self.thing_attr(thing, v))) - for k, v in self.__data_attrs__.iteritems()) + for k, v in self._data_attrs_.iteritems()) def thing_attr(self, thing, attr): """ @@ -156,15 +162,15 @@ class ThingJsonTemplate(JsonTemplate): which has to be gotten from the author_id attribute on most things). """ - import time if attr == "author": return thing.author.name elif attr == "created": 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 def data(self, thing): - from pylons import c if get_api_subtype(): return self.rendered_data(thing) else: @@ -174,33 +180,27 @@ class ThingJsonTemplate(JsonTemplate): return dict(kind = self.kind(thing), data = self.data(thing)) class SubredditJsonTemplate(ThingJsonTemplate): - __data_attrs__ = dict(id = "_id36", - name = "_fullname", - subscribers = "score", - title = "title", - url = "path", - description = "description", - created = "created") + _data_attrs_ = ThingJsonTemplate.data_attrs(subscribers = "score", + title = "title", + url = "path", + description = "description") class LinkJsonTemplate(ThingJsonTemplate): - __data_attrs__ = dict(id = "_id36", - name = "_fullname", - ups = "upvotes", - downs = "downvotes", - score = "score", - saved = "saved", - clicked = "clicked", - hidden = "hidden", - likes = "likes", - domain = "domain", - title = "title", - url = "url", - author = "author", - num_comments = "num_comments", - created = "created", - subreddit = "subreddit", - subreddit_id = "subreddit_id") - + _data_attrs_ = ThingJsonTemplate.data_attrs(ups = "upvotes", + downs = "downvotes", + score = "score", + saved = "saved", + clicked = "clicked", + hidden = "hidden", + likes = "likes", + domain = "domain", + title = "title", + url = "url", + author = "author", + num_comments = "num_comments", + subreddit = "subreddit", + subreddit_id = "subreddit_id") + def thing_attr(self, thing, attr): if attr == 'subreddit': return thing.subreddit.name @@ -216,18 +216,16 @@ class LinkJsonTemplate(ThingJsonTemplate): class CommentJsonTemplate(ThingJsonTemplate): - __data_attrs__ = dict(id = "_id36", - name = "_fullname", - ups = "upvotes", - downs = "downvotes", - replies = "child", - body = "body", - likes = "likes", - author = "author", - created = "created", - link_id = "link_id", - parent_id = "parent_id", - ) + _data_attrs_ = ThingJsonTemplate.data_attrs(ups = "upvotes", + downs = "downvotes", + replies = "child", + body = "body", + body_html = "body_html", + likes = "likes", + author = "author", + link_id = "link_id", + parent_id = "parent_id", + ) def thing_attr(self, thing, attr): from r2.models import Comment, Link @@ -238,6 +236,8 @@ class CommentJsonTemplate(ThingJsonTemplate): return make_fullname(Comment, thing.parent_id) except AttributeError: return make_fullname(Link, thing.link_id) + elif attr == "body_html": + return safemarkdown(thing.body) return ThingJsonTemplate.thing_attr(self, thing, attr) def kind(self, wrapped): @@ -260,8 +260,8 @@ class CommentJsonTemplate(ThingJsonTemplate): return d class MoreCommentJsonTemplate(CommentJsonTemplate): - __data_attrs__ = dict(id = "_id36", - name = "_fullname") + _data_attrs_ = dict(id = "_id36", + name = "_fullname") def points(self, wrapped): return [] @@ -269,18 +269,25 @@ class MoreCommentJsonTemplate(CommentJsonTemplate): return "more" class MessageJsonTemplate(ThingJsonTemplate): - __data_attrs__ = dict(id = "_id36", - name = "_fullname", - new = "new", - subject = "subject", - body = "body", - author = "author", - dest = "dest", - created = "created") + _data_attrs_ = ThingJsonTemplate.data_attrs(new = "new", + subject = "subject", + body = "body", + body_html = "body_html", + author = "author", + dest = "dest", + was_comment = "was_comment", + created = "created") def thing_attr(self, thing, attr): - if attr == "dest": + if attr == "was_comment": + return hasattr(thing, "was_comment") + elif attr == "context": + return ("" if not hasattr(thing, "was_comment") + else thing.permalink + "?context=3") + elif attr == "dest": return thing.to.name + elif attr == "body_html": + return safemarkdown(thing.body) return ThingJsonTemplate.thing_attr(self, thing, attr) def rendered_data(self, wrapped): @@ -313,15 +320,13 @@ class NullJsonTemplate(JsonTemplate): return None class ListingJsonTemplate(ThingJsonTemplate): - __data_attrs__ = dict(children = "things") + _data_attrs_ = dict(children = "things") def points(self, w): return [] def rendered_data(self, thing): - from r2.lib.filters import spaceCompress from r2.lib.template_helpers import replace_render - res = [] for a in thing.things: a.listing = thing