|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00046 defined('MOODLE_INTERNAL') || die(); 00047 00048 00056 class question_engine_data_mapper { 00061 protected $db; 00062 00066 public function __construct($db = null) { 00067 if (is_null($db)) { 00068 global $DB; 00069 $this->db = $DB; 00070 } else { 00071 $this->db = $db; 00072 } 00073 } 00074 00080 public function insert_questions_usage_by_activity(question_usage_by_activity $quba) { 00081 $record = new stdClass(); 00082 $record->contextid = $quba->get_owning_context()->id; 00083 $record->component = $quba->get_owning_component(); 00084 $record->preferredbehaviour = $quba->get_preferred_behaviour(); 00085 00086 $newid = $this->db->insert_record('question_usages', $record); 00087 $quba->set_id_from_database($newid); 00088 00089 foreach ($quba->get_attempt_iterator() as $qa) { 00090 $this->insert_question_attempt($qa, $quba->get_owning_context()); 00091 } 00092 } 00093 00100 public function insert_question_attempt(question_attempt $qa, $context) { 00101 $record = new stdClass(); 00102 $record->questionusageid = $qa->get_usage_id(); 00103 $record->slot = $qa->get_slot(); 00104 $record->behaviour = $qa->get_behaviour_name(); 00105 $record->questionid = $qa->get_question()->id; 00106 $record->variant = $qa->get_variant(); 00107 $record->maxmark = $qa->get_max_mark(); 00108 $record->minfraction = $qa->get_min_fraction(); 00109 $record->flagged = $qa->is_flagged(); 00110 $record->questionsummary = $qa->get_question_summary(); 00111 if (textlib::strlen($record->questionsummary) > question_bank::MAX_SUMMARY_LENGTH) { 00112 // It seems some people write very long quesions! MDL-30760 00113 $record->questionsummary = textlib::substr($record->questionsummary, 00114 0, question_bank::MAX_SUMMARY_LENGTH - 3) . '...'; 00115 } 00116 $record->rightanswer = $qa->get_right_answer_summary(); 00117 $record->responsesummary = $qa->get_response_summary(); 00118 $record->timemodified = time(); 00119 $record->id = $this->db->insert_record('question_attempts', $record); 00120 00121 foreach ($qa->get_step_iterator() as $seq => $step) { 00122 $this->insert_question_attempt_step($step, $record->id, $seq, $context); 00123 } 00124 } 00125 00133 protected function make_step_record(question_attempt_step $step, $questionattemptid, $seq) { 00134 $record = new stdClass(); 00135 $record->questionattemptid = $questionattemptid; 00136 $record->sequencenumber = $seq; 00137 $record->state = (string) $step->get_state(); 00138 $record->fraction = $step->get_fraction(); 00139 $record->timecreated = $step->get_timecreated(); 00140 $record->userid = $step->get_user_id(); 00141 return $record; 00142 } 00143 00150 protected function insert_step_data(question_attempt_step $step, $stepid, $context) { 00151 foreach ($step->get_all_data() as $name => $value) { 00152 if ($value instanceof question_file_saver) { 00153 $value->save_files($stepid, $context); 00154 } 00155 00156 $data = new stdClass(); 00157 $data->attemptstepid = $stepid; 00158 $data->name = $name; 00159 $data->value = $value; 00160 $this->db->insert_record('question_attempt_step_data', $data, false); 00161 } 00162 } 00163 00171 public function insert_question_attempt_step(question_attempt_step $step, 00172 $questionattemptid, $seq, $context) { 00173 00174 $record = $this->make_step_record($step, $questionattemptid, $seq); 00175 $record->id = $this->db->insert_record('question_attempt_steps', $record); 00176 00177 $this->insert_step_data($step, $record->id, $context); 00178 } 00179 00187 public function update_question_attempt_step(question_attempt_step $step, 00188 $questionattemptid, $seq, $context) { 00189 00190 $record = $this->make_step_record($step, $questionattemptid, $seq); 00191 $record->id = $step->get_id(); 00192 $this->db->update_record('question_attempt_steps', $record); 00193 00194 $this->db->delete_records('question_attempt_step_data', 00195 array('attemptstepid' => $record->id)); 00196 $this->insert_step_data($step, $record->id, $context); 00197 } 00198 00204 public function load_question_attempt_step($stepid) { 00205 $records = $this->db->get_recordset_sql(" 00206 SELECT 00207 qas.id AS attemptstepid, 00208 qas.questionattemptid, 00209 qas.sequencenumber, 00210 qas.state, 00211 qas.fraction, 00212 qas.timecreated, 00213 qas.userid, 00214 qasd.name, 00215 qasd.value 00216 00217 FROM {question_attempt_steps} qas 00218 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id 00219 00220 WHERE 00221 qas.id = :stepid 00222 ", array('stepid' => $stepid)); 00223 00224 if (!$records->valid()) { 00225 throw new coding_exception('Failed to load question_attempt_step ' . $stepid); 00226 } 00227 00228 $step = question_attempt_step::load_from_records($records, $stepid); 00229 $records->close(); 00230 00231 return $step; 00232 } 00233 00240 public function load_question_attempt($questionattemptid) { 00241 $records = $this->db->get_recordset_sql(" 00242 SELECT 00243 quba.contextid, 00244 quba.preferredbehaviour, 00245 qa.id AS questionattemptid, 00246 qa.questionusageid, 00247 qa.slot, 00248 qa.behaviour, 00249 qa.questionid, 00250 qa.variant, 00251 qa.maxmark, 00252 qa.minfraction, 00253 qa.flagged, 00254 qa.questionsummary, 00255 qa.rightanswer, 00256 qa.responsesummary, 00257 qa.timemodified, 00258 qas.id AS attemptstepid, 00259 qas.sequencenumber, 00260 qas.state, 00261 qas.fraction, 00262 qas.timecreated, 00263 qas.userid, 00264 qasd.name, 00265 qasd.value 00266 00267 FROM {question_attempts qa 00268 JOIN {question_usages} quba ON quba.id = qa.questionusageid 00269 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id 00270 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id 00271 00272 WHERE 00273 qa.id = :questionattemptid 00274 00275 ORDER BY 00276 qas.sequencenumber 00277 ", array('questionattemptid' => $questionattemptid)); 00278 00279 if (!$records->valid()) { 00280 throw new coding_exception('Failed to load question_attempt ' . $questionattemptid); 00281 } 00282 00283 $record = current($records); 00284 $qa = question_attempt::load_from_records($records, $questionattemptid, 00285 new question_usage_null_observer(), $record->preferredbehaviour); 00286 $records->close(); 00287 00288 return $qa; 00289 } 00290 00297 public function load_questions_usage_by_activity($qubaid) { 00298 $records = $this->db->get_recordset_sql(" 00299 SELECT 00300 quba.id AS qubaid, 00301 quba.contextid, 00302 quba.component, 00303 quba.preferredbehaviour, 00304 qa.id AS questionattemptid, 00305 qa.questionusageid, 00306 qa.slot, 00307 qa.behaviour, 00308 qa.questionid, 00309 qa.variant, 00310 qa.maxmark, 00311 qa.minfraction, 00312 qa.flagged, 00313 qa.questionsummary, 00314 qa.rightanswer, 00315 qa.responsesummary, 00316 qa.timemodified, 00317 qas.id AS attemptstepid, 00318 qas.sequencenumber, 00319 qas.state, 00320 qas.fraction, 00321 qas.timecreated, 00322 qas.userid, 00323 qasd.name, 00324 qasd.value 00325 00326 FROM {question_usages} quba 00327 LEFT JOIN {question_attempts} qa ON qa.questionusageid = quba.id 00328 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id 00329 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id 00330 00331 WHERE 00332 quba.id = :qubaid 00333 00334 ORDER BY 00335 qa.slot, 00336 qas.sequencenumber 00337 ", array('qubaid' => $qubaid)); 00338 00339 if (!$records->valid()) { 00340 throw new coding_exception('Failed to load questions_usage_by_activity ' . $qubaid); 00341 } 00342 00343 $quba = question_usage_by_activity::load_from_records($records, $qubaid); 00344 $records->close(); 00345 00346 return $quba; 00347 } 00348 00357 public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) { 00358 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot'); 00359 00360 $records = $this->db->get_records_sql(" 00361 SELECT 00362 qas.id, 00363 qa.id AS questionattemptid, 00364 qa.questionusageid, 00365 qa.slot, 00366 qa.behaviour, 00367 qa.questionid, 00368 qa.variant, 00369 qa.maxmark, 00370 qa.minfraction, 00371 qa.flagged, 00372 qa.questionsummary, 00373 qa.rightanswer, 00374 qa.responsesummary, 00375 qa.timemodified, 00376 qas.id AS attemptstepid, 00377 qas.sequencenumber, 00378 qas.state, 00379 qas.fraction, 00380 qas.timecreated, 00381 qas.userid 00382 00383 FROM {$qubaids->from_question_attempts('qa')} 00384 JOIN {question_attempt_steps} qas ON 00385 qas.id = {$this->latest_step_for_qa_subquery()} 00386 00387 WHERE 00388 {$qubaids->where()} AND 00389 qa.slot $slottest 00390 ", $params + $qubaids->from_where_params()); 00391 00392 return $records; 00393 } 00394 00407 public function load_questions_usages_question_state_summary( 00408 qubaid_condition $qubaids, $slots) { 00409 list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot'); 00410 00411 $rs = $this->db->get_recordset_sql(" 00412 SELECT 00413 qa.slot, 00414 qa.questionid, 00415 q.name, 00416 CASE qas.state 00417 {$this->full_states_to_summary_state_sql()} 00418 END AS summarystate, 00419 COUNT(1) AS numattempts 00420 00421 FROM {$qubaids->from_question_attempts('qa')} 00422 JOIN {question_attempt_steps} qas ON 00423 qas.id = {$this->latest_step_for_qa_subquery()} 00424 JOIN {question} q ON q.id = qa.questionid 00425 00426 WHERE 00427 {$qubaids->where()} AND 00428 qa.slot $slottest 00429 00430 GROUP BY 00431 qa.slot, 00432 qa.questionid, 00433 q.name, 00434 q.id, 00435 CASE qas.state 00436 {$this->full_states_to_summary_state_sql()} 00437 END 00438 00439 ORDER BY 00440 qa.slot, 00441 qa.questionid, 00442 q.name, 00443 q.id 00444 ", $params + $qubaids->from_where_params()); 00445 00446 $results = array(); 00447 foreach ($rs as $row) { 00448 $index = $row->slot . ',' . $row->questionid; 00449 00450 if (!array_key_exists($index, $results)) { 00451 $res = new stdClass(); 00452 $res->slot = $row->slot; 00453 $res->questionid = $row->questionid; 00454 $res->name = $row->name; 00455 $res->inprogress = 0; 00456 $res->needsgrading = 0; 00457 $res->autograded = 0; 00458 $res->manuallygraded = 0; 00459 $res->all = 0; 00460 $results[$index] = $res; 00461 } 00462 00463 $results[$index]->{$row->summarystate} = $row->numattempts; 00464 $results[$index]->all += $row->numattempts; 00465 } 00466 $rs->close(); 00467 00468 return $results; 00469 } 00470 00492 public function load_questions_usages_where_question_in_state( 00493 qubaid_condition $qubaids, $summarystate, $slot, $questionid = null, 00494 $orderby = 'random', $params, $limitfrom = 0, $limitnum = null) { 00495 00496 $extrawhere = ''; 00497 if ($questionid) { 00498 $extrawhere .= ' AND qa.questionid = :questionid'; 00499 $params['questionid'] = $questionid; 00500 } 00501 if ($summarystate != 'all') { 00502 list($test, $sparams) = $this->in_summary_state_test($summarystate); 00503 $extrawhere .= ' AND qas.state ' . $test; 00504 $params += $sparams; 00505 } 00506 00507 if ($orderby == 'random') { 00508 $sqlorderby = ''; 00509 } else if ($orderby) { 00510 $sqlorderby = 'ORDER BY ' . $orderby; 00511 } else { 00512 $sqlorderby = ''; 00513 } 00514 00515 // We always want the total count, as well as the partcular list of ids, 00516 // based on the paging and sort order. Becuase the list of ids is never 00517 // going to be too rediculously long. My worst-case scenario is 00518 // 10,000 students in the coures, each doing 5 quiz attempts. That 00519 // is a 50,000 element int => int array, which PHP seems to use 5MB 00520 // memeory to store on a 64 bit server. 00521 $params += $qubaids->from_where_params(); 00522 $params['slot'] = $slot; 00523 $qubaids = $this->db->get_records_sql_menu(" 00524 SELECT 00525 qa.questionusageid, 00526 1 00527 00528 FROM {$qubaids->from_question_attempts('qa')} 00529 JOIN {question_attempt_steps} qas ON 00530 qas.id = {$this->latest_step_for_qa_subquery()} 00531 JOIN {question} q ON q.id = qa.questionid 00532 00533 WHERE 00534 {$qubaids->where()} AND 00535 qa.slot = :slot 00536 $extrawhere 00537 00538 $sqlorderby 00539 ", $params); 00540 00541 $qubaids = array_keys($qubaids); 00542 $count = count($qubaids); 00543 00544 if ($orderby == 'random') { 00545 shuffle($qubaids); 00546 $limitfrom = 0; 00547 } 00548 00549 if (!is_null($limitnum)) { 00550 $qubaids = array_slice($qubaids, $limitfrom, $limitnum); 00551 } 00552 00553 return array($qubaids, $count); 00554 } 00555 00564 public function load_average_marks(qubaid_condition $qubaids, $slots = null) { 00565 if (!empty($slots)) { 00566 list($slottest, $slotsparams) = $this->db->get_in_or_equal( 00567 $slots, SQL_PARAMS_NAMED, 'slot'); 00568 $slotwhere = " AND qa.slot $slottest"; 00569 } else { 00570 $slotwhere = ''; 00571 $params = array(); 00572 } 00573 00574 list($statetest, $stateparams) = $this->db->get_in_or_equal(array( 00575 (string) question_state::$gaveup, 00576 (string) question_state::$gradedwrong, 00577 (string) question_state::$gradedpartial, 00578 (string) question_state::$gradedright, 00579 (string) question_state::$mangaveup, 00580 (string) question_state::$mangrwrong, 00581 (string) question_state::$mangrpartial, 00582 (string) question_state::$mangrright), SQL_PARAMS_NAMED, 'st'); 00583 00584 return $this->db->get_records_sql(" 00585 SELECT 00586 qa.slot, 00587 AVG(COALESCE(qas.fraction, 0)) AS averagefraction, 00588 COUNT(1) AS numaveraged 00589 00590 FROM {$qubaids->from_question_attempts('qa')} 00591 JOIN {question_attempt_steps} qas ON 00592 qas.id = {$this->latest_step_for_qa_subquery()} 00593 00594 WHERE 00595 {$qubaids->where()} 00596 $slotwhere 00597 AND qas.state $statetest 00598 00599 GROUP BY qa.slot 00600 00601 ORDER BY qa.slot 00602 ", $slotsparams + $stateparams + $qubaids->from_where_params()); 00603 } 00604 00613 public function load_attempts_at_question($questionid, qubaid_condition $qubaids) { 00614 global $DB; 00615 00616 $params = $qubaids->from_where_params(); 00617 $params['questionid'] = $questionid; 00618 00619 $records = $DB->get_recordset_sql(" 00620 SELECT 00621 quba.contextid, 00622 quba.preferredbehaviour, 00623 qa.id AS questionattemptid, 00624 qa.questionusageid, 00625 qa.slot, 00626 qa.behaviour, 00627 qa.questionid, 00628 qa.variant, 00629 qa.maxmark, 00630 qa.minfraction, 00631 qa.flagged, 00632 qa.questionsummary, 00633 qa.rightanswer, 00634 qa.responsesummary, 00635 qa.timemodified, 00636 qas.id AS attemptstepid, 00637 qas.sequencenumber, 00638 qas.state, 00639 qas.fraction, 00640 qas.timecreated, 00641 qas.userid, 00642 qasd.name, 00643 qasd.value 00644 00645 FROM {$qubaids->from_question_attempts('qa')} 00646 JOIN {question_usages} quba ON quba.id = qa.questionusageid 00647 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id 00648 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id 00649 00650 WHERE 00651 {$qubaids->where()} AND 00652 qa.questionid = :questionid 00653 00654 ORDER BY 00655 quba.id, 00656 qa.id, 00657 qas.sequencenumber 00658 ", $params); 00659 00660 $questionattempts = array(); 00661 while ($records->valid()) { 00662 $record = $records->current(); 00663 $questionattempts[$record->questionattemptid] = 00664 question_attempt::load_from_records($records, 00665 $record->questionattemptid, new question_usage_null_observer(), 00666 $record->preferredbehaviour); 00667 } 00668 $records->close(); 00669 00670 return $questionattempts; 00671 } 00672 00678 public function update_questions_usage_by_activity(question_usage_by_activity $quba) { 00679 $record = new stdClass(); 00680 $record->id = $quba->get_id(); 00681 $record->contextid = $quba->get_owning_context()->id; 00682 $record->component = $quba->get_owning_component(); 00683 $record->preferredbehaviour = $quba->get_preferred_behaviour(); 00684 00685 $this->db->update_record('question_usages', $record); 00686 } 00687 00693 public function update_question_attempt(question_attempt $qa) { 00694 $record = new stdClass(); 00695 $record->id = $qa->get_database_id(); 00696 $record->maxmark = $qa->get_max_mark(); 00697 $record->minfraction = $qa->get_min_fraction(); 00698 $record->flagged = $qa->is_flagged(); 00699 $record->questionsummary = $qa->get_question_summary(); 00700 $record->rightanswer = $qa->get_right_answer_summary(); 00701 $record->responsesummary = $qa->get_response_summary(); 00702 $record->timemodified = time(); 00703 00704 $this->db->update_record('question_attempts', $record); 00705 } 00706 00713 public function delete_questions_usage_by_activities(qubaid_condition $qubaids) { 00714 $where = "qa.questionusageid {$qubaids->usage_id_in()}"; 00715 $params = $qubaids->usage_id_in_params(); 00716 00717 $contextids = $this->db->get_records_sql_menu(" 00718 SELECT DISTINCT contextid, 1 00719 FROM {question_usages} 00720 WHERE id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params()); 00721 foreach ($contextids as $contextid => $notused) { 00722 $this->delete_response_files($contextid, "IN ( 00723 SELECT qas.id 00724 FROM {question_attempts} qa 00725 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id 00726 WHERE $where)", $params); 00727 } 00728 00729 if ($this->db->get_dbfamily() == 'mysql') { 00730 $this->delete_usage_records_for_mysql($qubaids); 00731 return; 00732 } 00733 00734 $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN ( 00735 SELECT qas.id 00736 FROM {question_attempts} qa 00737 JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id 00738 WHERE $where)", $params); 00739 00740 $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN ( 00741 SELECT qa.id 00742 FROM {question_attempts} qa 00743 WHERE $where)", $params); 00744 00745 $this->db->delete_records_select('question_attempts', 00746 "{question_attempts}.questionusageid {$qubaids->usage_id_in()}", 00747 $qubaids->usage_id_in_params()); 00748 00749 $this->db->delete_records_select('question_usages', 00750 "{question_usages}.id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params()); 00751 } 00752 00759 protected function delete_usage_records_for_mysql(qubaid_condition $qubaids) { 00760 // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API. 00761 $this->db->execute(' 00762 DELETE qu, qa, qas, qasd 00763 FROM {question_usages} qu 00764 JOIN {question_attempts} qa ON qa.questionusageid = qu.id 00765 LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id 00766 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id 00767 WHERE qu.id ' . $qubaids->usage_id_in(), 00768 $qubaids->usage_id_in_params()); 00769 } 00770 00778 protected function delete_attempt_steps_for_mysql($test, $params) { 00779 // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API. 00780 $this->db->execute(' 00781 DELETE qas, qasd 00782 FROM {question_attempt_steps} qas 00783 LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id 00784 WHERE qas.questionattemptid ' . $test, $params); 00785 } 00786 00792 public function delete_steps($stepids, $context) { 00793 if (empty($stepids)) { 00794 return; 00795 } 00796 list($test, $params) = $this->db->get_in_or_equal($stepids, SQL_PARAMS_NAMED); 00797 00798 if ($deletefiles) { 00799 $this->delete_response_files($context->id, $test, $params); 00800 } 00801 00802 if ($this->db->get_dbfamily() == 'mysql') { 00803 $this->delete_attempt_steps_for_mysql($test, $params); 00804 return; 00805 } 00806 00807 $this->db->delete_records_select('question_attempt_step_data', 00808 "attemptstepid $test", $params); 00809 $this->db->delete_records_select('question_attempt_steps', 00810 "attemptstepid $test", $params); 00811 } 00812 00821 protected function delete_response_files($contextid, $itemidstest, $params) { 00822 $fs = get_file_storage(); 00823 foreach (question_engine::get_all_response_file_areas() as $filearea) { 00824 $fs->delete_area_files_select($contextid, 'question', $filearea, 00825 $itemidstest, $params); 00826 } 00827 } 00828 00833 public function delete_previews($questionid) { 00834 $previews = $this->db->get_records_sql_menu(" 00835 SELECT DISTINCT quba.id, 1 00836 FROM {question_usages} quba 00837 JOIN {question_attempts} qa ON qa.questionusageid = quba.id 00838 WHERE quba.component = 'core_question_preview' AND 00839 qa.questionid = ?", array($questionid)); 00840 if (empty($previews)) { 00841 return; 00842 } 00843 $this->delete_questions_usage_by_activities(new qubaid_list($previews)); 00844 } 00845 00853 public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) { 00854 if (!$this->db->record_exists('question_attempts', array('id' => $qaid, 00855 'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) { 00856 throw new moodle_exception('errorsavingflags', 'question'); 00857 } 00858 00859 $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid)); 00860 } 00861 00868 protected function full_states_to_summary_state_sql() { 00869 $sql = ''; 00870 foreach (question_state::get_all() as $state) { 00871 $sql .= "WHEN '$state' THEN '{$state->get_summary_state()}'\n"; 00872 } 00873 return $sql; 00874 } 00875 00884 public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') { 00885 $states = question_state::get_all_for_summary_state($summarystate); 00886 return $this->db->get_in_or_equal(array_map('strval', $states), 00887 SQL_PARAMS_NAMED, $prefix, $equal); 00888 } 00889 00897 public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) { 00898 $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark, 00899 "questionusageid {$qubaids->usage_id_in()} AND slot = :slot", 00900 $qubaids->usage_id_in_params() + array('slot' => $slot)); 00901 } 00902 00916 public function sum_usage_marks_subquery($qubaid) { 00917 // To explain the COALESCE in the following SQL: SUM(lots of NULLs) gives 00918 // NULL, while SUM(one 0.0 and lots of NULLS) gives 0.0. We don't want that. 00919 // We always want to return a number, so the COALESCE is there to turn the 00920 // NULL total into a 0. 00921 return "SELECT COALESCE(SUM(qa.maxmark * qas.fraction), 0) 00922 FROM {question_attempts} qa 00923 JOIN {question_attempt_steps} qas ON qas.id = ( 00924 SELECT MAX(summarks_qas.id) 00925 FROM {question_attempt_steps} summarks_qas 00926 WHERE summarks_qas.questionattemptid = qa.id 00927 ) 00928 WHERE qa.questionusageid = $qubaid 00929 HAVING COUNT(CASE 00930 WHEN qas.state = 'needsgrading' AND qa.maxmark > 0 THEN 1 00931 ELSE NULL 00932 END) = 0"; 00933 } 00934 00944 public function question_attempt_latest_state_view($alias, qubaid_condition $qubaids) { 00945 return array("( 00946 SELECT {$alias}qa.id AS questionattemptid, 00947 {$alias}qa.questionusageid, 00948 {$alias}qa.slot, 00949 {$alias}qa.behaviour, 00950 {$alias}qa.questionid, 00951 {$alias}qa.variant, 00952 {$alias}qa.maxmark, 00953 {$alias}qa.minfraction, 00954 {$alias}qa.flagged, 00955 {$alias}qa.questionsummary, 00956 {$alias}qa.rightanswer, 00957 {$alias}qa.responsesummary, 00958 {$alias}qa.timemodified, 00959 {$alias}qas.id AS attemptstepid, 00960 {$alias}qas.sequencenumber, 00961 {$alias}qas.state, 00962 {$alias}qas.fraction, 00963 {$alias}qas.timecreated, 00964 {$alias}qas.userid 00965 00966 FROM {$qubaids->from_question_attempts($alias . 'qa')} 00967 JOIN {question_attempt_steps} {$alias}qas ON 00968 {$alias}qas.id = {$this->latest_step_for_qa_subquery($alias . 'qa.id')} 00969 WHERE {$qubaids->where()} 00970 ) $alias", $qubaids->from_where_params()); 00971 } 00972 00973 protected function latest_step_for_qa_subquery($questionattemptid = 'qa.id') { 00974 return "( 00975 SELECT MAX(id) 00976 FROM {question_attempt_steps} 00977 WHERE questionattemptid = $questionattemptid 00978 )"; 00979 } 00980 00987 public function questions_in_use(array $questionids, qubaid_condition $qubaids) { 00988 list($test, $params) = $this->db->get_in_or_equal($questionids); 00989 return $this->db->record_exists_select('question_attempts', 00990 'questionid ' . $test . ' AND questionusageid ' . 00991 $qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params()); 00992 } 00993 } 00994 00995 01006 class question_engine_unit_of_work implements question_usage_observer { 01008 protected $quba; 01009 01011 protected $modified = false; 01012 01017 protected $attemptsmodified = array(); 01018 01023 protected $attemptsadded = array(); 01024 01029 protected $stepsadded = array(); 01030 01035 protected $stepsmodified = array(); 01036 01041 protected $stepsdeleted = array(); 01042 01047 public function __construct(question_usage_by_activity $quba) { 01048 $this->quba = $quba; 01049 } 01050 01051 public function notify_modified() { 01052 $this->modified = true; 01053 } 01054 01055 public function notify_attempt_modified(question_attempt $qa) { 01056 $slot = $qa->get_slot(); 01057 if (!array_key_exists($slot, $this->attemptsadded)) { 01058 $this->attemptsmodified[$slot] = $qa; 01059 } 01060 } 01061 01062 public function notify_attempt_added(question_attempt $qa) { 01063 $this->attemptsadded[$qa->get_slot()] = $qa; 01064 } 01065 01066 public function notify_step_added(question_attempt_step $step, question_attempt $qa, $seq) { 01067 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) { 01068 return; 01069 } 01070 01071 if (($key = $this->is_step_added($step)) !== false) { 01072 return; 01073 } 01074 01075 if (($key = $this->is_step_modified($step)) !== false) { 01076 throw new coding_exception('Cannot add a step that has already been modified.'); 01077 } 01078 01079 if (($key = $this->is_step_deleted($step)) !== false) { 01080 unset($this->stepsdeleted[$step->get_id()]); 01081 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq); 01082 return; 01083 } 01084 01085 $stepid = $step->get_id(); 01086 if ($stepid) { 01087 if (array_key_exists($stepid, $this->stepsdeleted)) { 01088 unset($this->stepsdeleted[$stepid]); 01089 } 01090 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq); 01091 01092 } else { 01093 $this->stepsadded[] = array($step, $qa->get_database_id(), $seq); 01094 } 01095 } 01096 01097 public function notify_step_modified(question_attempt_step $step, question_attempt $qa, $seq) { 01098 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) { 01099 return; 01100 } 01101 01102 if (($key = $this->is_step_added($step)) !== false) { 01103 return; 01104 } 01105 01106 if (($key = $this->is_step_deleted($step)) !== false) { 01107 throw new coding_exception('Cannot modify a step after it has been deleted.'); 01108 } 01109 01110 $stepid = $step->get_id(); 01111 if (empty($stepid)) { 01112 throw new coding_exception('Cannot modify a step that has never been stored in the database.'); 01113 } 01114 01115 $this->stepsmodified[] = array($step, $qa->get_database_id(), $seq); 01116 } 01117 01118 public function notify_step_deleted(question_attempt_step $step, question_attempt $qa) { 01119 if (array_key_exists($qa->get_slot(), $this->attemptsadded)) { 01120 return; 01121 } 01122 01123 if (($key = $this->is_step_added($step)) !== false) { 01124 unset($this->stepsadded[$key]); 01125 return; 01126 } 01127 01128 if (($key = $this->is_step_modified($step)) !== false) { 01129 unset($this->stepsmodified[$key]); 01130 } 01131 01132 $stepid = $step->get_id(); 01133 if (empty($stepid)) { 01134 return; // Was never in the database. 01135 } 01136 01137 $this->stepsdeleted[$stepid] = $step; 01138 } 01139 01145 protected function is_step_added(question_attempt_step $step) { 01146 foreach ($this->stepsadded as $key => $data) { 01147 list($addedstep, $qaid, $seq) = $data; 01148 if ($addedstep === $step) { 01149 return $key; 01150 } 01151 } 01152 return false; 01153 } 01154 01160 protected function is_step_modified(question_attempt_step $step) { 01161 foreach ($this->stepsmodified as $key => $data) { 01162 list($modifiedstep, $qaid, $seq) = $data; 01163 if ($modifiedstep === $step) { 01164 return $key; 01165 } 01166 } 01167 return false; 01168 } 01169 01174 protected function is_step_deleted(question_attempt_step $step) { 01175 foreach ($this->stepsdeleted as $deletedstep) { 01176 if ($deletedstep === $step) { 01177 return true; 01178 } 01179 } 01180 return false; 01181 } 01182 01187 public function save(question_engine_data_mapper $dm) { 01188 $dm->delete_steps(array_keys($this->stepsdeleted), $this->quba->get_owning_context()); 01189 01190 foreach ($this->stepsmodified as $stepinfo) { 01191 list($step, $questionattemptid, $seq) = $stepinfo; 01192 $dm->update_question_attempt_step($step, $questionattemptid, $seq, 01193 $this->quba->get_owning_context()); 01194 } 01195 01196 foreach ($this->stepsadded as $stepinfo) { 01197 list($step, $questionattemptid, $seq) = $stepinfo; 01198 $dm->insert_question_attempt_step($step, $questionattemptid, $seq, 01199 $this->quba->get_owning_context()); 01200 } 01201 01202 foreach ($this->attemptsadded as $qa) { 01203 $dm->insert_question_attempt($qa, $this->quba->get_owning_context()); 01204 } 01205 01206 foreach ($this->attemptsmodified as $qa) { 01207 $dm->update_question_attempt($qa); 01208 } 01209 01210 if ($this->modified) { 01211 $dm->update_questions_usage_by_activity($this->quba); 01212 } 01213 } 01214 } 01215 01216 01228 class question_file_saver { 01230 protected $draftitemid; 01232 protected $component; 01234 protected $filearea; 01235 01240 protected $value = null; 01241 01248 public function __construct($draftitemid, $component, $filearea, $text = null) { 01249 $this->draftitemid = $draftitemid; 01250 $this->component = $component; 01251 $this->filearea = $filearea; 01252 $this->value = $this->compute_value($draftitemid, $text); 01253 } 01254 01261 protected function compute_value($draftitemid, $text) { 01262 global $USER; 01263 01264 $fs = get_file_storage(); 01265 $usercontext = get_context_instance(CONTEXT_USER, $USER->id); 01266 01267 $files = $fs->get_area_files($usercontext->id, 'user', 'draft', 01268 $draftitemid, 'sortorder, filepath, filename', false); 01269 01270 $string = ''; 01271 foreach ($files as $file) { 01272 $string .= $file->get_filepath() . $file->get_filename() . '|' . 01273 $file->get_contenthash() . '|'; 01274 } 01275 01276 if ($string) { 01277 $hash = md5($string); 01278 } else { 01279 $hash = ''; 01280 } 01281 01282 if (is_null($text)) { 01283 return $hash; 01284 } 01285 01286 // We add the file hash so a simple string comparison will say if the 01287 // files have been changed. First strip off any existing file hash. 01288 $text = preg_replace('/\s*<!-- File hash: \w+ -->\s*$/', '', $text); 01289 $text = file_rewrite_urls_to_pluginfile($text, $draftitemid); 01290 if ($hash) { 01291 $text .= '<!-- File hash: ' . $hash . ' -->'; 01292 } 01293 return $text; 01294 } 01295 01296 public function __toString() { 01297 return $this->value; 01298 } 01299 01304 public function save_files($itemid, $context) { 01305 file_save_draft_area_files($this->draftitemid, $context->id, 01306 $this->component, $this->filearea, $itemid); 01307 } 01308 } 01309 01310 01325 abstract class qubaid_condition { 01326 01332 public abstract function from_question_attempts($alias); 01333 01335 public abstract function where(); 01336 01341 public abstract function from_where_params(); 01342 01347 public abstract function usage_id_in(); 01348 01352 public abstract function usage_id_in_params(); 01353 } 01354 01355 01363 class qubaid_list extends qubaid_condition { 01365 protected $qubaids; 01366 protected $columntotest = null; 01367 protected $params; 01368 01373 public function __construct(array $qubaids) { 01374 $this->qubaids = $qubaids; 01375 } 01376 01377 public function from_question_attempts($alias) { 01378 $this->columntotest = $alias . '.questionusageid'; 01379 return '{question_attempts} ' . $alias; 01380 } 01381 01382 public function where() { 01383 global $DB; 01384 01385 if (is_null($this->columntotest)) { 01386 throw new coding_exception('Must call from_question_attempts before where().'); 01387 } 01388 if (empty($this->qubaids)) { 01389 $this->params = array(); 01390 return '1 = 0'; 01391 } 01392 01393 return $this->columntotest . ' ' . $this->usage_id_in(); 01394 } 01395 01396 public function from_where_params() { 01397 return $this->params; 01398 } 01399 01400 public function usage_id_in() { 01401 global $DB; 01402 01403 if (empty($this->qubaids)) { 01404 $this->params = array(); 01405 return '= 0'; 01406 } 01407 list($where, $this->params) = $DB->get_in_or_equal( 01408 $this->qubaids, SQL_PARAMS_NAMED, 'qubaid'); 01409 return $where; 01410 } 01411 01412 public function usage_id_in_params() { 01413 return $this->params; 01414 } 01415 } 01416 01417 01434 class qubaid_join extends qubaid_condition { 01435 public $from; 01436 public $usageidcolumn; 01437 public $where; 01438 public $params; 01439 01448 public function __construct($from, $usageidcolumn, $where = '', $params = array()) { 01449 $this->from = $from; 01450 $this->usageidcolumn = $usageidcolumn; 01451 $this->params = $params; 01452 if (empty($where)) { 01453 $where = '1 = 1'; 01454 } 01455 $this->where = $where; 01456 } 01457 01458 public function from_question_attempts($alias) { 01459 return "$this->from 01460 JOIN {question_attempts} {$alias} ON " . 01461 "{$alias}.questionusageid = $this->usageidcolumn"; 01462 } 01463 01464 public function where() { 01465 return $this->where; 01466 } 01467 01468 public function from_where_params() { 01469 return $this->params; 01470 } 01471 01472 public function usage_id_in() { 01473 return "IN (SELECT $this->usageidcolumn FROM $this->from WHERE $this->where)"; 01474 } 01475 01476 public function usage_id_in_params() { 01477 return $this->params; 01478 } 01479 }