yaga wip: reactions are 95% complete

This commit is contained in:
Zachary Doll 2013-10-25 17:05:01 -05:00
parent 13cdd804de
commit 6c909525c1
19 changed files with 981 additions and 123 deletions

View File

@ -0,0 +1,102 @@
<?php if(!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
/**
* All is the base class for controllers throughout the gamification applicati0n.
*
* @since 1.0
* @package Yaga
*/
class ActionsController extends DashboardController {
/** @var array List of objects to prep. They will be available as $this->$Name. */
public $Uses = array('Form', 'ActionModel');
/**
* If you use a constructor, always call parent.
* Delete this if you don't need it.
*
* @access public
*/
public function __construct() {
parent::__construct();
}
/**
* This is a good place to include JS, CSS, and modules used by all methods of this controller.
*
* Always called by dispatcher before controller's requested method.
*
* @since 1.0
* @access public
*/
public function Initialize() {
parent::Initialize();
Gdn_Theme::Section('Dashboard');
if($this->Menu) {
$this->Menu->HighlightRoute('/actions');
}
$this->AddJsFile('actions.js');
$this->AddCssFile('actions.css');
}
public function Settings($Page = '') {
$this->Permission('Yaga.Reactions.Manage');
$this->AddSideMenu('actions/settings');
$this->Title('Manage Reactions');
// Get list of actions from the model and pass to the view
$this->SetData('Actions', $this->ActionModel->GetActions());
$this->Render();
}
public function Edit($ActionID = NULL) {
$this->Permission('Yaga.Reactions.Manage');
$this->AddSideMenu('actions/settings');
$this->Form->SetModel($this->ActionModel);
if($ActionID) {
$this->Action = $this->ActionModel->GetAction($ActionID);
$this->Form->AddHidden('ActionID', $ActionID);
}
if($this->Form->IsPostBack() == FALSE) {
$this->Form->SetData($this->Action);
}
else {
if($this->Form->Save()) {
$Action = $this->ActionModel->GetAction($this->Form->GetValue('ActionID'));
$NewActionRow = '<tr id="ActionID_' . $Action->ActionID . '" data-actionid="'. $Action->ActionID . '">';
$NewActionRow .= "<td>$Action->Name</td>";
$NewActionRow .= "<td>$Action->Description</td>";
$NewActionRow .= "<td>$Action->Tooltip</td>";
$NewActionRow .= "<td>$Action->AwardValue</td>";
$NewActionRow .= '<td>' . Anchor(T('Edit'), 'yaga/actions/edit/' . $Action->ActionID, array('class' => 'Popup SmallButton')) . Anchor(T('Delete'), 'yaga/actions/delete/' . $Action->ActionID, array('class' => 'Hijack SmallButton')) . '</td>';
$NewActionRow .= '</tr>';
$this->JsonTarget('#ActionID_' . $this->Action->ActionID, $NewActionRow, 'ReplaceWith');
$this->InformMessage('Action updated successfully!');
}
}
$this->Render('add');
}
public function Add() {
$this->Edit();
}
public function Delete($ActionID) {
if(!$this->Request->IsPostBack()) {
throw PermissionException('Javascript');
}
$this->Permission('Yaga.Reactions.Manage');
$this->AddSideMenu('actions/settings');
$this->ActionModel->DeleteAction($ActionID);
$this->JsonTarget('#ActionID_' . $ActionID, null, 'SlideUp');
$this->Render('Blank', 'Utility', 'Dashboard');
}
}

View File

@ -0,0 +1,66 @@
<?php if(!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
/**
* All is the base class for controllers throughout the gamification applicati0n.
*
* @since 1.0
* @package Yaga
*/
class ConfigureController extends DashboardController {
/** @var array List of objects to prep. They will be available as $this->$Name. */
public $Uses = array('Database', 'Form');
/**
* If you use a constructor, always call parent.
* Delete this if you don't need it.
*
* @access public
*/
public function __construct() {
parent::__construct();
}
/**
* This is a good place to include JS, CSS, and modules used by all methods of this controller.
*
* Always called by dispatcher before controller's requested method.
*
* @since 1.0
* @access public
*/
public function Initialize() {
parent::Initialize();
Gdn_Theme::Section('Dashboard');
if ($this->Menu) {
$this->Menu->HighlightRoute('/yaga/configure');
}
$this->AddJsFile('yaga.js');
$this->AddCssFile('yaga.css');
}
public function Index() {
$this->Yaga();
}
public function Yaga() {
$this->Permission('Garden.Settings.Manage');
$ConfigModule = new ConfigurationModule($this);
$ConfigModule->Initialize(array(
'Yaga.Reactions.Enabled' => array('LabelCode' => 'Use Reactions', 'Control' => 'Checkbox'),
'Yaga.Badges.Enabled' => array('LabelCode' => 'Use Badges', 'Control' => 'Checkbox'),
'Yaga.Ranks.Enabled' => array('LabelCode' => 'Use Ranks', 'Control' => 'Checkbox')
));
$this->AddSideMenu('configure');
$this->SetData('Title', 'Gamification Settings');
$this->ConfigurationModule = $ConfigModule;
$ConfigModule->RenderAll();
}
public function Reactions($Page = '') {
}
}

View File

@ -31,12 +31,10 @@ class ReactController extends YagaController {
* @access public
*/
public function Initialize() {
parent::Initialize();
if(!$this->Request->IsPostBack()) {
//throw PermissionException('Javascript');
throw PermissionException('Javascript');
}
$this->AddJsFile('yaga.js');
$this->AddCssFile('yaga.css');
}
public function Index() {
@ -48,7 +46,7 @@ class ReactController extends YagaController {
// check to see if allowed to react
$this->Permission('Plugins.Reactions.Add');
if($this->ActionModel->GetAction($ActionID)) {
if(!$this->ActionModel->ActionExists($ActionID)) {
throw new Gdn_UserException('Invalid Action');
}
@ -68,25 +66,25 @@ class ReactController extends YagaController {
}
// It has passed through the gauntlet
$this->ReactionModel->SetReaction($DiscussionID, 'discussion', $UserID, $ActionID);
$this->ReactionModel->SetReaction($DiscussionID, 'discussion', $Discussion->InsertUserID, $UserID, $ActionID);
$this->JsonTarget($Anchor, $this->_RenderActions($DiscussionID, 'discussion', FALSE), 'ReplaceWith');
$this->Render('Blank', 'Utility', 'Dashboard');
}
public function Comment($DiscussionID, $ActionID) {
public function Comment($CommentID, $ActionID) {
// check to see if allowed to react
$this->Permission('Plugins.Reactions.Add');
if($this->ActionModel->GetAction($ActionID)) {
if(!$this->ActionModel->ActionExists($ActionID)) {
throw new Gdn_UserException('Invalid Action');
}
$Discussion = $this->DiscussionModel->GetID($DiscussionID);
$Comment = $this->CommentModel->GetID($CommentID);
if($Discussion) {
$Anchor = '#Discussion_' . $DiscussionID . ' .ReactMenu';
if($Comment) {
$Anchor = '#Comment_' . $CommentID . ' .ReactMenu';
}
else {
throw new Gdn_UserException('Invalid ID');
@ -94,30 +92,30 @@ class ReactController extends YagaController {
$UserID = Gdn::Session()->UserID;
if($Discussion->InsertUserID == $UserID) {
if($Comment->InsertUserID == $UserID) {
throw new Gdn_UserException('You cannot react to your own content.');
}
// It has passed through the gauntlet
$this->ReactionModel->SetReaction($DiscussionID, 'discussion', $UserID, $ActionID);
$this->ReactionModel->SetReaction($CommentID, 'comment', $Comment->InsertUserID, $UserID, $ActionID);
$this->JsonTarget($Anchor, $this->_RenderActions($DiscussionID, 'discussion', FALSE), 'ReplaceWith');
$this->JsonTarget($Anchor, $this->_RenderActions($CommentID, 'comment', FALSE), 'ReplaceWith');
$this->Render('Blank', 'Utility', 'Dashboard');
}
public function Activity($DiscussionID, $ActionID) {
public function Activity($ActivityID, $ActionID) {
// check to see if allowed to react
$this->Permission('Plugins.Reactions.Add');
if($this->ActionModel->GetAction($ActionID)) {
if(!$this->ActionModel->ActionExists($ActionID)) {
throw new Gdn_UserException('Invalid Action');
}
$Discussion = $this->DiscussionModel->GetID($DiscussionID);
$Activity = $this->ActivityModel->GetID($ActivityID);
if($Discussion) {
$Anchor = '#Discussion_' . $DiscussionID . ' .ReactMenu';
if($Activity) {
$Anchor = '#Activity_' . $ActivityID . ' .ReactMenu';
}
else {
throw new Gdn_UserException('Invalid ID');
@ -125,27 +123,26 @@ class ReactController extends YagaController {
$UserID = Gdn::Session()->UserID;
if($Discussion->InsertUserID == $UserID) {
if($Activity->InsertUserID == $UserID) {
throw new Gdn_UserException('You cannot react to your own content.');
}
// It has passed through the gauntlet
$this->ReactionModel->SetReaction($DiscussionID, 'discussion', $UserID, $ActionID);
$this->ReactionModel->SetReaction($ActivityID, 'activity', $Activity->InsertUserID, $UserID, $ActionID);
$this->JsonTarget($Anchor, $this->_RenderActions($DiscussionID, 'discussion', FALSE), 'ReplaceWith');
$this->JsonTarget($Anchor, $this->_RenderActions($ActivityID, 'activity', FALSE), 'ReplaceWith');
$this->Render('Blank', 'Utility', 'Dashboard');
}
private function _RenderActions($ID, $Type, $Echo = TRUE) {
$Reactions = $this->ReactionModel->GetReactions($ID, $Type);
//decho($Reactions);
$ActionsString = '';
foreach($Reactions as $Action) {
$ActionsString .= Anchor(
Wrap('&nbsp;', 'span', array('class' => 'ReactSprite React-' . $Action->ActionID)) .
WrapIf(count($Action->UserIDs), 'span', array('class' => 'Count')) .
Wrap($Action->Name, 'span', array('class' => 'ReactLabel')), 'discussion/react/' . $Type . '/' . $ID . '/' . $Action->ActionID,
Wrap($Action->Name, 'span', array('class' => 'ReactLabel')), 'react/' . $Type . '/' . $ID . '/' . $Action->ActionID,
'Hijack ReactButton'
);
}

View File

@ -1,43 +0,0 @@
<?php if(!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
/**
* All is the base class for controllers throughout the gamification applicati0n.
*
* @since 1.0
* @package Yaga
*/
class ReactionsController extends YagaController {
/** @var array List of objects to prep. They will be available as $this->$Name. */
public $Uses = array('Form');
/**
* If you use a constructor, always call parent.
* Delete this if you don't need it.
*
* @access public
*/
public function __construct() {
parent::__construct();
}
/**
* This is a good place to include JS, CSS, and modules used by all methods of this controller.
*
* Always called by dispatcher before controller's requested method.
*
* @since 1.0
* @access public
*/
public function Initialize() {
// There are 4 delivery types used by Render().
// DELIVERY_TYPE_ALL is the default and indicates an entire page view.
if($this->DeliveryType() == DELIVERY_TYPE_ALL)
$this->Head = new HeadModule($this);
// Call Gdn_Controller's Initialize() as well.
parent::Initialize();
}
}

View File

@ -9,9 +9,6 @@
*/
class YagaController extends Gdn_Controller {
/** @var array List of objects to prep. They will be available as $this->$Name. */
public $Uses = array('Form');
/**
* If you use a constructor, always call parent.
* Delete this if you don't need it.
@ -31,11 +28,6 @@ class YagaController extends Gdn_Controller {
* @access public
*/
public function Initialize() {
// There are 4 delivery types used by Render().
// DELIVERY_TYPE_ALL is the default and indicates an entire page view.
if($this->DeliveryType() == DELIVERY_TYPE_ALL)
$this->Head = new HeadModule($this);
// Call Gdn_Controller's Initialize() as well.
parent::Initialize();
}

View File

@ -1,16 +0,0 @@
By default, each application has it's own "design" folder for css and image
files off the root of that application (ie. /scaffolding/design/). If you wanted
to override them, you could copy them into your custom theme folder and edit
them there.
If you want a view to be used across applications (ie. one master view for
vanilla, scffolding, etc), you could place it in:
/themes/theme_name/views/default.master
/themes/theme_name/design/*.png,*.css
If you wanted a view to be specific to one application (ie. an altered master
view for scaffolding only), you could place it in:
/themes/theme_name/app_name/views/default.master
/themes/theme_name/app_name/design/*.png,*.css

406
design/reactions.css Normal file
View File

@ -0,0 +1,406 @@
.Reactions {
position: relative;
font-size: 11px;
line-height: 18px;
}
.Reactions .Handle {
display: inline-block;
opacity: .3;
}
.React {
position: absolute;
right: 0;
top: 0;
}
.ReactHeading {
display: inline-block;
padding: 0px 6px;
background: rgba(0,0,0,0.04);
color: rgba(0,0,0,0.4);
}
.Flag .ReactHeading {
border-radius: 4px 0 0 4px;
}
.React .ReactHeading {
border-radius: 0 4px 4px 0;
}
.Item:hover .ReactHeading {
background: rgba(0,0,0,0.8);
color: #fff;
}
.ReactButton {
display: inline-block;
margin: 0 2px;
line-height: 18px;
vertical-align: top;
position: relative;
}
.Flyout .ReactButton {
margin: 0;
}
.UserReactionWrap {
position: relative;
line-height: 1;
display: inline-block;
margin-right: 3px;
}
.UserReactionWrap .ReactSprite {
position: absolute;
right: -2px;
bottom: -2px;
}
/* Hover Mechanics */
.Reactions .Count {
border-radius: 6px;
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
vertical-align: top;
background: #494949;
background: rgba(0, 0, 0, 0.7);
}
.ToggleFlyout.Open .ReactButton {
opacity: 1 !important;
}
.Item:hover .Reactions .Handle {
opacity: .75;
}
.Reactions .ReactHandle {
display: none;
}
.Item:hover .Reactions a {
width: inherit;
}
.Reactions a.HasCount {
display: inline-block;
}
.InProgress .ReactSprite {
background-image: none !Important;
background: transparent !Important;
}
.InProgress .Count {
visibility: hidden;
}
.FilterButton.Selected {
font-weight: bold;
}
.DataCounts {
margin: 5px 0;
text-align: center;
}
/*.DataCounts a {
color: #000;
}
.DataCounts a:hover {
color: #ff0084;
}*/
.CountTotal {
display: block;
line-height: 100%;
font-size: 28px;
}
.CountTotal img {
height: 40px;
}
.CountItemWrap {
width: 10%;
display: inline-block;
}
.CountItem {
display: block;
text-align: center;
border: solid 2px transparent;
padding: 4px;
margin: 0 4px;
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
white-space: nowrap;
overflow: visible;
}
.Compact .Item .Author img {
height: 24px;
width: 24px;
}
.Compact .Author .PhotoWrap {
margin: 0 8px 4px 0;
}
.Compact .Title {
font-sixe: 13px;
}
.Compact .Reactions {
margin-top: 5px;
}
.Compact p {
margin: 5px 0;
}
.CountItem.Selected {
background: rgba(55, 173, 227, 0.1);
border: solid 2px rgba(55, 173, 227, .2);
padding: 4px
}
li.Buried {
padding: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.Buried, .Buried a {
color: #aaa;
font-size: 12px;
}
.Buried .Author .ProfilePhoto {
height: 21px;
width: 21px;
margin: 0 3px 0 0;
opacity: .4;
}
.Buried .Reactions {
display: none;
}
li.Buried * {
display: inline;
margin: 0;
white-space: nowrap;
padding-top: 0;
padding-bottom: 0;
}
.Buried br {
display: none;
}
.Buried .Item-BodyWrap,
.Buried .Options,
.Buried .Signature,
.Buried .RecordReactions {
display: none;
}
li.Buried:hover:before {
content: "Post is buried, click here to show the rest.";
display: block;
position: absolute;
background: rgba(0,0,0,.75);
color: #fff;
text-shadow: 0 1px 1px #000;
width: 100%;
height: 100%;
left: 0;
top: 0;
text-align: center;
vertical-align: middle;
line-height: 40px; /* approx, could cause problems */
}
/*.Promoted {
box-shadow: inset 0 80px 600px #fffcdc;
border-top: solid 1px #fff;
}
.Promoted .Message {
font-size: 110%;
}
.Promoted .Author .ProfilePhotoMedium {
box-shadow: 0px -1px 8px #fff;
}*/
/* Menu stuff */
.Flyout.Flags {
top: 25px;
z-index: 1000;
}
.FlagMenu > .SpFlyoutHandle {
left: 10px;
bottom: -12px;
z-index: 1001;
}
/* Reaction Revamp */
.Reactions .Bullet {
color: #999;
}
.React {
position: relative;
top: auto;
right: auto;
}
.Reactions {
margin: 15px 0 0 0;
}
.RecordReactions {
margin-top: 15px;
}
.RecordReactions + .Reactions {
margin-top: 0;
}
.Reactions .Count {
font-size: 8px;
background: #555;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
margin-right: 4px;
}
.Item .Reactions > * {
visibility: hidden;
}
.Item .Reactions > *.Visible {
visibility: visible;
}
.Item.Open .Reactions > *,
.Item:hover .Reactions > *,
.Item .Reactions > .FlagMenu {
visibility: visible;
}
.Reactions > a.React > .ReactSprite {
margin-right: 2px;
}
.Reactions .FlagMenu .MenuItems .ReactButton {
margin: 0;
padding: 0 15px;
}
.MenuItems-Reactions {
position: absolute;
width: 160px;
margin-bottom: 5px;
}
.MenuItems-Reactions a {
display: inline-block !important;
padding: 2px 3px 5px !important;
}
.MenuItems-Reactions .ProfilePhotoSmall {
height: 20px;
width: 20px;
}
/* OLD "Best Of" */
.ReactionFilters {
margin: 8px 0;
}
.ReactionFilters .ReactButton {
opacity: 1;
margin-right: 6px;
}
.ReactionFilters .ReactButton .ReactSprite {
background-image: url('reaction-sprites-color.png') !important;
}
.ReactionFilters .ReactLabel {
padding-left: 4px;
}
.BestOfData .DataList .Item:first-child {
border-top: 1px solid #BEC8CC;
}
/* End "Best Of" */
#infscr-loading {
text-align: center;
}
#infscr-loading img {
margin: 0 auto;
}
/* End "Best Of" */
/* iPad/iPhone */
@media only screen and (device-width: 768px),
only screen and (max-device-width: 480px) {
.Item .Reactions > * { visibility: visible; }
}
/** Best of tiling stuff. **/
.Tile {
float: left;
width: 278px;
border: solid 1px #ddd;
margin: 5px;
padding: 10px;
border-radius: 2px;
-webkit-transition: opacity .3s ease-in-out;
-moz-transition: opacity .3s ease-in-out;
-o-transition: opacity .3s ease-in-out;
-ms-transition: opacity .3s ease-in-out;
transition: opacity .3s ease-in-out;
}
.Tile > h3 {
margin-top: 0;
line-height: 1.2;
}
.Tile .Image img,
.Tile .Body img{
max-width: 100% !important;
float: none !important;
margin: 0 auto 10px !important;
display: block;
}
.Tile .AuthorWrap {
margin: 0 -10px -10px;
padding: 10px;
background: rgba(0,0,0,.03);
}
.Tile .AuthorWrap .ProfilePhoto {
height: 32px;
width: 32px;
}
.Tile .Reactions {
margin: 0;
line-height: 1.6;
}
.Tile .ReactButton {
position: relative;
}
.Tile .Message {
margin: 0;
}
.Tile .ReactLabel {
display: none;
background: #fff;
color: #000;
white-space: nowrap;
position: absolute;
line-height: 1;
padding: 3px 6px 4px;
left: -4px;
top: -23px;
}
.Tile .Reactions .Bullet,
.Tile .Reactions .Count {
display: none;
}
/**** Transitions ****/
.masonry,
.masonry .masonry-brick {
-webkit-transition-duration: 0.7s;
-moz-transition-duration: 0.7s;
-ms-transition-duration: 0.7s;
-o-transition-duration: 0.7s;
transition-duration: 0.7s;
}
.masonry {
-webkit-transition-property: height, width;
-moz-transition-property: height, width;
-ms-transition-property: height, width;
-o-transition-property: height, width;
transition-property: height, width;
}
.masonry .masonry-brick {
-webkit-transition-property: left, right, top;
-moz-transition-property: left, right, top;
-ms-transition-property: left, right, top;
-o-transition-property: left, right, top;
transition-property: left, right, top;
}

5
design/yaga.css Normal file
View File

@ -0,0 +1,5 @@
/* Copyright 2013 Zachary Doll */
#Panel div.Gamification h4 {
background-position: 164px -841px;
}

View File

@ -1,12 +0,0 @@
Any javascript files that are specific to this application can be placed in this
folder. They should be named after the application, controller, or controller
method that they are required for. For example:
entry.js: should be included in every page of the "entry" controller.
entry_apply.js: should be included only on the entry.apply() page.
Note 1: these are simply guidelines - you can name any file whatever you want and
include it anywhere you want.
Note 2: You can add a js file to the controller with:
$this->AddJsFile('filename.js');

4
js/reactions.js Normal file
View File

@ -0,0 +1,4 @@
/* Copyright 2013 Zachary Doll */
jQuery(document).ready(function($) {
});

View File

@ -41,12 +41,33 @@ class ActionModel extends Gdn_Model {
* @return dataset
*/
public function GetAction($ActionID) {
return $this->SQL
$Action = $this->SQL
->Select()
->From('Action')
->Where('ActionID', $ActionID)
->Get()
->FirstRow();
return $Action;
}
public function ActionExists($ActionID) {
return !empty($this->GetAction($ActionID));
}
public function DeleteAction($ActionID, $ReplacementID = NULL) {
if($this->ActionExists($ActionID)) {
$this->SQL->Delete('Action', array('ActionID' => $ActionID));
// TODO: Ask the user if they want to delete reactions or lump them in
// with another action
if($ReplacementID && $this->ActionExists($ReplacementID)) {
$this->SQL->Update('Reaction')
->Set('ActionID', $ReplacementID)
->Where('ActionID', $ActionID);
}
else {
$this->SQL->Delete('Reaction', array('ActionID' => $ActionID));
}
}
}
}

View File

@ -102,17 +102,16 @@ class ReactionModel extends Gdn_Model {
->FirstRow();
}
// TODO: Make this get the reactions received, not given
public function GetUserReactionCount($UserID, $ActionID) {
return $this->SQL
->Select()
->From('Reaction')
->Where('ActionID', $ActionID)
->Where('InsertUserID', $UserID)
->Where('ParentAuthorID', $UserID)
->GetCount();
}
public function SetReaction($ID, $Type, $UserID, $ActionID) {
public function SetReaction($ID, $Type, $AuthorID, $UserID, $ActionID) {
// clear the cache
unset(self::$_Reactions[$Type . $ID]);
@ -130,6 +129,7 @@ class ReactionModel extends Gdn_Model {
return $this->SQL
->Update('Reaction')
->Set('ActionID', $ActionID)
->Set('DateInserted', date(DATE_ISO8601))
->Where('ParentID', $ID)
->Where('ParentType', $Type)
->Where('InsertUserID', $UserID)
@ -143,7 +143,9 @@ class ReactionModel extends Gdn_Model {
array('ActionID' => $ActionID,
'ParentID' => $ID,
'ParentType' => $Type,
'InsertUserID' => $UserID));
'ParentAuthorID' => $AuthorID,
'InsertUserID' => $UserID,
'DateInserted' => date(DATE_ISO8601)));
}
}
}

View File

@ -2,7 +2,7 @@
/* Copyright 2013 Zachary Doll */
$ApplicationInfo['Yaga'] = array(
'Name' => 'Yet Another Gamification Application',
'Description' => 'Yaga provides customizable reactions, badges, and ranks for your Vanilla forum software.',
'Description' => 'Yaga provides customizable reactions, badges, and ranks for your Vanilla forum software. Increase user activity by letting users react to content with emotions. Give users badges based on statistics and engagement in your community. Create and award custom badges for special events and recognition. Award Ranks which can confer different (configurable) permissions based on community perception and participation.',
'Version' => '0.1',
'RequiredApplications' => array('Vanilla' => '2.0.18.8'),
'RegisterPermissions' => array(

View File

@ -1,4 +1,5 @@
<?php if(!defined('APPLICATION')) exit();
/**
* A special function that is automatically run upon enabling your application.
*
@ -6,17 +7,258 @@
*/
class YagaHooks implements Gdn_IPlugin {
static $_ReactionModel = NULL;
/**
* Display points in the user info list
* @param type $Sender
*/
public function UserInfoModule_OnBasicInfo_Handler($Sender) {
$Points = 0;
echo Wrap(T('Yaga.Points', 'Points'), 'dt') . ' ' . Wrap($Points, 'dd');
}
/**
* Display the reaction counts on the profile page
* @param type $Sender
*/
public function ProfileController_AfterUserInfo_Handler($Sender) {
$User = $Sender->User;
//decho($User);
echo '<div class="Yarbs ReactionsWrap">';
echo Wrap(T('Yarbs.Reactions.Title', 'Reactions'), 'h2', array('class' => 'H'));
// insert the reaction totals in the profile
$Actions = $this->_ReactionModel->GetActions();
$String = '';
foreach($Actions as $Action) {
//decho($Action);
$Count = $this->_ReactionModel->GetUserReactionCount($User->UserID, $Action->ActionID);
//decho($Count);
$TempString = Wrap(Wrap(Gdn_Format::BigNumber($Count), 'span', array('title' => $Count)), 'span', array('class' => 'Yarbs_ReactionCount CountTotal'));
$TempString .= Wrap($Action->Name, 'span', array('class' => 'Yarbs_ReactionName CountLabel'));
$String .= Wrap(Wrap(Anchor($TempString, '/profile/yarbs/' . $User->UserID . '/' . $User->Name . '/' . $Action->ActionID, array('class' => 'Yarbs_Reaction TextColor', 'title' => $Action->Description)), 'span', array('class' => 'CountItem')), 'span', array('class' => 'CountItemWrap'));
}
echo Wrap($String, 'div', array('class' => 'DataCounts'));
}
/**
* Display a record of reactions after the first post
* @param DiscussionController $Sender
*/
public function DiscussionController_AfterDiscussionBody_Handler($Sender) {
$Type = 'discussion';
$ID = $Sender->DiscussionID;
$this->_RenderCurrentReactions($ID, $Type);
}
/**
* Display a record of reactions after comments
* @param DiscussionController $Sender
*/
public function DiscussionController_AfterCommentBody_Handler($Sender) {
$Type = 'comment';
$ID = $Sender->EventArguments['Comment']->CommentID;
$this->_RenderCurrentReactions($ID, $Type);
}
/**
* Renders the reaction record for a specific item
* @param int $ID
* @param enum $Type 'discussion', 'activity', or 'comment'
*/
protected function _RenderCurrentReactions($ID, $Type) {
// check to see if allowed to view reactions
if(!Gdn::Session()->CheckPermission('Plugins.Reactions.View')) {
return;
}
if(empty($this->_ReactionModel)) {
$this->_ReactionModel = new ReactionModel();
}
$Reactions = $this->_ReactionModel->GetReactions($ID, $Type);
foreach($Reactions as $Reaction) {
if($Reaction->UserIDs) {
foreach($Reaction->UserIDs as $Index => $UserID) {
$User = Gdn::UserModel()->GetID($UserID);
$String = UserPhoto($User, array('Size' => 'Small'));
$String .= '<span class="ReactSprite ReactLol Reaction-' . $Reaction->ActionID . '"></span>';
$Wrapttributes = array(
'class' => 'UserReactionWrap',
'data-userid' => $User->UserID,
'title' => $User->Name . ' - ' . $Reaction->Name . ' on ' . Gdn_Format::Date($Reaction->Dates[$Index], '%B %e, %Y')
);
echo Wrap($String, 'span', $Wrapttributes);
}
}
}
}
/**
* Add and action list to discussion items
* @param DiscussionController $Sender
*/
public function DiscussionController_AfterReactions_Handler($Sender) {
if(C('Yaga.Reactions.Enabled') == FALSE) {
return;
}
$Sender->AddJsFile('reactions.js', 'yaga');
$Sender->AddCssFile('reactions.css', 'yaga');
// check to see if allowed to add reactions
if(!Gdn::Session()->CheckPermission('Plugins.Reactions.Add')) {
return;
}
// Users shouldn't be able to react to their own content
$Type = $Sender->EventArguments['RecordType'];
$ID = $Sender->EventArguments['RecordID'];
// Users shouldn't be able to react to their own content
if(Gdn::Session()->UserID != $Sender->EventArguments['Author']->UserID) {
$this->_RenderActions($ID, $Type);
}
}
/**
* Add the action list to any activity items that can be commented on
* @param ActivityController $Sender
*/
public function ActivityController_AfterActivityBody_Handler($Sender) {
$Activity = $Sender->EventArguments['Activity'];
$CurrentUserID = Gdn::Session()->UserID;
$Type = 'activity';
$ID = $Activity->ActivityID;
// Only allow reactions on activities that allow comments
if($Activity->AllowComments == 0) {
return;
}
// check to see if allowed to add reactions
if(!Gdn::Session()->CheckPermission('Plugins.Reactions.Add')) {
return;
}
// Activities can be by multiple users
if(is_array($Activity->ActivityUserID) && in_array($CurrentUserID, $Activity->ActivityUserID)) {
// User is part of a multiple user activity
}
else if($CurrentUserID == $Activity->ActivityUserID) {
// User is the author of this activity
}
else {
echo Wrap($this->_RenderActions($ID, $Type, FALSE), 'div', array('class' => 'Reactions'));
}
}
/**
* Renders an action list that also contains the current count of reactions
* an item has received
* @param int $ID
* @param enum $Type 'discussion', 'activity', or 'comment'
* @param bool $Echo Should it be echoed?
* @return mixed String if $Echo is false, TRUE otherwise
*/
public function _RenderActions($ID, $Type, $Echo = TRUE) {
if(empty($this->_ReactionModel)) {
$this->_ReactionModel = new ReactionModel();
}
$Reactions = $this->_ReactionModel->GetReactions($ID, $Type);
//decho($Reactions);
$ActionsString = '';
foreach($Reactions as $Action) {
$ActionsString .= Anchor(
Wrap('&nbsp;', 'span', array('class' => 'ReactSprite React-' . $Action->ActionID)) .
WrapIf(count($Action->UserIDs), 'span', array('class' => 'Count')) .
Wrap($Action->Name, 'span', array('class' => 'ReactLabel')), 'react/' . $Type . '/' . $ID . '/' . $Action->ActionID, 'Hijack ReactButton'
);
}
$AllActionsString = Wrap($ActionsString, 'span', array('class' => 'ReactMenu'));
if($Echo) {
echo $AllActionsString;
return true;
}
else {
return $AllActionsString;
}
}
/**
* Insert JS and CSS files into the appropiate controllers
*/
public function ProfileController_Render_Before($Sender) {
$this->_AddResources($Sender);
}
public function DiscussionController_Render_Before($Sender) {
$this->_AddResources($Sender);
}
public function CommentController_Render_Before($Sender) {
$this->_AddResources($Sender);
}
private function _AddResources($Sender) {
if(empty($this->_ReactionModel)) {
$this->_ReactionModel = new ReactionModel();
}
$Sender->AddCssFile('reactions.css', 'yaga');
}
/**
* Add resources to all dashboard pages
* @param type $Sender
*/
public function Base_Render_Before($Sender) {
if($Sender->MasterView == 'admin') {
$Sender->AddCssFile('yaga.css', 'yaga');
}
}
/**
* Special function automatically run upon clicking 'Enable' on your application.
* Change the word 'skeleton' anywhere you see it.
*/
public function Setup() {
// You need to manually include structure.php here for it to get run at install.
$Config = Gdn::Factory(Gdn::AliasConfig);
$Drop = C('Yaga.Version') === FALSE ? TRUE : FALSE;
$Explicit = TRUE;
include(PATH_APPLICATIONS . DS . 'yaga' . DS . 'settings' . DS . 'structure.php');
include(PATH_APPLICATIONS . DS . 'yaga' . DS . 'settings' . DS . 'stub.php');
// Stores a value in the config to indicate it has previously been installed.
// You can use if(C('Skeleton.Setup', FALSE)) to test whether to repeat part of your setup.
SaveToConfig('Yaga.Setup', TRUE);
$ApplicationInfo = array();
include(CombinePaths(array(PATH_APPLICATIONS . DS . 'yaga' . DS . 'settings' . DS . 'about.php')));
$Version = ArrayValue('Version', ArrayValue('Yaga', $ApplicationInfo, array()), 'Undefined');
SaveToConfig('Yaga.Version', $Version);
}
/**
* Add the settings page links
* @param Mixed $Sender
*/
public function Base_GetAppSettingsMenuItems_Handler($Sender) {
$Menu = $Sender->EventArguments['SideMenu'];
$Section = 'Gamification';
$Attrs = array('class' => $Section);
$Menu->AddItem($Section, $Section, FALSE, $Attrs);
$Menu->AddLink($Section, 'Settings', 'configure', 'Garden.Settings.Manage');
if(C('Yaga.Reactions.Enabled')) {
$Menu->AddLink($Section, 'Reactions', 'actions/settings', 'Yaga.Reactions.Manage');
}
if(C('Yaga.Badges.Enabled')) {
$Menu->AddLink($Section, 'Badges', 'badges/settings', 'Yaga.Badges.Manage');
}
if(C('Yaga.Ranks.Enabled')) {
$Menu->AddLink($Section, 'Ranks', 'ranks/settings', 'Yaga.Ranks.Manage');
}
}
/**

View File

@ -10,9 +10,9 @@ if(!isset($Explicit)) {
}
$Database = Gdn::Database();
$SQL = $Database->SQL(); // To run queries.
//$SQL = $Database->SQL(); // To run queries.
$Construct = $Database->Structure(); // To modify and add database tables.
$Validation = new Gdn_Validation(); // To validate permissions (if necessary).
//$Validation = new Gdn_Validation(); // To validate permissions (if necessary).
$Construct->Table('Reaction')
->PrimaryKey('ReactionID')
@ -20,6 +20,7 @@ $Construct->Table('Reaction')
->Column('ActionID', 'int', FALSE, 'index')
->Column('ParentID', 'int', TRUE)
->Column('ParentType', array('discussion', 'comment', 'activity'), TRUE)
->Column('ParentAuthorID', 'int', FALSE)
->Column('DateInserted', 'datetime')
->Set($Explicit, $Drop);

22
settings/stub.php Normal file
View File

@ -0,0 +1,22 @@
<?php if (!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
// Only do this once, ever.
if (!$Drop)
return;
$SQL = Gdn::Database()->SQL();
// Insert stub actions
$SQL->Insert('Action', array(
'Name' => 'Thumbs Up',
'Description' => 'I approve!',
'Tooltip' => 'This indicates casual approval',
'AwardValue' => 1
));
$SQL->Insert('Action', array(
'Name' => 'Thumbs Down',
'Description' => 'I disapprove!',
'Tooltip' => 'This indicates casual disapproval',
'AwardValue' => -1
));

View File

@ -1,3 +0,0 @@
The default theme does not have any customized views.
If you were creating a theme that required some customization, the
themes/your_theme/views folder would be the place to put them.

39
views/actions/add.php Normal file
View File

@ -0,0 +1,39 @@
<?php if (!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
if (is_object($this->Action)) {
echo Wrap(T('Edit Action'), 'h1');
}
else {
echo Wrap(T('Add Action'), 'h1');
}
echo $this->Form->Open(array('class' => 'Action'));
echo $this->Form->Errors();
?>
<ul>
<li>
<?php
echo $this->Form->Label('Username', 'Name');
echo $this->Form->TextBox('Name');
?>
</li>
<li>
<?php
echo $this->Form->Label('Description', 'Description');
echo $this->Form->TextBox('Description');
?>
</li>
<li>
<?php
echo $this->Form->Label('Tooltip', 'Tooltip');
echo $this->Form->TextBox('Tooltip');
?>
</li>
<li>
<?php
echo $this->Form->Label('Award Value', 'AwardValue');
echo $this->Form->TextBox('AwardValue');
?>
</li>
</ul>
<?php echo $this->Form->Close('Save');

View File

@ -0,0 +1,33 @@
<?php if (!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
echo Wrap($this->Title(), 'h1');
echo Wrap(Wrap('Add or edit the available actions that can be used as reactions.', 'div'), 'div', array('class' => 'Wrap'));
echo Wrap(Anchor('Add Action', 'yaga/actions/add', array('class' => 'Popup SmallButton')), 'div', array('class' => 'Wrap'));
?>
<table id="Actions" class="AltRows">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Tooltip</th>
<th>Award Value</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<?php
$Alt = '';
foreach($this->Data('Actions') as $Action) {
echo '<tr id="ActionID_' . $Action->ActionID . '" data-actionid="'. $Action->ActionID . '"' . ($Alt ? ' class="Alt"' : '') . '>';
echo "<td>$Action->Name</td>";
echo "<td>$Action->Description</td>";
echo "<td>$Action->Tooltip</td>";
echo "<td>$Action->AwardValue</td>";
echo '<td>' . Anchor(T('Edit'), 'yaga/actions/edit/' . $Action->ActionID, array('class' => 'Popup SmallButton')) . Anchor(T('Delete'), 'yaga/actions/delete/' . $Action->ActionID, array('class' => 'Hijack SmallButton')) . '</td>';
echo '</tr>';
}
?>
</tbody>
</table>