diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index fbe7a7bc7..117ca0f37 100755 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -252,8 +252,16 @@ class ApiController(RedditController, OAuth2ResourceController): queries.new_message(m, inbox_rel) @json_validate() - @api_doc(api_section.subreddits) + @api_doc(api_section.subreddits, uses_site=True, extensions=["json"]) def GET_submit_text(self, responder): + """Get the submission text for the subreddit. + + This text is set by the subreddit moderators and intended to be + displayed on the submission form. + + See also: [/api/site_admin](#POST_api_site_admin). + + """ if c.site.over_18 and not c.over18: submit_text = None submit_text_html = None @@ -903,7 +911,8 @@ class ApiController(RedditController, OAuth2ResourceController): The authenticated user must have been invited to moderate the subreddit by one of its current moderators. - See also: [/api/friend](#POST_api_friend). + See also: [/api/friend](#POST_api_friend) and + [/subreddits/mine](#GET_subreddits_mine_{where}). """ @@ -1578,12 +1587,26 @@ class ApiController(RedditController, OAuth2ResourceController): @validatedForm(VUser(), VModhash(), # nop is safe: handled after auth checks below - stylesheet_contents = nop('stylesheet_contents'), - prevstyle = VLength('prevstyle', max_length=36), + stylesheet_contents=nop('stylesheet_contents', + docs={"stylesheet_contents": + "the new stylesheet content"}), + prevstyle=VLength('prevstyle', max_length=36, + docs={"prevstyle": + "(optional) a revision ID"}), op = VOneOf('op',['save','preview'])) @api_doc(api_section.subreddits, uses_site=True) def POST_subreddit_stylesheet(self, form, jquery, stylesheet_contents = '', prevstyle='', op='save'): + """Update a subreddit's stylesheet. + + `op` should be `save` to update the contents of the stylesheet. + + If `prevstyle` is specified, it should be the revision ID the submitted + version is a modification of. Wiki-style edit conflict resolution will + be done when this is specified, otherwise the content will be blindly + overwritten. + + """ if form.has_errors("prevstyle", errors.TOO_LONG): return @@ -1681,10 +1704,16 @@ class ApiController(RedditController, OAuth2ResourceController): name = VCssName('img_name')) @api_doc(api_section.subreddits, uses_site=True) def POST_delete_sr_img(self, form, jquery, name): - """ - Called upon requested delete on /about/stylesheet. - Updates the site's image list, and causes the
  • which wraps - the image to be hidden. + """Remove an image from the subreddit's custom image set. + + The image will no longer count against the subreddit's image limit. + However, the actual image data may still be accessible for an + unspecified amount of time. If the image is currently referenced by the + subreddit's stylesheet, that stylesheet will no longer validate and + won't be editable until the image reference is removed. + + See also: [/api/upload_sr_img](#POST_api_upload_sr_img). + """ # just in case we need to kill this feature from XSS if g.css_killswitch: @@ -1702,8 +1731,12 @@ class ApiController(RedditController, OAuth2ResourceController): VModhash()) @api_doc(api_section.subreddits, uses_site=True) def POST_delete_sr_header(self, form, jquery): - """ - Called when the user request that the header on a sr be reset. + """Remove the subreddit's custom header image. + + The sitewide-default header image will be shown again after this call. + + See also: [/api/upload_sr_img](#POST_api_upload_sr_img). + """ # just in case we need to kill this feature from XSS if g.css_killswitch: @@ -1740,24 +1773,32 @@ class ApiController(RedditController, OAuth2ResourceController): file = VUploadLength('file', max_length=1024*500), name = VCssName("name"), img_type = VImageType('img_type'), - form_id = VLength('formid', max_length = 100), + form_id = VLength('formid', max_length = 100, + docs={"formid": "(optional) can be ignored"}), header = VInt('header', max=1, min=0)) @api_doc(api_section.subreddits, uses_site=True) def POST_upload_sr_img(self, file, header, name, form_id, img_type): - """ - Called on /about/stylesheet when an image needs to be replaced - or uploaded, as well as on /about/edit for updating the - header. Unlike every other POST in this controller, this - method does not get called with Ajax but rather is from the - original form POSTing to a hidden iFrame. Unfortunately, this - means the response needs to generate an page with a script tag - to fire the requisite updates to the parent document, and, - more importantly, that we can't use our normal toolkit for - passing those responses back. + """Add or replace a subreddit image or custom header logo. + + If the `header` value is `0`, an image for use in the subreddit + stylesheet is uploaded with the name specified in `name`. If the value + of `header` is `1` then the image uploaded will be the subreddit's new + logo and `name` will be ignored. + + The `img_type` field specifies whether to store the uploaded image as a + PNG or JPEG. + + Subreddits have a limited number of images that can be in use at any + given time. If no image with the specified name already exists, one of + the slots will be consumed. + + If an image with the specified name already exists, it will be + replaced. This does not affect the stylesheet immediately, but will + take effect the next time the stylesheet is saved. + + See also: [/api/delete_sr_img](#POST_api_delete_sr_img) and + [/api/delete_sr_header](#POST_api_delete_sr_header). - The result of this function is a rendered UploadedImage() - object in charge of firing the completedUploadImage() call in - JS. """ # default error list (default values will reset the errors in @@ -1846,6 +1887,31 @@ class ApiController(RedditController, OAuth2ResourceController): ) @api_doc(api_section.subreddits) def POST_site_admin(self, form, jquery, name, ip, sr, **kw): + """Create or configure a subreddit. + + If `sr` is specified, the request will attempt to modify the specified + subreddit. If not, a subreddit with name `name` will be created. + + This endpoint expects *all* values to be supplied on every request. If + modifying a subset of options, it may be useful to get the current + settings from [/about/edit.json](#GET_r_{subreddit}_about_edit.json) + first. + + The fields `prev_public_description_id`, `prev_description_id` and + `prev_submit_text_id` are optional. If specified, they should be the + wiki revision IDs of the last-seen versions of these pieces of text to + allow for wiki-style edit conflict resolution. + + For backwards compatibility, `description` is the sidebar text and + `public_description` is the publicly visible subreddit description. + + Most of the parameters for this endpoint are identical to options + visible in the user interface and their meanings are best explained + there. + + See also: [/about/edit.json](#GET_r_{subreddit}_about_edit.json). + + """ def apply_wikid_field(sr, form, pagename, value, prev, field, error): id_field_name = 'prev_%s_id' % field try: @@ -2677,6 +2743,15 @@ class ApiController(RedditController, OAuth2ResourceController): sr = VSubscribeSR('sr', 'sr_name')) @api_doc(api_section.subreddits) def POST_subscribe(self, action, sr): + """Subscribe to or unsubscribe from a subreddit. + + To subscribe, `action` should be `sub`. To unsubscribe, `action` should + be `unsub`. The user must have access to the subreddit to be able to + subscribe to it. + + See also: [/subreddits/mine/](#GET_subreddits_mine_{where}). + + """ # only users who can make edits are allowed to subscribe. # Anyone can leave. if sr and (action != 'sub' or sr.can_comment(c.user)): @@ -3382,6 +3457,7 @@ class ApiController(RedditController, OAuth2ResourceController): @json_validate(query=VLength("query", max_length=50)) @api_doc(api_section.subreddits, extensions=["json"]) def GET_subreddits_by_topic(self, responder, query): + """Return a list of subreddits that are relevant to a search query.""" if not g.CLOUDSEARCH_SEARCH_API: return [] diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index 5e5f3ee77..f932f0e74 100755 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -752,8 +752,16 @@ class FrontController(RedditController, OAuth2ResourceController): @validate(location=nop('location'), created=VOneOf('created', ('true','false'), default='false')) + @api_doc(api_section.subreddits, uri="/r/{subreddit}/about/edit", + extensions=["json"]) def GET_editreddit(self, location, created): - """Edit reddit form.""" + """Get the current settings of a subreddit. + + In the API, this returns the current settings of the subreddit as used + by [/api/site_admin](#POST_api_site_admin). On the HTML site, it will + display a form for editing the subreddit. + + """ c.profilepage = True if isinstance(c.site, FakeSubreddit): return self.abort404() @@ -845,10 +853,10 @@ class FrontController(RedditController, OAuth2ResourceController): @base_listing - @validate(query=nop('q')) + @validate(query=nop('q', docs={"q": "a search query"})) @api_doc(api_section.subreddits, uri='/subreddits/search', extensions=['json', 'xml']) def GET_search_reddits(self, query, reverse, after, count, num): - """Search reddits by title and description.""" + """Search subreddits by title and description.""" q = SubredditSearchQuery(query) results, etime, spane = self._search(q, num=num, reverse=reverse, diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index 6e0f95989..178088e70 100755 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -1023,8 +1023,16 @@ class RedditsController(ListingController): @listing_api_doc(section=api_section.subreddits, uri='/subreddits/{where}', - uri_variants=['/subreddits/popular', '/subreddits/new', '/subreddits/banned']) + uri_variants=['/subreddits/popular', '/subreddits/new']) def GET_listing(self, where, **env): + """Get all subreddits. + + The `where` parameter chooses the order in which the subreddits are + displayed. `popular` sorts on the activity of the subreddit and the + position of the subreddits can shift around. `new` sorts the subreddits + based on their creation date, newest first. + + """ self.where = where return ListingController.GET_listing(self, **env) @@ -1098,6 +1106,19 @@ class MyredditsController(ListingController, OAuth2ResourceController): uri='/subreddits/mine/{where}', uri_variants=['/subreddits/mine/subscriber', '/subreddits/mine/contributor', '/subreddits/mine/moderator']) def GET_listing(self, where='subscriber', **env): + """Get subreddits the user has a relationship with. + + The `where` parameter chooses which subreddits are returned as follows: + + * `subscriber` - subreddits the user is subscribed to + * `contributor` - subreddits the user is an approved submitter in + * `moderator` - subreddits the user is a moderator of + + See also: [/api/subscribe](#POST_api_subscribe), + [/api/friend](#POST_api_friend), and + [/api/accept_moderator_invite](#POST_api_accept_moderator_invite). + + """ self.where = where return ListingController.GET_listing(self, **env) diff --git a/r2/r2/lib/validator/validator.py b/r2/r2/lib/validator/validator.py index 2c34d1654..6dd841be0 100644 --- a/r2/r2/lib/validator/validator.py +++ b/r2/r2/lib/validator/validator.py @@ -365,6 +365,11 @@ class VLang(Validator): def run(self, lang): return VLang.validate_lang(lang) + def param_docs(self): + return { + self.param: "a valid IETF language tag (underscore separated)", + } + class VRequired(Validator): def __init__(self, param, error, *a, **kw): Validator.__init__(self, param, *a, **kw) @@ -559,6 +564,12 @@ class VLength(Validator): else: return text + def param_docs(self): + return { + self.param: + "a string no longer than %d characters" % self.max_length, + } + class VUploadLength(VLength): def run(self, upload, text2=''): # upload is expected to be a FieldStorage object @@ -567,6 +578,13 @@ class VUploadLength(VLength): else: self.set_error(self.empty_error, code=400) + def param_docs(self): + kibibytes = self.max_length / 1024 + return { + self.param: + "file upload with maximum size of %d KiB" % kibibytes, + } + class VPrintable(VLength): def run(self, text, text2 = ''): text = VLength.run(self, text, text2) @@ -1158,6 +1176,11 @@ class VSubscribeSR(VByName): return sr + def param_docs(self): + return { + self.param[0]: "name of a subreddit", + } + MIN_PASSWORD_LENGTH = 3 class VPassword(Validator): @@ -1495,6 +1518,11 @@ class VCssName(Validator): self.set_error(errors.BAD_CSS_NAME) return '' + def param_docs(self): + return { + self.param: "a valid subreddit image name", + } + class VMenu(Validator): @@ -1715,6 +1743,11 @@ class VImageType(Validator): return 'png' return img_type + def param_docs(self): + return { + self.param: "one of `png` or `jpg` (default: `png`)", + } + class ValidEmails(Validator): """Validates a list of email addresses passed in as a string and @@ -1837,6 +1870,10 @@ class VCnameDomain(Validator): except UnicodeEncodeError: self.set_error(errors.BAD_CNAME) + def param_docs(self): + # cnames are dead; don't advertise this. + return {} + # NOTE: make sure *never* to have res check these are present # otherwise, the response could contain reference to these errors...!