diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index a4a703ad48f..4f041fa9889 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -1232,6 +1232,17 @@ class quiz_attempt { return $this->quba->get_question_action_time($slot); } + /** + * Return the question type name for a given slot within the current attempt. + * + * @param int $slot the number used to identify this question within this attempt. + * @return string the question type name + * @since Moodle 3.1 + */ + public function get_question_type_name($slot) { + return $this->quba->get_question($slot)->get_type_name(); + } + /** * Get the time remaining for an in-progress attempt, if the time is short * enought that it would be worth showing a timer. diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php index 214b9b49da0..11835cca62b 100644 --- a/mod/quiz/classes/external.php +++ b/mod/quiz/classes/external.php @@ -774,4 +774,232 @@ class mod_quiz_external extends external_api { ); } + /** + * Utility function for validating a given attempt + * + * @param array $params Array of parameters including the attemptid and preflight data + * @return array containing the attempt object and access messages + * @throws moodle_quiz_exception + * @since Moodle 3.1 + */ + protected static function validate_attempt($params) { + global $USER; + + $attemptobj = quiz_attempt::create($params['attemptid']); + + $context = context_module::instance($attemptobj->get_cm()->id); + self::validate_context($context); + + // Check that this attempt belongs to this user. + if ($attemptobj->get_userid() != $USER->id) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt'); + } + + // General capabilities check. + $ispreviewuser = $attemptobj->is_preview_user(); + if (!$ispreviewuser) { + $attemptobj->require_capability('mod/quiz:attempt'); + } + + // Attempt closed?. + if ($attemptobj->is_finished()) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attemptalreadyclosed'); + } else if ($attemptobj->get_state() == quiz_attempt::OVERDUE) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'stateoverdue'); + } + + // Check the access rules. + $accessmanager = $attemptobj->get_access_manager(time()); + + $messages = $accessmanager->prevent_access(); + if (!$ispreviewuser && $messages) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'attempterror'); + } + + // User submitted data (like the quiz password). + if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) { + $provideddata = array(); + foreach ($params['preflightdata'] as $data) { + $provideddata[$data['name']] = $data['value']; + } + + $errors = $accessmanager->validate_preflight_check($provideddata, [], $params['attemptid']); + if (!empty($errors)) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), array_shift($errors)); + } + // Pre-flight check passed. + $accessmanager->notify_preflight_check_passed($params['attemptid']); + } + + // Check if the page is out of range. + if ($params['page'] != $attemptobj->force_page_number_into_range($params['page'])) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Invalid page number'); + } + + // Prevent out of sequence access. + if ($attemptobj->get_currentpage() != $params['page']) { + if ($attemptobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ && $attemptobj->get_currentpage() > $params['page']) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'Out of sequence access'); + } + } + + // Check slots. + $slots = $attemptobj->get_slots($params['page']); + + if (empty($slots)) { + throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noquestionsfound'); + } + + return array($attemptobj, $messages); + } + + /** + * Describes a single question structure. + * + * @return external_single_structure the question structure + * @since Moodle 3.1 + */ + private static function question_structure() { + return new external_single_structure( + array( + 'slot' => new external_value(PARAM_INT, 'slot number'), + 'type' => new external_value(PARAM_ALPHANUMEXT, 'question type, i.e: multichoice'), + 'page' => new external_value(PARAM_INT, 'page of the quiz this question appears on'), + 'html' => new external_value(PARAM_RAW, 'the question rendered'), + 'flagged' => new external_value(PARAM_BOOL, 'whether the question is flagged or not'), + 'number' => new external_value(PARAM_INT, 'question ordering number in the quiz', VALUE_OPTIONAL), + 'state' => new external_value(PARAM_ALPHA, 'the state where the question is in', VALUE_OPTIONAL), + 'status' => new external_value(PARAM_RAW, 'current formatted state of the question', VALUE_OPTIONAL), + 'mark' => new external_value(PARAM_RAW, 'the mark awarded', VALUE_OPTIONAL), + 'maxmark' => new external_value(PARAM_FLOAT, 'the maximum mark possible for this question attempt', VALUE_OPTIONAL), + ) + ); + } + + /** + * Return questions information for a given attempt. + * + * @param quiz_attempt $attemptobj the quiz attempt object + * @param bool $review whether if we are in review mode or not + * @param mixed $page string 'all' or integer page number + * @return array array of questions including data + */ + private static function get_attempt_questions_data(quiz_attempt $attemptobj, $review, $page = 'all') { + global $PAGE; + + $questions = array(); + $contextid = $attemptobj->get_quizobj()->get_context()->id; + $displayoptions = $attemptobj->get_display_options($review); + $renderer = $PAGE->get_renderer('mod_quiz'); + + foreach ($attemptobj->get_slots($page) as $slot) { + + $question = array( + 'slot' => $slot, + 'type' => $attemptobj->get_question_type_name($slot), + 'page' => $attemptobj->get_question_page($slot), + 'flagged' => $attemptobj->is_question_flagged($slot), + 'html' => $attemptobj->render_question($slot, $review, $renderer) . $PAGE->requires->get_end_code() + ); + + if ($attemptobj->is_real_question($slot)) { + $question['number'] = $attemptobj->get_question_number($slot); + $question['state'] = (string) $attemptobj->get_question_state($slot); + $question['status'] = $attemptobj->get_question_status($slot, $displayoptions->correctness); + } + if ($displayoptions->marks >= question_display_options::MAX_ONLY) { + $question['maxmark'] = $attemptobj->get_question_attempt($slot)->get_max_mark(); + } + if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) { + $question['mark'] = $attemptobj->get_question_mark($slot); + } + + $questions[] = $question; + } + return $questions; + } + + /** + * Describes the parameters for get_attempt_data. + * + * @return external_external_function_parameters + * @since Moodle 3.1 + */ + public static function get_attempt_data_parameters() { + return new external_function_parameters ( + array( + 'attemptid' => new external_value(PARAM_INT, 'attempt id'), + 'page' => new external_value(PARAM_INT, 'page number'), + 'preflightdata' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_ALPHANUMEXT, 'data name'), + 'value' => new external_value(PARAM_RAW, 'data value'), + ) + ), 'Preflight required data (like passwords)', VALUE_DEFAULT, array() + ) + ) + ); + } + + /** + * Returns information for the given attempt page for a quiz attempt in progress. + * + * @param int $attemptid attempt id + * @param int $page page number + * @param array $preflightdata preflight required data (like passwords) + * @return array of warnings and the attempt data, next page, message and questions + * @since Moodle 3.1 + * @throws moodle_quiz_exceptions + */ + public static function get_attempt_data($attemptid, $page, $preflightdata = array()) { + + $warnings = array(); + + $params = array( + 'attemptid' => $attemptid, + 'page' => $page, + 'preflightdata' => $preflightdata, + ); + $params = self::validate_parameters(self::get_attempt_data_parameters(), $params); + + list($attemptobj, $messages) = self::validate_attempt($params); + + if ($attemptobj->is_last_page($params['page'])) { + $nextpage = -1; + } else { + $nextpage = $params['page'] + 1; + } + + $result = array(); + $result['attempt'] = $attemptobj->get_attempt(); + $result['messages'] = $messages; + $result['nextpage'] = $nextpage; + $result['warnings'] = $warnings; + $result['questions'] = self::get_attempt_questions_data($attemptobj, false, $params['page']); + + return $result; + } + + /** + * Describes the get_attempt_data return value. + * + * @return external_single_structure + * @since Moodle 3.1 + */ + public static function get_attempt_data_returns() { + return new external_single_structure( + array( + 'attempt' => self::attempt_structure(), + 'messages' => new external_multiple_structure( + new external_value(PARAM_TEXT, 'access message'), + 'access messages, will only be returned for users with mod/quiz:preview capability, + for other users this method will throw an exception if there are messages'), + 'nextpage' => new external_value(PARAM_INT, 'next page number'), + 'questions' => new external_multiple_structure(self::question_structure()), + 'warnings' => new external_warnings(), + ) + ); + } + } diff --git a/mod/quiz/db/services.php b/mod/quiz/db/services.php index 1367b271a62..8ee0227c0b3 100644 --- a/mod/quiz/db/services.php +++ b/mod/quiz/db/services.php @@ -82,4 +82,13 @@ $functions = array( 'capabilities' => 'mod/quiz:attempt', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + + 'mod_quiz_get_attempt_data' => array( + 'classname' => 'mod_quiz_external', + 'methodname' => 'get_attempt_data', + 'description' => 'Returns information for the given attempt page for a quiz attempt in progress.', + 'type' => 'read', + 'capabilities' => 'mod/quiz:attempt', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ), ); diff --git a/mod/quiz/tests/external_test.php b/mod/quiz/tests/external_test.php index b3711459dc6..bd8a74133c8 100644 --- a/mod/quiz/tests/external_test.php +++ b/mod/quiz/tests/external_test.php @@ -30,6 +30,27 @@ global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +/** + * Silly class to access mod_quiz_external internal methods. + * + * @package mod_quiz + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.1 + */ +class testable_mod_quiz_external extends mod_quiz_external { + + /** + * Public accessor. + * + * @param array $params Array of parameters including the attemptid and preflight data + * @return array containing the attempt object and access messages + */ + public static function validate_attempt($params) { + return parent::validate_attempt($params); + } +} + /** * Quiz module external functions tests * @@ -66,6 +87,65 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual'); } + /** + * Create a quiz with questions including a started or finished attempt optionally + * + * @param boolean $startattempt whether to start a new attempt + * @param boolean $finishattempt whether to finish the new attempt + * @return array array containing the quiz, context and the attempt + */ + private function create_quiz_with_questions($startattempt = false, $finishattempt = false) { + + // Create a new quiz with attempts. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $data = array('course' => $this->course->id, + 'sumgrades' => 1); + $quiz = $quizgenerator->create_instance($data); + $context = context_module::instance($quiz->cmid); + + // Create a couple of questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + + $cat = $questiongenerator->create_question_category(); + $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); + quiz_add_quiz_question($question->id, $quiz); + $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); + quiz_add_quiz_question($question->id, $quiz); + + $quizobj = quiz::create($quiz->id, $this->student->id); + + // Set grade to pass. + $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', + 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); + $item->gradepass = 80; + $item->update(); + + if ($startattempt or $finishattempt) { + // Now, do one attempt. + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + quiz_attempt_save_started($quizobj, $quba, $attempt); + $attemptobj = quiz_attempt::create($attempt->id); + + if ($finishattempt) { + // Process some responses from the student. + $tosubmit = array(1 => array('answer' => '3.14')); + $attemptobj->process_submitted_actions(time(), false, $tosubmit); + + // Finish the attempt. + $attemptobj->process_finish(time(), false); + } + return array($quiz, $context, $quizobj, $attempt, $attemptobj); + } else { + return array($quiz, $context, $quizobj); + } + + } + /* * Test get quizzes by courses */ @@ -273,45 +353,8 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { */ public function test_get_user_attempts() { - // Create a new quiz with attempts. - $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); - $data = array('course' => $this->course->id, - 'sumgrades' => 1); - $quiz = $quizgenerator->create_instance($data); - - // Create a couple of questions. - $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - - $cat = $questiongenerator->create_question_category(); - $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); - quiz_add_quiz_question($question->id, $quiz); - - $quizobj = quiz::create($quiz->id, $this->student->id); - - // Set grade to pass. - $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', - 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); - $item->gradepass = 80; - $item->update(); - - // Start the passing attempt. - $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); - $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); - - $timenow = time(); - $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $this->student->id); - quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); - quiz_attempt_save_started($quizobj, $quba, $attempt); - - // Process some responses from the student. - $attemptobj = quiz_attempt::create($attempt->id); - $tosubmit = array(1 => array('answer' => '3.14')); - $attemptobj->process_submitted_actions($timenow, false, $tosubmit); - - // Finish the attempt. - $attemptobj = quiz_attempt::create($attempt->id); - $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); - $attemptobj->process_finish($timenow, false); + // Create a quiz with one attempt finished. + list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true, true); $this->setUser($this->student); $result = mod_quiz_external::get_user_attempts($quiz->id); @@ -591,34 +634,8 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { public function test_start_attempt() { global $DB; - // Create a new quiz with attempts. - $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); - $data = array('course' => $this->course->id, - 'sumgrades' => 1); - $quiz = $quizgenerator->create_instance($data); - $context = context_module::instance($quiz->cmid); - - try { - mod_quiz_external::start_attempt($quiz->id); - $this->fail('Exception expected due to missing questions.'); - } catch (moodle_quiz_exception $e) { - $this->assertEquals('noquestionsfound', $e->errorcode); - } - - // Create a question. - $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - - $cat = $questiongenerator->create_question_category(); - $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); - quiz_add_quiz_question($question->id, $quiz); - - $quizobj = quiz::create($quiz->id, $this->student->id); - - // Set grade to pass. - $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', - 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); - $item->gradepass = 80; - $item->update(); + // Create a new quiz with questions. + list($quiz, $context, $quizobj) = $this->create_quiz_with_questions(); $this->setUser($this->student); @@ -702,4 +719,197 @@ class mod_quiz_external_testcase extends externallib_advanced_testcase { } + /** + * Test validate_attempt + */ + public function test_validate_attempt() { + global $DB; + + // Create a new quiz with one attempt started. + list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); + + $this->setUser($this->student); + + // Invalid attempt. + try { + $params = array('attemptid' => -1, 'page' => 0); + testable_mod_quiz_external::validate_attempt($params); + $this->fail('Exception expected due to invalid attempt id.'); + } catch (dml_missing_record_exception $e) { + $this->assertEquals('invalidrecord', $e->errorcode); + } + + // Test OK case. + $params = array('attemptid' => $attempt->id, 'page' => 0); + $result = testable_mod_quiz_external::validate_attempt($params); + $this->assertEquals($attempt->id, $result[0]->get_attempt()->id); + $this->assertEquals([], $result[1]); + + // Test with preflight data. + $quiz->password = 'abc'; + $DB->update_record('quiz', $quiz); + + try { + $params = array('attemptid' => $attempt->id, 'page' => 0, + 'preflightdata' => array(array("name" => "quizpassword", "value" => 'bad'))); + testable_mod_quiz_external::validate_attempt($params); + $this->fail('Exception expected due to invalid passwod.'); + } catch (moodle_exception $e) { + $this->assertEquals(get_string('passworderror', 'quizaccess_password'), $e->errorcode); + } + + // Now, try everything correct. + $params['preflightdata'][0]['value'] = 'abc'; + $result = testable_mod_quiz_external::validate_attempt($params); + $this->assertEquals($attempt->id, $result[0]->get_attempt()->id); + $this->assertEquals([], $result[1]); + + // Try to open attempt in closed quiz. + $quiz->timeopen = time() - WEEKSECS; + $quiz->timeclose = time() - DAYSECS; + $DB->update_record('quiz', $quiz); + + try { + testable_mod_quiz_external::validate_attempt($params); + $this->fail('Exception expected due to passed dates.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('attempterror', $e->errorcode); + } + + // Page out of range. + $quiz->timeopen = 0; + $quiz->timeclose = 0; + $DB->update_record('quiz', $quiz); + $params['page'] = 4; + try { + testable_mod_quiz_external::validate_attempt($params); + $this->fail('Exception expected due to page out of range.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('Invalid page number', $e->errorcode); + } + + // Finish the attempt. + $attemptobj = quiz_attempt::create($attempt->id); + $attemptobj->process_finish(time(), false); + + try { + testable_mod_quiz_external::validate_attempt($params); + $this->fail('Exception expected due to attempt finished.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('attemptalreadyclosed', $e->errorcode); + } + + // Test user with no capabilities. + // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. + assign_capability('mod/quiz:attempt', CAP_PROHIBIT, $this->studentrole->id, $context->id); + // Empty all the caches that may be affected by this change. + accesslib_clear_all_caches_for_unit_testing(); + course_modinfo::clear_instance_cache(); + + try { + testable_mod_quiz_external::validate_attempt($params); + $this->fail('Exception expected due to missing permissions.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Now try with a different user. + $this->setUser($this->teacher); + + $params['page'] = 0; + try { + testable_mod_quiz_external::validate_attempt($params); + $this->fail('Exception expected due to not your attempt.'); + } catch (moodle_quiz_exception $e) { + $this->assertEquals('notyourattempt', $e->errorcode); + } + } + + /** + * Test get_attempt_data + */ + public function test_get_attempt_data() { + global $DB; + + // Create a new quiz with one attempt started. + list($quiz, $context, $quizobj, $attempt, $attemptobj) = $this->create_quiz_with_questions(true); + + $quizobj = $attemptobj->get_quizobj(); + $quizobj->preload_questions(); + $quizobj->load_questions(); + $questions = $quizobj->get_questions(); + + $this->setUser($this->student); + + // We receive one question per page. + $result = mod_quiz_external::get_attempt_data($attempt->id, 0); + $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); + + $this->assertEquals($attempt, (object) $result['attempt']); + $this->assertEquals(1, $result['nextpage']); + $this->assertCount(0, $result['messages']); + $this->assertCount(1, $result['questions']); + $this->assertEquals(1, $result['questions'][0]['slot']); + $this->assertEquals(1, $result['questions'][0]['number']); + $this->assertEquals('numerical', $result['questions'][0]['type']); + $this->assertEquals('todo', $result['questions'][0]['state']); + $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']); + $this->assertFalse($result['questions'][0]['flagged']); + $this->assertEquals(0, $result['questions'][0]['page']); + $this->assertEmpty($result['questions'][0]['mark']); + $this->assertEquals(1, $result['questions'][0]['maxmark']); + + // Now try the last page. + $result = mod_quiz_external::get_attempt_data($attempt->id, 1); + $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); + + $this->assertEquals($attempt, (object) $result['attempt']); + $this->assertEquals(-1, $result['nextpage']); + $this->assertCount(0, $result['messages']); + $this->assertCount(1, $result['questions']); + $this->assertEquals(2, $result['questions'][0]['slot']); + $this->assertEquals(2, $result['questions'][0]['number']); + $this->assertEquals('numerical', $result['questions'][0]['type']); + $this->assertEquals('todo', $result['questions'][0]['state']); + $this->assertEquals(get_string('notyetanswered', 'question'), $result['questions'][0]['status']); + $this->assertFalse($result['questions'][0]['flagged']); + $this->assertEquals(1, $result['questions'][0]['page']); + + // Finish previous attempt. + $attemptobj->process_finish(time(), false); + + // Change setting and expect two pages. + $quiz->questionsperpage = 4; + $DB->update_record('quiz', $quiz); + quiz_repaginate_questions($quiz->id, $quiz->questionsperpage); + + // Start with new attempt with the new layout. + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 2, false, $timenow, false, $this->student->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + quiz_attempt_save_started($quizobj, $quba, $attempt); + + // We receive two questions per page. + $result = mod_quiz_external::get_attempt_data($attempt->id, 0); + $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_data_returns(), $result); + $this->assertCount(2, $result['questions']); + $this->assertEquals(-1, $result['nextpage']); + + // Check questions looks good. + $found = 0; + foreach ($questions as $question) { + foreach ($result['questions'] as $rquestion) { + if ($rquestion['slot'] == $question->slot) { + $this->assertTrue(strpos($rquestion['html'], "qid=$question->id") !== false); + $found++; + } + } + } + $this->assertEquals(2, $found); + + } + } diff --git a/mod/quiz/version.php b/mod/quiz/version.php index 57b48ce8227..6c6f841352f 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111607; +$plugin->version = 2015111608; $plugin->requires = 2015111000; $plugin->component = 'mod_quiz'; $plugin->cron = 60;