From 87c52ce751aa0d0148ff7c8f382fb3bd3e2f3157 Mon Sep 17 00:00:00 2001 From: Logan Hanks Date: Mon, 3 Oct 2011 17:19:31 -0700 Subject: [PATCH] Implement a flair template management interface. --- r2/r2/controllers/api.py | 46 +++++++++++- r2/r2/lib/pages/pages.py | 96 +++++++++++++++++++++--- r2/r2/models/flair.py | 27 +++++++ r2/r2/public/static/css/reddit.css | 3 + r2/r2/public/static/js/flair.js | 24 ++++-- r2/r2/templates/flairpane.html | 3 +- r2/r2/templates/flairselector.html | 44 +++++++++++ r2/r2/templates/flairtemplateeditor.html | 49 ++++++++++++ r2/r2/templates/flairtemplatelist.html | 40 ++++++++++ r2/r2/templates/flairtemplatesample.html | 23 ++++++ r2/r2/templates/subredditinfobar.html | 20 +---- 11 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 r2/r2/templates/flairselector.html create mode 100644 r2/r2/templates/flairtemplateeditor.html create mode 100644 r2/r2/templates/flairtemplatelist.html create mode 100644 r2/r2/templates/flairtemplatesample.html diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index af890629a..f4b8028b4 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -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), diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 7cf7663ca..987577383 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -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' diff --git a/r2/r2/models/flair.py b/r2/r2/models/flair.py index cf2ab1c3b..d8b85c351 100644 --- a/r2/r2/models/flair.py +++ b/r2/r2/models/flair.py @@ -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. diff --git a/r2/r2/public/static/css/reddit.css b/r2/r2/public/static/css/reddit.css index f74848dc2..afc32d629 100644 --- a/r2/r2/public/static/css/reddit.css +++ b/r2/r2/public/static/css/reddit.css @@ -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; } diff --git a/r2/r2/public/static/js/flair.js b/r2/r2/public/static/js/flair.js index 78aff67e6..1931b5508 100644 --- a/r2/r2/public/static/js/flair.js +++ b/r2/r2/public/static/js/flair.js @@ -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(); }); }); diff --git a/r2/r2/templates/flairpane.html b/r2/r2/templates/flairpane.html index 6f82bd18f..ba37b52c8 100644 --- a/r2/r2/templates/flairpane.html +++ b/r2/r2/templates/flairpane.html @@ -52,6 +52,7 @@ + + ${unsafe(thing.tabs.render())} - ${unsafe(thing.flair_list.render())} diff --git a/r2/r2/templates/flairselector.html b/r2/r2/templates/flairselector.html new file mode 100644 index 000000000..6ba52ffcd --- /dev/null +++ b/r2/r2/templates/flairselector.html @@ -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: +
+ + ${_("Show my flair on this reddit. It looks like:")} +
+ + ${thing.wrapped_user} +
    + %for choice in thing.choices: +
  • + ${unsafe(choice.render())} +
  • + %endfor +
+
+
+%endif diff --git a/r2/r2/templates/flairtemplateeditor.html b/r2/r2/templates/flairtemplateeditor.html new file mode 100644 index 000000000..e33e0ad91 --- /dev/null +++ b/r2/r2/templates/flairtemplateeditor.html @@ -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"/> + +
+
+ %if thing.id: + + %endif + + %if thing.text or thing.css_class: + ${unsafe(thing.sample.render())} + %endif + + + + + + + + + + ${utils.error_field('BAD_CSS_NAME', 'css_class')} + ${utils.error_field('TOO_MUCH_FLAIR_CSS', 'css_class')} +
+
diff --git a/r2/r2/templates/flairtemplatelist.html b/r2/r2/templates/flairtemplatelist.html new file mode 100644 index 000000000..483bd852a --- /dev/null +++ b/r2/r2/templates/flairtemplatelist.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. +################################################################################ + +<%! + from r2.models import FlairTemplate + from r2.lib.pages.pages import FlairTemplateEditor + + empty_template = FlairTemplate() + empty_template._committed = True # to disable unnecessary warning +%> + +
+
+ %for flair_template in thing.templates: + ${unsafe(flair_template.render())} + %endfor +
+ ${unsafe(FlairTemplateEditor(empty_template).render())} +
+
+
diff --git a/r2/r2/templates/flairtemplatesample.html b/r2/r2/templates/flairtemplatesample.html new file mode 100644 index 000000000..ccc36a78e --- /dev/null +++ b/r2/r2/templates/flairtemplatesample.html @@ -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())} diff --git a/r2/r2/templates/subredditinfobar.html b/r2/r2/templates/subredditinfobar.html index 5f0cc9701..396feebab 100644 --- a/r2/r2/templates/subredditinfobar.html +++ b/r2/r2/templates/subredditinfobar.html @@ -36,22 +36,6 @@ hidden_data = dict(id = thing.sr._fullname))} -<%def name="flair_control(flair_user)"> - %if flair_user: -
- - ${_("Show my flair on this reddit. It looks like:")} -
- ${flair_user} -
-
- %endif - -

${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}