Named Multireddit API updates

Named multireddit objects now show additional fields:
* description_md
* description_html (read only)
* display_name
* key_color
* icon_url (read only)
* weighting_scheme
* copied_from (read only, requires owner)

The "visibility" field can now also be set to "hidden" via the API. Hidden
multireddits will not be shown on a user's sidebar on reddit.com, but will
still be visible to API consumers.

The "copied_from" field shows the multi's owner which multireddit
they copied from.

A "weighting_scheme" of "fresh" will favor newer content, rather than
forcing there to be at least 1 post from each subreddit. "classic"
weighting will use the old format. Note: "fresh" weighting will be enabled
in a future commit.

"key_color" must be an RGB color of the form #AABBCC. API consumers can
choose to set and make use of the key_color field for style purposes.

"icon_url" may contain a URL to an icon for this multireddit. API
consumers can choose to make use of this icon for style purposes.

"display_name" is a human-friendly name for this multireddit. API
consumers can choose to make use of this field to set/display friendlier
names for this multireddit.

Description fields are now included in the base multireddit object, and
"description_md" can be updated directly on the multireddit object.
The separate description endpoint is still available.

All of the above fields can be modified via the existing endpoint,
PUT /api/multi/<multipath>, except for fields marked read only.

Due to the number of new fields and the absence of an existing PATCH
endpoint for /api/multi/<multipath>, the existing PUT endpoint
has been updated to NOT clobber fields that aren't included in the
multi JSON, and to accept "partial" multireddit objects. This is to
prevent fields from getting clobbered by clients that haven't been
updated to send all the new data.
This commit is contained in:
Keith Mitchell
2015-02-09 16:26:08 -08:00
parent 6a0d5e31fb
commit 14087b336f
5 changed files with 139 additions and 51 deletions

View File

@@ -20,7 +20,7 @@
# Inc. All Rights Reserved.
###############################################################################
from pylons import c, request, response
from pylons import c, g, request, response
from pylons.i18n import _
from r2.config.extensions import set_extension
@@ -34,41 +34,51 @@ from r2.models.subreddit import (
LabeledMulti,
TooManySubredditsError,
)
from r2.lib.db import tdb_cassandra, thing
from r2.lib.wrapped import Wrapped
from r2.lib.db import tdb_cassandra
from r2.lib.validator import (
validate,
VUser,
VColor,
VLength,
VMarkdownLength,
VModhash,
VMultiPath,
VMultiByPath,
VOneOf,
VSubredditName,
VSRByName,
VUser,
VValidatedJSON,
VMarkdownLength,
VMultiPath,
VMultiByPath,
)
from r2.lib.pages.things import wrap_things
from r2.lib.jsontemplates import (
LabeledMultiJsonTemplate,
LabeledMultiDescriptionJsonTemplate,
)
from r2.lib.errors import errors, RedditError
from r2.lib.errors import RedditError
multi_sr_data_json_spec = VValidatedJSON.Object({
'name': VSubredditName('name', allow_language_srs=True),
})
MAX_DESC = 10000
MAX_DISP_NAME = 50
WRITABLE_MULTI_FIELDS = ('visibility', 'description_md', 'display_name',
'key_color', 'weighting_scheme')
multi_json_spec = VValidatedJSON.Object({
'visibility': VOneOf('visibility', ('private', 'public')),
multi_json_spec = VValidatedJSON.PartialObject({
'description_md': VMarkdownLength('description_md', max_length=MAX_DESC,
empty_error=None),
'display_name': VLength('display_name', max_length=MAX_DISP_NAME),
'key_color': VColor('key_color'),
'visibility': VOneOf('visibility', ('private', 'public', 'hidden')),
'weighting_scheme': VOneOf('weighting_scheme', ('classic', 'fresh')),
'subreddits': VValidatedJSON.ArrayOf(multi_sr_data_json_spec),
})
multi_description_json_spec = VValidatedJSON.Object({
'body_md': VMarkdownLength('body_md', max_length=10000, empty_error=None),
'body_md': VMarkdownLength('body_md', max_length=MAX_DESC, empty_error=None),
})
@@ -138,14 +148,25 @@ class MultiApiController(RedditController):
return sr_props
def _write_multi_data(self, multi, data):
multi.visibility = data['visibility']
srs = data.pop('subreddits', None)
if srs is not None:
multi.clear_srs()
try:
self._add_multi_srs(multi, srs)
except:
multi._revert()
raise
multi.clear_srs()
try:
self._add_multi_srs(multi, data['subreddits'])
except:
multi._revert()
raise
if 'icon_name' in data:
try:
multi.set_icon_by_name(data.pop('icon_name'))
except:
multi._revert()
raise
for key, val in data.iteritems():
if key in WRITABLE_MULTI_FIELDS:
setattr(multi, key, val)
multi._commit()
return multi

View File

@@ -283,31 +283,6 @@ class SubredditJsonTemplate(ThingJsonTemplate):
else:
return ThingJsonTemplate.thing_attr(self, thing, attr)
class LabeledMultiJsonTemplate(ThingJsonTemplate):
_data_attrs_ = ThingJsonTemplate.data_attrs(
can_edit="can_edit",
name="name",
path="path",
subreddits="srs",
visibility="visibility",
)
del _data_attrs_["id"]
def kind(self, wrapped):
return "LabeledMulti"
@classmethod
def sr_props(cls, thing, srs):
sr_props = thing.sr_props
return [dict(sr_props[sr._id], name=sr.name) for sr in srs]
def thing_attr(self, thing, attr):
if attr == "srs":
return self.sr_props(thing, thing.srs)
elif attr == "can_edit":
return c.user_is_loggedin and thing.can_edit(c.user)
else:
return ThingJsonTemplate.thing_attr(self, thing, attr)
class LabeledMultiDescriptionJsonTemplate(ThingJsonTemplate):
_data_attrs_ = dict(
@@ -326,6 +301,49 @@ class LabeledMultiDescriptionJsonTemplate(ThingJsonTemplate):
else:
return ThingJsonTemplate.thing_attr(self, thing, attr)
class LabeledMultiJsonTemplate(LabeledMultiDescriptionJsonTemplate):
_data_attrs_ = ThingJsonTemplate.data_attrs(
can_edit="can_edit",
copied_from="copied_from",
description_html="description_html",
description_md="description_md",
display_name="display_name",
key_color="key_color",
icon_url="icon_url",
name="name",
path="path",
subreddits="srs",
visibility="visibility",
weighting_scheme="weighting_scheme",
)
del _data_attrs_["id"]
def kind(self, wrapped):
return "LabeledMulti"
@classmethod
def sr_props(cls, thing, srs):
sr_props = thing.sr_props
return [dict(sr_props[sr._id], name=sr.name) for sr in srs]
def thing_attr(self, thing, attr):
if attr == "srs":
return self.sr_props(thing, thing.srs)
elif attr == "can_edit":
return c.user_is_loggedin and thing.can_edit(c.user)
elif attr == "copied_from":
if thing.can_edit(c.user):
return thing.copied_from
else:
return None
elif attr == "display_name":
return thing.display_name or thing.name
else:
super_ = super(LabeledMultiJsonTemplate, self)
return super_.thing_attr(thing, attr)
class IdentityJsonTemplate(ThingJsonTemplate):
_data_attrs_ = ThingJsonTemplate.data_attrs(
comment_karma="comment_karma",

View File

@@ -1964,11 +1964,18 @@ class ProfilePage(Reddit):
rb.push(scb)
multis = [m for m in LabeledMulti.by_owner(self.user)
if m.visibility == "public"]
if multis:
public_multis = [m for m in LabeledMulti.by_owner(self.user)
if m.is_public()]
if public_multis:
scb = SideContentBox(title=_("public multireddits"), content=[
SidebarMultiList(multis)
SidebarMultiList(public_multis)
])
rb.push(scb)
hidden_multis = [m for m in multis if m.is_hidden()]
if c.user == self.user and hidden_multis:
scb = SideContentBox(title=_("hidden multireddits"), content=[
SidebarMultiList(hidden_multis)
])
rb.push(scb)
@@ -4742,7 +4749,8 @@ class ListingChooser(Templated):
multis = LabeledMulti.by_owner(c.user)
multis.sort(key=lambda multi: multi.name.lower())
for multi in multis:
self.add_item("multi", multi.name, site=multi)
if not multi.is_hidden():
self.add_item("multi", multi.name, site=multi)
explore_sr = g.live_config["listing_chooser_explore_sr"]
if explore_sr:

View File

@@ -1737,6 +1737,21 @@ class VColor(Validator):
}
class VColor(Validator):
"""Validate a string as being a 6 digit hex color starting with #"""
color_re = re.compile(r"\A#[a-zA-Z0-9]{6}\Z")
def run(self, color):
if color and self.color_re.match(color):
return color
else:
return None
def param_docs(self):
return {
self.param: "A 6-digit rgb hex color, e.g. `#AABBCC`"
}
class VMenu(Validator):
def __init__(self, param, menu_cls, remember = True, **kw):

View File

@@ -279,6 +279,21 @@ class Subreddit(Thing, Printable, BaseSite):
'private',
}
KEY_COLORS = {
'': N_('default'),
'#ff4500': N_('orangered'),
'#ffd635': N_('yellow'),
'#fff03e': N_('highlight'),
'#7cd344': N_('green'),
'#25b79f': N_('teal'),
'#24a0ed': N_('blue'),
'#ea0027': N_('red'),
'#ff8717': N_('orange'),
'#c7e223': N_('lime'),
'#46a508': N_('dark green'),
'#008985': N_('dark teal'),
'#0079d3': N_('alien blue'),
}
# note: for purposely unrenderable reddits (like promos) set author_id = -1
@classmethod
def _new(cls, name, title, author_id, ip, lang = g.lang, type = 'public',
@@ -1554,7 +1569,12 @@ class LabeledMulti(tdb_cassandra.Thing, MultiReddit):
_defaults = dict(MultiReddit._defaults,
visibility='private',
description_md='',
copied_from=None, # for internal analysis/bookkeeping purposes
display_name='',
copied_from=None,
key_color=None,
icon_id=None,
icon_url=None,
weighting_scheme="classic",
)
_extra_schema_creation_args = {
"key_validation_class": tdb_cassandra.UTF8_TYPE,
@@ -1677,7 +1697,7 @@ class LabeledMulti(tdb_cassandra.Thing, MultiReddit):
@property
def allows_referrers(self):
if self.visibility != 'public':
if not self.is_public():
return False
return super(LabeledMulti, self).allows_referrers
@@ -1687,11 +1707,17 @@ class LabeledMulti(tdb_cassandra.Thing, MultiReddit):
return _('%s subreddits curated by /u/%s') % (self.name, self.owner.name)
return _('%s subreddits') % self.name
def is_public(self):
return self.visibility == "public"
def is_hidden(self):
return self.visibility == "hidden"
def can_view(self, user):
if c.user_is_admin:
return True
return user == self.owner or self.visibility == 'public'
return user == self.owner or self.is_public()
def can_edit(self, user):
if c.user_is_admin and self.owner == Account.system_user():