From 03bae0de48e2bf52138d7b0d8a85ab65c011dc74 Mon Sep 17 00:00:00 2001 From: PotatoDumplings Date: Mon, 22 Jul 2013 20:56:40 -0700 Subject: [PATCH 01/15] Moderators can now move comment threads between posts. --- r2/r2/controllers/api.py | 32 +++++++++ r2/r2/controllers/validator/validator.py | 14 ++++ r2/r2/lib/comment_tree.py | 14 ++-- r2/r2/lib/pages/pages.py | 12 +++- r2/r2/models/link.py | 35 ++++++++++ r2/r2/public/static/comments.js | 89 ++++++++++++++++++++++++ r2/r2/public/static/lesswrong.css | 56 +++++++++++++++ r2/r2/public/static/main.css | 3 +- r2/r2/templates/comment.html | 5 ++ r2/r2/templates/movebox.html | 70 +++++++++++++++++++ 10 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 r2/r2/templates/movebox.html diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 5e1fa212..36d65165 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -199,6 +199,38 @@ def POST_verifyemail(self, res, code): c.user._commit() res._success() + @Json + @validate(VModhash(), + VSrCanBan('id'), + thing = VByName('id'), + destination = VMoveURL('destination'), + reason = VComment('comment')) + def POST_move(self, res, thing, destination, reason): + if res._chk_errors((errors.NO_URL, errors.BAD_URL), + thing._fullname): + res._focus("destination_url_" + thing._fullname) + return + if res._chk_error(errors.COMMENT_TOO_LONG, + thing._fullname): + res._focus("comment_replacement_" + thing._fullname) + return + + comment, inbox_rel = Comment._new(Account._byID(thing.author_id), + destination, None, thing.body, + thing.ip) + children = list(Comment._query(Comment.c.parent_id == thing._id)) + for child in children: + child.recursive_move(destination, comment) + + thing.set_body('This comment was moved by ' + c.user.name + + ' to [here]({0}).\n\n'.format(comment.make_anchored_permalink(destination)) + + (reason if reason else '')) + + if g.write_query_queue: + queries.new_comment(comment, None) + + res._send_things(thing) + @Json @validate(VCaptcha(), VUser(), diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 0d4de246..cad46836 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -377,6 +377,20 @@ def run(self, name): except NotFound: return name +class VMoveURL(VRequired): + def __init__(self, item, *a, **kw): + VRequired.__init__(self, item, errors.NO_URL, *a, **kw) + + def run(self, url): + if not url: + return self.error() + else: + link = Link._byURL(url) + if not link: + return self.error(errors.BAD_URL) + else: + return link + class VSubredditTitle(Validator): def run(self, title): if not title: diff --git a/r2/r2/lib/comment_tree.py b/r2/r2/lib/comment_tree.py index 8bdd0c52..496fe5fa 100644 --- a/r2/r2/lib/comment_tree.py +++ b/r2/r2/lib/comment_tree.py @@ -88,13 +88,13 @@ def delete_comment(comment): def link_comments(link_id): key = comments_key(link_id) r = g.permacache.get(key) - if r: - return r - else: - with g.make_lock(lock_key(link_id)): - r = load_link_comments(link_id) - g.permacache.set(key, r) - return r + #if r: + # return r + #else: + with g.make_lock(lock_key(link_id)): + r = load_link_comments(link_id) + g.permacache.set(key, r) + return r def load_link_comments(link_id): q = Comment._query(Comment.c.link_id == link_id, diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 64a68168..6391ef3d 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -644,6 +644,7 @@ def __init__(self, link = None, comment = None, # link is a wrapped Link object self.link = self.link_listing.things[0] + self.movebox = MoveBox() link_title = ((self.link.title) if hasattr(self.link, 'title') else '') if comment: @@ -665,7 +666,7 @@ def __init__(self, link = None, comment = None, Reddit.__init__(self, title = title, body_class = 'post', robots = self.robots, *a, **kw) def content(self): - return self.content_stack(self.infobar, self.link_listing, self._content) + return self.content_stack(self.infobar, self.link_listing, self.movebox, self._content) def build_toolbars(self): return [] @@ -989,6 +990,15 @@ def __init__(self, link_name='', captcha=None, action = 'comment'): Wrapped.__init__(self, link_name = link_name, captcha = captcha, action = action) +class MoveBox(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 CommentListing(Wrapped): """Comment heading and sort, limit options""" def __init__(self, content, num_comments, nav_menus = []): diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index 284c43a5..ce6c7a40 100644 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -82,6 +82,18 @@ def __init__(self, *a, **kw): def by_url_key(cls, url): return base_url(url.lower()).encode('utf8') + @classmethod + def _byURL(cls, url): + url_re = re.compile("(?:http://)?.*?(/r/.*?)?(/lw/.*?/.*)") + id_re = re.compile("/lw/(\w*)/.*") + matcher = url_re.match(url) + if not matcher: + return False + matcher = id_re.match(matcher.group(2)) + link = Link._byID(int(matcher.group(1), 36)) + if not link._loaded: link._load() + return link + @classmethod def _by_url(cls, url, sr): from subreddit import Default @@ -174,6 +186,7 @@ def _submit(cls, title, article, author, sr, ip, tags, spam = False, date = None # Parse and create polls in the article l.set_article(article) + print l.url l.set_url_cache() # Add tags @@ -941,6 +954,28 @@ def has_children(self): child = list(q) return len(child)>0 + def recursive_move(self, destination, parent): + q = Comment._query(Comment.c.parent_id == self._id) + children = list(q) + comment, inbox_rel = Comment._new(Account._byID(self.author_id), + destination, parent, self.body, + self.ip) + if not children: + pass + else: + for child in children: + child.recursive_move(destination, comment) + + self.moderator_banned = not c.user_is_admin + self.banner = c.user.name + self._commit() + # NB: change table updated by reporting + from r2.models.report import unreport + unreport(self, correct=True, auto=False) + + if g.write_query_queue: + queries.new_comment(comment, None) + def can_delete(self): if not self._loaded: self._load() diff --git a/r2/r2/public/static/comments.js b/r2/r2/public/static/comments.js index 1299fd8f..ce3c7ace 100644 --- a/r2/r2/public/static/comments.js +++ b/r2/r2/public/static/comments.js @@ -107,6 +107,29 @@ Comment.prototype.show_editor = function(listing, where, text) { return edit_box; }; +Comment.prototype.show_move = function(listing, where, text) { + var edit_box = this.cloneAndReIDNodeOnce("samplemove"); + if (edit_box.parentNode != listing.listing) { + if (edit_box.parentNode) { + edit_box.parentNode.removeChild(edit_box); + } + listing.insert_node_before(edit_box, where); + } + else if (edit_box.parentNode.firstChild != edit_box) { + var p = edit_box.parentNode; + p.removeChild(edit_box); + p.insertBefore(edit_box, p.firstChild); + } + var box = this.$parent("comment_replacement"); + clearTitle(box); + box.value = text; + box.setAttribute("data-orig-value", text); + show(edit_box); + BeforeUnload.bind(Comment.checkModified, this._id); + return edit_box; +}; + + Comment.prototype.edit = function() { this.show_editor(this.parent_listing(), this.row, this.text); this.$parent("commentform").replace.value = "yes"; @@ -145,6 +168,14 @@ Comment.prototype.reply = function (showFlamebaitOverlay) { this.$parent("comment_reply").focus(); }; + +Comment.prototype.move = function() { + this.show_move(this.parent_listing(), this.row, ''); + this.$parent("moveform").replace.value = ""; + this.$parent("destination_url").focus(); + this.hide(); +}; + Comment.prototype.cancel = function() { var edit_box = this.cloneAndReIDNodeOnce("samplecomment"); hide(edit_box); @@ -152,10 +183,19 @@ Comment.prototype.cancel = function() { this.show(); }; +Comment.prototype.cancelmove = function() { + var edit_box = this.cloneAndReIDNodeOnce("samplemove"); + hide(edit_box); + BeforeUnload.unbind(Comment.checkModified, this._id); + this.show(); +}; + + Comment.comment = function(r, context) { var id = r.id; var parent_id = r.parent; new Comment(parent_id, context).cancel(); + new Comment(parent_id, context).cancelmove(); new Listing(parent_id, context).push(unsafe(r.content)); new Comment(r.id, context).show(); vl[id] = r.vl; @@ -191,6 +231,14 @@ Comment.editcomment = function(r, context) { com.show(); }; +Comment.move = function(r, context) { + var com = new Comment(r.id, context); + com.get('body').innerHTML = unsafe(r.contentHTML); + com.get('edit_body').innerHTML = unsafe(r.contentTxt); + com.cancelmove(); + com.show(); +}; + Comment.submitballot = function(r) { var com = new Comment(r.id); com.get('body').innerHTML = unsafe(r.contentHTML); @@ -276,6 +324,9 @@ function cancelReply(canceler) { new Comment(_id(canceler), Thing.getThingRow(canceler)).cancel(); }; +function cancelMove(canceler) { + new Comment(_id(canceler), Thing.getThingRow(canceler)).cancelmove(); +}; function reply(id, link, showFlamebaitOverlay) { if (logged) { @@ -286,6 +337,16 @@ function reply(id, link, showFlamebaitOverlay) { } }; +function move(id, link) { + if (logged) { + var com = new Comment(id, Thing.getThingRow(link)).move(); + } + else { + showcover(true, 'reply_' + id); + } +}; + + function chkcomment(form) { if(checkInProgress(form)) { var r = confirm("Request still in progress\n(click Cancel to attempt to stop the request)"); @@ -314,6 +375,34 @@ function chkcomment(form) { }); }; +function chkmove(form) { + if(checkInProgress(form)) { + var r = confirm("Request still in progress\n(click Cancel to attempt to stop the request)"); + if (r==false) + tagInProgress(form, false); + return false; + } + + var action = 'move'; + var context = Thing.getThingRow(form); + + tagInProgress(form, true); + + function cleanup_func(res_obj) { + tagInProgress(form, false); + + var obj = res_obj && res_obj.response && res_obj.response.object; + if (obj && obj.length) + for (var o = 0, ol = obj.length; o < ol; ++o) + Comment[action](obj[o].data, context); + } + + return post_form(form, action, null, null, true, null, { + handle_obj: false, + cleanup_func: cleanup_func + }); +}; + function tagInProgress(form, inProgress) { if (inProgress) form.addClassName("inprogress"); diff --git a/r2/r2/public/static/lesswrong.css b/r2/r2/public/static/lesswrong.css index 4b8fc211..6c3aea0a 100644 --- a/r2/r2/public/static/lesswrong.css +++ b/r2/r2/public/static/lesswrong.css @@ -183,6 +183,62 @@ div.inline-listing a:hover { text-decoration: none; } +.commentreplacement { + clear: both; + margin: 10px 15px 30px 0; +} +.commentreplacement textarea, input { + border-color: #e9e9e9; + -webkit-box-shadow: inset 1px 1px 5px #dbdbdb; + -moz-box-shadow: inset 1px 1px 5px #dbdbdb; + box-shadow: inset 1px 1px 5px #dbdbdb; + margin: 0 0 10px; + width: 100%; +} +.commentreplacement .buttons { + float: left; +} +.commentreplacement .buttons button, .commentreplacement .help-toggle button, .poll-voting-area button { + background: #f0eeee; /* Old browsers */ + background: -moz-linear-gradient(top, #f0eeee 0%, #c1c1c1 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0eeee), color-stop(100%,#c1c1c1)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f0eeee', endColorstr='#c1c1c1',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #f0eeee 0%,#c1c1c1 100%); /* W3C */ + border-color: #c6c6c6; + color: #666666; + cursor: pointer; + font-weight: bold; + padding: 5px 30px; +} +.commentreplacement .help-toggle button { + margin-right: -6px; +} + +.commentreplacement table.help { + clear: both; + margin: 5px 0 0 1px; + width: 480px; + border-collapse: collapse; +} +.commentreplacement .help, +.commentreplacement .help td, +.commentreplacement .help tr { + border: 1px solid #C0C0C0; + padding: 4px; + margin: 0px; +} +.commentreplacement .help-toggle { + float: right; +} +.commentreplacement .help-toggle a { + color: #538D4D; + font-weight: bold; + text-decoration: none; +} + #comment-controls label { padding-right: 5px; margin-left: 1em; diff --git a/r2/r2/public/static/main.css b/r2/r2/public/static/main.css index 812f0ff1..428dbd1f 100644 --- a/r2/r2/public/static/main.css +++ b/r2/r2/public/static/main.css @@ -826,7 +826,8 @@ div.comment-links ul li.retract a.yes, div.comment-links ul li.retract a.no { /* Temporary styling for text buttons until we have icons */ div.comment-links ul li.delete a, div.comment-links ul li.unban a, -div.comment-links ul li.ban a, div.comment-links ul li.ignore a { +div.comment-links ul li.ban a, div.comment-links ul li.ignore a, +div.comment-links ul li.move a { height: 20px; padding-top: 4px; } diff --git a/r2/r2/templates/comment.html b/r2/r2/templates/comment.html index 14adc951..d4bcc4bc 100644 --- a/r2/r2/templates/comment.html +++ b/r2/r2/templates/comment.html @@ -164,6 +164,11 @@ <% allow_delete = thing.can_be_deleted %> ${parent.delete_or_report_buttons(allow_delete,False,True)} ${parent.buttons()} + %if thing.can_ban: +
  • + ${parent.simple_button("move", fullname, _("Move"), "move", tool_tip=_("Move"))} +
  • + %endif ## Each comment has a hidden status span that is used to indicate voting errors. diff --git a/r2/r2/templates/movebox.html b/r2/r2/templates/movebox.html new file mode 100644 index 00000000..1adf01a2 --- /dev/null +++ b/r2/r2/templates/movebox.html @@ -0,0 +1,70 @@ +## 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-2008 +## CondeNet, Inc. All Rights Reserved. +################################################################################ +<%! + from r2.lib.template_helpers import get_domain, static, json +%> + +<%namespace file="utils.html" import="error_field"/> + +%if not thing.link_name: +