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'); ?>

+
+
+ data('RecordCount', null); + // if ($count !== null) { + // echo sprintf('%s comments.', $count); + // } + ?> +
+ $this, 'View' => 'pager-dashboard']); ?> +
+
+ + + + + + + + + + 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'); ?>

+
+
+ data('RecordCount', null); + // if ($count !== null) { + // echo sprintf('%s discussions.', $count); + // } + ?> +
+ $this, 'View' => 'pager-dashboard']); ?> +
+
+ + + + + + + + + + + + fetchViewLocation('_discussions')); ?> + +
_OrderDiscussionsUrl('top')); ?>_OrderDiscussionsUrl('views')); ?>_OrderDiscussionsUrl('comments')); ?>
+
\ No newline at end of file