diff --git a/r2/r2/config/environment.py b/r2/r2/config/environment.py index e82b48915..1001ce881 100644 --- a/r2/r2/config/environment.py +++ b/r2/r2/config/environment.py @@ -61,9 +61,9 @@ def load_environment(global_conf={}, app_conf={}): #tmpl_options['myghty.escapes'] = dict(l=webhelpers.auto_link, s=webhelpers.simple_format) tmpl_options = config['buffet.template_options'] - tmpl_options['mako.default_filters'] = ["websafe"] + tmpl_options['mako.default_filters'] = ["mako_websafe"] tmpl_options['mako.imports'] = \ - ["from r2.lib.filters import websafe, unsafe", + ["from r2.lib.filters import websafe, unsafe, mako_websafe", "from pylons import c, g, request", "from pylons.i18n import _, ungettext"] diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 59e543d92..3262a1fe3 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -43,11 +43,12 @@ from r2.lib.menus import CommentSortMenu from r2.lib.normalized_hot import expire_hot from r2.lib.captcha import get_iden from r2.lib.strings import strings -from r2.lib.filters import _force_unicode, websafe_json, spaceCompress +from r2.lib.filters import _force_unicode, websafe_json, websafe, spaceCompress from r2.lib.db import queries from r2.lib.media import force_thumbnail, thumbnail_url from r2.lib.comment_tree import add_comment, delete_comment from r2.lib import tracking, sup, cssfilter, emailer +from r2.lib.subreddit_search import search_reddits from simplejson import dumps @@ -89,28 +90,24 @@ class ApiController(RedditController): @validatedForm(VCaptcha(), name=VRequired('name', errors.NO_NAME), - email=VRequired('email', errors.NO_EMAIL), - replyto = ValidEmails("replyto", num = 1), + email=ValidEmails('email', num = 1), reason = VOneOf('reason', ('ad_inq', 'feedback')), - message=VRequired('message', errors.NO_MESSAGE), + message=VRequired('text', errors.NO_TEXT), ) - def POST_feedback(self, form, jquery, name, email, - replyto, reason, message): + def POST_feedback(self, form, jquery, name, email, reason, message): if not (form.has_errors('name', errors.NO_NAME) or - form.has_errors('email', errors.NO_EMAIL) or - (request.POST.get("replyto") and replyto is None and - form.has_errors("replyto", errors.BAD_EMAILS)) or - form.has_errors('personal', errors.NO_MESSAGE) or - form.chk_captcha(errors.BAD_CAPTCHA)): + form.has_errors('email', errors.BAD_EMAILS) or + form.has_errors('text', errors.NO_TEXT) or + form.has_errors('captcha', errors.BAD_CAPTCHA)): + if reason != 'ad_inq': - emailer.feedback_email(email, message, name = name or '', - reply_to = replyto or '') + emailer.feedback_email(email, message, name, reply_to = '') else: - emailer.ad_inq_email(email, message, name = name or '', - reply_to = replyto or '') + emailer.ad_inq_email(email, message, name, reply_to = '') + form.set_html(".status", _("thanks for your message! " "you should hear back from us shortly.")) - form.set_inputs(personal = "", captcha = "") + form.set_inputs(text = "", captcha = "") POST_ad_inq = POST_feedback @@ -121,7 +118,7 @@ class ApiController(RedditController): ip = ValidIP(), to = VExistingUname('to'), subject = VRequired('subject', errors.NO_SUBJECT), - body = VMessage('message')) + body = VMessage(['text', 'message'])) def POST_compose(self, form, jquery, to, subject, body, ip): """ handles message composition under /message/compose. @@ -129,16 +126,15 @@ class ApiController(RedditController): if not (form.has_errors("to", errors.USER_DOESNT_EXIST, errors.NO_USER) or form.has_errors("subject", errors.NO_SUBJECT) or - form.has_errors("message", errors.NO_MSG_BODY, - errors.COMMENT_TOO_LONG) or - form.chk_captcha(errors.BAD_CAPTCHA)): + form.has_errors("text", errors.NO_TEXT, errors.TOO_LONG) or + form.has_errors("captcha", errors.BAD_CAPTCHA)): spam = (c.user._spam or errors.BANNED_IP in c.errors or errors.BANNED_DOMAIN in c.errors) m, inbox_rel = Message._new(c.user, to, subject, body, ip, spam) - form.set_html(".success", _("your message has been delivered")) - form.set_inputs(to = "", subject = "", message = "") + form.set_html(".status", _("your message has been delivered")) + form.set_inputs(to = "", subject = "", text = "", captcha="") if g.write_query_queue: queries.new_message(m, inbox_rel) @@ -155,39 +151,57 @@ class ApiController(RedditController): url = VUrl(['url', 'sr']), title = VTitle('title'), save = VBoolean('save'), - then = VOneOf('then', ('tb', 'comments'), default='comments') - ) - def POST_submit(self, form, jquery, url, title, save, sr, ip, then): + selftext = VSelfText('text'), + kind = VOneOf('kind', ['link', 'self', 'poll']), + then = VOneOf('then', ('tb', 'comments'), default='comments')) + def POST_submit(self, form, jquery, url, selftext, kind, title, save, + sr, ip, then): + #backwards compatability + if url == 'self': + kind = 'self' + if isinstance(url, (unicode, str)): + # VUrl may have replaced 'url' by adding 'http://' form.set_inputs(url = url) - - should_ratelimit = sr.should_ratelimit(c.user, 'link') - #remove the ratelimit error if the user's karma is high - if not should_ratelimit: - c.errors.remove(errors.RATELIMIT) + if not kind: + # this should only happen if somebody is trying to post + # links in some automated manner outside of the regular + # submission page, and hasn't updated their script + return - # check for no url, or clear that error field on return - if form.has_errors("url", errors.NO_URL, errors.BAD_URL): - pass - elif form.has_errors("url", errors.ALREADY_SUB): - form.redirect(url[0].already_submitted_link) - # check for title, otherwise look it up and return it - elif form.has_errors("title", errors.NO_TITLE): - # try to fetch the title - title = get_title(url) - if title: - # note: focus first, since it clears the form - form.set_inputs(title = title) - # wipe out the no title error - form.clear_errors(errors.NO_TITLE) - return + if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, + errors.SUBREDDIT_REQUIRED): + # checking to get the error set in the form, but we can't + # check for rate-limiting if there's no subreddit + return + else: + should_ratelimit = sr.should_ratelimit(c.user, 'link') + #remove the ratelimit error if the user's karma is high + if not should_ratelimit: + c.errors.remove((errors.RATELIMIT, 'ratelimit')) - elif (form.has_errors("title", errors.TITLE_TOO_LONG) or - form.chk_captcha(errors.BAD_CAPTCHA, errors.RATELIMIT)): + if kind == 'link': + # check for no url, or clear that error field on return + if form.has_errors("url", errors.NO_URL, errors.BAD_URL): + pass + elif form.has_errors("url", errors.ALREADY_SUB): + form.redirect(url[0].already_submitted_link) + # check for title, otherwise look it up and return it + elif form.has_errors("title", errors.NO_TEXT): + pass + + elif kind == 'self' and form.has_errors('text', errors.TOO_LONG): pass - if form.has_error() or not title: return + if form.has_errors("title", errors.TOO_LONG, errors.NO_TEXT): + pass + + if form.has_errors('ratelimit', errors.RATELIMIT): + pass + + if form.has_error() or not title: + return # check whether this is spam: spam = (c.user._spam or @@ -195,12 +209,17 @@ class ApiController(RedditController): errors.BANNED_DOMAIN in c.errors) # well, nothing left to do but submit it - l = Link._submit(request.post.title, url, c.user, sr, ip, spam) - if url.lower() == 'self': + l = Link._submit(request.post.title, url if kind == 'link' else 'self', + c.user, sr, ip, spam) + + if kind == 'self': l.url = l.make_permalink_slow() l.is_self = True + l.selftext = selftext + l._commit() l.set_url_cache() + v = Vote.vote(c.user, l, True, ip, spam) if save: r = l._save(c.user) @@ -240,6 +259,18 @@ class ApiController(RedditController): form.redirect(path) + + @validatedForm(VUser(), + url = VSanitizedUrl(['url'])) + def POST_fetch_title(self, form, jquery, url): + if url: + title = get_title(url) + if title: + form.set_inputs(title = title) + form.set_html(".title-status", ""); + else: + form.set_html(".title-status", _("no title found")) + def _login(self, form, user, dest='', rem = None): """ AJAX login handler, used by both login and register to set the @@ -282,7 +313,9 @@ class ApiController(RedditController): form.has_errors("email", errors.BAD_EMAILS) or form.has_errors("passwd", errors.BAD_PASSWORD) or form.has_errors("passwd2", errors.BAD_PASSWORD_MATCH) or - form.chk_captcha(errors.BAD_CAPTCHA,errors.RATELIMIT)): + form.has_errors('ratelimit', errors.RATELIMIT) or + form.has_errors('captcha', errors.BAD_CAPTCHA)): + user = register(name, password) VRatelimit.ratelimit(rate_ip = True, prefix = "rate_register_") @@ -310,7 +343,6 @@ class ApiController(RedditController): self._subscribe(sr, sub) self._login(form, user, dest, rem) - @noresponse(VUser(), VModhash(), @@ -429,7 +461,6 @@ class ApiController(RedditController): """ # password is required to proceed if form.has_errors("curpass", errors.WRONG_PASSWORD): - form.set_input(curpass = "") return # check if the email is valid. If one is given and it is @@ -444,7 +475,7 @@ class ApiController(RedditController): updated = True # change password - if (password and + if (password and not (form.has_errors("newpass", errors.BAD_PASSWORD) or form.has_errors("verpass", errors.BAD_PASSWORD_MATCH))): change_password(c.user, password) @@ -454,6 +485,7 @@ class ApiController(RedditController): else: form.set_html('.status', _('your password has been updated')) + form.set_inputs(curpass = "", newpass = "", verpass = "") # the password has changed, so the user's cookie has been # invalidated. drop a new cookie. self.login(c.user) @@ -509,32 +541,41 @@ class ApiController(RedditController): Report.new(c.user, thing) - @validatedForm(VUser(), VModhash(), - comment = VByNameIfAuthor('parent'), - body = VComment('comment')) - def POST_editcomment(self, form, jquery, comment, body): - - if not form.has_errors("comment", - errors.BAD_COMMENT, errors.COMMENT_TOO_LONG, + @validatedForm(VUser(), + VModhash(), + item = VByNameIfAuthor('thing_id'), + text = VComment('text')) + def POST_editusertext(self, form, jquery, item, text): + if not form.has_errors("text", + errors.NO_TEXT, errors.TOO_LONG, errors.NOT_AUTHOR): - comment.body = body - comment.editted = True - comment._commit() - jquery.replace_things(comment, True, True) + if isinstance(item, Comment): + kind = 'comment' + item.body = text + elif isinstance(item, Link): + kind = 'link' + item.selftext = text - # flag search indexer that something has changed - tc.changed(comment) + item.editted = True + item._commit() + tc.changed(item) + if kind == 'link': + set_last_modified(item, 'comments') + + wrapper = make_wrapper(ListingController.builder_wrapper, + expand_children = True) + jquery(".content").replace_things(item, True, True, wrap = wrapper) @validatedForm(VUser(), - VModhash(), - VRatelimit(rate_user = True, rate_ip = True, - prefix = "rate_comment_"), - ip = ValidIP(), - parent = VSubmitParent('parent'), - comment = VComment('comment')) + VModhash(), + VRatelimit(rate_user = True, rate_ip = True, + prefix = "rate_comment_"), + ip = ValidIP(), + parent = VSubmitParent(['thing_id', 'parent']), + comment = VComment(['text', 'comment'])) def POST_comment(self, commentform, jquery, parent, comment, ip): should_ratelimit = True #check the parent type here cause we need that for the @@ -559,14 +600,14 @@ class ApiController(RedditController): if not should_ratelimit: c.errors.remove(errors.RATELIMIT) - if (not commentform.has_errors("comment", - errors.BAD_COMMENT, - errors.COMMENT_TOO_LONG, + if (not commentform.has_errors("text", + errors.NO_TEXT, + errors.TOO_LONG, errors.RATELIMIT) and not commentform.has_errors("parent", errors.DELETED_COMMENT)): spam = (c.user._spam or errors.BANNED_IP in c.errors) - + if is_message: to = Account._byID(parent.author_id) subject = parent.subject @@ -607,7 +648,6 @@ class ApiController(RedditController): # remove any null listings that may be present jquery("#noresults").hide() - #update the queries if g.write_query_queue: if is_message: @@ -627,10 +667,10 @@ class ApiController(RedditController): VCaptcha(), VRatelimit(rate_user = True, rate_ip = True, prefix = "rate_share_"), - share_from = VLength('share_from', length = 100), + share_from = VLength('share_from', max_length = 100), emails = ValidEmails("share_to"), reply_to = ValidEmails("replyto", num = 1), - message = VLength("message", length = 1000), + message = VLength("message", max_length = 1000), thing = VByName('parent')) def POST_share(self, shareform, jquery, emails, thing, share_from, reply_to, message): @@ -641,14 +681,11 @@ class ApiController(RedditController): if not should_ratelimit: c.errors.remove(errors.RATELIMIT) - if emails and (errors.NO_EMAILS in c.errors): - c.errors.remove(errors.NO_EMAILS) - - # share_from and messages share a comment_too_long error. + # share_from and messages share a too_long error. # finding an error on one necessitates hiding the other error - if shareform.has_errors("share_from", errors.COMMENT_TOO_LONG): + if shareform.has_errors("share_from", errors.TOO_LONG): shareform.find(".message-errors").children().hide() - elif shareform.has_errors("message", errors.COMMENT_TOO_LONG): + elif shareform.has_errors("message", errors.TOO_LONG): shareform.find(".share-form-errors").children().hide() # reply_to and share_to also share errors... elif shareform.has_errors("share_to", errors.BAD_EMAILS, @@ -656,11 +693,12 @@ class ApiController(RedditController): errors.TOO_MANY_EMAILS): shareform.find(".reply-to-errors").children().hide() elif shareform.has_errors("replyto", errors.BAD_EMAILS, - errors.NO_EMAILS, errors.TOO_MANY_EMAILS): shareform.find(".share-to-errors").children().hide() # lastly, check the captcha. - elif shareform.chk_captcha(errors.BAD_CAPTCHA, errors.RATELIMIT): + elif shareform.has_errors("captcha", errors.BAD_CAPTCHA): + pass + elif shareform.has_errors("ratelimit", errors.RATELIMIT): pass else: c.user.add_share_emails(emails) @@ -845,7 +883,7 @@ class ApiController(RedditController): @validate(VSrModerator(), VModhash(), - file = VLength('file', length=1024*500), + file = VLength('file', max_length=1024*500), name = VCssName("name"), header = nop('header')) def POST_upload_sr_img(self, file, header, name): @@ -910,9 +948,9 @@ class ApiController(RedditController): prefix = 'create_reddit_'), sr = VByName('sr'), name = VSubredditName("name"), - title = VSubredditTitle("title"), + title = VLength("title", max_length = 100), domain = VCnameDomain("domain"), - description = VSubredditDesc("description"), + description = VLength("description", max_length = 500), lang = VLang("lang"), over_18 = VBoolean('over_18'), show_media = VBoolean('show_media'), @@ -946,12 +984,12 @@ class ApiController(RedditController): elif not sr and form.has_errors("name", errors.SUBREDDIT_EXISTS, errors.BAD_SR_NAME): form.find('#example_name').hide() - elif form.has_errors('title', errors.NO_TITLE, errors.TITLE_TOO_LONG): + elif form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG): form.find('#example_title').hide() elif form.has_errors('domain', errors.BAD_CNAME, errors.USED_CNAME): form.find('#example_domain').hide() elif (form.has_errors(None, errors.INVALID_OPTION) or - form.has_errors('description', errors.DESC_TOO_LONG)): + form.has_errors('description', errors.TOO_LONG)): pass #creating a new reddit elif not sr: @@ -1141,8 +1179,11 @@ class ApiController(RedditController): @validatedForm(user = VUserWithEmail('name')) def POST_password(self, form, jquery, user): - if not form.has_errors('name', errors.USER_DOESNT_EXIST, - errors.NO_EMAIL_FOR_USER): + if form.has_errors('name', errors.USER_DOESNT_EXIST): + return + elif form.has_errors('name', errors.NO_EMAIL_FOR_USER): + return + else: emailer.password_email(user) form.set_html(".status", _("an email will be sent to that account's address shortly")) @@ -1150,16 +1191,17 @@ class ApiController(RedditController): @validatedForm(cache_evt = VCacheKey('reset', ('key', 'name')), password = VPassword(['passwd', 'passwd2'])) def POST_resetpassword(self, form, jquery, cache_evt, password): - if errors.BAD_USERNAME in c.errors: - # clear reset event -- the user failed to know their user name + if form.has_errors('name', errors.EXPIRED): cache_evt.clear() - return form.redirect('/password') - elif (not form.has_errors('passwd', errors.BAD_PASSWORD) and - not form.has_errors('passwd2', errors.BAD_PASSWORD_MATCH) and - cache_evt.user): + form.redirect('/password') + elif form.has_errors('passwd', errors.BAD_PASSWORD): + pass + elif form.has_errors('passwd2', errors.BAD_PASSWORD_MATCH): + pass + elif cache_evt.user: # successfully entered user name and valid new password change_password(cache_evt.user, password) - self._login(jquery, cache_evt.user, '/resetpassword') + self._login(jquery, cache_evt.user, '/') cache_evt.clear() @@ -1239,7 +1281,7 @@ class ApiController(RedditController): l.num_margin = 0 l.mid_margin = 0 - jquery.replace_things(l, stubs = True) + jquery(".content").replace_things(l, stubs = True) if show: jquery('.organic-listing .link:visible').hide() @@ -1289,7 +1331,7 @@ class ApiController(RedditController): # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url - if form.has_errors('title', errors.NO_TITLE): + if form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG): pass elif form.has_errors('url', errors.NO_URL, errors.BAD_URL): pass @@ -1345,7 +1387,7 @@ class ApiController(RedditController): @validate(VSponsor(), link = VByName('link_id'), - file = VLength('file',500*1024)) + file = VLength('file', 500*1024)) def POST_link_thumb(self, link=None, file=None): errors = dict(BAD_CSS_NAME = "", IMAGE_ERROR = "") try: @@ -1407,3 +1449,18 @@ class ApiController(RedditController): ip = request.ip) ) + @json_validate(query = nop('query')) + def POST_search_reddit_names(self, query): + names = [] + if query: + names = search_reddits(query) + + return {'names': names} + + @validate(link = VByName('link_id', thing_cls = Link)) + def POST_expando(self, link): + if not link: + abort(404, 'not found') + + wrapped = IDBuilder([link._fullname]).get_items()[0][0] + return spaceCompress(websafe(wrapped.link_child.content())) diff --git a/r2/r2/controllers/errors.py b/r2/r2/controllers/errors.py index 712ddd5f6..d46972504 100644 --- a/r2/r2/controllers/errors.py +++ b/r2/r2/controllers/errors.py @@ -25,17 +25,13 @@ from copy import copy error_list = dict(( ('USER_REQUIRED', _("please login to do that")), - ('NO_URL', _('url required')), + ('NO_URL', _('a url is required')), ('BAD_URL', _('you should check that url')), - ('NO_TITLE', _('title required')), - ('TITLE_TOO_LONG', _('you can be more succinct than that')), - ('COMMENT_TOO_LONG', _('you can be more succinct than that')), - ('BAD_CAPTCHA', _('your letters stink')), + ('BAD_CAPTCHA', _('care to try these again?')), ('BAD_USERNAME', _('invalid user name')), ('USERNAME_TAKEN', _('that username is already taken')), ('NO_THING_ID', _('id not specified')), ('NOT_AUTHOR', _("you can't do that")), - ('BAD_COMMENT', _('please enter a comment')), ('DELETED_COMMENT', _('that comment has been deleted')), ('DELETED_THING', _('that element has been deleted.')), ('BAD_PASSWORD', _('invalid password')), @@ -44,9 +40,7 @@ error_list = dict(( ('NO_NAME', _('please enter a name')), ('NO_EMAIL', _('please enter an email address')), ('NO_EMAIL_FOR_USER', _('no email address for that user')), - ('NO_MESSAGE', _('please enter a message')), ('NO_TO_ADDRESS', _('send it to whom?')), - ('NO_MSG_BODY', _('please enter a message')), ('NO_SUBJECT', _('please enter a subject')), ('USER_DOESNT_EXIST', _("that user doesn't exist")), ('NO_USER', _('please enter a username')), @@ -55,6 +49,7 @@ error_list = dict(( ('ALREADY_SUB', _("that link has already been submitted")), ('SUBREDDIT_EXISTS', _('that reddit already exists')), ('SUBREDDIT_NOEXIST', _('that reddit doesn\'t exist')), + ('SUBREDDIT_REQUIRED', _('you must specify a reddit')), ('BAD_SR_NAME', _('that name isn\'t going to work')), ('RATELIMIT', _('you are trying to submit too fast. try again in %(time)s.')), ('EXPIRED', _('your session has expired')), @@ -64,11 +59,13 @@ error_list = dict(( ('BAD_CNAME', "that domain isn't going to work"), ('USED_CNAME', "that domain is already in use"), ('INVALID_OPTION', _('that option is not valid')), - ('DESC_TOO_LONG', _('description is too long')), ('CHEATER', 'what do you think you\'re doing there?'), ('BAD_EMAILS', _('the following emails are invalid: %(emails)s')), ('NO_EMAILS', _('please enter at least one email address')), ('TOO_MANY_EMAILS', _('please only share to %(num)s emails at a time.')), + + ('TOO_LONG', _("this is too long (max: %(max_length)s)")), + ('NO_TEXT', _('we need something here')), )) errors = Storage([(e, e) for e in error_list.keys()]) @@ -97,8 +94,10 @@ class ErrorSet(object): def __init__(self): self.errors = {} - def __contains__(self, error_name): - return self.errors.has_key(error_name) + def __contains__(self, pair): + """Expectes an (error_name, field_name) tuple and checks to + see if it's in the errors list.""" + return self.errors.has_key(pair) def __getitem__(self, name): return self.errors[name] @@ -110,16 +109,16 @@ class ErrorSet(object): for x in self.errors: yield x - def _add(self, error_name, msg, msg_params = {}, field = None): - self.errors[error_name] = Error(error_name, msg, msg_params, - field = field) - def add(self, error_name, msg_params = {}, field = None): msg = error_list[error_name] - self._add(error_name, msg, msg_params = msg_params, field = field) + for field_name in tup(field): + e = Error(error_name, msg, msg_params, field = field_name) + self.errors[(error_name, field_name)] = e - def remove(self, error_name): - if self.errors.has_key(error_name): - del self.errors[error_name] + def remove(self, pair): + """Expectes an (error_name, field_name) tuple and removes it + from the errors list.""" + if self.errors.has_key(pair): + del self.errors[pair] class UserRequiredException(Exception): pass diff --git a/r2/r2/controllers/feedback.py b/r2/r2/controllers/feedback.py index c660ae419..fb123289a 100644 --- a/r2/r2/controllers/feedback.py +++ b/r2/r2/controllers/feedback.py @@ -26,29 +26,16 @@ from r2.lib.pages import FormPage, Feedback, Captcha class FeedbackController(RedditController): - def _feedback(self, name = '', email = '', message='', - replyto='', action=''): - title = _("inquire about advertising on reddit") if action else '' - captcha = Captcha() if not c.user_is_loggedin \ - or c.user.needs_captcha() else None - if request.get.has_key("done"): - success = _("thanks for your message! you should hear back from us shortly.") - else: - success = '' - return FormPage(_("advertise") if action == 'ad_inq' \ - else _("feedback"), - content = Feedback(captcha=captcha, - message=message, - replyto=replyto, - email=email, name=name, - success=success, - action=action, - title=title), + def GET_ad_inq(self): + title = _("inquire about advertising on reddit") + return FormPage('advertise', + content = Feedback(title=title, + action='ad_inq'), loginbox = False).render() - - def GET_ad_inq(self): - return self._feedback(action='ad_inq') - def GET_feedback(self): - return self._feedback() + title = _("send reddit feedback") + return FormPage('feedback', + content = Feedback(title=title, + action='feedback'), + loginbox = False).render() diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index 8e582b20a..82fcfee61 100644 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -109,7 +109,7 @@ class FrontController(RedditController): if not key and request.referer: referer_path = request.referer.split(g.domain)[-1] done = referer_path.startswith(request.fullpath) - elif not cache_evt.user: + elif not getattr(cache_evt, "user", None): return self.abort404() return BoringPage(_("reset password"), content=ResetPassword(key=key, done=done)).render() @@ -174,12 +174,11 @@ class FrontController(RedditController): # insert reply box only for logged in user if c.user_is_loggedin and article.subreddit_slow.can_comment(c.user): #no comment box for permalinks - if not comment: - displayPane.append(CommentReplyBox(link_name = - article._fullname)) - else: - displayPane.append(CommentReplyBox()) - + displayPane.append(UserText(item = article, creating = True, + post_form = 'comment', + display = not bool(comment), + cloneable = True)) + # finally add the comment listing displayPane.append(listing.listing()) @@ -498,7 +497,9 @@ class FrontController(RedditController): return res captcha = Captcha() if c.user.needs_captcha() else None - sr_names = Subreddit.submit_sr_names(c.user) if c.default_sr else () + sr_names = (Subreddit.submit_sr_names(c.user) or + Subreddit.submit_sr_names(None)) + return FormPage(_("submit"), content=NewLink(url=url or '', diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 6b9abfabb..451b025e3 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -48,12 +48,15 @@ class Validator(object): self.default = default self.post, self.get, self.url = post, get, url - def set_error(self, error, msg_params = {}): + def set_error(self, error, msg_params = {}, field = False): """ Adds the provided error to c.errors and flags that it is come from the validator's param """ - c.errors.add(error, msg_params = msg_params, field = self.param) + if field is False: + field = self.param + + c.errors.add(error, msg_params = msg_params, field = field) def __call__(self, url): a = [] @@ -135,7 +138,7 @@ def api_validate(response_function): simple_vals, param_vals, *a, **kw) except UserRequiredException: responder.send_failure(errors.USER_REQUIRED) - return self.response_func(dict(iter(responder))) + return self.response_func(responder.make_response()) return newfn return val return _api_validate @@ -146,6 +149,10 @@ def noresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw): self_method(self, *a, **kw) return self.response_func({}) +@api_validate +def json_validate(self, self_method, responder, simple_vals, param_vals, *a, **kw): + r = self_method(self, *a, **kw) + return self.response_func(r) @api_validate def validatedForm(self, self_method, responder, simple_vals, param_vals, @@ -156,16 +163,19 @@ def validatedForm(self, self_method, responder, simple_vals, param_vals, # clear out the status line as a courtesy form.set_html(".status", "") - # do the actual work - val = self_method(self, form, responder, *a, **kw) - # auto-refresh the captcha if there are errors. if (c.errors.errors and any(isinstance(v, VCaptcha) for v in simple_vals)): + form.has_errors('captcha', errors.BAD_CAPTCHA) form.new_captcha() - if val: return val - return self.response_func(dict(iter(responder))) + # do the actual work + val = self_method(self, form, responder, *a, **kw) + + if val: + return val + else: + return self.response_func(responder.make_response()) @@ -266,46 +276,41 @@ def chksrname(x): class VLength(Validator): - def __init__(self, item, length = 10000, - empty_error = errors.BAD_COMMENT, - length_error = errors.COMMENT_TOO_LONG, **kw): - Validator.__init__(self, item, **kw) - self.length = length - self.len_error = length_error - self.emp_error = empty_error + only_whitespace = re.compile(r"^\s*$", re.UNICODE) - def run(self, title): - if not title: - self.set_error(self.emp_error) - elif len(title) > self.length: - self.set_error(self.len_error) + def __init__(self, param, max_length, + empty_error = errors.NO_TEXT, + length_error = errors.TOO_LONG, + **kw): + Validator.__init__(self, param, **kw) + self.max_length = max_length + self.length_error = length_error + self.empty_error = empty_error + + def run(self, text, text2 = ''): + text = text or text2 + if self.empty_error and (not text or self.only_whitespace.match(text)): + self.set_error(self.empty_error) + elif len(text) > self.max_length: + self.set_error(self.length_error, {'max_length': self.max_length}) else: - return title + return text class VTitle(VLength): - only_whitespace = re.compile(r"^\s*$", re.UNICODE) - - def __init__(self, item, length = 300, **kw): - VLength.__init__(self, item, length = length, - empty_error = errors.NO_TITLE, - length_error = errors.TITLE_TOO_LONG, **kw) - - def run(self, title): - title = VLength.run(self, title) - if title and self.only_whitespace.match(title): - self.set_error(errors.NO_TITLE) - else: - return title + def __init__(self, param, max_length = 300, **kw): + VLength.__init__(self, param, max_length, **kw) class VComment(VLength): - def __init__(self, item, length = 10000, **kw): - VLength.__init__(self, item, length = length, **kw) + def __init__(self, param, max_length = 10000, **kw): + VLength.__init__(self, param, max_length, **kw) +class VSelfText(VLength): + def __init__(self, param, max_length = 10000, **kw): + VLength.__init__(self, param, max_length, **kw) class VMessage(VLength): - def __init__(self, item, length = 10000, **kw): - VLength.__init__(self, item, length = length, - empty_error = errors.NO_MSG_BODY, **kw) + def __init__(self, param, max_length = 10000, **kw): + VLength.__init__(self, param, max_length, **kw) class VSubredditName(VRequired): @@ -388,7 +393,7 @@ class VByNameIfAuthor(VByName): if not thing._loaded: thing._load() if c.user_is_loggedin and thing.author_id == c.user._id: return thing - return self.error(errors.NOT_AUTHOR) + return self.set_error(errors.NOT_AUTHOR) class VCaptcha(Validator): default_param = ('iden', 'captcha') @@ -466,7 +471,9 @@ class VSRSubmitPage(Validator): abort(403, "forbidden") class VSubmitParent(VByName): - def run(self, fullname): + def run(self, fullname, fullname2): + #for backwards compatability (with iphone app) + fullname = fullname or fullname2 if fullname: parent = VByName.run(self, fullname) if parent and parent._deleted: @@ -482,30 +489,34 @@ class VSubmitParent(VByName): class VSubmitSR(Validator): def run(self, sr_name): + if not sr_name: + self.set_error(errors.SUBREDDIT_REQUIRED) + return None + try: sr = Subreddit._by_name(sr_name) except (NotFound, AttributeError): self.set_error(errors.SUBREDDIT_NOEXIST) - sr = None + return None if sr and not (c.user_is_loggedin and sr.can_submit(c.user)): abort(403, "forbidden") else: return sr -pass_rx = re.compile(r".{3,20}") +pass_rx = re.compile(r"^.{3,20}$") def chkpass(x): return x if x and pass_rx.match(x) else None -class VPassword(VRequired): - def __init__(self, item, *a, **kw): - VRequired.__init__(self, item, errors.BAD_PASSWORD, *a, **kw) +class VPassword(Validator): def run(self, password, verify): if not chkpass(password): - return self.error() + self.set_error(errors.BAD_PASSWORD) + return elif verify != password: - return self.error(errors.BAD_PASSWORD_MATCH) + self.set_error(errors.BAD_PASSWORD_MATCH) + return password else: return password @@ -695,11 +706,11 @@ class VRatelimit(Validator): # when errors have associated field parameters, we'll need # to add that here if self.error == errors.RATELIMIT: - self.set_error(errors.RATELIMIT, {'time': time}) + self.set_error(errors.RATELIMIT, {'time': time}, + field = 'ratelimit') else: self.set_error(self.error) - @classmethod def ratelimit(self, rate_user = False, rate_ip = False, prefix = "rate_", seconds = None): @@ -736,16 +747,13 @@ class VCacheKey(Validator): self.key = key if key: uid = g.cache.get(str(self.cache_prefix + "_" + self.key)) - try: - a = Account._byID(uid, data = True) - if name and a.name.lower() != name.lower(): - self.set_error(errors.BAD_USERNAME) - else: - self.user = a - except NotFound: - self.set_error(errors.BAD_USERNAME) + if uid: + try: + self.user = Account._byID(uid, data = True) + except NotFound: + return + #found everything we need return self - self.set_error(errors.EXPIRED) class VOneOf(Validator): diff --git a/r2/r2/lib/emailer.py b/r2/r2/lib/emailer.py index 0779bf0b2..20792088d 100644 --- a/r2/r2/lib/emailer.py +++ b/r2/r2/lib/emailer.py @@ -51,6 +51,7 @@ def simple_email(to, fr, subj, body): def password_email(user): key = passhash(random.randint(0, 1000), user.email) passlink = 'http://' + g.domain + '/resetpassword/' + key + print passlink cache.set("reset_%s" %key, user._id, time=1800) simple_email(user.email, 'reddit@reddit.com', 'reddit.com password reset', diff --git a/r2/r2/lib/filters.py b/r2/r2/lib/filters.py index 7f3a4378e..5453486d3 100644 --- a/r2/r2/lib/filters.py +++ b/r2/r2/lib/filters.py @@ -88,13 +88,19 @@ def unsafe(text=''): def websafe_json(text=""): return c_websafe_json(_force_unicode(text)) -def websafe(text=''): +def mako_websafe(text = ''): if text.__class__ == _Unsafe: return text elif text.__class__ != unicode: text = _force_unicode(text) return c_websafe(text) +def websafe(text=''): + if text.__class__ != unicode: + text = _force_unicode(text) + #wrap the response in _Unsafe so make_websafe doesn't unescape it + return _Unsafe(c_websafe(text)) + from mako.filters import url_escape def edit_comment_filter(text = ''): try: diff --git a/r2/r2/lib/jsonresponse.py b/r2/r2/lib/jsonresponse.py index ee1bcae2b..b67ecd434 100644 --- a/r2/r2/lib/jsonresponse.py +++ b/r2/r2/lib/jsonresponse.py @@ -26,11 +26,17 @@ from r2.lib.filters import websafe_json from r2.lib.template_helpers import replace_render from r2.lib.jsontemplates import get_api_subtype from r2.lib.base import BaseController +from r2.models import IDBuilder, Listing + import simplejson -from pylons import c +from pylons import c, g def json_respond(x): - return websafe_json(simplejson.dumps(x or '')) + if g.debug: + return websafe_json(simplejson.dumps(x or '', + sort_keys=True, indent=4)) + else: + return websafe_json(simplejson.dumps(x or '')) class JsonResponse(object): """ @@ -42,14 +48,14 @@ class JsonResponse(object): self._clear() def _clear(self): - self._has_errors = set([]) + self._errors = set() self._new_captcha = False self._data = {} def send_failure(self, error): c.errors.add(error) self._clear() - self._has_errors.add(error) + self._errors.add((error, None)) def __call__(self, *a): return self @@ -57,58 +63,39 @@ class JsonResponse(object): def __getattr__(self, key): return self - def __iter__(self): + def make_response(self): res = {} if self._data: res['data'] = self._data - res['errors'] = [(e, c.errors[e].message) for e in self._has_errors] - yield ("json", res) - - def _mark_error(self, e): - pass - - def _unmark_error(self, e): - pass + res['errors'] = [(e[0], c.errors[e].message) for e in self._errors] + return {"json": res} + + def set_error(self, error_name, field_name): + self._errors.add((error_name, field_name)) def has_error(self): - return bool(self._has_errors) + return bool(self._errors) - def has_errors(self, input, *errors, **kw): - rval = False - for e in errors: - if e in c.errors: - # get list of params checked to generate this error - # if they exist, make sure they match input checked - fields = c.errors[e].fields - if not input or not fields or input in fields: - self._has_errors.add(e) - rval = True - self._mark_error(e) - else: - self._unmark_error(e) - - if rval and input: - self.focus_input(input) - return rval - - def clear_errors(self, *errors): - for e in errors: - if e in self._has_errors: - self._has_errors.remove(e) - self._unmark_error(e) + def has_errors(self, field_name, *errors, **kw): + have_error = False + for error_name in errors: + if (error_name, field_name) in c.errors: + self.set_error(error_name, field_name) + have_error = True + return have_error def _things(self, things, action, *a, **kw): """ function for inserting/replacing things in listings. """ - from r2.models import IDBuilder, Listing listing = None if isinstance(things, Listing): listing = things.listing() things = listing.things things = tup(things) if not all(isinstance(t, Wrapped) for t in things): - b = IDBuilder([t._fullname for t in things]) + wrap = kw.pop('wrap', Wrapped) + b = IDBuilder([t._fullname for t in things], wrap) things = b.get_items()[0] data = [replace_render(listing, t) for t in things] @@ -162,7 +149,9 @@ class JQueryResponse(JsonResponse): JsonResponse._clear(self) def send_failure(self, error): - JsonResponse.send_failure(self, error) + c.errors.add(error) + self._clear() + self._errors.add((self, error, None)) self.refresh() def __call__(self, *a): @@ -178,8 +167,25 @@ class JQueryResponse(JsonResponse): self.ops.append([self.objs[obj], newi, op, args]) return new - def __iter__(self): - yield ("jquery", self.ops) + def set_error(self, error_name, field_name): + #self is the form that had the error checked, but we need to + #add this error to the top_node of this response and give it a + #reference to the form. + self.top_node._errors.add((self, error_name, field_name)) + + def has_error(self): + return bool(self.top_node._errors) + + def make_response(self): + #add the error messages + for (form, error_name, field_name) in self._errors: + selector = ".error." + error_name + if field_name: + selector += ".field-" + field_name + message = c.errors[(error_name, field_name)].message + form.find(selector).show().html(message).end() + + return {"jquery": self.ops} # thing methods #-------------- @@ -196,22 +202,17 @@ class JQueryResponse(JsonResponse): # convenience methods: # -------------------- - def _mark_error(self, e): - self.find("." + e).show().html(c.errors[e].message).end() - - def _unmark_error(self, e): - self.find("." + e).html("").end() + #def _mark_error(self, e, field): + # self.find("." + e).show().html(c.errors[e].message).end() + # + #def _unmark_error(self, e): + # self.find("." + e).html("").end() def new_captcha(self): if not self._new_captcha: self.captcha(get_iden()) self._new_captcha = True - def chk_captcha(self, *errors): - if self.has_errors(None, *errors): - self.new_captcha() - return True - def get_input(self, name): return self.find("*[name=%s]" % name) diff --git a/r2/r2/lib/jsontemplates.py b/r2/r2/lib/jsontemplates.py index 4dc2ce04f..d0b564282 100644 --- a/r2/r2/lib/jsontemplates.py +++ b/r2/r2/lib/jsontemplates.py @@ -42,10 +42,6 @@ def make_typename(typ): def make_fullname(typ, _id): return '%s_%s' % (make_typename(typ), to36(_id)) -def mass_part_render(thing, **kw): - return dict([(k, spaceCompress(thing.part_render(v)).strip(' ')) \ - for k, v in kw.iteritems()]) - class JsonTemplate(Template): def __init__(self): pass @@ -253,8 +249,8 @@ class CommentJsonTemplate(ThingJsonTemplate): else: parent_id = make_fullname(Comment, parent_id) d = ThingJsonTemplate.rendered_data(self, wrapped) - d.update(mass_part_render(wrapped, contentHTML = 'commentBody', - contentTxt = 'commentText')) + d['contentText'] = self.thing_attr(wrapped, 'body') + d['contentHTML'] = self.thing_attr(wrapped, 'body_html') d['parent'] = parent_id d['link'] = make_fullname(Link, wrapped.link_id) return d @@ -268,6 +264,9 @@ class MoreCommentJsonTemplate(CommentJsonTemplate): def kind(self, wrapped): return "more" + def rendered_data(self, wrapped): + return ThingJsonTemplate.rendered_data(self, wrapped) + class MessageJsonTemplate(ThingJsonTemplate): _data_attrs_ = ThingJsonTemplate.data_attrs(new = "new", subject = "subject", diff --git a/r2/r2/lib/menus.py b/r2/r2/lib/menus.py index 27b244da0..3eca1a5d5 100644 --- a/r2/r2/lib/menus.py +++ b/r2/r2/lib/menus.py @@ -20,7 +20,7 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ -from wrapped import Wrapped +from wrapped import Wrapped, Styled from pylons import c, request, g from utils import query_string, timeago from strings import StringHandler, plurals @@ -150,30 +150,6 @@ menu = MenuHandler(hot = _('hot'), current_promos = _('promoted links'), ) -class Styled(Wrapped): - """Rather than creating a separate template for every possible - menu/button style we might want to use, this class overrides the - render function to render only the <%def> in the template whose - name matches 'style'. - - Additionally, when rendering, the '_id' and 'css_class' attributes - are intended to be used in the outermost container's id and class - tag. - """ - def __init__(self, style, _id = '', css_class = '', **kw): - self._id = _id - self.css_class = css_class - self.style = style - Wrapped.__init__(self, **kw) - - def render(self, **kw): - """Using the canonical template file, only renders the <%def> - in the template whose name is given by self.style""" - style = kw.get('style', c.render_style or 'html') - return Wrapped.part_render(self, self.style, style = style, **kw) - - - def menu_style(type): """Simple manager function for the styled menus. Returns a (style, css_class) pair given a 'type', defaulting to style = @@ -185,12 +161,11 @@ def menu_style(type): srdrop = ('dropdown', 'srdrop'), flatlist = ('flatlist', 'flat-list'), tabmenu = ('tabmenu', ''), + formtab = ('tabmenu', 'formtab'), flat_vert = ('flatlist', 'flat-vert'), ) return d.get(type, default) - - class NavMenu(Styled): """generates a navigation menu. The intention here is that the 'style' parameter sets what template/layout to use to differentiate, say, @@ -337,7 +312,7 @@ 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) + NavButton.__init__(self, title, '#', style = style, **kw) def build(self, *a, **kw): self.path = 'javascript:void(0)' @@ -391,7 +366,7 @@ class SortMenu(SimpleGetMenu): options = ('hot', 'new', 'top', 'old', 'controversial') def __init__(self, **kw): - kw['title'] = _("sort by") + kw['title'] = _("sorted by") SimpleGetMenu.__init__(self, **kw) @classmethod @@ -521,6 +496,11 @@ class SubredditMenu(NavMenu): """Always return False so the title is always displayed""" return None +class JsNavMenu(NavMenu): + def find_selected(self): + """Always return the first element.""" + return self.options[0] + # -------------------- # TODO: move to admin area class AdminReporterMenu(SortMenu): diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 92c07c58e..2d1caabcb 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -19,8 +19,11 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ -from r2.lib.wrapped import Wrapped, NoTemplateFound -from r2.models import IDBuilder, LinkListing, Account, Default, FakeSubreddit, Subreddit, Friends, All, Sub, NotFound, DomainSR +from r2.lib.wrapped import Wrapped, NoTemplateFound, Styled +from r2.models import IDBuilder, LinkListing, Account, Default +from r2.models import FakeSubreddit, Subreddit +from r2.models import Friends, All, Sub, NotFound, DomainSR +from r2.models import make_wrapper from r2.config import cache from r2.lib.jsonresponse import json_respond from r2.lib.jsontemplates import is_api @@ -32,13 +35,16 @@ from r2.lib.traffic import load_traffic, load_summary from r2.lib.captcha import get_iden from r2.lib.filters import spaceCompress, _force_unicode, _force_utf8 from r2.lib.menus import NavButton, NamedButton, NavMenu, PageNameNav, JsButton -from r2.lib.menus import SubredditButton, SubredditMenu, OffsiteButton, menu +from r2.lib.menus import SubredditButton, SubredditMenu +from r2.lib.menus import OffsiteButton, menu, JsNavMenu from r2.lib.strings import plurals, rand_strings, strings, Score from r2.lib.utils import title_to_url, query_string, UrlParser, to_js, vote_hash from r2.lib.template_helpers import add_sr, get_domain +from r2.lib.subreddit_search import popular_searches -import sys, random, datetime, locale, calendar, re +import sys, random, datetime, locale, calendar, simplejson, re import graph +from itertools import chain from urllib import quote datefmt = _force_utf8(_('%d %b %Y')) @@ -274,7 +280,6 @@ class Reddit(Wrapped): if c.user_is_loggedin: more_buttons.append(NamedButton('saved', False)) - more_buttons.append(NamedButton('recommended', False)) if c.user_is_admin: more_buttons.append(NamedButton('admin')) @@ -287,6 +292,11 @@ class Reddit(Wrapped): if c.user_is_admin: more_buttons.append(NamedButton('traffic')) + #if there's only one button in the dropdown, get rid of the dropdown + if len(more_buttons) == 1: + main_buttons.append(more_buttons[0]) + more_buttons = [] + toolbar = [NavMenu(main_buttons, type='tabmenu')] if more_buttons: toolbar.append(NavMenu(more_buttons, title=menu.more, type='tabdrop')) @@ -300,13 +310,13 @@ class Reddit(Wrapped): return "" @staticmethod - def content_stack(*a): + def content_stack(panes, css_class = None): """Helper method for reordering the content stack.""" - return PaneStack(filter(None, a)) + return PaneStack(filter(None, panes), css_class = css_class) def content(self): """returns a Wrapped (or renderable) item for the main content div.""" - return self.content_stack(self.infobar, self.nav_menu, self._content) + return self.content_stack((self.infobar, self.nav_menu, self._content)) class ClickGadget(Wrapped): def __init__(self, links, *a, **kw): @@ -417,11 +427,15 @@ class MessagePage(Reddit): if not kw.has_key('show_sidebar'): kw['show_sidebar'] = False Reddit.__init__(self, *a, **kw) - self.replybox = CommentReplyBox() + self.replybox = UserText(item = None, creating = True, + post_form = 'comment', display = False, + cloneable = True) def content(self): - return self.content_stack(self.replybox, self.infobar, - self.nav_menu, self._content) + return self.content_stack((self.replybox, + self.infobar, + self.nav_menu, + self._content)) def build_toolbars(self): buttons = [NamedButton('compose'), @@ -486,8 +500,11 @@ class LoginPage(BoringPage): class Login(Wrapped): """The two-unit login and register form.""" def __init__(self, user_reg = '', user_login = '', dest=''): - Wrapped.__init__(self, user_reg = user_reg, user_login = user_login, - dest = dest) + Wrapped.__init__(self, + user_reg = user_reg, + user_login = user_login, + dest = dest, + captcha = Captcha()) class SearchPage(BoringPage): @@ -501,8 +518,8 @@ class SearchPage(BoringPage): BoringPage.__init__(self, pagename, robots='noindex', *a, **kw) def content(self): - return self.content_stack(self.searchbar, self.infobar, - self.nav_menu, self._content) + return self.content_stack((self.searchbar, self.infobar, + self.nav_menu, self._content)) class CommentsPanel(Wrapped): """the side-panel on the reddit toolbar frame that shows the top @@ -530,10 +547,11 @@ class LinkInfoPage(Reddit): def __init__(self, link = None, comment = None, link_title = '', *a, **kw): - # TODO: temp hack until we find place for builder_wrapper from r2.controllers.listingcontroller import ListingController + wrapper = make_wrapper(ListingController.builder_wrapper, + expand_children = True) link_builder = IDBuilder(link._fullname, - wrap = ListingController.builder_wrapper) + wrap = wrapper) # link_listing will be the one-element listing at the top self.link_listing = LinkListing(link_builder, nextprev=False).listing() @@ -553,6 +571,7 @@ class LinkInfoPage(Reddit): else: params = {'title':_force_unicode(link_title), 'site' : c.site.name} title = strings.link_info_title % params + Reddit.__init__(self, title = title, *a, **kw) def build_toolbars(self): @@ -579,8 +598,11 @@ class LinkInfoPage(Reddit): return toolbar def content(self): - return self.content_stack(self.infobar, self.link_listing, - self.nav_menu, self._content) + return self.content_stack((self.infobar, self.link_listing, + PaneStack([PaneStack((self.nav_menu, + self._content))], + title = _("comments"), + css_class = "commentarea"))) def rightbox(self): rb = Reddit.rightbox(self) @@ -612,8 +634,6 @@ class EditReddit(Reddit): else: return [] - - class SubredditsPage(Reddit): """container for rendering a list of reddits. The corner searchbox is hidden and its functionality subsumed by an in page @@ -651,8 +671,8 @@ class SubredditsPage(Reddit): NavMenu(buttons, base_path = '/reddits', type="tabmenu")] def content(self): - return self.content_stack(self.searchbar, self.nav_menu, - self.sr_infobar, self._content) + return self.content_stack((self.searchbar, self.nav_menu, + self.sr_infobar, self._content)) def rightbox(self): ps = Reddit.rightbox(self) @@ -663,7 +683,7 @@ class MySubredditsPage(SubredditsPage): """Same functionality as SubredditsPage, without the search box.""" def content(self): - return self.content_stack(self.nav_menu, self.infobar, self._content) + return self.content_stack((self.nav_menu, self.infobar, self._content)) def votes_visible(user): @@ -868,15 +888,6 @@ class Captcha(Wrapped): self.iden = get_captcha() Wrapped.__init__(self) -class CommentReplyBox(Wrapped): - """Used on LinkInfoPage to render the comment reply form at the - top of the comment listing as well as the template for the forms - which are JS inserted when clicking on 'reply' in either a comment - or message listing.""" - def __init__(self, link_name='', captcha=None, action = 'comment'): - Wrapped.__init__(self, link_name = link_name, captcha = captcha, - action = action) - class PermalinkMessage(Wrapped): """renders the box on comment pages that state 'you are viewing a single comment's thread'""" @@ -886,12 +897,14 @@ class PermalinkMessage(Wrapped): class PaneStack(Wrapped): """Utility class for storing and rendering a list of block elements.""" - def __init__(self, panes=[], div_id = None, css_class=None, div=False): + def __init__(self, panes=[], div_id = None, css_class=None, div=False, + title=""): div = div or div_id or css_class or False self.div_id = div_id self.css_class = css_class self.div = div self.stack = list(panes) + self.title = title Wrapped.__init__(self) def append(self, item): @@ -952,7 +965,6 @@ class Frame(Wrapped): dorks_re = re.compile(r"https?://?([-\w.]*\.)?digg\.com/\w+\.\w+(/|$)") class FrameToolbar(Wrapped): """The reddit voting toolbar used together with Frame.""" - extension_handling = False def __init__(self, link = None, title = None, url = None, expanded = False, **kw): self.title = title self.url = url @@ -998,11 +1010,41 @@ class FrameToolbar(Wrapped): Wrapped.__init__(self, **kw) + extension_handling = False class NewLink(Wrapped): """Render the link submission form""" def __init__(self, captcha = None, url = '', title= '', subreddits = (), then = 'comments'): + tabs = (('link', ('link-desc', 'url-field')), + ('text', ('text-desc', 'text-field'))) + all_fields = set(chain(*(parts for (tab, parts) in tabs))) + + buttons = [] + self.default_tabs = tabs[0][1] + self.default_tab = tabs[0][0] + for tab_name, parts in tabs: + to_show = ','.join('#' + p for p in parts) + to_hide = ','.join('#' + p for p in all_fields if p not in parts) + onclick = "return select_form_tab(this, '%s', '%s');" + onclick = onclick % (to_show, to_hide) + + if tab_name == self.default_tab: + self.default_show = to_show + self.default_hide = to_hide + + buttons.append(JsButton(tab_name, onclick=onclick, css_class=tab_name)) + + self.formtabs_menu = JsNavMenu(buttons, type = 'formtab') + self.default_tabs = tabs[0][1] + + self.sr_searches = simplejson.dumps(popular_searches()) + + if isinstance(c.site, FakeSubreddit): + self.default_sr = subreddits[0] if subreddits else g.default_sr + else: + self.default_sr = c.site.name + Wrapped.__init__(self, captcha = captcha, url = url, title = title, subreddits = subreddits, then = then) @@ -1082,11 +1124,22 @@ class ButtonDemoPanel(Wrapped): class Feedback(Wrapped): """The feedback and ad inquery form(s)""" - def __init__(self, captcha=None, title=None, action='/feedback', - message='', name='', email='', replyto='', success = False): - Wrapped.__init__(self, captcha = captcha, title = title, action = action, - message = message, name = name, email = email, replyto = replyto, - success = success) + def __init__(self, title, action): + email = name = '' + if c.user_is_loggedin: + email = getattr(c.user, "email", "") + name = c.user.name + + captcha = None + if not c.user_is_loggedin or c.user.needs_captcha(): + captcha = Captcha() + + Wrapped.__init__(self, + captcha = captcha, + title = title, + action = action, + email = email, + name = name) class WidgetDemoPanel(Wrapped): @@ -1260,7 +1313,7 @@ class DetailsPage(LinkInfoPage): def content(self): # TODO: a better way? from admin_pages import Details - return self.content_stack(self.link_listing, Details(link = self.link)) + return self.content_stack((self.link_listing, Details(link = self.link))) class Cnameframe(Wrapped): """The frame page.""" @@ -1289,8 +1342,7 @@ class PromotePage(Reddit): buttons = [NamedButton('current_promos', dest = ''), NamedButton('new_promo')] - menu = NavMenu(buttons, title='show', base_path = '/promote', - type='flatlist') + menu = NavMenu(buttons, base_path = '/promote', type='flatlist') if nav_menus: nav_menus.insert(0, menu) @@ -1330,6 +1382,83 @@ class PromoteLinkForm(Wrapped): listing = listing, *a, **kw) +class TabbedPane(Wrapped): + def __init__(self, tabs): + """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)) + + self.tabmenu = JsNavMenu(buttons, type = 'tabpane') + self.tabs = tabs + + Wrapped.__init__(self) + +class LinkChild(Wrapped): + def __init__(self, link, load = False, expand = False, nofollow = False): + self.link = link + self.expand = expand + self.load = load or expand + self.nofollow = nofollow + Wrapped.__init__(self) + + def content(self): + return '' + +class MediaChild(LinkChild): + css_style = "video" + def content(self): + return self.link.media_object + +class SelfTextChild(LinkChild): + css_style = "selftext" + def content(self): + u = UserText(self.link, self.link.selftext, + editable = c.user == self.link.author, + nofollow = self.nofollow) + #have to force the render style to html for now cause of some + #c.render_style weirdness + return u.render(style = 'html') + +class SelfText(Wrapped): + def __init__(self, link): + Wrapped.__init__(self, link = link) + +class UserText(Wrapped): + def __init__(self, + item, + text = '', + have_form = True, + editable = False, + creating = False, + nofollow = False, + display = True, + post_form = 'editusertext', + cloneable = False, + extra_css = ''): + + css_class = "usertext" + if cloneable: + css_class += " cloneable" + if extra_css: + css_class += " " + extra_css + + Wrapped.__init__(self, + item = item, + text = text, + have_form = have_form, + editable = editable, + creating = creating, + nofollow = nofollow, + display = display, + post_form = post_form, + cloneable = cloneable, + css_class = css_class) + + def button(self): + pass + class Traffic(Wrapped): @staticmethod def slice_traffic(traffic, *indices): @@ -1491,3 +1620,4 @@ class RedditTraffic(Traffic): class InnerToolbarFrame(Wrapped): def __init__(self, link, expanded = False): Wrapped.__init__(self, link = link, expanded = expanded) + diff --git a/r2/r2/lib/scraper.py b/r2/r2/lib/scraper.py index ab5cf556c..a2ddd1a23 100644 --- a/r2/r2/lib/scraper.py +++ b/r2/r2/lib/scraper.py @@ -281,7 +281,7 @@ def make_scraper(url): #Youtube class YoutubeScraper(MediaScraper): - media_template = '' + media_template = '' thumbnail_template = 'http://img.youtube.com/vi/$video_id/default.jpg' video_id_rx = re.compile('.*v=([A-Za-z0-9-_]+).*') diff --git a/r2/r2/lib/strings.py b/r2/r2/lib/strings.py index 79a90adb0..dcf7f8795 100644 --- a/r2/r2/lib/strings.py +++ b/r2/r2/lib/strings.py @@ -121,6 +121,9 @@ string_dict = dict( also get there by clicking the link's title (in the middle of the toolbar, to the right of the comments button). """), + + submit_link = _("""You are submitting a link. The key to a successful submission is interesting content and a deceptive title."""), + submit_text = _("""You are submitting a text-based post. Speak your mind. A title is required, but expanding further in the text field is not. Beginning your title with "vote up if" is violation of intergalactic law."""), ) class StringHandler(object): diff --git a/r2/r2/lib/subreddit_search.py b/r2/r2/lib/subreddit_search.py new file mode 100644 index 000000000..d70b2f742 --- /dev/null +++ b/r2/r2/lib/subreddit_search.py @@ -0,0 +1,46 @@ +from r2.models import * +from r2.lib import utils + +from pylons import g + +sr_prefix = 'sr_search_' + + +def load_all_reddits(): + query_cache = {} + + q = Subreddit._query(Subreddit.c.type == 'public', + Subreddit.c._downs > 1, + sort = (desc('_downs'), desc('_ups')), + data = True) + for sr in utils.fetch_things2(q): + name = sr.name.lower() + for i in xrange(len(name)): + prefix = name[:i + 1] + names = query_cache.setdefault(prefix, []) + if len(names) < 10: + names.append(sr.name) + + g.rendercache.set_multi(query_cache, prefix = sr_prefix) + +def search_reddits_cached(query): + return g.rendercache.get(sr_prefix + query) or [] + +def search_reddits(query): + return search_reddits_cached(str(query.lower())) + +@memoize('popular_searches', time = 3600) +def popular_searches(): + top_reddits = Subreddit._query(Subreddit.c.type == 'public', + sort = desc('_downs'), + limit = 100, + data = True) + top_searches = {} + for sr in top_reddits: + name = sr.name.lower() + for i in xrange(min(len(name), 3)): + query = name[:i + 1] + r = search_reddits(query) + top_searches[query] = r + return top_searches + diff --git a/r2/r2/lib/template_helpers.py b/r2/r2/lib/template_helpers.py index e7ebb7981..8c92cb184 100644 --- a/r2/r2/lib/template_helpers.py +++ b/r2/r2/lib/template_helpers.py @@ -20,6 +20,7 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from r2.models import * +from r2.lib.jsontemplates import is_api from filters import unsafe, websafe from r2.lib.utils import vote_hash, UrlParser @@ -102,8 +103,11 @@ def replace_render(listing, item, style = None, display = True): pass return rendered_item - child_txt = ( hasattr(item, "child") and item.child )\ - and item.child.render(style = style) or "" + if is_api(): + child_txt = "" + else: + child_txt = ( hasattr(item, "child") and item.child )\ + and item.child.render(style = style) or "" # handle API calls differently from normal request: dicts not strings are passed around if isinstance(rendered_item, dict): @@ -239,7 +243,7 @@ def add_sr(path, sr_path = True, nocname=False, force_hostname = False): path to include c.site.path. """ # don't do anything if it is just an anchor - if path.startswith('#'): + if path.startswith('#') or path.startswith('javascript:'): return path u = UrlParser(path) diff --git a/r2/r2/lib/wrapped.py b/r2/r2/lib/wrapped.py index a56003747..5191fbebe 100644 --- a/r2/r2/lib/wrapped.py +++ b/r2/r2/lib/wrapped.py @@ -24,6 +24,7 @@ from utils import storage from itertools import chain import sys + sys.setrecursionlimit(500) class NoTemplateFound(Exception): pass @@ -111,3 +112,26 @@ def SimpleWrapped(**kw): kw.update(kw1) Wrapped.__init__(self, *a, **kw) return _SimpleWrapped + +class Styled(Wrapped): + """Rather than creating a separate template for every possible + menu/button style we might want to use, this class overrides the + render function to render only the <%def> in the template whose + name matches 'style'. + + Additionally, when rendering, the '_id' and 'css_class' attributes + are intended to be used in the outermost container's id and class + tag. + """ + def __init__(self, style, _id = '', css_class = '', **kw): + self._id = _id + self.css_class = css_class + self.style = style + Wrapped.__init__(self, **kw) + + def render(self, **kw): + """Using the canonical template file, only renders the <%def> + in the template whose name is given by self.style""" + from pylons import c + style = kw.get('style', c.render_style or 'html') + return Wrapped.part_render(self, self.style, style = style, **kw) diff --git a/r2/r2/models/builder.py b/r2/r2/models/builder.py index 9bf518a09..2b9d51754 100644 --- a/r2/r2/models/builder.py +++ b/r2/r2/models/builder.py @@ -596,6 +596,14 @@ class CommentBuilder(Builder): return final +def make_wrapper(parent_wrapper = Wrapped, **params): + def wrapper_fn(thing): + w = parent_wrapper(thing) + for k, v in params.iteritems(): + setattr(w, k, v) + return w + return wrapper_fn + class TopCommentBuilder(CommentBuilder): """A comment builder to fetch only the top-level, non-spam, non-deleted comments""" diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index 67f510f7f..c0e33375e 100644 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -52,6 +52,7 @@ class Link(Thing, Printable): promote_until = None, promoted_by = None, disable_comments = False, + selftext = '', ip = '0.0.0.0') def __init__(self, *a, **kw): @@ -204,6 +205,7 @@ class Link(Thing, Printable): if c.user_is_admin: return False + link_child = wrapped.link_child s = (str(i) for i in (wrapped._fullname, bool(c.user_is_sponsor), bool(c.user_is_loggedin), @@ -224,7 +226,12 @@ class Link(Thing, Printable): wrapped.show_reports, wrapped.can_ban, wrapped.thumbnail, - wrapped.moderator_banned)) + wrapped.moderator_banned, + #link child stuff + bool(link_child), + bool(link_child) and link_child.load, + bool(link_child) and link_child.expand + )) # htmllite depends on other get params s = ''.join(s) if c.render_style == "htmllite": @@ -284,7 +291,8 @@ class Link(Thing, Printable): item.thumbnail = thumbnail_url(item) else: item.thumbnail = g.default_thumb - + + item.score = max(0, item.score) item.domain = (domain(item.url) if not item.is_self @@ -329,6 +337,20 @@ class Link(Thing, Printable): item.domain_path = "/domain/%s" % item.domain if item.is_self: item.domain_path = item.subreddit_path + + #this is wrong, but won't be so wrong when we move this + #whole chunk of code into pages.py + from r2.lib.pages import MediaChild, SelfTextChild + item.link_child = None + item.editable = False + if item.media_object: + item.link_child = MediaChild(item, load = True) + elif item.selftext: + expand = getattr(item, 'expand_children', False) + item.link_child = SelfTextChild(item, expand = expand, + nofollow = item.nofollow) + #draw the edit button if the contents are pre-expanded + item.editable = expand and item.author == c.user item.tblink = "http://%s/tb/%s" % ( get_domain(cname = c.cname, subreddit=False), @@ -549,9 +571,18 @@ class Comment(Thing, Printable): item.author != c.user and not item.show_spam))) - if item._deleted and not c.user_is_admin: - item.author = DeletedUser() - item.body = '[deleted]' + extra_css = '' + if item._deleted: + if c.user_is_admin: + extra_css += "grayed" + else: + item.author = DeletedUser() + item.body = '[deleted]' + + + if c.focal_comment == item._id36: + extra_css += 'border' + # don't collapse for admins, on profile pages, or if deleted item.collapsed = ((item.score < min_score) and @@ -570,6 +601,12 @@ class Comment(Thing, Printable): item.score_fmt = Score.points item.permalink = item.make_permalink(item.link, item.subreddit) + #will seem less horrible when add_props is in pages.py + from r2.lib.pages import UserText + item.usertext = UserText(item, item.body, + editable = item.author == c.user, + nofollow = item.nofollow, + extra_css = extra_css) class StarkComment(Comment): """Render class for the comments in the top-comments display in the reddit toolbar""" diff --git a/r2/r2/public/static/blog-collapsed-hover.png b/r2/r2/public/static/blog-collapsed-hover.png new file mode 100644 index 000000000..57239808a Binary files /dev/null and b/r2/r2/public/static/blog-collapsed-hover.png differ diff --git a/r2/r2/public/static/blog-collapsed.png b/r2/r2/public/static/blog-collapsed.png new file mode 100644 index 000000000..cefb6bb81 Binary files /dev/null and b/r2/r2/public/static/blog-collapsed.png differ diff --git a/r2/r2/public/static/blog-expanded-hover.png b/r2/r2/public/static/blog-expanded-hover.png new file mode 100644 index 000000000..a0524faab Binary files /dev/null and b/r2/r2/public/static/blog-expanded-hover.png differ diff --git a/r2/r2/public/static/blog-expanded.png b/r2/r2/public/static/blog-expanded.png new file mode 100644 index 000000000..2db0f6fe4 Binary files /dev/null and b/r2/r2/public/static/blog-expanded.png differ diff --git a/r2/r2/public/static/css/reddit.css b/r2/r2/public/static/css/reddit.css index 9bbf50afd..1f6a103d2 100644 --- a/r2/r2/public/static/css/reddit.css +++ b/r2/r2/public/static/css/reddit.css @@ -23,6 +23,10 @@ body { z-index: 1; } +textarea { font: normal small verdana, arial, helvetica, sans-serif; } + +/*html,body { height: 100%; }*/ + /* IE dumbness patch. hidden input in a hidden block that is * subsequently shown leads to the input to "show" and generate undesired * )padding. This makes it go away. */ @@ -69,7 +73,6 @@ input[type=checkbox], input[type=radio] { margin-top: .4em; } /* forms */ -.iform th { text-align: right; color: black; font-weight: normal; text-transform: lowercase; } .wrong {color: red; font-weight: normal} .subform input.text { width: 25em } @@ -112,13 +115,15 @@ ul.flat-vert {text-align: left;} .pref { font-weight: bold; } #header { + border-bottom: 1px solid #5f99cf; + position: relative; background-color: #cee3f8; z-index: 99; } -#header-img {margin-top: 2px;} +#header-img {margin-top: 2px; margin-right: 5px;} #header-top { position: absolute; @@ -147,6 +152,7 @@ ul.flat-vert {text-align: left;} font-weight: bold; margin-right: 1ex; font-variant: small-caps; + line-height: 100%; font-size: 1.2em; } .pagename a {color: black; } @@ -234,19 +240,20 @@ ul.flat-vert {text-align: left;} white-space: nowrap; display: inline; } + .tabmenu li { display: inline; font-weight: bold; margin: 0px 3px; - padding: 2px 6px 0px 6px; - background-color: #eff7ff; } -.tabmenu li.selected { padding: 0; } +.tabmenu li a { + padding: 2px 6px 0 6px; + background-color: #eff7ff; + } .tabmenu li.selected a{ color: orangered; - padding: 2px 6px 1px 6px; background-color: white; border: 1px solid #5f99cf; border-bottom: 1px solid white; @@ -458,7 +465,7 @@ before enabling */ .midcol { float: left; - margin-right: 2px; + margin-right: 4px; margin-left: 7px; background: transparent; overflow: hidden; @@ -609,6 +616,33 @@ before enabling */ .organic-help-button { padding: 0 .5ex; } +.menuarea { + border-bottom: 1px dotted gray; + padding: 5px 10px; + margin: 5px 310px 5px 5px; + font-size: larger; +} + +.menuarea .spacer {display: inline; margin-right: 15px} + +.commentarea h1 { + margin: 10px 310px 0px 10px; + padding-bottom: 3px; + border-bottom: 1px dotted gray; +} + +.commentarea .menuarea { + border: none; + margin: 0 310px 10px 10px; + padding: 0; + color: gray; +} + +.commentarea > .usertext { + margin: 0 0 10px 10px; + overflow: auto; +} + .infobar { background-color: #f6e69f; padding: 5px 10px; @@ -642,16 +676,6 @@ before enabling */ margin-right: 15px; } -.menuarea { - border-bottom: 1px dotted gray; - padding: 5px 10px; - margin: 5px 310px 5px 5px; - font-size: larger; -} - -.menuarea .spacer {display: inline; margin-right: 15px} - - /*top link*/ a.star { text-decoration: none; color: #ff8b60 } @@ -662,7 +686,8 @@ a.star { text-decoration: none; color: #ff8b60 } .entry .buttons li { display: inline; padding: 0 4px; - border: none;} + border: none; +} .entry .buttons li.first {padding-left: 0px;} @@ -753,6 +778,7 @@ a.star { text-decoration: none; color: #ff8b60 } .linkcompressed .midcol { width: 15px; } .linkcompressed .entry .buttons li a:hover { text-decoration: underline} +.linkcompressed .expando-button {display: none} .warm-entry .rank { color: #EDA179; } @@ -822,57 +848,8 @@ a.star { text-decoration: none; color: #ff8b60 } margin-left: 15px; } -.commentreply { - margin: 10px; - margin-left: 15px; - width: 40em; -} - textarea.gray { color: gray; } -.commentreply textarea { - border: 1px solid #369; - width: 100%; - margin: 0px; - margin-bottom: -1px; -} -.commentreply .buttons { - margin: 0px; - float: left; -} -.commentreply .buttons button { - margin: 5px; -} - -.commentreply table.help { - margin: 0px; - margin-top: 5px; - font-size: larger; - width: 100%; -} - -.commentreply .help, -.commentreply .help td, -.commentreply .help tr { - border: 1px solid #C0C0C0; - padding: 4px; - margin: 0px; -} -.commentreply .help-toggle { - float:right; - margin-top: 7px; -} - -.permamessage { - font-size: larger; - border: 1px dotted black; - padding: 5px 5px 5px 18px; - white-space: nowrap; - background-image: url(/static/permalink-arrow.png); - background-repeat: no-repeat; - background-position: 5px center; -} - .deepthread { padding-right: 30px; background-image: url(/static/continue-thread.png); @@ -916,8 +893,6 @@ textarea.gray { color: gray; } margin-right: 2px; } -.commentbody { } - .commentbody.border { background-color: #ffc; padding-left: 5px} .commentbody.grayed { color: gray; @@ -1108,7 +1083,7 @@ textarea.gray { color: gray; } } -.status { color: red; } +.status { margin-left: 5px; color: red; font-size: small;} .error { color: red; font-size: small; margin: 5px; } .line-through { text-decoration: line-through } @@ -1220,6 +1195,9 @@ textarea.gray { color: gray; } width: 250px; } +#passform h1 {margin-bottom: 0px} +#passform p {margin-bottom: 5px; font-size: small} + /* cover */ .cover { position: fixed; @@ -1632,6 +1610,16 @@ textarea.gray { color: gray; } } /* default form styles */ + + +form .spacer + .spacer { + margin: 15px 0; +} + +form input[type=checkbox], +form input[type=radio] {margin: 2px .5em 0 0; } + + .pretty-form { font-size: larger; vertical-align: top; @@ -1639,7 +1627,7 @@ textarea.gray { color: gray; } .pretty-form p {margin: 3px ;} .pretty-form input[type=checkbox], -.pretty-form input[type=radio] {margin: 2px .5em 0px .5em; } +.pretty-form input[type=radio] {margin: 2px .5em 0 0; } .pretty-form img { margin: 3px .5em} .pretty-form input[type=text], .pretty-form textarea, @@ -1660,29 +1648,24 @@ textarea.gray { color: gray; } .pretty-form select, .pretty-form b, .pretty-form textarea, -.pretty-form button { margin: 3px .5em; } +.pretty-form button { margin: 3px 5px; } .pretty-form th { text-align: right } +/* delete page */ +.delete-field { + background-color: white; + padding: 10px; +} + /*pref page boxes*/ .pretty-form.short-text input[type=text], .pretty-form.short-text textarea, .pretty-form.short-text input[type=password] {width: 2em } -/*update password*/ -.pretty-form.medium-text input[type=text], -.pretty-form.medium-text textarea, -.pretty-form.medium-text input[type=password] {width: 15em } - /*submit*/ -.pretty-form.long-text input[type=text], -.pretty-form.long-text textarea, -.pretty-form.long-text input[type=password] {padding: 2px; width: 40em } - -/*forgot password*/ -#passform h1 { margin: 0px; } -#passform p { font-size: smaller; color: orangered; margin-bottom: 7px} -#passform.pretty-form button { padding: 0px 1px; } +#url-field button {margin: 10px 5px 0 0;} +#url-field .title-status { color: red; font-size: small} /*opt-out/in form*/ .opt-form { font-size: larger } @@ -1815,8 +1798,8 @@ ul#image-preview-list .description pre { #preview-table > table > tbody > tr { padding-bottom: 10px; } #preview-table > table > tbody > tr > td { padding: 5px; padding-right: 15px;} -#preview-table > table > tbody > tr > th { padding: 5px; padding-right: 15px;} #preview-table > table > tbody > tr > th { + padding: 5px; padding-right: 15px; font-weight: bold; vertical-align: top; font-size: larger; @@ -1984,6 +1967,293 @@ ul#image-preview-list .description pre { .toggle.deltranslator-button { display: inline; } +/****************/ + +#sr { margin-left: 0px } + +#sr-list-wrapper { + width: 454px; + height: 200px; + border: 1px solid gray; + border-top: none; + margin: 0 5px; + font-size: smaller; + position: relative; +} + +#sr-list-cover { + position: absolute; + background: gray url(/static/throbber.gif) no-repeat scroll center center; + height: 100%; + width: 100%; + opacity: .7; + filter:alpha(opacity=70); /* IE patch */ + z-index: 1000; + display: none; +} + +#sr-list { + overflow: auto; + position: absolute; + height: 100%; + width: 100%; +} + + +#sr-searchfield { margin: 0 5px; } + +.sr-name { + font-size: small; + vertical-align: top; + padding: 3px 3px 3px 0; +} + +.sr-description { + padding: 3px + } + +.sr-row { + cursor: default; + } + +.sr-row.sr-selected { + background: #EFF7FF url(/static/rightarrow.png) no-repeat scroll 0px 5px; +} + +.sr-arrow { + width: 10px; + height: 12px; + } + +#sr-autocomplete-area { + position: relative; + z-index: 100; + } + +#sr-drop-down { + position: absolute; + width: 498px; + border: 1px solid gray; + background: white; + display: none; +} + +#sr-drop-down table { + width: 100%; +} + +.sr-name-row { + cursor: default; + } + +.sr-name-row.sr-selected { + background-color: #369; + color: white; +} + +.submit-header { + font-size: larger; + font-weight: bold; +} + +#suggested-reddits { + margin-top: 5px; + font-size: small; +} + +#suggested-reddits ul { + +} + +#suggested-reddits li { + display: inline; + padding-right: 5px; +} + + +/*** new menu shit ***/ + +.formtabs-content { + width: 520px; + border-top: 4px solid #5f99cf; + padding-top: 10px; + } + +.formtabs-content .infobar { + margin: 0; + padding: 5px; + } + +ul.tabmenu.formtab { + display: block; + padding-left: 10px; + font-size: larger; +} + +.tabmenu.formtab li { + margin: 0; + } + +.tabmenu.formtab a { + font-weight: normal; + outline: none; + padding: 0px 12px; + vertical-align: bottom; + + border: 1px solid #c1c1c1; + border-bottom: none; +} + +.tabmenu.formtab .selected a { + color:white; + font-size: 130%; + background-color: #5f99cf; + border: none; +} + + +/******* embed stuff ******/ +.expando { + clear: left; + margin: 5px 0 5px 0; +} + +.expando-content { + display: none; +} + + +.expando-button { + float: left; + height: 23px; + width: 23px; + margin: 2px 5px 2px 0; + background: white none no-repeat scroll center center; +} + +.expando-button.selftext.collapsed {background-image: url(/static/blog-collapsed.png);} +.expando-button.selftext.collapsed:hover, .eb-sch {background-image: url(/static/blog-collapsed-hover.png);} +.expando-button.selftext.expanded, .eb-se { + margin-bottom: 5px; + background-image: url(/static/blog-expanded.png); +} +.expando-button.selftext.expanded:hover, .eb-seh {background-image: url(/static/blog-expanded-hover.png);} + +.expando-button.video.collapsed {background-image: url(/static/vid-collapsed.png);} + +.expando-button.video.collapsed:hover, .eb-vch {background-image: url(/static/vid-collapsed-hover.png);} +.expando-button.video.expanded, .eb-ve {background-image: url(/static/vid-expanded.png);} +.expando-button.video.expanded:hover, .eb-veh {background-image: url(/static/vid-expanded-hover.png);} + + +/******** self text stuff ****/ +.link .usertext .md, +.linkcompressed .usertext .md { + padding: 0 5px; + background-color: #fafafa; + border: 1px solid #369; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; +} + +.usertext { + font-size: small; + position: relative; +} + +.usertext-edit { + margin-top: 5px; + padding: 0 1px; /* so the border of help/textbox don't get chopped off */ + width: 500px; +} + +.usertext-edit textarea { + width: 500px; + height: 100px; +} + +/*permalinks*/ +.usertext.border .usertext-body { + background-color: #ffc; padding-left: 5px; +} + +/*admin see deleted comment*/ +.usertext.grayed .usertext-body { + color: gray; + background-color: #e0e0e0; + padding-left: 5px; +} + +.usertext button { + margin: 5px 5px 10px 0; +} + +.usertext .help-toggle { + font-size: smaller; + float:right; + margin-top: 5px; +} + +.usertext .bottom-area { + /* this restricts children floats to the container */ + overflow: hidden; + width: 100%; +} + +.usertext table.markhelp { + background-color: white; + margin: 5px 0px; + width: 100%; +} + +.usertext .markhelp, +.usertext .markhelp td, +.usertext .markhelp tr { + border: 1px solid #C0C0C0; + padding: 4px; + margin: 0px; +} + +.usertext .markhelp .spaces {background-color: #c0c0c0} + +/*** roundfield stuff *******/ +.roundfield { + width: 500px; + background-color: #cee3f8; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + padding: 5px 10px 10px 10px; + font-size: large; +} + +.roundfield .title { +} + +.roundfield .roundfield-content { + margin-top: 5px; + border: none; + vertical-align: top; +} + +.roundfield .usertext-edit { + width: 500px; +} + +.roundfield textarea, +.roundfield input[type=text], +.roundfield input[type=password] { + font-size: 100%; + width: 492px; + padding: 3px; + margin: 0; + border: 1px solid gray; +} + +.roundfield.captcha .capimage { + margin-bottom: 10px; +} + +/***traffic stuff***/ .traffic-table {margin: 10px 20px; } .traffic-table a:hover { text-decoration: underline; } .traffic-table th { font-weight: bold; text-align: center;} @@ -2005,3 +2275,6 @@ ul#image-preview-list .description pre { border: 1px solid #B0B0B0; margin-left: 10px; margin-bottom: 10px; } + + + diff --git a/r2/r2/public/static/js/jquery.reddit.js b/r2/r2/public/static/js/jquery.reddit.js index eb1d08c5f..bd83656a9 100644 --- a/r2/r2/public/static/js/jquery.reddit.js +++ b/r2/r2/public/static/js/jquery.reddit.js @@ -125,8 +125,9 @@ function handleResponse(action) { objs[new_i] = objs[old_i][args]; if(objs[new_i]) objs[new_i]._obj = objs[old_i]; - else + else { $.debug("unrecognized"); + } } else { $.debug("unrecognized"); @@ -137,7 +138,7 @@ function handleResponse(action) { }; var api_loc = '/api/'; -$.request = function(op, parameters, worker_in, block, get_only) { +$.request = function(op, parameters, worker_in, block, type, get_only) { /* Uniquitous reddit AJAX poster. Automatically addes handleResponse(action) worker to deal with the API result. The @@ -153,6 +154,7 @@ $.request = function(op, parameters, worker_in, block, get_only) { parameters = $.with_default(parameters, {}); worker_in = $.with_default(worker_in, handleResponse(action)); + type = $.with_default(type, "json"); if (typeof(worker_in) != 'function') worker_in = handleResponse(action); var worker = function(r) { @@ -178,10 +180,11 @@ $.request = function(op, parameters, worker_in, block, get_only) { op = api_loc + op; /*if( document.location.host == reddit.ajax_domain ) /* normal AJAX post */ + if(get_only) { - $.get(op, parameters, worker, "json"); + $.get(op, parameters, worker, type); } else { - $.post(op, parameters, worker, "json"); + $.post(op, parameters, worker, type); } /*else { /* cross domain it is... * / op = "http://" + reddit.ajax_domain + op + "?callback=?"; @@ -359,7 +362,7 @@ $.fn.randomize_ids = function() { return $(this); } -$.replace_things = function(things, keep_children, reveal, stubs) { +$.fn.replace_things = function(things, keep_children, reveal, stubs) { /* Given the api-html structured things, insert them into the DOM * in such a way as to remove any elements with the same thing_id. * "keep_children" is a boolean to determine whether or not any @@ -369,9 +372,10 @@ $.replace_things = function(things, keep_children, reveal, stubs) { * animate the transition from old to new. */ var midcol = $(".midcol:visible:first").css("width"); var numcol = $(".rank:visible:first").css("width"); + var self = this; return $.map(things, function(thing) { var data = thing.data; - var existing = $.things(data.id); + var existing = $(self).things(data.id); if(stubs) existing = existing.filter(".stub"); existing.after($.unsafe(data.content)); diff --git a/r2/r2/public/static/js/reddit.js b/r2/r2/public/static/js/reddit.js index e1ed9bcf2..0131b05d9 100644 --- a/r2/r2/public/static/js/reddit.js +++ b/r2/r2/public/static/js/reddit.js @@ -56,7 +56,7 @@ function post_form(form, where, statusfunc, nametransformfunc, block) { function get_form_fields(form, fields) { fields = fields || {}; /* consolidate the form's inputs for submission */ - $(form).find("select, input, textarea").not(".gray").each(function() { + $(form).find("select, input, textarea").not(".gray, :disabled").each(function() { if (($(this).attr("type") != "radio" && $(this).attr("type") != "checkbox") || $(this).attr("checked")) @@ -158,7 +158,15 @@ function toggle_label (elem, callback, cancelback) { function toggle(elem, callback, cancelback) { var self = $(elem).parent().andSelf().filter(".option"); var sibling = self.removeClass("active") - .siblings().addClass("active").get(0); + .siblings().addClass("active").get(0); + + /* + var self = $(elem).siblings().andSelf(); + var sibling = self.filter(":hidden").debug(); + self = self.filter(":visible").removeClass("active"); + sibling = sibling.addClass("active").get(0); + */ + if(cancelback && !sibling.onclick) { sibling.onclick = function() { return toggle(sibling, cancelback, callback); @@ -238,10 +246,7 @@ function get_organic(elem, next) { /* links */ function linkstatus(form) { - var title = $(form).find("#title").attr("value"); - if(title) - return reddit.status_msg.submitting; - return reddit.status_msg.fetching; + return reddit.status_msg.submitting; }; @@ -271,22 +276,8 @@ function unfriend(user_name, container_name, type) { } }; -function show_media(obj) { - obj = $.unsafe(obj); - return function(elem) { - var where = $(elem).thing().find(".embededmedia"); - if (where.length) - where.show().html(obj); - else - $(elem).new_thing_child('
' + obj + '
'); - } -}; - -function cancelMedia(elem) { - return cancelToggleForm(elem, ".embededmedia", ".media-button"); -}; - function share(elem) { + $.request("new_captcha"); $(elem).new_thing_child($(".sharelink:first").clone(true) .attr("id", "sharelink_" + $(elem).thing_id()), false); @@ -299,56 +290,12 @@ function cancelShare(elem) { /* Comment generation */ function helpon(elem) { - $(elem).parents("form:first").children(".markhelp:first").show(); + $(elem).parents(".usertext-edit:first").children(".markhelp:first").show(); }; function helpoff(elem) { - $(elem).parents("form:first").children(".markhelp:first").hide(); + $(elem).parents(".usertext-edit:first").children(".markhelp:first").hide(); }; - -function chkcomment(form) { - var entry = $(form).find("textarea"); - if( entry.hasClass("gray") || !entry.attr("value") ) { - return false; - } else if(form.replace.value) - return post_form(form, 'editcomment', null, null, true); - else - return post_form(form, 'comment', null, null, true); -}; - -function comment_edit(elem) { - return $(".commentreply:first").clone(true) - .find("button[name=cancel]").show().end() - .attr("id", "commentreply_" + $(elem).thing_id()); -}; - -function reply(elem) { - $(elem).new_thing_child(comment_edit(elem)) - .find('textarea:first').focus(); -}; - -function editcomment(elem) { - var comment = $(elem).thing(); - var thing_name = comment.thing_id(); - var edit = comment_edit(elem); - var content = comment.find(".edit-body:first").html(); - content = decodeURIComponent(content.replace(/\+/g, " ")); - edit.prependTo(comment) - .hide() - .find("button[name=comment]").hide().end() - .find("button[name=edit]").show().end() - .find("textarea") - .attr("value", content) - .removeClass("gray") - edit.attr("parent").value = thing_name; - edit.attr("replace").value = 1; - - comment.children(".midcol, .entry").hide(); - edit.find("textarea:first").focus(); - edit.show(); -}; - - function hidecomment(elem) { $(elem).thing().hide() .find(".noncollapsed:first, .midcol:first, .child:first").hide().end() @@ -364,15 +311,6 @@ function showcomment(elem) { return false; }; -function cancelReply(elem) { - var on_hide = function(form) { - $.things($(form).attr("parent").value) - .children(".midcol, .entry").show(); - }; - return cancelToggleForm(elem, ".commentreply", ".reply-button", on_hide); -}; - - function morechildren(form, link_id, children, depth) { $(form).html(reddit.status_msg.loading) .css("color", "red"); @@ -650,6 +588,304 @@ function register(elem) { return post_user(this, "register"); }; +/***submit stuff***/ +function fetch_title() { + var url_field = $("#url-field"); + var error = url_field.find(".NO_URL"); + var status = url_field.find(".title-status"); + var url = $("#url").val(); + if (url) { + status.show().text("loading..."); + error.hide(); + $.request("fetch_title", {url: url}); + } + else { + status.hide(); + error.show().text("a url is required"); + } +} + +/**** sr completing ****/ +function sr_cache() { + if (!$.defined(reddit.sr_cache)) { + reddit.sr_cache = new Array(); + } + return reddit.sr_cache; +} + +function highlight_reddit(item) { + $("#sr-drop-down").children('.sr-selected').removeClass('sr-selected'); + if (item) { + $(item).addClass('sr-selected'); + } +} + +function update_dropdown(sr_names) { + var drop_down = $("#sr-drop-down"); + if (!sr_names.length) { + drop_down.hide(); + return; + } + + var first_row = drop_down.children(":first"); + first_row.removeClass('sr-selected'); + drop_down.children().remove(); + + $.each(sr_names, function(i) { + if (i > 10) return; + var name = sr_names[i]; + var new_row = first_row.clone(); + new_row.text(name); + drop_down.append(new_row); + }); + + drop_down.show(); +} + +function sr_search(query) { + query = query.toLowerCase(); + var cache = sr_cache(); + if (!cache[query]) { + $.request('search_reddit_names', {query: query}, + function (r) { + cache[query] = r['names']; + update_dropdown(r['names']); + }); + } + else { + update_dropdown(cache[query]); + } +} + +function sr_name_up(e) { + var new_sr_name = $("#sr-autocomplete").val(); + var old_sr_name = window.old_sr_name || ''; + window.old_sr_name = new_sr_name; + + if (new_sr_name == '') { + hide_sr_name_list(); + } + else if (e.keyCode == 38 || e.keyCode == 40 || e.keyCode == 9) { + } + else if (e.keyCode == 27 && reddit.orig_sr) { + $("#sr-autocomplete").val(reddit.orig_sr); + hide_sr_name_list(); + } + else if (new_sr_name != old_sr_name) { + reddit.orig_sr = new_sr_name; + sr_search($("#sr-autocomplete").val()); + } +} + +function sr_name_down(e) { + var input = $("#sr-autocomplete"); + + if (e.keyCode == 38 || e.keyCode == 40) { + var dir = e.keyCode == 38 && 'up' || 'down'; + + var cur_row = $("#sr-drop-down .sr-selected:first"); + var first_row = $("#sr-drop-down .sr-name-row:first"); + var last_row = $("#sr-drop-down .sr-name-row:last"); + + var new_row = null; + if (dir == 'down') { + if (!cur_row.length) new_row = first_row; + else if (cur_row.get(0) == last_row.get(0)) new_row = null; + else new_row = cur_row.next(':first'); + } + else { + if (!cur_row.length) new_row = last_row; + else if (cur_row.get(0) == first_row.get(0)) new_row = null; + else new_row = cur_row.prev(':first'); + } + highlight_reddit(new_row); + if (new_row) { + input.val($.trim(new_row.text())); + } + else { + input.val(reddit.orig_sr); + } + return false; + } + else if (e.keyCode == 13) { + hide_sr_name_list(); + input.parents("form").submit(); + return false; + } +} + +function hide_sr_name_list(e) { + $("#sr-drop-down").hide(); +} + +function sr_dropdown_mdown(row) { + reddit.sr_mouse_row = row; //global + return false; +} + +function sr_dropdown_mup(row) { + if (reddit.sr_mouse_row == row) { + var name = $(row).text(); + $("#sr-autocomplete").val(name); + $("#sr-drop-down").hide(); + } +} + +function set_sr_name(link) { + var name = $(link).text(); + $("#sr-autocomplete").trigger('focus').val(name); +} + +/*** tabbed pane stuff ***/ +function select_form_tab(elem, to_show, to_hide) { + //change the menu + var link_parent = $(elem).parent(); + link_parent + .addClass('selected') + .siblings().removeClass('selected'); + + //swap content and enable/disable form elements + var content = link_parent.parent('ul').next('.formtabs-content'); + content.find(to_show) + .show() + .find(":input").removeAttr("disabled").end(); + content.find(to_hide) + .hide() + .find(":input").attr("disabled", true); +} + +/**** expando stuff ********/ +function expando_cache() { + if (!$.defined(reddit.thing_child_cache)) { + reddit.thing_child_cache = new Array(); + } + return reddit.thing_child_cache; +} + +function expando_child(elem) { + var child_cache = expando_cache(); + var thing = $(elem).thing(); + + //swap button + var button = thing.find(".expando-button"); + button + .addClass("expanded") + .removeClass("collapsed") + .get(0).onclick = function() {unexpando_child(elem)}; + + //load content + var expando = thing.find(".expando"); + var key = thing.thing_id() + "_cache"; + if (!child_cache[key]) { + $.request("expando", + {"link_id":thing.thing_id()}, + function(r) { + child_cache[key] = r; + expando.html($.unsafe(r)); + }, + false, "html"); + } + else { + expando.html($.unsafe(child_cache[key])); + } + expando.show(); +} + +function unexpando_child(elem) { + var thing = $(elem).thing(); + var button = thing.find(".expando-button"); + button + .addClass("collapsed") + .removeClass("expanded") + .get(0).onclick = function() {expando_child(elem)}; + + thing.find(".expando").hide().empty(); +} + +/******* editting comments *********/ +function show_edit_usertext(form) { + var edit = form.find(".usertext-edit"); + var body = form.find(".usertext-body"); + var textarea = edit.find('div > textarea'); + + //max of the height of the content or the min values from the css. + var body_width = Math.max(body.children(".md").width(), 500); + var body_height = Math.max(body.children(".md").height(), 100); + + //we need to show the textbox first so it has dimensions + body.hide(); + edit.show(); + + //restore original (?) css width/height. I can't explain why, but + //this is important. + textarea.css('width', ''); + textarea.css('height', ''); + + //if there would be scroll bars, expand the textarea to the size + //of the rendered body text + if (textarea.get(0).scrollHeight > textarea.height()) { + var new_width = Math.max(body_width - 5, textarea.width()); + textarea.width(new_width); + edit.width(new_width); + + var new_height = Math.max(body_height, textarea.height()); + textarea.height(new_height); + } + + form + .find(".cancel, .save").show().end() + .find(".help-toggle").show().end(); + + textarea.focus(); +} + +function hide_edit_usertext(form) { + form + .find(".usertext-edit").hide().end() + .find(".usertext-body").show().end() + .find(".cancel, .save").hide().end() + .find(".help-toggle").hide().end() + .find(".markhelp").hide().end() +} + +function comment_reply_for_elem(elem) { + elem = $(elem); + var thing = elem.thing(); + var thing_id = elem.thing_id(); + //try to find a previous form + var form = thing.find(".child .usertext:first"); + if (!form.length || form.parent().thing_id() != thing.thing_id()) { + form = $(".usertext.cloneable:first").clone(true); + elem.new_thing_child(form); + form.attr("thing_id").value = thing_id; + form.attr("id", "commentreply_" + thing_id); + form.find(".error").hide(); + } + return form; +} + +function edit_usertext(elem) { + show_edit_usertext($(elem).thing().find(".usertext:first")); +} + +function cancel_usertext(elem) { + hide_edit_usertext($(elem).thing().find(".usertext:first")); +} + +function save_usertext(elem) { +} + +function reply(elem) { + var form = comment_reply_for_elem(elem); + //show the right buttons + show_edit_usertext(form); + //re-show the whole form if required + form.show(); + //update the cancel button to call the toggle button's click + form.find(".cancel").get(0).onclick = function() {form.hide()}; +} + + function populate_click_gadget() { /* if we can find the click-gadget, populate it */ if($('.click-gadget').length) { @@ -659,7 +895,8 @@ function populate_click_gadget() { clicked = $.uniq(clicked, 5); clicked.sort(); - $.request('gadget/click/' + clicked.join(','), undefined, undefined, undefined, true); + $.request('gadget/click/' + clicked.join(','), undefined, undefined, + undefined, "json", true); } } } @@ -809,4 +1046,3 @@ $(function() { }); - diff --git a/r2/r2/public/static/poll-collapsed-hover.png b/r2/r2/public/static/poll-collapsed-hover.png new file mode 100644 index 000000000..015183e86 Binary files /dev/null and b/r2/r2/public/static/poll-collapsed-hover.png differ diff --git a/r2/r2/public/static/poll-collapsed.png b/r2/r2/public/static/poll-collapsed.png new file mode 100644 index 000000000..689b1d2b9 Binary files /dev/null and b/r2/r2/public/static/poll-collapsed.png differ diff --git a/r2/r2/public/static/poll-expanded-hover.png b/r2/r2/public/static/poll-expanded-hover.png new file mode 100644 index 000000000..13261482a Binary files /dev/null and b/r2/r2/public/static/poll-expanded-hover.png differ diff --git a/r2/r2/public/static/poll-expanded.png b/r2/r2/public/static/poll-expanded.png new file mode 100644 index 000000000..6af1c097d Binary files /dev/null and b/r2/r2/public/static/poll-expanded.png differ diff --git a/r2/r2/public/static/rightarrow.png b/r2/r2/public/static/rightarrow.png new file mode 100644 index 000000000..4192cdcf7 Binary files /dev/null and b/r2/r2/public/static/rightarrow.png differ diff --git a/r2/r2/public/static/throbber.gif b/r2/r2/public/static/throbber.gif new file mode 100644 index 000000000..471c1a4f9 Binary files /dev/null and b/r2/r2/public/static/throbber.gif differ diff --git a/r2/r2/public/static/vid-collapsed-hover.png b/r2/r2/public/static/vid-collapsed-hover.png new file mode 100644 index 000000000..1e8dcb272 Binary files /dev/null and b/r2/r2/public/static/vid-collapsed-hover.png differ diff --git a/r2/r2/public/static/vid-collapsed.png b/r2/r2/public/static/vid-collapsed.png new file mode 100644 index 000000000..69789f983 Binary files /dev/null and b/r2/r2/public/static/vid-collapsed.png differ diff --git a/r2/r2/public/static/vid-expanded-hover.png b/r2/r2/public/static/vid-expanded-hover.png new file mode 100644 index 000000000..594e962e8 Binary files /dev/null and b/r2/r2/public/static/vid-expanded-hover.png differ diff --git a/r2/r2/public/static/vid-expanded.png b/r2/r2/public/static/vid-expanded.png new file mode 100644 index 000000000..814f93934 Binary files /dev/null and b/r2/r2/public/static/vid-expanded.png differ diff --git a/r2/r2/templates/captcha.html b/r2/r2/templates/captcha.html index 6aeafb659..4b400ff94 100644 --- a/r2/r2/templates/captcha.html +++ b/r2/r2/templates/captcha.html @@ -23,7 +23,35 @@ <%! from r2.lib.template_helpers import static %> <%namespace file="utils.html" import="error_field"/> -${captchagen(thing.iden, thing.error)} +<%namespace name="utils" file="utils.html"/> + +##${captchagen(thing.iden, thing.error)} + +${rounded_captcha()} + +<%def name="captcha_basics(iden='')"> + <% + iden = getattr(thing, "iden", iden) + %> + + + i wonder if these things even work + + +<%def name="rounded_captcha()"> +<%utils:round_field title="${_('are you human?')}" description="${_('(sorry)')}" css_class="captcha"> + ${captcha_basics()} + + ${error_field("BAD_CAPTCHA", "captcha")} + + <%def name="captchagen(iden, error='', tabulate=False, tabular = True, size=60, label=True, show_error = True)"> %if tabulate: @@ -34,14 +62,7 @@ ${captchagen(thing.iden, thing.error)} %endif - i wonder if these things even work + ${captcha_basics(iden)} %if tabular: @@ -57,8 +78,7 @@ ${captchagen(thing.iden, thing.error)} %endif - - - - - - diff --git a/r2/r2/templates/commentreplybox.htmllite b/r2/r2/templates/commentreplybox.htmllite deleted file mode 100644 index a080b61ce..000000000 --- a/r2/r2/templates/commentreplybox.htmllite +++ /dev/null @@ -1,22 +0,0 @@ -## 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-2009 -## CondeNet, Inc. All Rights Reserved. -################################################################################ - diff --git a/r2/r2/templates/commentreplybox.xml b/r2/r2/templates/commentreplybox.xml deleted file mode 100644 index a080b61ce..000000000 --- a/r2/r2/templates/commentreplybox.xml +++ /dev/null @@ -1,22 +0,0 @@ -## 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-2009 -## CondeNet, Inc. All Rights Reserved. -################################################################################ - diff --git a/r2/r2/templates/createsubreddit.html b/r2/r2/templates/createsubreddit.html index 1e2440b91..8f88e17f3 100644 --- a/r2/r2/templates/createsubreddit.html +++ b/r2/r2/templates/createsubreddit.html @@ -85,8 +85,8 @@ function update_title(elem) { %if not thing.site: - ${error_field("SUBREDDIT_EXISTS", "span")} - ${error_field("BAD_SR_NAME", "span")} + ${error_field("SUBREDDIT_EXISTS", "name")} + ${error_field("BAD_SR_NAME", "name")} ${_("no spaces, e.g. slashdot")} @@ -106,8 +106,8 @@ function update_title(elem) { %endif - ${error_field("NO_TITLE", "span")} - ${error_field("TITLE_TOO_LONG", "span")} + ${error_field("NO_TEXT", "title")} + ${error_field("TOO_LONG", "title")} %if not thing.site: ${_("e.g. slashdot: news for nerds, stuff that matters")} @@ -129,7 +129,7 @@ function update_title(elem) { - ${error_field("DESC_TOO_LONG", "span")} + ${error_field("TOO_LONG", "description")} @@ -199,8 +199,8 @@ function update_title(elem) { what's this? - ${error_field("BAD_CNAME", "span")} - ${error_field("USED_CNAME", "span")} + ${error_field("BAD_CNAME", "domain")} + ${error_field("USED_CNAME", "domain")} %if thing.site and thing.site.can_change_stylesheet(c.user) and not g.css_killswitch: @@ -265,5 +265,5 @@ function update_title(elem) { >${text} - ${error_field("RATELIMIT", "span")} + ${error_field("RATELIMIT", "ratelimit")} diff --git a/r2/r2/templates/feedback.html b/r2/r2/templates/feedback.html index dc6cdf64b..4f77ccf69 100644 --- a/r2/r2/templates/feedback.html +++ b/r2/r2/templates/feedback.html @@ -20,93 +20,57 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ -<%namespace file="utils.html" import="error_field"/> +<%namespace file="utils.html" import="error_field"/ +<%namespace name="utils" file="utils.html"/> + +<%! + from r2.lib.pages import UserText +%> + %if thing.title:

${thing.title}

%endif -%if not thing.success:
-

${thing.success or ''} -

%if c.user_is_loggedin: - + %endif - -
- - - - - - - - - - - - - - - - - %if thing.captcha: - ${thing.captcha.render()} - %endif -
- - ${error_field("NO_NAME", "span")} -
- %if c.user_is_loggedin and hasattr(c.user, "email"): - -
- - - %if thing.replyto: - - %else: - - %endif - ${error_field("BAD_EMAILS", "span")} - %else: - %if thing.email: - - %else: - - %endif - %endif - ${_("(only used to reply)")} - - ${error_field("NO_EMAIL", "span")} -
- - - - ${error_field("NO_MESSAGE", "span")} -
-
- - -
-
-
-%endif + + +
+ <%utils:round_field title="${_('your name')}"> + + ${error_field("NO_NAME", "name", "span")} + +
+ +
+ <%utils:round_field title="${_('email')}" description="${_('(only used to reply)')}"> + + ${error_field("BAD_EMAILS", "email", "span")} + ${error_field("NO_EMAIL", "email", "span")} + +
+ +
+ <%utils:round_field title="${_('message')}"> + ${UserText(None, have_form = False, creating = True).render()} + +
+ +
+ %if thing.captcha: + ${thing.captcha.render()} + %endif +
+ + +  + + + diff --git a/r2/r2/templates/link.html b/r2/r2/templates/link.html index 8d552f569..ee8a123c9 100644 --- a/r2/r2/templates/link.html +++ b/r2/r2/templates/link.html @@ -61,6 +61,13 @@ +<%def name="bottom_buttons()"> + + + <%def name="entry()">

<%call expr="make_link('title', 'title')"> @@ -69,13 +76,50 @@ ${self.domain()}

+ + ##the expando button + %if thing.link_child and not thing.link_child.expand: + + %endif +

${self.tagline()}

- + + <% + child_content = "" + if thing.link_child and thing.link_child.load: + child_content = unsafe(thing.link_child.content()) + expand = thing.link_child and thing.link_child.expand + %> + + ##if we're not on a permalink page we'll render the buttons on top + %if not expand: + ${bottom_buttons()} + %endif + +
+ %if expand: + ${child_content} + %else: + loading... + %endif +
+ + ##if we are on a permalink page, we'll render the buttons below + %if expand: + ${bottom_buttons()} + %endif + + ##populate the expando cache if we have something + %if not expand and child_content: + + %endif <%def name="subreddit()" buffered="True"> @@ -145,6 +189,11 @@ newwindow = c.user.pref_newwindow)} %endif + %if thing.editable: +
  • + ${parent.simple_button(_("edit"), "edit_usertext")} +
  • + %endif
  • - ${parent.toggle_button("media-button", _("watch"), _("cancel"), - 'show_media("%s")' % websafe(thing.media_object), - "cancelMedia")} -
  • - %endif <%def name="thumbnail()"> diff --git a/r2/r2/templates/linkinfobar.html b/r2/r2/templates/linkinfobar.html index 9dc39f7ac..d5e221428 100644 --- a/r2/r2/templates/linkinfobar.html +++ b/r2/r2/templates/linkinfobar.html @@ -21,7 +21,7 @@ ################################################################################ <%namespace file="printable.html" import="state_button, thing_css_class" /> -
    +
    %if not thing.a.is_self: diff --git a/r2/r2/templates/login.html b/r2/r2/templates/login.html index b381b8d41..0a281de97 100644 --- a/r2/r2/templates/login.html +++ b/r2/r2/templates/login.html @@ -67,8 +67,8 @@ %if register: - ${error_field("BAD_USERNAME", kind="span")} - ${error_field("USERNAME_TAKEN", kind="span")} + ${error_field("BAD_USERNAME", "user", kind="span")} + ${error_field("USERNAME_TAKEN", "user", kind="span")} %endif %if register: @@ -79,7 +79,7 @@ %if register: - ${error_field("BAD_EMAILS", kind="span")} + ${error_field("BAD_EMAILS", "email", kind="span")} %endif %endif @@ -88,9 +88,9 @@ %if register: - ${error_field("BAD_PASSWORD", kind="span")} + ${error_field("BAD_PASSWORD", "passwd", kind="span")} %else: - ${error_field("WRONG_PASSWORD", kind="span")} + ${error_field("WRONG_PASSWORD", "passwd", kind="span")} %endif %if register: @@ -98,12 +98,12 @@ - ${error_field("BAD_PASSWORD_MATCH", kind="span")} + ${error_field("BAD_PASSWORD_MATCH", "passwd2", kind="span")}
  • - ${captchagen('', tabulate=True, label=False, size=30)} - ${error_field("DRACONIAN", kind="span")} - ${error_field("RATELIMIT", kind="span")} + <% iden = hasattr(thing, "captcha") and thing.captcha.iden or '' %> + ${captchagen(iden, tabulate=True, label=False, size=30)} + ${error_field("RATELIMIT", "ratelimit")}
  • %endif
  • diff --git a/r2/r2/templates/loginformwide.html b/r2/r2/templates/loginformwide.html index 2985dc8ce..2ecc67219 100644 --- a/r2/r2/templates/loginformwide.html +++ b/r2/r2/templates/loginformwide.html @@ -44,7 +44,7 @@ - ${error_field("WRONG_PASSWORD", "div")} + ${error_field("WRONG_PASSWORD", "passwd", "div")}
    diff --git a/r2/r2/templates/message.html b/r2/r2/templates/message.html index a8b98973d..abf9de5aa 100644 --- a/r2/r2/templates/message.html +++ b/r2/r2/templates/message.html @@ -66,10 +66,6 @@ ${unsafe(safemarkdown(thing.body))} -<%def name="commentText()"> - ${edit_comment_filter(thing.body)} - - <%def name="buttons()"> %if hasattr(thing, "was_comment"):
  • @@ -81,7 +77,7 @@ ${unsafe(safemarkdown(thing.body))} ${parent.buttons()} %if c.user_is_loggedin:
  • - ${parent.simple_button(_("reply {verb}"), "reply")} + ${parent.simple_button(_("reply"), "reply")}
  • %endif %endif @@ -94,11 +90,7 @@ ${unsafe(safemarkdown(thing.body))}

    ${self.commentBody()}
    - %if thing.author == c.user: -
    ${self.commentText()}\ -
    - %endif -
      +
        ${self.buttons()}
      diff --git a/r2/r2/templates/messagecompose.html b/r2/r2/templates/messagecompose.html index 6382492c1..7c3b72ceb 100644 --- a/r2/r2/templates/messagecompose.html +++ b/r2/r2/templates/messagecompose.html @@ -20,57 +20,48 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ -<%namespace file="utils.html" import="error_field, submit_form, success_field"/> +<%namespace file="utils.html" import="error_field, submit_form"/> +<%namespace name="utils" file="utils.html"/> + +<%! + from r2.lib.pages import UserText +%>

      ${_("send a message")}

      -<%call expr="submit_form( - onsubmit='return post_form(this, \'compose\', null, null, true)', - method='post', _id = 'compose-message', - action='/message/compose', _class='iform')"> -${success_field(_("your message has been delivered"), successful=thing.success)} -
    ${_("toolbar link")}
    - - - - - - - - - - - - - - - - %if thing.captcha: - ${thing.captcha.render()} - %endif - - - - -
    - ${error_field("NO_USER", "span")} - ${error_field("USER_DOESNT_EXIST", "span")}
    - - - - ${error_field("NO_SUBJECT", "span")}
    - - - ${error_field("NO_MSG_BODY", "span")} - ${error_field("COMMENT_TOO_LONG", "span")}
    - - - - -
    - +<%utils:submit_form onsubmit="return post_form(this, 'compose', null, null, true)", + method="post", _id = "compose-message", + action="/message/compose"> + +
    + <%utils:round_field title="${_('to')}", description="${_('(username)')}"> + + ${error_field("NO_USER", "to")} + ${error_field("USER_DOESNT_EXIST", "to")} + +
    + +
    + <%utils:round_field title="${_('subject')}"> + + ${error_field("NO_SUBJECT", "subject", "span")} + +
    + +
    + <%utils:round_field title="${_('message')}"> + ${UserText(None, have_form = False, creating = True).render()} + +
    + +
    + %if thing.captcha: + ${thing.captcha.render()} + %endif +
    + + + + + diff --git a/r2/r2/templates/morechildren.html b/r2/r2/templates/morechildren.html index c344e2122..3d48b5c81 100644 --- a/r2/r2/templates/morechildren.html +++ b/r2/r2/templates/morechildren.html @@ -38,9 +38,6 @@ load more comments  (${thing.count} ${ungettext("reply", -<%def name="commentText()"> - - <%def name="arrows()"> diff --git a/r2/r2/templates/morerecursion.html b/r2/r2/templates/morerecursion.html index 690fdda3d..754ae32a0 100644 --- a/r2/r2/templates/morerecursion.html +++ b/r2/r2/templates/morerecursion.html @@ -29,9 +29,6 @@ ${_("continue this thread")} -<%def name="commentText()"> - - <%def name="arrows()"> diff --git a/r2/r2/templates/navbutton.html b/r2/r2/templates/navbutton.html index a2b63cece..209072075 100644 --- a/r2/r2/templates/navbutton.html +++ b/r2/r2/templates/navbutton.html @@ -31,7 +31,7 @@ <%def name="js(selected = False)"> ${plain_link(thing.selected_title() if selected else thing.title, - '/', _sr_path = False, nocname = thing.nocname, + thing.path, _sr_path = False, nocname = True, _class = thing.css_class, _id = thing._id, onclick = thing.onclick)} diff --git a/r2/r2/templates/navmenu.html b/r2/r2/templates/navmenu.html index 3e060d4ad..349acc3bd 100644 --- a/r2/r2/templates/navmenu.html +++ b/r2/r2/templates/navmenu.html @@ -111,7 +111,8 @@ <%def name="tabmenu()"> %if thing: -
      +
        %for i, option in enumerate(thing): <% diff --git a/r2/r2/templates/newlink.html b/r2/r2/templates/newlink.html index 937436636..5d2def1c4 100644 --- a/r2/r2/templates/newlink.html +++ b/r2/r2/templates/newlink.html @@ -22,90 +22,109 @@ <%! from r2.lib.strings import strings + from r2.lib.pages import UserText from r2.lib.template_helpers import add_sr %> + <%namespace file="utils.html" import="error_field, submit_form, plain_link, text_with_links"/> +<%namespace name="utils" file="utils.html"/> -%if thing.subreddits: -

        ${_("submit")}

        -%else: -

        ${_("submit to %(site)s") % dict(site=c.site.name)}

        +

        ${_("submit to reddit")}

        + +<%utils:submit_form onsubmit="return post_form(this, 'submit', linkstatus, null, true)" + action=${add_sr("/submit")}, + _class="submit content", + _id="newlink"> + +${thing.formtabs_menu.render()} +
        + +
        + +
        ${strings.submit_text}
        +
        + +
        + <%utils:round_field title="${_('title')}" id="title-field"> + + ${error_field("NO_TEXT", "title", "span")} + ${error_field("TOO_LONG", "title", "span")} + +
        + +
        + <%utils:round_field title="${_('url')}" id="url-field"> + + + ${error_field("NO_URL", "url", "span")} + ${error_field("BAD_URL", "url", "span")} + ${error_field("ALREADY_SUB", "url", "span")} +
        + + +
        + +
        + +
        + <%utils:round_field title="${_('text')}", description="${_('(optional)')}" id="text-field"> + + ${UserText(None, have_form = False, creating = True).render()} + +
        + +
        + <%utils:round_field title="${_('reddit')}" id="reddit-field"> +
        + +
          +
        • nothin
        • +
        +
        + + ${error_field("SUBREDDIT_NOEXIST", "sr", "span")} + ${error_field("SUBREDDIT_REQUIRED", "sr", "span")} + +
        + ${_("popular choices")} +
          + %for name in thing.subreddits: +
        • + ${name} +
        • + %endfor +
        +
        + +
        + +%if thing.captcha: + ${thing.captcha.render()} %endif + +
        -<%call expr="submit_form(onsubmit='return post_form(this, \'submit\', linkstatus, null, true)', - action=add_sr('/submit'), _class='long-text content', _id='newlink')"> - %if not thing.subreddits: - - %endif - - - - - - - - - - - %if thing.subreddits: - - - - - %endif - - - - - %if thing.captcha: - ${thing.captcha.render()} - %endif - - - - -
        - - - - ${error_field("NO_URL", "span")} - ${error_field("BAD_URL", "span")} - ${error_field("ALREADY_SUB", "span")} -
        - - - ${error_field("NO_TITLE", "span")} - ${error_field("TITLE_TOO_LONG", "span")} -
        - - - -
        - - -
        - - - ${error_field("RATELIMIT", "span")} -
        - - - -

        - ${text_with_links(_("use reddit from your toolbar with the %(bookmarklets)s"), - bookmarklets = (_('reddit bookmarklets'), '/bookmarklets'))} -

        - +
        + + + ${error_field("RATELIMIT", "ratelimit")} +
        + + diff --git a/r2/r2/templates/panestack.html b/r2/r2/templates/panestack.html index 3ed97782c..e5101dc1b 100644 --- a/r2/r2/templates/panestack.html +++ b/r2/r2/templates/panestack.html @@ -26,6 +26,10 @@ ${thing.css_class and ("class='%s'" % str(thing.css_class)) or ""}> %endif + %if thing.title: +

        ${thing.title}

        + %endif + ${t.render() if t else ''} %if thing.div: diff --git a/r2/r2/templates/password.html b/r2/r2/templates/password.html index 2936e2fee..653563426 100644 --- a/r2/r2/templates/password.html +++ b/r2/r2/templates/password.html @@ -21,33 +21,24 @@ ################################################################################ <%namespace file="utils.html" import="error_field, success_field"/> -${success_field(_('you should receive an email shortly'), - successful=thing.success, hide='passform')} +<%namespace name="utils" file="utils.html"/>

        ${_("what's my password?")}

        -

        ${_("enter your user name below to receive your login information")}

        - - - - - - - - - -
        - - - - - -
        - - ${error_field("USER_DOESNT_EXIST", "span")} - ${error_field("NO_EMAIL_FOR_USER", "span")} -
        +

        ${_("enter your user name below to receive your login information")}

        + +
        + <%utils:round_field title="${_('username')}"> + + ${error_field("USER_DOESNT_EXIST", "name")} + ${error_field("NO_EMAIL_FOR_USER", "name")} + +
        + + + +
        diff --git a/r2/r2/templates/permalinkmessage.html b/r2/r2/templates/permalinkmessage.html index 440df39eb..44d42aa23 100644 --- a/r2/r2/templates/permalinkmessage.html +++ b/r2/r2/templates/permalinkmessage.html @@ -20,11 +20,10 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ -
        -
        -

        - ${_("you are viewing a single comment's thread")} -

        -

        ${_("see the above comments")}

        -
        +
        + ${_("you are viewing a single comment's thread.")} +

        + ${_("view the rest of the comments")} +  → +

        diff --git a/r2/r2/templates/prefdelete.html b/r2/r2/templates/prefdelete.html index f4fa7879d..5d41d8ffc 100644 --- a/r2/r2/templates/prefdelete.html +++ b/r2/r2/templates/prefdelete.html @@ -21,28 +21,36 @@ ################################################################################ <%namespace file="utils.html" import="error_field"/> +<%namespace name="utils" file="utils.html"/> + <% import random %> <%def name="areyousure(name)"> - ${_("are you sure?")} - <% yes, no = random.choice(((_("yes"), _("no")), (_("no"), _("yes")))) %> - + <% yes, no = random.choice(((_("yes"), _("no")), (_("no"), _("yes")))) %> + +
        + <%utils:round_field title="${_('are you sure?')}"> +
        + - - + +
        + + - - +
        + +

        ${_("delete your reddit account? hope you have a good reason.")}

        -
        - + ${areyousure("areyousure1")} ${areyousure("areyousure2")} ${areyousure("areyousure3")} -
        +
        diff --git a/r2/r2/templates/prefupdate.html b/r2/r2/templates/prefupdate.html index 46da41800..9d77cf278 100644 --- a/r2/r2/templates/prefupdate.html +++ b/r2/r2/templates/prefupdate.html @@ -21,34 +21,42 @@ ################################################################################ <%namespace file="utils.html" import="error_field"/> +<%namespace name="utils" file="utils.html"/> -
        ${_("update your email or password")} + + - - - - - - - - - - - - - - - - - - - - - -
        ${_("current password (required)")}:${error_field("WRONG_PASSWORD")}
        ${_("email")}: - ${error_field("BAD_EMAILS")} -
        ${_("new password")}:${error_field("BAD_PASSWORD")}
        ${_("verify password")}: ${error_field("BAD_PASSWORD_MATCH")} -
        + +
        + <%utils:round_field title="${_('current password')}" description="${_('(required)')}"> + + ${error_field("WRONG_PASSWORD", "curpass")} + +
        + +
        + <%utils:round_field title="${_('email')}"> + + ${error_field("BAD_EMAILS", "email")} + +
        + +
        + <%utils:round_field title="${_('new password')}"> + + ${error_field("BAD_PASSWORD", "newpass")} + +
        + +
        + <%utils:round_field title="${_('verify password')}"> + + ${error_field("BAD_PASSWORD_MATCH", "verpass")} + +
        + +
        diff --git a/r2/r2/templates/printable.html b/r2/r2/templates/printable.html index 7ae88258d..90991d3e3 100644 --- a/r2/r2/templates/printable.html +++ b/r2/r2/templates/printable.html @@ -305,14 +305,15 @@ thing id-${what._fullname} callback, cancelback, css_class = '', alt_css_class = '', reverse = False, - login_required = False)"> + login_required = False, + style = '',)"> <% if reverse: callback, cancelback = cancelback, callback title, alt_title = alt_title, title css_class, alt_css_class = alt_css_class, css_class %> - + - ${error_field("NO_TITLE", "span")} + ${error_field("NO_TEXT", "title")} + ${error_field("TOO_LONG", "title")} @@ -56,9 +57,9 @@ value="${('self' if thing.link.is_self else thing.link.url) if thing.link else ""}"/> - ${error_field("NO_URL", "span")} - ${error_field("BAD_URL", "span")} - ${error_field("ALREADY_SUB", "span")} + ${error_field("NO_URL", "url")} + ${error_field("BAD_URL", "url")} + ${error_field("ALREADY_SUB", "url")} @@ -78,7 +79,7 @@ %endif - ${error_field("SUBREDDIT_NOEXIST")} + ${error_field("SUBREDDIT_NOEXIST", "sr")} @@ -121,7 +122,7 @@ %endif - ${error_field("BAD_NUMBER", "span")} + ${error_field("BAD_NUMBER", "timelimitlength")} @@ -148,5 +149,5 @@ >${text} - ${error_field("RATELIMIT", "span")} + ${error_field("RATELIMIT", "ratelimit")}
        diff --git a/r2/r2/templates/redditheader.html b/r2/r2/templates/redditheader.html index f18f5f5d1..d7bcb1aca 100644 --- a/r2/r2/templates/redditheader.html +++ b/r2/r2/templates/redditheader.html @@ -97,4 +97,10 @@
        +
        +
        +
        +
        +
        +
    diff --git a/r2/r2/templates/resetpassword.html b/r2/r2/templates/resetpassword.html index 85b8895ca..b7f4e5d3e 100644 --- a/r2/r2/templates/resetpassword.html +++ b/r2/r2/templates/resetpassword.html @@ -21,6 +21,7 @@ ################################################################################ <%namespace file="utils.html" import="error_field, success_field"/> +<%namespace name="utils" file="utils.html"/> %if thing.done:

    your password has been reset and you've been logged in. Go use the site!

    @@ -28,52 +29,27 @@ ${error_field("EXPIRED", 'p')}
    -

    reset your password

    + onsubmit="return post_form(this,'resetpassword')"> + +

    choose a new password

    - - - - - - - - - - - - - - - - - - - -
    - ${_("username")} - - -
    - ${_("new password")} - - - - ${error_field("BAD_PASSWORD")} -
    - ${_("verify password")} - - - - ${error_field("BAD_PASSWORD_MATCH")} -
    - - - -
    + +
    + <%utils:round_field title="${_('new password')}"> + + ${error_field("BAD_PASSWORD", "passwd")} + +
    + +
    + <%utils:round_field title="${_('verify password')}"> + + ${error_field("BAD_PASSWORD_MATCH", "passwd2")} + +
    + + + +
    %endif diff --git a/r2/r2/templates/commentreplybox.mobile b/r2/r2/templates/selftext.html similarity index 81% rename from r2/r2/templates/commentreplybox.mobile rename to r2/r2/templates/selftext.html index a786aafc0..08e05d831 100644 --- a/r2/r2/templates/commentreplybox.mobile +++ b/r2/r2/templates/selftext.html @@ -16,10 +16,17 @@ ## 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-2009 +## All portions of the code written by CondeNet are Copyright (c) 2006-2008 ## CondeNet, Inc. All Rights Reserved. ################################################################################ +<%! + from r2.lib.filters import unsafe, safemarkdown, keep_space +%> +<%namespace file="utils.html" import="error_field"/> - - +
    +
    + ${unsafe(safemarkdown(thing.link.selftext))} +
    +
    diff --git a/r2/r2/templates/sharelink.html b/r2/r2/templates/sharelink.html index b62f88712..301e619d8 100644 --- a/r2/r2/templates/sharelink.html +++ b/r2/r2/templates/sharelink.html @@ -6,7 +6,7 @@ class="pretty-form sharelink" action="/post/share" ${"" if thing.link_name else "style='display:none'"}>
    - ${error_field("RATELIMIT")} + ${error_field("RATELIMIT", "ratelimit")} @@ -22,9 +22,9 @@ @@ -42,7 +42,7 @@ name="share_from" /> @@ -60,8 +60,8 @@ value="${c.user.email if hasattr(c.user, 'email') else ''}"/> @@ -80,18 +80,16 @@ %if thing.captcha: - ${captchagen(thing.captcha.iden, thing.captcha.error, - tabulate = False, size = 30)} + ${captchagen('', tabulate = False, size = 30)} %endif diff --git a/r2/r2/templates/subredditstylesheet.html b/r2/r2/templates/subredditstylesheet.html index bd9e46bb4..5a30863cc 100644 --- a/r2/r2/templates/subredditstylesheet.html +++ b/r2/r2/templates/subredditstylesheet.html @@ -108,7 +108,7 @@
    - ${error_field("BAD_EMAILS")} - ${error_field("TOO_MANY_EMAILS")} + ${error_field("BAD_EMAILS", "replyto")} + ${error_field("TOO_MANY_EMAILS", "replyto")}
    - ${error_field("COMMENT_TOO_LONG")} + ${error_field("TOO_LONG", "message")}
    - @@ -99,6 +97,7 @@ onclick="return cancelShare(this);"> ${_("cancel")} + - ${error_field("BAD_CSS_NAME", "span")} + ${error_field("BAD_CSS_NAME", "name")}
    ${_("(image names should consist of alphanumeric characters and '-' only)")} diff --git a/r2/r2/templates/tabbedpane.html b/r2/r2/templates/tabbedpane.html new file mode 100644 index 000000000..450ffc720 --- /dev/null +++ b/r2/r2/templates/tabbedpane.html @@ -0,0 +1,14 @@ +${thing.tabmenu.render()} +
    + %for i, (name, title, pane) in enumerate(thing.tabs): +
    0 else ""}> + ${pane.render()} +
    + %endfor + ##select the front tab and init the other tabs + + +
    diff --git a/r2/r2/templates/userlist.html b/r2/r2/templates/userlist.html index 5910d8108..9c47d0429 100644 --- a/r2/r2/templates/userlist.html +++ b/r2/r2/templates/userlist.html @@ -34,7 +34,7 @@ - ${error_field("USER_DOESNT_EXIST", "span")} + ${error_field("USER_DOESNT_EXIST", "name")} %endif diff --git a/r2/r2/templates/usertext.html b/r2/r2/templates/usertext.html new file mode 100644 index 000000000..f6653f488 --- /dev/null +++ b/r2/r2/templates/usertext.html @@ -0,0 +1,145 @@ +## 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-2009 +## CondeNet, Inc. All Rights Reserved. +################################################################################ +<%! + from r2.lib.filters import unsafe, safemarkdown, keep_space + from r2.lib.utils import randstr +%> + +<%namespace file="printable.html" import="toggle_button" /> +<%namespace file="utils.html" import="error_field"/> + +<%def name="action_button(name, btn_type, onclick, display)"> + + + +%if thing.have_form: +
    +%else: +
    +%endif + + ##this is set for both editting selftext and creating new comments + + + %if not thing.creating: +
    + ${unsafe(safemarkdown(thing.text, nofollow = thing.nofollow))} +
    + %endif + + %if thing.editable or thing.creating: + ##keep this on one line so we don't add extra spaces +
    + ##this div prevents IE7 from adding extra margin to the textarea. +
    + +
    + +
    + ${toggle_button("help-toggle", _("formatting help"), _("hide help"), + "helpon", "helpoff", + style = "" if thing.creating else "display: none")} + + ${error_field("TOO_LONG", "text", "span")} + ${error_field("NO_TEXT", "text", "span")} + +
    + ${action_button("save", "submit", "", + thing.creating and thing.have_form)} + ${action_button("cancel", "button", "cancel_usertext(this)", False)} + %if thing.have_form: + + %endif +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + %endif + +%if thing.have_form: + +%else: +
    +%endif diff --git a/r2/r2/templates/utils.html b/r2/r2/templates/utils.html index 74e70be73..b6e09045c 100644 --- a/r2/r2/templates/utils.html +++ b/r2/r2/templates/utils.html @@ -51,7 +51,7 @@ id="${arg}_${thing and thing._fullname or ''}" <%def name="submit_form(onsubmit='', action='', _class='', method='post', _id='', **params)"> -
    -<%def name="error_field(name, kind='p')"> - <${kind} class="error ${name}" - style="${'' if name in c.errors else 'display:none'}"> - %if name in c.errors: - ${c.errors[name].message} +<%def name="error_field(error_name, field_name, kind='span')"> + <${kind} class="error ${error_name} field-${field_name}" + style="${'' if error_name in c.errors else 'display:none'}"> + %if error_name in c.errors: + ${c.errors[error_name].message} %endif @@ -393,3 +393,20 @@ ${unsafe(txt)}
    + +<%def name="round_field(title, description = '', css_class= '', **kw)"> +
    + ${title} + + %if description: + ${description} + %endif +
    + ${caller.body()} +
    +
    +