MDL-64739 tool_analytics: Restrict models to specific contexts
authorDavid Monllaó <davidm@moodle.com>
Mon, 16 Sep 2019 02:56:42 +0000 (10:56 +0800)
committerDavid Monllaó <davidm@moodle.com>
Mon, 21 Oct 2019 01:54:39 +0000 (09:54 +0800)
15 files changed:
admin/tool/analytics/classes/output/form/edit_model.php
admin/tool/analytics/classes/output/helper.php
admin/tool/analytics/classes/output/invalid_analysables.php
admin/tool/analytics/createmodel.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
analytics/classes/analysis.php
analytics/classes/local/analyser/base.php
analytics/classes/local/analyser/by_course.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/tests/manager_test.php
analytics/tests/prediction_test.php
analytics/upgrade.txt
lang/en/analytics.php

index 228ab81..731d829 100644 (file)
@@ -89,18 +89,6 @@ class edit_model extends \moodleform {
             $mform->addHelpButton('indicators', 'indicators', 'tool_analytics');
         }
 
-        // Contexts restriction.
-        $contexts = array();
-        if ($this->_customdata['contexts']) {
-            $contexts = \tool_analytics\output\helper::contexts_to_options($this->_customdata['contexts']);
-        }
-        $options = array(
-            'multiple' => true
-        );
-        $mform->addElement('autocomplete', 'contexts', get_string('contexts', 'tool_analytics'), $contexts, $options);
-        $mform->setType('contexts', PARAM_ALPHANUMEXT);
-        $mform->addHelpButton('contexts', 'indicators', 'tool_analytics');
-
         // Time-splitting methods.
         if (!empty($this->_customdata['invalidcurrenttimesplitting'])) {
             $mform->addElement('html', $OUTPUT->notification(
@@ -117,6 +105,17 @@ class edit_model extends \moodleform {
         $mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings);
         $mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics');
 
+        // Contexts restriction.
+        if (!empty($this->_customdata['contexts'])) {
+
+            \core_collator::asort($this->_customdata['contexts']);
+            $options = ['multiple' => true, 'noselectionstring' => get_string('all')];
+            $mform->addElement('autocomplete', 'contexts', get_string('contexts', 'tool_analytics'), $this->_customdata['contexts'],
+                $options);
+            $mform->setType('contexts', PARAM_INT);
+            $mform->addHelpButton('contexts', 'contexts', 'tool_analytics');
+        }
+
         // Predictions processor.
         if (!$this->_customdata['staticmodel']) {
             $defaultprocessor = \core_analytics\manager::get_predictions_processor_name(
@@ -158,20 +157,37 @@ class edit_model extends \moodleform {
     public function validation($data, $files) {
         $errors = parent::validation($data, $files);
 
+        $targetclass = \tool_analytics\output\helper::option_to_class($data['target']);
+        $target = \core_analytics\manager::get_target($targetclass);
+
         if (!empty($data['timesplitting'])) {
             $timesplittingclass = \tool_analytics\output\helper::option_to_class($data['timesplitting']);
             if (\core_analytics\manager::is_valid($timesplittingclass, '\core_analytics\local\time_splitting\base') === false) {
                 $errors['timesplitting'] = get_string('errorinvalidtimesplitting', 'analytics');
             }
 
-            $targetclass = \tool_analytics\output\helper::option_to_class($data['target']);
             $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingclass);
-            $target = \core_analytics\manager::get_target($targetclass);
             if (!$target->can_use_timesplitting($timesplitting)) {
                 $errors['timesplitting'] = get_string('invalidtimesplitting', 'tool_analytics');
             }
         }
 
+        if (!empty($data['contexts'])) {
+
+            $analyserclass = $target->get_analyser_class();
+            if (!$potentialcontexts = $analyserclass::potential_context_restrictions()) {
+                $errors['contexts'] = get_string('errornocontextrestrictions', 'analytics');
+            } else {
+
+                // Flip the contexts array so we can just diff by key.
+                $selectedcontexts = array_flip($data['contexts']);
+                $invalidcontexts = array_diff_key($selectedcontexts, $potentialcontexts);
+                if (!empty($invalidcontexts)) {
+                    $errors['contexts'] = get_string('errorinvalidcontexts', 'analytics');
+                }
+            }
+        }
+
         if (!$this->_customdata['staticmodel']) {
             if (empty($data['indicators'])) {
                 $errors['indicators'] = get_string('errornoindicators', 'analytics');
index e0b900b..7d28625 100644 (file)
@@ -151,39 +151,4 @@ class helper {
         $singleselect = new \single_select($url, 'contextid', $contexts, $selected, $nothing);
         return $singleselect->export_for_template($output);
     }
-
-    /**
-     * Converts a list of contexts to an array of options that can be used in a autocomplete moodleform field.
-     *
-     * @param array $contexts Array of context ids.
-     * @param bool $includeall Wether to include the "all" context option (the system context).
-     * @param bool $shortentext Wether to shorten the context names.
-     * @return array Associative array with context ids as keys and context names as values.
-     */
-    public static function contexts_to_options(array $contexts, ?bool $includeall = false, ?bool $shortentext = true): array {
-
-        foreach ($contexts as $contextid) {
-            $context = \context::instance_by_id($contextid);
-
-            // Special name for system level predictions as showing "System is not visually nice".
-            if ($contextid == SYSCONTEXTID) {
-                $contextname = get_string('allpredictions', 'tool_analytics');
-            } else {
-                if ($shortentext) {
-                    $contextname = shorten_text($context->get_context_name(false, true), 40);
-                } else {
-                    $contextname = $context->get_context_name(false, true);
-                }
-            }
-            $contexts[$contextid] = $contextname;
-        }
-
-        if ($includeall) {
-            $contexts[0] = get_string('all');
-        }
-
-        \core_collator::asort($contexts);
-
-        return $contexts;
-    }
 }
index fd205f1..2011754 100644 (file)
@@ -76,7 +76,8 @@ class invalid_analysables implements \renderable, \templatable {
 
         $offset = $this->page * $this->perpage;
 
-        $analysables = $this->model->get_analyser(['notimesplitting' => true])->get_analysables_iterator();
+        $contexts = $this->model->get_contexts();
+        $analysables = $this->model->get_analyser(['notimesplitting' => true])->get_analysables_iterator(null, $contexts);
 
         $skipped = 0;
         $enoughresults = false;
index ad4fc83..b2d61cb 100644 (file)
@@ -52,6 +52,7 @@ $customdata = array(
     'indicators' => \core_analytics\manager::get_all_indicators(),
     'timesplittings' => \core_analytics\manager::get_all_time_splittings(),
     'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
+    'contexts' => \core_analytics\manager::get_potential_context_restrictions(),
 );
 $mform = new \tool_analytics\output\form\edit_model(null, $customdata);
 
@@ -86,8 +87,8 @@ if ($mform->is_cancelled()) {
         $indicators = array_diff_key($indicators, $invalidindicators);
     }
 
-    // Update the model with the valid list of indicators.
-    $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
+    // Update the model with the rest of the data provided in the form.
+    $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
 
     $message = '';
     $messagetype = \core\output\notification::NOTIFY_SUCCESS;
index ca3d1a2..acace09 100644 (file)
@@ -45,6 +45,8 @@ $string['component'] = 'Component';
 $string['componentcore'] = 'Core';
 $string['componentselect'] = 'Select all models provided by the component \'{$a}\'';
 $string['componentselectnone'] = 'Unselect all';
+$string['contexts'] = 'Contexts';
+$string['contexts_help'] = 'The model will be limited to this set of contexts. No context restrictions will be applied if no contexts are selected.';
 $string['createmodel'] = 'Create model';
 $string['currenttimesplitting'] = 'Current analysis interval';
 $string['delete'] = 'Delete';
index 4886ff5..65a600e 100644 (file)
@@ -132,7 +132,8 @@ switch ($action) {
             'targetname' => $model->get_target()->get_name(),
             'indicators' => $model->get_potential_indicators(),
             'timesplittings' => $potentialtimesplittings,
-            'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors()
+            'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
+            'contexts' => ($model->get_analyser())::potential_context_restrictions()
         );
         $mform = new \tool_analytics\output\form\edit_model(null, $customdata);
 
@@ -157,7 +158,7 @@ switch ($action) {
                 $predictionsprocessor = false;
             }
 
-            $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
+            $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
             redirect($returnurl);
         }
 
@@ -168,6 +169,9 @@ switch ($action) {
         $callable = array('\tool_analytics\output\helper', 'class_to_option');
         $modelobj->indicators = array_map($callable, json_decode($modelobj->indicators));
         $modelobj->timesplitting = \tool_analytics\output\helper::class_to_option($modelobj->timesplitting);
+        if ($modelobj->contextids) {
+            $modelobj->contexts = array_map($callable, json_decode($modelobj->contextids));
+        }
         $modelobj->predictionsprocessor = \tool_analytics\output\helper::class_to_option($modelobj->predictionsprocessor);
         $mform->set_data($modelobj);
         $mform->display();
index ad39b48..f11bfae 100644 (file)
@@ -75,9 +75,10 @@ class analysis {
     /**
      * Runs the analysis.
      *
+     * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
      * @return null
      */
-    public function run() {
+    public function run(array $contexts = []) {
 
         $options = $this->analyser->get_options();
 
@@ -89,7 +90,7 @@ class analysis {
         } else {
             $action = 'prediction';
         }
-        $analysables = $this->analyser->get_analysables_iterator($action);
+        $analysables = $this->analyser->get_analysables_iterator($action, $contexts);
 
         $processedanalysables = $this->get_processed_analysables();
 
index 729187f..6ea0d22 100644 (file)
@@ -267,38 +267,42 @@ abstract class base {
     /**
      * Returns labelled data (training and evaluation).
      *
+     * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
      * @return \stored_file[]
      */
-    public function get_labelled_data() {
+    public function get_labelled_data(array $contexts = []) {
         // Delegates all processing to the analysis.
         $result = new \core_analytics\local\analysis\result_file($this->get_modelid(), true, $this->get_options());
         $analysis = new \core_analytics\analysis($this, true, $result);
-        $analysis->run();
+        $analysis->run($contexts);
         return $result->get();
     }
 
     /**
      * Returns unlabelled data (prediction).
      *
+     * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
      * @return \stored_file[]
      */
-    public function get_unlabelled_data() {
+    public function get_unlabelled_data(array $contexts = []) {
         // Delegates all processing to the analysis.
         $result = new \core_analytics\local\analysis\result_file($this->get_modelid(), false, $this->get_options());
         $analysis = new \core_analytics\analysis($this, false, $result);
-        $analysis->run();
+        $analysis->run($contexts);
         return $result->get();
     }
 
     /**
      * Returns indicator calculations as an array.
+     *
+     * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
      * @return array
      */
-    public function get_static_data() {
+    public function get_static_data(array $contexts = []) {
         // Delegates all processing to the analysis.
         $result = new \core_analytics\local\analysis\result_array($this->get_modelid(), false, $this->get_options());
         $analysis = new \core_analytics\analysis($this, false, $result);
-        $analysis->run();
+        $analysis->run($contexts);
         return $result->get();
     }
 
@@ -423,6 +427,33 @@ abstract class base {
         return false;
     }
 
+    /**
+     * Returns an array of context levels that can be used to restrict the contexts used during analysis.
+     *
+     * The contexts provided to self::get_analysables_iterator will match these contextlevels.
+     *
+     * @return array Array of context levels or an empty array if context restriction is not supported.
+     */
+    public static function context_restriction_support(): array {
+        return [];
+    }
+
+    /**
+     * Returns the possible contexts used by the analyser.
+     *
+     * This method uses separate logic for each context level because to iterate through
+     * the list of contexts calling get_context_name for each of them would be expensive
+     * in performance terms.
+     *
+     * This generic implementation returns all the contexts in the site for the provided context level.
+     * Overwrite it for specific restrictions in your analyser.
+     *
+     * @return int[]
+     */
+    public static function potential_context_restrictions() {
+        return \core_analytics\manager::get_potential_context_restrictions(static::context_restriction_support());
+    }
+
     /**
      * Get the sql of a default implementation of the iterator.
      *
index 7e6bc66..59b6627 100644 (file)
@@ -65,4 +65,13 @@ abstract class by_course extends base {
             return \core_analytics\course::instance($record, $context);
         });
     }
+
+    /**
+     * Can be limited to course categories or specific courses.
+     *
+     * @return array
+     */
+    public static function context_restriction_support(): array {
+        return [CONTEXT_COURSE, CONTEXT_COURSECAT];
+    }
 }
index 60f1f45..a36a1fe 100644 (file)
@@ -646,6 +646,8 @@ class manager {
             $usedanalysablesanalysableids = array_flip($usedanalysablesanalysableids);
 
             $analyser = $model->get_analyser(array('notimesplitting' => true));
+
+            // We do not honour the list of contexts in this model as it can contain stale records.
             $analysables = $analyser->get_analysables_iterator();
 
             $analysableids = [];
@@ -913,4 +915,61 @@ class manager {
 
         return [$target, $indicators];
     }
+
+    /**
+     * Return the context restrictions that can be applied to the provided context levels.
+     *
+     * @throws \coding_exception
+     * @param  array|null $contextlevels The list of context levels provided by the analyser. Null if all of them.
+     * @return array Associative array with contextid as key and the short version of the context name as value.
+     */
+    public static function get_potential_context_restrictions(?array $contextlevels = null) {
+        global $DB;
+
+        if (empty($contextlevels) && !is_null($contextlevels)) {
+            return false;
+        }
+
+        if (!is_null($contextlevels)) {
+            foreach ($contextlevels as $contextlevel) {
+                if ($contextlevel !== CONTEXT_COURSE && $contextlevel !== CONTEXT_COURSECAT) {
+                    throw new \coding_exception('Only CONTEXT_COURSE and CONTEXT_COURSECAT are supported at the moment.');
+                }
+            }
+        }
+
+        $contexts = [];
+
+        // We have a separate process for each context level for performance reasons (to iterate through mdl_context calling
+        // get_context_name() would be too slow).
+        $contextsystem = \context_system::instance();
+        if (is_null($contextlevels) || in_array(CONTEXT_COURSECAT, $contextlevels)) {
+
+            $sql = "SELECT cc.id, cc.name, ctx.id AS contextid
+                      FROM {course_categories} cc
+                      JOIN {context} ctx ON ctx.contextlevel = :ctxlevel AND ctx.instanceid = cc.id";
+            $coursecats = $DB->get_recordset_sql($sql, ['ctxlevel' => CONTEXT_COURSECAT]);
+            foreach ($coursecats as $record) {
+                $contexts[$record->contextid] = get_string('category') . ': ' .
+                    format_string($record->name, true, array('context' => $contextsystem));
+            }
+            $coursecats->close();
+        }
+
+        if (is_null($contextlevels) || in_array(CONTEXT_COURSE, $contextlevels)) {
+
+            $sql = "SELECT c.id, c.shortname, ctx.id AS contextid
+                      FROM {course} c
+                      JOIN {context} ctx ON ctx.contextlevel = :ctxlevel AND ctx.instanceid = c.id";
+            $courses = $DB->get_recordset_sql($sql, ['ctxlevel' => CONTEXT_COURSE]);
+            foreach ($courses as $record) {
+                $contexts[$record->contextid] = get_string('course') . ': ' .
+                    format_string($record->shortname, true, array('context' => $contextsystem));
+            }
+            $courses->close();
+        }
+
+        return $contexts;
+    }
+
 }
index 9a524be..cc92516 100644 (file)
@@ -120,6 +120,11 @@ class model {
      */
     protected $indicators = null;
 
+    /**
+     * @var \context[]
+     */
+    protected $contexts = null;
+
     /**
      * Unique Model id created from site info and last model modification.
      *
@@ -459,9 +464,11 @@ class model {
      * @param \core_analytics\local\indicator\base[]|false $indicators False to respect current indicators
      * @param string|false $timesplittingid False to respect current time splitting method
      * @param string|false $predictionsprocessor False to respect current predictors processor value
+     * @param int[]|false $contextids List of context ids for this model. False to respect the current list of contexts.
      * @return void
      */
-    public function update($enabled, $indicators = false, $timesplittingid = '', $predictionsprocessor = false) {
+    public function update($enabled, $indicators = false, $timesplittingid = '', $predictionsprocessor = false,
+            $contextids = false) {
         global $USER, $DB;
 
         \core_analytics\manager::check_can_manage_models();
@@ -486,6 +493,15 @@ class model {
             $predictionsprocessor = $this->model->predictionsprocessor;
         }
 
+        if ($contextids !== false) {
+            $contextsstr = json_encode($contextids);
+
+            // Reset the internal cache.
+            $this->contexts = null;
+        } else {
+            $contextsstr = $this->model->contextids;
+        }
+
         if ($this->model->timesplitting !== $timesplittingid ||
                 $this->model->indicators !== $indicatorsstr ||
                 $this->model->predictionsprocessor !== $predictionsprocessor) {
@@ -514,6 +530,7 @@ class model {
         $this->model->indicators = $indicatorsstr;
         $this->model->timesplitting = $timesplittingid;
         $this->model->predictionsprocessor = $predictionsprocessor;
+        $this->model->contextids = $contextsstr;
         $this->model->timemodified = $now;
         $this->model->usermodified = $USER->id;
 
@@ -603,7 +620,7 @@ class model {
         // Before get_labelled_data call so we get an early exception if it is not ready.
         $predictor = $this->get_predictions_processor();
 
-        $datasets = $this->get_analyser()->get_labelled_data();
+        $datasets = $this->get_analyser()->get_labelled_data($this->get_contexts());
 
         // No datasets generated.
         if (empty($datasets)) {
@@ -695,7 +712,7 @@ class model {
         // Before get_labelled_data call so we get an early exception if it is not ready.
         $predictor = $this->get_predictions_processor();
 
-        $datasets = $this->get_analyser()->get_labelled_data();
+        $datasets = $this->get_analyser()->get_labelled_data($this->get_contexts());
 
         // No training if no files have been provided.
         if (empty($datasets) || empty($datasets[$this->model->timesplitting])) {
@@ -766,7 +783,7 @@ class model {
             // Before get_unlabelled_data call so we get an early exception if it is not ready.
             $predictor = $this->get_predictions_processor();
 
-            $samplesdata = $this->get_analyser()->get_unlabelled_data();
+            $samplesdata = $this->get_analyser()->get_unlabelled_data($this->get_contexts());
 
             // Get the prediction samples file.
             if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) {
@@ -802,7 +819,7 @@ class model {
         } else {
             // Predictions based on assumptions.
 
-            $indicatorcalculations = $this->get_analyser()->get_static_data();
+            $indicatorcalculations = $this->get_analyser()->get_static_data($this->get_contexts());
             // Get the prediction samples file.
             if (empty($indicatorcalculations) || empty($indicatorcalculations[$this->model->timesplitting])) {
 
@@ -1956,6 +1973,30 @@ class model {
         return [$allsampleids, $allsamplesdata];
     }
 
+    /**
+     * Contexts where this model should be active.
+     *
+     * @return \context[] Empty array if there are no context restrictions.
+     */
+    public function get_contexts() {
+        if ($this->contexts !== null) {
+            return $this->contexts;
+        }
+
+        if (!$this->model->contextids) {
+            $this->contexts = [];
+            return $this->contexts;
+        }
+        $contextids = json_decode($this->model->contextids);
+
+        // We don't expect this list to be massive as contexts need to be selected manually using the edit model form.
+        $this->contexts = array_map(function($contextid) {
+            return \context::instance_by_id($contextid, IGNORE_MISSING);
+        }, $contextids);
+
+        return $this->contexts;
+    }
+
     /**
      * Purges the insights cache.
      */
index 02166a8..28d5067 100644 (file)
@@ -487,4 +487,25 @@ class analytics_manager_testcase extends advanced_testcase {
         $this->assertNotEmpty($indicators);
         $this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $indicators);
     }
+
+    /**
+     * test_get_potential_context_restrictions description
+     */
+    public function test_get_potential_context_restrictions() {
+        $this->resetAfterTest();
+
+        // No potential context restrictions.
+        $this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));
+
+        // Include the all context levels so the frontpage and the misc. category get included.
+        $this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions());
+
+        $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->create_category();
+        $this->assertCount(4, \core_analytics\manager::get_potential_context_restrictions());
+        $this->assertCount(4, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE, CONTEXT_COURSECAT]));
+
+        $this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE]));
+        $this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT]));
+    }
 }
index 2b437f6..1fd9ac2 100644 (file)
@@ -126,6 +126,57 @@ class core_analytics_prediction_testcase extends advanced_testcase {
             array('modelid' => $model->get_id())));
     }
 
+    /**
+     * test_model_contexts
+     */
+    public function test_model_contexts() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+
+        $misc = $DB->get_record('course_categories', ['name' => 'Miscellaneous']);
+        $miscctx = \context_coursecat::instance($misc->id);
+
+        $category = $this->getDataGenerator()->create_category();
+        $categoryctx = \context_coursecat::instance($category->id);
+
+        // One course per category.
+        $courseparams = array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0,
+            'category' => $category->id);
+        $course1 = $this->getDataGenerator()->create_course($courseparams);
+        $course1ctx = \context_course::instance($course1->id);
+        $courseparams = array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0,
+            'category' => $misc->id);
+        $course2 = $this->getDataGenerator()->create_course($courseparams);
+
+        $model = $this->add_perfect_model('test_static_target_shortname');
+
+        // Just 1 category.
+        $model->update(true, false, '\core\analytics\time_splitting\no_splitting', false, [$categoryctx->id]);
+        $this->assertCount(1, $model->predict()->predictions);
+
+        // Now with 2 categories.
+        $model->update(true, false, false, false, [$categoryctx->id, $miscctx->id]);
+
+        // The courses in the new category are processed.
+        $this->assertCount(1, $model->predict()->predictions);
+
+        // Clear the predictions generated by the model and predict() again.
+        $model->clear();
+        $this->assertCount(2, $model->predict()->predictions);
+
+        // Course context restriction.
+        $model->update(true, false, '\core\analytics\time_splitting\no_splitting', false, [$course1ctx->id]);
+
+        // Nothing new as the course was already analysed.
+        $result = $model->predict();
+        $this->assertTrue(empty($result->predictions));
+
+        $model->clear();
+        $this->assertCount(1, $model->predict()->predictions);
+    }
+
     /**
      * test_ml_training_and_prediction
      *
index 27c543e..4773d4a 100644 (file)
@@ -32,6 +32,8 @@ information provided here is intended especially for developers.
 * \core_analytics\predictor::delete_output_dir has a new 2nd parameter, $uniquemodelid.
 * Analyser's get_analysables_iterator and get_iterator_sql have a new $contexts parameter to limit the returned analysables to
   the ones that depend on the provided contexts.
+* Analysers can implement a context_restriction_support() method to restrict models to a subset of the
+  contents in the site. Only CONTEXT_COURSE and CONTEXT_COURSECAT are supported.
 
 === 3.7 ===
 
index 1c72cb1..c32225a 100644 (file)
@@ -46,8 +46,10 @@ $string['errorimportmissingcomponents'] = 'The provided model requires the follo
 $string['errorimportversionmismatches'] = 'The version of the following components differs from the version installed on this site: {$a}. You can use the option \'Ignore version mismatches\' to ignore these differences.';
 $string['errorimportmissingclasses'] = 'The following analytics components are not available on this site: {$a->missingclasses}.';
 $string['errorinvalidindicator'] = 'Invalid {$a} indicator';
+$string['errorinvalidcontexts'] = 'Some of the selected contexts can not be used in this target.';
 $string['errorinvalidtarget'] = 'Invalid {$a} target';
 $string['errorinvalidtimesplitting'] = 'Invalid analysis interval; please ensure you add the fully qualified class name.';
+$string['errornocontextrestrictions'] = 'The selected target does not support context restrictions';
 $string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
 $string['errornoexportconfigrequirements'] = 'Only non-static models with an analysis interval can be exported.';
 $string['errornoindicators'] = 'This model does not have any indicators.';