Version 4.7.0
This commit is contained in:
1 parent
517a5e1f70
commit
8ba7d43898
1933 files changed
+65613
-11332
No files matched your search
@@ -77,22 +77,25 @@ abstract class _AbstractAchievementAction
|
||||
/**
|
||||
* Get the labels for the people this action might give awards to
|
||||
*
|
||||
* @param array|NULL $filters Current filter values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function awardOptions(): array
|
||||
public function awardOptions( ?array $filters ): array
|
||||
{
|
||||
return [
|
||||
'subject' => '',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the "other" people we need to award =stuff to
|
||||
*
|
||||
* @param mixed $extra Any additional information about what is happening (e.g. if a post is being made: the post object)
|
||||
* @param mixed $extra Any additional information about what is happening (e.g. if a post is being made: the post object)
|
||||
* @param array|NULL $filters Current filter values
|
||||
* @return array
|
||||
*/
|
||||
public function awardOther( $extra = NULL ): array
|
||||
public function awardOther( $extra = NULL, ?array $filters = NULL ): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
+31
-4
@@ -122,7 +122,7 @@ abstract class _AbstractContentAchievementAction extends AbstractAchievementActi
|
||||
$return[ $k ] = $v;
|
||||
}
|
||||
$return['milestone'] = $nthFilter;
|
||||
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ abstract class _AbstractContentAchievementAction extends AbstractAchievementActi
|
||||
{
|
||||
$item = $extra->item();
|
||||
}
|
||||
|
||||
|
||||
if ( isset( $filters[ 'nodes_' . str_replace( '\\', '-', \get_class( $item ) ) ] ) )
|
||||
{
|
||||
if ( !\in_array( $item->container()->_id, $filters[ 'nodes_' . str_replace( '\\', '-', \get_class( $item ) ) ] ) )
|
||||
@@ -193,9 +193,36 @@ abstract class _AbstractContentAchievementAction extends AbstractAchievementActi
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
else
|
||||
{
|
||||
/* Make sure the class is a contentRouter approved class, mostly to stop private messages being awarded points */
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $extension )
|
||||
{
|
||||
foreach ( $extension->classes as $class )
|
||||
{
|
||||
if ( \get_class( $extra ) == $class )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
elseif ( isset( $class::$commentClass ) and \get_class( $extra ) == $class::$commentClass )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
elseif ( isset( $class::$reviewClass ) and \get_class( $extra ) == $class::$reviewClass )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Stop PHPStorm from complaining about incorrect return types >_> */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,6 +68,7 @@ class _Badge extends \IPS\Node\Model
|
||||
'app' => 'core',
|
||||
'module' => 'achievements',
|
||||
'prefix' => 'badges_',
|
||||
'all' => 'badges_manage',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -135,8 +136,11 @@ class _Badge extends \IPS\Node\Model
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
/* Allow SVGs without the obscure hash removing the file extension */
|
||||
\IPS\File::$safeFileExtensions[] = 'svg';
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'badge_name', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_badges_{$this->id}" : NULL ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'badge_image', $this->image ? \IPS\File::get( 'core_Badges', $this->image ) : NULL, TRUE, array( 'obscure' => FALSE, 'checkImage' => TRUE, 'allowedFileTypes' => array_merge( \IPS\Image::$imageExtensions, ['svg'] ), 'storageExtension' => 'core_Badges', 'allowStockPhotos' => FALSE ), function( $val ) {
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'badge_image', $this->image ? \IPS\File::get( 'core_Badges', $this->image ) : NULL, TRUE, array( 'obscure' => TRUE, 'checkImage' => TRUE, 'allowedFileTypes' => array_merge( \IPS\Image::supportedExtensions(), ['svg'] ), 'storageExtension' => 'core_Badges', 'allowStockPhotos' => FALSE ), function( $val ) {
|
||||
if( !$val )
|
||||
{
|
||||
throw new \DomainException('achievements_bad_image');
|
||||
@@ -228,12 +232,47 @@ class _Badge extends \IPS\Node\Model
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$stats = $this->getBadgeStats();
|
||||
|
||||
return ( 100 / $stats['memberCount'] * $stats['badgeCount'][ $this->id ] ) < \IPS\Settings::i()->rare_badge_percent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of people who have this badge
|
||||
*
|
||||
* @param bool|null $roundUp Round number to nearest 100 to be less specific
|
||||
* @return int
|
||||
*/
|
||||
public function ownedByCount( bool $roundUp=FALSE )
|
||||
{
|
||||
if ( $stats = $this->getBadgeStats() and isset( $stats['badgeCount'][ $this->id ] ) )
|
||||
{
|
||||
if ( $roundUp )
|
||||
{
|
||||
return ceil( $stats['badgeCount'][$this->id] / 100 ) * 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $stats['badgeCount'][ $this->id ];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics for this badge
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getBadgeStats(): ?array
|
||||
{
|
||||
/* Fetch the counts we need from the datastore. If they are old or non-existent, refresh and store in datastore for next time. */
|
||||
$stats = NULL;
|
||||
|
||||
if( isset( \IPS\Data\Store::i()->badgeRarityStats ) )
|
||||
if( isset( \IPS\Data\Store::i()->badgeStats ) )
|
||||
{
|
||||
$stats = json_decode( \IPS\Data\Store::i()->badgeRarityStats, true );
|
||||
$stats = json_decode( \IPS\Data\Store::i()->badgeStats, true );
|
||||
|
||||
if( !isset( $stats['expiration'] ) OR time() > $stats['expiration'] OR !isset( $stats['badgeCount'][ $this->id ] ) )
|
||||
{
|
||||
@@ -243,21 +282,38 @@ class _Badge extends \IPS\Node\Model
|
||||
|
||||
if( $stats === NULL )
|
||||
{
|
||||
$exclude = json_decode( \IPS\Settings::i()->rules_exclude_groups, TRUE );
|
||||
$where = [ [ 'completed=?', true ] ];
|
||||
$subQuery = NULL;
|
||||
|
||||
if ( \is_array( $exclude ) and \count( $exclude ) )
|
||||
{
|
||||
$subQuery = \IPS\Db::i()->select( 'member_id', 'core_members', [ \IPS\Db::i()->in( 'member_group_id', $exclude ) ] );
|
||||
$where[] = [ \IPS\Db::i()->in( 'member_group_id', $exclude, TRUE ) ];
|
||||
}
|
||||
|
||||
$stats = array(
|
||||
'expiration' => time() + 3600, // Cache for 60 minutes
|
||||
'memberCount' => \IPS\Db::i()->select( 'COUNT(*)', 'core_members', [ 'completed=?', true ] )->first(),
|
||||
'expiration' => time() + ( 3600 * 6 ), // Cache for 6 hours
|
||||
'memberCount' => \IPS\Db::i()->select( 'COUNT(*)', 'core_members', $where )->first(),
|
||||
'badgeCount' => array(),
|
||||
);
|
||||
|
||||
foreach( \IPS\Db::i()->select( 'id', 'core_badges' ) as $badgeId )
|
||||
{
|
||||
$stats['badgeCount'][ $badgeId ] = \IPS\Db::i()->select( 'COUNT(*)', 'core_member_badges', [ 'badge=?', $badgeId ] )->first();
|
||||
$where = [ [ 'badge=?', $badgeId ] ];
|
||||
|
||||
if ( \is_array( $exclude ) and \count( $exclude ) )
|
||||
{
|
||||
$where[] = [ \IPS\Db::i()->in( 'core_member_badges.member', $subQuery, TRUE ) ];
|
||||
}
|
||||
|
||||
$stats['badgeCount'][ $badgeId ] = \IPS\Db::i()->select( 'COUNT(*)', 'core_member_badges', $where )->first();
|
||||
}
|
||||
|
||||
\IPS\Data\Store::i()->badgeRarityStats = json_encode( $stats );
|
||||
\IPS\Data\Store::i()->badgeStats = json_encode( $stats );
|
||||
}
|
||||
|
||||
return ( 100 / $stats['memberCount'] * $stats['badgeCount'][ $this->id ] ) < \IPS\Settings::i()->rare_badge_percent;
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -444,4 +500,24 @@ class _Badge extends \IPS\Node\Model
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse string name Name
|
||||
* @apiresponse array statistcs Badge Statistics
|
||||
*/
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL )
|
||||
{
|
||||
$return = array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->_title,
|
||||
'statistics' => $this->getBadgeStats(),
|
||||
);
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class _Rank extends \IPS\Node\Model
|
||||
{
|
||||
if ( $this->icon )
|
||||
{
|
||||
return \IPS\File::get( 'core_Badges', $this->icon );
|
||||
return \IPS\File::get( 'core_Ranks', $this->icon );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -197,9 +197,12 @@ class _Rank extends \IPS\Node\Model
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
/* Allow SVGs without the obscure hash removing the file extension */
|
||||
\IPS\File::$safeFileExtensions[] = 'svg';
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'member_ranks_word_custom', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_member_rank_{$this->id}" : NULL ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'member_ranks_points', $this->points ?: 0, TRUE, array( 'min' => 0 ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'member_ranks_icon', $this->icon ? \IPS\File::get( 'core_Badges', $this->icon ) : NULL, TRUE, array( 'obscure' => FALSE, 'allowedFileTypes' => array_merge( \IPS\Image::$imageExtensions, ['svg'] ), 'checkImage' => TRUE, 'storageExtension' => 'core_Badges' ), function( $val ) {
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'member_ranks_icon', $this->icon ? \IPS\File::get( 'core_Ranks', $this->icon ) : NULL, TRUE, array( 'obscure' => TRUE, 'allowedFileTypes' => array_merge( \IPS\Image::supportedExtensions(), ['svg'] ), 'checkImage' => TRUE, 'storageExtension' => 'core_Ranks' ), function( $val ) {
|
||||
if( !$val )
|
||||
{
|
||||
throw new \DomainException('achievements_bad_image');
|
||||
@@ -285,8 +288,8 @@ class _Rank extends \IPS\Node\Model
|
||||
{
|
||||
try
|
||||
{
|
||||
$image = \IPS\File::get( 'core_Badges', $oldImage );
|
||||
$newImage = \IPS\File::create( 'core_Badges', $image->originalFilename, $image->contents() );
|
||||
$image = \IPS\File::get( 'core_Ranks', $oldImage );
|
||||
$newImage = \IPS\File::create( 'core_Ranks', $image->originalFilename, $image->contents() );
|
||||
$this->icon = (string) $newImage;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
@@ -309,7 +312,7 @@ class _Rank extends \IPS\Node\Model
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\File::get( 'core_Badges', $this->icon )->delete();
|
||||
\IPS\File::get( 'core_Ranks', $this->icon )->delete();
|
||||
}
|
||||
catch( \Exception $ex ) { }
|
||||
}
|
||||
@@ -332,7 +335,7 @@ class _Rank extends \IPS\Node\Model
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->_title,
|
||||
'icon' => $this->icon ? $this->_icon->url : $this->_icon,
|
||||
'icon' => $this->icon ? (string) $this->_icon->url : $this->_icon,
|
||||
'points' => $this->points
|
||||
);
|
||||
}
|
||||
@@ -419,7 +422,7 @@ class _Rank extends \IPS\Node\Model
|
||||
|
||||
if ( ! empty( $insert['icon_name'] ) and ! empty( $insert['icon_data'] ) )
|
||||
{
|
||||
$insert['icon'] = (string) \IPS\File::create( 'core_Badges', $insert['icon_name'], $insert['icon_data'], NULL, TRUE, NULL, FALSE );
|
||||
$insert['icon'] = (string) \IPS\File::create( 'core_Ranks', $insert['icon_name'], $insert['icon_data'], NULL, TRUE, NULL, FALSE );
|
||||
|
||||
unset( $insert['icon_name'] );
|
||||
unset( $insert['icon_data'] );
|
||||
|
||||
@@ -102,7 +102,7 @@ class _Recognize extends \IPS\Patterns\ActiveRecord
|
||||
$member->awardPoints( $points, 0, [], ['subject'], $recognize->id );
|
||||
}
|
||||
|
||||
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_recognize', $member, [ $recognize ], [ $recognize->id ] );
|
||||
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_recognize', $member, [ $member, $recognize ], [ $recognize->id ] );
|
||||
$notification->recipients->attach( $member );
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class _Rule extends \IPS\Node\Model
|
||||
$extraElements[] = $filterElement;
|
||||
}
|
||||
|
||||
$awardTo = $achievementAction->awardOptions();
|
||||
$awardTo = $achievementAction->awardOptions( $this->filters );
|
||||
$awardSubjectField = new \IPS\Helpers\Form\Custom( "{$exploded[1]}_{$exploded[5]}_award_subject", [ 'points' => $this->points_subject, 'badge' => $this->badge_subject ], FALSE, [
|
||||
'getHtml' => function( $field ) {
|
||||
return \IPS\Theme::i()->getTemplate('achievements')->awardField( $field->name, $field->value );
|
||||
@@ -228,7 +228,7 @@ class _Rule extends \IPS\Node\Model
|
||||
{
|
||||
if ( isset( $values[$values['achievement_rule_action'] . '_award_' . $type . '_badge'] ) )
|
||||
{
|
||||
if ( $values[$values['achievement_rule_action'] . '_award_subject']['badge'] )
|
||||
if ( $values[$values['achievement_rule_action'] . '_award_' . $type]['badge'] )
|
||||
{
|
||||
\IPS\Lang::saveCustom( 'core', "core_award_" . $type . "_badge_{$this->id}", $values[$values['achievement_rule_action'] . '_award_' . $type . '_badge'] );
|
||||
}
|
||||
@@ -244,7 +244,7 @@ class _Rule extends \IPS\Node\Model
|
||||
$return = [
|
||||
'action' => $values['achievement_rule_action'],
|
||||
'filters' => $filters ? json_encode( $filters ) : NULL,
|
||||
'milestone' => isset( $filters['milestone'] ) ? $filters['milestone'] : NULL,
|
||||
'milestone' => ( isset( $filters['milestone'] ) and \is_numeric( $filters['milestone'] ) ) ? $filters['milestone'] : NULL,
|
||||
'points_subject'=> \intval( $values[ $values['achievement_rule_action'] . '_award_subject' ]['points'] ),
|
||||
'badge_subject' => $values[ $values['achievement_rule_action'] . '_award_subject' ]['badge'] ? \intval( $values[ $values['achievement_rule_action'] . '_award_subject' ]['badge'] ): NULL,
|
||||
'points_other' => \intval( isset( $values[ $values['achievement_rule_action'] . '_award_other' ] ) ? $values[ $values['achievement_rule_action'] . '_award_other' ]['points'] : 0 ),
|
||||
@@ -322,10 +322,33 @@ class _Rule extends \IPS\Node\Model
|
||||
{
|
||||
parent::delete();
|
||||
|
||||
\IPS\Db::i()->delete( 'core_achievements_log_milestones', array( 'milestone_rule=?', $this->id ) );
|
||||
|
||||
\IPS\Lang::deleteCustom( 'core', "core_award_subject_badge_{$this->id}" );
|
||||
\IPS\Lang::deleteCustom( 'core', "core_award_other_badge_{$this->id}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Clone the rule
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
if ( $this->skipCloneDuplication === TRUE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oldId = $this->id;
|
||||
|
||||
parent::__clone();
|
||||
|
||||
foreach ( array( 'subject' => "core_award_subject_badge_{$this->id}", 'other' => "core_award_other_badge_{$this->id}") as $fieldKey => $langKey ) {
|
||||
$oldLangKey = str_replace( $this->id, $oldId, $langKey );
|
||||
\IPS\Lang::saveCustom( 'core', $langKey, iterator_to_array(\IPS\Db::i()->select( 'word_custom, lang_id', 'core_sys_lang_words', array('word_key=?', $oldLangKey ) )->setKeyField('lang_id')->setValueField('word_custom' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details about the rebuild progress so far
|
||||
*
|
||||
@@ -360,6 +383,7 @@ class _Rule extends \IPS\Node\Model
|
||||
public static function rebuildAllAchievements( ?\IPS\DateTime $time )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_achievements_log' );
|
||||
\IPS\Db::i()->delete( 'core_achievements_log_milestones' );
|
||||
\IPS\Db::i()->delete( 'core_points_log' );
|
||||
\IPS\Db::i()->delete( 'core_member_badges', [ 'rule > 0' ] ); /* 0 is a manually awarded badge. Don't hate the coder, hate the player */
|
||||
\IPS\Db::i()->update( 'core_members', [ 'achievements_points' => 0 ] );
|
||||
@@ -410,7 +434,12 @@ class _Rule extends \IPS\Node\Model
|
||||
{
|
||||
foreach( $rules as $ruleId => $rule )
|
||||
{
|
||||
static::load( $ruleId )->delete();
|
||||
try
|
||||
{
|
||||
static::load( $ruleId )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ){}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,6 +479,12 @@ class _Rule extends \IPS\Node\Model
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Skip the forum related rule if we don't have forums installed */
|
||||
if ( $xml->name == 'action' AND !static::canImport( $xml->readString() ) )
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
switch ( $xml->name )
|
||||
{
|
||||
case 'action':
|
||||
@@ -566,4 +601,49 @@ class _Rule extends \IPS\Node\Model
|
||||
|
||||
unset( \IPS\Data\Store::i()->achievementRules );
|
||||
}
|
||||
|
||||
/**
|
||||
* Only import the node if the application is installed
|
||||
*
|
||||
* @param string $value Actionname
|
||||
* @return bool
|
||||
*/
|
||||
public static function canImport( string $value ) : bool
|
||||
{
|
||||
$appKey = explode( '_', $value );
|
||||
return \IPS\Application::appIsEnabled( $appKey[0] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this rule have a milestone set?
|
||||
*
|
||||
* @param array|null $filters Actionname
|
||||
* @return bool
|
||||
*/
|
||||
public static function ruleHasMilestone( ?array $filters ): bool
|
||||
{
|
||||
if ( \is_array( $filters ) and \count( $filters ) )
|
||||
{
|
||||
foreach( $filters as $key => $value )
|
||||
{
|
||||
if ( mb_substr( $key, 0, 9 ) == 'milestone' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get the title to store in the log
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function titleForLog()
|
||||
{
|
||||
$exploded = explode( '_', $this->action );
|
||||
return \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'AchievementAction__' . $exploded[1] . '_title' ) . " #" . $this->id;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user