1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Representation of a prediction.
19 *
20 * @package   core_analytics
21 * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
22 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace core_analytics;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Representation of a prediction.
31 *
32 * @package   core_analytics
33 * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
34 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36class prediction {
37
38    /**
39     * Prediction details (one of the default prediction actions)
40     */
41    const ACTION_PREDICTION_DETAILS = 'predictiondetails';
42
43    /**
44     * Prediction useful (one of the default prediction actions)
45     */
46    const ACTION_USEFUL = 'useful';
47
48    /**
49     * Prediction not useful (one of the default prediction actions)
50     */
51    const ACTION_NOT_USEFUL = 'notuseful';
52
53    /**
54     * Prediction already fixed (one of the default prediction actions)
55     */
56    const ACTION_FIXED = 'fixed';
57
58    /**
59     * Prediction not applicable.
60     */
61    const ACTION_NOT_APPLICABLE = 'notapplicable';
62
63    /**
64     * Prediction incorrectly flagged.
65     */
66    const ACTION_INCORRECTLY_FLAGGED = 'incorrectlyflagged';
67
68    /**
69     * @var \stdClass
70     */
71    private $prediction;
72
73    /**
74     * @var array
75     */
76    private $sampledata;
77
78    /**
79     * @var array
80     */
81    private $calculations = array();
82
83    /**
84     * Constructor
85     *
86     * @param \stdClass|int $prediction
87     * @param array $sampledata
88     * @return void
89     */
90    public function __construct($prediction, $sampledata) {
91        global $DB;
92
93        if (is_scalar($prediction)) {
94            $prediction = $DB->get_record('analytics_predictions', array('id' => $prediction), '*', MUST_EXIST);
95        }
96        $this->prediction = $prediction;
97
98        $this->sampledata = $sampledata;
99
100        $this->format_calculations();
101    }
102
103    /**
104     * Get prediction object data.
105     *
106     * @return \stdClass
107     */
108    public function get_prediction_data() {
109        return $this->prediction;
110    }
111
112    /**
113     * Get prediction sample data.
114     *
115     * @return array
116     */
117    public function get_sample_data() {
118        return $this->sampledata;
119    }
120
121    /**
122     * Gets the prediction calculations
123     *
124     * @return array
125     */
126    public function get_calculations() {
127        return $this->calculations;
128    }
129
130    /**
131     * Stores the executed action.
132
133     * Prediction instances should be retrieved using \core_analytics\manager::get_prediction,
134     * It is the caller responsability to check that the user can see the prediction.
135     *
136     * @param string $actionname
137     * @param \core_analytics\local\target\base $target
138     */
139    public function action_executed($actionname, \core_analytics\local\target\base $target) {
140        global $USER, $DB;
141
142        $context = \context::instance_by_id($this->get_prediction_data()->contextid, IGNORE_MISSING);
143        if (!$context) {
144            throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
145        }
146
147        // Check that the provided action exists.
148        $actions = $target->prediction_actions($this, true);
149        foreach ($actions as $action) {
150            if ($action->get_action_name() === $actionname) {
151                $found = true;
152            }
153        }
154        $bulkactions = $target->bulk_actions([$this]);
155        foreach ($bulkactions as $action) {
156            if ($action->get_action_name() === $actionname) {
157                $found = true;
158            }
159        }
160        if (empty($found)) {
161            throw new \moodle_exception('errorunknownaction', 'analytics');
162        }
163
164        $predictionid = $this->get_prediction_data()->id;
165
166        $action = new \stdClass();
167        $action->predictionid = $predictionid;
168        $action->userid = $USER->id;
169        $action->actionname = $actionname;
170        $action->timecreated = time();
171        $DB->insert_record('analytics_prediction_actions', $action);
172
173        $eventdata = array (
174            'context' => $context,
175            'objectid' => $predictionid,
176            'other' => array('actionname' => $actionname)
177        );
178        \core\event\prediction_action_started::create($eventdata)->trigger();
179    }
180
181    /**
182     * Get the executed actions.
183     *
184     * Actions could be filtered by actionname.
185     *
186     * @param array $actionnamefilter Limit the results obtained to this list of action names.
187     * @param int $userid the user id. Current user by default.
188     * @return array of actions.
189     */
190    public function get_executed_actions(array $actionnamefilter = null, int $userid = 0): array {
191        global $USER, $DB;
192
193        $conditions[] = "predictionid = :predictionid";
194        $params['predictionid'] = $this->get_prediction_data()->id;
195        if (!$userid) {
196            $userid = $USER->id;
197        }
198        $conditions[] = "userid = :userid";
199        $params['userid'] = $userid;
200        if ($actionnamefilter) {
201            list($actionsql, $actionparams) = $DB->get_in_or_equal($actionnamefilter, SQL_PARAMS_NAMED);
202            $conditions[] = "actionname $actionsql";
203            $params = $params + $actionparams;
204        }
205        return $DB->get_records_select('analytics_prediction_actions', implode(' AND ', $conditions), $params);
206    }
207
208    /**
209     * format_calculations
210     *
211     * @return \stdClass[]
212     */
213    private function format_calculations() {
214
215        $calculations = json_decode($this->prediction->calculations, true);
216
217        foreach ($calculations as $featurename => $value) {
218
219            list($indicatorclass, $subtype) = $this->parse_feature_name($featurename);
220
221            if ($indicatorclass === 'range') {
222                // Time range indicators don't belong to any indicator class, we don't store them.
223                continue;
224            } else if (!\core_analytics\manager::is_valid($indicatorclass, '\core_analytics\local\indicator\base')) {
225                throw new \moodle_exception('errorpredictionformat', 'analytics');
226            }
227
228            $this->calculations[$featurename] = new \stdClass();
229            $this->calculations[$featurename]->subtype = $subtype;
230            $this->calculations[$featurename]->indicator = \core_analytics\manager::get_indicator($indicatorclass);
231            $this->calculations[$featurename]->value = $value;
232        }
233    }
234
235    /**
236     * parse_feature_name
237     *
238     * @param string $featurename
239     * @return string[]
240     */
241    private function parse_feature_name($featurename) {
242
243        $indicatorclass = $featurename;
244        $subtype = false;
245
246        // Some indicator result in more than 1 feature, we need to see which feature are we dealing with.
247        $separatorpos = strpos($featurename, '/');
248        if ($separatorpos !== false) {
249            $subtype = substr($featurename, ($separatorpos + 1));
250            $indicatorclass = substr($featurename, 0, $separatorpos);
251        }
252
253        return array($indicatorclass, $subtype);
254    }
255}
256