From 6bf8586735b24297c0489645198491eb6d4484b0 Mon Sep 17 00:00:00 2001
From: Marcin Kurczewski <mkurczew@gmail.com>
Date: Sun, 5 Oct 2014 16:19:08 +0200
Subject: [PATCH] Added comment scoring

---
 TODO                                          |  1 -
 public_html/css/comments.css                  | 17 +++++-
 .../js/Presenters/PostCommentListPresenter.js | 53 +++++++++++++----
 public_html/templates/comment-list-item.tpl   | 37 ++++++++----
 src/Controllers/CommentController.php         | 19 +++++--
 src/Controllers/ScoreController.php           | 17 ++++++
 .../ViewProxies/CommentViewProxy.php          | 16 ++++--
 .../CommentEntityConverter.php                |  1 +
 .../EntityConverters/ScoreEntityConverter.php |  2 +
 src/Dao/ScoreDao.php                          |  4 ++
 src/Entities/Comment.php                      |  7 +++
 src/Entities/Score.php                        | 11 ++++
 src/Services/ScoreService.php                 |  2 +-
 src/Upgrades/Upgrade16.php                    | 57 +++++++++++++++++++
 src/di.php                                    |  1 +
 15 files changed, 209 insertions(+), 36 deletions(-)
 create mode 100644 src/Upgrades/Upgrade16.php

diff --git a/TODO b/TODO
index 03f12696..b8da2db7 100644
--- a/TODO
+++ b/TODO
@@ -36,7 +36,6 @@ everything related to tags:
 
 everything related to comments:
     - markdown
-    - score
 
 refactors:
     - add enum validation in IValidatables (needs refactors of enums and
diff --git a/public_html/css/comments.css b/public_html/css/comments.css
index 41ac6de7..5a92e5e8 100644
--- a/public_html/css/comments.css
+++ b/public_html/css/comments.css
@@ -39,19 +39,30 @@
 	line-height: 16pt;
 	vertical-align: middle;
 }
+.comment .score,
 .comment .date {
 	color: silver;
-	font-size: 80%;
+	font-size: 90%;
 	padding-left: 0.5em;
 }
+.comment .score-up.active,
+.comment .score-down.active {
+	font-weight: bold;
+}
 .comment .header .ops a {
 	color: silver;
 	font-size: 80%;
+	cursor: pointer;
 }
-.comment .header .ops a:before {
+.comment .header .ops a:first-of-type:before {
+	margin-left: 0.5em;
 	content: '[';
 }
-.comment .header .ops a:after {
+.comment .header .ops a:not(:first-of-type):before {
+	content: '|';
+	margin: 0 0.3em;
+}
+.comment .header .ops a:last-of-type:after {
 	content: ']';
 }
 
diff --git a/public_html/js/Presenters/PostCommentListPresenter.js b/public_html/js/Presenters/PostCommentListPresenter.js
index 1e6b7105..b9305447 100644
--- a/public_html/js/Presenters/PostCommentListPresenter.js
+++ b/public_html/js/Presenters/PostCommentListPresenter.js
@@ -97,18 +97,31 @@ App.Presenters.PostCommentListPresenter = function(
 			comment: comment,
 			formatRelativeTime: util.formatRelativeTime,
 			formatMarkdown: util.formatMarkdown,
+			canVote: auth.isLoggedIn(),
 			canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.editOwnComments : privileges.editAllComments,
 			canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.deleteOwnComments : privileges.deleteAllComments,
 		}) + '</li>');
 		util.loadImagesNicely($item.find('img'));
 		$targetList.append($item);
+
 		$item.find('a.edit').click(function(e) {
 			e.preventDefault();
 			editCommentStart($item, comment);
 		});
+
 		$item.find('a.delete').click(function(e) {
 			e.preventDefault();
-			deleteComment($item, comment);
+			deleteComment(comment);
+		});
+
+		$item.find('a.score-up').click(function(e) {
+			e.preventDefault();
+			score(comment, jQuery(this).hasClass('active') ? 0 : 1);
+		});
+
+		$item.find('a.score-down').click(function(e) {
+			e.preventDefault();
+			score(comment, jQuery(this).hasClass('active') ? 0 : -1);
 		});
 	}
 
@@ -132,6 +145,16 @@ App.Presenters.PostCommentListPresenter = function(
 		});
 	}
 
+	function updateComment(comment) {
+		comments = _.map(comments, function(c) { return c.id === comment.id ? comment : c; });
+		render();
+	}
+
+	function addComment(comment) {
+		comments.push(comment);
+		render();
+	}
+
 	function submitComment($form, commentToEdit) {
 		$form.find('.preview').slideUp();
 		var $textarea = $form.find('textarea');
@@ -152,14 +175,11 @@ App.Presenters.PostCommentListPresenter = function(
 				$form.slideUp(function() {
 					$form.remove();
 				});
-				comments = _.map(comments, function(c) { return c.id === commentToEdit.id ? comment : c; });
+				updateComment(comment);
 			} else {
-				comments.push(comment);
+				addComment(comment);
 			}
-			render();
-		}).fail(function(response) {
-			window.alert(response.json && response.json.error || response);
-		});
+		}).fail(showGenericError);
 	}
 
 	function editCommentStart($item, comment) {
@@ -171,7 +191,7 @@ App.Presenters.PostCommentListPresenter = function(
 		$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
 	}
 
-	function deleteComment($item, comment) {
+	function deleteComment(comment) {
 		if (!window.confirm('Are you sure you want to delete this comment?')) {
 			return;
 		}
@@ -179,9 +199,20 @@ App.Presenters.PostCommentListPresenter = function(
 			.then(function(response) {
 				comments = _.filter(comments, function(c) { return c.id !== comment.id; });
 				renderComments(comments, true);
-			}).fail(function(response) {
-				window.alert(response.json && response.json.error || response);
-			});
+			}).fail(showGenericError);
+	}
+
+	function score(comment, scoreValue) {
+		promise.wait(api.post('/comments/' + comment.id + '/score', {score: scoreValue}))
+			.then(function(response) {
+				comment.score = response.json.score;
+				comment.ownScore = parseInt(response.json.score);
+				updateComment(comment);
+			}).fail(showGenericError);
+	}
+
+	function showGenericError(response) {
+		window.alert(response.json && response.json.error || response);
 	}
 
 	return {
diff --git a/public_html/templates/comment-list-item.tpl b/public_html/templates/comment-list-item.tpl
index 86421f3d..14e45eb2 100644
--- a/public_html/templates/comment-list-item.tpl
+++ b/public_html/templates/comment-list-item.tpl
@@ -31,19 +31,32 @@
 				<%= formatRelativeTime(comment.creationTime) %>
 			</span>
 
-			<span class="ops">
-				<% if (canEditComment) { %>
-					<a class="edit" href="#"><!--
-						-->edit<!--
-					--></a>
-				<% } %>
-
-				<% if (canDeleteComment) { %>
-					<a class="delete" href="#"><!--
-						-->delete<!--
-					--></a>
-				<% } %>
+			<span class="score">
+				Score: <%= comment.score %>
 			</span>
+
+			<span class="ops"><!--
+				--><% if (canVote) { %><!--
+					--><a class="score-up <% print(comment.ownScore === 1 ? 'active' : '') %>"><!--
+						-->vote up<!--
+					--></a><!--
+					--><a class="score-down <% print(comment.ownScore === -1 ? 'active' : '') %>"><!--
+						-->vote down<!--
+					--></a><!--
+				--><% } %><!--
+
+				--><% if (canEditComment) { %><!--
+					--><a class="edit"><!--
+						-->edit<!--
+					--></a><!--
+				--><% } %><!--
+
+				--><% if (canDeleteComment) { %><!--
+					--><a class="delete"><!--
+						-->delete<!--
+					--></a><!--
+				--><% } %><!--
+			--></span>
 		</div>
 
 		<div class="content">
diff --git a/src/Controllers/CommentController.php b/src/Controllers/CommentController.php
index b35cb0e3..88581f67 100644
--- a/src/Controllers/CommentController.php
+++ b/src/Controllers/CommentController.php
@@ -63,7 +63,10 @@ class CommentController extends AbstractController
 		{
 			$data[] = [
 				'post' => $this->postViewProxy->fromEntity($post),
-				'comments' => $this->commentViewProxy->fromArray($this->commentService->getByPost($post))];
+				'comments' => $this->commentViewProxy->fromArray(
+					$this->commentService->getByPost($post),
+					$this->getCommentsFetchConfig()),
+			];
 		}
 
 		return [
@@ -88,7 +91,7 @@ class CommentController extends AbstractController
 		$filter->addRequirement($requirement);
 
 		$result = $this->commentService->getFiltered($filter);
-		$entities = $this->commentViewProxy->fromArray($result->getEntities());
+		$entities = $this->commentViewProxy->fromArray($result->getEntities(), $this->getCommentsFetchConfig());
 		return ['data' => $entities];
 	}
 
@@ -98,7 +101,7 @@ class CommentController extends AbstractController
 
 		$post = $this->postService->getByNameOrId($postNameOrId);
 		$comment = $this->commentService->createComment($post, $this->inputReader->text);
-		return $this->commentViewProxy->fromEntity($comment);
+		return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
 	}
 
 	public function editComment($commentId)
@@ -111,7 +114,7 @@ class CommentController extends AbstractController
 				: \Szurubooru\Privilege::EDIT_ALL_COMMENTS);
 
 		$comment = $this->commentService->updateComment($comment, $this->inputReader->text);
-		return $this->commentViewProxy->fromEntity($comment);
+		return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
 	}
 
 	public function deleteComment($commentId)
@@ -125,4 +128,12 @@ class CommentController extends AbstractController
 
 		return $this->commentService->deleteComment($comment);
 	}
+
+	private function getCommentsFetchConfig()
+	{
+		return
+		[
+			\Szurubooru\Controllers\ViewProxies\CommentViewProxy::FETCH_OWN_SCORE => true,
+		];
+	}
 }
diff --git a/src/Controllers/ScoreController.php b/src/Controllers/ScoreController.php
index dd1b2e79..ca6afc87 100644
--- a/src/Controllers/ScoreController.php
+++ b/src/Controllers/ScoreController.php
@@ -6,6 +6,7 @@ class ScoreController extends AbstractController
 	private $privilegeService;
 	private $authService;
 	private $postService;
+	private $commentService;
 	private $scoreService;
 	private $inputReader;
 
@@ -13,12 +14,14 @@ class ScoreController extends AbstractController
 		\Szurubooru\Services\PrivilegeService $privilegeService,
 		\Szurubooru\Services\AuthService $authService,
 		\Szurubooru\Services\PostService $postService,
+		\Szurubooru\Services\CommentService $commentService,
 		\Szurubooru\Services\ScoreService $scoreService,
 		\Szurubooru\Helpers\InputReader $inputReader)
 	{
 		$this->privilegeService = $privilegeService;
 		$this->authService = $authService;
 		$this->postService = $postService;
+		$this->commentService = $commentService;
 		$this->scoreService = $scoreService;
 		$this->inputReader = $inputReader;
 	}
@@ -27,6 +30,8 @@ class ScoreController extends AbstractController
 	{
 		$router->get('/api/posts/:postNameOrId/score', [$this, 'getPostScore']);
 		$router->post('/api/posts/:postNameOrId/score', [$this, 'setPostScore']);
+		$router->get('/api/comments/:commentId/score', [$this, 'getCommentScore']);
+		$router->post('/api/comments/:commentId/score', [$this, 'setCommentScore']);
 	}
 
 	public function getPostScore($postNameOrId)
@@ -41,6 +46,18 @@ class ScoreController extends AbstractController
 		return $this->setScore($post);
 	}
 
+	public function getCommentScore($commentId)
+	{
+		$comment = $this->commentService->getById($commentId);
+		return $this->getScore($comment);
+	}
+
+	public function setCommentScore($commentId)
+	{
+		$comment = $this->commentService->getById($commentId);
+		return $this->setScore($comment);
+	}
+
 	private function setScore(\Szurubooru\Entities\Entity $entity)
 	{
 		$this->privilegeService->assertLoggedIn();
diff --git a/src/Controllers/ViewProxies/CommentViewProxy.php b/src/Controllers/ViewProxies/CommentViewProxy.php
index f905b4ac..a26a9a4d 100644
--- a/src/Controllers/ViewProxies/CommentViewProxy.php
+++ b/src/Controllers/ViewProxies/CommentViewProxy.php
@@ -3,14 +3,19 @@ namespace Szurubooru\Controllers\ViewProxies;
 
 class CommentViewProxy extends AbstractViewProxy
 {
-	private $postViewProxy;
+	private $authService;
+	private $scoreService;
 	private $userViewProxy;
 
+	const FETCH_OWN_SCORE = 'fetchOwnScore';
+
 	public function __construct(
-		PostViewProxy $postViewProxy,
+		\Szurubooru\Services\AuthService $authService,
+		\Szurubooru\Services\ScoreService $scoreService,
 		UserViewProxy $userViewProxy)
 	{
-		$this->postViewProxy = $postViewProxy;
+		$this->authService = $authService;
+		$this->scoreService = $scoreService;
 		$this->userViewProxy = $userViewProxy;
 	}
 
@@ -25,8 +30,11 @@ class CommentViewProxy extends AbstractViewProxy
 			$result->text = $comment->getText();
 			$result->postId = $comment->getPostId();
 			$result->user = $this->userViewProxy->fromEntity($comment->getUser());
+			$result->score = $comment->getScore();
+
+			if (!empty($config[self::FETCH_OWN_SCORE]) and $this->authService->isLoggedIn())
+				$result->ownScore = $this->scoreService->getScoreValue($this->authService->getLoggedInUser(), $comment);
 		}
 		return $result;
 	}
 }
-
diff --git a/src/Dao/EntityConverters/CommentEntityConverter.php b/src/Dao/EntityConverters/CommentEntityConverter.php
index c39363c2..3335a217 100644
--- a/src/Dao/EntityConverters/CommentEntityConverter.php
+++ b/src/Dao/EntityConverters/CommentEntityConverter.php
@@ -24,6 +24,7 @@ class CommentEntityConverter extends AbstractEntityConverter implements IEntityC
 		$entity->setText($array['text']);
 		$entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
 		$entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime']));
+		$entity->setMeta(\Szurubooru\Entities\Comment::META_SCORE, intval($array['score']));
 		return $entity;
 	}
 }
diff --git a/src/Dao/EntityConverters/ScoreEntityConverter.php b/src/Dao/EntityConverters/ScoreEntityConverter.php
index 62e74676..ce7ec43b 100644
--- a/src/Dao/EntityConverters/ScoreEntityConverter.php
+++ b/src/Dao/EntityConverters/ScoreEntityConverter.php
@@ -10,6 +10,7 @@ class ScoreEntityConverter extends AbstractEntityConverter implements IEntityCon
 			'id' => $entity->getId(),
 			'userId' => $entity->getUserId(),
 			'postId' => $entity->getPostId(),
+			'commentId' => $entity->getCommentId(),
 			'time' => $this->entityTimeToDbTime($entity->getTime()),
 			'score' => $entity->getScore(),
 		];
@@ -20,6 +21,7 @@ class ScoreEntityConverter extends AbstractEntityConverter implements IEntityCon
 		$entity = new \Szurubooru\Entities\Score($array['id']);
 		$entity->setUserId($array['userId']);
 		$entity->setPostId($array['postId']);
+		$entity->setCommentId($array['commentId']);
 		$entity->setTime($this->dbTimeToEntityTime($array['time']));
 		$entity->setScore(intval($array['score']));
 		return $entity;
diff --git a/src/Dao/ScoreDao.php b/src/Dao/ScoreDao.php
index 44549506..5b43f596 100644
--- a/src/Dao/ScoreDao.php
+++ b/src/Dao/ScoreDao.php
@@ -23,6 +23,8 @@ class ScoreDao extends AbstractDao implements ICrudDao
 
 		if ($entity instanceof \Szurubooru\Entities\Post)
 			$query->where('postId', $entity->getId());
+		elseif ($entity instanceof \Szurubooru\Entities\Comment)
+			$query->where('commentId', $entity->getId());
 		else
 			throw new \InvalidArgumentException();
 
@@ -42,6 +44,8 @@ class ScoreDao extends AbstractDao implements ICrudDao
 
 			if ($entity instanceof \Szurubooru\Entities\Post)
 				$score->setPostId($entity->getId());
+			elseif ($entity instanceof \Szurubooru\Entities\Comment)
+				$score->setCommentId($entity->getId());
 			else
 				throw new \InvalidArgumentException();
 		}
diff --git a/src/Entities/Comment.php b/src/Entities/Comment.php
index 4857e0fa..87654204 100644
--- a/src/Entities/Comment.php
+++ b/src/Entities/Comment.php
@@ -12,6 +12,8 @@ class Comment extends Entity
 	const LAZY_LOADER_USER = 'user';
 	const LAZY_LOADER_POST = 'post';
 
+	const META_SCORE = 'score';
+
 	public function getUserId()
 	{
 		return $this->userId;
@@ -83,4 +85,9 @@ class Comment extends Entity
 		$this->lazySave(self::LAZY_LOADER_POST, $post);
 		$this->postId = $post->getId();
 	}
+
+	public function getScore()
+	{
+		return $this->getMeta(self::META_SCORE, 0);
+	}
 }
diff --git a/src/Entities/Score.php b/src/Entities/Score.php
index 459117bc..e9aa188d 100644
--- a/src/Entities/Score.php
+++ b/src/Entities/Score.php
@@ -5,6 +5,7 @@ class Score extends Entity
 {
 	private $postId;
 	private $userId;
+	private $commentId;
 	private $time;
 	private $score;
 
@@ -28,6 +29,16 @@ class Score extends Entity
 		$this->postId = $postId;
 	}
 
+	public function getCommentId()
+	{
+		return $this->commentId;
+	}
+
+	public function setCommentId($commentId)
+	{
+		$this->commentId = $commentId;
+	}
+
 	public function getTime()
 	{
 		return $this->time;
diff --git a/src/Services/ScoreService.php b/src/Services/ScoreService.php
index cc844274..f4361995 100644
--- a/src/Services/ScoreService.php
+++ b/src/Services/ScoreService.php
@@ -47,7 +47,7 @@ class ScoreService
 
 		$transactionFunc = function() use ($user, $entity, $scoreValue)
 		{
-			if ($scoreValue !== 1)
+			if (($scoreValue !== 1) and ($entity instanceof \Szurubooru\Entities\Post))
 				$this->favoritesDao->delete($user, $entity);
 
 			return $this->scoreDao->setScore($user, $entity, $scoreValue);
diff --git a/src/Upgrades/Upgrade16.php b/src/Upgrades/Upgrade16.php
new file mode 100644
index 00000000..2304c833
--- /dev/null
+++ b/src/Upgrades/Upgrade16.php
@@ -0,0 +1,57 @@
+<?php
+namespace Szurubooru\Upgrades;
+
+class Upgrade16 implements IUpgrade
+{
+	public function run(\Szurubooru\DatabaseConnection $databaseConnection)
+	{
+		$pdo = $databaseConnection->getPDO();
+
+		$pdo->exec('ALTER TABLE scores ADD COLUMN commentId INTEGER');
+		$pdo->exec('ALTER TABLE comments ADD COLUMN score INTEGER NOT NULL DEFAULT 0');
+
+		$pdo->exec('DROP TRIGGER IF EXISTS scoresDelete');
+		$pdo->exec('DROP TRIGGER IF EXISTS scoresInsert');
+		$pdo->exec('DROP TRIGGER IF EXISTS scoresUpdate');
+
+		$pdo->exec('
+			CREATE TRIGGER scoresDelete AFTER DELETE ON scores
+			FOR EACH ROW
+			BEGIN
+				UPDATE posts SET
+					score = (SELECT SUM(score) FROM scores WHERE scores.postId = posts.id)
+				WHERE posts.id = OLD.postId;
+
+				UPDATE comments SET
+					score = (SELECT SUM(score) FROM scores WHERE scores.commentId = comments.id)
+				WHERE comments.id = OLD.commentId;
+			END');
+
+		$pdo->exec('
+			CREATE TRIGGER scoresInsert AFTER INSERT ON scores
+			FOR EACH ROW
+			BEGIN
+				UPDATE posts SET
+					score = (SELECT SUM(score) FROM scores WHERE scores.postId = posts.id)
+				WHERE posts.id = NEW.postId;
+
+				UPDATE comments SET
+					score = (SELECT SUM(score) FROM scores WHERE scores.commentId = comments.id)
+				WHERE comments.id = NEW.commentId;
+			END');
+
+		$pdo->exec('
+			CREATE TRIGGER scoresUpdate AFTER UPDATE ON scores
+			FOR EACH ROW
+			BEGIN
+				UPDATE posts SET
+					score = (SELECT SUM(score) FROM scores WHERE scores.postId = posts.id)
+				WHERE posts.id IN (OLD.postId, NEW.postId);
+
+				UPDATE comments SET
+					score = (SELECT SUM(score) FROM scores WHERE scores.commentId = comments.id)
+				WHERE comments.id IN (OLD.commentId, NEW.commentId);
+			END');
+	}
+}
+
diff --git a/src/di.php b/src/di.php
index 31869f61..0dcfddaa 100644
--- a/src/di.php
+++ b/src/di.php
@@ -31,6 +31,7 @@ return [
 			$container->get(\Szurubooru\Upgrades\Upgrade13::class),
 			$container->get(\Szurubooru\Upgrades\Upgrade14::class),
 			$container->get(\Szurubooru\Upgrades\Upgrade15::class),
+			$container->get(\Szurubooru\Upgrades\Upgrade16::class),
 		];
 	}),