mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-24 22:38:09 -05:00
Add user "flair" for subreddits.
Flair is a new relation between subreddits and accounts (stored in a manner similar to but distinct from subreddit membership). This relation can have a text field and a CSS class name associated with it (this data is actually stored under the account). The flair data is then incorporated into any mention of the account within the context of the subreddit (namely, on links and comments submitted by the user).
This commit is contained in:
@@ -206,6 +206,8 @@ db_table_jury_account_link = relation, account, link, main
|
||||
db_table_ad = thing, main
|
||||
db_table_adsr = relation, ad, subreddit, main
|
||||
|
||||
db_table_flair = relation, subreddit, account, main
|
||||
|
||||
disallow_db_writes = False
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified
|
||||
from r2.lib.utils import query_string, timefromnow, randstr
|
||||
from r2.lib.utils import timeago, tup, filter_links, levenshtein
|
||||
from r2.lib.pages import EnemyList, FriendList, ContributorList, ModList, \
|
||||
BannedList, BoringPage, FormPage, CssError, UploadedImage, \
|
||||
FlairList, BannedList, BoringPage, FormPage, CssError, UploadedImage, \
|
||||
ClickGadget, UrlParser
|
||||
from r2.lib.utils.trial_utils import indict, end_trial, trial_info
|
||||
from r2.lib.pages.things import wrap_links, default_thing_wrapper
|
||||
@@ -1281,7 +1281,7 @@ class ApiController(RedditController):
|
||||
css_on_cname = VBoolean("css_on_cname"),
|
||||
)
|
||||
def POST_site_admin(self, form, jquery, name, ip, sr,
|
||||
sponsor_text, sponsor_url, sponsor_name, **kw):
|
||||
sponsor_text, sponsor_url, sponsor_name, **kw):
|
||||
# the status button is outside the form -- have to reset by hand
|
||||
form.parent().set_html('.status', "")
|
||||
|
||||
@@ -1926,6 +1926,121 @@ class ApiController(RedditController):
|
||||
award._commit()
|
||||
form.set_html(".status", _('saved'))
|
||||
|
||||
@validatedForm(VFlairManager(),
|
||||
VModhash(),
|
||||
user = VExistingUname("name"),
|
||||
text = VLength("text", max_length=64),
|
||||
css_class = VCssName("css_class"))
|
||||
def POST_flair(self, form, jquery, user, text, css_class):
|
||||
# Check validation.
|
||||
if form.has_errors('name', errors.USER_DOESNT_EXIST, errors.NO_USER):
|
||||
return
|
||||
if form.has_errors('css_class', errors.BAD_CSS_NAME):
|
||||
form.set_html(".status:first", _('invalid css class'))
|
||||
return
|
||||
|
||||
# Make sure the flair relation is up-to-date, for listings.
|
||||
if not c.site.is_flair(user):
|
||||
c.site.add_flair(user)
|
||||
new = True
|
||||
else:
|
||||
new = False
|
||||
|
||||
# Save the flair details in the account data.
|
||||
setattr(user, 'flair_%s_text' % c.site._id, text)
|
||||
setattr(user, 'flair_%s_css_class' % c.site._id, css_class)
|
||||
user._commit()
|
||||
|
||||
if new:
|
||||
user_row = FlairList().user_row(user)
|
||||
jquery("#flair-table").show(
|
||||
).find("table").insert_table_rows(user_row)
|
||||
else:
|
||||
form.set_html('.status', _('saved'))
|
||||
form.set_html(
|
||||
'.user',
|
||||
WrappedUser(user, force_show_flair=True).render(style='html'))
|
||||
|
||||
@validate(VFlairManager(),
|
||||
VModhash(),
|
||||
flair_csv = nop('flair_csv'))
|
||||
def POST_flaircsv(self, flair_csv):
|
||||
limit = 100 # max of 100 flair settings per call
|
||||
flair_text_limit = 64 # max of 64 chars in flair text
|
||||
results = FlairCsv()
|
||||
infile = csv.reader(flair_csv.strip().split('\n'))
|
||||
for i, row in enumerate(infile):
|
||||
line_result = results.add_line()
|
||||
line_no = i + 1
|
||||
if line_no > limit:
|
||||
line_result.error('row',
|
||||
'limit of %d rows per call reached' % limit)
|
||||
break
|
||||
|
||||
try:
|
||||
name, text, css_class = row
|
||||
except ValueError:
|
||||
line_result.error('row', 'improperly formatted row, ignoring')
|
||||
continue
|
||||
|
||||
user = VExistingUname('name').run(name)
|
||||
if not user:
|
||||
line_result.error('user',
|
||||
"unable to resolve user `%s', ignoring"
|
||||
% name)
|
||||
continue
|
||||
|
||||
if not text and not css_class:
|
||||
# this is equivalent to unflairing
|
||||
text = None
|
||||
css_class = None
|
||||
|
||||
if text and len(text) > flair_text_limit:
|
||||
line_result.warn('text',
|
||||
'truncating flair text to %d chars'
|
||||
% flair_text_limit)
|
||||
text = text[:flair_text_limit]
|
||||
|
||||
if css_class and not VCssName('css_class').run(css_class):
|
||||
line_result.error('css',
|
||||
"invalid css class `%s', ignoring"
|
||||
% css_class)
|
||||
continue
|
||||
|
||||
# all validation passed, enflair the user
|
||||
if text or css_class:
|
||||
mode = 'added'
|
||||
c.site.add_flair(user)
|
||||
else:
|
||||
mode = 'removed'
|
||||
c.site.remove_flair(user)
|
||||
setattr(user, 'flair_%s_text' % c.site._id, text)
|
||||
setattr(user, 'flair_%s_css_class' % c.site._id, css_class)
|
||||
user._commit()
|
||||
line_result.status = '%s flair for user %s' % (mode, user.name)
|
||||
line_result.ok = True
|
||||
|
||||
return BoringPage(_("API"), content = results).render()
|
||||
|
||||
@validatedForm(VUser(),
|
||||
VModhash(),
|
||||
flair_enabled = VBoolean("flair_enabled"))
|
||||
def POST_setflairenabled(self, form, jquery, flair_enabled):
|
||||
setattr(c.user, 'flair_%s_enabled' % c.site._id, flair_enabled)
|
||||
c.user._commit()
|
||||
jquery.refresh()
|
||||
|
||||
@noresponse(VFlairManager(),
|
||||
VModhash(),
|
||||
nuser = VExistingUname("name"),
|
||||
iuser = VByName("id"))
|
||||
def POST_unflair(self, nuser, iuser):
|
||||
user = iuser or nuser
|
||||
c.site.remove_flair(user)
|
||||
setattr(user, 'flair_%s_text' % c.site._id, None)
|
||||
setattr(user, 'flair_%s_css_class' % c.site._id, None)
|
||||
user._commit()
|
||||
|
||||
@validatedForm(VAdmin(),
|
||||
award = VByName("fullname"),
|
||||
description = VLength("description", max_length=1000),
|
||||
|
||||
@@ -83,6 +83,7 @@ error_list = dict((
|
||||
('NO_SELFS', _("that reddit doesn't allow text posts")),
|
||||
('NO_LINKS', _("that reddit only allows text posts")),
|
||||
('TOO_OLD', _("that's a piece of history now; it's too late to reply to it")),
|
||||
('BAD_CSS_NAME', _('invalid css name')),
|
||||
))
|
||||
errors = Storage([(e, e) for e in error_list.keys()])
|
||||
|
||||
|
||||
@@ -483,6 +483,8 @@ class FrontController(RedditController):
|
||||
extension_handling = "private"
|
||||
elif is_moderator and location == 'traffic':
|
||||
pane = RedditTraffic()
|
||||
elif is_moderator and location == 'flair':
|
||||
pane = FlairList()
|
||||
elif c.user_is_sponsor and location == 'ads':
|
||||
pane = RedditAds()
|
||||
elif (not location or location == "about") and is_api():
|
||||
|
||||
@@ -642,6 +642,14 @@ class VSrModerator(Validator):
|
||||
or c.user_is_admin):
|
||||
abort(403, "forbidden")
|
||||
|
||||
class VFlairManager(VSrModerator):
|
||||
"""Validates that a user is permitted to manage flair for a subreddit.
|
||||
|
||||
Currently this is the same as VSrModerator. It's a separate class to act as
|
||||
a placeholder if we ever need to give mods a way to delegate this aspect of
|
||||
subreddit administration."""
|
||||
pass
|
||||
|
||||
class VSrCanDistinguish(VByName):
|
||||
def run(self, thing_name):
|
||||
if c.user_is_admin:
|
||||
@@ -1002,16 +1010,22 @@ class VBid(VNumber):
|
||||
return float(bid)
|
||||
|
||||
|
||||
|
||||
class VCssName(Validator):
|
||||
"""
|
||||
returns a name iff it consists of alphanumeric characters and
|
||||
possibly "-", and is below the length limit.
|
||||
"""
|
||||
|
||||
r_css_name = re.compile(r"\A[a-zA-Z0-9\-]{1,100}\Z")
|
||||
|
||||
def run(self, name):
|
||||
if name and self.r_css_name.match(name):
|
||||
return name
|
||||
if name:
|
||||
if self.r_css_name.match(name):
|
||||
return name
|
||||
else:
|
||||
self.set_error(errors.BAD_CSS_NAME)
|
||||
return ''
|
||||
|
||||
|
||||
class VMenu(Validator):
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ menu = MenuHandler(hot = _('hot'),
|
||||
contributors = _("edit approved submitters"),
|
||||
banned = _("ban users"),
|
||||
banusers = _("ban users"),
|
||||
flair = _("edit user flair"),
|
||||
|
||||
popular = _("popular"),
|
||||
create = _("create"),
|
||||
|
||||
@@ -23,7 +23,7 @@ from r2.lib.wrapped import Wrapped, Templated, CachedTemplate
|
||||
from r2.models import Account, FakeAccount, DefaultSR, make_feedurl
|
||||
from r2.models import FakeSubreddit, Subreddit, Ad, AdSR
|
||||
from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, MultiReddit
|
||||
from r2.models import Link, Printable, Trophy, bidding, PromotionWeights
|
||||
from r2.models import Link, Printable, Trophy, bidding, PromotionWeights, Comment, Flair
|
||||
from r2.config import cache
|
||||
from r2.lib.tracking import AdframeInfo
|
||||
from r2.lib.jsonresponse import json_respond
|
||||
@@ -185,6 +185,7 @@ class Reddit(Templated):
|
||||
NamedButton('reports', css_class = 'reddit-reported'),
|
||||
NamedButton('spam', css_class = 'reddit-spam'),
|
||||
NamedButton('banned', css_class = 'reddit-ban'),
|
||||
NamedButton('flair', css_class = 'reddit-flair'),
|
||||
])
|
||||
return [NavMenu(buttons, type = "flat_vert", base_path = "/about/",
|
||||
css_class = "icon-menu", separator = '')]
|
||||
@@ -506,6 +507,7 @@ class SubredditInfoBar(CachedTemplate):
|
||||
NavButton(menu.banusers, 'banned'),
|
||||
NamedButton('traffic'),
|
||||
NavButton(menu.community_settings, 'edit'),
|
||||
NavButton(menu.flair, 'flair'),
|
||||
])
|
||||
return [NavMenu(buttons, type = "flat_vert", base_path = "/about/",
|
||||
separator = '')]
|
||||
@@ -2241,12 +2243,24 @@ class Page_down(Templated):
|
||||
message = kw.get('message', _("This feature is currently unavailable. Sorry"))
|
||||
Templated.__init__(self, message = message)
|
||||
|
||||
def wrapped_flair(user, subreddit):
|
||||
if not hasattr(subreddit, '_id'):
|
||||
return False, '', ''
|
||||
|
||||
get_flair_attr = lambda a, default=None: getattr(
|
||||
user, 'flair_%s_%s' % (subreddit._id, a), default)
|
||||
|
||||
return (get_flair_attr('enabled', default=True), get_flair_attr('text'),
|
||||
get_flair_attr('css_class'))
|
||||
|
||||
class WrappedUser(CachedTemplate):
|
||||
FLAIR_CSS_PREFIX = 'flair-'
|
||||
|
||||
def __init__(self, user, attribs = [], context_thing = None, gray = False):
|
||||
attribs.sort()
|
||||
author_cls = 'author'
|
||||
|
||||
author_title = None
|
||||
author_title = ''
|
||||
if gray:
|
||||
author_cls += ' gray'
|
||||
for tup in attribs:
|
||||
@@ -2255,6 +2269,11 @@ class WrappedUser(CachedTemplate):
|
||||
if tup[1] == 'F' and '(' in tup[3]:
|
||||
author_title = tup[3]
|
||||
|
||||
flair_text, flair_css_class = wrapped_flair(user, context_thing)
|
||||
has_flair = bool(flair_text)
|
||||
if flair_css_class:
|
||||
flair_css_class = self.FLAIR_CSS_PREFIX + flair_css_class
|
||||
|
||||
target = None
|
||||
ip_span = None
|
||||
context_deleted = None
|
||||
@@ -2269,6 +2288,9 @@ class WrappedUser(CachedTemplate):
|
||||
|
||||
CachedTemplate.__init__(self,
|
||||
name = user.name,
|
||||
has_flair = has_flair,
|
||||
flair_text = flair_text,
|
||||
flair_css_class = flair_css_class,
|
||||
author_cls = author_cls,
|
||||
author_title = author_title,
|
||||
attribs = attribs,
|
||||
@@ -2351,6 +2373,36 @@ class UserList(Templated):
|
||||
def container_name(self):
|
||||
return c.site._fullname
|
||||
|
||||
class FlairList(UserList):
|
||||
"""List of users who are tagged with flair within a subreddit."""
|
||||
type = 'flair'
|
||||
destination = 'flair'
|
||||
remove_action = 'unflair'
|
||||
|
||||
def __init__(self):
|
||||
self.cells = ('user', 'flair', 'remove')
|
||||
self.table_headers = (_('user'), _('flair text, css'), '')
|
||||
UserList.__init__(self)
|
||||
|
||||
@property
|
||||
def form_title(self):
|
||||
return _('manage subreddit flair')
|
||||
|
||||
@property
|
||||
def table_title(self):
|
||||
return _('users with flair on %(reddit)s' % dict(reddit = c.site.name))
|
||||
|
||||
def user_ids(self):
|
||||
return c.site.flair
|
||||
|
||||
def user_row(self, user):
|
||||
get_flair_attr = lambda a: getattr(user,
|
||||
'flair_%s_%s' % (c.site._id, a), '')
|
||||
user.flair_text = get_flair_attr('text')
|
||||
user.flair_css_class = get_flair_attr('css_class')
|
||||
return UserTableItem(user, self.type, self.cells, self.container_name,
|
||||
True, self.remove_action, user)
|
||||
|
||||
class FriendList(UserList):
|
||||
"""Friend list on /pref/friends"""
|
||||
type = 'friend'
|
||||
|
||||
@@ -188,6 +188,10 @@ class Subreddit(Thing, Printable):
|
||||
def subscribers(self):
|
||||
return self.subscriber_ids()
|
||||
|
||||
@property
|
||||
def flair(self):
|
||||
return self.flair_ids()
|
||||
|
||||
def spammy(self):
|
||||
return self._spam
|
||||
|
||||
@@ -966,6 +970,30 @@ Subreddit.__bases__ += (UserRel('moderator', SRMember),
|
||||
UserRel('subscriber', SRMember, disable_ids_fn = True),
|
||||
UserRel('banned', SRMember))
|
||||
|
||||
class Flair(Relation(Subreddit, Account)):
|
||||
@classmethod
|
||||
def store(cls, sr, account, text = None, css_class = None):
|
||||
flair = Flair(sr, account, 'flair', text = text, css_class = css_class)
|
||||
flair._commit()
|
||||
|
||||
setattr(account, 'flair_%s_text' % sr._id, text)
|
||||
setattr(account, 'flair_%s_css_class' % sr._id, css_class)
|
||||
account._commit()
|
||||
|
||||
@classmethod
|
||||
@memoize('flair.all_flair_by_sr')
|
||||
def all_flair_by_sr_cache(cls, sr_id):
|
||||
q = cls._query(cls.c._thing1_id == sr_id)
|
||||
return [t._id for t in q]
|
||||
|
||||
@classmethod
|
||||
def all_flair_by_sr(cls, sr_id, _update=False):
|
||||
relids = cls.all_flair_by_sr_cache(sr_id, _update=_update)
|
||||
return cls._byID(relids).itervalues()
|
||||
|
||||
Subreddit.__bases__ += (UserRel('flair', Flair,
|
||||
disable_ids_fn = True,
|
||||
disable_reverse_ids_fn = True),)
|
||||
|
||||
class SubredditPopularityByLanguage(tdb_cassandra.View):
|
||||
_use_db = True
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
<span class="gray">(${_("leaves this page")})</span>
|
||||
</li>
|
||||
<li>
|
||||
<lable for="header-title">header mouseover text:</lable>
|
||||
<label for="header-title">header mouseover text:</label>
|
||||
<input type="text" name="header-title" id="header-title"
|
||||
value="${thing.site.header_title}"
|
||||
/>
|
||||
|
||||
@@ -75,5 +75,21 @@
|
||||
<span title="${thing.rel._date.strftime('%Y-%m-%d %H:%M:%S')}">
|
||||
${timesince(thing.rel._date)}
|
||||
</span>
|
||||
%elif thing.name == "flair":
|
||||
<form action="/post/flair" id="flair-${thing.rel._fullname}"
|
||||
method="post" class="pretty-form medium-text flair-entry"
|
||||
onsubmit="return post_form(this, 'flair');">
|
||||
<input type="hidden" name="name" value="${thing.user.name}" />
|
||||
<input type="text" maxlength="32" name="text" class="tiny"
|
||||
onfocus="$(this).parent().addClass('edited')"
|
||||
value="${getattr(thing.user, 'flair_text', '')}" />
|
||||
<input type="text" maxlength="32" name="css_class" class="tiny"
|
||||
onfocus="$(this).parent().addClass('edited')"
|
||||
value="${getattr(thing.user, 'flair_css_class', '')}" />
|
||||
<button onclick="$(this).parent().removeClass('edited')" type="submit">
|
||||
submit
|
||||
</button>
|
||||
<span class="status"></span>
|
||||
</form>
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
|
||||
<%namespace file="utils.html" import="plain_link" />
|
||||
|
||||
<%def name="_flair(user)">
|
||||
%if user.has_flair:
|
||||
<span class="flair ${user.flair_css_class}">${user.flair_text}</span>
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
%if context_deleted and not c.user_is_admin:
|
||||
[deleted]
|
||||
%else:
|
||||
@@ -31,8 +37,9 @@
|
||||
<span>${_(thing.thing.original_author.name)}</span>
|
||||
%else:
|
||||
${plain_link(thing.name + thing.karma, "/user/%s" % thing.name,
|
||||
_class = thing.author_cls + (" id-%s" % thing.fullname),
|
||||
_sr_path = False, target=target, title=thing.author_title)}
|
||||
_class = thing.author_cls + (" id-%s" % thing.fullname),
|
||||
_sr_path = False, target=target, title=thing.author_title)}
|
||||
${_flair(thing)}
|
||||
<span class="userattrs">
|
||||
%if thing.attribs:
|
||||
 [
|
||||
|
||||
Reference in New Issue
Block a user