You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

351 lines
11 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
class GoalLib
{
public static $runner;
public function listGoals()
{
$table = $this->table();
$list = $table->fetchAll(['goalId', 'enabled', 'name', 'description', 'type', 'eligible'], [], -1, -1, [
'name' => 'ASC',
]);
return array_map(function ($goal) {
$goal['eligible'] = json_decode($goal['eligible'], true);
return $goal;
}, $list);
}
public function listConditions()
{
$table = $this->table();
$table->useExceptions();
$list = $table->fetchAll(['goalId', 'conditions'], [], -1, -1, [
]);
return array_map(function ($goal) {
$goal['conditions'] = json_decode($goal['conditions'], true);
return $goal;
}, $list);
}
public function removeGoal($goalId)
{
$this->table()->delete(['goalId' => $goalId]);
TikiLib::lib('goalevent')->touch();
}
public function preserveGoals(array $ids)
{
$table = $this->table();
return $table->deleteMultiple(
[
'goalId' => $table->notIn($ids),
]
);
}
public function replaceGoal($goalId, array $data)
{
$base = null;
if ($goalId) {
$base = $this->fetchGoal($goalId);
}
if (! $base) {
$base = [
'name' => 'No name',
'description' => '',
'type' => 'user',
'enabled' => 0,
'daySpan' => 14,
'from' => null,
'to' => null,
'eligible' => [],
'conditions' => [
[
'label' => tr('Goal achieved'),
'operator' => 'atMost',
'count' => 0,
'metric' => 'goal-count-unbounded',
'hidden' => 1,
],
],
'rewards' => [],
];
}
$data = array_merge($base, $data);
$data['eligible'] = json_encode((array) $data['eligible']);
$data['conditions'] = json_encode((array) $data['conditions']);
$data['rewards'] = json_encode((array) $data['rewards']);
if ($goalId) {
$this->table()->update($data, ['goalId' => $goalId]);
} else {
$goalId = $this->table()->insert($data);
}
TikiLib::lib('goalevent')->touch();
return $goalId;
}
public function fetchGoal($goalId)
{
$goal = $this->table()->fetchFullRow(['goalId' => $goalId]);
if ($goal) {
$goal['eligible'] = json_decode($goal['eligible'], true) ?: [];
$goal['conditions'] = json_decode($goal['conditions'], true) ?: [];
$goal['rewards'] = json_decode($goal['rewards'], true) ?: [];
return $goal;
}
}
public function isEligible(array $goal, array $context)
{
if ($goal['type'] == 'user') {
return count(array_intersect($context['groups'], $goal['eligible'])) > 0;
} elseif ($context['group']) {
return in_array($context['group'], $goal['eligible']);
} else {
return false;
}
}
public function evaluateConditions(array $goal, array $context)
{
$this->prepareConditions($goal);
$runner = $this->getRunner();
$goal['complete'] = true;
foreach ($goal['conditions'] as & $cond) {
$arguments = [];
foreach (['eventType', 'trackerItemBadge'] as $arg) {
if (isset($cond[$arg])) {
$arguments[$arg] = $cond[$arg];
}
}
$runner->setFormula($cond['metric']);
$runner->setVariables(array_merge($goal, $context, $arguments));
$cond['metric'] = $runner->evaluate();
if ($cond['operator'] == 'atLeast') {
$cond['complete'] = $cond['metric'] >= $cond['count'];
$cond['metric'] = min($cond['count'], $cond['metric']);
} else {
$cond['complete'] = $cond['metric'] <= $cond['count'];
}
$goal['complete'] = $goal['complete'] && $cond['complete'];
}
if ($goal['complete']) {
$tx = TikiDb::get()->begin();
TikiLib::events()->trigger('tiki.goal.reached', [
'type' => 'goal',
'object' => $goal['goalId'],
'name' => $goal['name'],
'goalType' => $goal['type'],
'user' => $context['user'],
'group' => $context['group'],
]);
$rewardlib = TikiLib::lib('goalreward');
if ($goal['type'] == 'group') {
$rewardlib->giveRewardsToMembers($context['group'], $goal['rewards']);
} else {
$rewardlib->giveRewardsToUser($context['user'], $goal['rewards']);
}
$tx->commit();
}
return $goal;
}
public function unevaluateConditions($goal)
{
$goal['complete'] = false;
foreach ($goal['conditions'] as & $cond) {
$cond['metric'] = 0;
$cond['complete'] = false;
}
return $goal;
}
public function evaluateAllGoals()
{
$tx = TikiDb::get()->begin();
foreach ($this->listGoals() as $goal) {
if ($goal['enabled']) {
$this->prepareConditions($goal);
foreach ($this->enumerateContexts($goal) as $context) {
$this->evaluateConditions($goal, $context);
}
}
}
$tx->commit();
}
private function prepareConditions(array &$goal)
{
if (isset($goal['prepared'])) {
return;
}
// listGoals does not extract all information, so when conditions are missing, reload
if (! isset($goal['conditions'])) {
$goal = $this->fetchGoal($goal['goalId']);
}
$runner = $this->getRunner();
foreach ($goal['conditions'] as & $cond) {
$metric = $this->prepareMetric($cond['metric'], $goal);
$cond['metric'] = $runner->setFormula($metric);
}
$goal['prepared'] = true;
}
private function enumerateContexts($goal)
{
if ($goal['type'] == 'group') {
foreach ($goal['eligible'] as $groupName) {
yield ['user' => null, 'group' => $groupName];
}
} else {
$userlib = TikiLib::lib('user');
$done = [];
foreach ($goal['eligible'] as $groupName) {
foreach ($userlib->get_group_users($groupName) as $user) {
if (! isset($done[$user])) {
yield ['user' => $user, 'group' => null];
$done[$user] = true;
}
}
}
}
}
public static function getRunner()
{
if (! self::$runner) {
self::$runner = new Math_Formula_Runner(
[
'Math_Formula_Function_' => '',
'Tiki_Formula_Function_' => '',
]
);
}
return self::$runner;
}
private function prepareMetric($metric, $goal)
{
switch ($metric) {
case 'event-count':
$metric = '(result-count
(filter-date)
(filter-target)
(filter (content eventType) (field "event_type"))
(filter (type "goalevent"))
)';
break;
case 'event-count-unbounded':
$metric = '(result-count
(filter-target)
(filter (content eventType) (field "event_type"))
(filter (type "goalevent"))
)';
break;
case 'goal-count':
$metric = '(result-count
(filter-date)
(filter-target)
(filter (content "tiki.goal.reached") (field "event_type"))
(filter (type "goalevent"))
(filter (content (concat "goal:" goalId)) (field "target"))
)';
break;
case 'goal-count-unbounded':
$metric = '(result-count
(filter-target)
(filter (content "tiki.goal.reached") (field "event_type"))
(filter (type "goalevent"))
(filter (content (concat "goal:" goalId)) (field "target"))
)';
break;
case 'has-badge':
$metric = '(relation-present
(qualifier "tiki.badge.received")
(from type (if (equals type "user") user group))
(to "trackeritem" trackerItemBadge)
)';
break;
}
if ($goal['daySpan']) {
$metric = str_replace('(filter-date)', '(filter (range "modification_date") (from (concat daySpan " days ago")) (to "now"))', $metric);
} else {
$metric = str_replace('(filter-date)', '(filter (range "modification_date") (from from) (to to))', $metric);
}
if ($goal['type'] == 'user') {
$metric = str_replace('(filter-target)', '(filter (content user) (field "user"))', $metric);
} else {
$metric = str_replace('(filter-target)', '(filter (multivalue group) (field "goal_groups"))', $metric);
}
return $metric;
}
public function getMetricList()
{
return [
'event-count' => ['label' => tr('Event Count'), 'arguments' => ['eventType']],
'event-count-unbounded' => ['label' => tr('Event Count (Forever)'), 'arguments' => ['eventType']],
'goal-count' => ['label' => tr('Goal Reached (Periodic)'), 'arguments' => []],
'goal-count-unbounded' => ['label' => tr('Goal Reached (Forever)'), 'arguments' => []],
'has-badge' => ['label' => tr('Has Badge'), 'arguments' => ['trackerItemBadge']],
];
}
public function listEligibleGroups()
{
global $prefs;
$groups = TikiLib::lib('user')->list_all_groups();
return array_diff($groups, $prefs['goal_group_blacklist']);
}
private function table()
{
return TikiDb::get()->table('tiki_goals');
}
}