Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
...
  • 18 commits
  • 30 files changed
  • 2 commit comments
  • 3 contributors
View
7 r2/r2/__init__.py
@@ -24,6 +24,13 @@
This file loads the finished app from r2.config.middleware.
"""
+# _strptime is imported with PyImport_ImportModuleNoBlock which can fail
+# miserably when multiple threads try to import it simultaneously.
+# import this here to get it over with
+# see "Non Blocking Module Imports" in:
+# http://code.google.com/p/modwsgi/wiki/ApplicationIssues
+import _strptime
+
# defer the (hefty) import until it's actually needed. this allows
# modules below r2 to be imported before cython files are built, also
# provides a hefty speed boost to said imports when they don't need
View
178 r2/r2/controllers/api.py
@@ -2122,8 +2122,7 @@ def POST_editaward(self, form, jquery, award, colliding_award, codename,
@validatedForm(VFlairManager(),
VModhash(),
- user = VExistingUname("name", allow_deleted=True,
- prefer_existing=True),
+ user = VFlairAccount("name"),
text = VFlairText("text"),
css_class = VFlairCss("css_class"))
@api_doc(api_section.flair)
@@ -2173,8 +2172,7 @@ def POST_flair(self, form, jquery, user, text, css_class):
@validatedForm(VFlairManager(),
VModhash(),
- user = VExistingUname("name", allow_deleted=True,
- prefer_existing=True))
+ user = VFlairAccount("name"))
@api_doc(api_section.flair)
def POST_deleteflair(self, form, jquery, user):
# Check validation.
@@ -2216,8 +2214,7 @@ def POST_flaircsv(self, flair_csv):
line_result.error('row', 'improperly formatted row, ignoring')
continue
- user = VExistingUname('name', allow_deleted=True,
- prefer_existing=True).run(name)
+ user = VFlairAccount('name').run(name)
if not user:
line_result.error('user',
"unable to resolve user `%s', ignoring"
@@ -2275,10 +2272,12 @@ def POST_setflairenabled(self, form, jquery, flair_enabled):
VModhash(),
flair_enabled = VBoolean("flair_enabled"),
flair_position = VOneOf("flair_position", ("left", "right")),
+ link_flair_position = VOneOf("link_flair_position",
+ ("", "left", "right")),
flair_self_assign_enabled = VBoolean("flair_self_assign_enabled"))
@api_doc(api_section.flair)
def POST_flairconfig(self, form, jquery, flair_enabled, flair_position,
- flair_self_assign_enabled):
+ link_flair_position, flair_self_assign_enabled):
if c.site.flair_enabled != flair_enabled:
c.site.flair_enabled = flair_enabled
ModAction.create(c.site, c.user, action='editflair',
@@ -2287,6 +2286,10 @@ def POST_flairconfig(self, form, jquery, flair_enabled, flair_position,
c.site.flair_position = flair_position
ModAction.create(c.site, c.user, action='editflair',
details='flair_position')
+ if c.site.link_flair_position != link_flair_position:
+ c.site.link_flair_position = link_flair_position
+ ModAction.create(c.site, c.user, action='editflair',
+ details='link_flair_position')
if c.site.flair_self_assign_enabled != flair_self_assign_enabled:
c.site.flair_self_assign_enabled = flair_self_assign_enabled
ModAction.create(c.site, c.user, action='editflair',
@@ -2296,8 +2299,7 @@ def POST_flairconfig(self, form, jquery, flair_enabled, flair_position,
@paginated_listing(max_page_size=1000)
@validate(VFlairManager(),
- user = VOptionalExistingUname('name', allow_deleted=True,
- prefer_existing=True))
+ user = VFlairAccount('name'))
@api_doc(api_section.flair)
def GET_flairlist(self, num, after, reverse, count, user):
flair = FlairList(num, after, reverse, '', user)
@@ -2308,10 +2310,12 @@ def GET_flairlist(self, num, after, reverse, count, user):
flair_template = VFlairTemplateByID('flair_template_id'),
text = VFlairText('text'),
css_class = VFlairCss('css_class'),
- text_editable = VBoolean('text_editable'))
+ text_editable = VBoolean('text_editable'),
+ flair_type = VOneOf('flair_type', (USER_FLAIR, LINK_FLAIR),
+ default=USER_FLAIR))
@api_doc(api_section.flair)
def POST_flairtemplate(self, form, jquery, flair_template, text,
- css_class, text_editable):
+ css_class, text_editable, flair_type):
if text is None:
text = ''
if css_class is None:
@@ -2336,7 +2340,8 @@ def POST_flairtemplate(self, form, jquery, flair_template, text,
try:
flair_template = FlairTemplateBySubredditIndex.create_template(
c.site._id, text=text, css_class=css_class,
- text_editable=text_editable)
+ text_editable=text_editable,
+ flair_type=flair_type)
except OverflowError:
form.set_html(".status:first", _('max flair templates reached'))
return
@@ -2345,16 +2350,24 @@ def POST_flairtemplate(self, form, jquery, flair_template, text,
# Push changes back to client.
if new:
- jquery('#empty-flair-template').before(
- FlairTemplateEditor(flair_template).render(style='html'))
+ empty_ids = {
+ USER_FLAIR: '#empty-user-flair-template',
+ LINK_FLAIR: '#empty-link-flair-template',
+ }
+ empty_id = empty_ids[flair_type]
+ jquery(empty_id).before(
+ FlairTemplateEditor(flair_template, flair_type)
+ .render(style='html'))
empty_template = FlairTemplate()
empty_template._committed = True # to disable unnecessary warning
- jquery('#empty-flair-template').html(
- FlairTemplateEditor(empty_template).render(style='html'))
+ jquery(empty_id).html(
+ FlairTemplateEditor(empty_template, flair_type)
+ .render(style='html'))
form.set_html('.status', _('saved'))
else:
jquery('#%s' % flair_template._id).html(
- FlairTemplateEditor(flair_template).render(style='html'))
+ FlairTemplateEditor(flair_template, flair_type)
+ .render(style='html'))
form.set_html('.status', _('saved'))
jquery('input[name="text"]').data('saved', text)
jquery('input[name="css_class"]').data('saved', css_class)
@@ -2372,36 +2385,64 @@ def POST_deleteflairtemplate(self, form, jquery, flair_template):
ModAction.create(c.site, c.user, action='editflair',
details='flair_delete_template')
- @validatedForm(VFlairManager(), VModhash())
+ @validatedForm(VFlairManager(), VModhash(),
+ flair_type = VOneOf('flair_type', (USER_FLAIR, LINK_FLAIR),
+ default=USER_FLAIR))
@api_doc(api_section.flair)
- def POST_clearflairtemplates(self, form, jquery):
- FlairTemplateBySubredditIndex.clear(c.site._id)
+ def POST_clearflairtemplates(self, form, jquery, flair_type):
+ FlairTemplateBySubredditIndex.clear(c.site._id, flair_type=flair_type)
jquery.refresh()
ModAction.create(c.site, c.user, action='editflair',
details='flair_clear_template')
@validate(VUser(),
- user = VOptionalExistingUname('name'))
- def POST_flairselector(self, user):
+ user = VFlairAccount('name'),
+ link = VFlairLink('link'))
+ def POST_flairselector(self, user, link):
+ if link:
+ if hasattr(c.site, '_id') and c.site._id == link.sr_id:
+ site = c.site
+ else:
+ site = Subreddit._byID(link.sr_id, data=True)
+ return FlairSelector(link=link, site=site).render()
if user and not (c.user_is_admin or c.site.is_moderator(c.user)):
# ignore user parameter if c.user is not mod/admin
user = None
- return FlairSelector(user).render()
+ return FlairSelector(user=user).render()
@validatedForm(VUser(),
VModhash(),
- user = VOptionalExistingUname('name'),
- flair_template = VFlairTemplateByID('flair_template_id'),
- text = VFlairText("text"))
+ user = VFlairAccount('name'),
+ link = VFlairLink('link'),
+ flair_template_id = nop('flair_template_id'),
+ text = VFlairText('text'))
@api_doc(api_section.flair)
- def POST_selectflair(self, form, jquery, user, flair_template, text):
- if not flair_template:
- # TODO: serve error to client
- g.log.debug('invalid flair template for subreddit %s', c.site._id)
- return
+ def POST_selectflair(self, form, jquery, user, link, flair_template_id,
+ text):
+ if link:
+ flair_type = LINK_FLAIR
+ if hasattr(c.site, '_id') and c.site._id == link.sr_id:
+ site = c.site
+ else:
+ site = Subreddit._byID(link.sr_id, data=True)
+ else:
+ flair_type = USER_FLAIR
+ site = c.site
- if not c.site.is_moderator(c.user) and not c.user_is_admin:
- if not c.site.flair_self_assign_enabled:
+ if flair_template_id:
+ try:
+ flair_template = FlairTemplateBySubredditIndex.get_template(
+ site._id, flair_template_id, flair_type=flair_type)
+ except NotFound:
+ # TODO: serve error to client
+ g.log.debug('invalid flair template for subreddit %s', site._id)
+ return
+ else:
+ flair_template = None
+ text = None
+
+ if not site.is_moderator(c.user) and not c.user_is_admin:
+ if not site.flair_self_assign_enabled:
# TODO: serve error to client
g.log.debug('flair self-assignment not permitted')
return
@@ -2410,34 +2451,63 @@ def POST_selectflair(self, form, jquery, user, flair_template, text):
user = c.user
# Ignore given text if user doesn't have permission to customize it.
- if not flair_template.text_editable:
+ if not (flair_template and flair_template.text_editable):
text = None
if not text:
- text = flair_template.text
+ text = flair_template.text if flair_template else None
- css_class = flair_template.css_class
+ css_class = flair_template.css_class if flair_template else None
+ text_editable = (
+ flair_template.text_editable if flair_template else False)
- c.site.add_flair(user)
- setattr(user, 'flair_%s_text' % c.site._id, text)
- setattr(user, 'flair_%s_css_class' % c.site._id, css_class)
- user._commit()
+ if flair_type == USER_FLAIR:
+ site.add_flair(user)
+ setattr(user, 'flair_%s_text' % site._id, text)
+ setattr(user, 'flair_%s_css_class' % site._id, css_class)
+ user._commit()
- if (c.site.is_moderator(c.user) or c.user_is_admin) and c.user != user:
- ModAction.create(c.site, c.user, action='editflair', target=user,
- details='flair_edit')
+ if ((c.site.is_moderator(c.user) or c.user_is_admin)
+ and c.user != user):
+ ModAction.create(c.site, c.user, action='editflair',
+ target=user, details='flair_edit')
- # Push some client-side updates back to the browser.
- u = WrappedUser(user, force_show_flair=True,
- flair_text_editable=flair_template.text_editable,
- include_flair_selector=True)
- flair = u.render(style='html')
- jquery('.tagline .flairselectable.id-%s'
- % user._fullname).parent().html(flair)
- jquery('#flairrow_%s input[name="text"]' % user._id36).data(
- 'saved', text).val(text)
- jquery('#flairrow_%s input[name="css_class"]' % user._id36).data(
- 'saved', css_class).val(css_class)
+ # Push some client-side updates back to the browser.
+ u = WrappedUser(user, force_show_flair=True,
+ flair_text_editable=text_editable,
+ include_flair_selector=True)
+ flair = u.render(style='html')
+ jquery('.tagline .flairselectable.id-%s'
+ % user._fullname).parent().html(flair)
+ jquery('#flairrow_%s input[name="text"]' % user._id36).data(
+ 'saved', text).val(text)
+ jquery('#flairrow_%s input[name="css_class"]' % user._id36).data(
+ 'saved', css_class).val(css_class)
+ elif flair_type == LINK_FLAIR:
+ link.flair_text = text
+ link.flair_css_class = css_class
+ link._commit()
+
+ if ((c.site.is_moderator(c.user) or c.user_is_admin)):
+ ModAction.create(c.site, c.user, action='editflair',
+ target=link, details='flair_edit')
+
+ # Push some client-side updates back to the browser.
+
+ jquery('.id-%s .entry .linkflair' % link._fullname).remove()
+ title_path = '.id-%s .entry > .title > .title' % link._fullname
+
+ # TODO: move this to a template
+ if flair_template:
+ flair = '<span class="linkflair %s">%s</span>' % (
+ ' '.join('linkflair-' + c for c in css_class.split()), text)
+ if c.site.link_flair_position == 'left':
+ jquery(title_path).before(flair)
+ elif c.site.link_flair_position == 'right':
+ jquery(title_path).after(flair)
+
+ # TODO: close the selector popup more gracefully
+ jquery('body').click()
@validatedForm(secret_used=VAdminOrAdminSecret("secret"),
award=VByName("fullname"),
View
1 r2/r2/controllers/errors.py
@@ -88,6 +88,7 @@
('TOO_OLD', _("that's a piece of history now; it's too late to reply to it")),
('BAD_CSS_NAME', _('invalid css name')),
('TOO_MUCH_FLAIR_CSS', _('too many flair css classes')),
+ ('BAD_FLAIR_TARGET', _('not a valid flair target')),
('OAUTH2_INVALID_CLIENT', _('invalid client id')),
('OAUTH2_ACCESS_DENIED', _('access denied by the user')),
('CONFIRM', _("please confirm the form")),
View
7 r2/r2/controllers/front.py
@@ -694,6 +694,7 @@ def GET_search_reddits(self, query, reverse, after, count, num):
simple=True).render()
return res
+ search_help_page = "/help/search"
verify_langs_regex = re.compile(r"\A[a-z][a-z](,[a-z][a-z])*\Z")
@base_listing
@validate(query = VLength('q', max_length=512),
@@ -734,7 +735,11 @@ def GET_search(self, query, num, reverse, after, count, sort, restrict_sr):
cleanup_message = strings.invalid_search_query % {
"clean_query": cleaned
}
-
+ cleanup_message += " "
+ cleanup_message += strings.search_help % {"search_help":
+ self.search_help_page
+ }
+
res = SearchPage(_('search results'), query, t, num, content=spane,
nav_menus = [SearchSortMenu(default=sort)],
search_params = dict(sort = sort),
View
61 r2/r2/controllers/validator/validator.py
@@ -1028,45 +1028,27 @@ def param_docs(self):
pass
return params
-class VOptionalExistingUname(VRequired):
- def __init__(self, item, allow_deleted=False, prefer_existing=False,
- *a, **kw):
- self.allow_deleted = allow_deleted
- self.prefer_existing = prefer_existing
+class VExistingUname(VRequired):
+ def __init__(self, item, *a, **kw):
VRequired.__init__(self, item, errors.NO_USER, *a, **kw)
def run(self, name):
- if self.prefer_existing:
- result = self._lookup(name, False)
- if not result and self.allow_deleted:
- result = self._lookup(name, True)
- else:
- result = self._lookup(name, self.allow_deleted)
- return result or self.error(errors.USER_DOESNT_EXIST)
-
- def _lookup(self, name, allow_deleted):
if name and name.startswith('~') and c.user_is_admin:
try:
user_id = int(name[1:])
return Account._byID(user_id, True)
except (NotFound, ValueError):
- return None
+ self.error(errors.USER_DOESNT_EXIST)
# make sure the name satisfies our user name regexp before
# bothering to look it up.
name = chkuser(name)
if name:
try:
- return Account._by_name(name, allow_deleted=allow_deleted)
+ return Account._by_name(name)
except NotFound:
- return None
-
-class VExistingUname(VOptionalExistingUname):
- def run(self, name):
- user = VOptionalExistingUname.run(self, name)
- if not user:
- self.error()
- return user
+ self.error(errors.USER_DOESNT_EXIST)
+ self.error()
def param_docs(self):
return {
@@ -1667,6 +1649,36 @@ def run(self, name):
if name and self.target_re.match(name):
return name
+class VFlairAccount(VRequired):
+ def __init__(self, item, *a, **kw):
+ VRequired.__init__(self, item, errors.BAD_FLAIR_TARGET, *a, **kw)
+
+ def _lookup(self, name, allow_deleted):
+ try:
+ return Account._by_name(name, allow_deleted=allow_deleted)
+ except NotFound:
+ return None
+
+ def run(self, name):
+ if not name:
+ return self.error()
+ return (
+ self._lookup(name, False)
+ or self._lookup(name, True)
+ or self.error())
+
+class VFlairLink(VRequired):
+ def __init__(self, item, *a, **kw):
+ VRequired.__init__(self, item, errors.BAD_FLAIR_TARGET, *a, **kw)
+
+ def run(self, name):
+ if not name:
+ return self.error()
+ try:
+ return Link._by_fullname(name, data=True)
+ except NotFound:
+ return self.error()
+
class VFlairCss(VCssName):
def __init__(self, param, max_css_classes=10, **kw):
self.max_css_classes = max_css_classes
@@ -1688,7 +1700,6 @@ def run(self, css):
return css
-
class VFlairText(VLength):
def __init__(self, param, max_length=64, **kw):
VLength.__init__(self, param, max_length, **kw)
View
12 r2/r2/lib/menus.py
@@ -130,7 +130,7 @@ def __getattr__(self, attr):
contributors = _("edit approved submitters"),
banned = _("ban users"),
banusers = _("ban users"),
- flair = _("edit user flair"),
+ flair = _("edit flair"),
log = _("moderation log"),
modqueue = _("moderation queue"),
@@ -363,11 +363,15 @@ def selected_title(self):
class JsButton(NavButton):
"""A button which fires a JS event and thus has no path and cannot
be in the 'selected' state"""
- def __init__(self, title, style = 'js', **kw):
- NavButton.__init__(self, title, '#', style = style, **kw)
+ def __init__(self, title, style = 'js', tab_name = None, **kw):
+ NavButton.__init__(self, title, '#', style = style, tab_name = tab_name,
+ **kw)
def build(self, *a, **kw):
- self.path = 'javascript:void(0)'
+ if self.tab_name:
+ self.path = '#' + self.tab_name
+ else:
+ self.path = 'javascript:void(0)'
def is_selected(self):
return False
View
133 r2/r2/lib/pages/pages.py
@@ -25,6 +25,7 @@
from r2.models import Friends, All, Sub, NotFound, DomainSR, Random, Mod, RandomNSFW, MultiReddit, ModSR
from r2.models import Link, Printable, Trophy, bidding, PromotionWeights, Comment
from r2.models import Flair, FlairTemplate, FlairTemplateBySubredditIndex
+from r2.models import USER_FLAIR, LINK_FLAIR
from r2.models.oauth2 import OAuth2Client
from r2.models import ModAction
from r2.models import Thing
@@ -2497,14 +2498,18 @@ def __init__(self, num, after, reverse, name, user):
tabs = [
('grant', _('grant flair'), FlairList(num, after, reverse, name,
user)),
- ('templates', _('edit flair templates'), FlairTemplateList()),
+ ('templates', _('user flair templates'),
+ FlairTemplateList(USER_FLAIR)),
+ ('link_templates', _('link flair templates'),
+ FlairTemplateList(LINK_FLAIR)),
]
Templated.__init__(
self,
- tabs=TabbedPane(tabs),
+ tabs=TabbedPane(tabs, linkable=True),
flair_enabled=c.site.flair_enabled,
flair_position=c.site.flair_position,
+ link_flair_position=c.site.link_flair_position,
flair_self_assign_enabled=c.site.flair_self_assign_enabled)
class FlairList(Templated):
@@ -2590,21 +2595,27 @@ def add_line(self):
return self.results_by_line[-1]
class FlairTemplateList(Templated):
+ def __init__(self, flair_type):
+ Templated.__init__(self, flair_type=flair_type)
+
@property
def templates(self):
- ids = FlairTemplateBySubredditIndex.get_template_ids(c.site._id)
+ ids = FlairTemplateBySubredditIndex.get_template_ids(
+ c.site._id, flair_type=self.flair_type)
fts = FlairTemplate._byID(ids)
- return [FlairTemplateEditor(fts[i]) for i in ids]
+ return [FlairTemplateEditor(fts[i], self.flair_type) for i in ids]
class FlairTemplateEditor(Templated):
- def __init__(self, flair_template):
+ def __init__(self, flair_template, flair_type):
Templated.__init__(self,
id=flair_template._id,
text=flair_template.text,
css_class=flair_template.css_class,
text_editable=flair_template.text_editable,
- sample=FlairTemplateSample(flair_template),
- position=getattr(c.site, 'flair_position', 'right'))
+ sample=FlairTemplateSample(flair_template,
+ flair_type),
+ position=getattr(c.site, 'flair_position', 'right'),
+ flair_type=flair_type)
def render(self, *a, **kw):
res = Templated.render(self, *a, **kw)
@@ -2614,11 +2625,16 @@ def render(self, *a, **kw):
class FlairTemplateSample(Templated):
"""Like a read-only version of FlairTemplateEditor."""
- def __init__(self, flair_template):
- wrapped_user = WrappedUser(c.user, subreddit=c.site, force_show_flair=True,
- flair_template=flair_template)
- Templated.__init__(self, flair_template_id=flair_template._id,
- wrapped_user=wrapped_user)
+ def __init__(self, flair_template, flair_type):
+ if flair_type == USER_FLAIR:
+ wrapped_user = WrappedUser(c.user, subreddit=c.site,
+ force_show_flair=True,
+ flair_template=flair_template)
+ else:
+ wrapped_user = None
+ Templated.__init__(self,
+ flair_template=flair_template,
+ wrapped_user=wrapped_user, flair_type=flair_type)
class FlairPrefs(CachedTemplate):
def __init__(self):
@@ -2637,37 +2653,55 @@ def __init__(self):
user_flair_enabled=user_flair_enabled,
wrapped_user=wrapped_user)
+class FlairSelectorLinkSample(CachedTemplate):
+ def __init__(self, link, site, flair_template):
+ flair_position = getattr(site, 'link_flair_position', 'right')
+ CachedTemplate.__init__(self,
+ title=link.title,
+ flair_position=flair_position,
+ flair_template_id=flair_template._id,
+ flair_text=flair_template.text,
+ flair_css_class=flair_template.css_class,
+ flair_text_editable=False,
+ )
+
class FlairSelector(CachedTemplate):
"""Provide user with flair options according to subreddit settings."""
- def __init__(self, user=None):
+ def __init__(self, user=None, link=None, site=None):
if user is None:
user = c.user
+ if site is None:
+ site = c.site
+ admin = bool(c.user_is_admin or site.is_moderator(c.user))
- position = getattr(c.site, 'flair_position', 'right')
-
- attr_pattern = 'flair_%s_%%s' % c.site._id
- text = getattr(user, attr_pattern % 'text', '')
- css_class = getattr(user, attr_pattern % 'css_class', '')
-
- ids = FlairTemplateBySubredditIndex.get_template_ids(c.site._id)
- template_dict = FlairTemplate._byID(ids)
- templates = [template_dict[i] for i in ids]
- for template in templates:
- if template.covers((text, css_class)):
- matching_template = template._id
- break
+ if link:
+ flair_type = LINK_FLAIR
+ target = link
+ target_name = link._fullname
+ attr_pattern = 'flair_%s'
+ position = getattr(site, 'link_flair_position', 'right')
+ target_wrapper = (
+ lambda flair_template: FlairSelectorLinkSample(
+ link, site, flair_template))
else:
- matching_template = None
-
- admin = bool(c.user_is_admin or c.site.is_moderator(c.user))
-
- if c.site.flair_self_assign_enabled or admin:
- choices = [
- WrappedUser(
- user, subreddit=c.site, force_show_flair=True,
- flair_template=template,
- flair_text_editable=admin or template.text_editable)
- for template in templates]
+ flair_type = USER_FLAIR
+ target = user
+ target_name = user.name
+ position = getattr(site, 'flair_position', 'right')
+ attr_pattern = 'flair_%s_%%s' % c.site._id
+ target_wrapper = (
+ lambda flair_template: WrappedUser(
+ user, subreddit=site, force_show_flair=True,
+ flair_template=flair_template,
+ flair_text_editable=admin or template.text_editable))
+
+ text = getattr(target, attr_pattern % 'text', '')
+ css_class = getattr(target, attr_pattern % 'css_class', '')
+ templates, matching_template = self._get_templates(
+ site, flair_type, text, css_class)
+
+ if site.flair_self_assign_enabled or admin:
+ choices = [target_wrapper(template) for template in templates]
# If one of the templates is already selected, modify its text to match
# the user's current flair.
@@ -2678,13 +2712,23 @@ def __init__(self, user=None):
choice.flair_text = text
break
- wrapped_user = WrappedUser(user, subreddit=c.site,
- force_show_flair=True)
-
Templated.__init__(self, text=text, css_class=css_class,
position=position, choices=choices,
matching_template=matching_template,
- wrapped_user=wrapped_user)
+ target_name=target_name)
+
+ def _get_templates(self, site, flair_type, text, css_class):
+ ids = FlairTemplateBySubredditIndex.get_template_ids(
+ site._id, flair_type)
+ template_dict = FlairTemplate._byID(ids)
+ templates = [template_dict[i] for i in ids]
+ for template in templates:
+ if template.covers((text, css_class)):
+ matching_template = template._id
+ break
+ else:
+ matching_template = None
+ return templates, matching_template
class FriendList(UserList):
@@ -2984,17 +3028,18 @@ def __init__(self):
else g.default_sr
class TabbedPane(Templated):
- def __init__(self, tabs):
+ def __init__(self, tabs, linkable=False):
"""Renders as tabbed area where you can choose which tab to
render. Tabs is a list of tuples (tab_name, tab_pane)."""
buttons = []
for tab_name, title, pane in tabs:
- buttons.append(JsButton(title, onclick="return select_tab_menu(this, '%s');" % tab_name))
+ onclick = "return select_tab_menu(this, '%s')" % tab_name
+ buttons.append(JsButton(title, tab_name=tab_name, onclick=onclick))
self.tabmenu = JsNavMenu(buttons, type = 'tabmenu')
self.tabs = tabs
- Templated.__init__(self)
+ Templated.__init__(self, linkable=linkable)
class LinkChild(object):
def __init__(self, link, load = False, expand = False, nofollow = False):
View
8 r2/r2/lib/pages/things.py
@@ -34,7 +34,8 @@ class PrintableButtons(Styled):
def __init__(self, style, thing,
show_delete = False, show_report = True,
show_distinguish = False, show_marknsfw = False,
- show_unmarknsfw = False, show_indict = False, is_link=False, **kw):
+ show_unmarknsfw = False, show_indict = False, is_link=False,
+ show_flair = False, **kw):
show_ignore = (thing.show_reports or
(thing.reveal_trial_info and not thing.show_spam))
approval_checkmark = getattr(thing, "approval_checkmark", None)
@@ -56,6 +57,7 @@ def __init__(self, style, thing,
show_distinguish = show_distinguish,
show_marknsfw = show_marknsfw,
show_unmarknsfw = show_unmarknsfw,
+ show_flair = show_flair,
**kw)
class BanButtons(PrintableButtons):
@@ -89,6 +91,9 @@ def __init__(self, thing, comments = True, delete = True, report = True):
else:
show_unmarknsfw = False
+ # add "or is_author" to allow submitters to edit flair on their links
+ show_flair = thing.can_ban
+
# do we show the delete button?
show_delete = is_author and delete and not thing._deleted
# disable the delete button for live sponsored links
@@ -128,6 +133,7 @@ def __init__(self, thing, comments = True, delete = True, report = True):
show_distinguish = show_distinguish,
show_marknsfw = show_marknsfw,
show_unmarknsfw = show_unmarknsfw,
+ show_flair = show_flair,
show_comments = comments,
# promotion
promoted = thing.promoted,
View
1 r2/r2/lib/strings.py
@@ -141,6 +141,7 @@
search_failed = _("Our search machines are under too much load to handle your request right now. :( Sorry for the inconvenience. Try again in a little bit -- but please don't mash reload; that only makes the problem worse."),
invalid_search_query = _("I couldn't understand your query, so I simplified it and searched for \"%(clean_query)s\" instead."),
completely_invalid_search_query = _("I couldn't understand your search query. Please try again."),
+ search_help = _("You may also want to check the [search help page](%(search_help)s) for more information."),
generic_quota_msg = _("You've submitted too many links recently. Please try again in an hour."),
verified_quota_msg = _("Looks like you're either a brand new user or your posts have not been doing well recently. You may have to wait a bit to post again. In the meantime feel free to [check out the reddiquette](%(reddiquette)s) or join the conversation in a different thread."),
unverified_quota_msg = _("Looks like you're either a brand new user or your posts have not been doing well recently. You may have to wait a bit to post again. In the meantime feel free to [check out the reddiquette](%(reddiquette)s), join the conversation in a different thread, or [verify your email address](%(verify)s)."),
View
1 r2/r2/lib/wrapped.pyx
@@ -464,6 +464,7 @@ class CachedTemplate(Templated):
if c.user and hasattr(c.site, '_id'):
keys.extend([
c.site.flair_enabled, c.site.flair_position,
+ c.site.link_flair_position,
c.user.flair_enabled_in_sr(c.site._id),
c.user.pref_show_flair])
keys = [make_cachable(x, *a) for x in keys]
View
78 r2/r2/models/flair.py
@@ -33,6 +33,9 @@
from account import Account
from subreddit import Subreddit
+USER_FLAIR = 'USER_FLAIR'
+LINK_FLAIR = 'LINK_FLAIR'
+
class Flair(Relation(Subreddit, Account)):
@classmethod
def store(cls, sr, account, text = None, css_class = None):
@@ -126,7 +129,7 @@ def covers(self, other_template):
class FlairTemplateBySubredditIndex(tdb_cassandra.Thing):
- """A list of FlairTemplate IDs for a subreddit.
+ """Lists of FlairTemplate IDs for a subreddit.
The FlairTemplate references are stored as an arbitrary number of attrs.
The lexicographical ordering of these attr names gives the ordering for
@@ -139,10 +142,13 @@ class FlairTemplateBySubredditIndex(tdb_cassandra.Thing):
_use_db = True
_connection_pool = 'main'
- _key_prefix = 'ft_'
+ _key_prefixes = {
+ USER_FLAIR: 'ft_',
+ LINK_FLAIR: 'link_ft_',
+ }
@classmethod
- def _new(cls, sr_id):
+ def _new(cls, sr_id, flair_type=USER_FLAIR):
idx = cls(_id=to36(sr_id), sr_id=sr_id)
idx._commit()
return idx
@@ -157,84 +163,102 @@ def by_sr(cls, sr_id, create=False):
raise
@classmethod
- def create_template(cls, sr_id, text='', css_class='', text_editable=False):
+ def create_template(cls, sr_id, text='', css_class='', text_editable=False,
+ flair_type=USER_FLAIR):
idx = cls.by_sr(sr_id, create=True)
- if len(idx._index_keys()) >= cls.MAX_FLAIR_TEMPLATES:
+ if len(idx._index_keys(flair_type)) >= cls.MAX_FLAIR_TEMPLATES:
raise OverflowError
ft = FlairTemplate._new(text=text, css_class=css_class,
text_editable=text_editable)
- idx.insert(ft._id)
+ idx.insert(ft._id, flair_type=flair_type)
return ft
@classmethod
- def get_template_ids(cls, sr_id):
+ def get_template_ids(cls, sr_id, flair_type=USER_FLAIR):
try:
- return list(cls.by_sr(sr_id))
+ return list(cls.by_sr(sr_id).iter_template_ids(flair_type))
except tdb_cassandra.NotFound:
return []
@classmethod
- def get_template(cls, sr_id, ft_id):
- if ft_id not in cls.get_template_ids(sr_id):
- return None
- return FlairTemplate._byID(ft_id)
+ def get_template(cls, sr_id, ft_id, flair_type=None):
+ if flair_type:
+ flair_types = [flair_type]
+ else:
+ flair_types = [USER_FLAIR, LINK_FLAIR]
+ for flair_type in flair_types:
+ if ft_id in cls.get_template_ids(sr_id, flair_type=flair_type):
+ return FlairTemplate._byID(ft_id)
+ return None
@classmethod
- def clear(cls, sr_id):
+ def clear(cls, sr_id, flair_type=USER_FLAIR):
try:
idx = cls.by_sr(sr_id)
except tdb_cassandra.NotFound:
# Everything went better than expected.
return
- for k in idx._index_keys():
+ for k in idx._index_keys(flair_type):
del idx[k]
# TODO: delete the orphaned FlairTemplate row
idx._commit()
- def _index_keys(self):
+ def _index_keys(self, flair_type):
keys = set(self._dirties.iterkeys())
keys |= frozenset(self._orig.iterkeys())
keys -= self._deletes
- return [k for k in keys if k.startswith(self._key_prefix)]
+ key_prefix = self._key_prefixes[flair_type]
+ return [k for k in keys if k.startswith(key_prefix)]
@classmethod
- def _make_index_key(cls, position):
- return '%s%08d' % (cls._key_prefix, position)
+ def _make_index_key(cls, position, flair_type):
+ return '%s%08d' % (cls._key_prefixes[flair_type], position)
- def __iter__(self):
- return (getattr(self, key) for key in sorted(self._index_keys()))
+ def iter_template_ids(self, flair_type):
+ return (getattr(self, key)
+ for key in sorted(self._index_keys(flair_type)))
- def insert(self, ft_id, position=None):
+ def insert(self, ft_id, position=None, flair_type=USER_FLAIR):
"""Insert template reference into index at position.
A position value of None means to simply append.
"""
- ft_ids = list(self)
+ ft_ids = list(self.iter_template_ids(flair_type))
if position is None:
position = len(ft_ids)
if position < 0 or position > len(ft_ids):
raise IndexError(position)
ft_ids.insert(position, ft_id)
# Rewrite ALL the things.
- for k in self._index_keys():
+ for k in self._index_keys(flair_type):
del self[k]
for i, ft_id in enumerate(ft_ids):
- setattr(self, self._make_index_key(i), ft_id)
+ setattr(self, self._make_index_key(i, flair_type), ft_id)
self._commit()
- def delete_by_id(self, ft_id):
- for key in self._index_keys():
+ def delete_by_id(self, ft_id, flair_type=None):
+ if flair_type:
+ flair_types = [flair_type]
+ else:
+ flair_types = [USER_FLAIR, LINK_FLAIR]
+ for flair_type in flair_types:
+ if self._delete_by_id(ft_id, flair_type):
+ return True
+ g.log.debug("couldn't find %s to delete", ft_id)
+ return False
+
+ def _delete_by_id(self, ft_id, flair_type):
+ for key in self._index_keys(flair_type):
ft = getattr(self, key)
if ft == ft_id:
# TODO: delete the orphaned FlairTemplate row
g.log.debug('deleting ft %s (%s)', ft, key)
del self[key]
self._commit()
return True
- g.log.debug("couldn't find %s to delete", ft_id)
return False
View
12 r2/r2/models/link.py
@@ -61,7 +61,9 @@ class Link(Thing, Printable):
disable_comments = False,
selftext = '',
noselfreply = False,
- ip = '0.0.0.0')
+ ip = '0.0.0.0',
+ flair_text = None,
+ flair_css_class = None)
_essentials = ('sr_id', 'author_id')
_nsfw = re.compile(r"\bnsfw\b", re.I)
@@ -255,6 +257,14 @@ def wrapped_cache_key(wrapped, style):
elif style == "compact":
s.append(c.permalink_page)
s.append(getattr(wrapped, 'media_object', {}))
+ s.append(wrapped.flair_text)
+ s.append(wrapped.flair_css_class)
+
+ # if browsing a single subreddit, incorporate link flair position
+ # in the key so 'flair' buttons show up appropriately for mods
+ if hasattr(c.site, '_id'):
+ s.append(c.site.link_flair_position)
+
return s
def make_permalink(self, sr, force_domain = False):
View
7 r2/r2/models/modaction.py
@@ -39,7 +39,7 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
'addcontributor': _('add contributor'),
'removecontributor': _('remove contributor'),
'editsettings': _('edit settings'),
- 'editflair': _('edit user flair'),
+ 'editflair': _('edit flair'),
'distinguish': _('distinguish'),
'marknsfw': _('mark nsfw')}
@@ -54,7 +54,7 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
'addcontributor': _('added approved contributor'),
'removecontributor': _('removed approved contributor'),
'editsettings': _('edited settings'),
- 'editflair': _('edited user flair'),
+ 'editflair': _('edited flair'),
'distinguish': _('distinguished'),
'marknsfw': _('marked nsfw')}
@@ -90,7 +90,8 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
'flair_delete': _('delete flair'),
'flair_csv': _('edit by csv'),
'flair_enabled': _('toggle flair enabled'),
- 'flair_position': _('toggle flair position'),
+ 'flair_position': _('toggle user flair position'),
+ 'link_flair_position': _('toggle link flair position'),
'flair_self_enabled': _('toggle user assigned flair enabled'),
'flair_template': _('add/edit flair templates'),
'flair_delete_template': _('delete flair template'),
View
2 r2/r2/models/subreddit.py
@@ -75,6 +75,7 @@ class Subreddit(Thing, Printable):
link_type = 'any', # one of ('link', 'self', 'any')
flair_enabled = True,
flair_position = 'right', # one of ('left', 'right')
+ link_flair_position = '', # one of ('', 'left', 'right')
flair_self_assign_enabled = False,
)
_essentials = ('type', 'name', 'lang')
@@ -652,6 +653,7 @@ class FakeSubreddit(Subreddit):
def __init__(self):
Subreddit.__init__(self)
self.title = ''
+ self.link_flair_position = 'left'
def is_moderator(self, user):
return c.user_is_loggedin and c.user_is_admin
View
30 r2/r2/public/static/css/reddit.css
@@ -664,7 +664,7 @@ ul.flat-vert {text-align: left;}
a.author { margin-right: 0.5em; }
-.flair {
+.flair, .linkflair {
display: inline-block;
margin-right: .5em;
padding: 0 2px;
@@ -680,7 +680,10 @@ a.author { margin-right: 0.5em; }
font-size: xx-small;
}
+.linkflair { font-size: x-small; }
+
.link .flair {
+ font-size: x-small;
margin-top: -1px;
}
@@ -765,14 +768,27 @@ a.author { margin-right: 0.5em; }
text-decoration: none !important;
}
-.flairselector li { border: 1px solid white; cursor: pointer; }
+.flairselector li {
+ border: 1px solid white;
+ cursor: pointer;
+ display: block !important;
+ padding-left: 4px;
+}
+
+.flairselector li a {
+ color: #369 !important;
+ font-weight: normal !important;
+}
+
.flairselector li:hover { background-color: #bbb; border: 1px solid #bbb; }
.flairselector li a:hover { text-decoration: none; }
.flairselector li.selected { border: dashed 1px black; }
+.flairselector .title { font-size: x-small !important; }
.flairselector form {
border-top: solid 1px gray;
clear: both;
+ display: block;
padding-top: 4px;
text-align: center;
}
@@ -788,6 +804,8 @@ a.author { margin-right: 0.5em; }
.flairselector .customizer input { display: none; }
.flairselector .customizer button { display: inline !important; }
+.flairselector .flairremove { display: none; }
+
.media-button .option { color: red; }
.media-button .option.active {
background: transparent none no-repeat scroll right center;
@@ -798,7 +816,13 @@ a.author { margin-right: 0.5em; }
.embededmedia { margin-top: 5px; margin-left: 60px; }
-.thing .title { color: blue; padding: 0px; overflow: hidden; }
+.thing .title {
+ color: blue;
+ margin-right: .4em;
+ padding: 0px;
+ overflow: hidden;
+}
+
.thing .title:visited { color: #551a8b }
.thing .title.click { color: #551a8b }
View
37 r2/r2/public/static/js/flair.js
@@ -1,7 +1,7 @@
$(function() {
function showSaveButton(field) {
$(field).parent().parent().addClass("edited");
- $(field).parent().parent().find(".status").html("");
+ $(field).parent().parent().find(".status").empty();
}
function onEdit() {
@@ -54,14 +54,25 @@ $(function() {
});
} else {
customizer.removeClass("texteditable");
- input.attr("disabled", "disabled");
- input.css("display", "none");
+ input.attr("disabled", "disabled").hide();
}
- $(".flairselection").html($(this).first().children().clone());
- $(".flairselector button").removeAttr("disabled");
+ var remover = $(".flairselector .flairremove").detach();
+ $(".flairselection").html($(this).first().children().clone())
+ .append(remover);
+ $(".flairselector .flairremove").css("display", "inline-block");
return false;
}
+ function removeFlairInSelector(e) {
+ var form = $(this).parent().parent();
+ $(form).children('input[name="flair_template_id"]').val("");
+ $(form).children(".customizer").hide();
+ var remover = $(".flairselector .flairremove").detach();
+ $(remover).hide();
+ $(".flairselector li").removeClass("selected");
+ $(".flairselection").empty().append(remover);
+ }
+
function postFlairSelection(e) {
$(this).parent().parent().siblings("input").val(this.id);
post_form(this.parentNode.parentNode.parentNode, "selectflair");
@@ -142,14 +153,14 @@ $(function() {
.find(".customizer input")
.attr("disabled", "disabled")
.end()
- .find("button")
- .attr("disabled", "disabled")
- .end()
.find("li.selected")
.each(selectFlairInSelector)
.end()
.find("li:not(.error)")
.click(selectFlairInSelector)
+ .end()
+ .find(".flairremove")
+ .click(removeFlairInSelector)
.end();
}
@@ -165,9 +176,12 @@ $(function() {
($(button).position().left + $(button).width() - 18) + "px")
.css("top", $(button).position().top + "px");
- var name = $(selector).siblings("form").find("input").val();
- $.request("flairselector", {"name": name}, handleResponse, true,
- "html");
+ var params = {};
+ $(selector).siblings("form").find("input").each(
+ function(idx, inp) {
+ params[inp.name] = inp.value;
+ });
+ $.request("flairselector", params, handleResponse, true, "html");
return false;
}
@@ -194,6 +208,7 @@ $(function() {
$(".flairtoggle input").change(function() { $(this).parent().submit(); });
$(".tagline").delegate(".flairselectbtn", "click", openFlairSelector);
+ $(".thing").delegate(".flairselectbtn", "click", openFlairSelector);
$(".flairselector .dropdown").click(toggleFlairSelector);
});
View
15 r2/r2/templates/flairpane.html
@@ -50,7 +50,7 @@
${_("allow users to assign their own flair")}
</label>
</%utils:line_field>
- <%utils:line_field title="${_('flair position')}">
+ <%utils:line_field title="${_('user flair position')}">
<table class="small-field">
${utils.radio_type('flair_position', "left", _("left"),
_("position flair to the left of the username"),
@@ -60,6 +60,19 @@
thing.flair_position == 'right')}
</table>
</%utils:line_field>
+ <%utils:line_field title="${_('link flair position')}">
+ <table class="small-field">
+ ${utils.radio_type('link_flair_position', "", _("none"),
+ _("don't show link flair"),
+ not thing.link_flair_position)}
+ ${utils.radio_type('link_flair_position', "left", _("left"),
+ _("position flair to the left of the link"),
+ thing.link_flair_position == 'left')}
+ ${utils.radio_type('link_flair_position', "right", _("right"),
+ _("position flair to the right of the link"),
+ thing.link_flair_position == 'right')}
+ </table>
+ </%utils:line_field>
<div class="save-button">
<button type="submit">${_("save options")}</button>
</div>
View
8 r2/r2/templates/flairselector.html
@@ -39,8 +39,12 @@
</ul>
</div>
<form action="/post/selectflair" method="post">
- <div class="flairselection"></div>
- <input type="hidden" name="name" value="${thing.wrapped_user.name}">
+ <div class="flairselection">
+ <div class="flairremove">
+ (<a href="javascript://void(0)">${_('remove flair')}</a>)
+ </div>
+ </div>
+ <input type="hidden" name="name" value="${thing.target_name}">
<input type="hidden" name="flair_template_id">
<div class="customizer">
<input type="text" size="16" maxlength="64" name="text">
View
40 r2/r2/templates/flairselectorlinksample.html
@@ -0,0 +1,40 @@
+## 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 CondeNet, Inc.
+##
+## All portions of the code written by CondeNet are Copyright (c) 2006-2010
+## CondeNet, Inc. All Rights Reserved.
+################################################################################
+
+<%def name="flair()">
+ <span class="flair ${thing.flair_css_class}">
+ ${thing.flair_text}
+ </span>
+</%def>
+
+%if thing.flair_position == 'left':
+ ${flair()}
+%endif
+<%
+ title = thing.title
+ if len(title) > 10:
+ title = title[:7] + '...'
+%>
+<a class="title" href="javascript://void(0)">${title}</a>
+%if thing.flair_position == 'right':
+ ${flair()}
+%endif
View
1 r2/r2/templates/flairtemplateeditor.html
@@ -29,6 +29,7 @@
%if thing.id:
<input type="hidden" name="flair_template_id" value="${thing.id}" />
%endif
+ <input type="hidden" name="flair_type" value="${thing.flair_type}" />
<span class="flaircell flairsample-${thing.position} tagline">
%if thing.text or thing.css_class:
${unsafe(thing.sample.render())}
View
14 r2/r2/templates/flairtemplatelist.html
@@ -21,7 +21,7 @@
################################################################################
<%!
- from r2.models import FlairTemplate
+ from r2.models import FlairTemplate, USER_FLAIR, LINK_FLAIR
from r2.lib.pages.pages import FlairTemplateEditor
empty_template = FlairTemplate()
@@ -39,12 +39,20 @@
%for flair_template in thing.templates:
${flair_template}
%endfor
- <div id="empty-flair-template">
- ${FlairTemplateEditor(empty_template)}
+
+ <%
+ if thing.flair_type == USER_FLAIR:
+ empty_id = 'empty-user-flair-template'
+ elif thing.flair_type == LINK_FLAIR:
+ empty_id = 'empty-link-flair-template'
+ %>
+ <div id="${empty_id}">
+ ${FlairTemplateEditor(empty_template, thing.flair_type)}
</div>
</div>
<form class="clearflairtemplates"
method="post" action="/api/clearflairtemplates">
+ <input type="hidden" name="flair_type" value="${thing.flair_type}" />
<button class="flairtemplateclear">
${_("clear all flair templates")}
</button>
View
33 r2/r2/templates/flairtemplatesample.html
@@ -20,4 +20,35 @@
## CondeNet, Inc. All Rights Reserved.
################################################################################
-${thing.wrapped_user}
+<%namespace file="link.html" import="entry" />
+
+<%
+from r2.models import USER_FLAIR, LINK_FLAIR
+%>
+
+<%def name="flair()">
+ %if thing.flair_template.text or thing.flair_template.css_class:
+ ## TODO: clean this up
+ <span class=
+ "linkflair ${' '.join('linkflair-' + c for c in thing.flair_template.css_class.split())}">
+ ${thing.flair_template.text}
+ </span>
+ %endif
+</%def>
+
+
+%if thing.flair_type == USER_FLAIR:
+ ${thing.wrapped_user}
+%elif thing.flair_type == LINK_FLAIR:
+ <div class="thing">
+ <p class="title">
+ %if c.site.link_flair_position != 'right':
+ <%call expr="flair()" />
+ %endif
+ <a class="title loggedin" href="javascript://void(0)">Link sample</a>
+ %if c.site.link_flair_position == 'right':
+ <%call expr="flair()" />
+ %endif
+ </p>
+ </div>
+%endif
View
15 r2/r2/templates/link.html
@@ -78,11 +78,26 @@
</ul>
</%def>
+<%def name="flair()">
+ %if thing.flair_text or thing.flair_css_class:
+ ## TODO: clean this up
+ <span class="linkflair ${' '.join('linkflair-' + c for c in thing.flair_css_class.split())}">
+ ${thing.flair_text}
+ </span>
+ %endif
+</%def>
+
<%def name="entry()">
<p class="title">
+ %if c.site.link_flair_position == 'left':
+ <%call expr="flair()" />
+ %endif
<%call expr="make_link('title', 'title')">
${thing.title}
</%call>
+ %if c.site.link_flair_position == 'right':
+ <%call expr="flair()" />
+ %endif
%if getattr(thing, "approval_checkmark", None):
<img class="approval-checkmark" title="${thing.approval_checkmark}"
src="${static('green-check.png')}"
View
6 r2/r2/templates/link.htmllite
@@ -43,6 +43,9 @@
${optionalstyle("margin-left: 28px; min-height:32px;")}
%endif
>
+ %if c.site.link_flair_position == 'left' and thing.flair_text:
+ <span class="linkflair">${thing.flair_text}</span>
+ %endif
<a href="${thing.href_url}" class="reddit-link-title"
${optionalstyle("text-decoration:none;color:#336699;font-size:small;")}
%if thing.nofollow:
@@ -61,6 +64,9 @@
>
${thing.title}
</a>
+ %if c.site.link_flair_position == 'right' and thing.flair_text:
+ <span class="linkflair">${thing.flair_text}</span>
+ %endif
%if not expanded:
<br />
%endif
View
3 r2/r2/templates/modaction.html
@@ -40,7 +40,8 @@
<td class="button">${thing.button}</td>