Added comment scoring

This commit is contained in:
Marcin Kurczewski 2014-10-05 16:19:08 +02:00
parent 28e87dca93
commit 6bf8586735
15 changed files with 209 additions and 36 deletions

1
TODO
View file

@ -36,7 +36,6 @@ everything related to tags:
everything related to comments: everything related to comments:
- markdown - markdown
- score
refactors: refactors:
- add enum validation in IValidatables (needs refactors of enums and - add enum validation in IValidatables (needs refactors of enums and

View file

@ -39,19 +39,30 @@
line-height: 16pt; line-height: 16pt;
vertical-align: middle; vertical-align: middle;
} }
.comment .score,
.comment .date { .comment .date {
color: silver; color: silver;
font-size: 80%; font-size: 90%;
padding-left: 0.5em; padding-left: 0.5em;
} }
.comment .score-up.active,
.comment .score-down.active {
font-weight: bold;
}
.comment .header .ops a { .comment .header .ops a {
color: silver; color: silver;
font-size: 80%; font-size: 80%;
cursor: pointer;
} }
.comment .header .ops a:before { .comment .header .ops a:first-of-type:before {
margin-left: 0.5em;
content: '['; 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: ']'; content: ']';
} }

View file

@ -97,18 +97,31 @@ App.Presenters.PostCommentListPresenter = function(
comment: comment, comment: comment,
formatRelativeTime: util.formatRelativeTime, formatRelativeTime: util.formatRelativeTime,
formatMarkdown: util.formatMarkdown, formatMarkdown: util.formatMarkdown,
canVote: auth.isLoggedIn(),
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.editOwnComments : privileges.editAllComments, canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.editOwnComments : privileges.editAllComments,
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.deleteOwnComments : privileges.deleteAllComments, canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.deleteOwnComments : privileges.deleteAllComments,
}) + '</li>'); }) + '</li>');
util.loadImagesNicely($item.find('img')); util.loadImagesNicely($item.find('img'));
$targetList.append($item); $targetList.append($item);
$item.find('a.edit').click(function(e) { $item.find('a.edit').click(function(e) {
e.preventDefault(); e.preventDefault();
editCommentStart($item, comment); editCommentStart($item, comment);
}); });
$item.find('a.delete').click(function(e) { $item.find('a.delete').click(function(e) {
e.preventDefault(); 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) { function submitComment($form, commentToEdit) {
$form.find('.preview').slideUp(); $form.find('.preview').slideUp();
var $textarea = $form.find('textarea'); var $textarea = $form.find('textarea');
@ -152,14 +175,11 @@ App.Presenters.PostCommentListPresenter = function(
$form.slideUp(function() { $form.slideUp(function() {
$form.remove(); $form.remove();
}); });
comments = _.map(comments, function(c) { return c.id === commentToEdit.id ? comment : c; }); updateComment(comment);
} else { } else {
comments.push(comment); addComment(comment);
} }
render(); }).fail(showGenericError);
}).fail(function(response) {
window.alert(response.json && response.json.error || response);
});
} }
function editCommentStart($item, comment) { function editCommentStart($item, comment) {
@ -171,7 +191,7 @@ App.Presenters.PostCommentListPresenter = function(
$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); }); $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?')) { if (!window.confirm('Are you sure you want to delete this comment?')) {
return; return;
} }
@ -179,9 +199,20 @@ App.Presenters.PostCommentListPresenter = function(
.then(function(response) { .then(function(response) {
comments = _.filter(comments, function(c) { return c.id !== comment.id; }); comments = _.filter(comments, function(c) { return c.id !== comment.id; });
renderComments(comments, true); renderComments(comments, true);
}).fail(function(response) { }).fail(showGenericError);
window.alert(response.json && response.json.error || response); }
});
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 { return {

View file

@ -31,19 +31,32 @@
<%= formatRelativeTime(comment.creationTime) %> <%= formatRelativeTime(comment.creationTime) %>
</span> </span>
<span class="ops"> <span class="score">
<% if (canEditComment) { %> Score: <%= comment.score %>
<a class="edit" href="#"><!--
-->edit<!--
--></a>
<% } %>
<% if (canDeleteComment) { %>
<a class="delete" href="#"><!--
-->delete<!--
--></a>
<% } %>
</span> </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>
<div class="content"> <div class="content">

View file

@ -63,7 +63,10 @@ class CommentController extends AbstractController
{ {
$data[] = [ $data[] = [
'post' => $this->postViewProxy->fromEntity($post), 'post' => $this->postViewProxy->fromEntity($post),
'comments' => $this->commentViewProxy->fromArray($this->commentService->getByPost($post))]; 'comments' => $this->commentViewProxy->fromArray(
$this->commentService->getByPost($post),
$this->getCommentsFetchConfig()),
];
} }
return [ return [
@ -88,7 +91,7 @@ class CommentController extends AbstractController
$filter->addRequirement($requirement); $filter->addRequirement($requirement);
$result = $this->commentService->getFiltered($filter); $result = $this->commentService->getFiltered($filter);
$entities = $this->commentViewProxy->fromArray($result->getEntities()); $entities = $this->commentViewProxy->fromArray($result->getEntities(), $this->getCommentsFetchConfig());
return ['data' => $entities]; return ['data' => $entities];
} }
@ -98,7 +101,7 @@ class CommentController extends AbstractController
$post = $this->postService->getByNameOrId($postNameOrId); $post = $this->postService->getByNameOrId($postNameOrId);
$comment = $this->commentService->createComment($post, $this->inputReader->text); $comment = $this->commentService->createComment($post, $this->inputReader->text);
return $this->commentViewProxy->fromEntity($comment); return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
} }
public function editComment($commentId) public function editComment($commentId)
@ -111,7 +114,7 @@ class CommentController extends AbstractController
: \Szurubooru\Privilege::EDIT_ALL_COMMENTS); : \Szurubooru\Privilege::EDIT_ALL_COMMENTS);
$comment = $this->commentService->updateComment($comment, $this->inputReader->text); $comment = $this->commentService->updateComment($comment, $this->inputReader->text);
return $this->commentViewProxy->fromEntity($comment); return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
} }
public function deleteComment($commentId) public function deleteComment($commentId)
@ -125,4 +128,12 @@ class CommentController extends AbstractController
return $this->commentService->deleteComment($comment); return $this->commentService->deleteComment($comment);
} }
private function getCommentsFetchConfig()
{
return
[
\Szurubooru\Controllers\ViewProxies\CommentViewProxy::FETCH_OWN_SCORE => true,
];
}
} }

View file

@ -6,6 +6,7 @@ class ScoreController extends AbstractController
private $privilegeService; private $privilegeService;
private $authService; private $authService;
private $postService; private $postService;
private $commentService;
private $scoreService; private $scoreService;
private $inputReader; private $inputReader;
@ -13,12 +14,14 @@ class ScoreController extends AbstractController
\Szurubooru\Services\PrivilegeService $privilegeService, \Szurubooru\Services\PrivilegeService $privilegeService,
\Szurubooru\Services\AuthService $authService, \Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\PostService $postService, \Szurubooru\Services\PostService $postService,
\Szurubooru\Services\CommentService $commentService,
\Szurubooru\Services\ScoreService $scoreService, \Szurubooru\Services\ScoreService $scoreService,
\Szurubooru\Helpers\InputReader $inputReader) \Szurubooru\Helpers\InputReader $inputReader)
{ {
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
$this->authService = $authService; $this->authService = $authService;
$this->postService = $postService; $this->postService = $postService;
$this->commentService = $commentService;
$this->scoreService = $scoreService; $this->scoreService = $scoreService;
$this->inputReader = $inputReader; $this->inputReader = $inputReader;
} }
@ -27,6 +30,8 @@ class ScoreController extends AbstractController
{ {
$router->get('/api/posts/:postNameOrId/score', [$this, 'getPostScore']); $router->get('/api/posts/:postNameOrId/score', [$this, 'getPostScore']);
$router->post('/api/posts/:postNameOrId/score', [$this, 'setPostScore']); $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) public function getPostScore($postNameOrId)
@ -41,6 +46,18 @@ class ScoreController extends AbstractController
return $this->setScore($post); 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) private function setScore(\Szurubooru\Entities\Entity $entity)
{ {
$this->privilegeService->assertLoggedIn(); $this->privilegeService->assertLoggedIn();

View file

@ -3,14 +3,19 @@ namespace Szurubooru\Controllers\ViewProxies;
class CommentViewProxy extends AbstractViewProxy class CommentViewProxy extends AbstractViewProxy
{ {
private $postViewProxy; private $authService;
private $scoreService;
private $userViewProxy; private $userViewProxy;
const FETCH_OWN_SCORE = 'fetchOwnScore';
public function __construct( public function __construct(
PostViewProxy $postViewProxy, \Szurubooru\Services\AuthService $authService,
\Szurubooru\Services\ScoreService $scoreService,
UserViewProxy $userViewProxy) UserViewProxy $userViewProxy)
{ {
$this->postViewProxy = $postViewProxy; $this->authService = $authService;
$this->scoreService = $scoreService;
$this->userViewProxy = $userViewProxy; $this->userViewProxy = $userViewProxy;
} }
@ -25,8 +30,11 @@ class CommentViewProxy extends AbstractViewProxy
$result->text = $comment->getText(); $result->text = $comment->getText();
$result->postId = $comment->getPostId(); $result->postId = $comment->getPostId();
$result->user = $this->userViewProxy->fromEntity($comment->getUser()); $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; return $result;
} }
} }

View file

@ -24,6 +24,7 @@ class CommentEntityConverter extends AbstractEntityConverter implements IEntityC
$entity->setText($array['text']); $entity->setText($array['text']);
$entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime'])); $entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
$entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime'])); $entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime']));
$entity->setMeta(\Szurubooru\Entities\Comment::META_SCORE, intval($array['score']));
return $entity; return $entity;
} }
} }

View file

@ -10,6 +10,7 @@ class ScoreEntityConverter extends AbstractEntityConverter implements IEntityCon
'id' => $entity->getId(), 'id' => $entity->getId(),
'userId' => $entity->getUserId(), 'userId' => $entity->getUserId(),
'postId' => $entity->getPostId(), 'postId' => $entity->getPostId(),
'commentId' => $entity->getCommentId(),
'time' => $this->entityTimeToDbTime($entity->getTime()), 'time' => $this->entityTimeToDbTime($entity->getTime()),
'score' => $entity->getScore(), 'score' => $entity->getScore(),
]; ];
@ -20,6 +21,7 @@ class ScoreEntityConverter extends AbstractEntityConverter implements IEntityCon
$entity = new \Szurubooru\Entities\Score($array['id']); $entity = new \Szurubooru\Entities\Score($array['id']);
$entity->setUserId($array['userId']); $entity->setUserId($array['userId']);
$entity->setPostId($array['postId']); $entity->setPostId($array['postId']);
$entity->setCommentId($array['commentId']);
$entity->setTime($this->dbTimeToEntityTime($array['time'])); $entity->setTime($this->dbTimeToEntityTime($array['time']));
$entity->setScore(intval($array['score'])); $entity->setScore(intval($array['score']));
return $entity; return $entity;

View file

@ -23,6 +23,8 @@ class ScoreDao extends AbstractDao implements ICrudDao
if ($entity instanceof \Szurubooru\Entities\Post) if ($entity instanceof \Szurubooru\Entities\Post)
$query->where('postId', $entity->getId()); $query->where('postId', $entity->getId());
elseif ($entity instanceof \Szurubooru\Entities\Comment)
$query->where('commentId', $entity->getId());
else else
throw new \InvalidArgumentException(); throw new \InvalidArgumentException();
@ -42,6 +44,8 @@ class ScoreDao extends AbstractDao implements ICrudDao
if ($entity instanceof \Szurubooru\Entities\Post) if ($entity instanceof \Szurubooru\Entities\Post)
$score->setPostId($entity->getId()); $score->setPostId($entity->getId());
elseif ($entity instanceof \Szurubooru\Entities\Comment)
$score->setCommentId($entity->getId());
else else
throw new \InvalidArgumentException(); throw new \InvalidArgumentException();
} }

View file

@ -12,6 +12,8 @@ class Comment extends Entity
const LAZY_LOADER_USER = 'user'; const LAZY_LOADER_USER = 'user';
const LAZY_LOADER_POST = 'post'; const LAZY_LOADER_POST = 'post';
const META_SCORE = 'score';
public function getUserId() public function getUserId()
{ {
return $this->userId; return $this->userId;
@ -83,4 +85,9 @@ class Comment extends Entity
$this->lazySave(self::LAZY_LOADER_POST, $post); $this->lazySave(self::LAZY_LOADER_POST, $post);
$this->postId = $post->getId(); $this->postId = $post->getId();
} }
public function getScore()
{
return $this->getMeta(self::META_SCORE, 0);
}
} }

View file

@ -5,6 +5,7 @@ class Score extends Entity
{ {
private $postId; private $postId;
private $userId; private $userId;
private $commentId;
private $time; private $time;
private $score; private $score;
@ -28,6 +29,16 @@ class Score extends Entity
$this->postId = $postId; $this->postId = $postId;
} }
public function getCommentId()
{
return $this->commentId;
}
public function setCommentId($commentId)
{
$this->commentId = $commentId;
}
public function getTime() public function getTime()
{ {
return $this->time; return $this->time;

View file

@ -47,7 +47,7 @@ class ScoreService
$transactionFunc = function() use ($user, $entity, $scoreValue) $transactionFunc = function() use ($user, $entity, $scoreValue)
{ {
if ($scoreValue !== 1) if (($scoreValue !== 1) and ($entity instanceof \Szurubooru\Entities\Post))
$this->favoritesDao->delete($user, $entity); $this->favoritesDao->delete($user, $entity);
return $this->scoreDao->setScore($user, $entity, $scoreValue); return $this->scoreDao->setScore($user, $entity, $scoreValue);

View file

@ -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');
}
}

View file

@ -31,6 +31,7 @@ return [
$container->get(\Szurubooru\Upgrades\Upgrade13::class), $container->get(\Szurubooru\Upgrades\Upgrade13::class),
$container->get(\Szurubooru\Upgrades\Upgrade14::class), $container->get(\Szurubooru\Upgrades\Upgrade14::class),
$container->get(\Szurubooru\Upgrades\Upgrade15::class), $container->get(\Szurubooru\Upgrades\Upgrade15::class),
$container->get(\Szurubooru\Upgrades\Upgrade16::class),
]; ];
}), }),