diff --git a/README.md b/README.md index bd6fa3b..907939b 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ Enrich your videos by the powerful features of the social video player. Create p # Changelog +### v1.16 +* added Timing Mode +* bug fixes +* ``ep5 version 2.6`` + ### v1.15 * added Gradebook integration * added Multiple-choice questions to Quiz-mode diff --git a/amd/src/ivs_activity_settings_page.js b/amd/src/ivs_activity_settings_page.js index 6b5c455..db5f55f 100644 --- a/amd/src/ivs_activity_settings_page.js +++ b/amd/src/ivs_activity_settings_page.js @@ -22,13 +22,13 @@ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/m set_display_option('#id_annotations_enabled_value', '#id_mod_ivsnotification'); set_display_option('#id_exam_mode_enabled_value', '#id_mod_ivsgrades'); - set_display_option_timing_mode( '#id_match_question_enabled_value', '#fgroup_id_show_videotest_feedback'); - set_display_option_timing_mode('#id_match_question_enabled_value', '#fgroup_id_show_videotest_solution'); + set_display_option_timing_mode( '#id_match_question_enabled_value', '#fgroup_id_show_realtime_results'); + set_display_option_timing_mode('#id_match_question_enabled_value', '#fgroup_id_show_timing_take_summary'); $('#id_match_question_enabled_value').change(function () { //Show timing mode checkboxes - set_display_option_timing_mode( $('#id_match_question_enabled_value'), '#fgroup_id_show_videotest_feedback'); - set_display_option_timing_mode($('#id_match_question_enabled_value'), '#fgroup_id_show_videotest_solution'); + set_display_option_timing_mode( $('#id_match_question_enabled_value'), '#fgroup_id_show_realtime_results'); + set_display_option_timing_mode($('#id_match_question_enabled_value'), '#fgroup_id_show_timing_take_summary'); return; }); diff --git a/classes/MoodleMatchController.php b/classes/MoodleMatchController.php index 5323824..00a5607 100644 --- a/classes/MoodleMatchController.php +++ b/classes/MoodleMatchController.php @@ -26,6 +26,7 @@ use ltiservice_gradebookservices\local\service\gradebookservices; use mod_ivs\gradebook\GradebookService; +use mod_ivs\ivs_match\MatchTimingType; use mod_ivs\ivs_match\question\QuestionSummary; use mod_ivs\ivs_match\AssessmentConfig; use mod_ivs\ivs_match\IvsMatchControllerBase; @@ -37,8 +38,11 @@ use mod_ivs\ivs_match\IIvsMatch; use mod_ivs\ivs_match\MatchConfig; use mod_ivs\ivs_match\MatchTake; +use mod_ivs\ivs_match\timing\MatchTimingTakeResult; use mod_ivs\IvsHelper; use mod_ivs\output\match\question_click_answer_view; +use mod_ivs\output\match\timing_question_answer_view; +use mod_ivs\output\match\timing_type_answer_view; use mod_ivs\output\match\question_single_choice_answer_view; use mod_ivs\output\match\question_text_answer_view; use mod_ivs\settings\SettingsDefinition; @@ -106,7 +110,8 @@ public function match_question_get_db($questionid, $skipaccess = false) { throw new MatchQuestionNotFoundException(); } - return $this->record_to_player_question((array) $questionfromdb); + $questionfromdb = $this->record_to_player_question((array) $questionfromdb); + return $questionfromdb; } @@ -129,6 +134,8 @@ public function match_questions_get_by_video_db($videoid, $order = 'timecode', $ $questions[$question->id] = $this->record_to_player_question((array) $question); } + $questions = $this->match_questions_reformat_timing_questions($videoid, $questions); + return $questions; } @@ -156,6 +163,9 @@ public function match_questions_get_by_video_db_order($videoid, $order = 'timeco foreach ($records as $question) { $questions[$question->id] = $this->record_to_player_question((array) $question); } + + $questions = $this->match_questions_reformat_timing_questions($videoid, $questions); + return $questions; } @@ -389,6 +399,7 @@ public function match_question_answers_get_by_question_and_user_db($questionid, foreach ($record as $answer) { $answers[$answer->id] = $this->record_to_player_answer((array) $answer); } + return $answers; } @@ -439,6 +450,53 @@ public function match_question_answers_get_by_question_and_user_for_reporting($q return $detail; } + /** + * Gets all necessary data for reporting + * + * @param int $questionid + * @param int $userid + * @param bool $skipaccess + * @return array + * @throws \mod_ivs\ivs_match\exception\MatchQuestionAccessDeniedException + * @throws \mod_ivs\ivs_match\exception\MatchQuestionNotFoundException + */ + public function match_question_answers_get_by_timing_type_and_user_for_reporting($currenttimingtype, $userid, $videoid, $skipaccess = false) { + + //get all answers by timing type + $timingtypeanswers = $this->match_question_answers_get_by_timing_type_and_user_db($currenttimingtype->id, $userid, $videoid, $skipaccess); + $detail = []; + + $counterfirst = 0; + $counterlast = 0; + + foreach ($timingtypeanswers as $questionid => $answers){ + if (!empty($answers)){ + $answers = array_values($answers); + if ($answers[0]['is_correct']){ + $counterfirst++; + } + if (end($answers)['is_correct']){ + $counterlast++; + } + } + } + + $detail = array( + + 'userid' => $userid, + 'type' => 'timing_question', + 'videoid' => $videoid, + 'question' => [ + 'nid' => $currenttimingtype->id, + 'title' => $currenttimingtype->title, + 'question_body' => $currenttimingtype->description, + ], + 'answers' => [$counterfirst, $counterlast] + ); + + return $detail; + } + /** * Get a collection of answers by video and user keyed by question_id * @@ -888,7 +946,7 @@ public function get_assessment_type_options(){ return[ AssessmentConfig::ASSESSMENT_TYPE_NONE => get_string('ivs_match_config_assessment_mode_none', 'ivs'), AssessmentConfig::ASSESSMENT_TYPE_QUIZ => get_string('ivs_match_config_assessment_mode_quiz', 'ivs'), - //AssessmentConfig::ASSESSMENT_TYPE_TIMING => get_string('ivs_match_config_assessment_mode_timing', 'ivs'), + AssessmentConfig::ASSESSMENT_TYPE_TIMING => get_string('ivs_match_config_assessment_mode_timing', 'ivs'), ]; } @@ -903,7 +961,6 @@ public function get_assessment_type_options(){ */ public function assessment_config_get_by_user_and_video($userid, $videoid, $includesimulation = false) { - $assessmentconfig = []; global $DB; $ivs = $DB->get_record('ivs', array('id' => $videoid), '*', MUST_EXIST); $moodlematchcontroller = new MoodleMatchController(); @@ -1052,6 +1109,16 @@ public function get_match_question_title($question) { return !empty($question['title']) ? $question['title'] : shorten_text($question['question_body']); } + /** + * Get the title from a match question + * @param array $question + * + * @return mixed + */ + public function get_match_question_timing_title($timingtype) { + return !empty($timingtype->description) ? ($timingtype->title . ': ' . shorten_text($timingtype->description)) : $timingtype->label; + } + /** * Get question summary raw data * @@ -1149,6 +1216,15 @@ public function get_question_summary_formated($question, $coursestudents) { round($questionsummary->last_attempt_correct * 100 / $questionsummary->num_students_participation, 0) . '%'; break; + case 'timing_question': + $data->question_type = get_string('ivs_match_question_summary_question_type_timing', 'ivs'); + $data->question_first_try = $questionsummary->num_students_participation == 0 ? '0%' : + round($questionsummary->first_attempt_correct * 100 / $questionsummary->num_students_participation, 0) . + '%'; + $data->question_last_try = $questionsummary->num_students_participation == 0 ? '0%' : + round($questionsummary->last_attempt_correct * 100 / $questionsummary->num_students_participation, 0) . + '%'; + break; } $data->question_answered = $questionsummary->num_students_participation . ' / ' . $questionsummary->num_students_total; @@ -1233,6 +1309,8 @@ public function get_question_answers_data($detailarray, $questions, $cmid, $vide $data->text_question = false; $data->single_choice_question = false; $data->click_question = false; + $data->timing_question = false; + $timing_types = false; for ($i = $offset; $i < $offset + $perpage; $i++) { if ($i == $totalcount) { @@ -1256,6 +1334,18 @@ public function get_question_answers_data($detailarray, $questions, $cmid, $vide $data->question_type = get_string('ivs_match_question_summary_question_type_single', 'ivs'); $data->single_choice_question = true; $renderable = new question_single_choice_answer_view($answer, $answerusers[$i]); + break; + case 'timing_question': + $data->question_type = get_string('ivs_match_question_summary_question_type_timing', 'ivs'); + $data->timing_question = true; + $url = $_SERVER['REQUEST_URI']; + if (strpos($url, 'question_answers.php')){ + $renderable = new timing_question_answer_view($answer, $answerusers[$i]); + $timing_types = true; + }else{ + $renderable = new timing_type_answer_view($answer, $answerusers[$i]); + } + break; } @@ -1273,14 +1363,28 @@ public function get_question_answers_data($detailarray, $questions, $cmid, $vide } if (!empty($output)) { // Render all Questions in Dropdown. - foreach ($questions as $question) { + if ($timing_types){ + foreach ($questions as $question) { + + $label = $controller->get_match_question_title($question); - $label = $controller->get_match_question_title($question); + $questionurl = new moodle_url('/mod/ivs/question_answers.php?id=' . $cmid . '&vid=' . $videoid . '&qid=' . + $question['nid'] . '&perpage=10'); + $selected = required_param('qid', PARAM_INT) == $question['nid'] ? 'selected' : ''; + $data->dropdown_options[] = ''; + } + }else{ + $timingtypes = $controller->match_timing_type_get_db($videoid); + foreach ($timingtypes as $timingtype) { - $questionurl = new moodle_url('/mod/ivs/question_answers.php?id=' . $cmid . '&vid=' . $videoid . '&qid=' . - $question['nid'] . '&perpage=10'); - $selected = required_param('qid', PARAM_INT) == $question['nid'] ? 'selected' : ''; - $data->dropdown_options[] = ''; + $label = $controller->get_match_question_timing_title($timingtype); + + $questionurl = new moodle_url('/mod/ivs/question_type_answers.php?id=' . $cmid . '&vid=' . $videoid . '&qid=' . + $timingtype->id . '&perpage=10'); + $qid = required_param('qid', PARAM_ALPHANUMEXT); + $selected = $qid == $timingtype->id ? 'selected' : ''; + $data->dropdown_options[] = ''; + } } // Render Pager Options in Dropdown. @@ -1310,7 +1414,8 @@ public function get_question_answers_data($detailarray, $questions, $cmid, $vide $data->single_choice_correct = get_string("ivs_match_question_answer_menu_label_single_choice_correct", 'ivs'); $data->single_choice_selected_answer = get_string("ivs_match_question_answer_menu_label_last_single_choice_selected_answer", 'ivs'); - + $data->first_timing_answer = get_string("ivs_match_question_answer_menu_label_first_timing_answer", 'ivs'); + $data->last_timing_answer = get_string("ivs_match_question_answer_menu_label_last_timing_answer", 'ivs'); return $data; } @@ -1428,6 +1533,105 @@ public function get_question_answers_data_text_question($answer, $courseuser) { return $data; } + /** + * Get all answers for a timing questions + * @param array $answer + * @param \stdClass $courseuser + * + * @return \stdClass + */ + public function get_question_answers_data_timing_type($answer, $courseuser) { + $data = new \stdClass; + + $user = IvsHelper::get_user($courseuser->id); + $controller = $this; + + $data->fullname = $user['fullname']; + $data->id = $courseuser->id; + + foreach ($answer as $key => $value) { + + $questions = $controller->match_questions_get_by_video_db($value['videoid']); + $timingquestioncount = 0; + foreach($questions as $question){ + if ($value['question']['nid'] == $question['type_data']['timing_type_id']){ + $timingquestioncount++; + } + } + + + $data->first = '0/' . $timingquestioncount; + $data->last = '0/' . $timingquestioncount; + $data->retries = '-'; + + $takes = $controller->match_takes_get_by_user_and_video_db($user['user']->id, $value['videoid'], $value['videoid']); + $numtakes = count($takes); + + + + if ($numtakes > 0) { + $data->retries = $numtakes - 1; + if ($value['userid'] === $courseuser->id) { + $data->first = $value['answers'][0] . '/' . $timingquestioncount; + $data->last = $value['answers'][1] . '/' . $timingquestioncount; + break; + } + } + } + return $data; + } + + /** + * Get all answers for a timing questions + * @param array $answer + * @param \stdClass $courseuser + * + * @return \stdClass + */ + public function get_question_answers_data_timing_question($answer, $courseuser) { + $data = new \stdClass; + + $user = IvsHelper::get_user($courseuser->id); + + $data->fullname = $user['fullname']; + $data->id = $courseuser->id; + + $controller = $this; + + foreach ($answer as $key => $value) { + + $data->first = '-'; + $data->last = '-'; + $data->retries = '-'; + if (!empty($value['answers'][1]) && $value['answers'][1]['user_id'] === $courseuser->id) { + + $userid = $courseuser->id; + $answers = $controller->match_question_answers_get_by_question_and_user_db($value['question']['nid'], $userid); + $numanswers = count($answers); + + if ($numanswers > 0) { + $data->retries = $numanswers - 1; + + $lastanswer = $value['answers'][1]; + if (!empty($value['answers'][0])) { + $firstanswer = $value['answers'][0]; + } else { + $firstanswer = $value['answers'][1]; + } + + $data->first = !empty($firstanswer['is_correct']) ? 1 : 0; + $data->last = !empty($lastanswer['is_correct']) ? 1 : 0; + + } + + break; + + } + } + + return $data; + } + private function get_formative_assessment_config($userid, $ivs) { $videoid = $ivs->id; @@ -1468,6 +1672,9 @@ private function get_videotest_assessment_config_by_user($userid, $ivs) { $settingscontroller = new SettingsService(); $activitysettings = $settingscontroller->get_settings_for_activity($videoid, $ivs->course); + $gradebookservice = new GradebookService(); + $grade_methods = $gradebookservice->ivs_get_grade_method_options(); + if ($this->has_edit_access($videoid)) { $assconf = new AssessmentConfig(); @@ -1488,10 +1695,16 @@ private function get_videotest_assessment_config_by_user($userid, $ivs) { $assconf->matchConfig = $this->match_video_get_config_db($videoid); $assconf->takes_left = $activitysettings['exam_mode_enabled']->value ? $this->get_remaining_attempts($userid, $videoid, $contextid) : 1; $assconf->takes = $this->match_takes_get_by_user_and_video_db($userid, $videoid, $contextid); + $assconf->grade_method = $grade_methods[$activitysettings[SettingsDefinition::SETTING_PLAYER_VIDEOTEST_GRADE_METHOD]->value]; + $assconf->exam_enabled = (int)$activitysettings[SettingsDefinition::SETTING_PLAYER_EXAM_ENABLED]->value; $num_takes = count($assconf->takes); $already_passed = FALSE; + if ($assconf->matchConfig->is_timing_mode()){ + $assconf->matchConfig->rate .= $this->get_success_rate_points_label($videoid, $assconf); + } + if ($num_takes == 0) { $assconf->status = AssessmentConfig::TAKES_LEFT_NEW; $assconf->status_description = get_string("ivs_match_config_status_not_started_label", 'ivs'); @@ -1508,35 +1721,18 @@ private function get_videotest_assessment_config_by_user($userid, $ivs) { } } - $gradebookservice = new GradebookService(); - $scoreinfo = $gradebookservice->ivs_gradebook_get_score_info_by_takes($assconf->takes, $ivs); + $score = $gradebookservice->ivs_gradebook_get_score_by_takes($assconf->takes, $ivs); - if ($scoreinfo['score'] >= $assconf->matchConfig->rate){ + if ($score >= $assconf->matchConfig->rate){ $already_passed = TRUE; } - if ($already_passed) { - $assconf->status_description = get_string("ivs_match_config_status_passed_label", 'ivs') . $scoreinfo['desc'] . $scoreinfo['score'] . '%'; - if($assconf->takes_left > 0) { - $assconf->status = AssessmentConfig::TAKES_LEFT_COMPLETED_SUCCESS; - }else{ - if($assconf->matchConfig->assessment_type == AssessmentConfig::ASSESSMENT_TYPE_TIMING){ - $assconf->status = AssessmentConfig::NO_TAKES_LEFT_COMPLETED_SUCCESS_NO_SUMMARY; - }else{ - $assconf->status = AssessmentConfig::NO_TAKES_LEFT_COMPLETED_SUCCESS; - } - } + if ($assconf->matchConfig->is_quiz_mode()){ + $this->get_quiz_status($assconf, $already_passed, $score, $take_in_progress); } - elseif ($assconf->takes_left == 0) { - $assconf->status = AssessmentConfig::NO_TAKES_LEFT_COMPLETED_FAILED; - $assconf->status_description = get_string("ivs_match_config_status_failed_label", 'ivs') . $scoreinfo['desc'] . $scoreinfo['score'] . '%'; - }else{ - $assconf->status = AssessmentConfig::TAKES_LEFT_PROGRESS; - if($take_in_progress) { - $assconf->status_description = get_string("ivs_match_config_status_progress_label", 'ivs'); - }else{ - $assconf->status_description = get_string("ivs_match_config_status_not_passed_label", 'ivs') . $scoreinfo['desc'] . $scoreinfo['score']. '%'; - } + + if ($assconf->matchConfig->is_timing_mode()){ + $this->get_timing_status($assconf, $already_passed, $ivs); } } @@ -1545,6 +1741,51 @@ private function get_videotest_assessment_config_by_user($userid, $ivs) { return $assessmentconfig; } + private function get_quiz_status(&$assconf, $already_passed, $score, $take_in_progress) { + + if ($already_passed) { + $assconf->status_description = get_string("ivs_match_config_status_passed_label", 'ivs') . $score . '%'; + $assconf->status = AssessmentConfig::NO_TAKES_LEFT_COMPLETED_SUCCESS; + if($assconf->takes_left > 0 || $assconf->matchConfig->attempts == 0) { + $assconf->status = AssessmentConfig::TAKES_LEFT_COMPLETED_SUCCESS; + } + } + elseif ($assconf->takes_left == 0) { + $assconf->status = AssessmentConfig::NO_TAKES_LEFT_COMPLETED_FAILED; + $assconf->status_description = get_string("ivs_match_config_status_failed_label", 'ivs') . $score. '%'; + }else{ + $assconf->status = AssessmentConfig::TAKES_LEFT_PROGRESS; + if($take_in_progress) { + $assconf->status_description = get_string("ivs_match_config_status_progress_label", 'ivs'); + }else{ + $assconf->status_description = get_string("ivs_match_config_status_not_passed_label", 'ivs') . $score . '%'; + } + } + } + + private function get_timing_status(&$assconf, $already_passed, $ivs) { + $gradebookservice = new GradebookService(); + + if ($already_passed) { + $assconf->status_description = get_string("ivs_match_config_timing_status_passed_label", 'ivs'); + $assconf->status = AssessmentConfig::NO_TAKES_LEFT_COMPLETED_SUCCESS_NO_SUMMARY; + if($assconf->takes_left > 0 || $assconf->matchConfig->attempts == 0) { + $assconf->status = AssessmentConfig::TAKES_LEFT_COMPLETED_SUCCESS; + } + } + elseif ($assconf->takes_left == 0) { + $assconf->status = AssessmentConfig::NO_TAKES_LEFT_COMPLETED_FAILED; + $assconf->status_description = get_string("ivs_match_config_timing_status_not_passed_label", 'ivs'); + }else{ + $assconf->status = AssessmentConfig::TAKES_LEFT_PROGRESS; + $assconf->status_description = get_string("ivs_match_config_timing_status_not_passed_label", 'ivs'); + } + + $assconf->status_description .= $gradebookservice->get_rendered_timing_take_summary($assconf->takes, $ivs); + } + + + private function get_quiz_match_config($ivs) { global $DB; $gradebookservice = new GradebookService(); @@ -1599,8 +1840,8 @@ private function get_timing_match_config($ivs) { $mc->player_controls_enabled = (int) $activitysettings['player_controls_enabled']->value; $mc->rate = !empty($gradesettings) ? (int)$gradesettings->gradepass : 100; $mc->attempts = $activitysettings[SettingsDefinition::SETTING_PLAYER_VIDEOTEST_ATTEMPTS]->value; - $mc->show_feedback = $activitysettings[SettingsDefinition::SETTING_PLAYER_SHOW_VIDEOTEST_FEEDBACK]->value; - $mc->show_solution = $activitysettings[SettingsDefinition::SETTING_PLAYER_SHOW_VIDEOTEST_SOLUTION]->value; + $mc->show_feedback = $activitysettings[SettingsDefinition::SETTING_PLAYER_SHOW_REALTIME_RESULTS]->value; + $mc->show_solution = false; return $mc; } @@ -1623,18 +1864,21 @@ private function get_ivs_videotest_context_label($ivs) { } - public function match_timing_type_get_db($ivs, $skip_access = FALSE) { + public function match_timing_type_get_db($videoid, $skip_access = FALSE) { + global $DB; + $ivs = $DB->get_record('ivs', array('id' => $videoid), '*', MUST_EXIST); + $timingtypes = []; if(!empty($ivs->match_config)) { - $data = json_decode($ivs->match_config, TRUE); + $matchconfig = json_decode($ivs->match_config, TRUE); - if(!empty($data['timing_types'])) { - return $data['timing_types']; + foreach ($matchconfig['timing_types'] as $timingtype){ + $timingtypes[] = new MatchTimingType($timingtype); } } - return []; + return $timingtypes; } public function match_timing_type_insert_db($videoid, $data, $user_id = NULL, $skip_access = FALSE) { @@ -1663,25 +1907,15 @@ public function match_timing_type_delete_db($videoid, $timing_type_id, $skip_acc throw new MatchQuestionAccessDeniedException(null, "Access denied"); } - if(empty($ivs->match_config)) { - $match_settings = []; - }else { - $match_settings = json_decode($ivs->match_config, TRUE); - } + $matchtimingtypes = $this->match_timing_type_get_db($videoid, TRUE); //check if command id exists - foreach ($match_settings['timing_types'] as $k => $c) { - if ($c['id'] === $timing_type_id) { - unset($match_settings['timing_types'][$k]); + foreach ($matchtimingtypes as $i => $matchtimingtype) { + if ($matchtimingtype->id === $timing_type_id) { + unset($matchtimingtypes[$i]); } } - - //normalize keys - $match_settings['timing_types'] = array_values($match_settings['timing_types']); - - $ivs->match_config = json_encode((array) $match_settings); - $DB->insert_record('ivs', $ivs); - + $this->update_timing_types($ivs, $matchtimingtypes); } protected function saveTimingType($post_data, $ivs, $skip_access = FALSE) { @@ -1693,11 +1927,8 @@ protected function saveTimingType($post_data, $ivs, $skip_access = FALSE) { throw new MatchQuestionAccessDeniedException(null, "Access denied"); } - $timing_types = $this->match_timing_type_get_db($ivs, $skip_access); $post_data = (object) $post_data; - - //parse data $id = $post_data->id; @@ -1707,18 +1938,18 @@ protected function saveTimingType($post_data, $ivs, $skip_access = FALSE) { } - $timing_type['type'] = $post_data->type; + $timing_type_array['type'] = $post_data->type; - $timing_type['timestamp'] = $post_data->timestamp; - $timing_type['duration'] = $post_data->duration; - $timing_type['title'] = $post_data->title; + $timing_type_array['timestamp'] = $post_data->timestamp; + $timing_type_array['duration'] = $post_data->duration; + $timing_type_array['title'] = $post_data->title; $position = explode(',', $post_data->btn['position']); $new_pos = []; foreach ($position as $pos){ $new_pos[] = $pos; } - $timing_type = [ + $timing_type_array = [ 'title' => $post_data->title, 'duration' => $post_data->duration, 'weight' => $post_data->weight, @@ -1731,37 +1962,144 @@ protected function saveTimingType($post_data, $ivs, $skip_access = FALSE) { 'description' => $post_data->btn['description'] ] ]; + $timing_type_array['id'] = $id; - $timing_type['id'] = $id; - + $matchtimingtype = new MatchTimingType($timing_type_array); + $timing_types = $this->match_timing_type_get_db($videoid, $skip_access); - //check existing id $is_new = TRUE; - foreach ($timing_types as $k => $c) { - if ($c['id'] === $id) { - $timing_types[$k] = $timing_type; + foreach ($timing_types as &$timing_type) { + if ($timing_type->id == $matchtimingtype->id) { + $timing_type = $matchtimingtype; $is_new = FALSE; } } + if ($is_new) { - $timing_types[] = $timing_type; + + $timing_types[] = $matchtimingtype; } - //save node + $this->update_timing_types($ivs, $timing_types); + + return $matchtimingtype; + + } + private function update_timing_types($ivs, $matchtimingtypes = []) { + + global $DB; if(empty($ivs->match_config)) { $match_settings = []; }else { $match_settings = json_decode($ivs->match_config, TRUE); } - $match_settings['timing_types'] = $timing_types; + $matchtimingtypesjson = array_map( function ($timingtype){ + return $timingtype->to_player_json(); + }, $matchtimingtypes); - $ivs->match_config = json_encode((array) $match_settings); + $match_settings['timing_types'] = $matchtimingtypesjson; + $ivs->match_config = json_encode($match_settings); $DB->update_record('ivs', $ivs); - return $timing_type; } + + private function get_success_rate_points_label($videoid, $assconf) { + $successratelabel = ''; + $pointstotal = 0; + + global $DB; + $matchcontroller = new MoodleMatchController(); + $matchquestions = $DB->get_records('ivs_matchquestion', array('video_id' => $videoid)); + $timingtypes = $matchcontroller->match_timing_type_get_db($videoid); + foreach ($matchquestions as $matchquestion) { + $type_data = unserialize($matchquestion->type_data); + $timingtype = MatchTimingTakeResult::find_object_by_id($type_data['timing_type_id'], $timingtypes); + $pointstotal += $timingtype->score; + } + + + $pointstosuccess = $pointstotal / 100 * $assconf->matchConfig->rate; + $successratelabel = '% (' . $pointstosuccess . ' ' . get_string('ivs_grademethod_timing_take_summary_points', 'ivs') . ')'; + return $successratelabel; + } + + private function match_questions_reformat_timing_questions($videoid, $questions) { + + $timingtypes = $this->match_timing_type_get_db($videoid); + $formatedquestions = []; + + foreach($questions as $question){ + if ($question['type'] === 'timing_question'){ + + foreach($timingtypes as $timingtype){ + if ($timingtype->id === $question['type_data']['timing_type_id']){ + $question['title'] = $this->match_format_question_time($question['timestamp']) . ' - ' . $this->match_format_question_time($question['timestamp'] + $question['duration']) . ' ' . $timingtype->title; + $question['question_body'] = $timingtype->description; + break; + } + } + } + $formatedquestions[$question['nid']] = $question; + } + return $formatedquestions; + } + + private function match_format_question_time($milliseconds) { + return sprintf('%02d:%02d', floor(($milliseconds % 3600000) / 60000), floor(($milliseconds % 60000) / 1000)); + } + + private function match_question_answers_get_by_timing_type_and_user_db( $timingtypeid, $userid, $videoid, $skipaccess) { + + + $matchquestions = $this->match_questions_get_by_video_db($videoid); + + foreach ($matchquestions as $questionId => $question){ + if ($question['type'] !== 'timing_question'){ + unset($matchquestions[$questionId]); + continue; + } + + if ($question['type_data']['timing_type_id'] !== $timingtypeid){ + unset($matchquestions[$questionId]); + } + } + + + global $DB; + $timingtypeanswers = []; + foreach($matchquestions as $id => $matchquestion){ + + + $record = $DB->get_records('ivs_matchanswer', array( + 'question_id' => $id, + 'user_id' => $userid + )); + + $answers = []; + foreach ($record as $answer) { + $answers[$answer->id] = $this->record_to_player_answer((array) $answer); + } + $timingtypeanswers[$id] = $answers; + } + + return $timingtypeanswers; + } + + public function match_timing_get_current_timing_type($instance, $qid) + { + //Get current timing type + $timingtypes = $this->match_timing_type_get_db($instance); + $currenttimingtype = null; + foreach ($timingtypes as $timingtype){ + if ($timingtype->id === $qid){ + $currenttimingtype = $timingtype; + break; + } + } + return $currenttimingtype; + } } diff --git a/classes/StatisticsService.php b/classes/StatisticsService.php index c7f6512..8ca2221 100644 --- a/classes/StatisticsService.php +++ b/classes/StatisticsService.php @@ -170,6 +170,8 @@ private function numIVSMatchQuestionsTypes() { $this->database->count_records_sql("SELECT COUNT(id) FROM {ivs_matchquestion} WHERE type='click_question'"); $questiontypes['text_question'] = $this->database->count_records_sql("SELECT COUNT(id) FROM {ivs_matchquestion} WHERE type='text_question'"); + $questiontypes['timing_question'] = + $this->database->count_records_sql("SELECT COUNT(id) FROM {ivs_matchquestion} WHERE type='timing_question'"); return $questiontypes; } diff --git a/classes/UpdateService.php b/classes/UpdateService.php index 8cc5d0f..8c4a64d 100755 --- a/classes/UpdateService.php +++ b/classes/UpdateService.php @@ -78,7 +78,7 @@ public function settingInvertUpdate() { * @return void */ public function alterVideocommentTableForCommentType() { - $this->database->execute("ALTER TABLE {ivs_videocomment} ADD comment_type VARCHAR(255) DEFAULT 'comment'"); + $this->database->execute("ALTER TABLE {ivs_videocomment} ADD COLUMN IF NOT EXISTS comment_type VARCHAR(255) DEFAULT 'comment'"); $allcomments = $this->database->get_records_sql("SELECT id FROM {ivs_videocomment}"); foreach ($allcomments as $comment) { diff --git a/classes/admin_setting_configtext_ivs_custom.php b/classes/admin_setting_configtext_ivs_custom.php index 9ef21df..5fa1496 100644 --- a/classes/admin_setting_configtext_ivs_custom.php +++ b/classes/admin_setting_configtext_ivs_custom.php @@ -27,6 +27,7 @@ public function validate($data) { if ($data > IVS_SETTING_PLAYER_ANNOTATION_AUDIO_MAX_DURATION || $data < 0) { return get_string('ivs_setting_annotation_audio_max_duration_validation', 'mod_ivs'); } + return true; } -} \ No newline at end of file +} diff --git a/classes/admin_setting_configtext_ivs_custom_with_lock.php b/classes/admin_setting_configtext_ivs_custom_with_lock.php index 39cce64..678a302 100644 --- a/classes/admin_setting_configtext_ivs_custom_with_lock.php +++ b/classes/admin_setting_configtext_ivs_custom_with_lock.php @@ -18,6 +18,7 @@ use admin_setting_configtext; use admin_setting_flag; +use mod_ivs\settings\SettingsDefinition; class admin_setting_configtext_ivs_custom_with_lock extends admin_setting_configtext { /** @@ -36,13 +37,28 @@ public function __construct($name, $visiblename, $description, $defaultsetting, } public function validate($data) { - if (!is_numeric($data)) { - return get_string('ivs_setting_annotation_audio_max_duration_validation', 'mod_ivs'); + + + if ($this->name == SettingsDefinition::SETTING_PLAYER_ANNOTATION_AUDIO_MAX_DURATION){ + if (!is_numeric($data)) { + return get_string('ivs_setting_annotation_audio_max_duration_validation', 'mod_ivs'); + } + + if ($data > IVS_SETTING_PLAYER_ANNOTATION_AUDIO_MAX_DURATION || $data < 0) { + return get_string('ivs_setting_annotation_audio_max_duration_validation', 'mod_ivs'); + } } - if ($data > IVS_SETTING_PLAYER_ANNOTATION_AUDIO_MAX_DURATION || $data < 0) { - return get_string('ivs_setting_annotation_audio_max_duration_validation', 'mod_ivs'); + if ($this->name == SettingsDefinition::SETTING_PLAYER_VIDEOTEST_GRADE_TO_PASS){ + if (!is_numeric($data)) { + return get_string('ivs_setting_grade_to_pass_validation', 'mod_ivs'); + } + + if ($data > 100 || $data < 0) { + return get_string('ivs_setting_grade_to_pass_validation', 'mod_ivs'); + } } + return true; } -} \ No newline at end of file +} diff --git a/classes/gradebook/GradebookService.php b/classes/gradebook/GradebookService.php index 202b8be..ea728b3 100644 --- a/classes/gradebook/GradebookService.php +++ b/classes/gradebook/GradebookService.php @@ -26,9 +26,12 @@ namespace mod_ivs\gradebook; use core_course\analytics\target\course_gradetopass; +use core_table\local\filter\string_filter; use enrol_self\self_test; use Helper\MoodleHelper; +use mod_ivs\exception\ivs_exception; use mod_ivs\ivs_match\AssessmentConfig; +use mod_ivs\ivs_match\timing\MatchTimingTakeResult; use mod_ivs\MoodleMatchController; use mod_ivs\settings\SettingsDefinition; use mod_ivs\settings\SettingsService; @@ -106,10 +109,9 @@ public function ivs_get_attempt_options() { * @param $takes * @return mixed|null */ - private function get_grade_item_best_attempt($takes) { + public function get_best_score_by_takes($takes) { $score = null; - foreach ($takes as $take) { if (isset($take->score) && ($score === null || $take->score > $score)) { $score = $take->score; @@ -124,7 +126,7 @@ private function get_grade_item_best_attempt($takes) { * @param $takes * @return float|int */ - private function get_grade_item_average($takes) { + private function get_average_score_by_takes($takes) { $total = 0; $count = 0; @@ -146,8 +148,9 @@ private function get_grade_item_average($takes) { * @param $takes * @return int */ - private function get_grade_item_first_attempt($takes) { - return $takes[0]->score ?? 0; + private function get_first_score_by_takes($takes) { + $score = $takes[0]->score ?? 0; + return $score; } /** @@ -155,9 +158,10 @@ private function get_grade_item_first_attempt($takes) { * @param $takes * @return int */ - private function get_grade_item_last_attempt($takes) { + private function get_last_score_by_takes($takes) { $take = end($takes); - return $take->score ?? 0; + $score = $take->score ?? 0; + return $score; } /** @@ -266,36 +270,165 @@ public function ivs_get_grade_settings($ivs){ * Returns score and description for ivs activity quiz view * @param $takes * @param $ivs - * @return array + * @return float * @throws \coding_exception */ - public function ivs_gradebook_get_score_info_by_takes($takes, $ivs) { + public function ivs_gradebook_get_score_by_takes($takes, $ivs) { + $settingsservice = new SettingsService(); + $activitysettings = $settingsservice->get_settings_for_activity($ivs->id, $ivs->course); + $gradingmethod = $activitysettings[SettingsDefinition::SETTING_PLAYER_VIDEOTEST_GRADE_METHOD]->value; + + switch ($gradingmethod) { + case self::GRADE_METHOD_BEST_ATTEMPT: + $score = $this->get_best_score_by_takes($takes); + break; + case self::GRADE_METHOD_AVERAGE: + $score = $this->get_average_score_by_takes($takes); + break; + case self::GRADE_METHOD_FIRST_ATTEMPT: + $score = $this->get_first_score_by_takes($takes); + break; + case self::GRADE_METHOD_LAST_ATTEMPT: + $score = $this->get_last_score_by_takes($takes); + break; + } + + return round($score, 2 ); + } + + public function ivs_gradebook_get_timing_take_summary_data_by_grade_method($takes, $ivs){ $settingsservice = new SettingsService(); $activitysettings = $settingsservice->get_settings_for_activity($ivs->id, $ivs->course); - $score = 0; $gradingmethod = $activitysettings[SettingsDefinition::SETTING_PLAYER_VIDEOTEST_GRADE_METHOD]->value; switch ($gradingmethod) { case self::GRADE_METHOD_BEST_ATTEMPT: - $score = $this->get_grade_item_best_attempt($takes); - $desc = get_string('ivs_match_config_grade_mode_best_score_label', 'ivs'); + $matchtimingresult = $this->get_best_timing_take_summary_by_takes($takes); break; case self::GRADE_METHOD_AVERAGE: - $score = $this->get_grade_item_average($takes); - $desc = get_string('ivs_match_config_grade_mode_average_score_label', 'ivs'); + $matchtimingresult = $this->get_average_timing_take_summary_by_takes($takes); break; case self::GRADE_METHOD_FIRST_ATTEMPT: - $score = $this->get_grade_item_first_attempt($takes); - $desc = get_string('ivs_match_config_grade_mode_first_attempt_score_label', 'ivs'); + $matchtimingresult = $this->get_first_timing_take_summary_by_takes($takes); break; case self::GRADE_METHOD_LAST_ATTEMPT: - $score = $this->get_grade_item_last_attempt($takes); - $desc = get_string('ivs_match_config_grade_mode_last_attempt_score_label', 'ivs'); + $matchtimingresult = $this->get_last_timing_take_summary_by_takes($takes); break; } - return ['score' => round($score, 2 ), 'desc' => $desc]; + + return $matchtimingresult; + } + + private function get_best_timing_take_summary_by_takes($takes){ + $score = null; + foreach ($takes as $take) { + if (isset($take->score) && ($score === null || $take->score > $score)) { + $score = $take->score; + $besttake = $take; + } + } + + $matchtimingresult = $this->get_evaluated_timing_type_result_by_take($besttake); + + return $matchtimingresult; + + } + + + + private function get_average_timing_take_summary_by_takes($takes){ + + $numtakes = count($takes); + $matchtimingtakeresultavg = new MatchTimingTakeResult(); + + foreach ($takes as $take) { + $matchtimingtakeresult = $this->get_evaluated_timing_type_result_by_take($take); + + $matchtimingtakeresultavg->pointsuser += $matchtimingtakeresult->pointsuser; + $matchtimingtakeresultavg->pointstotal += $matchtimingtakeresult->pointstotal; + + foreach($matchtimingtakeresult->summary as $k => $v){ + $matchtimingtakeresultavg->summary[$k]['timing_type'] = $v['timing_type']; + if (array_key_exists('sum_points', $matchtimingtakeresultavg->summary[$k])){ + $matchtimingtakeresultavg->summary[$k]['sum_points'] += $v['sum_points']; + }else{ + $matchtimingtakeresultavg->summary[$k]['sum_points'] = $v['sum_points']; + } + + if (array_key_exists('num_correct', $matchtimingtakeresultavg->summary[$k])){ + $matchtimingtakeresultavg->summary[$k]['num_correct'] += $v['num_correct']; + }else{ + $matchtimingtakeresultavg->summary[$k]['num_correct'] = $v['num_correct']; + } + } + + } + + $matchtimingtakeresultavg->pointsuser = $matchtimingtakeresultavg->pointsuser / $numtakes; + $matchtimingtakeresultavg->pointstotal = $matchtimingtakeresultavg->pointstotal / $numtakes; + foreach($matchtimingtakeresultavg->summary as $k => $v){ + + $matchtimingtakeresultavg->summary[$k]['num_correct'] = $v['num_correct'] / $numtakes; + $matchtimingtakeresultavg->summary[$k]['sum_points'] = $v['num_correct'] * $v['timing_type']->score / $numtakes; + } + + $matchtimingtakeresultavg->calculate_score(); + + return $matchtimingtakeresultavg; + + } + + private function get_first_timing_take_summary_by_takes($takes){ + $firsttake = $takes[0]; + $matchtimingresult = $this->get_evaluated_timing_type_result_by_take($firsttake); + return $matchtimingresult; + } + + private function get_last_timing_take_summary_by_takes($takes){ + $lasttake = end($takes); + $matchtimingresult = $this->get_evaluated_timing_type_result_by_take($lasttake); + return $matchtimingresult; + } + + private function get_evaluated_timing_type_result_by_take($take){ + $matchcontroller = new MoodleMatchController(); + + $matchtake = $matchcontroller->match_take_get_db($take->id); + $takeanswers = $matchcontroller->match_question_answers_get_by_take($take->id); + $questions = $matchcontroller->match_questions_get_by_video_db($matchtake->videoid, 'timecode', true); + $timingtypes = $matchcontroller->match_timing_type_get_db($matchtake->videoid); + $matchtimingresult = MatchTimingTakeResult::evaluate_take($timingtypes, $questions,$takeanswers); + + return $matchtimingresult; + } + + public function get_rendered_timing_take_summary($takes, $ivs) { + global $DB; + $course = $DB->get_record('course', array('id' => $ivs->course), '*', MUST_EXIST); + $settingscontroller = new SettingsService(); + $activitysettings = $settingscontroller->get_settings_for_activity($ivs->id, $course->id); + $timingtakesummaryenabled = $activitysettings['show_timing_take_summary']->value; + + if ($timingtakesummaryenabled) { + + $timingtakesummary = '


' . get_string('ivs_grademethod_timing_take_summary_thanks', 'ivs') . '
@@ -102,20 +107,45 @@ ?>
-
+
+ + + + + + + + + render($renderable); + } + ?> + +
- render($renderable); - } - ?> -
+ +
+
+ + + + + + + + download_dataformat_selector(get_string("ivs_match_download_summary_details_label", 'ivs'), - 'question_overview_details_download.php', 'download', array('player_id' => $ivs->id, 'cmid' => $cmid)); + foreach ($timingtypes as $timingtype) { + $renderable = new \mod_ivs\output\match\question_type_overview($timingtype, $cm); + echo $renderer->render($renderable); + } ?> - + +
+ +
diff --git a/templates/question_answers_view.mustache b/templates/question_answers_view.mustache index 5356329..b561a2c 100644 --- a/templates/question_answers_view.mustache +++ b/templates/question_answers_view.mustache @@ -110,6 +110,36 @@
{{/single_choice_question}} + + {{#timing_question}} +
+ + + + + + + + + + + + + + + + + + + + {{#answers}} + {{{.}}} + {{/answers}} + +
{{id_label}}{{id}}{{type_label}}{{question_type}}{{title_label}}{{{label}}}
{{question_label}}{{{question}}}
{{name}}{{user_id}}{{first_click_answer}}{{click_retries}}{{last_click_answer}}
+
+ {{/timing_question}} +