diff --git a/Voting/addon.json b/Voting/addon.json
new file mode 100644
index 0000000..bd643c5
--- /dev/null
+++ b/Voting/addon.json
@@ -0,0 +1,22 @@
+{
+ "key": "Voting",
+ "name": "Voting",
+ "description": "This plugin allows users to vote for comments and discussions.",
+ "version": "1.0.0",
+ "documentationUrl": "http://discussions.topcoder.com",
+ "type": "addon",
+ "priority": "100",
+ "icon": "topcoder-logo.jpeg",
+ "mobileFriendly": true,
+ "hasLocale": false,
+ "authors": [
+ {
+ "name": "Topcoder",
+ "email": "support@topcoder.com",
+ "homepage": "https://topcoder.com"
+ }
+ ],
+ "require": {
+ "vanilla": ">=3.0"
+ }
+}
diff --git a/Voting/class.voting.plugin.php b/Voting/class.voting.plugin.php
new file mode 100644
index 0000000..a4bbce7
--- /dev/null
+++ b/Voting/class.voting.plugin.php
@@ -0,0 +1,201 @@
+AddCSSFile('voting.css', 'plugins/Voting');
+ $Sender->AddJSFile('voting.js', 'plugins/Voting');
+ }
+
+ public function addVotingBox($sender, $args) {
+ $session = Gdn::Session();
+ $object = $args['Object'];
+ $VoteType = $args['Type'] == 'Discussion' ? 'votediscussion' : 'votecomment';
+ $id = $args['Type'] == 'Discussion' ? val('DiscussionID', $object) : val('CommentID', $object);
+ $score = val('Score', $object);
+ $cssClass = '';
+ $voteUpUrl = '/discussion/'.$VoteType.'/'.$id.'/voteup/'.$session->TransientKey().'/';
+ $voteDownUrl = '/discussion/'.$VoteType.'/'.$id.'/votedown/'.$session->TransientKey().'/';
+ if (!$session->IsValid()) {
+ $voteUpUrl = Gdn::Authenticator()->SignInUrl($sender->SelfUrl);
+ $voteDownUrl = $voteUpUrl;
+ $cssClass = ' SignInPopup';
+ }
+
+ if($args['Type'] == 'Discussion') {
+ $discussionModel = new DiscussionModel();
+ $currentUserVote = $discussionModel->GetUserScore($id, $session->UserID);
+ } else {
+ $commentModel = new CommentModel();
+ $currentUserVote = $commentModel->GetUserScore($id, $session->UserID);
+ }
+ $cssClassVoteUp = $cssClassVoteDown = '';
+ if($currentUserVote > 0) {
+ $cssClassVoteUp = ' Voted';
+ } else if($currentUserVote < 0){
+ $cssClassVoteDown = ' Voted';
+ }
+
+ $formattedScore = $this->formattedScore($score);
+ echo '';
+ echo Anchor(Wrap('Vote Up', 'span', array('class' => 'ArrowSprite SpriteUp'.$cssClassVoteUp , 'rel' => 'nofollow')), $voteUpUrl, 'VoteUp'.$cssClass);
+ echo Wrap($formattedScore, 'span', array('class' => 'CountVoices'));
+ echo Anchor(Wrap('Vote Down', 'span', array('class' => 'ArrowSprite SpriteDown'.$cssClassVoteDown, 'rel' => 'nofollow')), $voteDownUrl, 'VoteDown'.$cssClass);
+ echo ' | ';
+
+ }
+
+ private function formattedScore($score) {
+ if(StringIsNullOrEmpty($score)) {
+ $formattedScore = '0';
+ } else {
+ $formattedScore = $score <= 0 ? Gdn_Format::BigNumber($score):'+' . Gdn_Format::BigNumber($score);
+ }
+
+ return $formattedScore;
+ }
+
+
+ public function discussionController_BeforeInlineDiscussionOptions_handler($sender, $args) {
+ $this->addVotingBox($sender, $args);
+ }
+
+ public function discussionController_BeforeInlineCommentOptions_handler($sender, $args) {
+ $this->addVotingBox($sender, $args);
+ }
+
+ public function postController_BeforeInlineCommentOptions_handler($sender, $args) {
+ $this->addVotingBox($sender, $args);
+ }
+
+
+ /**
+ * Add the files to discussions page
+ */
+ public function discussionController_render_Before($sender) {
+ $this->AddJsCss($sender);
+ }
+
+
+ /**
+ * Increment/decrement comment scores
+ */
+ public function discussionController_VoteComment_create($sender) {
+ $CommentID = GetValue(0, $sender->RequestArgs, 0);
+ $VoteType = GetValue(1, $sender->RequestArgs);
+ $TransientKey = GetValue(2, $sender->RequestArgs);
+ $Session = Gdn::Session();
+ $FinalVote = 0;
+ $Total = 0;
+ if ($Session->IsValid() && $Session->ValidateTransientKey($TransientKey) && $CommentID > 0) {
+ $CommentModel = new CommentModel();
+ $OldUserVote = $CommentModel->GetUserScore($CommentID, $Session->UserID);
+ switch ($VoteType) {
+ case 'voteup':
+ $NewUserVote = 1;
+ break;
+ case 'votedown':
+ $NewUserVote = -1;
+ break;
+ default:
+ $NewUserVote = 0;
+ }
+ $FinalVote = intval($OldUserVote) + intval($NewUserVote);
+ if ($FinalVote == 2 || $FinalVote == -2) {
+ // user cancelled a voice
+ $FinalVote = 0;
+ } else {
+ $FinalVote = $NewUserVote;
+ }
+
+ $Total = $CommentModel->SetUserScore($CommentID, $Session->UserID, $FinalVote);
+ }
+ $sender->DeliveryType(DELIVERY_TYPE_BOOL);
+ $sender->SetJson('TotalScore', $this->formattedScore($Total));
+ $sender->SetJson('FinalVote', $FinalVote);
+ $sender->SetJson('VoteUpCssClass', $FinalVote > 0? 'Voted':'');
+ $sender->SetJson('VoteDownCssClass', $FinalVote < 0? 'Voted':'');
+ $sender->Render();
+ }
+
+ /**
+ * Increment/decrement discussion scores
+ */
+ public function discussionController_VoteDiscussion_create($sender) {
+ $DiscussionID = GetValue(0, $sender->RequestArgs, 0);
+ $TransientKey = GetValue(1, $sender->RequestArgs);
+ $VoteType = FALSE;
+ if ($TransientKey == 'voteup' || $TransientKey == 'votedown') {
+ $VoteType = $TransientKey;
+ $TransientKey = GetValue(2, $sender->RequestArgs);
+ }
+ $Session = Gdn::Session();
+ $NewUserVote = 0;
+ $Total = 0;
+ if ($Session->IsValid() && $Session->ValidateTransientKey($TransientKey) && $DiscussionID > 0) {
+ $DiscussionModel = new DiscussionModel();
+ $OldUserVote = $DiscussionModel->GetUserScore($DiscussionID, $Session->UserID);
+
+ switch ($VoteType) {
+ case 'voteup':
+ $NewUserVote = 1;
+ break;
+ case 'votedown':
+ $NewUserVote = -1;
+ break;
+ default:
+ $NewUserVote = 0;
+ }
+
+ $FinalVote = intval($OldUserVote) + intval($NewUserVote);
+ if ($FinalVote == 2 || $FinalVote == -2) {
+ // user cancelled a voice
+ $FinalVote = 0;
+ } else {
+ $FinalVote = $NewUserVote;
+ }
+ $Total = $DiscussionModel->SetUserScore($DiscussionID, $Session->UserID, $FinalVote);
+ }
+ $sender->DeliveryType(DELIVERY_TYPE_BOOL);
+ $sender->SetJson('TotalScore', $this->formattedScore($Total));
+ $sender->SetJson('FinalVote', $FinalVote);
+ $sender->SetJson('VoteUpCssClass', $FinalVote > 0? 'Voted':'');
+ $sender->SetJson('VoteDownCssClass', $FinalVote < 0? 'Voted':'');
+ $sender->Render();
+ }
+
+ /**
+ * Grab the score field whenever the discussions are queried.
+ */
+ public function DiscussionModel_AfterDiscussionSummaryQuery_Handler($Sender) {
+ $Sender->SQL->Select('d.Score');
+ }
+
+ /**
+ * Add voting css to post controller.
+ */
+ public function PostController_Render_Before($Sender) {
+ $this->AddJsCss($Sender);
+ }
+
+ public function Setup() {
+ }
+
+ public function OnDisable() {
+ }
+
+ public function dashboardNavModule_init_handler($sender) {
+ /** @var DashboardNavModule $nav */
+ $nav = $sender;
+ $sort = -1;
+ $nav->addGroupToSection('Moderation', t('Voting'), 'voting', '', ['after'=>'site'])
+ ->addLinkToSectionIf('Garden.Settings.Manage', 'Moderation', t('Discussions'), '/voting/discussions',
+ 'voting.discussions', '', $sort)
+ ->addLinkToSectionIf('Garden.Settings.Manage', 'Moderation', t('Comments'), '/voting/comments',
+ 'voting.comments', '', $sort);
+
+ }
+}
\ No newline at end of file
diff --git a/Voting/controllers/class.votingcontroller.php b/Voting/controllers/class.votingcontroller.php
new file mode 100644
index 0000000..00145e6
--- /dev/null
+++ b/Voting/controllers/class.votingcontroller.php
@@ -0,0 +1,167 @@
+config = $config instanceof ConfigurationInterface ? $config : Gdn::getContainer()->get(ConfigurationInterface::class);
+ $this->discussionModel = Gdn::getContainer()->get(DiscussionModel::class);
+ parent::__construct();
+ }
+
+ /**
+ * Highlight menu path. Automatically run on every use.
+ */
+ public function initialize() {
+ parent::initialize();
+ Gdn_Theme::section('Dashboard');
+ if ($this->Menu) {
+ $this->Menu->highlightRoute('/dashboard/settings');
+ }
+ $this->fireEvent('Init');
+ }
+
+ /**
+ * Discussion list.
+ * @param string $page Page number.
+ * @param string $sort
+ * @throws Exception
+ */
+ public function discussions($page = '', $sort = 'top') {
+ $this->permission('Garden.Settings.Manage');
+
+ // Page setup
+ $this->addJsFile('jquery.gardenmorepager.js');
+ $this->title(t('Voting Discussions'));
+ $this->setHighlightRoute('voting/discussions');
+ Gdn_Theme::section('Moderation');
+
+ // Input Validation.
+ list($offset, $limit) = offsetLimit($page, PagerModule::$DefaultPageSize);
+
+ $DiscussionModel = new DiscussionModel();
+ $DiscussionModel->setSort($sort);
+
+ $where = ['Announce' => 'all', 'd.Score is not null' => ''];
+ // Get Discussion Count
+ $CountDiscussions = $DiscussionModel->getCount($where);
+
+ $this->setData('RecordCount', $CountDiscussions);
+ if ($offset >= $CountDiscussions) {
+ $offset = $CountDiscussions - $limit;
+ }
+
+ // Get Discussions and Announcements
+ $discussionData = $DiscussionModel->getWhereRecent($where, $limit, $offset);
+ $this->setData('Discussions', $discussionData);
+
+ // Deliver json data if necessary
+ if ($this->_DeliveryType != DELIVERY_TYPE_ALL && $this->_DeliveryMethod == DELIVERY_METHOD_XHTML) {
+ $this->setJson('LessRow', $this->Pager->toString('less'));
+ $this->setJson('MoreRow', $this->Pager->toString('more'));
+ $this->View = 'discussions';
+ }
+
+ $this->render();
+ }
+
+ /**
+ * Comment list.
+ * @param string $page Page number.
+ * @param string $sort
+ * @throws Exception
+ */
+ public function comments($page = '', $sort = 'top') {
+ $this->permission('Garden.Settings.Manage');
+
+ // Page setup
+ $this->addJsFile('jquery.gardenmorepager.js');
+ $this->title(t('Voting Comments'));
+ $this->setHighlightRoute('voting/comments');
+ Gdn_Theme::section('Moderation');
+
+ // Input Validation.
+ list($offset, $limit) = offsetLimit($page, PagerModule::$DefaultPageSize);
+
+ $CommentModel = new CommentModel();
+
+ switch (strtolower($sort)) {
+ case 'top':
+ $CommentModel->OrderBy(array('c.Score desc', 'c.CommentID desc'));
+ break;
+ default:
+ $CommentModel->OrderBy(array('c.Score desc', 'c.CommentID desc'));
+ break;
+ }
+
+ $where = ['Score is not null' => ''];
+ // Get Comment Count
+ $CountComments = $CommentModel->getCount($where);
+
+ $this->setData('RecordCount', $CountComments);
+ if ($offset >= $CountComments) {
+ $offset = $CountComments - $limit;
+ }
+
+ $data = $CommentModel->getWhere($where,'', '' , $limit, $offset);
+ $this->setData('Comments', $data);
+
+ // Deliver json data if necessary
+ if ($this->_DeliveryType != DELIVERY_TYPE_ALL && $this->_DeliveryMethod == DELIVERY_METHOD_XHTML) {
+ $this->setJson('LessRow', $this->Pager->toString('less'));
+ $this->setJson('MoreRow', $this->Pager->toString('more'));
+ $this->View = 'discussions';
+ }
+
+ $this->render();
+ }
+
+
+ /**
+ * Build URL to order users by value passed.
+ */
+ protected function _OrderDiscussionsUrl($field) {
+ $get = Gdn::request()->get();
+ $get['sort'] = $field;
+ $get['Page'] = 'p1';
+ return '/voting/discussions?'.http_build_query($get);
+ }
+
+ /**
+ * Build URL to order users by value passed.
+ */
+ protected function _OrderCommentsUrl($field) {
+ $get = Gdn::request()->get();
+ $get['sort'] = $field;
+ $get['Page'] = 'p1';
+ return '/voting/comments?'.http_build_query($get);
+ }
+
+
+}
diff --git a/Voting/design/voting.css b/Voting/design/voting.css
new file mode 100644
index 0000000..624a3f2
--- /dev/null
+++ b/Voting/design/voting.css
@@ -0,0 +1,10 @@
+.Voter .CountVoices{
+ margin-left: 10px;
+ margin-right: 10px;
+}
+.Voter a{
+ color: #C0C0C0;
+}
+.Voted {
+ color: #0AB88A;
+}
\ No newline at end of file
diff --git a/Voting/js/voting.js b/Voting/js/voting.js
new file mode 100644
index 0000000..35393e4
--- /dev/null
+++ b/Voting/js/voting.js
@@ -0,0 +1,42 @@
+jQuery(document).ready(function($) {
+
+ // Handle Vote button clicks
+ $(document).on('click', '.Voter a', function() {
+
+ var btn = this;
+ var parent = $(this).parents('.Voter');
+ var votes = $(parent).find('span.CountVoices');
+ var voteUp = $(parent).find('.SpriteUp');
+ var voteDown = $(parent).find('.SpriteDown');
+ $.ajax({
+ type: "POST",
+ url: btn.href,
+ data: 'DeliveryType=BOOL&DeliveryMethod=JSON',
+ dataType: 'json',
+ error: function(XMLHttpRequest, textStatus, errorThrown) {
+ gdn.informError(xhr);
+ },
+ success: function(json) {
+ // Change the Vote count
+ $(votes).text(json.TotalScore);
+ $(voteUp).removeClass('Voted');
+ $(voteDown).removeClass('Voted');
+ $(voteUp).addClass(json.VoteUpCssClass);
+ $(voteDown).addClass(json.VoteDownCssClass);
+ gdn.inform(json);
+ }
+ });
+ return false;
+ });
+
+});
+
+// Updates The Total Of Comments After A Comment Has Been Added
+$(document).on('CommentAdded', function() {
+ $('.VotingSort strong').html($('.MessageList li.ItemComment').length+' Comment'+($('.MessageList li.ItemComment').length > 1 ? 's' : ''));
+});
+
+// Updates The Total Of Comments After A Comment Has Been Deleted
+$(document).on('CommentDeleted', function() {
+ $('.VotingSort strong').html($('.MessageList li.ItemComment').length+' Comment'+($('.MessageList li.ItemComment').length > 1 ? 's' : ''));
+});
\ No newline at end of file
diff --git a/Voting/topcoder-logo.jpeg b/Voting/topcoder-logo.jpeg
new file mode 100644
index 0000000..8287449
Binary files /dev/null and b/Voting/topcoder-logo.jpeg differ
diff --git a/Voting/views/voting/_comments.php b/Voting/views/voting/_comments.php
new file mode 100644
index 0000000..b66a073
--- /dev/null
+++ b/Voting/views/voting/_comments.php
@@ -0,0 +1,43 @@
+data('Comments')->result() as $comment) {
+ $Alt = !$Alt;
+ $commentUrl = commentUrl($comment, '', '/');
+ $discussionUrl = discussionUrl($comment->DiscussionID, '', '/');
+ ?>
+
CommentID}"; ?>"
+ data-commentid="CommentID?>">
+
+ getID($comment->InsertUserID, DATASET_TYPE_ARRAY);
+ $authorBlock = new MediaItemModule(val('Name', $recordUser), userUrl($recordUser));
+ $date = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($comment->DateInserted, true,
+ DateTimeFormatter::FORCE_FULL_FORMAT);
+ $authorBlock->setView('media-sm')
+ ->setImage(userPhotoUrl($recordUser))
+ ->addTitleMetaIf((bool)$recordUser['Banned'], wrap(t('Banned'), 'span', ['class' => 'text-danger']))
+ ->addTitleMeta(plural($recordUser['CountComments'], '%s comment', '%s comments'))
+ ->addMeta($date);
+ // ->addMetaIf(($viewPersonalInfo && val('RecordIPAddress', $Row)), iPAnchor($Row['RecordIPAddress']));
+
+ echo $authorBlock;
+ echo ' ';
+ echo Gdn_Format::excerpt($comment->Body, $comment->Format); //Gdn_Format::to($comment->Body, $comment->Format);
+ echo ' ';
+ ?>
+ |
+ Score ?> |
+
+ t('View Post'), 'target'=>'_blank', 'aria-label' => t('View Post'), 'class' => 'btn btn-icon btn-icon-sm'];
+ echo anchor(dashboardSymbol('external-link', 'icon icon-text'), $commentUrl, '', $attr);
+ ?>
+ |
+
+ data('Discussions')->result() as $discussion) {
+ $Alt = !$Alt;
+ $discussionUrl = discussionUrl($discussion, '', '/');
+ ?>
+ discussionID}"; ?>"
+ data-userid="discussionID?>">
+
+ getID($discussion->InsertUserID, DATASET_TYPE_ARRAY);
+ $authorBlock = new MediaItemModule(val('Name', $recordUser), userUrl($recordUser));
+ $date = Gdn::getContainer()->get(DateTimeFormatter::class)->formatDate($discussion->DateInserted, true,
+ DateTimeFormatter::FORCE_FULL_FORMAT);
+ $authorBlock->setView('media-sm')
+ ->setImage(userPhotoUrl($recordUser))
+ ->addTitleMetaIf((bool)$recordUser['Banned'], wrap(t('Banned'), 'span', ['class' => 'text-danger']))
+ ->addTitleMeta(plural($recordUser['CountDiscussions'], '%s discussion', '%s discussions'))
+ ->addMeta($date);
+ // ->addMetaIf(($viewPersonalInfo && val('RecordIPAddress', $Row)), iPAnchor($Row['RecordIPAddress']));
+
+ echo $authorBlock;
+ // $ancestors = $this->buildBreadcrumbs($this->CategoryID);
+ // array_push($ancestors, ['Name' => $discussion->Name]);
+ $category = CategoryModel::categories($discussion->CategoryID);
+ echo ' ';
+ //echo anchor(htmlspecialchars(val('Name', $category)), categoryUrl($discussion->CategoryID));
+ echo htmlspecialchars(val('Name', $category));
+ echo ' ';
+ echo ' ';
+ echo htmlspecialchars($discussion->Name);
+ echo ' ';
+ echo '';
+ echo Gdn_Format::excerpt($discussion->Body, $discussion->Format);
+ echo ' ';
+ ?>
+ |
+ Score ?> |
+ CountViews ?> |
+ CountComments ?> |
+
+ t('View Post'), 'target'=>'_blank', 'aria-label' => t('View Post'), 'class' => 'btn btn-icon btn-icon-sm'];
+ echo anchor(dashboardSymbol('external-link', 'icon icon-text'), $discussionUrl, '', $attr);
+ ?>
+ |
+
+
+ data('Title'); ?>
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+ fetchViewLocation('_comments')); ?>
+
+
+
\ No newline at end of file
diff --git a/Voting/views/voting/discussions.php b/Voting/views/voting/discussions.php
new file mode 100644
index 0000000..9769db4
--- /dev/null
+++ b/Voting/views/voting/discussions.php
@@ -0,0 +1,32 @@
+
+ data('Title'); ?>
+
+
+
+
+
+ |
+ _OrderDiscussionsUrl('top')); ?> |
+ _OrderDiscussionsUrl('views')); ?> |
+ _OrderDiscussionsUrl('comments')); ?> |
+ |
+
+
+
+ fetchViewLocation('_discussions')); ?>
+
+
+
\ No newline at end of file