updates to JSON api and responses to /api POST interface:

* timestamps returned both as local time and as UTC (created_utc)
 * comment bodies are returned both in markdown and excaped sanitized html (using the same filter as reddit.com listings)
 * new POST parameter api_type=json returns simplified JSON responses for api interaction
 * /api/login returns a valid modhash and cookie when api_type=json
This commit is contained in:
KeyserSosa
2009-03-16 11:42:36 -07:00
parent 49bcf1bdfe
commit c58779c6d0
6 changed files with 242 additions and 186 deletions

View File

@@ -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)

View File

@@ -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')),

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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