diff options
author | smccammon@mozilla.com <smccammon@mozilla.com@4eb1ac78-321c-0410-a911-ec516a8615a5> | 2009-06-26 06:55:31 (GMT) |
---|---|---|
committer | smccammon@mozilla.com <smccammon@mozilla.com@4eb1ac78-321c-0410-a911-ec516a8615a5> | 2009-06-26 06:55:31 (GMT) |
commit | 6fb6d9888199b17dd2a0136bd87ce8cf4abccda2 (patch) | |
tree | 1e846e539bf63d9db7c2e5b2fbf25b2048a4199b | |
parent | e8056c2f02b07167a557e6f53d83cf17359374a9 (diff) |
Bug 495491, added editor comments to add-on reviews, r=clouserw
git-svn-id: http://svn.mozilla.org/addons/trunk@28610 4eb1ac78-321c-0410-a911-ec516a8615a5
-rw-r--r-- | site/app/config/sql/remora.sql | 43 | ||||
-rw-r--r-- | site/app/controllers/components/editors.php | 79 | ||||
-rw-r--r-- | site/app/controllers/editors_controller.php | 32 | ||||
-rw-r--r-- | site/app/models/versioncomment.php | 272 | ||||
-rw-r--r-- | site/app/tests/data/remora-test-data.sql | 14 | ||||
-rw-r--r-- | site/app/tests/models/versioncomment.test.php | 21 | ||||
-rw-r--r-- | site/app/views/editors/email/notify_version_comment_plain.thtml | 18 | ||||
-rw-r--r-- | site/app/views/editors/review.thtml | 44 | ||||
-rw-r--r-- | site/app/webroot/css/editors.css | 26 | ||||
-rw-r--r-- | site/app/webroot/img/developers/comments.png | bin | 0 -> 1806 bytes | |||
-rw-r--r-- | site/app/webroot/js/editors.js | 95 |
11 files changed, 641 insertions, 3 deletions
diff --git a/site/app/config/sql/remora.sql b/site/app/config/sql/remora.sql index 33646b2..134079f 100644 --- a/site/app/config/sql/remora.sql +++ b/site/app/config/sql/remora.sql @@ -793,6 +793,49 @@ CREATE TABLE `reviews_moderation_flags` ( CONSTRAINT `reviews_moderation_flags_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Table structure for table `versioncomments` +-- +DROP TABLE IF EXISTS `versioncomments`; +CREATE TABLE `versioncomments` ( + `id` int(11) unsigned NOT NULL auto_increment, + `version_id` int(11) unsigned NOT NULL default '0', + `user_id` int(11) unsigned NOT NULL default '0', + `reply_to` int(11) unsigned default NULL, + `subject` varchar(1000) NOT NULL default '', + `comment` text NOT NULL, + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `modified` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`id`), + KEY `version_id` (`version_id`), + KEY `reply_to` (`reply_to`), + KEY `created` (`created`), + CONSTRAINT `versioncomments_ibfk_1` FOREIGN KEY (`version_id`) REFERENCES `versions` (`id`), + CONSTRAINT `versioncomments_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), + CONSTRAINT `versioncomments_ibfk_3` FOREIGN KEY (`reply_to`) REFERENCES `versioncomments` (`id`) ON DELETE CASCADE + +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Editor comments for version discussion threads'; + +-- +-- Table structure for table `users_versioncomments` +-- +DROP TABLE IF EXISTS `users_versioncomments`; +CREATE TABLE `users_versioncomments` ( + `user_id` int(11) unsigned NOT NULL, + `comment_id` int(11) unsigned NOT NULL, + `subscribed` tinyint(1) unsigned NOT NULL default '1', + `created` datetime NOT NULL default '0000-00-00 00:00:00', + `modified` datetime NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`user_id`,`comment_id`), + KEY `user_id` (`user_id`), + KEY `comment_id` (`comment_id`), + CONSTRAINT `users_versioncomments_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `users_versioncomments_ibfk_2` FOREIGN KEY (`comment_id`) REFERENCES `versioncomments` (`id`) ON DELETE CASCADE + +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Editor subscriptions to version discussion threads'; + + -- -- Table structure for table `stats_share_counts` -- diff --git a/site/app/controllers/components/editors.php b/site/app/controllers/components/editors.php index 2992af2..aaf35d0 100644 --- a/site/app/controllers/components/editors.php +++ b/site/app/controllers/components/editors.php @@ -325,6 +325,85 @@ class EditorsComponent extends Object { $this->controller->Email->subject = sprintf('Mozilla Add-ons: %s %s', $emailInfo['name'], $emailInfo['version']); $this->controller->Email->send(); } + + /** + * Post a new comment to a version by an editor + * @param int $versionId version ID + * @param array $data POST data + * @return int id of new comment on success, false on error + */ + function postVersionComment($versionId, $data) { + $returnId = false; + + $session = $this->controller->Session->read('User'); + + $commentData = $data['Versioncomment']; + $commentData['version_id'] = $versionId; + $commentData['user_id'] = $session['id']; + + // validation + if (empty($commentData['subject'])) { + $this->controller->Error->addError(___('editor_review_error_comment_subject_required', 'Comment subject is required')); + } + if (empty($commentData['comment'])) { + $this->controller->Error->addError(___('editor_review_error_comment_body_required', 'Comment body is required')); + } + + // cake does not turn '' into NULL + if ($commentData['reply_to'] === '') { + $commentData['reply_to'] = null; + } + + if ($this->controller->Error->noErrors()) { + if ($this->controller->Versioncomment->save($commentData)) { + $returnId = $this->controller->Versioncomment->id; + } else { + $this->controller->Error->addError(___('editor_review_error_comment_save_fail', 'Failed to save comment')); + } + } + + return $returnId; + } + + + /** + * Notify subscribed editors of a new comment posted to a thread + * @param int $commentId id of new comment + * @param int $rootId id of thread's root comment + * @return void + */ + function versionCommentNotify($commentId, $rootId) { + $comment = $this->controller->Versioncomment->findById($commentId); + $userIds = $this->controller->Versioncomment->getSubscribers($rootId); + + // nothing to send or nobody to send it to + if (empty($comment) || empty($userIds)) { return; } + + // fetch details + $addon = $this->controller->Addon->getAddon($comment['Version']['addon_id']); + $subscribers = $this->controller->User->findAllById($userIds, null, null, null, null, -1); + + // send out notification email(s) + $emailInfo = array( + 'addon' => $addon['Translation']['name']['string'], + 'version' => $comment['Version']['version'], + 'versionid' => $comment['Version']['id'], + 'commentid' => $commentId, + 'subject' => $comment['Versioncomment']['subject'], + 'author' => "{$comment['User']['firstname']} {$comment['User']['lastname']}", + ); + $this->controller->publish('info', $emailInfo, false); + + // load the spam cannon + $this->controller->Email->template = '../editors/email/notify_version_comment'; + $this->controller->Email->subject = $emailInfo['subject']; + + // fire away + foreach ($subscribers as &$subscriber) { + $this->controller->Email->to = $subscriber['User']['email']; + $result = $this->controller->Email->send(); + } + } /** * Jump to specific item in queue diff --git a/site/app/controllers/editors_controller.php b/site/app/controllers/editors_controller.php index 545567a..c7623a6 100644 --- a/site/app/controllers/editors_controller.php +++ b/site/app/controllers/editors_controller.php @@ -46,7 +46,7 @@ class EditorsController extends AppController var $uses = array('Addon', 'AddonCategory', 'Addontype', 'Application', 'Approval', 'Appversion', 'Cannedresponse', 'EditorSubscription', 'Eventlog', 'Favorite', 'File', 'Platform', 'Review', 'ReviewsModerationFlag', 'Category', 'Translation', - 'User', 'Version'); + 'User', 'Version', 'Versioncomment'); var $components = array('Amo', 'Audit', 'Developers', 'Editors', 'Email', 'Error', 'Image', 'Pagination'); var $helpers = array('Html', 'Javascript', 'Ajax', 'Listing', 'Localization', 'Pagination'); @@ -258,6 +258,7 @@ class EditorsController extends AppController $this->Addontype->bindFully(); $this->Version->bindFully(); $this->Addon->bindFully(); + $this->Versioncomment->bindFully(); if (!$version = $this->Version->findById($id, null, null, 1)) { $this->flash(_('error_version_notfound'), '/editors/queue'); @@ -282,7 +283,11 @@ class EditorsController extends AppController if (!empty($this->data)) { //pr($this->data); - if ($this->data['Approval']['ActionField'] == 'info') { + if (isset($this->data['Versioncomment'])) { + // new editor comment + $commentId = $this->Editors->postVersionComment($id, $this->data); + } + elseif ($this->data['Approval']['ActionField'] == 'info') { // request more information $this->Editors->requestInformation($addon, $this->data); } @@ -292,8 +297,22 @@ class EditorsController extends AppController else { $this->Editors->reviewPendingFiles($addon, $this->data); } - + if ($this->Error->noErrors()) { + if (isset($this->data['Versioncomment'])) { + // autosubscribe to notifications + $threadRoot = $this->Versioncomment->getThreadRoot($id, $commentId); + if ($threadRoot) { + $this->Versioncomment->subscribe($threadRoot['Versioncomment']['id'], $session['id']); + } + + // notify subscribed editors of post + $this->Editors->versionCommentNotify($commentId, $threadRoot['Versioncomment']['id']); + + $this->flash(___('editors_comment_posted', 'Comment successfully posted'), '/editors/review/'.$id.'#editorComment'.$commentId); + return; + } + // if editor chose to be reminded of the next upcoming update, save this if ($this->data['Approval']['subscribe']) $this->EditorSubscription->subscribeToUpdates($session['id'], $addon['Addon']['id']); @@ -369,6 +388,12 @@ class EditorsController extends AppController } //pr($history); + + //Editor Comments + $comments = $this->Versioncomment->getThreadTree($version['Version']['id']); + + //pr($comments); + if ($addon['Addon']['status'] == STATUS_NOMINATED) { $reviewType = 'nominated'; @@ -395,6 +420,7 @@ class EditorsController extends AppController $this->publish('addontype', $addon['Addon']['addontype_id']); $this->publish('approval', $this->Amo->getApprovalStatus()); $this->publish('history', $history); + $this->publish('comments', $comments); $this->publish('errors', $this->Error->errors); $this->publish('reviewType', $reviewType, false); $this->publish('filtered', $filtered); diff --git a/site/app/models/versioncomment.php b/site/app/models/versioncomment.php new file mode 100644 index 0000000..cb5cb0e --- /dev/null +++ b/site/app/models/versioncomment.php @@ -0,0 +1,272 @@ +<?php +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * 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 addons.mozilla.org site. + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Scott McCammon <smccammon@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +class Versioncomment extends AppModel +{ + var $name = "Versioncomment"; + + var $belongsTo = array( + 'Version' => array( + 'className' => 'Version', + 'conditions' => '', + 'order' => '', + 'foreignKey' => 'version_id' + ), + 'User' => array( + 'className' => 'User', + 'conditions' => '', + 'order' => '', + 'foreignKey' => 'user_id', + 'fields' => array('id','email','firstname','lastname','nickname') + ), + ); + + var $belongsTo_full = array( + 'Version' => array( + 'className' => 'Version', + 'conditions' => '', + 'order' => '', + 'foreignKey' => 'version_id' + ), + 'User' => array( + 'className' => 'User', + 'conditions' => '', + 'order' => '', + 'foreignKey' => 'user_id', + 'fields' => array('id','email','firstname','lastname','nickname') + ), + ); + + var $hasAndBelongsToMany_full = array( + 'Subscriber' => array( + 'className' => 'User', + 'joinTable' => 'users_versioncomments', + 'associationForeignKey' => 'user_id', + 'foreignKey' => 'comment_id', + 'conditions' => 'users_versioncomments.subscribed=1', + 'fields' => array('id','email','firstname','lastname','nickname') + ), + ); + + var $validate = array( + 'subject' => VALID_NOT_EMPTY, + 'comment' => VALID_NOT_EMPTY + ); + + + /** + * Return all comments for a version, sorted by thread and with depth information + * + * @param int $versionId - the id of a version + * @return array of comments + */ + function getThreadTree($versionId) { + $comments = $this->findAll(array('Versioncomment.version_id' => $versionId), + null, 'Versioncomment.reply_to, Versioncomment.created'); + $tree = array(); + foreach ($comments as &$node) { + if (!is_null($node['Versioncomment']['reply_to'])) { + // sorting by reply_to puts all the root nodes at top + // once we stop seeing reply_to nulls, we're done with root nodes + break; + } + + // build a flattened tree for each root node + array_splice($tree, count($tree), 0, $this->_depthFirstSearch($node, $comments, 0)); + } + return $tree; + } + + /** + * Returns the comment at the root of the thread containing the specified comment + * + * @param int $versionId - the id of a version + * @param int $commentId - the id of a comment + * @return array or null if not found + */ + function getThreadRoot($versionId, $commentId) { + $root = null; + + // fetch all comments tied to this version + $comments = $this->findAll(array('Versioncomment.version_id' => $versionId), + null, 'Versioncomment.reply_to, Versioncomment.created', null, null, -1); + + // iteratively search for a node and its ancestors (yes, recursion would be prettier) + $nextId = $commentId; + while ($nextId) { + $searchId = $nextId; + $nextId = null; + + foreach ($comments as &$node) { + if ($node['Versioncomment']['id'] == $searchId) { + // found the node... + if (empty($node['Versioncomment']['reply_to'])) { + return $node; // ...and we have the root! + } + + // ...and we start the search over for the next ancestor + $nextId = $node['Versioncomment']['reply_to']; + break; + } + } + } + return null; + } + + /** + * Subscribes a user to a comment thread + * + * @param int $id - the id of a comment + * @param int $userId - the id of the user + * @param bool $force - subscribes unconditionally even if already unsubscribed + * @return void + */ + function subscribe($id, $userId, $force=false) { + // Perform SQL escaping. + $db =& ConnectionManager::getDataSource($this->useDbConfig); + $e_id = $db->value($id); + $e_userId = $db->value($userId); + + // check for existing subscription + $rows = $this->query("SELECT * + FROM users_versioncomments AS uvc + WHERE user_id='{$e_userId}' AND comment_id='{$e_id}'"); + + if (empty($rows)) { + $this->query("INSERT INTO users_versioncomments + (user_id, comment_id, subscribed, created, modified) + VALUES ('{$e_userId}', '{$e_id}', 1, NOW(), NOW())"); + } + elseif (!$rows[0]['uvc']['subscribed'] && $force) { + $this->query("UPDATE users_versioncomments + SET subscribed='1', modified=NOW() + WHERE user_id='{$e_userId}' AND comment_id='{$e_id}'"); + } + } + + /** + * Unsubscribe a user from a comment thread + * + * @param int $id - id of the treads root comment + * @param int $userId - id of user + * @return void + */ + function unsubscribe($id, $userId) { + // Perform SQL escaping. + $db =& ConnectionManager::getDataSource($this->useDbConfig); + $e_id = $db->value($id); + $e_userId = $db->value($userId); + + // check for existing subscription + $rows = $this->query("SELECT * + FROM users_versioncomments AS uvc + WHERE user_id='{$e_userId}' AND comment_id='{$e_id}'"); + + if (empty($rows)) { + $this->query("INSERT INTO users_versioncomments + (user_id, comment_id, subscribed, created, modified) + VALUES ('{$e_userId}', '{$e_id}', 0, NOW(), NOW())"); + } + elseif ($rows[0]['uvc']['subscribed']) { + $this->query("UPDATE users_versioncomments + SET subscribed='0', modified=NOW() + WHERE user_id='{$e_userId}' AND comment_id='{$e_id}'"); + } + } + + /** + * Get the ids of all root comments (threads) subscribed to by a user + * @param int $userId + * @return array versioncomment ids, false on error + */ + function getSubscriptionsByUser($userId) { + if (!is_numeric($userId)) return false; + + $res = $this->query("SELECT comment_id FROM users_versioncomments WHERE user_id = {$userId}"); + if (empty($res)) return array(); + + $ids = array(); + foreach ($res as &$row) $ids[] = $row['users_versioncomments']['comment_id']; + return $ids; + } + + /** + * Get the ids of all users subscribed to a thread's root comment + * @param int $commentId + * @return array user ids, false on error + */ + function getSubscribers($commentId) { + if (!is_numeric($commentId)) return false; + + $res = $this->query("SELECT user_id FROM users_versioncomments + WHERE comment_id = {$commentId} AND subscribed = 1"); + if (empty($res)) return array(); + + $ids = array(); + foreach ($res as &$row) $ids[] = $row['users_versioncomments']['user_id']; + return $ids; + } + + + /** + * Returns a comment and all decendents in its thread tree + * Tree depth is added to each node in the returned results + * @param array $node (by reference) - root comment + * @param array $allNodes (by reference) - set of all comments to search + * @param int $depth - depth of node + * @return array - flattened comment tree + */ + function _depthFirstSearch(&$node, &$allNodes, $depth) { + if (array_key_exists('depth', $node)) { + // we have already visited this node - shouldn't happen, but be safe + return array(); + } + $node['depth'] = $depth; + $toReturn = array(&$node); + foreach ($allNodes as &$subNode) { + // look for descendants , recurse, and append to results + if ($subNode['Versioncomment']['reply_to'] === $node['Versioncomment']['id']) { + // the ugly php equivalent of python's somelist.extend(anotherlist) + // "replace 0 items at the end of the array with all items in this other array" + array_splice($toReturn, count($toReturn), 0, + $this->_depthFirstSearch($subNode, $allNodes, $depth+1)); + } + } + return $toReturn; + } +} diff --git a/site/app/tests/data/remora-test-data.sql b/site/app/tests/data/remora-test-data.sql index 03c2710..a3367e0 100644 --- a/site/app/tests/data/remora-test-data.sql +++ b/site/app/tests/data/remora-test-data.sql @@ -1010,6 +1010,20 @@ INSERT INTO `versions` (`id`, `addon_id`, `version`, `approvalnotes`, `releaseno (28900, 4023, '1.0', '', 46713, '2007-02-12 22:16:19', '2007-02-12 22:16:49'), (28901, 4023, '0.9', '', 46713, '2007-02-12 22:16:19', '2007-02-20 22:16:49'); +-- +-- Dumping data for table `versioncomments` +-- +INSERT INTO `versioncomments` (`id`, `version_id`, `user_id`, `reply_to`, `subject`, `comment`, `created`, `modified`) VALUES +(1, 1, 1, NULL, 'Test comment', 'First post!', '2006-04-28 09:02:34', '2006-04-28 09:02:34'), +(2, 1, 2, 1, 're: Test comment', 'Very lame, dude.\r\n\r\n> First post!', '2006-04-28 09:05:34', '2006-04-28 09:05:34'); + +-- +-- Dumping data for table `users_versioncomments` +-- +INSERT INTO `users_versioncomments` (`user_id`, `comment_id`, `subscribed`, `created`, `modified`) VALUES +(1, 1, 0, '2006-04-28 09:02:34', '2006-04-28 09:02:34'), +(2, 1, 0, '2006-04-28 09:05:34', '2006-04-28 09:05:34'); + -- -- Set up the materialized views for search -- diff --git a/site/app/tests/models/versioncomment.test.php b/site/app/tests/models/versioncomment.test.php new file mode 100644 index 0000000..36b0265 --- /dev/null +++ b/site/app/tests/models/versioncomment.test.php @@ -0,0 +1,21 @@ +<?php + +class VersioncommentModelTest extends UnitTestCase { + + function VersioncommentModelTest() { + loadModel('Versioncomment'); + $this->Versioncomment = new Versioncomment(); + } + + function testGetThreadRoot() { + $root = $this->Versioncomment->getThreadRoot(1, 2); + $this->assertEqual($root['Versioncomment']['id'], 1, 'Expected root versioncomment of thread'); + } + + function testGetThreadTree() { + $comments = $this->Versioncomment->getThreadTree(1); + $this->assertEqual($comments[0]['Versioncomment']['id'], 1, 'Expected root versioncomment of thread'); + $this->assertEqual($comments[1]['depth'], 1, 'Expected depth of first reply'); + } +} +?> diff --git a/site/app/views/editors/email/notify_version_comment_plain.thtml b/site/app/views/editors/email/notify_version_comment_plain.thtml new file mode 100644 index 0000000..8b98869 --- /dev/null +++ b/site/app/views/editors/email/notify_version_comment_plain.thtml @@ -0,0 +1,18 @@ +Dear editor, + +An editor has commented on an add-on review. + +Comment Information: + +Add-on Name: <?=$info['addon']?> +Version Number: <?=$info['version']?> +Comment Subject: <?=$info['subject']?> +Comment Author: <?=$info['author']?> +Comment Link: <?=FULL_BASE_URL.$html->url("/editors/review/{$info['versionid']}#editorComment{$info['commentid']}")?> + + +Thanks! +Mozilla Add-ons +<?=SITE_URL?> + +This notification was sent to you because you have participated in the discussion of this review. Subscription management tools will be available in a future addons.mozilla.org release. diff --git a/site/app/views/editors/review.thtml b/site/app/views/editors/review.thtml index 8a28497..2e6b6b1 100644 --- a/site/app/views/editors/review.thtml +++ b/site/app/views/editors/review.thtml @@ -147,6 +147,7 @@ <span id="previews_link"><a href="#previews"><?=___('editors_review_a_previews')?></a></span> <?=(!empty($addon['Addon']['homepage'])) ? '<span id="homepage_link">'.$html->link(___('editors_review_a_item_homepage'), $addon['Addon']['homepage']).'</span>' : ''?> <?=($this->controller->SimpleAcl->actionAllowed('Admin', 'EditAnyAddon', $this->controller->Session->read('User'))) ? '<span id="edit_link">'.$html->link(___('editors_review_a_edit_item'), '/developers/edit/'.$addon['Addon']['id']).'</span>' : ''?> + <span id="comments_link"><a href="#editorComments"><?=___('editors_review_a_comments', 'Editor Comments')?></a></span> </div> <div id="form"> <?php @@ -323,5 +324,48 @@ if (!empty($addon['Translation']['developercomments']['string'])) { } ?> </div> +<br class="clear"> +<div id="editorComments"> + <div class="sectionHeader"> + <div class="name"><a name="editorComments"></a><?=___('editors_review_header_comments', 'Editor Comments')?></div> + <div class="top"><a href="#top"><?=_('editors_review_link_pagetop')?></a></div> + <br class="clear"> + <div><a href="#" class="expandAllThreads"><?=___('editors_review_expand_all', '(Expand All)')?></a> <a href="#" class="collapseAllThreads"><?=___('editors_review_collapse_all', '(Collapse All)')?></a></div> + </div> + <br class="clear"> + <?php foreach ($comments as $c): ?> + <div id="editorComment<?=$c['Versioncomment']['id']?>" class="editorComment commentDepth<?=$c['depth']?><?=($c['Versioncomment']['reply_to'] ? " commentReply{$c['Versioncomment']['reply_to']}" : '')?>"> + <div class="commentHeader"><?=$c['Versioncomment']['subject']?></div> + <div class="commentSubheader"> + <?="{$c['User']['firstname']} {$c['User']['lastname']} "?> + <?=$html->link("({$c['User']['email']})", "mailto:{$c['User']['email']}")?> + <?=(" - " . strftime('%c', strtotime($c['Versioncomment']['created'])))?> + </div> + <div class="commentBody"> + <?=$c['Versioncomment']['comment']?> + <div class="commentFooter"><a href="#editorComment<?=$c['Versioncomment']['id']?>" class="replyLink"><?=___('editors_review_link_reply', '(Reply)')?></a></div> + </div> + </div> + <?php endforeach; ?> + + <a href="#" class="newThreadLink"><?=___('editors_review_link_new_thread', '(New Thread)')?></a> + <?=$html->formTag('/editors/review/'.$version['Version']['id'], 'post', array('id'=>'editorCommentForm', 'class'=>'hidden'))?> + <div> + <?=$html->input('Versioncomment/subject', array('size'=>'50'))?> + <label for="VersioncommentSubject"><?=___('editors_review_label_subject', 'Subject')?></label><br /> + <?=$html->textarea('Versioncomment/comment', array('cols'=>'70', 'rows'=>'10'))?><br /> + + <?=$html->hidden('Versioncomment/reply_to', array('value'=>''))?> + <input type="submit" value="<?=___('editors_review_input_post_comment', 'Post Comment')?>" /> + <input type="button" value="<?=___('editors_review_input_cancel', 'Cancel')?>" id="VersioncommentCancel" /> + </div> + </form> </div> +<script type="text/javascript"> +// <![CDATA[ + $(document).ready(function() { + editors_review.init(); + }); +// ]]> +</script> diff --git a/site/app/webroot/css/editors.css b/site/app/webroot/css/editors.css index d7353da..71827d5 100644 --- a/site/app/webroot/css/editors.css +++ b/site/app/webroot/css/editors.css @@ -118,6 +118,29 @@ input#FilterAddonOrAuthor { width: 31%; float: left; } +.editorComment { + margin-top: 2px; + padding: 4px 6px 2px 6px; +} +.commentHeader { + font-weight: bold; + background: url('../img/icn-collapse.png') no-repeat top right; +} +.html-rtl .commentHeader { + background-position: top left; +} +.commentHeader.collapsed { + background-image: url('../img/icn-expand.png'); +} +.commentSubheader { + font-style: italic; +} +.commentFooter { + margin: 6px 0; +} +#editorCommentForm { + margin: 10px 0; +} #header2 { border-right: 1px solid #888; border-left: 1px solid #888; @@ -191,6 +214,9 @@ input#FilterAddonOrAuthor { #links #edit_link { background: url('../img/edit.png') no-repeat; } +#links #comments_link { + background: url('../img/developers/comments.png') no-repeat; +} #form { margin: 15px 50px; border: 1px solid #999; diff --git a/site/app/webroot/img/developers/comments.png b/site/app/webroot/img/developers/comments.png Binary files differnew file mode 100644 index 0000000..18f279a --- /dev/null +++ b/site/app/webroot/img/developers/comments.png diff --git a/site/app/webroot/js/editors.js b/site/app/webroot/js/editors.js index d8eab46..b7d2f76 100644 --- a/site/app/webroot/js/editors.js +++ b/site/app/webroot/js/editors.js @@ -64,6 +64,101 @@ var editors_queue = { /************************************************* * editors/review * *************************************************/ +var editors_review = { + init: function () { + // indent comment replies + var margin = 'margin-left'; + if ($('body').hasClass('html-rtl')) { + margin = 'margin-right'; + } + var depth = 1; + var comments = $('#editorComments .commentDepth'+depth); + while (comments.length > 0) { + comments.css(margin, 2*depth+'em'); + depth += 1; + comments = $('#editorComments .commentDepth'+depth); + } + + // click a comment header to toggle showing/hiding its body + $('#editorComments .commentHeader').click(this.toggleOneComment); + + // comment expand/collapse all links + $('#editorComments .expandAllThreads').click(this.expandAllComments); + $('#editorComments .collapseAllThreads').click(this.collapseAllComments); + + // show the comment form + $('#editorComments .replyLink, #editorComments .newThreadLink').click(this.showCommentForm); + + // hide the comment form + $('#VersioncommentCancel').click(function() { + $(this.form).slideUp('fast'); + }); + + // highlight any comment specified in the location hash + if (window.location.hash.match(/^#editorComment(\d+)$/)) { + $(window.location.hash+' .commentBody').show(); + $(window.location.hash).css('background-color', '#efefef'); + } + }, + + // toggle showing/hiding a single comment body + toggleOneComment: function(e) { + var commentBody = $('.commentBody', $(this).parent()); + if (! commentBody.is(':animated')) { + if (commentBody.is(':visible')) { + commentBody.slideUp('fast'); + $(this).addClass('collapsed'); + } else { + commentBody.slideDown('fast'); + $(this).removeClass('collapsed'); + } + } + return false; + }, + + // show all comment bodies + expandAllComments: function(e) { + $('#editorComments .commentBody').show(); + $('#editorComments .commentHeader').removeClass('collapsed'); + return false; + }, + + // hide all comment bodies + collapseAllComments: function(e) { + $('#editorComments .commentBody').hide(); + $('#editorComments .commentHeader').addClass('collapsed'); + return false; + }, + + // move and show the comment form under the link clicked + showCommentForm: function(e) { + var re = /^#editorComment(\d+)$/; + var match = re.exec($(this).attr('href')); + var commentId = ''; + if (match) { + commentId = match[1]; + } + + // when relocating the form, initialize subject and comment + if (! $(this).next().is('#editorCommentForm')) { + if (match) { + var subject = $('#editorComment'+commentId+' .commentHeader').text(); + subject = subject.replace(/^(re: )?/, 're: '); + $('#VersioncommentSubject').val(subject); + } else { + $('#VersioncommentSubject').val(''); + } + $('#VersioncommentComment').val(''); + $('#editorCommentForm').insertAfter(this); + } + // always initialize reply_to just to be safe + $('#VersioncommentReplyTo').val(commentId); + + $('#editorCommentForm').slideDown('fast'); + return false; + } +} + //Array of possible actions var actions = ['public', 'sandbox', 'info', 'superreview']; |