diff --git a/.gitignore b/.gitignore index 0d211d3..69d7715 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vendor composer.lock +\.sass-cache diff --git a/configure.php b/configure.php index be9ade2..1c6f6e0 100644 --- a/configure.php +++ b/configure.php @@ -1,7 +1,7 @@ dbprefix; -// If they pressed Submit on the quiz content -if ( isset($_POST['gift']) ) { - $gift = $_POST['gift']; - $_SESSION['gift'] = $gift; - - // Some sanity checking... - $retval = check_gift($gift); - if ( ! $retval ) { - header( 'Location: '.addSession('configure.php') ) ; - return; - } - - // This is not JSON - no one cares - $LINK->setJson($gift); - $_SESSION['success'] = 'Quiz updated'; - unset($_SESSION['gift']); +// check to see if there are results from this link already +$results_rows = $PDOX->allRowsDie("SELECT result_id, R.link_id AS link_id, R.user_id AS user_id, M.role as role, + sourcedid, service_id, grade, note, R.json AS json, R.note AS note + FROM lti_result AS R + JOIN lti_link AS L ON L.link_id = R.link_id AND R.link_id = :LI + JOIN lti_context AS C ON L.context_id = C.context_id AND C.context_id = :CI + JOIN lti_membership AS M ON R.user_id = M.user_id AND C.context_id = M.context_id + WHERE L.link_id = :LI AND M.role = 0 AND R.json IS NOT NULL", + array(':LI'=>$LINK->id, ':CI'=>$CONTEXT->id)); + +if (!empty($_POST)) { + + $gift = parse_configure_post(); + + // Sanity check + $retval = check_gift($gift); + if ( ! $retval ) { + header( 'Location: '.addSession('configure.php') ) ; + return; + } + + $LINK->setJson($gift); + $_SESSION['success'] = 'Quiz updated'; + if ($_POST['save_quiz'] == "Save and Return") { header( 'Location: '.addSession('index.php') ) ; - return; -} - -// Check to see if we are supposed to preload a quiz -$files = false; -$lock = false; -if ( isset ($CFG->giftquizzes) && is_dir($CFG->giftquizzes) ) { - $files1 = scandir($CFG->giftquizzes); - $files = array(); - foreach($files1 as $file) { - if ( $file == '.lock' ) { - $lock = trim(file_get_contents($CFG->giftquizzes.'/'.$file)); - continue; - } - if ( strpos($file, '.') === 0 ) continue; - $files[] = $file; - } - sort($files); -} -if ( count($files) < 1 ) { - $_SESSION['error'] = "Found no files in ".$CFG->giftquizzes; - header( 'Location: '.addSession('configure.php') ) ; - return; -} -// print_r($files); -// echo("LOCK = ".$lock); - -$default = isset($_SESSION['default_quiz']) ? $_SESSION['default_quiz'] : false; - -// Load up the selected file -if ( $files && isset($_POST['file']) ) { - $key = isset($_POST['lock']) ? $_POST['lock'] : false; - if ( $lock && $lock != $key ) { - $_SESSION['error'] = 'Incorrect password'; - header( 'Location: '.addSession('configure.php') ) ; - return; - } - - $name = $_POST['file']; - if ( ! in_array($name, $files) ) { - $_SESSION['error'] = 'Quiz file not found: '.$_POST['file']; - header( 'Location: '.addSession('configure.php') ) ; - return; - } - - $gift = file_get_contents($CFG->giftquizzes.'/'.$name); - $_SESSION['gift'] = $gift; - - // Also pre-check for sanity - $retval = check_gift($gift); - if ( ! $retval ) { - header( 'Location: '.addSession('configure.php') ) ; - return; - } - - $_SESSION['success'] = 'Preloaded quiz content from file. Make sure to save the quiz below.'; + } else { header( 'Location: '.addSession('configure.php') ) ; - return; -} - -// Load up the quiz from session or DB -if ( isset($_SESSION['gift']) ) { - $gift = $_SESSION['gift']; - unset($_SESSION['gift']); -} else { - $gift = $LINK->getJson(); -} - -// Clean up the JSON for presentation -if ( $gift === false || strlen($gift) < 1 ) { - if ( $default != false && $lock == false && in_array($default, $files) ) { - $gift = file_get_contents($CFG->giftquizzes.'/'.$default); - $_SESSION['success'] = 'Loaded quiz '.$default.' as default'; - } else { - $gift = getSampleGIFT(); - } + } + return; } // View $OUTPUT->header(); +?> + +bodyStart(); $OUTPUT->topNav(); +echo('
'); +echo('Cancel '); +echo('Input GIFT Quiz Format '); +echo('
'); $OUTPUT->flashMessages(); ?> -

Be careful in making any changes if this quiz has submissions.

-\n"); -// echo(''."\n"); -echo(''."\n"); -foreach($files as $file) { - if ( $default && $default == $file ) { - echo(''."\n"); - } else { - echo(''."\n"); - } -} -echo("\n"); -if ( $lock != false ) { - echo(' Password '); -} -echo(''); -echo("\n"); +
+ +
+

WARNING: Results have already been recorded for this quiz - are you sure you want to make changes?

+
+ +
+
+
+ +
+ -

-The assignment is configured by carefully editing the gift below. -The documentation for the GIFT format comes from -Moodle Documentation. -

- - -

- -

+
+ +
+ + + +

+ +
+
-footer(); +footerStart(); +$OUTPUT->templateInclude(array('common', 'tf_authoring', 'mc_authoring', 'sa_authoring')); +?> + + + +footerEnd(); diff --git a/configure_parse.php b/configure_parse.php new file mode 100644 index 0000000..fc17b11 --- /dev/null +++ b/configure_parse.php @@ -0,0 +1,98 @@ +array()); // initialize our array + foreach ($_POST as $key => $value) { // Loop through the entire POST + // We're only interested in keys which have "questionX" in them, and which have a value associated with them + if ((strpos($key, "question".$num) > 0) &&($value != Null)) { + // Trim off the "_questionX" part of the key and make that the key name + $key_name = implode("_", explode("_", $key, -1)); + // Is this an answer option? + if (strpos($key_name, "answer") !== false) { + // Get the number for this answer from the key name (format "answerX" or "answerX_iscorrect") + $key_parts = explode("_", $key_name); // make an array from the key name (['answerX', 'questionX']) + $answer_index = explode("answer", $key_parts[0])[1]; + // is this the text of the answer or an indicator that this is the correct answer? + if (sizeof($key_parts) > 1 && $key_parts[1] == 'iscorrect') { + // T/F questions are a little different. We can identify them because they have an 'iscorrect' key + $question_details['answer'][$answer_index]['iscorrect'] = true; + } else { + $question_details['answer'][$answer_index] = array('text'=>$value, 'iscorrect'=>false); + } + } else { + // It's not an answer option, so jus save the k-v pair + $question_details[$key_name] = $value; + } + } + } + + // if we actaully found something return it + if (sizeof($question_details) > 1) { + return $question_details; + } else { + // otherwise, return Null, which will indicate to parse_configure_post that we're done + return Null; + } +} + +// Create the GIFT format string from the question details returned by get_question +function create_gift_format($question) { + $answers = Null; // initialize + if ($question['type'] == "true_false_question") { + // if the answer is "true", make answers "T". Otherwise, make it "F" + $answers = (($question['answer'][1]['text'] == 'true') ? "T" : "F"); + } elseif (($question['type'] == "multiple_choice_question") || ($question['type'] == "multiple_answers_question")) { + $answers = array(); + // iterate through the answers to this question + foreach ($question['answer'] as $answer) { + if ($answer['iscorrect']) { + array_push($answers, "={$answer['text']}"); // GIFT indication for a correct answer + } else { + array_push($answers, "~{$answer['text']}"); // Incorrect answer + } + } + $answers= implode(" ", $answers); // smash all the answers together seperated by spaces + } elseif ($question['type'] == "short_answer_question") { + // only difference between MC/MA and SA answers is that SA answers are only provided if they are "correct" + // ignore the "is correct" bit of the array, and just add every one of the answers + $answers = array(); + foreach ($question['answer'] as $answer) { + array_push($answers, "={$answer['text']}"); + } + $answers= implode(" ", $answers); + } + // was the html box checked? if so, prepend the text with [html] for the parser + if (isset($question['html'])) { + $question['text'] = '[html]'.$question['text']; + } + // create the formatted string and return it + return "::{$question['title']}:: {$question['text']} {{$answers}}"; +} diff --git a/css/authoring.css b/css/authoring.css new file mode 100644 index 0000000..faa73de --- /dev/null +++ b/css/authoring.css @@ -0,0 +1,90 @@ +h1 { + font-size: 18px; } + +fieldset { + width: 100%; } + +.warning { + background-color: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 5px; + color: #721c24; } + +.error-list { + margin: auto; + padding: 1em; + width: 80%; } + .error-list > p:first-child { + font-weight: bold; + text-align: center; } + .error-list div { + text-align: center; } + +.question-container { + display: flex; + flex-flow: column; + margin: 2em auto; + width: 80%; } + .question-container > div:first-of-type { + align-items: center; + background-color: #e7e7e7; + border: 1px solid #808080; + border-radius: 5px; + display: flex; + flex-flow: row; + justify-content: space-between; + margin: 1em 0; + padding: 0 .25em; + padding-left: .75em; } + .question-container > label { + margin-top: .5em; } + +.question-title { + display: inline-block; + margin: 0; + margin-right: 2em; } + +.question-content-container { + margin: auto; + margin-top: 1em; + width: 90%; } + +.truefalse-container { + display: flex; + justify-content: space-between; + width: 25%; } + .truefalse-container > * { + margin: 0 .5em; } + +.right { + margin-left: auto; + margin-right: 0; + text-align: right; } + +.possible-answer { + align-items: center; + display: flex; + margin: 0 auto; + min-height: 48px; } + .possible-answer > * { + margin: 0 .5em; } + .possible-answer > [type="text"] { + width: 50%; } + +.question-controls { + align-items: center; + display: flex; } + +.add-question-type-select { + margin: .5em; + width: 66%; } + +.btn { + margin: .5em; + min-width: 35px; } + +.quiz-controls { + margin: 2em auto; + width: 80%; } + +/*# sourceMappingURL=authoring.css.map */ diff --git a/css/authoring.css.map b/css/authoring.css.map new file mode 100644 index 0000000..fec3e0c --- /dev/null +++ b/css/authoring.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAOA,EAAG;EACD,SAAS,EAAE,IAAI;;AAGjB,QAAS;EACP,KAAK,EAAE,IAAI;;AAGb,QAAS;EACP,gBAAgB,EAbL,OAAO;EAclB,MAAM,EAAE,iBAAoB;EAC5B,aAAa,EAAE,GAAG;EAClB,KAAK,EAdE,OAAO;;AAiBhB,WAAY;EACV,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,GAAG;EACZ,KAAK,EAAE,GAAG;EAEV,2BAAgB;IACd,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,MAAM;EAGpB,eAAI;IACF,UAAU,EAAE,MAAM;;AAItB,mBAAoB;EAClB,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,MAAM;EACjB,MAAM,EAAE,QAAQ;EAChB,KAAK,EAAE,GAAG;EAEV,uCAAoB;IAClB,WAAW,EAAE,MAAM;IACnB,gBAAgB,EA5CP,OAAO;IA6ChB,MAAM,EAAE,iBAAe;IACvB,aAAa,EAAE,GAAG;IAClB,OAAO,EAAE,IAAI;IACb,SAAS,EAAE,GAAG;IACd,eAAe,EAAE,aAAa;IAC9B,MAAM,EAAE,KAAK;IACb,OAAO,EAAE,OAAO;IAChB,YAAY,EAAE,KAAK;EAGrB,2BAAQ;IACN,UAAU,EAAE,IAAI;;AAIpB,eAAgB;EACd,OAAO,EAAE,YAAY;EACrB,MAAM,EAAE,CAAC;EACT,YAAY,EAAE,GAAG;;AAGnB,2BAA4B;EAC1B,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,GAAG;EACf,KAAK,EAAE,GAAG;;AAGZ,oBAAqB;EACnB,OAAO,EAAE,IAAI;EACb,eAAe,EAAE,aAAa;EAC9B,KAAK,EAAE,GAAG;EAEV,wBAAI;IACF,MAAM,EAAE,MAAM;;AAIlB,MAAO;EACL,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,CAAC;EACf,UAAU,EAAE,KAAK;;AAGnB,gBAAiB;EACf,WAAW,EAAE,MAAM;EACnB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,MAAM;EACd,UAAU,EAAE,IAAI;EAEhB,oBAAI;IACF,MAAM,EAAE,MAAM;EAIhB,gCAAgB;IACd,KAAK,EAAE,GAAG;;AAId,kBAAmB;EACjB,WAAW,EAAE,MAAM;EACnB,OAAO,EAAE,IAAI;;AAGf,yBAA0B;EACxB,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,GAAG;;AAGZ,IAAK;EACH,MAAM,EAAE,IAAI;EACZ,SAAS,EAAE,IAAI;;AAGjB,cAAe;EACb,MAAM,EAAE,QAAQ;EAChB,KAAK,EAAE,GAAG", +"sources": ["scss/authoring.scss"], +"names": [], +"file": "authoring.css" +} diff --git a/css/scss/authoring.scss b/css/scss/authoring.scss new file mode 100644 index 0000000..8de61e5 --- /dev/null +++ b/css/scss/authoring.scss @@ -0,0 +1,124 @@ +$grey: #808080; +$light-grey: #e7e7e7; + +$light-pink: #f8d7da; +$dark-pink: #f5c6cb; +$maroon: #721c24; + +h1 { + font-size: 18px; +} + +fieldset { + width: 100%; +} + +.warning { + background-color: $light-pink; + border: 1px solid $dark-pink; + border-radius: 5px; + color: $maroon; +} + +.error-list { + margin: auto; + padding: 1em; + width: 80%; + + > p:first-child { + font-weight: bold; + text-align: center; + } + + div { + text-align: center; + } +} + +.question-container { + display: flex; + flex-flow: column; + margin: 2em auto; + width: 80%; + + > div:first-of-type { + align-items: center; + background-color: $light-grey; + border: 1px solid $grey; + border-radius: 5px; + display: flex; + flex-flow: row; + justify-content: space-between; + margin: 1em 0; + padding: 0 .25em; + padding-left: .75em; + } + + > label { + margin-top: .5em; + } +} + +.question-title { + display: inline-block; + margin: 0; + margin-right: 2em; +} + +.question-content-container { + margin: auto; + margin-top: 1em; + width: 90%; +} + +.truefalse-container { + display: flex; + justify-content: space-between; + width: 25%; + + > * { + margin: 0 .5em; + } +} + +.right { + margin-left: auto; + margin-right: 0; + text-align: right; +} + +.possible-answer { + align-items: center; + display: flex; + margin: 0 auto; + min-height: 48px; + + > * { + margin: 0 .5em; + } + + + > [type="text"] { + width: 50%; + } +} + +.question-controls { + align-items: center; + display: flex; +} + +.add-question-type-select { + margin: .5em; + width: 66%; +} + +.btn { + margin: .5em; + min-width: 35px; +} + +.quiz-controls { + margin: 2em auto; + width: 80%; +} diff --git a/index.php b/index.php index 8023930..30f3c43 100644 --- a/index.php +++ b/index.php @@ -235,9 +235,11 @@ function percent($x) { TEMPLATES[type] = template; } $('#quiz').append(template(question)); + } + // Resize the window in case we made it too long in the authoring form + lti_frameResize($(document.body).height()+50); - } }).fail( function() { alert('Unable to load quiz data'); } ); }); diff --git a/js/authoring.js b/js/authoring.js new file mode 100644 index 0000000..157acb1 --- /dev/null +++ b/js/authoring.js @@ -0,0 +1,247 @@ +// Add a new question when the dropdown is changed +$("#question_type_select").change(function() { + var selected_value = $("#question_type_select").val(); + if (selected_value != "") { // As long as the selected value isn't the placeholder + // Create a new context for the templates + var context = {}; + context.count = $("#quiz_content").children().length+1; + switch(context.type) { + case "true_false_question": addTrueFalse(context); break; + case "multiple_choice_question": addMultipleChoice(context); break; // Multiple choice and multiple answer are handled the same + case "multiple_answers_question": addMultipleChoice(context); break; // Multiple choice and multiple answer are handled the same + case "short_answer_question": addShortAnswer(context); break; + default: + } + + context.type = selected_value; + addQuestion(context); + $("#question_type_select").val(""); // reset the dropdown + set_focus_on_lastquestion(); + } +}); + +/* Add a question to the form with the given context + context should have, at minimum, "count" and "type" + if more is included in the context (i.e. we are loading a quiz from a saved gift), + what is required changes based on the type of question (provided by parse_gift(), but for reference): + T/F: "answer" (either "T" or "F") + MC/MA/SA: "parsed_answer" (array of arrays, each in the form [bool, 'answer_text', '', 'question_id']) +*/ +function addQuestion(context) { + switch(context.type) { + case "true_false_question": + context.PrettyType = "True/False"; + $('#quiz_content').append(tsugiHandlebarsRender('common', context)) + addTrueFalse(context); + break; + case "multiple_choice_question": + context.PrettyType = "Multiple Choice/Multiple Answer"; + $('#quiz_content').append(tsugiHandlebarsRender('common', context)) + addMultipleChoice(context); + break; // Multiple choice and multiple answer are handled the same + case "multiple_answers_question": + context.PrettyType = "Multiple Choice/Multiple Answer"; + $('#quiz_content').append(tsugiHandlebarsRender('common', context)) + addMultipleChoice(context); + break; // Multiple choice and multiple answer are handled the same + case "short_answer_question": + context.PrettyType = "Short Answer"; + $('#quiz_content').append(tsugiHandlebarsRender('common', context)) + addShortAnswer(context); + break; + default: console.log("unrecognized question type: " + context.type); + } + + set_enabled_question_types("#question"+context.count+"_type_select", context.type); + + lti_frameResize(); +} + +// Add a True/False question to the form. If the context has an answer, fill it out +function addTrueFalse(context) { + if (context.answer == "T") { + context.answer_true = true; + } else if (context.answer == "F") { + context.answer_false = true; + } + addAnswer('#content_question'+context.count, 'tf_authoring', context) +} + +// Add a Multiple Choice/Multiple Answer Question to the form. If there are answers in the context, add them +function addMultipleChoice(context) { + if ("parsed_answer" in context) { + for (var a=0; a