Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsmccammon@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)
commit6fb6d9888199b17dd2a0136bd87ce8cf4abccda2 (patch)
tree1e846e539bf63d9db7c2e5b2fbf25b2048a4199b
parente8056c2f02b07167a557e6f53d83cf17359374a9 (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.sql43
-rw-r--r--site/app/controllers/components/editors.php79
-rw-r--r--site/app/controllers/editors_controller.php32
-rw-r--r--site/app/models/versioncomment.php272
-rw-r--r--site/app/tests/data/remora-test-data.sql14
-rw-r--r--site/app/tests/models/versioncomment.test.php21
-rw-r--r--site/app/views/editors/email/notify_version_comment_plain.thtml18
-rw-r--r--site/app/views/editors/review.thtml44
-rw-r--r--site/app/webroot/css/editors.css26
-rw-r--r--site/app/webroot/img/developers/comments.pngbin0 -> 1806 bytes
-rw-r--r--site/app/webroot/js/editors.js95
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
new file mode 100644
index 0000000..18f279a
--- /dev/null
+++ b/site/app/webroot/img/developers/comments.png
Binary files differ
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'];