Commit 4d99c42f authored by Zachary Doll's avatar Zachary Doll

Merge 1.1 development into master

parents 0a10a8ae 1b83ac54
This diff is collapsed.
......@@ -2,7 +2,14 @@
**Y**​et **A**​nother **G**​amification **A**​pplication is a Garden application that provides a gamification platform for Vanilla Forums and other Garden applications. It integrates by default with Vanilla Forums. Out of the box, it provides Reactions, Badges, and Ranks.
It is released under the GPLv3 and may be released under a different license _**with permission**_.
It is released under the GPLv2 and may be released under a different license _**with permission**_.
## Requirements
Yaga requires:
* Vanilla 2.2.x (Garden is the actual requirement)
* Pretty URLs enabled
## Contributing
......@@ -13,4 +20,4 @@ Please contribute fixes against the `develop` branch.
Yaga's [main documentation page is here](http://hgtonight.github.io/Application-Yaga/).
---
Copyright 2013 - 2014 © Zachary Doll
Copyright 2013 - 2016 © Zachary Doll
These terms apply to your contribution of materials to a product or project owned or managed by us ('project'), and set out the intellectual property rights you grant to us (Zachary Doll) in the contributed materials. If this contribution is on behalf of a company, the term 'you' will also mean the company for which you are making the contribution. If you agree to be bound by these terms, check the box below.
Read this agreement carefully before agreeing.
1. The term 'contribution' means any source code, object code, patch, tool, sample, graphic, specification, manual, documentation, or any other material posted or submitted by you to a project.
2. With respect to any worldwide copyrights, or copyright applications and registrations, in your contribution:
* you hereby assign to us joint ownership, and to the extent that such assignment is or becomes invalid, ineffective or unenforceable, you hereby grant to us a perpetual, irrevocable, non-exclusive, worldwide, no-charge, royalty-free, unrestricted license to exercise all rights under those copyrights. This includes, at our option, the right to sublicense these same rights to third parties through multiple levels of sublicensees or other licensing arrangements;
* you agree that each of us can do all things in relation to your contribution as if each of us were the sole owners, and if one of us makes a derivative work of your contribution, the one who makes the derivative work (or has it made) will be the sole owner of that derivative work;
* you agree that you will not assert any moral rights in your contribution against us, our licensees or transferees;
* you agree that we may register a copyright in your contribution and exercise all ownership rights associated with it; and
* you agree that neither of us has any duty to consult with, obtain the consent of, pay or render an accounting to the other for any use or distribution of your contribution.
3. With respect to any patents you own, or that you can license without payment to any third party, you hereby grant to us a perpetual, irrevocable, non-exclusive, worldwide, no-charge, royalty-free license to:
* make, have made, use, sell, offer to sell, import, and otherwise transfer your contribution in whole or in part, alone or in combination with or included in any product, work or materials arising out of the project to which your contribution was submitted, and
* at our option, to sublicense these same rights to third parties through multiple levels of sublicensees or other licensing arrangements.
4. Except as set out above, you keep all right, title, and interest in your contribution. The rights that you grant to us under these terms are effective on the date you first submitted a contribution to us, even if your submission took place before the date you sign these terms. Any contribution we make available under any license will also be made available under a suitable FSF (Free Software Foundation) or OSI (Open Source Initiative) approved license.
5. With respect to your contribution, you represent that:
* it is an original work and that you can legally grant the rights set out in these terms;
* it does not to the best of your knowledge violate any third party's copyrights, trademarks, patents, or other intellectual property rights; and
* you are authorized to sign this contract on behalf of your company (if you are making the contribution on behalf of a company).
6. These terms will be governed by the laws of the State of North Dakota and applicable US Federal law. Any choice of law rules will not apply.
......@@ -31,6 +31,7 @@ class ActionController extends DashboardController {
$this->AddJsFile('jquery-ui-1.10.0.custom.min.js');
$this->AddJsFile('admin.actions.js');
$this->AddCssFile('reactions.css');
$this->removeCssFile('magnific-popup.css');
}
/**
......@@ -89,22 +90,17 @@ class ActionController extends DashboardController {
}
}
else {
if($this->Form->Save()) {
if($Edit) {
$Action = $this->ActionModel->GetByID($this->Form->GetFormValue('ActionID'));
}
else {
$Action = $this->ActionModel->GetNewestAction();
}
$NewActionRow = ActionRow($Action);
$NewID = $this->Form->Save();
if($NewID) {
$Action = $this->ActionModel->GetByID($NewID);
$ActionRow = RenderActionRow($Action);
if($Edit) {
$this->JsonTarget('#ActionID_' . $this->Action->ActionID, $NewActionRow, 'ReplaceWith');
$this->JsonTarget('#ActionID_' . $this->Action->ActionID, $ActionRow, 'ReplaceWith');
$this->InformMessage(T('Yaga.ActionUpdated'));
}
else {
$this->JsonTarget('#Actions', $NewActionRow, 'Append');
$this->JsonTarget('#Actions', $ActionRow, 'Append');
$this->InformMessage(T('Yaga.Action.Added'));
}
}
......
......@@ -28,8 +28,10 @@ class BadgeController extends DashboardController {
if($this->Menu) {
$this->Menu->HighlightRoute('/badge');
}
$this->AddJsFile('jquery-ui-1.10.0.custom.min.js');
$this->AddJsFile('admin.badges.js');
$this->AddCssFile('badges.css');
$this->removeCssFile('magnific-popup.css');
}
/**
......@@ -95,12 +97,13 @@ class BadgeController extends DashboardController {
if($TmpImage) {
// Generate the target image name
$TargetImage = $Upload->GenerateTargetName(PATH_UPLOADS);
$TargetImage = $Upload->GenerateTargetName(PATH_UPLOADS, FALSE);
$ImageBaseName = pathinfo($TargetImage, PATHINFO_BASENAME);
// Save the uploaded image
$Parts = $Upload->SaveAs($TmpImage, 'yaga' . DS . $ImageBaseName);
$RelativeUrl = StringBeginsWith($Parts['Url'], Gdn_Url::WebRoot(TRUE), TRUE, TRUE);
$AssetRoot = Gdn::Request()->UrlDomain(TRUE).Gdn::Request()->AssetRoot();
$RelativeUrl = StringBeginsWith($Parts['Url'], $AssetRoot, TRUE, TRUE);
$this->Form->SetFormValue('Photo', $RelativeUrl);
}
......@@ -307,4 +310,28 @@ class BadgeController extends DashboardController {
$this->Render();
}
/**
* This takes in a sort array and updates the badge sort order.
*
* Renders the Save tree and/or the Result of the sort update.
*
* @since 1.1
*/
public function Sort() {
// Check permission
$this->Permission('Yaga.Badges.Manage');
$Request = Gdn::Request();
if($Request->IsPostBack()) {
$SortArray = $Request->GetValue('SortArray', NULL);
$Saves = $this->BadgeModel->SaveSort($SortArray);
$this->SetData('Result', TRUE);
$this->SetData('Saves', $Saves);
}
else {
$this->SetData('Result', FALSE);
}
$this->RenderData();
}
}
<?php if(!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
/**
* This is all the frontend pages dealing with badges
*
* @since 1.0
* @package Yaga
*/
class BadgesController extends Gdn_Controller {
/**
* @var array These objects will be created on instantiation and available via
* $this->ObjectName
*/
public $Uses = array('BadgeModel', 'BadgeAwardModel');
/**
* This sets the badges controller up to look like a front end page. It also
* adds some leader board modules.
*/
public function Initialize() {
parent::Initialize();
$this->Application = 'Yaga';
$this->Head = new HeadModule($this);
$this->AddJsFile('jquery.js');
$this->AddJsFile('jquery-ui.js');
$this->AddJsFile('jquery.livequery.js');
$this->AddJsFile('jquery.popup.js');
$this->AddJsFile('global.js');
$this->AddCssFile('style.css');
$this->AddCssFile('badges.css');
$this->AddModule('BadgesModule');
$Module = new LeaderBoardModule();
$Module->GetData('w');
$this->AddModule($Module);
$Module = new LeaderBoardModule();
$this->AddModule($Module);
}
/**
* Render a blank page if no methods were specified in dispatch
*/
public function Index() {
//$this->Render('Blank', 'Utility', 'Dashboard');
$this->All();
}
/**
* This renders out the full list of badges
*/
public function All() {
$this->Title(T('Yaga.Badges.All'));
$UserID = Gdn::Session()->UserID;
// Get list of badges from the model and pass to the view
$AllBadges = $this->BadgeModel->GetWithEarned($UserID);
$this->SetData('Badges', $AllBadges);
//$this->SetData('Earned')
$this->Render('all');
}
/**
* Show some facets about a specific badge
*
* @param int $BadgeID
* @param string $Slug
* @throws NotFoundException
*/
public function Detail($BadgeID, $Slug = NULL) {
$UserID = Gdn::Session()->UserID;
$Badge = $this->BadgeModel->GetByID($BadgeID);
$AwardCount = $this->BadgeAwardModel->GetCount($BadgeID);
$UserBadgeAward = $this->BadgeAwardModel->Exists($UserID, $BadgeID);
$RecentAwards = $this->BadgeAwardModel->GetRecent($BadgeID);
if(!$Badge) {
throw NotFoundException('Badge');
}
$this->SetData('AwardCount', $AwardCount);
$this->SetData('RecentAwards', $RecentAwards);
$this->SetData('UserBadgeAward', $UserBadgeAward);
$this->SetData('Badge', $Badge);
$this->Title(T('Yaga.Badge.View') . $Badge->Name);
$this->Render();
}
}
......@@ -49,7 +49,7 @@ class BestController extends Gdn_Controller {
public function Index($Page = 0) {
list($Offset, $Limit) = $this->_TranslatePage($Page);
$this->Title(T('Yaga.BestContent.Recent'));
$this->_Content = $this->ActedModel->GetRecent('week', $Limit, $Offset);
$this->_Content = $this->ActedModel->GetRecent($Limit, $Offset);
$this->_BuildPager($Offset, $Limit, '/best/%1$s/');
$this->SetData('ActiveFilter', 'Recent');
$this->Render('index');
......
......@@ -30,6 +30,7 @@ class RankController extends DashboardController {
}
$this->AddJsFile('jquery-ui-1.10.0.custom.min.js');
$this->AddJsFile('admin.ranks.js');
$this->removeCssFile('magnific-popup.css');
}
/**
......@@ -51,12 +52,13 @@ class RankController extends DashboardController {
if($TmpImage) {
// Generate the target image name
$TargetImage = $Upload->GenerateTargetName(PATH_UPLOADS);
$TargetImage = $Upload->GenerateTargetName(PATH_UPLOADS, FALSE);
$ImageBaseName = pathinfo($TargetImage, PATHINFO_BASENAME);
// Save the uploaded image
$Parts = $Upload->SaveAs($TmpImage, 'yaga' . DS . $ImageBaseName);
$RelativeUrl = StringBeginsWith($Parts['Url'], Gdn_Url::WebRoot(TRUE), TRUE, TRUE);
$AssetRoot = Gdn::Request()->UrlDomain(TRUE).Gdn::Request()->AssetRoot();
$RelativeUrl = StringBeginsWith($Parts['Url'], $AssetRoot, TRUE, TRUE);
SaveToConfig('Yaga.Ranks.Photo', $RelativeUrl);
if(C('Yaga.Ranks.Photo') == $Parts['SaveName']) {
......@@ -196,7 +198,7 @@ class RankController extends DashboardController {
$this->Permission('Yaga.Ranks.Manage');
$this->AddSideMenu('rank/settings');
$Rank = $this->RankModel->Get($RankID);
$Rank = $this->RankModel->GetByID($RankID);
if($Rank->Enabled) {
$Enable = FALSE;
......@@ -211,7 +213,7 @@ class RankController extends DashboardController {
$Slider = Wrap(Wrap(Anchor($ToggleText, 'rank/toggle/' . $Rank->RankID, 'Hijack SmallButton'), 'span', array('class' => "ActivateSlider ActivateSlider-{$ActiveClass}")), 'td');
$this->RankModel->Enable($RankID, $Enable);
$this->JsonTarget('#RankID_' . $RankID . ' td:nth-child(5)', $Slider, 'ReplaceWith');
$this->JsonTarget('#RankID_' . $RankID . ' td:nth-child(6)', $Slider, 'ReplaceWith');
$this->Render('Blank', 'Utility', 'Dashboard');
}
......
......@@ -49,28 +49,35 @@ class ReactController extends Gdn_Controller {
throw PermissionException();
}
switch($Type) {
case 'discussion':
$Model = new DiscussionModel();
$AnchorID = '#Discussion_';
break;
case 'comment':
$Model = new CommentModel();
$AnchorID = '#Comment_';
break;
case 'activity':
$Model = new ActivityModel();
$AnchorID = '#Activity_';
break;
default:
$Item = null;
$AnchorID = '#' . ucfirst($Type) . '_';
$ItemOwnerID = 0;
if(in_array($Type, array('discussion', 'comment'))) {
$Item = GetRecord($Type, $ID);
}
else if($Type == 'activity') {
$Model = new ActivityModel();
$Item = $Model->GetID($ID, DATASET_TYPE_ARRAY);
}
else {
$this->EventArguments = array(
'TypeFound' => FALSE,
'TargetType' => $Type,
'TargetID' => $ID,
'Item' => &$Item,
'AnchorID' => &$AnchorID,
'ItemOwnerID' => &$ItemOwnerID
);
$this->FireEvent('CustomType');
if(!$this->EventArguments['TypeFound']) {
throw new Gdn_UserException(T('Yaga.Action.InvalidTargetType'));
break;
}
}
$Item = $Model->GetID($ID);
if($Item) {
$Anchor = $AnchorID . $ID . ' .ReactMenu';
$Anchor = $AnchorID . $ID;
}
else {
throw new Gdn_UserException(T('Yaga.Action.InvalidTargetID'));
......@@ -81,13 +88,12 @@ class ReactController extends Gdn_Controller {
switch($Type) {
case 'comment':
case 'discussion':
$ItemOwnerID = $Item->InsertUserID;
$ItemOwnerID = $Item['InsertUserID'];
break;
case 'activity':
$ItemOwnerID = $Item['RegardingUserID'];
break;
default:
throw new Gdn_UserException(T('Yaga.Action.InvalidTargetType'));
break;
}
......@@ -98,7 +104,8 @@ class ReactController extends Gdn_Controller {
// It has passed through the gauntlet
$this->ReactionModel->Set($ID, $Type, $ItemOwnerID, $UserID, $ActionID);
$this->JsonTarget($Anchor, RenderReactionList($ID, $Type, FALSE), 'ReplaceWith');
$this->JsonTarget($Anchor . ' .ReactMenu', RenderReactionList($ID, $Type), 'ReplaceWith');
$this->JsonTarget($Anchor . ' .ReactionRecord', RenderReactionRecord($ID, $Type), 'ReplaceWith');
// Don't render anything
$this->Render('Blank', 'Utility', 'Dashboard');
......
......@@ -43,6 +43,14 @@ class RulesController extends Gdn_Controller {
$TempRules[$className] = $Rule->Name();
}
}
// TODO: Don't reuse badge model?
$Model = Yaga::BadgeModel();
$Model->EventArguments['Rules'] = &$TempRules;
$Model->FireAs = 'Yaga';
$Model->FireEvent('AfterGetRules');
asort($TempRules);
if(empty($TempRules)) {
$Rules = serialize(FALSE);
}
......
......@@ -31,10 +31,13 @@ class YagaController extends DashboardController {
$this->AddSideMenu('yaga/settings');
$this->AddCssFile('yaga.css');
$this->removeCssFile('magnific-popup.css');
}
/**
* Redirect to settings by default
*
* @since 1.0
*/
public function Index() {
$this->Settings();
......@@ -42,6 +45,8 @@ class YagaController extends DashboardController {
/**
* This handles all the core settings for the gamification application.
*
* @since 1.0
*/
public function Settings() {
$this->Permission('Garden.Settings.Manage');
......@@ -63,6 +68,10 @@ class YagaController extends DashboardController {
'LabelCode' => 'Yaga.Ranks.Use',
'Control' => 'Checkbox'
),
'Yaga.MenuLinks.Show' => array(
'LabelCode' => 'Yaga.MenuLinks.Show',
'Control' => 'Checkbox'
),
'Yaga.LeaderBoard.Enabled' => array(
'LabelCode' => 'Yaga.LeaderBoard.Use',
'Control' => 'Checkbox'
......@@ -78,11 +87,112 @@ class YagaController extends DashboardController {
));
$this->ConfigurationModule = $ConfigModule;
$this->Render();
$this->Render('settings');
}
/**
* Performs the necessary functions to change a backend controller into a
* frontend controller
*
* @since 1.1
*/
private function FrontendStyle() {
$this->RemoveCssFile('admin.css');
unset($this->Assets['Panel']['SideMenuModule']);
$this->AddCssFile('style.css');
$this->MasterView = 'default';
$WeeklyModule = new LeaderBoardModule();
$WeeklyModule->SlotType = 'w';
$this->AddModule($WeeklyModule);
$AllTimeModule = new LeaderBoardModule();
$this->AddModule($AllTimeModule);
}
/**
* Displays a summary of ranks currently configured on a frontend page to help
* users understand what is valued in this community
*
* @since 1.1
*/
public function Ranks() {
$this->permission('Yaga.Ranks.View');
$this->FrontendStyle();
$this->AddCssFile('ranks.css');
$this->Title(T('Yaga.Ranks.All'));
// Get list of ranks from the model and pass to the view
$this->SetData('Ranks', Yaga::RankModel()->Get());
$this->Render('ranks');
}
/**
* Displays a summary of badges currently configured on a frontend page to
* help users understand what is valued in this community.
*
* Also provides a convenience redirect to badge details
*
* @param int $BadgeID The badge ID you want to see details
* @param string $Slug The badge slug you want to see details
* @since 1.1
*/
public function Badges($BadgeID = FALSE, $Slug = NULL) {
$this->permission('Yaga.Badges.View');
$this->FrontendStyle();
$this->AddCssFile('badges.css');
$this->AddModule('BadgesModule');
if(is_numeric($BadgeID)) {
return $this->BadgeDetail($BadgeID, $Slug);
}
$this->Title(T('Yaga.Badges.All'));
// Get list of badges from the model and pass to the view
$UserID = Gdn::Session()->UserID;
$AllBadges = Yaga::BadgeModel()->GetWithEarned($UserID);
$this->SetData('Badges', $AllBadges);
$this->Render('badges');
}
/**
* Displays information about the specified badge including recent recipients
* of the badge.
*
* @param int $BadgeID
* @param string $Slug
*/
public function BadgeDetail($BadgeID, $Slug = NULL) {
$this->permission('Yaga.Badges.View');
$Badge = Yaga::BadgeModel()->GetByID($BadgeID);
if(!$Badge) {
throw NotFoundException('Badge');
}
$UserID = Gdn::Session()->UserID;
$BadgeAwardModel = Yaga::BadgeAwardModel();
$AwardCount = $BadgeAwardModel->GetCount($BadgeID);
$UserBadgeAward = $BadgeAwardModel->Exists($UserID, $BadgeID);
$RecentAwards = $BadgeAwardModel->GetRecent($BadgeID);
$this->SetData('AwardCount', $AwardCount);
$this->SetData('RecentAwards', $RecentAwards);
$this->SetData('UserBadgeAward', $UserBadgeAward);
$this->SetData('Badge', $Badge);
$this->Title(T('Yaga.Badge.View') . $Badge->Name);
$this->Render('badgedetail');
}
/**
* Import a Yaga transport file
*
* @since 1.0
*/
public function Import() {
$this->Title(T('Yaga.Import'));
......@@ -130,6 +240,8 @@ class YagaController extends DashboardController {
/**
* Create a Yaga transport file
*
* @since 1.0
*/
public function Export() {
$this->Title(T('Yaga.Export'));
......@@ -163,6 +275,7 @@ class YagaController extends DashboardController {
* Yaga sections to be included in the transport file.
*
* @return array
* @since 1.0
*/
protected function _FindIncludes() {
$FormValues = $this->Form->FormValues();
......@@ -183,6 +296,7 @@ class YagaController extends DashboardController {
* @param array An array containing the config areas to transfer
* @param string Where to save the transport file
* @return mixed False on failure, the path to the transport file on success
* @since 1.0
*/
protected function _ExportData($Include = array(), $Path = NULL) {
$StartTime = microtime(TRUE);
......@@ -287,6 +401,7 @@ class YagaController extends DashboardController {
*
* @param string The transport file path
* @return boolean Whether or not the transport file was extracted successfully
* @since 1.0
*/
protected function _ExtractZip($Filename) {
if(!file_exists($Filename)) {
......@@ -331,6 +446,7 @@ class YagaController extends DashboardController {
* @param array Which tables should be overwritten
* @return bool Pass/Fail on the import being executed. Errors can exist on the
* form with a passing return value.
* @since 1.0
*/
protected function _ImportData($Info, $Include) {
if(!$Info) {
......@@ -373,6 +489,7 @@ class YagaController extends DashboardController {
* @param array The nested array
* @param string What should the configuration strings be prefixed with
* @return array
* @since 1.0
*/
protected function _NestedToDotNotation($Configs, $Prefix = '') {
$ConfigStrings = array();
......@@ -394,6 +511,7 @@ class YagaController extends DashboardController {
*
* @param stdClass The metadata object read in from the transport file
* @return boolean Whether or not the checksum is valid
* @since 1.0
*/
protected function _ValidateChecksum($MetaData) {
$Hashes = array();
......@@ -433,19 +551,20 @@ class YagaController extends DashboardController {
}
/**
* Returns a list of all files in a directory, recursively (Thanks @businessdad)
*
* @param string Directory The directory to scan for files
* @return array A list of Files and, optionally, Directories.
*/
protected function _GetFiles($Directory) {
* Returns a list of all files in a directory, recursively (Thanks @businessdad)
*
* @param string Directory The directory to scan for files
* @return array A list of Files and, optionally, Directories.
* @since 1.0
*/
protected function _GetFiles($Directory) {
$Files = array_diff(scandir($Directory), array('.', '..'));
$Result = array();
foreach($Files as $File) {
$FileName = $Directory . '/' . $File;
if(is_dir($FileName)) {
$Result = array_merge($Result, $this->_GetFiles($FileName));
continue;
$Result = array_merge($Result, $this->_GetFiles($FileName));
continue;
}
$Result[] = $FileName;
}
......
......@@ -24,7 +24,8 @@
}
/* Badge detail page */
.ProfilePhotoMedium {
.Box .ProfilePhotoMedium,
.ContentColumn .ProfilePhotoMedium {
height: 24px;
margin-bottom: 3px;
margin-right: 3px;
......