RESTful API for multis.

This commit is contained in:
Max Goodman
2013-03-09 01:36:55 -08:00
parent 1d5b3d7451
commit fc6724530b
8 changed files with 257 additions and 0 deletions

View File

@@ -309,6 +309,10 @@ def make_map():
action='relnote')
mc('/api/:action', controller='api')
mc("/api/multi/mine", controller="multiapi", action="my_multis")
mc("/api/multi/*path/r/:sr_name", controller="multiapi", action="multi_subreddit")
mc("/api/multi/*path", controller="multiapi", action="multi")
mc("/api/v1/:action", controller="oauth2frontend",
requirements=dict(action="authorize"))
mc("/api/v1/:action", controller="oauth2access",

View File

@@ -39,6 +39,7 @@ api('promotedlink', PromotedLinkJsonTemplate)
api('comment', CommentJsonTemplate)
api('message', MessageJsonTemplate)
api('subreddit', SubredditJsonTemplate)
api('labeledmulti', LabeledMultiJsonTemplate)
api('morerecursion', MoreCommentJsonTemplate)
api('morechildren', MoreCommentJsonTemplate)
api('reddit', RedditJsonTemplate)

View File

@@ -78,6 +78,7 @@ def load_controllers():
from api import ApiminimalController
from api_docs import ApidocsController
from apiv1 import APIv1Controller
from multi import MultiApiController
from oauth2 import OAuth2FrontendController
from oauth2 import OAuth2AccessController
from redirect import RedirectController

View File

@@ -66,6 +66,9 @@ section_info = {
'subreddits': {
'title': _('subreddits'),
},
'multis': {
'title': _('multis'),
},
'users': {
'title': _('users'),
},
@@ -172,6 +175,7 @@ class ApidocsController(RedditController):
from r2.controllers.captcha import CaptchaController
from r2.controllers.front import FrontController
from r2.controllers.wiki import WikiApiController
from r2.controllers.multi import MultiApiController
from r2.controllers import listingcontroller
api_controllers = [
@@ -179,6 +183,7 @@ class ApidocsController(RedditController):
(ApiController, '/api'),
(ApiminimalController, '/api'),
(WikiApiController, '/api/wiki'),
(MultiApiController, '/api/multi'),
(CaptchaController, ''),
(FrontController, '')
]

154
r2/r2/controllers/multi.py Normal file
View File

@@ -0,0 +1,154 @@
# 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 reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
# Inc. All Rights Reserved.
###############################################################################
from pylons import c, request
from r2.config.extensions import set_extension
from r2.controllers.api_docs import api_doc, api_section
from r2.controllers.reddit_base import RedditController
from r2.controllers.oauth2 import (
OAuth2ResourceController,
require_oauth2_scope,
)
from r2.models.subreddit import (
Subreddit,
LabeledMulti,
TooManySubredditsException,
)
from r2.lib.db import tdb_cassandra
from r2.lib.wrapped import Wrapped
from r2.lib.validator import (
validate,
VUser,
VModhash,
VSRByName,
VJSON,
VMultiPath,
VMultiByPath,
)
from r2.lib.pages.things import wrap_things
from r2.lib.errors import errors, reddit_http_error, RedditError
from r2.lib.base import abort
class MultiApiController(RedditController, OAuth2ResourceController):
def pre(self):
set_extension(request.environ, "json")
self.check_for_bearer_token()
RedditController.pre(self)
def on_validation_error(self, error):
abort(reddit_http_error(
code=error.code,
error_name=error.name,
explanation=error.message,
fields=error.fields,
))
@require_oauth2_scope("read")
@api_doc(api_section.multis)
@validate(VUser())
def GET_my_multis(self):
"""Fetch a list of multis belonging to the current user."""
multis = LabeledMulti.by_owner(c.user)
wrapped = wrap_things(*multis)
resp = [w.render() for w in wrapped]
return self.api_wrapper(resp)
@require_oauth2_scope("read")
@api_doc(
api_section.multis,
uri="/api/multi/{multipath}",
)
@validate(multi=VMultiByPath("path", require_view=True))
def GET_multi(self, multi):
"""Fetch a multi's data and subreddit list by name."""
resp = wrap_things(multi)[0].render()
return self.api_wrapper(resp)
@require_oauth2_scope("subscribe")
@api_doc(api_section.multis, extends=GET_multi)
@validate(
VUser(),
VModhash(),
info=VMultiPath("path"),
data=VJSON("model"),
)
def PUT_multi(self, info, data):
"""Create or update a multi."""
if info['username'] != c.user.name:
raise RedditError('BAD_MULTI_NAME', code=400, fields="path")
try:
multi = LabeledMulti._byID(info['path'])
except tdb_cassandra.NotFound:
multi = LabeledMulti.create(info['path'], c.user)
if 'visibility' in data:
if data['visibility'] not in ('private', 'public'):
raise RedditError('INVALID_OPTION', code=400, fields="data")
multi.visibility = data['visibility']
multi._commit()
return self.GET_multi(path=info['path'])
@require_oauth2_scope("subscribe")
@api_doc(api_section.multis, extends=GET_multi)
@validate(
VUser(),
VModhash(),
multi=VMultiByPath("path", require_edit=True),
)
def DELETE_multi(self, multi):
"""Delete a multi."""
multi.delete()
@require_oauth2_scope("subscribe")
@api_doc(
api_section.multis,
uri="/api/multi/{multipath}/r/{srname}",
)
@validate(
VUser(),
VModhash(),
multi=VMultiByPath("path", require_edit=True),
sr=VSRByName('sr_name'),
)
def PUT_multi_subreddit(self, multi, sr):
"""Add a subreddit to a multi."""
try:
multi.add_srs({sr._id: {}})
except TooManySubredditsException as e:
raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)
@require_oauth2_scope("subscribe")
@api_doc(api_section.multis, extends=PUT_multi_subreddit)
@validate(
VUser(),
VModhash(),
multi=VMultiByPath("path", require_edit=True),
sr=VSRByName('sr_name'),
)
def DELETE_multi_subreddit(self, multi, sr):
"""Remove a subreddit from a multi."""
multi.del_srs(sr._id)

View File

@@ -121,6 +121,11 @@ error_list = dict((
('BAD_JSONP_CALLBACK', _('that jsonp callback contains invalid characters')),
('INVALID_PERMISSION_TYPE', _("permissions don't apply to that type of user")),
('INVALID_PERMISSIONS', _('invalid permissions string')),
('BAD_MULTI_NAME', _('that name isn\'t going to work')),
('MULTI_NOT_FOUND', _('that multireddit doesn\'t exist')),
('MULTI_CANNOT_EDIT', _('you can\'t change that multireddit')),
('MULTI_TOO_MANY_SUBREDDITS', _('no more space for subreddits in that multireddit')),
('BAD_JSON', _('unable to parse JSON data')),
))
errors = Storage([(e, e) for e in error_list.keys()])

View File

@@ -250,6 +250,24 @@ class SubredditJsonTemplate(ThingJsonTemplate):
else:
return ThingJsonTemplate.thing_attr(self, thing, attr)
class LabeledMultiJsonTemplate(ThingJsonTemplate):
_data_attrs_ = ThingJsonTemplate.data_attrs(
path="path",
name="name",
subreddits="srs",
visibility="visibility",
)
del _data_attrs_["id"]
def kind(self, wrapped):
return "LabeledMulti"
def thing_attr(self, thing, attr):
if attr == "srs":
return [{"name": sr.name} for sr in thing.srs]
else:
return ThingJsonTemplate.thing_attr(self, thing, attr)
class IdentityJsonTemplate(ThingJsonTemplate):
_data_attrs_ = ThingJsonTemplate.data_attrs(name = "name",
link_karma = "safe_karma",

View File

@@ -38,6 +38,7 @@ from r2.lib.permissions import ModeratorPermissionSet
from r2.models import *
from r2.lib.authorize import Address, CreditCard
from r2.lib.utils import constant_time_compare
from r2.lib.require import require, require_split, RequirementException
from r2.lib.errors import errors, RedditError, UserRequiredException
from r2.lib.errors import VerifiedUserRequiredException
@@ -2165,3 +2166,71 @@ class VPermissions(Validator):
self.set_error(errors.INVALID_PERMISSIONS, field=self.param[1])
return (None, None)
return type, perm_set
class VJSON(VRequired):
def __init__(self, item, *args, **kwargs):
VRequired.__init__(self, item, errors.BAD_JSON, *args, **kwargs)
def run(self, json_str):
if not json_str:
return self.error()
else:
try:
return json.loads(json_str)
except ValueError:
return self.error()
def param_docs(self):
return {
self.param: "JSON data",
}
class VMultiPath(Validator):
@classmethod
def normalize(self, path):
if path[0] != '/':
path = '/' + path
path = path.lower().rstrip('/')
return path
def run(self, path):
try:
require(path)
path = self.normalize(path)
require(path.startswith('/user/'))
user, username, m, name = require_split(path, 5, sep='/')[1:]
require(m == 'm')
username = chkuser(username)
require(username)
require(subreddit_rx.match(name))
return {'path': path, 'username': username, 'name': name}
except RequirementException:
self.set_error('BAD_MULTI_NAME', code=400)
def param_docs(self):
return {
self.param: "multireddit url path",
}
class VMultiByPath(Validator):
def __init__(self, param, require_view=True, require_edit=False):
Validator.__init__(self, param)
self.require_view = require_view
self.require_edit = require_edit
def run(self, path):
path = VMultiPath.normalize(path)
multi = LabeledMulti._byID(path)
if not multi or (self.require_view and not multi.can_view(c.user)):
return self.set_error('MULTI_NOT_FOUND', code=404)
if self.require_edit and not multi.can_edit(c.user):
return self.set_error('MULTI_CANNOT_EDIT', code=403)
return multi
def param_docs(self):
return {
self.param: "multireddit url path",
}