Implement a flair template management interface.

This commit is contained in:
Logan Hanks
2011-10-03 17:19:31 -07:00
parent 2750d7b1f7
commit 87c52ce751
11 changed files with 338 additions and 37 deletions

View File

@@ -33,8 +33,9 @@ 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, \
FlairList, FlairCsv, BannedList, BoringPage, FormPage, CssError, \
UploadedImage, ClickGadget, UrlParser, WrappedUser
BannedList, BoringPage, FormPage, CssError, UploadedImage, ClickGadget, \
UrlParser, WrappedUser
from r2.lib.pages import FlairList, FlairCsv, FlairTemplateEditor
from r2.lib.utils.trial_utils import indict, end_trial, trial_info
from r2.lib.pages.things import wrap_links, default_thing_wrapper
@@ -2021,6 +2022,47 @@ class ApiController(RedditController):
flair = FlairList(num, after, reverse, '', user)
return BoringPage(_("API"), content = flair).render()
@validatedForm(VFlairManager(),
VModhash(),
flair_template_id = nop('id'),
text = VFlairText('text'),
css_class = VFlairCss('css_class'))
def POST_flairtemplate(self, form, jquery, flair_template_id, text,
css_class):
# Check validation.
if form.has_errors('css_class', errors.BAD_CSS_NAME):
form.set_html(".status:first", _('invalid css class'))
return
if form.has_errors('css_class', errors.TOO_MUCH_FLAIR_CSS):
form.set_html(".status:first", _('too many css classes'))
return
# Load flair template thing.
if flair_template_id:
ft = FlairTemplate._byID(flair_template_id)
ft.text = text
ft.css_class = css_class
ft._commit()
new = False
else:
ft = FlairTemplateBySubredditIndex.create_template(
c.site._id, text=text, css_class=css_class)
new = True
# TODO(intortus): ...
# Push changes back to client.
if new:
jquery('#empty-flair-template').before(
FlairTemplateEditor(ft).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'))
else:
jquery('input[name="text"]').data('saved', text)
jquery('input[name="css_class"]').data('saved', css_class)
form.set_html('.status', _('saved'))
@validatedForm(VAdminOrAdminSecret("secret"),
award = VByName("fullname"),
description = VLength("description", max_length=1000),

View File

@@ -23,7 +23,8 @@ 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, Comment, Flair
from r2.models import Link, Printable, Trophy, bidding, PromotionWeights, Comment
from r2.models import Flair, FlairTemplate, FlairTemplateBySubredditIndex
from r2.config import cache
from r2.lib.tracking import AdframeInfo
from r2.lib.jsonresponse import json_respond
@@ -491,13 +492,10 @@ class SubredditInfoBar(CachedTemplate):
# so the menus cache properly
self.path = request.path
# if user has flair, then it will be displayed in this bar
self.flair_user = None
if c.user_is_loggedin:
wrapped_user = WrappedUser(c.user, subreddit=self.sr,
force_show_flair=True)
if wrapped_user.has_flair:
self.flair_user = wrapped_user
self.flair_selector = FlairSelector()
else:
self.flair_selector = None
CachedTemplate.__init__(self)
@@ -2272,7 +2270,8 @@ class WrappedUser(CachedTemplate):
FLAIR_CSS_PREFIX = 'flair-'
def __init__(self, user, attribs = [], context_thing = None, gray = False,
subreddit = None, force_show_flair = None):
subreddit = None, force_show_flair = None,
flair_template = None):
attribs.sort()
author_cls = 'author'
@@ -2288,6 +2287,12 @@ class WrappedUser(CachedTemplate):
flair = wrapped_flair(user, subreddit or c.site, force_show_flair)
flair_enabled, flair_position, flair_text, flair_css_class = flair
has_flair = bool(flair_text or flair_css_class)
if flair_template:
flair_text = flair_template.text
flair_css_class = flair_template.css_class
has_flair = True
if flair_css_class:
# This is actually a list of CSS class *suffixes*. E.g., "a b c"
# should expand to "flair-a flair-b flair-c".
@@ -2400,9 +2405,16 @@ class FlairPane(Templated):
def __init__(self, num, after, reverse, name, user):
# Make sure c.site isn't stale before rendering.
c.site = Subreddit._byID(c.site._id)
tabs = [
('templates', _('edit flair templates'), FlairTemplateList()),
('grant', _('grant flair'), FlairList(num, after, reverse, name,
user)),
]
Templated.__init__(
self,
flair_list=FlairList(num, after, reverse, name, user),
tabs=TabbedPane(tabs),
flair_enabled=c.site.flair_enabled,
flair_position=c.site.flair_position)
@@ -2488,6 +2500,72 @@ class FlairCsv(Templated):
self.results_by_line.append(self.LineResult())
return self.results_by_line[-1]
class FlairTemplateList(Templated):
@property
def templates(self):
ids = FlairTemplateBySubredditIndex.get_template_ids(c.site._id)
fts = FlairTemplate._byID(ids)
return [FlairTemplateEditor(fts[i]) for i in ids]
class FlairTemplateEditor(Templated):
def __init__(self, flair_template):
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'))
def render(self, *a, **kw):
res = Templated.render(self, *a, **kw)
if not g.template_debug:
res = spaceCompress(res)
return res
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)
class FlairSelector(CachedTemplate):
"""Provide user with flair options according to subreddit settings."""
def __init__(self):
get_flair_attr = lambda a, default=None: getattr(
c.user, 'flair_%s_%s' % (c.site._id, a), default)
user_flair_enabled = get_flair_attr('enabled', default=True)
text = get_flair_attr('text')
css_class = get_flair_attr('css_class')
sr_flair_enabled = getattr(c.site, 'flair_enabled', True)
ids = FlairTemplateBySubredditIndex.get_template_ids(c.site._id)
# TODO(intortus): Maintain sorting.
templates = FlairTemplate._byID(ids).values()
for template in templates:
if template.covers((text, css_class)):
matching_template = template._id
break
else:
matching_template = None
choices = [WrappedUser(c.user, subreddit=c.site, force_show_flair=True,
flair_template=template)
for template in templates]
wrapped_user = WrappedUser(c.user, subreddit=c.site,
force_show_flair=True)
Templated.__init__(self, user_flair_enabled=user_flair_enabled,
text=text, css_class=css_class,
sr_flair_enabled=sr_flair_enabled,
choices=choices, matching_template=matching_template,
wrapped_user=wrapped_user)
class FriendList(UserList):
"""Friend list on /pref/friends"""
type = 'friend'

View File

@@ -87,6 +87,10 @@ class FlairTemplate(tdb_cassandra.Thing):
@classmethod
def _new(cls, text='', css_class='', text_editable=False):
if text is None:
text = ''
if css_class is None:
css_class = ''
ft = cls(text=text, css_class=css_class, text_editable=text_editable)
ft._commit()
return ft
@@ -97,6 +101,29 @@ class FlairTemplate(tdb_cassandra.Thing):
self._id = str(uuid.uuid1())
return tdb_cassandra.Thing._commit(self, *a, **kw)
def covers(self, other_template):
"""Returns true if other_template is a subset of this one.
The value for other_template may be another FlairTemplate, or a tuple
of (text, css_class). The latter case is treated like a FlairTemplate
that doesn't permit editable text.
For example, if self permits editable text, then this method will return
True as long as just the css_classes match. On the other hand, if self
doesn't permit editable text but other_template does, this method will
return False.
"""
if isinstance(other_template, FlairTemplate):
text_editable = other_template.text_editable
text, css_class = other_template.text, other_template.css_class
else:
text_editable = False
text, css_class = other_template
if self.css_class != css_class:
return False
return self.text_editable or (not text_editable and self.text == text)
class FlairTemplateBySubredditIndex(tdb_cassandra.Thing):
"""A list of FlairTemplate IDs for a subreddit.

View File

@@ -671,6 +671,9 @@ ul.flat-vert {text-align: left;}
margin-right: 4ex;
}
.flairsample-left { text-align: right !important; }
.flairsample-right { text-align: left !important; }
.flairrow .tagline { text-align: left; width:36ex; }
.flairlist .header { text-align: center; }
.flairlist .flaircell input[type="text"] { width: 28ex; }

View File

@@ -14,18 +14,26 @@ $(function() {
showSaveButton(this);
}
function onSubmit() {
function onSubmit(action) {
$(this).removeClass("edited");
return post_form(this, "flair");
return post_form(this, action);
}
/* Attach event handlers to the various flair forms that may be on page. */
$(".flairrow form").submit(onSubmit);
$(".flaircell input").focus(onFocus);
$(".flaircell input").keyup(onEdit);
function makeOnSubmit(action) {
return function() { return onSubmit.call(this, action); };
}
// Attach event handlers to the various flair forms that may be on page.
$(".flairlist").delegate(".flairtemplate form", "submit",
makeOnSubmit('flairtemplate'));
$(".flairlist").delegate("form.flair-entry", "submit",
makeOnSubmit('flair'));
$(".flairlist").delegate(".flaircell input", "focus", onFocus);
$(".flairlist").delegate(".flaircell input", "keyup", onEdit);
// Event handlers for sidebar flair prefs.
$(".flairtoggle").submit(function() {
return post_form(this, 'setflairenabled');
});
return post_form(this, 'setflairenabled');
});
$(".flairtoggle input").change(function() { $(this).parent().submit(); });
});

View File

@@ -52,6 +52,7 @@
</div>
</form>
</div>
${unsafe(thing.tabs.render())}
${unsafe(thing.flair_list.render())}
</div>

View File

@@ -0,0 +1,44 @@
## 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.
################################################################################
%if thing.sr_flair_enabled:
<form class="toggle flairtoggle">
<input id="flair_enabled" type="checkbox" name="flair_enabled"
%if thing.user_flair_enabled:
checked="checked"
%endif
>
${_("Show my flair on this reddit. It looks like:")}
<div class="flairselector">
<input type="hidden" name="flair_template_id">
<span class="dropdown selected">${thing.wrapped_user}</span>
<ul class="drop-choices">
%for choice in thing.choices:
<li class="flairsample-${thing.position}"
id="${choice.flair_template_id}">
${unsafe(choice.render())}
</li>
%endfor
</ul>
</div>
</form>
%endif

View File

@@ -0,0 +1,49 @@
## 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.
################################################################################
<%namespace name="utils" file="utils.html"/>
<div class="flairtemplate flairrow">
<form action="/post/flairtemplate"
method="post" class="medium-text flair-entry">
%if thing.id:
<input type="hidden" name="id" value="${thing.id}" />
%endif
<span class="flaircell flairsample-${thing.position} tagline">
%if thing.text or thing.css_class:
${unsafe(thing.sample.render())}
%endif
</span>
<span class="flaircell">
<input type="text" size="32" maxlength="64" name="text"
value="${thing.text}" />
</span>
<span class="flaircell">
<input type="text" size="32" maxlength="1000" name="css_class"
value="${thing.css_class}" />
</span>
<button type="submit">save</button>
<span class="status"></span>
${utils.error_field('BAD_CSS_NAME', 'css_class')}
${utils.error_field('TOO_MUCH_FLAIR_CSS', 'css_class')}
</form>
</div>

View File

@@ -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.
################################################################################
<%!
from r2.models import FlairTemplate
from r2.lib.pages.pages import FlairTemplateEditor
empty_template = FlairTemplate()
empty_template._committed = True # to disable unnecessary warning
%>
<div class="usertable">
<div class="flairtemplatelist flairlist pretty-form">
%for flair_template in thing.templates:
${unsafe(flair_template.render())}
%endfor
<div id="empty-flair-template">
${unsafe(FlairTemplateEditor(empty_template).render())}
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
## 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.
################################################################################
${unsafe(thing.wrapped_user.render())}

View File

@@ -36,22 +36,6 @@
hidden_data = dict(id = thing.sr._fullname))}
</%def>
<%def name="flair_control(flair_user)">
%if flair_user:
<form class="toggle flairtoggle">
<input id="flair_enabled" type="checkbox" name="flair_enabled"
%if flair_user.flair_enabled:
checked="checked"
%endif
>
${_("Show my flair on this reddit. It looks like:")}
<div class="tagline">
${flair_user}
</div>
</form>
%endif
</%def>
<div class="titlebox">
<h1 class="hover redditname">
${plain_link(thing.sr.name, thing.sr.path, _sr_path=False, _class="hover")}
@@ -75,7 +59,9 @@
_("you are no longer an approved submitter"))}
%endif
${flair_control(thing.flair_user)}
%if thing.flair_selector:
${unsafe(thing.flair_selector.render())}
%endif
%if thing.sr.description:
${thing.sr.usertext}