From d45fe5678744babe568c6599c6fdb84b6207d125 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 29 Jun 2026 09:27:30 -0500 Subject: [PATCH] Remove the WebworkWebservice and replace it with the webwork API. In reality the functionality of the `WebworkWebservice` routes is basically the same, but the `WebworkWebservice` package is an unnecessary layer of code for the functionality of these routes. So it is removed. The `WeBWorK::ContentGenerator::RenderViaRPC` still handles the `render_rpc` route and its functionality is unchanged. It just doesn't go through the unnecessary layer of the `WebworkWebservice` package. The `lib/RenderProblem.pm` module is integrated directly into the `WeBWorK::ContentGenerator::RenderViaRPC` package. The `instructor_rpc` route has been removed, and is replaced with the `api` routes, and the `WeBWorK::ContentGenerator::InstructorRPCHandler` package functionality is now in the `WeBWorK::ContentGenerator::API` package. The `api` routes are fundamentally different than the `instructor_rpc` route. Instead of the single `instructor_rpc` route that requires an `rpc_command` parameter, the desired API command is part of the route. So, for example, instead of using the `webwork2/instructor_rpc` route with the parameter `rpc_command=putUserProblem` you use the route `webwork2/api/putUserProblem`. The remainder of the parameters for each command are the same as before. All of the `api` routes are implemented in the `WeBWorK::ContentGenerator::API` packages which are basically the previous `WebworkWebservice` packages. One notable difference is that each method is responsible for checking any necessary permissions required to use the route it provides. This is the same as all of the other `ContentGenerator` packages. There is a difference in the rendered JSON response for these routes. The `server_response` and `result_data` keys are not there at all anymore. Each route can determine what it really needs to return. Before the `result_data` was really what should have just been the returned data, and the `server_response` return value was a useless thing to return that was never used. Note that all of the methods pass through the `WeBWorK::ContentGenerator::API` base package, and that could render a response with the `error` key (if the user is not authenticated or the command is not found), and any of the `API` methods that provide a route can do so as well. The `renderError` method in the base package is a convenience for doing so. At this point all `api` routes require the user to be authenticated, but if we need `api` routes that do not require authentication (as before with the `instructor_rpc` route), that would not be hard to implement with some slight restructuring. All of webwork2's usage of these routes in JavaScript has been updated. Generally the `instructor_rpc` route is only used by webwork2, so this change should not affect anyone else. Part of the reason for this is that the `instructor_rpc` route was already not only for instructors. Students now use this route for tests to obtain the server time which ensures the remaining time displayed during a test is correct. The plan is to eventually implement a way of saving answers during tests via an api route as well. --- conf/defaults.config | 4 +- htdocs/js/GatewayQuiz/gateway.js | 12 +- htdocs/js/PGProblemEditor/pgproblemeditor.js | 61 +- .../js/ProblemGrader/singleproblemgrader.js | 41 +- htdocs/js/SetMaker/setmaker.js | 41 +- htdocs/js/TagWidget/tagwidget.js | 17 +- lib/FormatRenderedProblem.pm | 247 ++++---- lib/HardcopyRenderedProblem.pm | 84 ++- lib/WeBWorK.pm | 4 +- lib/WeBWorK/Authen/CAS.pm | 6 +- lib/WeBWorK/ContentGenerator.pm | 6 +- lib/WeBWorK/ContentGenerator/API.pm | 71 +++ .../ContentGenerator/API/CourseActions.pm | 531 ++++++++++++++++++ .../ContentGenerator/API/LibraryActions.pm | 155 +++++ .../ContentGenerator/API/ProblemActions.pm | 168 ++++++ .../ContentGenerator/API/SetActions.pm | 426 ++++++++++++++ .../ContentGenerator/InstructorRPCHandler.pm | 85 --- lib/WeBWorK/ContentGenerator/RenderViaRPC.pm | 331 ++++++++++- lib/WeBWorK/Utils/Routes.pm | 12 +- lib/WebworkWebservice.pm | 285 ---------- lib/WebworkWebservice/CourseActions.pm | 527 ----------------- lib/WebworkWebservice/LibraryActions.pm | 200 ------- lib/WebworkWebservice/ProblemActions.pm | 183 ------ lib/WebworkWebservice/RenderProblem.pm | 338 ----------- lib/WebworkWebservice/SetActions.pm | 522 ----------------- templates/RPCRenderFormats/default.html.ep | 15 +- templates/RPCRenderFormats/default.json.ep | 2 +- 27 files changed, 1883 insertions(+), 2491 deletions(-) create mode 100644 lib/WeBWorK/ContentGenerator/API.pm create mode 100644 lib/WeBWorK/ContentGenerator/API/CourseActions.pm create mode 100644 lib/WeBWorK/ContentGenerator/API/LibraryActions.pm create mode 100644 lib/WeBWorK/ContentGenerator/API/ProblemActions.pm create mode 100644 lib/WeBWorK/ContentGenerator/API/SetActions.pm delete mode 100644 lib/WeBWorK/ContentGenerator/InstructorRPCHandler.pm delete mode 100644 lib/WebworkWebservice.pm delete mode 100644 lib/WebworkWebservice/CourseActions.pm delete mode 100644 lib/WebworkWebservice/LibraryActions.pm delete mode 100644 lib/WebworkWebservice/ProblemActions.pm delete mode 100644 lib/WebworkWebservice/RenderProblem.pm delete mode 100644 lib/WebworkWebservice/SetActions.pm diff --git a/conf/defaults.config b/conf/defaults.config index 8abf21ba86..2d4eb5c8d0 100644 --- a/conf/defaults.config +++ b/conf/defaults.config @@ -765,7 +765,7 @@ $authen{admin_module} = ['WeBWorK::Authen::Basic_TheLastOption']; modify_tags => "admin", edit_restricted_files => "admin", - # Permission to render problems using the WebworkWebservice. + # Permission to render problems using the render_rpc endpoint. # Users with only webservice_render_problem can render problems with a provided filename. # Users with both permissions can also render problems with providing the problem source. # Note the Problem Editor requires having both permissions. @@ -1303,7 +1303,7 @@ $pgRoot = $pg{directories}{root}; ################################################################################ # Webservices ################################################################################ -# The following options only apply to actions performed via requests to $webwork_url/instructor_rpc. +# The following options only apply to actions performed via API requests to $webwork_url/api. $webservices = { # Enable createCourse, addUser, dropUser, deleteUser, editUser, and changeUserPassword enableCourseActions => 0, diff --git a/htdocs/js/GatewayQuiz/gateway.js b/htdocs/js/GatewayQuiz/gateway.js index 2949d051b5..2e7dbfe5a9 100644 --- a/htdocs/js/GatewayQuiz/gateway.js +++ b/htdocs/js/GatewayQuiz/gateway.js @@ -154,8 +154,6 @@ } }; - const basicWebserviceURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/instructor_rpc`; - const updateTimeDelta = async () => { const authenParams = {}; const user = document.getElementsByName('user')[0]; @@ -166,14 +164,10 @@ const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); - const response = await fetch(basicWebserviceURL, { + const response = await fetch(`${webworkConfig?.webwork_url ?? '/webwork2'}/api/getCurrentServerTime`, { method: 'post', mode: 'same-origin', - body: new URLSearchParams({ - ...authenParams, - rpc_command: 'getCurrentServerTime', - courseID: timerDiv.dataset.courseId - }), + body: new URLSearchParams({ ...authenParams, courseID: timerDiv.dataset.courseId }), signal: controller.signal }).catch(() => { /* Errors are ignored */ @@ -183,7 +177,7 @@ if (response && response.ok) { const data = await response.json(); - timeDelta = Math.round(new Date().getTime() / 1000) - data.result_data.currentServerTime; + timeDelta = Math.round(new Date().getTime() / 1000) - data.currentServerTime; } }; diff --git a/htdocs/js/PGProblemEditor/pgproblemeditor.js b/htdocs/js/PGProblemEditor/pgproblemeditor.js index 4318e11f1f..6c96fa53a2 100644 --- a/htdocs/js/PGProblemEditor/pgproblemeditor.js +++ b/htdocs/js/PGProblemEditor/pgproblemeditor.js @@ -124,7 +124,7 @@ bsToast.show(); }; - const webserviceURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/instructor_rpc`; + const apiURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/api`; // Send a request to the server to save the temporary file for the currently edited file. // This temporary file could be used for recovery, and is displayed if the page is reloaded. @@ -136,7 +136,6 @@ const sessionKey = document.getElementsByName('key')[0]; if (sessionKey) request_object.key = sessionKey.value; - request_object.rpc_command = 'saveFile'; request_object.outputFilePath = document.getElementsByName('temp_file_path')[0]?.value ?? ''; request_object.fileContents = webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? ''; @@ -151,11 +150,14 @@ revertRadio.checked = true; } - fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) }) + fetch(`${apiURL}/saveFile`, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) }) .then((response) => response.json()) .then((data) => { - showMessage(data.server_response, data.result_data); - if (data.result_data) { + if (data.error) { + showMessage(data.error); + } else { + showMessage(data.message, true); + // Add the temporary file coloring and change the current file to the saved file. document.querySelectorAll('.set-file-info').forEach((nfo) => nfo.classList.add('temporaryFile')); for (const currentFile of document.querySelectorAll('.current-file')) { @@ -217,22 +219,24 @@ const sessionKey = document.getElementsByName('key')[0]; if (sessionKey) request_object.key = sessionKey.value; - request_object.rpc_command = 'tidyPGCode'; request_object.pgCode = webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? ''; - fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) }) + fetch(`${apiURL}/tidyPGCode`, { + method: 'post', + mode: 'same-origin', + body: new URLSearchParams(request_object) + }) .then((response) => response.json()) .then((data) => { if (data.error) throw new Error(data.error); - if (!data.result_data) throw new Error('An invalid response was received.'); - if (data.result_data.status) { - if (data.result_data.errors) { + if (data.status) { + if (data.errors) { renderArea.innerHTML = '
' + '

PG perltidy errors:

' + '
' +
-							data.result_data.errors
+							data.errors
 								.replace(/^[\s\S]*Begin Error Output Stream\n\n/, '')
 								.replace(/\n\d*: To save a full \.LOG file rerun with -g/, '') +
 							'
'; @@ -240,12 +244,12 @@ showMessage('Errors occurred perltidying code.', false); return; } - if (request_object.pgCode === data.result_data.tidiedPGCode) { + if (request_object.pgCode === data.tidiedPGCode) { showMessage('There were no changes to the code.', true); if (!(renderArea.firstChild instanceof HTMLIFrameElement)) render(); } else { - if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.source = data.result_data.tidiedPGCode; - else document.getElementById('problemContents').value = data.result_data.tidiedPGCode; + if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.source = data.tidiedPGCode; + else document.getElementById('problemContents').value = data.tidiedPGCode; saveTempFile(); showMessage('Successfully perltidied code.', true); if (!(renderArea.firstChild instanceof HTMLIFrameElement)) render(); @@ -263,30 +267,31 @@ const sessionKey = document.getElementsByName('key')[0]; if (sessionKey) request_object.key = sessionKey.value; - request_object.rpc_command = 'convertCodeToPGML'; request_object.pgCode = webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? ''; - fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) }) + fetch(`${apiURL}/convertCodeToPGML`, { + method: 'post', + mode: 'same-origin', + body: new URLSearchParams(request_object) + }) .then((response) => response.json()) .then((data) => { - if (data.error) throw new Error(data.error); - if (!data.result_data) throw new Error('An invalid response was received.'); - if (data.result_data.error) { + if (data.error) { renderArea.innerHTML = '
' + '

PGML conversion error:

' + - data.result_data.error + + data.error + '
'; showMessage('Errors occurred when converting code to PGML.', false); return; } - if (request_object.pgCode === data.result_data.pgmlCode) { + if (request_object.pgCode === data.pgmlCode) { showMessage('There were no changes to the code.', true); } else { - if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.source = data.result_data.pgmlCode; - else document.getElementById('problemContents').value = data.result_data.pgmlCode; + if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.source = data.pgmlCode; + else document.getElementById('problemContents').value = data.pgmlCode; saveTempFile(); showMessage('Successfully converted code to PGML', true); if (!(renderArea.firstChild instanceof HTMLIFrameElement)) render(); @@ -304,16 +309,18 @@ const sessionKey = document.getElementsByName('key')[0]; if (sessionKey) request_object.key = sessionKey.value; - request_object.rpc_command = 'runPGCritic'; request_object.pgCode = webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? ''; - fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) }) + fetch(`${apiURL}/runPGCritic`, { + method: 'post', + mode: 'same-origin', + body: new URLSearchParams(request_object) + }) .then((response) => response.json()) .then((data) => { if (data.error) throw new Error(data.error); - if (!data.result_data) throw new Error('An invalid response was received.'); - renderArea.innerHTML = data.result_data.html; + renderArea.innerHTML = data.html; scrollToRenderArea(); }) .catch((err) => showMessage(`Error: ${err?.message ?? err}`)); diff --git a/htdocs/js/ProblemGrader/singleproblemgrader.js b/htdocs/js/ProblemGrader/singleproblemgrader.js index 9c9f16fbe9..76355ad5bb 100644 --- a/htdocs/js/ProblemGrader/singleproblemgrader.js +++ b/htdocs/js/ProblemGrader/singleproblemgrader.js @@ -107,29 +107,31 @@ } // Save the score. - const basicWebserviceURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/instructor_rpc`; + const apiURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/api`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { - const response = await fetch(basicWebserviceURL, { - method: 'post', - mode: 'same-origin', - body: new URLSearchParams({ - ...authenParams, - rpc_command: saveData.versionId !== '0' ? 'putProblemVersion' : 'putUserProblem', - courseID: saveData.courseId, - user_id: saveData.studentId, - set_id: saveData.setId, - version_id: saveData.versionId, - problem_id: saveData.problemId, - status: parseInt(scoreInput.value) / 100, - ...(saveData.saveSubStatus === '1' ? { sub_status: parseInt(scoreInput.value) / 100 } : {}), - mark_graded: true - }), - signal: controller.signal - }); + const response = await fetch( + `${apiURL}/${saveData.versionId !== '0' ? 'putProblemVersion' : 'putUserProblem'}`, + { + method: 'post', + mode: 'same-origin', + body: new URLSearchParams({ + ...authenParams, + courseID: saveData.courseId, + user_id: saveData.studentId, + set_id: saveData.setId, + version_id: saveData.versionId, + problem_id: saveData.problemId, + status: parseInt(scoreInput.value) / 100, + ...(saveData.saveSubStatus === '1' ? { sub_status: parseInt(scoreInput.value) / 100 } : {}), + mark_graded: true + }), + signal: controller.signal + } + ); clearTimeout(timeoutId); @@ -170,11 +172,10 @@ const timeoutId = setTimeout(() => controller.abort(), 10000); try { - const response = await fetch(basicWebserviceURL, { + const response = await fetch(`${apiURL}/putPastAnswer`, { method: 'post', body: new URLSearchParams({ ...authenParams, - rpc_command: 'putPastAnswer', courseID: saveData.courseId, answer_id: saveData.pastAnswerId, comment_string: comment diff --git a/htdocs/js/SetMaker/setmaker.js b/htdocs/js/SetMaker/setmaker.js index 8987d2e958..93b41df685 100644 --- a/htdocs/js/SetMaker/setmaker.js +++ b/htdocs/js/SetMaker/setmaker.js @@ -1,6 +1,5 @@ (() => { - const webworkURL = webworkConfig?.webwork_url ?? '/webwork2'; - const basicWebserviceURL = `${webworkURL}/instructor_rpc`; + const apiURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/api`; let unloading = false; window.addEventListener('beforeunload', () => (unloading = true)); @@ -68,7 +67,7 @@ }); }; - const init_webservice = (command) => { + const init_webservice = () => { const authenParams = {}; const user = document.getElementsByName('user')[0]; if (user) authenParams.user = user.value; @@ -76,12 +75,9 @@ if (sessionKey) authenParams.key = sessionKey.value; return { - rpc_command: 'listLib', library_name: 'Library', - command: 'buildtree', ...authenParams, - courseID: document.getElementsByName('hidden_course_id')[0]?.value, - rpc_command: command + courseID: document.getElementsByName('hidden_course_id')[0]?.value }; }; @@ -114,7 +110,7 @@ const lib_update = async (who, what) => { const child = { subject: 'chapter', chapter: 'section', section: 'count' }; - const requestObject = init_webservice('searchLib'); + const requestObject = init_webservice(); requestObject.library_subject = librarySubject?.value ?? ''; requestObject.library_chapter = libraryChapter?.value ?? ''; requestObject.library_section = librarySection?.value ?? ''; @@ -140,7 +136,7 @@ const timeoutId = setTimeout(() => controller.abort(), 10000); try { - const response = await fetch(basicWebserviceURL, { + const response = await fetch(`${apiURL}/searchLib`, { method: 'post', mode: 'same-origin', body: new URLSearchParams(requestObject), @@ -156,7 +152,7 @@ if (data.error) { throw data.error; } else { - const num = data.result_data[0]; + const num = data[0]; countLine.firstElementChild.innerHTML = num === '1' ? 'There is 1 matching WeBWorK problem' @@ -164,7 +160,7 @@ } } } catch (e) { - alertToast(basicWebserviceURL, e?.message ?? e); + alertToast(`${apiURL}/searchLib`, e?.message ?? e); } return; } @@ -190,7 +186,7 @@ const timeoutId = setTimeout(() => controller.abort(), 10000); try { - const response = await fetch(basicWebserviceURL, { + const response = await fetch(`${apiURL}/searchLib`, { method: 'post', mode: 'same-origin', body: new URLSearchParams(requestObject), @@ -206,12 +202,12 @@ if (data.error) { throw data.error; } else { - setselect(`library_${who}`, data.result_data); + setselect(`library_${who}`, data); lib_update(child[who], 'clear'); } } } catch (e) { - alertToast(basicWebserviceURL, e?.message ?? e); + alertToast(`${apiURL}/searchLib`, e?.message ?? e); } }; @@ -268,7 +264,7 @@ return; } - const request = init_webservice('addProblem'); + const request = init_webservice(); request.set_id = target; const pathlist = []; @@ -281,14 +277,14 @@ try { // The requests must be awaited in the for loop so that the problems are added in the correct order. - // FIXME: It would be better to add a WebworkWebservice method to add multiple problems in one request. + // FIXME: It would be better to add a api call to add multiple problems in one request. for (const path of pathlist) { request.problemPath = path; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); - const response = await fetch(basicWebserviceURL, { + const response = await fetch(`${apiURL}/addProblem`, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request), @@ -305,7 +301,7 @@ } } } catch (e) { - alertToast(basicWebserviceURL, e?.message ?? e); + alertToast(`${apiURL}/addProblem`, e?.message ?? e); return; } @@ -330,15 +326,14 @@ // Update the messages about which problems are in the current set. const markinset = async () => { - const ro = init_webservice('listGlobalSetProblems'); + const ro = init_webservice(); ro.set_id = document.getElementById('local_sets')?.value; - ro.command = 'true'; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { - const response = await fetch(basicWebserviceURL, { + const response = await fetch(`${apiURL}/listGlobalSetProblems`, { method: 'post', mode: 'same-origin', body: new URLSearchParams(ro), @@ -352,7 +347,7 @@ if (data.error) { throw data.error; } else { - const paths = data.result_data.map((problem) => problem.path); + const paths = data.map((problem) => problem.source_file); const shownProbs = document.querySelectorAll('[name^="filetrial"]'); for (const shownProb of shownProbs) { const inset = document.getElementById(`inset${shownProb.name.replace('filetrial', '')}`); @@ -364,7 +359,7 @@ throw 'Unknown server communication error.'; } } catch (e) { - alertToast(basicWebserviceURL, e?.message ?? e); + alertToast(`${apiURL}/listGlobalSetProblems`, e?.message ?? e); } }; diff --git a/htdocs/js/TagWidget/tagwidget.js b/htdocs/js/TagWidget/tagwidget.js index 3ac04cd62b..37fc6d8bc4 100644 --- a/htdocs/js/TagWidget/tagwidget.js +++ b/htdocs/js/TagWidget/tagwidget.js @@ -55,7 +55,7 @@ const taxonomy = await response.json(); - const webServiceURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/instructor_rpc`; + const apiURL = `${webworkConfig?.webwork_url ?? '/webwork2'}/api`; const readFromTaxonomy = (category, values) => { const subjectTaxonomy = taxonomy; @@ -72,7 +72,7 @@ return []; // Should not get here }; - const createWebServiceObject = (command, values = {}) => { + const createWebServiceObject = (values = {}) => { const authenParams = {}; const user = document.getElementsByName('user')[0]; if (user) authenParams.user = user.value; @@ -80,7 +80,6 @@ if (sessionKey) authenParams.key = sessionKey.value; return { - rpc_command: command, library_name: 'Library', command: 'searchLib', courseID: document.getElementsByName('hidden_course_id')[0]?.value, @@ -237,16 +236,16 @@ } async getTags() { - const response = await fetch(webServiceURL, { + const response = await fetch(`${apiURL}/getProblemTags`, { method: 'post', mode: 'same-origin', - body: new URLSearchParams(createWebServiceObject('getProblemTags', { command: this.filePath })) + body: new URLSearchParams(createWebServiceObject({ command: this.filePath })) }).catch((err) => `Error requesting problem tags: ${err.message ?? err}`); if (typeof response === 'string') return showMessage(response); if (!response.ok) return showMessage('Unable to obtain problem tags.'); const data = await response.json(); if (data.error) return showMessage(data.error); - this.tags = data.result_data; + this.tags = data; } update(category, values, clear = false) { @@ -304,11 +303,11 @@ } async savetags() { - const response = await fetch(webServiceURL, { + const response = await fetch(`${apiURL}/setProblemTags`, { method: 'post', mode: 'same-origin', body: new URLSearchParams( - createWebServiceObject('setProblemTags', { + createWebServiceObject({ library_subject: this.subjectSelect.value, library_chapter: this.chapterSelect.value, library_section: this.sectionSelect.value, @@ -322,7 +321,7 @@ if (!response.ok) return showMessage('Unable to save problem tags.'); const data = await response.json(); if (data.error) return showMessage(data.error); - showMessage(data.server_response, true); + showMessage(data.message, true); } } diff --git a/lib/FormatRenderedProblem.pm b/lib/FormatRenderedProblem.pm index 029de024af..f1f7477d80 100644 --- a/lib/FormatRenderedProblem.pm +++ b/lib/FormatRenderedProblem.pm @@ -6,9 +6,7 @@ FormatRenderedProblem.pm =cut package FormatRenderedProblem; - -use strict; -use warnings; +use Mojo::Base -signatures; use Digest::SHA qw(sha1_base64); use Mojo::Util qw(xml_escape); @@ -18,11 +16,10 @@ use Mojo::DOM; use WeBWorK::Utils qw(getAssetURL); use WeBWorK::Utils::LanguageAndDirection qw(get_lang_and_dir get_problem_lang_and_dir); -sub formatRenderedProblem { - my $ws = shift; # $ws is a WebworkWebservice object. - my $ce = $ws->ce; +sub formatRenderedProblem ($c, $renderedProblem) { + my $ce = $c->ce; - my $rh_result = $ws->return_object; + my $inputsRef = $c->req->params->to_hash; my $forbidGradePassback = 1; # Default is to forbid, due to the security issue @@ -37,19 +34,19 @@ sub formatRenderedProblem { my $renderErrorOccurred = 0; - my $problemText = $rh_result->{text} // ''; - if ($rh_result->{flags}{error_flag}) { - $rh_result->{problem_result}{score} = 0; # force score to 0 for such errors. - $renderErrorOccurred = 1; - $forbidGradePassback = 1; # due to render error + my $problemText = $renderedProblem->{text} // ''; + if ($renderedProblem->{flags}{error_flag}) { + $renderedProblem->{problem_result}{score} = 0; # force score to 0 for such errors. + $renderErrorOccurred = 1; + $forbidGradePassback = 1; # due to render error } - my $SITE_URL = $ws->c->server_root_url; + my $SITE_URL = $c->server_root_url; - my $displayMode = $ws->{inputs_ref}{displayMode} // 'MathJax'; + my $displayMode = $inputsRef->{displayMode} // 'MathJax'; # HTML document language setting - my $formLanguage = $ws->{inputs_ref}{language} // 'en'; + my $formLanguage = $inputsRef->{language} // 'en'; # Third party CSS # The second element of each array in the following is whether or not the file is a theme file. @@ -68,8 +65,8 @@ sub formatRenderedProblem { if (ref($ce->{pg}{specialPGEnvironmentVars}{extra_css_files}) eq 'ARRAY') { push(@cssFiles, { file => $_, external => 0 }) for @{ $ce->{pg}{specialPGEnvironmentVars}{extra_css_files} }; } - if (ref($rh_result->{flags}{extra_css_files}) eq 'ARRAY') { - push @cssFiles, @{ $rh_result->{flags}{extra_css_files} }; + if (ref($renderedProblem->{flags}{extra_css_files}) eq 'ARRAY') { + push @cssFiles, @{ $renderedProblem->{flags}{extra_css_files} }; } my %cssFilesAdded; # Used to avoid duplicates my @extra_css_files; @@ -99,13 +96,13 @@ sub formatRenderedProblem { ); # Get the requested format. - my $formatName = $ws->{inputs_ref}{outputformat} // 'simple'; + my $formatName = $inputsRef->{outputformat} // 'simple'; # Add JS files requested by problems via ADD_JS_FILE() in the PG file. my @extra_js_files; - if (ref($rh_result->{flags}{extra_js_files}) eq 'ARRAY') { + if (ref($renderedProblem->{flags}{extra_js_files}) eq 'ARRAY') { my %jsFiles; - for (@{ $rh_result->{flags}{extra_js_files} }) { + for (@{ $renderedProblem->{flags}{extra_js_files} }) { next if $jsFiles{ $_->{file} }; $jsFiles{ $_->{file} } = 1; my %attributes = ref($_->{attributes}) eq 'HASH' ? %{ $_->{attributes} } : (); @@ -122,17 +119,17 @@ sub formatRenderedProblem { # PG files can request their language and text direction be set. If we do not have access to a default course # language, fall back to the $formLanguage instead. my %PROBLEM_LANG_AND_DIR = - get_problem_lang_and_dir($rh_result->{flags}, $ce->{perProblemLangAndDirSettingMode}, $formLanguage); + get_problem_lang_and_dir($renderedProblem->{flags}, $ce->{perProblemLangAndDirSettingMode}, $formLanguage); my $PROBLEM_LANG_AND_DIR = join(' ', map {qq{$_="$PROBLEM_LANG_AND_DIR{$_}"}} keys %PROBLEM_LANG_AND_DIR); - my $previewMode = defined($ws->{inputs_ref}{previewAnswers}) || 0; - my $submitMode = defined($ws->{inputs_ref}{WWsubmit}) || 0; - my $showCorrectMode = defined($ws->{inputs_ref}{WWcorrectAns}) || 0; + my $previewMode = defined($inputsRef->{previewAnswers}) || 0; + my $submitMode = defined($inputsRef->{WWsubmit}) || 0; + my $showCorrectMode = defined($inputsRef->{WWcorrectAns}) || 0; # A problemUUID should be added to the request as a parameter. It is used by PG to create a proper UUID for use in # aliases for resources. It should be unique for a course, user, set, problem, and version. - my $problemUUID = $ws->{inputs_ref}{problemUUID} // ''; - my $problemResult = $rh_result->{problem_result} // {}; - my $showSummary = $ws->{inputs_ref}{showSummary} // 1; + my $problemUUID = $inputsRef->{problemUUID} // ''; + my $problemResult = $renderedProblem->{problem_result} // {}; + my $showSummary = $inputsRef->{showSummary} // 1; # Result summary my $resultSummary = ''; @@ -146,13 +143,13 @@ sub formatRenderedProblem { && ($submitMode || $showCorrectMode) && $problemResult->{summary}) { - $resultSummary = $ws->c->c( - $ws->c->tag( + $resultSummary = $c->c( + $c->tag( 'h2', class => 'fs-3 mb-2', - $ws->c->maketext('Results for this submission') + $c->maketext('Results for this submission') ) - . $ws->c->tag('div', role => 'alert', $ws->c->b($problemResult->{summary})) + . $c->tag('div', role => 'alert', $c->b($problemResult->{summary})) )->join(''); } @@ -160,26 +157,27 @@ sub formatRenderedProblem { my $answerhashXML = ''; if ($formatName eq 'ptx') { my $dom = Mojo::DOM->new->xml(1); - for my $answer (sort keys %{ $rh_result->{answers} }) { + for my $answer (sort keys %{ $renderedProblem->{answers} }) { $dom->append_content($dom->new_tag( $answer, - map { $_ => ($rh_result->{answers}{$answer}{$_} // '') } keys %{ $rh_result->{answers}{$answer} } + map { $_ => ($renderedProblem->{answers}{$answer}{$_} // '') } + keys %{ $renderedProblem->{answers}{$answer} } )); } $dom->wrap_content(''); $answerhashXML = $dom->to_string; - $ws->c->res->headers->content_type('text/xml; charset=utf-8') - if $ws->c->current_route eq 'render_rpc' && ($ws->c->param('displayMode') // '') eq 'PTX'; + $c->res->headers->content_type('text/xml; charset=utf-8') + if $c->current_route eq 'render_rpc' && ($inputsRef->{displayMode} // '') eq 'PTX'; } - # Make sure $rh_result->{debug_messages} an array reference as saveGradeToLTI might add to it. - $rh_result->{debug_messages} = [] unless ref $rh_result->{debug_messages} eq 'ARRAY'; + # Make sure $renderedProblem->{debug_messages} an array reference as saveGradeToLTI might add to it. + $renderedProblem->{debug_messages} = [] unless ref $renderedProblem->{debug_messages} eq 'ARRAY'; $forbidGradePassback = 1 if !$forbidGradePassback && !$submitMode; # Try to save the grade to an LTI if one provided us data (depending on $forbidGradePassback) - my $LTIGradeMessage = saveGradeToLTI($ws, $ce, $rh_result, $forbidGradePassback); + my $LTIGradeMessage = saveGradeToLTI($c, $ce, $renderedProblem, $forbidGradePassback); # Execute and return the interpolated problem template @@ -191,9 +189,9 @@ sub formatRenderedProblem { my $output = {}; # Everything that ships out with other formats can be constructed from these - $output->{rh_result} = $rh_result; - $output->{inputs_ref} = $ws->{inputs_ref}; - $output->{input} = $ws->{input}; + $output->{rh_result} = $renderedProblem; + $output->{inputs_ref} = $inputsRef; + $output->{input} = $c->{input}; # FIXME: What is this? # The following could be constructed from the above, but this is a convenience $output->{resultSummary} = $resultSummary->to_string if $resultSummary; @@ -201,7 +199,7 @@ sub formatRenderedProblem { $output->{dir} = $PROBLEM_LANG_AND_DIR{dir}; $output->{extra_css_files} = \@extra_css_files; $output->{extra_js_files} = \@extra_js_files; - $output->{webwork_js_config} = $ws->c->webwork_js_config($ws->{inputs_ref}{showMathJaxErrors} // 0); + $output->{webwork_js_config} = $c->webwork_js_config($inputsRef->{showMathJaxErrors} // 0); # Include third party css and javascript files. Only jquery, jquery-ui, mathjax, and bootstrap are needed for # PG. See the comments before the subroutine definitions for load_css and load_js in pg/macros/PG.pl. @@ -214,7 +212,7 @@ sub formatRenderedProblem { $output->{pg_version} = $ce->{PG_VERSION}; # Convert to JSON and render. - return $ws->c->render(data => encode_json($output)); + return $c->render(data => encode_json($output)); } # Setup arnd render the appropriate template in the templates/RPCRenderFormats folder depending on the outputformat. @@ -223,22 +221,21 @@ sub formatRenderedProblem { template => $formatName eq 'ptx' ? 'RPCRenderFormats/ptx' : 'RPCRenderFormats/default', $formatName eq 'json' ? (format => 'json') : (), formatName => $formatName, - ws => $ws, ce => $ce, lh => $lh, - rh_result => $rh_result, + rh_result => $renderedProblem, SITE_URL => $SITE_URL, - FORM_ACTION_URL => $SITE_URL . $ws->c->webwork_url . '/' . $ws->c->current_route, + FORM_ACTION_URL => $SITE_URL . $c->webwork_url . '/' . $c->current_route, COURSE_LANG_AND_DIR => get_lang_and_dir($formLanguage), - theme => $ws->{inputs_ref}{theme} || $ce->{defaultTheme}, - courseID => $ws->{inputs_ref}{courseID} // '', - user => $ws->{inputs_ref}{user} // '', - passwd => $ws->{inputs_ref}{passwd} // '', - disableCookies => $ws->{inputs_ref}{disableCookies} // '', - key => $ws->authen->{session_key}, + theme => $inputsRef->{theme} || $ce->{defaultTheme}, + courseID => $inputsRef->{courseID} // '', + user => $inputsRef->{user} // '', + passwd => $inputsRef->{passwd} // '', + disableCookies => $inputsRef->{disableCookies} // '', + key => $c->authen->{session_key}, PROBLEM_LANG_AND_DIR => $PROBLEM_LANG_AND_DIR, - problemSeed => $rh_result->{problem_seed} // $ws->{inputs_ref}{problemSeed} // 6666, - psvn => $rh_result->{psvn} // $ws->{inputs_ref}{psvn} // 54321, + problemSeed => $renderedProblem->{problem_seed} // $inputsRef->{problemSeed} // 6666, + psvn => $renderedProblem->{psvn} // $inputsRef->{psvn} // 54321, problemUUID => $problemUUID, displayMode => $displayMode, third_party_css => \@third_party_css, @@ -246,55 +243,55 @@ sub formatRenderedProblem { third_party_js => \@third_party_js, extra_js_files => \@extra_js_files, problemText => $problemText, - extra_header_text => $ws->{inputs_ref}{extra_header_text} // '', + extra_header_text => $inputsRef->{extra_header_text} // '', resultSummary => $resultSummary, showScoreSummary => $submitMode && !$renderErrorOccurred && $problemResult, answerhashXML => $answerhashXML, LTIGradeMessage => $LTIGradeMessage, - sourceFilePath => $ws->{inputs_ref}{sourceFilePath} // '', - problemSource => $ws->{inputs_ref}{problemSource} // '', - rawProblemSource => $ws->{inputs_ref}{rawProblemSource} // '', - uriEncodedProblemSource => $ws->{inputs_ref}{uriEncodedProblemSource} // '', - fileName => $ws->{inputs_ref}{fileName} // '', + sourceFilePath => $inputsRef->{sourceFilePath} // '', + problemSource => $inputsRef->{problemSource} // '', + rawProblemSource => $inputsRef->{rawProblemSource} // '', + uriEncodedProblemSource => $inputsRef->{uriEncodedProblemSource} // '', + fileName => $inputsRef->{fileName} // '', formLanguage => $formLanguage, - isInstructor => $ws->{inputs_ref}{isInstructor} // '', - forceScaffoldsOpen => $ws->{inputs_ref}{forceScaffoldsOpen} // '', + isInstructor => $inputsRef->{isInstructor} // '', + forceScaffoldsOpen => $inputsRef->{forceScaffoldsOpen} // '', showSummary => $showSummary, - showHints => $ws->{inputs_ref}{showHints} // '', - showSolutions => $ws->{inputs_ref}{showSolutions} // '', - showPreviewButton => $ws->{inputs_ref}{showPreviewButton} // '', - showCheckAnswersButton => $ws->{inputs_ref}{showCheckAnswersButton} // '', - showCorrectAnswersButton => $ws->{inputs_ref}{showCorrectAnswersButton} // '', - showCorrectAnswersOnlyButton => $ws->{inputs_ref}{showCorrectAnswersOnlyButton} // 0, - showFooter => $ws->{inputs_ref}{showFooter} // '', - problem_data => encode_json($rh_result->{PERSISTENCE_HASH}), - showMathJaxErrors => $ws->{inputs_ref}{showMathJaxErrors} // 0, - pretty_print => \&pretty_print + showHints => $inputsRef->{showHints} // '', + showSolutions => $inputsRef->{showSolutions} // '', + showPreviewButton => $inputsRef->{showPreviewButton} // '', + showCheckAnswersButton => $inputsRef->{showCheckAnswersButton} // '', + showCorrectAnswersButton => $inputsRef->{showCorrectAnswersButton} // '', + showCorrectAnswersOnlyButton => $inputsRef->{showCorrectAnswersOnlyButton} // 0, + showFooter => $inputsRef->{showFooter} // '', + problem_data => encode_json($renderedProblem->{PERSISTENCE_HASH}), + showMathJaxErrors => $inputsRef->{showMathJaxErrors} // 0, ); - return $ws->c->render(%template_params) if $formatName eq 'json' || !$ws->{inputs_ref}{send_pg_flags}; - return $ws->c->render( - json => { html => $ws->c->render_to_string(%template_params)->to_string, pg_flags => $rh_result->{flags} }); + return $c->render(%template_params) if $formatName eq 'json' || !$inputsRef->{send_pg_flags}; + return $c->render(json => + { html => $c->render_to_string(%template_params)->to_string, pg_flags => $renderedProblem->{flags} }); } -sub saveGradeToLTI { - my ($ws, $ce, $rh_result, $forbidGradePassback) = @_; +sub saveGradeToLTI ($c, $ce, $renderedProblem, $forbidGradePassback) { # When $forbidGradePassback is set, we will block the actual submission, # but we still provide the LTI data in the hidden fields. + my $inputsRef = $c->req->params->to_hash; + return '' - if !(defined($ws->{inputs_ref}{lis_outcome_service_url}) - && defined($ws->{inputs_ref}{'oauth_consumer_key'}) - && defined($ws->{inputs_ref}{'oauth_signature_method'}) - && defined($ws->{inputs_ref}{'lis_result_sourcedid'}) - && defined($ce->{'LISConsumerKeyHash'}{ $ws->{inputs_ref}{'oauth_consumer_key'} })); - - my $request_url = $ws->{inputs_ref}{lis_outcome_service_url}; - my $consumer_key = $ws->{inputs_ref}{'oauth_consumer_key'}; - my $signature_method = $ws->{inputs_ref}{'oauth_signature_method'}; - my $sourcedid = $ws->{inputs_ref}{'lis_result_sourcedid'}; + if !(defined($inputsRef->{lis_outcome_service_url}) + && defined($inputsRef->{'oauth_consumer_key'}) + && defined($inputsRef->{'oauth_signature_method'}) + && defined($inputsRef->{'lis_result_sourcedid'}) + && defined($ce->{'LISConsumerKeyHash'}{ $inputsRef->{'oauth_consumer_key'} })); + + my $request_url = $inputsRef->{lis_outcome_service_url}; + my $consumer_key = $inputsRef->{'oauth_consumer_key'}; + my $signature_method = $inputsRef->{'oauth_signature_method'}; + my $sourcedid = $inputsRef->{'lis_result_sourcedid'}; my $consumer_secret = $ce->{'LISConsumerKeyHash'}{$consumer_key}; - my $score = $rh_result->{problem_result} ? $rh_result->{problem_result}{score} : 0; + my $score = $renderedProblem->{problem_result} ? $renderedProblem->{problem_result}{score} : 0; my $LTIGradeMessage = ''; @@ -367,80 +364,24 @@ EOS $response->content =~ /\s*(\w+)\s*<\/imsx_codeMajor>/; my $message = $1; if ($message ne 'success') { - $LTIGradeMessage = $ws->c->tag('p', "Unable to update LMS grade. Error: $message")->to_string; - push(@{ $rh_result->{debug_messages} }, xml_escape($response->content)); + $LTIGradeMessage = $c->tag('p', "Unable to update LMS grade. Error: $message")->to_string; + push(@{ $renderedProblem->{debug_messages} }, xml_escape($response->content)); } else { - $LTIGradeMessage = $ws->c->tag('p', 'Grade successfully saved.')->to_string; + $LTIGradeMessage = $c->tag('p', 'Grade successfully saved.')->to_string; } } else { - $LTIGradeMessage = $ws->c->tag('p', 'Unable to update LMS grade. Error: ' . $response->message)->to_string; - push(@{ $rh_result->{debug_messages} }, xml_escape($response->content)); + $LTIGradeMessage = $c->tag('p', 'Unable to update LMS grade. Error: ' . $response->message)->to_string; + push(@{ $renderedProblem->{debug_messages} }, xml_escape($response->content)); } } # save parameters for next time - $LTIGradeMessage .= $ws->c->hidden_field(lis_outcome_service_url => $request_url)->to_string; - $LTIGradeMessage .= $ws->c->hidden_field(oauth_consumer_key => $consumer_key)->to_string; - $LTIGradeMessage .= $ws->c->hidden_field(oauth_signature_method => $signature_method)->to_string; - $LTIGradeMessage .= $ws->c->hidden_field(lis_result_sourcedid => $sourcedid)->to_string; + $LTIGradeMessage .= $c->hidden_field(lis_outcome_service_url => $request_url)->to_string; + $LTIGradeMessage .= $c->hidden_field(oauth_consumer_key => $consumer_key)->to_string; + $LTIGradeMessage .= $c->hidden_field(oauth_signature_method => $signature_method)->to_string; + $LTIGradeMessage .= $c->hidden_field(lis_result_sourcedid => $sourcedid)->to_string; return $LTIGradeMessage; } -# Nice output for debugging -sub pretty_print { - my ($r_input, $level) = @_; - return 'undef' unless defined $r_input; - - $level //= 4; - $level--; - return 'too deep' unless $level > 0; - - my $ref = ref($r_input); - - if (!$ref) { - return xml_escape($r_input); - } elsif (eval { %$r_input || 1 }) { - # `eval { %$r_input || 1 }` will pick up all objectes that can be accessed like a hash and so works better than - # `ref $r_input`. Do not use `"$r_input" =~ /hash/i` because that will pick up strings containing the word - # hash, and that will cause an error below. - my $out = - '
' - . ($ref eq 'HASH' - ? '' - : '
' - . "$ref
") - . '
'; - for my $key (sort keys %$r_input) { - # Safety feature - we do not want to display the contents of %seed_ce which - # contains the database password and lots of other things, and explicitly hide - # certain internals of the CourseEnvironment in case one slips in. - next - if (($key =~ /database/) - || ($key eq "ConfigValues") - || ($key eq "ENV") - || ($key eq "externalPrograms") - || ($key eq "permissionLevels") - || ($key eq "seed_ce")); - $out .= - '
' - . xml_escape($key) - . '
' - . qq{
=>
} - . qq{
} - . pretty_print($r_input->{$key}, $level) - . '
'; - } - $out .= '
'; - return $out; - } elsif ($ref eq 'ARRAY') { - return '[ ' . join(', ', map { pretty_print($_, $level) } @$r_input) . ' ]'; - } elsif ($ref eq 'CODE') { - return 'CODE'; - } else { - return xml_escape($r_input); - } -} - 1; diff --git a/lib/HardcopyRenderedProblem.pm b/lib/HardcopyRenderedProblem.pm index b248ef0ef9..761af595ec 100644 --- a/lib/HardcopyRenderedProblem.pm +++ b/lib/HardcopyRenderedProblem.pm @@ -3,14 +3,12 @@ HardcopyRenderedProblem.pm -- Generate a pdf file or zip file containing a tex file and the necessary files to generate the pdf file from the result of the -renderProblem method. +C method. =cut package HardcopyRenderedProblem; - -use strict; -use warnings; +use Mojo::Base -signatures; use File::Path; use String::ShellQuote; @@ -18,22 +16,18 @@ use Archive::Zip qw(:ERROR_CODES); use Mojo::File qw(path tempdir); use XML::LibXML; -sub hardcopyRenderedProblem { - my $ws = shift; # $ws is a WebworkWebservice object. - my $c = $ws->c; - my $ce = $ws->ce; - - my $rh_result = $ws->return_object; +sub hardcopyRenderedProblem ($c, $renderedProblem) { + my $ce = $c->ce; # Deal with PG errors - return $rh_result->{errors} if $rh_result->{flags}{error_flag}; + return $renderedProblem->{errors} if $renderedProblem->{flags}{error_flag}; - return 'This problem has no content.' unless $rh_result->{text}; + return 'This problem has no content.' unless $renderedProblem->{text}; my @errors; - my $courseID = $ws->{inputs_ref}{courseID}; - my $userID = $ws->{inputs_ref}{user}; + my $courseID = $c->req->param('courseID'); + my $userID = $c->req->param('user'); # Create the parent directory for the temporary working directory. my $temp_dir_parent_path = path("$ce->{webworkDirs}{tmp}/$courseID/hardcopy/$userID"); @@ -53,7 +47,7 @@ sub hardcopyRenderedProblem { # Use the basename of the source file path without the extension prefixed with the course id and user id for the # working directory name and download filename. my $returnFileName = - "$courseID.$userID." . ((($ws->{inputs_ref}{sourceFilePath} =~ s/^.*\///r) =~ s/\.[^.]*$//r) || 'hardcopy'); + "$courseID.$userID." . ((($c->req->param('sourceFilePath') =~ s/^.*\///r) =~ s/\.[^.]*$//r) || 'hardcopy'); # Create a subdirectory of that to do all of the work in. This directory will be zipped # if the tex outputformat is specified or if pdf generation fails or has errors. @@ -71,12 +65,12 @@ sub hardcopyRenderedProblem { push(@errors, qq{Failed to open file "$tex_file" for writing: $!}); return join("\n", @errors); } - write_tex($ws, $fh, \@errors); + write_tex($c, $renderedProblem, $fh, \@errors); $fh->close; # Call the pdf generation subroutine if the pdf outputformat was specified or if no outputformat was specified. - if (!$ws->{inputs_ref}{outputformat} || $ws->{inputs_ref}{outputformat} eq 'pdf') { - generate_hardcopy_pdf($ws, $working_dir, \@errors); + if (!$c->req->param('outputformat') || $c->req->param('outputformat') eq 'pdf') { + generate_hardcopy_pdf($c, $working_dir, \@errors); # Send the pdf file if it was successfully generated with no errors. my $pdf_file = $working_dir->child('hardcopy.pdf'); @@ -90,7 +84,7 @@ sub hardcopyRenderedProblem { # Call the tex generation subroutine if the tex outputformat was specified, # or if there were errors in generating the pdf file. - generate_hardcopy_tex($ws, $working_dir, \@errors); + generate_hardcopy_tex($c, $renderedProblem, $working_dir, \@errors); # Send the zip file if it exists. my $zip_file = $temp_dir_path->child('hardcopy.zip'); @@ -107,12 +101,11 @@ sub hardcopyRenderedProblem { } # This subroutine assumes that the TeX source file is located at $working_dir/hardcopy.tex. -sub generate_hardcopy_tex { - my ($ws, $working_dir, $errors) = @_; +sub generate_hardcopy_tex ($c, $renderedProblem, $working_dir, $errors) { my $src_file = $working_dir->child('hardcopy.tex'); # Copy the common tex files into the working directory - my $ce = $ws->c->ce; + my $ce = $c->ce; my $assetsTex_dir = path($ce->{webworkDirs}{assetsTex}); for (qw{webwork2.sty webwork_logo.png}) { eval { $assetsTex_dir->child($_)->copy_to($working_dir) }; @@ -131,7 +124,7 @@ sub generate_hardcopy_tex { if $@; # Attempt to copy image files used into the working directory. - my $resource_list = $ws->return_object->{resource_list}; + my $resource_list = $renderedProblem->{resource_list}; if ($resource_list && keys %$resource_list) { my $data = eval { $src_file->slurp }; unless ($@) { @@ -166,9 +159,7 @@ sub generate_hardcopy_tex { } # This subroutine assumes that the TeX source file is located at $working_dir/hardcopy.tex. -sub generate_hardcopy_pdf { - my ($ws, $working_dir, $errors) = @_; - +sub generate_hardcopy_pdf ($c, $working_dir, $errors) { # Save the current working directory and change to the temporary directory. my $cwd = path->to_abs; chdir($working_dir); @@ -176,9 +167,9 @@ sub generate_hardcopy_pdf { # Generate the pdf file with the configured LaTeX external command. my $latex_cmd = 'TEXINPUTS=.:' - . shell_quote($ws->c->ce->{webworkDirs}{assetsTex}) . ':' - . shell_quote($ws->c->ce->{pg}{directories}{assetsTex}) . ': ' - . $ws->c->ce->{externalPrograms}{latex2pdf} + . shell_quote($c->ce->{webworkDirs}{assetsTex}) . ':' + . shell_quote($c->ce->{pg}{directories}{assetsTex}) . ': ' + . $c->ce->{externalPrograms}{latex2pdf} . ' > latex.stdout 2> latex.stderr hardcopy'; if (my $rawexit = system $latex_cmd) { @@ -196,13 +187,11 @@ sub generate_hardcopy_pdf { return; } -sub write_tex { - my ($ws, $FH, $errors) = @_; - my $c = $ws->c; +sub write_tex ($c, $renderedProblem, $FH, $errors) { my $ce = $c->ce; # get theme - my $theme = $c->param('hardcopy_theme') // $ce->{hardcopyThemePGEditor}; + my $theme = $c->req->param('hardcopy_theme') // $ce->{hardcopyThemePGEditor}; my $themeFile; if (-e "$ce->{courseDirs}{hardcopyThemes}/$theme") { $themeFile = "$ce->{courseDirs}{hardcopyThemes}/$theme"; @@ -219,7 +208,7 @@ sub write_tex { print $FH $themeTree->findvalue('/theme/presetheader'); print $FH $themeTree->findvalue('/theme/postsetheader'); print $FH $themeTree->findvalue('/theme/problemheader'); - write_problem_tex($ws, $FH); + write_problem_tex($c, $renderedProblem, $FH); print $FH $themeTree->findvalue('/theme/problemfooter'); print $FH $themeTree->findvalue('/theme/setfooter'); print $FH $themeTree->findvalue('/theme/postamble'); @@ -227,20 +216,17 @@ sub write_tex { return; } -sub write_problem_tex { - my ($ws, $FH) = @_; - my $c = $ws->c; - - my $rh_result = $ws->return_object; - - print $FH " {\\footnotesize\\path|$ws->{inputs_ref}{sourceFilePath}|}\n\n\\vspace{\\baselineskip}" - if ($ws->{inputs_ref}{showSourceFile}); +sub write_problem_tex ($c, $renderedProblem, $FH) { + if ($c->req->param('showSourceFile')) { + my $sourceFilePath = $c->req->param('sourceFilePath'); + print $FH " {\\footnotesize\\path|$sourceFilePath|}\n\n\\vspace{\\baselineskip}"; + } - print $FH $rh_result->{text}; + print $FH $renderedProblem->{text}; # Write the correct answers if requested and there are answers to write. - if ($ws->{inputs_ref}{WWcorrectAns}) { - my @ans_entry_order = @{ $rh_result->{flags}{ANSWER_ENTRY_ORDER} // [] }; + if ($c->req->param('WWcorrectAns')) { + my @ans_entry_order = @{ $renderedProblem->{flags}{ANSWER_ENTRY_ORDER} // [] }; if (@ans_entry_order) { my $correctTeX = "\n\n\\vspace{\\baselineskip}\\par{\\small{\\it " @@ -250,8 +236,8 @@ sub write_problem_tex { for (@ans_entry_order) { $correctTeX .= "\\item\n\$\\displaystyle " - . ($rh_result->{answers}{$_}{correct_ans_latex_string} - || "\\text{$rh_result->{answers}{$_}{correct_ans}}") . "\$\n"; + . ($renderedProblem->{answers}{$_}{correct_ans_latex_string} + || "\\text{$renderedProblem->{answers}{$_}{correct_ans}}") . "\$\n"; } $correctTeX .= "\\end{itemize}}\\par\n"; @@ -262,9 +248,9 @@ sub write_problem_tex { # If there are any PG warnings and the view_problem_debugging_info parameter was set, # then append the warnings to end of the tex file. - if ($ws->{inputs_ref}{view_problem_debugging_info} && $rh_result->{pg_warnings}) { + if ($c->req->param('view_problem_debugging_info') && $renderedProblem->{pg_warnings}) { print $FH "\n\n\\vspace{\\baselineskip}\\par\n" . $c->maketext('Warning messages:') . "\n\\begin{itemize}\n"; - for (split("\n", $rh_result->{pg_warnings})) { + for (split("\n", $renderedProblem->{pg_warnings})) { print $FH "\\item \\verb|$_|\n"; } print $FH "\\end{itemize}\n"; diff --git a/lib/WeBWorK.pm b/lib/WeBWorK.pm index 36f79a7ca3..bc13739dfb 100644 --- a/lib/WeBWorK.pm +++ b/lib/WeBWorK.pm @@ -201,10 +201,10 @@ async sub dispatch ($c) { await WeBWorK::ContentGenerator::LoginProctor->new($c)->go; return 0; } - } elsif ($c->current_route ne 'instructor_rpc') { + } elsif ($c->current_route ne 'api') { # If any other page is opened, then revoke proctor authorization if it has been granted. # Otherwise the student will be able to re-enter the test without again obtaining proctor authorization. - # Do NOT do this for the instructor_rpc route. The only student usage of this route is to get the + # Do NOT do this for the api routes. The only api route allowed for students is the route that gets the # current server time during a gateway quiz, and that definitely should not revoke proctor # authorization. delete $c->authen->session->{proctor_authorization_granted}; diff --git a/lib/WeBWorK/Authen/CAS.pm b/lib/WeBWorK/Authen/CAS.pm index de1e5ad414..42d0a5aa03 100644 --- a/lib/WeBWorK/Authen/CAS.pm +++ b/lib/WeBWorK/Authen/CAS.pm @@ -54,11 +54,11 @@ sub get_credentials { $self->{external_auth} = 1; # This next part is necessary because some parts of webwork (e.g., - # WebworkWebservice.pm) need to replace the get_credentials() routine, + # the API) need to replace the get_credentials() routine, # but only replace the one in the parent class (out of caution, # presumably). Therefore, we end up here even when authenticating - # for WebworkWebservice.pm. This would cause authentication failures - # when authenticating javascript web service requests (e.g., the + # for the API. This would cause authentication failures + # when authenticating javascript api requests (e.g., the # Library Browser). if ($c->{rpc}) { diff --git a/lib/WeBWorK/ContentGenerator.pm b/lib/WeBWorK/ContentGenerator.pm index 9ab1063d64..8cb5f426d0 100644 --- a/lib/WeBWorK/ContentGenerator.pm +++ b/lib/WeBWorK/ContentGenerator.pm @@ -93,11 +93,9 @@ The method content() is called to send the page content to client. =cut async sub go ($c) { - my $ce = $c->ce; - # If grades are being passed back to the lti, then periodically update all of the # grades because things can get out of sync if instructors add or modify sets. - massUpdate($c) if $c->stash('courseID') && ref($c->db) && $ce->{LTIGradeMode}; + massUpdate($c) if $c->stash('courseID') && ref($c->db) && $c->ce->{LTIGradeMode}; # Check to determine if this is a problem set response. Individual content generators must check # $c->{invalidSet} and react appropriately. @@ -105,7 +103,7 @@ async sub go ($c) { # We only write to the activity log if it has been defined and if we are in a specific course. The latter check is # to prevent attempts to write to a course log file when viewing the top-level list of courses page. - writeCourseLog($ce, 'activity_log', $c->prepare_activity_entry) + writeCourseLog($c->ce, 'activity_log', $c->prepare_activity_entry) if ($c->stash('courseID') && $c->ce->{courseFiles}{logs}{activity_log}); my $tx = $c->render_later->tx; diff --git a/lib/WeBWorK/ContentGenerator/API.pm b/lib/WeBWorK/ContentGenerator/API.pm new file mode 100644 index 0000000000..ea1c6dcf34 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/API.pm @@ -0,0 +1,71 @@ +package WeBWorK::ContentGenerator::API; +use Mojo::Base 'WeBWorK::ContentGenerator', -signatures; + +=head1 NAME + +WeBWorK::ContentGenerator::API is the base class for API requests. + +=head1 Description + +This is the base class for API requests. All controller modules that handle API +requests should derive from this package. + +A controller module that derives from this package must also define a C +method that takes a C<$command> parameter and returns the code for that command +if it is available in the module. + +Note that it is expected that any API command render a valid JSON response. If +an error occurs, then the C method should be called and its result +returned. + +The current packages that derive from this module and that contain API calls +executed by this module are: + + WeBWorK::ContentGenerator::API::LibraryActions; + WeBWorK::ContentGenerator::API::SetActions; + WeBWorK::ContentGenerator::API::CourseActions; + WeBWorK::ContentGenerator::API::ProblemActions; + +=cut + +use WeBWorK::Utils::Logs qw(writeCourseLog); + +use WeBWorK::ContentGenerator::API::LibraryActions; +use WeBWorK::ContentGenerator::API::SetActions; +use WeBWorK::ContentGenerator::API::CourseActions; +use WeBWorK::ContentGenerator::API::ProblemActions; + +sub initializeRoute ($c, $routeCaptures) { + $c->{rpc} = 1; + + # Get the courseID from the request parameters. + $routeCaptures->{courseID} = $c->stash->{courseID} = $c->req->param('courseID') if $c->req->param('courseID'); + + return; +} + +sub go ($c) { + return $c->renderError($c->maketext('Authentication failed. Log in again to continue.')) + unless $c->authen->was_verified; + + writeCourseLog($c->ce, 'activity_log', $c->prepare_activity_entry) + if $c->stash('courseID') && $c->ce->{courseFiles}{logs}{activity_log}; + + my $command = $c->stash->{command}; + + for my $package ( + 'WeBWorK::ContentGenerator::API::LibraryActions', 'WeBWorK::ContentGenerator::API::SetActions', + 'WeBWorK::ContentGenerator::API::CourseActions', 'WeBWorK::ContentGenerator::API::ProblemActions' + ) + { + if (my $apiCall = $package->apiCall($command)) { return $package->new($c)->$apiCall; } + } + + return $c->renderError("Invalid api command $command."); +} + +sub renderError ($c, $errorMessage) { + return $c->render(json => { error => $errorMessage }); +} + +1; diff --git a/lib/WeBWorK/ContentGenerator/API/CourseActions.pm b/lib/WeBWorK/ContentGenerator/API/CourseActions.pm new file mode 100644 index 0000000000..e273118401 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/API/CourseActions.pm @@ -0,0 +1,531 @@ +package WeBWorK::ContentGenerator::API::CourseActions; +use Mojo::Base 'WeBWorK::ContentGenerator::API', -signatures; + +use Time::HiRes qw(gettimeofday); +use Date::Format; +use Data::Structure::Util qw(unbless); +use Mojo::JSON qw(false true); + +use WeBWorK::DB; +use WeBWorK::DB::Utils qw(initializeUserProblem); +use WeBWorK::Utils qw(cryptPassword); +use WeBWorK::Utils::CourseManagement qw(addCourse); +use WeBWorK::Utils::Files qw(surePathToFile path_is_subdir); +use WeBWorK::ConfigValues qw(getConfigValues); +use WeBWorK::Debug qw(debug); + +our @apiCalls = qw( + createCourse + listUsers + addUser + dropUser + deleteUser + editUser + changeUserPassword + getCourseSettings + updateSetting + saveFile + getCurrentServerTime +); + +sub apiCall ($invocant, $command) { + return (grep { $_ eq $command } @apiCalls) && $invocant->can($command); +} + +sub createCourse ($c) { + return $c->renderError('You do not have permission for the createCourse API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'create_and_delete_courses'); + + my $admin_ce = $c->ce; + my $db = $c->db; + my $authz = $c->authz; + + return $c->renderError('Course actions disabled by configuration.') + unless $admin_ce->{webservices}{enableCourseActions}; + + return $c->renderError('Course creation allowed only for admin course users.') + unless $admin_ce->{courseName} eq $admin_ce->{admin_course_id}; + + return $c->renderError("Course ID cannot exceed $admin_ce->{maxCourseIdLength} characters.") + if length($c->req->param('name')) > $admin_ce->{maxCourseIdLength}; + + # Bring up a minimal course environment for the new course. + my $ce = WeBWorK::CourseEnvironment->new({ courseName => $c->req->param('name') }); + + # Copy users from the admin course. + my @users; + for my $userID ($db->listUsers) { + push @users, [ $db->getUser($userID), $db->getPassword($userID), $db->getPermissionLevel($userID) ] + if $authz->hasPermissions($userID, 'create_and_delete_courses'); + } + + # Try to actually create the course. + eval { + addCourse( + courseID => $c->req->param('name'), + ce => $ce, + users => \@users + ); + addLog($ce, 'New course created: ' . $c->req->param('name')); + }; + return $c->renderError("Unable to create course: $@") if $@; + + return $c->render(json => { message => 'New course ' . $c->req->param('name') . ' created.' }); +} + +sub listUsers ($c) { + return $c->renderError('You do not have permission for the listUsers API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + my $db = $c->db; + my $ce = $c->ce; + + my @userInfo = map { unbless($_) } $db->getUsersWhere({ user_id => { not_like => 'set_id:%' } }); + my $numGlobalSets = $db->countGlobalSets; + + for my $user (@userInfo) { + my $permissionLevel = $db->getPermissionLevel($user->{user_id}); + $user->{permission} = $permissionLevel->{permission}; + + $user->{num_user_sets} = $db->countUserSets($user->{user_id}) . '/' . $numGlobalSets; + + my $Key = $db->getKey($user->{user_id}); + $user->{login_status} = $Key && time <= $Key->timestamp + $ce->{sessionTimeout} ? 'active' : 'inactive'; + } + + return $c->render(json => \@userInfo); +} + +sub addUser ($c) { + return $c->renderError('You do not have permission for the addUser API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + my $db = $c->db; + my $ce = $c->ce; + + return $c->renderError('Course actions disabled by configuration.') + unless $ce->{webservices}{enableCourseActions}; + + return $c->renderError('The user_id parameter is required.') + unless $c->req->param('user_id') && $c->req->param('user_id') =~ /\S/; + + my $user_id = $c->req->param('user_id') =~ s/^\s*|\s*$//gr; + + my $response = {}; + + my $olduser = $db->getUser($c->req->param('user_id')); + my $permission; + if ($olduser) { + if ($olduser->status ne $ce->{statuses}{Enrolled}{abbrevs}[0]) { + # Re-enroll the existing user. + $olduser->status($ce->{statuses}{Enrolled}{abbrevs}[0]); + $db->putUser($olduser); + addLog($ce, 'User ' . $c->req->param('user_id') . " re-enrolled in $ce->{courseName}"); + + $permission = $db->getPermissionLevel($c->req->param('user_id')); + + $response->{user_added} = true; + $response->{message} = 'User ' . $c->req->param('user_id') . " re-enrolled in $ce->{courseName}."; + } else { + $response->{message} = 'User ' . $c->req->param('user_id') . " already enrolled in $ce->{courseName}."; + } + } else { + # Add a new user. + my $ce = $c->ce; + + my $new_student = + $db->newUser(user_id => $c->req->param('user_id'), status => $ce->{statuses}{Enrolled}{abbrevs}[0]); + $new_student->first_name($c->req->param('first_name')) if $c->req->param('first_name'); + $new_student->last_name($c->req->param('last_name')) if $c->req->param('last_name'); + $new_student->student_id($c->req->param('student_id')) if defined $c->req->param('student_id'); + $new_student->email_address($c->req->param('email_address')) if $c->req->param('email_address'); + $new_student->recitation($c->req->param('recitation')) if defined $c->req->param('recitation'); + $new_student->section($c->req->param('section')) if defined $c->req->param('section'); + $new_student->comment($c->req->param('comment')) if $c->req->param('comment'); + + my $cryptedpassword = ''; + if ($c->req->param('password')) { + $cryptedpassword = cryptPassword($c->req->param('password') =~ s/^\s*|\s*$//gr); + } elsif ($new_student->student_id) { + $cryptedpassword = cryptPassword($new_student->student_id); + } + my $password = $db->newPassword(user_id => $c->req->param('user_id')); + $password->password($cryptedpassword); + + $permission = $c->req->param('permission') // 0; + if (defined($ce->{userRoles}{$permission})) { + $permission = $db->newPermissionLevel( + user_id => $c->req->param('user_id'), + permission => $ce->{userRoles}{$permission} + ); + } else { + $permission = $db->newPermissionLevel( + user_id => $c->req->param('user_id'), + permission => $ce->{userRoles}{student} + ); + } + + # Commit changes to db + $db->addUser($new_student); + $db->addPassword($password); + eval { $db->addPermissionLevel($permission); }; + + $response->{user_added} = true; + $response->{message} = 'User ' . $c->req->param('user_id') . " added to $ce->{courseName}."; + addLog($ce, 'User ' . $c->req->param('user_id') . " added to $ce->{courseName}"); + } + + # Assign all visible sets to the user if requested. + if ($c->req->param('assign_visible_sets')) { + $response->{sets_assigned} = assignVisibleSets($db, $c->req->param('user_id')) ? false : true; + $response->{message} .= ' Visible sets assigned to ' . $c->req->param('user_id') . '.'; + } + + return $c->render(json => $response); +} + +sub dropUser ($c) { + return $c->renderError('You do not have permission for the dropUser API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + my $db = $c->db; + my $ce = $c->ce; + + return $c->renderError('Course actions disabled by configuration.') unless $ce->{webservices}{enableCourseActions}; + + return $c->renderError('The user_id parameter is required.') + unless $c->req->param('user_id') && $c->req->param('user_id') =~ /\S/; + + my $user = $db->getUser($c->req->param('user_id')); + return $c->renderError('Could not find ' . $c->req->param('user_id') . " in $ce->{courseName}.") + unless $user; + + $user->status($ce->{statuses}{Drop}{abbrevs}[0]); + $db->putUser($user); + addLog($ce, 'User ' . $c->req->param('user_id') . " dropped from $ce->{courseName}"); + + return $c->render(json => { message => 'User ' . $c->req->param('user_id') . " dropped from $ce->{courseName}" }); +} + +sub deleteUser ($c) { + return $c->renderError('You do not have permission for the deleteUser API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + my $db = $c->db; + my $ce = $c->ce; + + return $c->renderError('Course actions disabled by configuration.') unless $ce->{webservices}{enableCourseActions}; + + return $c->renderError('The user_id parameter is required.') + unless $c->req->param('user_id') && $c->req->param('user_id') =~ /\S/; + + return $c->renderError('Could not find ' . $c->req->param('user_id') . " in $ce->{courseName}.") + unless $db->getUser($c->req->param('user_id')); + + return $c->renderError('You cannot delete yourself from the course.') + if $c->req->param('user_id') eq $c->req->param('user'); + + my $del = $db->deleteUser($c->req->param('user_id')); + return $c->renderError('User ' . $c->req->param('user_id') . ' could not be deleted.') unless $del; + + addLog($ce, 'User ' . $c->req->param('user_id') . " deleted from $ce->{courseName}"); + return $c->render(json => { message => 'User ' . $c->req->param('user_id') . " deleted from $ce->{courseName}" }); +} + +sub editUser ($c) { + return $c->renderError('You do not have permission for the editUser API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + my $db = $c->db; + my $ce = $c->ce; + + return $c->renderError('Course actions disabled by configuration.') unless $ce->{webservices}{enableCourseActions}; + + return $c->renderError('The user_id parameter is required.') + unless $c->req->param('user_id') && $c->req->param('user_id') =~ /\S/; + + my $user = $db->getUser($c->req->param('user_id')); + return $c->renderError('User ' . $c->req->param('user_id') . ' not found.') unless $user; + + # Get the permission level, so that it can be verified that the permission level of the + # user being edited is less than or equal to that of the one doing the editing. + my $callerPermission = $db->getPermissionLevel($c->req->param('user')); + my $permissionLevel = $db->getPermissionLevel($c->req->param('user_id')); + + return $c->renderError('You do not have permission to edit ' . $c->req->param('user_id')) + unless $callerPermission && $permissionLevel && $callerPermission->permission >= $permissionLevel->permission; + + my $response = {}; + + for my $field ($user->NONKEYFIELDS()) { + $user->$field($c->req->param($field)) if defined $c->req->param($field); + } + $db->putUser($user); + $response->{message} = 'User data updated.'; + $response->{user} = unbless($user); + + if (defined $c->req->param('permission') && $c->req->param('permission') =~ /\d*/) { + if ($c->req->param('user_id') eq $c->req->param('user')) { + $response->{message} .= ' You cannot change your own permissions.'; + $response->{permission_changed} = false; + } else { + $permissionLevel->permission($c->req->param('permission')); + $db->putPermissionLevel($permissionLevel); + $response->{message} .= ' Permissions updated.'; + $response->{user}{permission} = $permissionLevel->{permission}; + } + } else { + $response->{permission_changed} = false; + } + + $response->{password_changed} = false; + + # If the new_password parameter is set and not equal to the empty string and not all spaces, + # then change the password or set the password if it is not set. + if (defined $c->req->param('new_password') && $c->req->param('new_password') =~ /\S/) { + my $password = cryptPassword($c->req->param('new_password') =~ s/^\s*|\s*$//gr); + my $dbPassword = $db->getPassword($c->req->param('user_id')); + if ($dbPassword) { + $dbPassword->password($password); + $db->putPassword($dbPassword); + } else { + $dbPassword = $db->newPassword(user_id => $c->req->param('user_id'), password => $password); + $db->addPassword($dbPassword); + } + $response->{message} .= ' Password changed.'; + $response->{password_changed} = true; + } + + addLog($ce, "User edited: $response->{message}"); + return $c->render(json => $response); +} + +sub changeUserPassword ($c) { + return $c->renderError('You do not have permission for the changeUserPassword API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + my $db = $c->db; + my $ce = $c->ce; + + return $c->renderError('Course actions disabled by configuration.') unless $ce->{webservices}{enableCourseActions}; + + return $c->renderError('The user_id parameter is required.') + unless $c->req->param('user_id') && $c->req->param('user_id') =~ /\S/; + return $c->renderError('The new_password parameter is required.') + unless $c->req->param('new_password') && $c->req->param('new_password') =~ /\S/; + + my $user = $db->getUser($c->req->param('user_id')); + return $c->renderError('User ' . $c->req->param('user_id') . ' not found.') unless $user; + + # Get the permission level, so that it can be verified that the permission level of the user being edited is less + # than or equal to that of the one doing the editing. + my $callerPermission = $db->getPermissionLevel($c->req->param('user')); + my $permissionLevel = $db->getPermissionLevel($c->req->param('user_id')); + return $c->renderError('You do not have permission to change the password for ' . $c->req->param('user_id')) + unless $callerPermission + && $permissionLevel + && $callerPermission->{permission} >= $permissionLevel->{permission}; + + my $password = cryptPassword($c->req->param('new_password') =~ s/^\s*|\s*$//gr); + + my $dbPassword = $db->getPassword($user->user_id); + if ($dbPassword) { + $dbPassword->password($password); + $db->putPassword($dbPassword); + } else { + $dbPassword = $db->newPassword(user_id => $c->req->param('user_id'), password => $password); + $db->addPassword($dbPassword); + } + + addLog($ce, 'New password set for ' . $c->req->param('user_id')); + return $c->render(json => { message => 'New password set for ' . $c->req->param('user_id') }); +} + +sub addLog ($ce, $msg) { + return unless $ce->{webservices}{enableCourseActionsLog}; + + my ($sec, $msec) = gettimeofday; + my $date = time2str("%a %b %d %H:%M:%S.$msec %Y", $sec); + + if (open my $f, '>>', $ce->{webservices}{courseActionsLogfile}) { + print $f "[$date] $msg\n"; + close $f; + } else { + debug(qq{Error: Unable to open web services log file "$ce->{webservices}{courseActionsLogfile}": $!}); + } + return; +} + +sub assignVisibleSets { + my ($db, $userID) = @_; + my @globalSetIDs = $db->listGlobalSets; + my @GlobalSets = $db->getGlobalSets(@globalSetIDs); + + my $i = -1; + for my $GlobalSet (@GlobalSets) { + $i++; + if (not defined $GlobalSet) { + debug("Record not found for global set $globalSetIDs[$i]"); + next; + } + if (!$GlobalSet->visible) { + next; + } + + my $setID = $GlobalSet->set_id; + my $UserSet = $db->newUserSet; + $UserSet->user_id($userID); + $UserSet->set_id($setID); + my @results; + my $set_assigned = 0; + eval { $db->addUserSet($UserSet) }; + + return 0 if $@ && !WeBWorK::DB::Ex::RecordExists->caught; + + my @GlobalProblems = grep { defined $_ } $db->getAllGlobalProblems($setID); + for my $GlobalProblem (@GlobalProblems) { + my $seed = int(rand(2423)) + 36; + my $UserProblem = $db->newUserProblem; + $UserProblem->user_id($userID); + $UserProblem->set_id($GlobalProblem->set_id); + $UserProblem->problem_id($GlobalProblem->problem_id); + initializeUserProblem($UserProblem, $seed); + eval { $db->addUserProblem($UserProblem) }; + return 0 if $@ && !WeBWorK::DB::Ex::RecordExists->caught; + } + } + + return 0; +} + +sub getCourseSettings ($c) { + return $c->renderError('You do not have permission for the getCourseSettings API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + my $ce = $c->ce; + my $ConfigValues = getConfigValues($ce); + + for my $oneConfig (@$ConfigValues) { + for my $hash (@$oneConfig) { + next unless ref $hash eq 'HASH'; + my $value; + if ($hash->{type} eq 'setting') { + $value = $c->db->getSettingValue($hash->{var}); + } elsif (defined $hash->{var}) { + my @keys = $hash->{var} =~ m/([^{}]+)/g; + next unless @keys; + + $value = $ce; + for (@keys) { $value = $value->{$_}; } + } + $hash->{value} = $value if defined $value; + } + } + + push( + @$ConfigValues, + [ + 'tz_abbr', + DateTime::TimeZone->new(name => $ce->{siteDefaults}->{timezone})->short_name_for_datetime(DateTime->now) + ] + ); + + return $c->render(json => $ConfigValues); +} + +sub updateSetting ($c) { + return $c->renderError('You do not have permission for the updateSetting API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'manage_course_files'); + + my $ce = $c->ce; + + # FIXME: There is no check in this method that the var and value passed in are valid. + my $setVar = $c->req->param('var'); + my $setValue = $c->req->param('value'); + + my $filename = "$ce->{courseDirs}{root}/simple.conf"; + + my $fileoutput = "#!perl +# This file is automatically generated by WeBWorK's web-based +# configuration module. Do not make changes directly to this +# file. It will be overwritten the next time configuration +# changes are saved.\n\n"; + + # Read in the file + open(my $DAT, '<', $filename) + or return $c->renderError("Unable to read $filename. " + . "Ensure that the file exists and the server has write permission for this file."); + my @raw_data = <$DAT>; + close($DAT); + + my $varFound = 0; + + for my $line (@raw_data) { + chomp $line; + if ($line =~ /^\$/) { + my @tmp = split(/\$/, $line); + my ($var, $value) = split(/\s+=\s+/, $tmp[1]); + if ($var eq $setVar) { + $fileoutput .= "\$$var = $setValue;\n"; + $varFound = 1; + } else { + # The value includes the semicolon that hopefully was in the file. + $fileoutput .= "\$$var = $value\n"; + } + } + } + + $fileoutput .= "\$$setVar = $setValue;\n" unless $varFound; + + open(my $OUTPUTFILE, '>', $filename) + or return $c->renderError( + "Unable to write to $filename. Ensure that the server has write permission for this file."); + print $OUTPUTFILE $fileoutput; + close $OUTPUTFILE; + + return $c->render(json => { message => 'Successfully updated course setting' }); +} + +# This saves a file to the course's templates directory. +sub saveFile ($c) { + return $c->renderError('You do not have permission for the saveFile API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_problem_sets'); + + my $ce = $c->ce; + + my $outputFilePath = $c->req->param('outputFilePath'); + + if ($outputFilePath && $outputFilePath =~ /\S/) { + return $c->renderError($c->maketext( + 'File not saved. The file "[_1]" is not contained in the templates directory!', + $outputFilePath + )) + unless path_is_subdir($outputFilePath, $ce->{courseDirs}{templates}, 1); + + $outputFilePath = "$ce->{courseDirs}{templates}/$outputFilePath" unless $outputFilePath =~ m|^/|; + + # Make sure any missing directories are created. + surePathToFile($ce->{courseDirs}{templates}, $outputFilePath); + + # Save the file. + open(my $outfile, '>:encoding(UTF-8)', $outputFilePath) + or + return $c->renderError($c->maketext('File not saved. Failed to open "[_1]" for writing.', $outputFilePath)); + print $outfile $c->req->param('fileContents'); + close $outfile; + } + + return $c->render( + json => { + message => + $c->maketext('Saved to file "[_1]"', $outputFilePath =~ s/$ce->{courseDirs}{templates}/[TMPL]/r) + } + ); +} + +# Note that no permission is required to get the current server time. The user only needs to be authenticated. +sub getCurrentServerTime ($c) { + return $c->render(json => { currentServerTime => $c->submitTime }); +} + +1; diff --git a/lib/WeBWorK/ContentGenerator/API/LibraryActions.pm b/lib/WeBWorK/ContentGenerator/API/LibraryActions.pm new file mode 100644 index 0000000000..c9ee5c953c --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/API/LibraryActions.pm @@ -0,0 +1,155 @@ +package WeBWorK::ContentGenerator::API::LibraryActions; +use Mojo::Base 'WeBWorK::ContentGenerator::API', -signatures; + +use File::Find; + +use WeBWorK::Utils::ListingDB; + +our @apiCalls = qw( + listLib + searchLib + getProblemTags + setProblemTags +); + +sub apiCall ($invocant, $command) { + return (grep { $_ eq $command } @apiCalls) && $invocant->can($command); +} + +# Idea from http://www.perlmonks.org/index.pl?node=How%20to%20map%20a%20directory%20tree%20to%20a%20perl%20hash%20tree +sub build_tree ($dirPath) { + my $tree = {}; + my $node = $tree; + my @s; + find( + { + wanted => sub { + unless ($File::Find::dir =~ /.svn/ || $File::Find::name =~ /.svn/) { + $node = (pop @s)->[1] while @s && $File::Find::dir ne $s[-1][0]; + return $node->{$_} = -s if -f; + push @s, [ $File::Find::name, $node ]; + $node = $node->{$_} = {}; + } + }, + follow_fast => 1 + }, + $dirPath + ); + return { $dirPath => $tree->{'.'} }; +} + +sub listLib ($c) { + return $c->renderError('You do not have permission for the listLib API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + my $rh = $c->req->params->to_hash; + $rh->{library_name} //= 'Library'; + $rh->{library_name} =~ s|^/||; + my $dirPath = $c->ce->{courseDirs}{templates} . '/' . $rh->{library_name}; + my $maxdepth = $rh->{maxdepth} // 2; + my $dirPath2 = $dirPath . (($rh->{dirPath}) ? '/' . $rh->{dirPath} : ''); + + my @tare = $dirPath2 =~ m|/|g; + my @outListLib; + my %libDirectoryList; + + # Counts depth below the current directory. + my $depthfinder = sub { + my $path = shift; + my @count = $path =~ m|/|g; + my $depth = @count; + return $depth - @tare; + }; + + # Find .pg files. + my $wanted = sub { + my $name = $File::Find::name; + push(@outListLib, $name) if $name =~ /\.pg$/; + }; + + my $wanted_directory = sub { + my $dir = $File::Find::dir; + $File::Find::prune = 1 if $depthfinder->($dir) > $maxdepth; + if ($dir =~ /\S/) { + $dir =~ s|^$dirPath2/*||; + $libDirectoryList{$dir} = {}; + } + }; + + my $command = $rh->{command} // 'all'; + + if ($command eq 'all') { + find({ wanted => $wanted, follow_fast => 1 }, $dirPath); + return $c->render(json => [ sort @outListLib ]); + } + + if ($command eq 'dirOnly') { + if (-e $dirPath2 && $dirPath2 !~ m|//|) { + find({ wanted => $wanted_directory, follow_fast => 1 }, $dirPath2); + delete $libDirectoryList{''}; + return $c->render(json => \%libDirectoryList); + } else { + return $c->renderError("Can't open directory $dirPath2"); + } + } + + return $c->render(json => build_tree($dirPath)) if $command eq 'buildtree'; + + if ($command eq 'files') { + if (-e $dirPath2 && $dirPath2 !~ m|//|) { + find({ wanted => $wanted, follow_fast => 1 }, $dirPath2); + return $c->render(json => [ sort @outListLib ]); + } else { + return $c->renderError("Can't open directory $dirPath2"); + } + } + + return $c->renderError("Unrecognized command $command"); +} + +# API for searching the OPL database +sub searchLib ($c) { + return $c->renderError('You do not have permission for the searchLib API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + my $rh = $c->req->params->to_hash; + $c->{level} = [ split(//, $rh->{library_levels}) ] if $rh->{library_levels}; + return $c->render(json => [ WeBWorK::Utils::ListingDB::getDBTextbooks($c) ]) if $rh->{command} eq 'getDBTextbooks'; + return $c->render(json => [ WeBWorK::Utils::ListingDB::getAllDBsubjects($c) ]) + if $rh->{command} eq 'getAllDBsubjects'; + return $c->render(json => [ WeBWorK::Utils::ListingDB::getAllDBchapters($c) ]) + if $rh->{command} eq 'getAllDBchapters'; + return $c->render( + json => [ + map { $c->ce->{courseDirs}{templates} . "/$_->{filepath}" } WeBWorK::Utils::ListingDB::getDBListings($c) + ] + ) if $rh->{command} eq 'getDBListings'; + return $c->render(json => [ WeBWorK::Utils::ListingDB::getAllDBsections($c) ]) + if $rh->{command} eq 'getSectionListings'; + return $c->render(json => [ WeBWorK::Utils::ListingDB::countDBListings($c) ]) + if $rh->{command} eq 'countDBListings'; + + return $c->renderError("Unrecognized command $rh->{command}"); +} + +sub getProblemTags ($c) { + return $c->renderError('You do not have permission for the getProblemTags API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render(json => WeBWorK::Utils::ListingDB::getProblemTags($c->req->param('command'))); +} + +sub setProblemTags ($c) { + return $c->renderError('You do not have permission for the setProblemTags API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_tags'); + + # result is [success, message] with success = 0 or 1 + my $result = WeBWorK::Utils::ListingDB::setProblemTags( + $c->req->param('command'), $c->req->param('library_subject'), + $c->req->param('library_chapter'), $c->req->param('library_section'), + $c->req->param('library_levels'), $c->req->param('library_status') + ); + return $c->render(json => { message => $result->[1] }); +} + +1; diff --git a/lib/WeBWorK/ContentGenerator/API/ProblemActions.pm b/lib/WeBWorK/ContentGenerator/API/ProblemActions.pm new file mode 100644 index 0000000000..bad624bd37 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/API/ProblemActions.pm @@ -0,0 +1,168 @@ +package WeBWorK::ContentGenerator::API::ProblemActions; +use Mojo::Base 'WeBWorK::ContentGenerator::API', -signatures; + +use Data::Structure::Util qw(unbless); + +use WeBWorK::PG::Tidy qw(pgtidy); +use WeBWorK::PG::ConvertToPGML qw(convertToPGML); +use WeBWorK::PG::Critic qw(critiquePGCode); + +our @apiCalls = qw( + getUserProblem + putUserProblem + putProblemVersion + putPastAnswer + tidyPGCode + convertCodeToPGML + runPGCritic +); + +sub apiCall ($invocant, $command) { + return (grep { $_ eq $command } @apiCalls) && $invocant->can($command); +} + +sub getUserProblem ($c) { + return $c->renderError('You do not have permission for the getUserProblem API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render( + json => unbless($c->db->getUserProblem( + $c->req->param('user_id'), + $c->req->param('set_id'), + $c->req->param('problem_id') + )) + ); +} + +sub putUserProblem ($c) { + return $c->renderError('You do not have permission for the putUserProblem API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'problem_grader'); + + my $userProblem = + $c->db->getUserProblem($c->req->param('user_id'), $c->req->param('set_id'), $c->req->param('problem_id')); + return $c->renderError('User problem not found.') unless $userProblem; + + if ($c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data')) { + for ( + 'source_file', 'value', 'max_attempts', 'showMeAnother', + 'showMeAnotherCount', 'prPeriod', 'prCount', 'problem_seed', + 'attempted', 'last_answer', 'num_correct', 'num_incorrect', + 'att_to_open_children', 'counts_parent_grade', 'flags' + ) + { + $userProblem->{$_} = $c->req->param($_) if defined $c->req->param($_); + } + } + + # The status and sub_status are the only things that users with the problem_grader permission can change. + # This method cannot be called without the problem_grader permission. + $userProblem->{status} = $c->req->param('status') if defined $c->req->param('status'); + $userProblem->{sub_status} = $c->req->param('sub_status') if defined $c->req->param('sub_status'); + + # Remove the needs_grading flag if the mark_graded parameter is set. + $userProblem->{flags} =~ s/:needs_grading$// if $c->req->param('mark_graded'); + + eval { $c->db->putUserProblem($userProblem) }; + return $c->renderError("putUserProblem: $@") if $@; + + return $c->render(json => unbless($userProblem)); +} + +sub putProblemVersion ($c) { + return $c->renderError('You do not have permission for the putProblemVersion API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'problem_grader'); + + my $problemVersion = $c->db->getProblemVersion( + $c->req->param('user_id'), $c->req->param('set_id'), + $c->req->param('version_id'), $c->req->param('problem_id') + ); + return $c->renderError('Problem version not found.') unless $problemVersion; + + if ($c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data')) { + for ( + 'source_file', 'value', 'max_attempts', 'showMeAnother', + 'showMeAnotherCount', 'prPeriod', 'prCount', 'problem_seed', + 'attempted', 'last_answer', 'num_correct', 'num_incorrect', + 'att_to_open_children', 'counts_parent_grade', 'flags' + ) + { + $problemVersion->{$_} = $c->req->param($_) if defined $c->req->param($_); + } + } + + # The status and sub_status are the only things that users with the problem_grader permission can change. + # This method cannot be called without the problem_grader permission. + $problemVersion->{status} = $c->req->param('status') if defined $c->req->param('status'); + $problemVersion->{sub_status} = $c->req->param('sub_status') if defined $c->req->param('sub_status'); + + # Remove the needs_grading flag if the mark_graded parameter is set. + $problemVersion->{flags} =~ s/:needs_grading$// if $c->req->param('mark_graded'); + + eval { $c->db->putProblemVersion($problemVersion) }; + return $c->renderError("putProblemVersion: $@") if $@; + + return $c->render(json => unbless($problemVersion)); +} + +sub putPastAnswer ($c) { + return $c->renderError('You do not have permission for the putPastAnswer API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'problem_grader'); + + my $pastAnswer = $c->db->getPastAnswer($c->req->param('answer_id')); + return $c->renderError('Past answer not found.') unless $pastAnswer; + + $pastAnswer->{user_id} = $c->req->param('user_id') if $c->req->param('user_id'); + + if ($c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data')) { + for ( + 'set_id', 'problem_id', 'source_file', 'timestamp', + 'scores', 'answer_string', 'comment_string', 'problem_seed' + ) + { + $pastAnswer->{$_} = $c->req->param('$_') if defined $c->req->param('$_'); + } + } + + # The comment_string is the only thing that users with the problem_grader permission can change. + # This method cannot be called without the problem_grader permission. + $pastAnswer->{comment_string} = $c->req->param('comment_string') if defined $c->req->param('comment_string'); + + eval { $c->db->putPastAnswer($pastAnswer) }; + return $c->renderError("putPastAnswer $@") if $@; + + return $c->render(json => unbless($pastAnswer)); +} + +sub tidyPGCode ($c) { + return $c->renderError('You do not have permission for the tidyPGCode API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + local @ARGV = (); + my $result = + pgtidy(source => \($c->req->param('pgCode')), destination => \(my $tidiedPGCode), errorfile => \(my $errors)); + + return $c->render(json => { tidiedPGCode => $tidiedPGCode, status => $result, errors => $errors }); +} + +sub convertCodeToPGML ($c) { + return $c->renderError('You do not have permission for the convertCodeToPGML API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render(json => convertToPGML($c->req->param('pgCode'))); +} + +sub runPGCritic ($c) { + return $c->renderError('You do not have permission for the runPGCritic API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render( + json => { + html => $c->render_to_string( + template => 'ContentGenerator/Instructor/PGProblemEditor/pg_critic', + violations => [ critiquePGCode($c->req->param('pgCode')) ] + ) + } + ); +} + +1; diff --git a/lib/WeBWorK/ContentGenerator/API/SetActions.pm b/lib/WeBWorK/ContentGenerator/API/SetActions.pm new file mode 100644 index 0000000000..81965f9656 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/API/SetActions.pm @@ -0,0 +1,426 @@ +package WeBWorK::ContentGenerator::API::SetActions; +use Mojo::Base 'WeBWorK::ContentGenerator::API', -signatures; + +use Mojo::JSON qw(from_json); +use Data::Structure::Util qw(unbless); + +use WeBWorK::Utils qw(max); +use WeBWorK::Utils::Instructor qw(assignProblemToAllSetUsers assignSetToGivenUsers); +use WeBWorK::Utils::JITAR qw(seq_to_jitar_id jitar_id_to_seq); + +our @apiCalls = qw( + listGlobalSets + listGlobalSetProblems + getSets + getUserSets + getSet + updateSetProperties + listSetUsers + createNewSet + assignSetToUsers + deleteProblemSet + reorderProblems + updateProblem + updateUserSet + getSetUserSets + saveUserSets + unassignSetFromUsers + addProblem + deleteProblem +); + +sub apiCall ($invocant, $command) { + return (grep { $_ eq $command } @apiCalls) && $invocant->can($command); +} + +sub listGlobalSets ($c) { + return $c->renderError('You do not have permission for the listGlobalSets API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render(json => [ $c->db->listGlobalSets ]); +} + +sub listGlobalSetProblems ($c) { + return $c->renderError('You do not have permission for the listGlobalSetProblems API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render( + json => [ map { unbless($_) } $c->db->getGlobalProblemsWhere({ set_id => $c->req->param('set_id') }) ]); +} + +# FIXME: Use the remainder of these API calls very carefully. Many of these are not well thought out. Parameters are +# rarely verified, and some of them can do some very damaging things if not used correctly. None of them are used by +# webwork2 at this point. If any of them ever are, make sure their implementations are fixed. + +# This returns all problem sets of a course. +sub getSets ($c) { + return $c->renderError('You do not have permission for the getSets API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + my $db = $c->db; + + my @sets = map { unbless($_) } $db->getGlobalSetsWhere; + + for my $set (@sets) { + $set->{assigned_users} = [ $db->listSetUsers($set->{set_id}) ]; + } + + return $c->render(json => \@sets); +} + +# This returns all problem sets of a course for a given user. +# The set is stored in the set_id and the user in user_id +sub getUserSets ($c) { + return $c->renderError('You do not have permission for the getUserSets API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render( + json => [ map { unbless($_) } $c->db->getGlobalSets($c->db->listUserSets($c->req->param('user_id'))) ]); +} + +# This returns a single problem set with name stored in set_id +sub getSet ($c) { + return $c->renderError('You do not have permission for the getSet API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render(json => unbless($c->db->getGlobalSet($c->req->param('set_id')))); +} + +sub updateSetProperties ($c) { + return $c->renderError('You do not have permission for the updateSetProperties API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_problem_sets'); + + my $db = $c->db; + + my $set = $db->getGlobalSet($c->req->param('set_id')); + $set->set_header($c->req->param('set_header')); + $set->hardcopy_header($c->req->param('hardcopy_header')); + $set->open_date($c->req->param('open_date')); + $set->due_date($c->req->param('due_date')); + $set->answer_date($c->req->param('answer_date')); + $set->visible($c->req->param('visible')); + $set->enable_reduced_scoring($c->req->param('enable_reduced_scoring')); + $set->assignment_type($c->req->param('assignment_type')); + $set->attempts_per_version($c->req->param('attempts_per_version')); + $set->time_interval($c->req->param('time_interval')); + $set->versions_per_interval($c->req->param('versions_per_interval')); + $set->version_time_limit($c->req->param('version_time_limit')); + $set->version_creation_time($c->req->param('version_creation_time')); + $set->problem_randorder($c->req->param('problem_randorder')); + $set->version_last_attempt_time($c->req->param('version_last_attempt_time')); + $set->problems_per_page($c->req->param('problems_per_page')); + $set->hide_score($c->req->param('hide_score')); + $set->hide_score_by_problem($c->req->param('hide_score_by_problem')); + $set->hide_work($c->req->param('hide_work')); + $set->time_limit_cap($c->req->param('time_limit_cap')); + $set->restrict_ip($c->req->param('restrict_ip')); + $set->relax_restrict_ip($c->req->param('relax_restrict_ip')); + $set->restricted_login_proctor($c->req->param('restricted_login_proctor')); + + $db->putGlobalSet($set); + + # Update the assigned_users list. The following seems to work if there are only additions or subtractions from the + # assigned_users field. Perhaps a better way to do this is to check users that are new or missing and add or delete + # them. + + my @usersForTheSetBefore = $db->listSetUsers($c->req->param('set_id')); + + my @usersForTheSetNow = split(/,/, $c->req->param('assigned_users')); + + for my $user (@usersForTheSetNow) { + if (!(grep {/^$user$/} @usersForTheSetBefore)) { + my $userSet = $db->newUserSet; + $userSet->user_id($user); + $userSet->set_id($c->req->param('set_id')); + $db->addUserSet($userSet); + } + } + + for my $user (@usersForTheSetBefore) { + if (!(grep {/^$user$/} @usersForTheSetNow)) { + $db->deleteUserSet($user, $c->req->param('set_id')); + } + } + + return $c->render( + json => { updated_set => unbless($set), message => 'Successfully updated set ' . $c->req->param('set_id') } + ); +} + +sub listSetUsers ($c) { + return $c->renderError('You do not have permission for the listSetUsers API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + return $c->render(json => [ $c->db->listSetUsers($c->req->param('set_id')) ]); +} + +sub createNewSet ($c) { + return $c->renderError('You do not have permission for the createNewSet API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_problem_sets'); + + my $newSetName = $c->req->param('set_id'); + + return $c->renderError('Invalid set name') if $newSetName !~ /^[\w .-]*$/; + + $newSetName =~ s/\s/_/g; + + my $db = $c->db; + + return $c->renderError("The set name '$newSetName' already exists. " + . 'Pick a different name if you would like to create a new set.') + if defined($db->getGlobalSet($newSetName)); + + my $now = time; + my $newSetRecord = $db->newGlobalSet; + $newSetRecord->set_id($newSetName); + $newSetRecord->set_header('defaultHeader'); + $newSetRecord->hardcopy_header('defaultHeader'); + $newSetRecord->open_date($c->req->param('open_date') // $now); + $newSetRecord->due_date($c->req->param('due_date') // ($now + 1209600)); + $newSetRecord->answer_date($c->req->param('answer_date') // ($now + 1209600)); + $newSetRecord->reduced_scoring_date($c->req->param('reduced_scoring_date') // ($now + 1209600)); + $newSetRecord->visible($c->req->param('visible') // 1); + $newSetRecord->enable_reduced_scoring($c->req->param('enable_reduced_scoring') // 0); + $newSetRecord->assignment_type($c->req->param('assignment_type') // 'default'); + $newSetRecord->description($c->req->param('description')); + $newSetRecord->restricted_release($c->req->param('restricted_release')); + $newSetRecord->restricted_status($c->req->param('restricted_status') // 1); + $newSetRecord->attempts_per_version($c->req->param('attempts_per_version') // 0); + $newSetRecord->time_interval($c->req->param('time_interval') // 0); + $newSetRecord->versions_per_interval($c->req->param('versions_per_interval') // 0); + $newSetRecord->version_time_limit($c->req->param('version_time_limit') // 0); + $newSetRecord->version_creation_time($c->req->param('version_creation_time')); + $newSetRecord->problem_randorder($c->req->param('problem_randorder')); + $newSetRecord->version_last_attempt_time($c->req->param('version_last_attempt_time')); + $newSetRecord->problems_per_page($c->req->param('problems_per_page') // 0); + $newSetRecord->hide_score($c->req->param('hide_score')); + $newSetRecord->hide_score_by_problem($c->req->param('hide_score_by_problem')); + $newSetRecord->hide_work($c->req->param('hide_work')); + $newSetRecord->time_limit_cap($c->req->param('time_limit_cap')); + $newSetRecord->restrict_ip($c->req->param('restrict_ip') // 'No'); + $newSetRecord->relax_restrict_ip($c->req->param('relax_restrict_ip') // 'No'); + $newSetRecord->hide_hint($c->req->param('hide_hint') // 0); + $newSetRecord->restrict_prob_progression($c->req->param('restrict_prob_progression') // 0); + $newSetRecord->email_instructor($c->req->param('email_instructor') // 0); + + $db->addGlobalSet($newSetRecord); + my $message = "Successfully created new set $newSetName."; + + my $selfassign = $c->req->param('selfassign') // ''; + if ($selfassign && $selfassign !~ /false/i) { + my $userSet = $db->newUserSet; + $userSet->user_id($c->req->param('user')); + $userSet->set_id($newSetName); + $db->addUserSet($userSet); + $message .= ' Set was assigned to ' . $c->req->param('user') . '.'; + } + return $c->render(json => { message => $message }); +} + +sub assignSetToUsers ($c) { + return $c->renderError('You do not have permission for the assignSetToUsers API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'assign_problem_sets'); + + my $db = $c->db; + + my $setID = $c->req->param('set_id'); + + return $c->renderError("The set $setID does not exist.") unless $db->existsGlobalSet($setID); + + my %setUsers = map { $_ => 1 } $db->listSetUsers($setID); + + my @usersToAdd; + for my $user (split(',', $c->req->param('users'))) { + next if $setUsers{$user}; + push @usersToAdd, $user; + } + assignSetToGivenUsers($db, $c->ce, $setID, 1, $db->getUsers(@usersToAdd)); + + return $c->render(json => { message => "Successfully assigned users to set $setID" }); +} + +sub deleteProblemSet ($c) { + return $c->renderError('You do not have permission for the deleteProblemSet API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_problem_sets'); + return $c->render(json => { message => 'Deleted problem set ' . $c->req->param('set_id') . '.' }) + if $c->db->deleteGlobalSet($c->req->param('set_id')) != 0E0; + return $c->renderError('Unable to delete problem set ' . $c->req->param('set_id') . '. Does the set exist?'); +} + +sub reorderProblems ($c) { + return $c->renderError('You do not have permission for the reorderProblems API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_problem_sets'); + + my $db = $c->db; + my $setID = $c->req->param('set_id'); + my @problemList = split(/,/, $c->req->param('probList')); + my $templatesDir = $c->ce->{courseDirs}{templates}; + + for my $problem ($db->getAllGlobalProblems($setID)) { + my $recordFound = 0; + for (my $i = 0; $i < @problemList; ++$i) { + $problemList[$i] =~ s|^$templatesDir/*||; + if ($problem->{source_file} eq $problemList[$i]) { + if ($db->existsGlobalProblem($setID, $i + 1)) { + $problem->problem_id($i + 1); + $db->putGlobalProblem($problem); + } else { + $db->deleteGlobalProblem($setID, $problem->{problem_id}); + $problem->problem_id($i + 1); + $db->addGlobalProblem($problem); + } + } + $recordFound = 1; + } + return $c->renderError("Problem $problem->{source_file} for set $setID not found.") + unless $recordFound; + } + + return $c->render(json => { message => 'Successfully reordered problems' }); +} + +sub updateProblem ($c) { + return $c->renderError('You do not have permission for the updateProblem API command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_problem_sets'); + + my $setID = $c->req->param('set_id'); + my $path = $c->req->param('problemPath'); + + my @problem = $c->db->getGlobalProblemsWhere({ set_id => $setID, source_file => $path }); + return $c->renderError("Unable to find the problem in the set $setID with path $path.") + unless @problem && @problem == 1; + + $problem[0]->value($c->req->param('value') // 1); + $c->db->putGlobalProblem($problem[0]); + + return $c->render(json => { message => "Updated problem in set $setID with source file $path." }); +} + +# This updates the userSet for a problem set (only the open, due and answer dates are updated). +# Note that this does not validate the dates. +sub updateUserSet ($c) { + return $c->renderError('You do not have permission for the updateUserSet api command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + for my $userID (split(',', $c->req->param('users'))) { + my $set = $c->db->getUserSet($userID, $c->req->param('set_id')); + if ($set) { + $set->open_date($c->req->param('open_date')); + $set->due_date($c->req->param('due_date')); + $set->answer_date($c->req->param('answer_date')); + $c->db->putUserSet($set); + } else { + my $newSet = $c->db->newUserSet; + $newSet->user_id($userID); + $newSet->set_id($c->req->param('set_id')); + $newSet->open_date($c->req->param('open_date')); + $newSet->due_date($c->req->param('due_date')); + $newSet->answer_date($c->req->param('answer_date')); + $newSet = $c->db->addUserSet($newSet); + } + } + + return $c->render( + json => { + message => 'Successfully updated set ' + . $c->req->param('set_id') + . ' for users ' + . $c->req->param('users') . '.' + } + ); +} + +sub getSetUserSets ($c) { + return $c->renderError('You do not have permission for the getSetUserSets api command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'access_instructor_tools'); + + my $db = $c->db; + + my @userData; + + for my $userID ($db->listSetUsers($c->req->param('set_id'))) { + push(@userData, unbless($db->getUserSet($userID, $c->req->param('set_id')))); + } + + return $c->render(json => \@userData); +} + +sub saveUserSets ($c) { + return $c->renderError('You do not have permission for the saveUserSets api command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + for my $override (@{ from_json($c->req->param('overrides')) }) { + my $set = $c->db->getUserSet($override->{user_id}, $c->req->param('set_id')); + if ($override->{open_date}) { $set->{open_date} = $override->{open_date}; } + if ($override->{due_date}) { $set->{due_date} = $override->{due_date}; } + if ($override->{answer_date}) { $set->{answer_date} = $override->{answer_date}; } + $c->db->putUserSet($set); + } + + return $c->render(json => { message => 'Updated the overrides for set ' . $c->req->param('set_id') . '.' }); +} + +sub addProblem ($c) { + return $c->renderError('You do not have permission for the saveUserSets api command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + my $db = $c->db; + my $setID = $c->req->param('set_id'); + my $file = $c->req->param('problemPath'); + + my $problemID = $c->req->param('problemID'); + my $set = $db->getGlobalSet($setID); + return $c->renderError("Set $setID not found.") unless $set; + + if (!defined $problemID || $problemID eq '') { + if ($set->assignment_type eq 'jitar') { + # For jitar sets the next problem id is the next top level problem. + my @problemIDs = $db->listGlobalProblems($setID); + my @seq = (0); + @seq = jitar_id_to_seq($problemIDs[-1]) if $#problemIDs != -1; + $problemID = seq_to_jitar_id($seq[0] + 1); + } else { + $problemID = max($db->listGlobalProblems($setID)) + 1; + } + } + + my $problemRecord = $db->newGlobalProblem( + problem_id => $problemID, + set_id => $setID, + source_file => $file, + value => defined $c->req->param('value') + && $c->req->param('value') ne '' ? $c->req->param('value') : $c->ce->{problemDefaults}{value}, + max_attempts => $c->req->param('maxAttempts') // $c->ce->{problemDefaults}{max_attempts}, + showMeAnother => $c->req->param('showMeAnother') // $c->ce->{problemDefaults}{showMeAnother}, + showHintsAfter => $c->req->param('showHintsAfter') // $c->ce->{problemDefaults}{showHintsAfter}, + showMeAnotherCount => 0, + att_to_open_children => $c->req->param('att_to_open_children') + || $c->ce->{problemDefaults}{att_to_open_children}, + counts_parent_grade => $c->req->param('counts_parent_grade') + || $c->ce->{problemDefaults}{counts_parent_grade}, + prPeriod => $c->req->param('prPeriod') // $c->ce->{problemDefaults}->{prPeriod}, + prCount => 0 + ); + $db->addGlobalProblem($problemRecord); + + assignProblemToAllSetUsers($db, $problemRecord); + + return $c->render(json => { message => "Problem added to $setID" }); +} + +sub deleteProblem ($c) { + return $c->renderError('You do not have permission for the saveUserSets api command.') + unless $c->authz->hasPermissions($c->authen->{user_id}, 'modify_student_data'); + + my $setID = $c->req->param('set_id'); + my $path = $c->req->param('problemPath'); + + my @problem = $c->db->getGlobalProblemsWhere({ set_id => $setID, source_file => $path }); + return $c->renderError("Unable to find the problem in the set $setID with path $path.") + unless @problem && @problem == 1; + $c->db->deleteGlobalProblem($setID, $problem[0]); + + return $c->render(json => { message => "Problem removed from $setID" }); +} + +1; diff --git a/lib/WeBWorK/ContentGenerator/InstructorRPCHandler.pm b/lib/WeBWorK/ContentGenerator/InstructorRPCHandler.pm deleted file mode 100644 index 37239ed351..0000000000 --- a/lib/WeBWorK/ContentGenerator/InstructorRPCHandler.pm +++ /dev/null @@ -1,85 +0,0 @@ -package WeBWorK::ContentGenerator::InstructorRPCHandler; -use Mojo::Base 'WeBWorK::ContentGenerator', -signatures, -async_await; - -=head1 NAME - -WeBWorK::ContentGenerator::InstructorRPCHandler is a front end for instructor -calls to the rpc WebworkWebservice - -=head1 Description - -Receives requests containing WebworkWebservice remote procedure call commands, -executes them, and returns the results. - -Note that the WebworkWebservice renderProblem command is not supported by this -endpoint. The render_rpc endpoint defined in the -WeBWorK::ContentGenerator::RenderViaRPC module handles that command. - -Note that there will always be a valid JSON response to this endpoint. If an -error occurs, then the response will contain an "error" key. - -=cut - -# FIXME: This is no longer "instructor" only. Even students can use the getCurrentServerTime command. Really, it never -# was "instructor" only. Usage of all commands is based on permissions, and there have always been non-instructor users -# that have some of these permissions. So this module and the corresponding route should really be renamed. - -use WebworkWebservice; - -sub initializeRoute ($c, $routeCaptures) { - $c->{rpc} = 1; - - # Get the courseID from the parameters. - $routeCaptures->{courseID} = $c->stash->{courseID} = $c->param('courseID') if $c->param('courseID'); - - return; -} - -async sub pre_header_initialize ($c) { - unless ($c->authen->was_verified) { - $c->{output} = $c->maketext('Authentication failed. Log in again to continue.'); - return; - } - - my $rpc_command = $c->param('rpc_command'); - - unless ($rpc_command) { - $c->{output} = 'instructor_rpc: rpc_command not provided.'; - return; - } - - # The renderProblem command is not supported by this method. - # The render_rpc endpoint should be used for that instead. - if ($rpc_command eq 'renderProblem') { - $c->{output} = - 'instructor_rpc: The renderProblem command is not supported by this endpoint. Use render_rpc instead'; - return; - } - - # Call the WebworkWebservice to execute the requested command. - my $rpc_service = WebworkWebservice->new($c); - await $rpc_service->rpc_execute($rpc_command); - $c->{output} = $rpc_service; - - return; -} - -sub content ($c) { - # This endpoint always responds with a valid JSON response. - - return $c->render(json => { error => $c->{output} }) if (ref($c->{output}) !~ /WebworkWebservice/); - - my $rpc_service = $c->{output}; - if ($rpc_service->error_string) { - return $c->render(json => { error => $rpc_service->error_string }); - } else { - return $c->render( - json => { - server_response => $rpc_service->return_object->{text}, - result_data => $rpc_service->return_object->{ra_out} // '' - } - ); - } -} - -1; diff --git a/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm b/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm index 715848411e..92094990c0 100644 --- a/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm +++ b/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm @@ -11,13 +11,23 @@ webwork webservice. Receives WeBWorK requests presented as HTML forms, containing the requisite information for rendering a problem. This package checks that authentication -succeeded, calls WebworkWebservice::RenderProblem::renderProblem, and then -passes its return value to FormatRenderedProblem::formatRenderedProblem. The -result is returned in the JSON or HTML format as determined by the request type. +succeeded, calls renderProblem, and then passes its return value to +FormatRenderedProblem::formatRenderedProblem. The result is returned in the +JSON or HTML format as determined by the request type. =cut -use WebworkWebservice; +use Benchmark; +use Mojo::Util qw(url_unescape); + +use WeBWorK::Debug qw(debug); +use WeBWorK::DB::Utils qw(global2user fake_set fake_problem); +use WeBWorK::Utils qw(decode_utf8_base64); +use WeBWorK::Utils::Files qw(readFile path_is_subdir); +use WeBWorK::Utils::Logs qw(writeCourseLog); +use WeBWorK::Utils::Rendering qw(renderPG); +use FormatRenderedProblem; +use HardcopyRenderedProblem; sub initializeRoute ($c, $routeCaptures) { $c->{rpc} = 1; @@ -51,41 +61,302 @@ sub initializeRoute ($c, $routeCaptures) { return; } -async sub pre_header_initialize ($c) { - $c->{wantsjson} = ($c->param('outputformat') // '') eq 'json' || ($c->param('send_pg_flags') // 0); +sub renderError ($c, $message) { + return $c->render(($c->req->param('outputformat') // '') eq 'json' + || ($c->req->param('send_pg_flags') // 0) ? (json => { error => $message }) : (text => $message)); +} - unless ($c->authen->was_verified) { - $c->{output} = - $c->{wantsjson} - ? { error => $c->maketext('Authentication failed. Log in again to continue.') } - : $c->maketext('Authentication failed. Log in again to continue.'); - return; - } +async sub go ($c) { + writeCourseLog($c->ce, 'activity_log', $c->prepare_activity_entry) + if ($c->stash('courseID') && $c->ce->{courseFiles}{logs}{activity_log}); - $c->param('displayMode', 'tex') - if $c->param('outputformat') && ($c->param('outputformat') eq 'pdf' || $c->param('outputformat') eq 'tex'); + return $c->renderError($c->maketext('Authentication failed. Log in again to continue.')) + unless $c->authen->was_verified; - # Call the WebworkWebservice to render the problem and store the result in $c->return_object. - my $rpc_service = WebworkWebservice->new($c); - await $rpc_service->rpc_execute('renderProblem'); - if ($rpc_service->error_string) { - $c->{output} = $c->{wantsjson} ? { error => $rpc_service->error_string } : $rpc_service->error_string; - return; + return $c->renderError($c->maketext('User does not have permission to render problems.')) + unless $c->authz->hasPermissions($c->authen->{user_id}, 'webservice_render_problem'); + + if ($c->req->param('problemSource') + || $c->req->param('rawProblemSource') + || $c->req->param('uriEncodedProblemSource')) + { + # If the problem source is provided, check that the user is allow to render problem source. + return $c->renderError($c->maketext('User does not have permission to render problem source.')) + unless $c->authz->hasPermissions($c->authen->{user_id}, 'webservice_render_source'); + } elsif (defined $c->req->param('sourceFilePath') && $c->req->param('sourceFilePath') =~ /\S/) { + # If the source file path is provided, ensure it is contained in the course's templates directory. + return $c->renderError($c->maketext('Unable to render the source file path as it is unsafe.')) + unless path_is_subdir($c->ce->{courseDirs}{templates} . '/' . $c->req->param('sourceFilePath'), + $c->ce->{courseDirs}{templates}); } - # Format the return in the requested format. A response is rendered unless there is an error. - $c->{output} = $rpc_service->formatRenderedProblem; + my $tx = $c->render_later->tx; - return; + $c->req->param('displayMode', 'tex') + if $c->req->param('outputformat') + && ($c->req->param('outputformat') eq 'pdf' || $c->req->param('outputformat') eq 'tex'); + + my $renderedProblem = await $c->renderProblem; + + if ($c->req->param('outputformat') + && ($c->req->param('outputformat') eq 'tex' || $c->req->param('outputformat') eq 'pdf')) + { + my $result = HardcopyRenderedProblem::hardcopyRenderedProblem($c, $renderedProblem); + return if $c->res->code; + return $c->renderError($result); + } + return FormatRenderedProblem::formatRenderedProblem($c, $renderedProblem); } -sub content ($c) { - # If there were no errors a response will have been rendered. Return in that case. - return if $c->res->code; +our $UNIT_TESTS_ON = 0; + +async sub renderProblem ($c) { + my $rh = $c->req->params->to_hash; + + my $problemSeed = $rh->{problemSeed} // '1234'; + + my $beginTime = Benchmark->new; + + my $ce = $c->ce; + my $db = $c->db; + + # Determine an effective user for this interaction or create one if it is not given. + # Use effectiveUser if given, and $rh->{user} otherwise. + my $effectiveUserName; + if (defined $rh->{effectiveUser} && $rh->{effectiveUser} =~ /\S/) { + $effectiveUserName = $rh->{effectiveUser}; + } else { + $effectiveUserName = $rh->{user}; + } + + if ($UNIT_TESTS_ON) { + print STDERR "RenderProblem.pm: user = $rh->{user}\n"; + print STDERR "RenderProblem.pm: courseName = $rh->{courseID}\n"; + print STDERR "RenderProblem.pm: effectiveUserName = $effectiveUserName\n"; + print STDERR 'environment fileName', $rh->{fileName}, "\n"; + } + + # The effectiveUser is the student this problem version was written for + # The user might also be the effective user but it could be + # an instructor checking out how well the problem is working. + + my $effectiveUser = $db->getUser($effectiveUserName); + my $effectiveUserPermissionLevel; + my $effectiveUserPassword; + unless (defined $effectiveUser) { + $effectiveUser = $db->newUser; + $effectiveUserPermissionLevel = $db->newPermissionLevel; + $effectiveUserPassword = $db->newPassword; + $effectiveUser->user_id($effectiveUserName); + $effectiveUserPermissionLevel->user_id($effectiveUserName); + $effectiveUserPassword->user_id($effectiveUserName); + $effectiveUserPassword->password(''); + $effectiveUser->last_name($rh->{studentName} || 'foobar'); + $effectiveUser->first_name(''); + $effectiveUser->student_id($rh->{studentID} || 'foobar'); + $effectiveUser->email_address($rh->{email} || ''); + $effectiveUser->section($rh->{section} || ''); + $effectiveUser->recitation($rh->{recitation} || ''); + $effectiveUser->comment(''); + $effectiveUser->status('C'); + $effectiveUserPermissionLevel->permission(0); + } + + # Insure that set and problem are defined. Define the set and problem information from data in the environment if + # necessary. + my $setName = $rh->{set_id} // $rh->{setNumber} // ''; + + my $setVersionId = $rh->{version_id} || 0; + + my $problemNumber = $rh->{probNum} // 0; + my $psvn = $rh->{psvn} // 1234; + my $problemValue = $rh->{problemValue} // 1; + my $lastAnswer = ''; + + debug('effectiveUserName: ' . $effectiveUserName); + debug('setName: ' . $setName); + debug('setVersionId: ' . $setVersionId); + debug('problemNumber: ' . $problemNumber); + debug('problemSeed:' . $problemSeed); + debug('psvn: ' . $psvn); + debug('problemValue: ' . $problemValue); + + my $setRecord = + $setVersionId + ? $db->getMergedSetVersion($effectiveUserName, $setName, $setVersionId) + : $db->getMergedSet($effectiveUserName, $setName); + + if (defined $setRecord && ref $setRecord) { + # If an actual set from the database is used, the passed in psvn is ignored. + # So save the actual psvn used and pass that on to the renderer. + $psvn = $setRecord->psvn; + } else { + # if a User Set does not exist for this user and this set + # then we check the Global Set + # if that does not exist we create a fake set + # if it does, we add fake user data + my $userSetClass = $db->{set_user}{record}; + my $globalSet = $db->getGlobalSet($setName); + + if (!defined $globalSet) { + $setRecord = fake_set($db); + } else { + $setRecord = global2user($userSetClass, $globalSet); + } + + # Initializations + $setRecord->set_id($setName); + $setRecord->set_header(''); + $setRecord->hardcopy_header('defaultHeader'); + $setRecord->open_date(time - 60 * 60 * 24 * 7); # one week ago + $setRecord->due_date(time + 60 * 60 * 24 * 7 * 2); # in two weeks + $setRecord->answer_date(time + 60 * 60 * 24 * 7 * 3); # in three weeks + $setRecord->psvn($rh->{psvn} // 1234); + } + + # obtain the merged problem for $effectiveUser + my $problemRecord = + !$problemNumber ? undef + : $setVersionId ? $db->getMergedProblemVersion($effectiveUserName, $setName, $setVersionId, $problemNumber) + : $db->getMergedProblem($effectiveUserName, $setName, $problemNumber); + + if (defined $problemRecord) { + # If a problem from the database is used, the passed in problem seed is ignored. + # So save the actual seed used and pass that on to the renderer. + $problemSeed = $problemRecord->problem_seed; + } else { + # If that is not yet defined obtain the global problem, + # convert it to a user problem, and add fake user data + my $userProblemClass = $db->{problem_user}{record}; + my $globalProblem = $db->getGlobalProblem($setName, $problemNumber); + # if the global problem doesn't exist either, bail! + if (not defined $globalProblem) { + $problemRecord = fake_problem($db); + } else { + $problemRecord = global2user($userProblemClass, $globalProblem); + } + # initializations + $problemRecord->user_id($effectiveUserName); + $problemRecord->problem_id($problemNumber); + $problemRecord->set_id($setName); + $problemRecord->problem_seed($problemSeed); + $problemRecord->status(0); + $problemRecord->value($problemValue); + # We are faking it + $problemRecord->attempted(2000); + $problemRecord->num_correct(1000); + $problemRecord->num_incorrect(1000); + $problemRecord->last_answer($lastAnswer); + } + + if ($UNIT_TESTS_ON) { + print STDERR 'setRecord is ', $c->dumper($setRecord); + print STDERR 'template directory path ', $ce->{courseDirs}{templates}, "\n"; + print STDERR 'RenderProblem.pm: source file is ', $rh->{sourceFilePath}, "\n"; + print STDERR "RenderProblem.pm: problem source is included in the request \n" + if defined($rh->{problemSource}) && $rh->{problemSource}; + } + + # Initialize problem source + my $r_problem_source; + if ($rh->{problemSource}) { + $r_problem_source = \(decode_utf8_base64($rh->{problemSource}) =~ tr/\r/\n/r); + $problemRecord->source_file($rh->{fileName} ? $rh->{fileName} : $rh->{sourceFilePath}); + } elsif ($rh->{rawProblemSource}) { + $r_problem_source = \$rh->{rawProblemSource}; + $problemRecord->source_file($rh->{fileName} ? $rh->{fileName} : $rh->{sourceFilePath}); + } elsif ($rh->{uriEncodedProblemSource}) { + $r_problem_source = \(url_unescape($rh->{uriEncodedProblemSource})); + $problemRecord->source_file($rh->{fileName} ? $rh->{fileName} : $rh->{sourceFilePath}); + } elsif (defined $rh->{sourceFilePath} && $rh->{sourceFilePath} =~ /\S/) { + $problemRecord->source_file($rh->{sourceFilePath}); + $r_problem_source = \(readFile($ce->{courseDirs}{templates} . '/' . $rh->{sourceFilePath})); + } + + if ($UNIT_TESTS_ON) { + print STDERR 'template directory path ', $ce->{courseDirs}{templates}, "\n"; + print STDERR 'RenderProblem.pm: source file is ', $problemRecord->source_file, "\n"; + print STDERR "RenderProblem.pm: problem source is included in the request \n" if defined($rh->{problemSource}); + } + # now we're sure we have valid UserSet and UserProblem objects + + # Other initializations + my $translationOptions = { + displayMode => $rh->{displayMode} // 'MathJax', + showHints => $rh->{showHints}, + showSolutions => $rh->{showSolutions}, + processAnswers => $rh->{processAnswers} // 1, + catchWarnings => 1, + r_source => $r_problem_source, + problemUUID => $rh->{problemUUID} // 0, + permissionLevel => $rh->{permissionLevel} || 0, + effectivePermissionLevel => $rh->{effectivePermissionLevel} || $rh->{permissionLevel} || 0, + useMathQuill => $ce->{pg}{specialPGEnvironmentVars}{entryAssist} eq 'MathQuill', + useMathView => $ce->{pg}{specialPGEnvironmentVars}{entryAssist} eq 'MathView', + isInstructor => $rh->{isInstructor} // 0, + forceScaffoldsOpen => $rh->{WWcorrectAnsOnly} ? 1 : ($rh->{forceScaffoldsOpen} // 0), + QUIZ_PREFIX => $rh->{answerPrefix}, + showFeedback => $rh->{previewAnswers} || $rh->{WWsubmit} || $rh->{WWcorrectAns}, + showAttemptAnswers => $rh->{WWcorrectAnsOnly} ? 0 + : ($rh->{showAttemptAnswers} // $ce->{pg}{options}{showEvaluatedAnswers}), + showAttemptPreviews => ( + $rh->{WWcorrectAnsOnly} ? 0 + : ($rh->{showAttemptPreviews} // ($rh->{previewAnswers} || $rh->{WWsubmit} || $rh->{WWcorrectAns})) + ), + showAttemptResults => $rh->{showAttemptResults} // ($rh->{WWsubmit} || $rh->{WWcorrectAns}), + forceShowAttemptResults => ( + $rh->{WWcorrectAnsOnly} ? 1 + : ( + $rh->{forceShowAttemptResults} + || ($rh->{isInstructor} + && ($rh->{showAttemptResults} // ($rh->{WWsubmit} || $rh->{WWcorrectAns}))) + ) + ), + showMessages => ( + $rh->{WWcorrectAnsOnly} ? 0 + : ($rh->{showMessages} // ($rh->{previewAnswers} || $rh->{WWsubmit} || $rh->{WWcorrectAns})) + ), + showCorrectAnswers => + ($rh->{WWcorrectAnsOnly} ? 1 : ($rh->{showCorrectAnswers} // ($rh->{WWcorrectAns} ? 2 : 0))), + debuggingOptions => { + show_resource_info => $rh->{show_resource_info} // 0, + view_problem_debugging_info => $rh->{view_problem_debugging_info} // 0, + show_pg_info => $rh->{show_pg_info} // 0, + show_answer_hash_info => $rh->{show_answer_hash_info} // 0, + show_answer_group_info => $rh->{show_answer_group_info} // 0 + }, + defined $rh->{problem_data} && $rh->{problem_data} ne '' ? (problemData => $rh->{problem_data}) : () + }; + + $ce->{pg}{specialPGEnvironmentVars}{problemPreamble} = { TeX => '', HTML => '' } if $rh->{noprepostambles}; + $ce->{pg}{specialPGEnvironmentVars}{problemPostamble} = { TeX => '', HTML => '' } if $rh->{noprepostambles}; + + my $pg = await renderPG($c, $effectiveUser, $setRecord, $problemRecord, $setRecord->psvn, $rh, $translationOptions); + + # New version of output: + return { + text => $pg->{body_text}, + header_text => $pg->{head_text}, + post_header_text => $pg->{post_header_text}, + answers => $pg->{answers}, + errors => $pg->{errors}, + pg_warnings => $pg->{warnings}, + PG_ANSWERS_HASH => $pg->{PG_ANSWERS_HASH}, + PERSISTENCE_HASH => $pg->{PERSISTENCE_HASH}, + problem_result => $pg->{result}, + problem_state => $pg->{state}, + flags => $pg->{flags}, + psvn => $psvn, + problem_seed => $problemSeed, + resource_list => $pg->{resource_list}, + warning_messages => ref $pg->{warning_messages} eq 'ARRAY' ? $pg->{warning_messages} : [], + debug_messages => ref $pg->{debug_messages} eq 'ARRAY' ? $pg->{debug_messages} : [], + compute_time => logTimingInfo($beginTime, Benchmark->new), + }; +} - # Handle rendering of errors. - return $c->render(json => $c->{output}) if $c->{wantsjson}; - return $c->render(text => $c->{output}); +sub logTimingInfo ($beginTime, $endTime) { + return Benchmark::timestr(Benchmark::timediff($endTime, $beginTime)); } 1; diff --git a/lib/WeBWorK/Utils/Routes.pm b/lib/WeBWorK/Utils/Routes.pm index 92cfb5b793..815bc334e9 100644 --- a/lib/WeBWorK/Utils/Routes.pm +++ b/lib/WeBWorK/Utils/Routes.pm @@ -14,7 +14,7 @@ PLEASE FOR THE LOVE OF GOD UPDATE THIS IF YOU CHANGE THE ROUTES BELOW!!! course_admin /$ce->{admin_course_id} -> logout, options, instructor_tools render_rpc /render_rpc - instructor_rpc /instructor_rpc + api /api/$command ltiadvanced_content_selection /ltiadvanced/content_selection @@ -140,7 +140,7 @@ my %routeParameters = ( # 'course_admin' is also a child of 'root' but that is a special case that is setup separately. children => [ qw( render_rpc - instructor_rpc + api ltiadvanced_content_selection ltiadvantage_login ltiadvantage_launch @@ -170,10 +170,10 @@ my %routeParameters = ( module => 'RenderViaRPC', path => '/render_rpc' }, - instructor_rpc => { - title => 'instructor_rpc', - module => 'InstructorRPCHandler', - path => '/instructor_rpc', + api => { + title => 'api', + module => 'API', + path => { '/api/#command' => [ command => qr/[\w-]*/ ] }, methods => ['POST'] }, diff --git a/lib/WebworkWebservice.pm b/lib/WebworkWebservice.pm deleted file mode 100644 index ef4f03f29d..0000000000 --- a/lib/WebworkWebservice.pm +++ /dev/null @@ -1,285 +0,0 @@ -package WebworkWebservice; - -=head1 NAME - -WebworkWebservice - -=head1 SYNOPSIS - - my $rpc_service = WebworkWebservice->new($c); - await $rpc_service->rpc_execute('command_to_execute'); - -After that, if the command is 'renderProblem', use - - my $result = $rpc_service->formatRenderedProblem; - -to obtain the result in the requested 'outputformat'. - -=head1 DESCRIPTION - -The WebworkWebservice executes a requested command and returns with the result. -The webservice command methods are available are in the following modules: - - WebworkWebservice::RenderProblem; - WebworkWebservice::LibraryActions; - WebworkWebservice::SetActions; - WebworkWebservice::CourseActions; - WebworkWebservice::ProblemActions - -Note that WebworkWebservice contains the formatRenderedProblem method for -formatting the reply returned by the renderProblem command. - -Also note that the WeBWorK::ContentGenerator::RenderViaRPC module implements the -renderProblem command, and the WeBWorK::ContentGenerator::InstructorPRCHandler -module has implements all other commands. - -=cut - -use strict; -use warnings; - -use Future::AsyncAwait; - -use WeBWorK::Localize; -use WeBWorK::CourseEnvironment; -use WebworkWebservice::RenderProblem; -use WebworkWebservice::LibraryActions; -use WebworkWebservice::SetActions; -use WebworkWebservice::CourseActions; -use WebworkWebservice::ProblemActions; -use FormatRenderedProblem; -use HardcopyRenderedProblem; - -=head2 new (constructor) - -=cut - -sub new { - my ($invocant, $c, %options) = @_; - my $class = ref $invocant || $invocant; - return bless { - c => $c, - inputs_ref => $c->req->params->to_hash, - return_object => {}, - error_string => '', - %options - }, $class; -} - -=head2 Accessor methods - - return_object - error_string - -=cut - -sub return_object { - my ($self, $object) = @_; - $self->{return_object} = $object if defined $object && ref $object; - return $self->{return_object}; -} - -sub error_string { - my ($self, $string) = @_; - $self->{error_string} = $string if defined $string && $string =~ /\S/; - return $self->{error_string}; -} - -=head2 rpc_execute - -This method executes a WebworkWebservice command, and makes sure that -credentials are returned in the result on success. The result will be stored in -the result_object of the instance. An error_string will be set on failure. - -=cut - -async sub rpc_execute { - my ($self, $command) = @_; - my $c = $self->c; - my $user_id = $c->param('user'); - - $command //= 'renderProblem'; - - my $permission = command_permission($command); - - return $self->error_string(__PACKAGE__ . ": Invalid command $command") if $permission eq 'invalid'; - - # Check that the user has permission to perform this command. - return $self->error_string(__PACKAGE__ . ": User $user_id does not have permission for the command $command") - unless $c->authz->hasPermissions($user_id, $permission); - - # Determine the package that contains the method for this command. - my $command_package = ''; - for my $package ( - 'WebworkWebservice::RenderProblem', 'WebworkWebservice::LibraryActions', - 'WebworkWebservice::SetActions', 'WebworkWebservice::CourseActions', - 'WebworkWebservice::ProblemActions' - ) - { - if ($package->can($command)) { - $command_package = $package; - last; - } - } - - return $self->error_string( - __PACKAGE__ . ": Unable to find a method for $command. This shouldn't happen. Report this error.") - unless $command_package; - - my $result = eval { - my $out = $command_package->$command($self, $self->{inputs_ref}); - return await $out if ref $out eq 'Future' || ref $out eq 'Mojo::Promise'; - return $out; - }; - - if ($@) { - my $error = $@; - chomp $error; - return $self->error_string(__PACKAGE__ . " call to $command resulted in the following errors: $error"); - } - return $self->error_string(__PACKAGE__ . " call to $command returned no result") if !ref $result; - - return $self->return_object($result); -} - -=over - -=item formatRenderedProblem - -This is called by WeBWorK::ContentGenerator::RenderViaRPC::pre_header_initialize -to format the return result of the WebworkWebservice::renderProblem method. -This method calls HardcopyRenderedProblem::hardcopyRenderedProblem if the -outputformat is tex or pdf, and calls FormatRenderedProblem::formatRenderedProblem -otherwise. - -=back - -=cut - -sub formatRenderedProblem { - my $self = shift; - return HardcopyRenderedProblem::hardcopyRenderedProblem($self) - if $self->{inputs_ref}{outputformat} - && ($self->{inputs_ref}{outputformat} eq 'tex' || $self->{inputs_ref}{outputformat} eq 'pdf'); - return FormatRenderedProblem::formatRenderedProblem($self); -} - -=head2 c - -Returns the WeBWorK::Controller object contained in $webworkRPC. - -=cut - -sub c { - my $self = shift; - return $self->{c}; -} - -=head2 Pass through methods which access the data in the WeBWorK::Controller object - - ce - db - params - authz - authen - maketext - -=cut - -sub ce { - my $self = shift; - return $self->{c}->ce; -} - -sub db { - my $self = shift; - return $self->{c}->db; -} - -sub param { - my ($self, $param) = @_; - return $self->{c}->param($param); -} - -sub authz { - my $self = shift; - return $self->{c}->authz; -} - -sub authen { - my $self = shift; - return $self->{c}->authen; -} - -sub maketext { - my $self = shift; - return $self->{c}->language_handle->(@_); -} - -=head2 command_permission - -This returns the permission required to perform the commands offered by the -WebworkWebservice. Note that all available commands must be listed here or the -command will not be allowed. - -=cut - -sub command_permission { - my ($command) = @_; - return { - # WebworkWebservice::CourseActions - createCourse => 'create_and_delete_courses', - listUsers => 'access_instructor_tools', - addUser => 'modify_student_data', - dropUser => 'modify_student_data', - deleteUser => 'modify_student_data', - editUser => 'modify_student_data', - changeUserPassword => 'modify_student_data', - getCourseSettings => 'access_instructor_tools', - updateSetting => 'manage_course_files', - saveFile => 'modify_problem_sets', - getCurrentServerTime => 'record_answers_after_open_date_with_attempts', - - # WebworkWebservice::LibraryActions - listLib => 'access_instructor_tools', - searchLib => 'access_instructor_tools', - getProblemTags => 'access_instructor_tools', - setProblemTags => 'modify_tags', - - # WebworkWebservice::ProblemActions - getUserProblem => 'access_instructor_tools', - # Note: The modify_student_data permission is checked in the following three methods and only the status, - # sub_status, and comment_string can actually be modified by users with the problem_grader permission only. - putUserProblem => 'problem_grader', - putProblemVersion => 'problem_grader', - putPastAnswer => 'problem_grader', - tidyPGCode => 'access_instructor_tools', - convertCodeToPGML => 'access_instructor_tools', - runPGCritic => 'access_instructor_tools', - - # WebworkWebservice::RenderProblem - renderProblem => 'webservice_render_problem', - - # WebworkWebservice::SetActions - listGlobalSets => 'access_instructor_tools', - listGlobalSetProblems => 'access_instructor_tools', - getSets => 'access_instructor_tools', - getUserSets => 'access_instructor_tools', - getSet => 'access_instructor_tools', - updateSetProperties => 'modify_problem_sets', - listSetUsers => 'access_instructor_tools', - createNewSet => 'modify_problem_sets', - assignSetToUsers => 'assign_problem_sets', - deleteProblemSet => 'modify_problem_sets', - reorderProblems => 'modify_problem_sets', - updateProblem => 'modify_problem_sets', - updateUserSet => 'modify_student_data', - getSetUserSets => 'access_instructor_tools', - saveUserSets => 'modify_student_data', - unassignSetFromUsers => 'modify_student_data', - addProblem => 'modify_problem_sets', - deleteProblem => 'modify_problem_sets', - }->{$command} // 'invalid'; -} - -1; diff --git a/lib/WebworkWebservice/CourseActions.pm b/lib/WebworkWebservice/CourseActions.pm deleted file mode 100644 index e3bb0a723c..0000000000 --- a/lib/WebworkWebservice/CourseActions.pm +++ /dev/null @@ -1,527 +0,0 @@ -# Course manipulation functions for webwork webservices -package WebworkWebservice::CourseActions; - -use strict; -use warnings; - -use Time::HiRes qw/gettimeofday/; -use Date::Format; -use Data::Structure::Util qw(unbless); - -use WeBWorK::DB; -use WeBWorK::DB::Utils qw(initializeUserProblem); -use WeBWorK::Utils qw(cryptPassword); -use WeBWorK::Utils::CourseManagement qw(addCourse); -use WeBWorK::Utils::Files qw(surePathToFile path_is_subdir); -use WeBWorK::ConfigValues qw(getConfigValues); -use WeBWorK::Debug qw(debug); - -sub createCourse { - my ($invocant, $self, $params) = @_; - - my $admin_ce = $self->ce; - my $db = $self->db; - my $authz = $self->authz; - - # Make sure course actions are enabled - die "Course actions disabled by configuration.\n" unless $admin_ce->{webservices}{enableCourseActions}; - - # Only users from the admin course with appropriate permissions are allowed to create a course. - die "Course creation allowed only for admin course users.\n" - unless $admin_ce->{courseName} eq $admin_ce->{admin_course_id}; - - die "Course ID cannot exceed $admin_ce->{maxCourseIdLength} characters.\n" - if length($params->{name}) > $admin_ce->{maxCourseIdLength}; - - # Bring up a minimal course environment for the new course. - my $ce = WeBWorK::CourseEnvironment->new({ courseName => $params->{name} }); - - # Copy user from admin course. - # Modified from do_add_course in WeBWorK::ContentGenerator::CourseAdmin. - my @users; - for my $userID ($db->listUsers) { - push @users, [ $db->getUser($userID), $db->getPassword($userID), $db->getPermissionLevel($userID) ] - if $authz->hasPermissions($userID, 'create_and_delete_courses'); - } - - # Try to actually create the course. - eval { - addCourse( - courseID => $params->{name}, - ce => $ce, - users => \@users - ); - addLog($ce, "New course created: $params->{name}"); - return 1; - } or die "$@\n"; - - return { text => "New course $params->{name} created." }; -} - -sub listUsers { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $ce = $self->ce; - - my @userInfo = map { unbless($_) } $db->getUsersWhere({ user_id => { not_like => 'set_id:%' } }); - my $numGlobalSets = $db->countGlobalSets; - - for my $user (@userInfo) { - my $permissionLevel = $db->getPermissionLevel($user->{user_id}); - $user->{permission} = $permissionLevel->{permission}; - - $user->{num_user_sets} = $db->countUserSets($user->{user_id}) . '/' . $numGlobalSets; - - my $Key = $db->getKey($user->{user_id}); - $user->{login_status} = $Key && time <= $Key->timestamp + $ce->{sessionTimeout} ? 'active' : 'inactive'; - } - - return { - ra_out => \@userInfo, - text => "Users for course: $ce->{courseName}" - }; -} - -sub addUser { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $ce = $self->ce; - - # Make sure course actions are enabled - die "Course actions disabled by configuration.\n" unless $ce->{webservices}{enableCourseActions}; - - # Check parameters. - die "The user_id parameter is required\n" unless $params->{user_id} && $params->{user_id} =~ /\S/; - - my $user_id = $params->{user_id} =~ s/^\s*|\s*$//g; - - my $out = { ra_out => {} }; - - my $olduser = $db->getUser($params->{user_id}); - my $permission; - if ($olduser) { - if ($olduser->status != $ce->{statuses}{Enrolled}{abbrevs}[0]) { - # Re-enroll the existing user. - $olduser->status($ce->{statuses}{Enrolled}{abbrevs}[0]); - $db->putUser($olduser); - addLog($ce, "User $params->{user_id} re-enrolled in $ce->{courseName}"); - - $permission = $db->getPermissionLevel($params->{user_id}); - - $out->{ra_out}{user_added} = \1; - $out->{text} = "User $params->{user_id} re-enrolled in $ce->{courseName}."; - } else { - $out->{text} = "User $params->{user_id} already enrolled in $ce->{courseName}."; - } - } else { - # Add a new user. - my $ce = $self->ce; - - # student record - my $enrolled = $ce->{statuses}->{Enrolled}->{abbrevs}->[0]; - my $new_student = $db->{user}->{record}->new(); - $new_student->user_id($params->{user_id}); - $new_student->first_name($params->{first_name}) if $params->{first_name}; - $new_student->last_name($params->{last_name}) if $params->{last_name}; - $new_student->status($enrolled); - $new_student->student_id($params->{student_id}) if defined $params->{student_id}; - $new_student->email_address($params->{email_address}) if $params->{email_address}; - $new_student->recitation($params->{recitation}) if defined $params->{recitation}; - $new_student->section($params->{section}) if defined $params->{section}; - $new_student->comment($params->{comment}) if $params->{comment}; - - # Password record - my $cryptedpassword = ''; - if ($params->{password}) { - $cryptedpassword = cryptPassword($params->{password} =~ s/^\s*|\s*$//gr); - } elsif ($new_student->student_id) { - $cryptedpassword = cryptPassword($new_student->student_id); - } - my $password = $db->newPassword(user_id => $params->{user_id}); - $password->password($cryptedpassword); - - # Permission record - $permission = $params->{permission} // 0; - if (defined($ce->{userRoles}{$permission})) { - $permission = $db->newPermissionLevel( - user_id => $params->{user_id}, - permission => $ce->{userRoles}{$permission} - ); - } else { - $permission = $db->newPermissionLevel( - user_id => $params->{user_id}, - permission => $ce->{userRoles}{student} - ); - } - - # Commit changes to db - $db->addUser($new_student); - $db->addPassword($password); - eval { $db->addPermissionLevel($permission); }; - - $out->{ra_out}{user_added} = \1; - $out->{text} = "User $params->{user_id} added to $ce->{courseName}."; - addLog($ce, "User $params->{user_id} added to $ce->{courseName}"); - } - - # Assign all visible sets to the user if requested. - if ($params->{assign_visible_sets}) { - $out->{ra_out}{sets_assigned} = assignVisibleSets($db, $params->{user_id}) ? \0 : \1; - $out->{text} .= " Visible sets assigned to $params->{user_id}."; - } - - return $out; -} - -sub dropUser { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $ce = $self->ce; - - # Make sure course actions are enabled - die "Course actions disabled by configuration.\n" unless $ce->{webservices}{enableCourseActions}; - - # Check parameters. - die "The user_id parameter is required\n" unless $params->{user_id} && $params->{user_id} =~ /\S/; - - # Mark user as dropped - my $user = $db->getUser($params->{user_id}); - - die "Could not find $params->{user_id} in $ce->{courseName}\n" unless $user; - - $user->status($ce->{statuses}{Drop}{abbrevs}[0]); - $db->putUser($user); - addLog($ce, "User $params->{user_id} dropped from $ce->{courseName}"); - return { text => "User $params->{user_id} dropped from $ce->{courseName}" }; -} - -sub deleteUser { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $ce = $self->ce; - - # Make sure course actions are enabled - die "Course actions disabled by configuration.\n" unless $ce->{webservices}{enableCourseActions}; - - die "The user_id parameter is required\n" unless $params->{user_id} && $params->{user_id} =~ /\S/; - - my $User = $db->getUser($params->{user_id}); - die "Record for user $params->{user_id} not found\n" unless $User; - - die q{You can't delete yourself from the course.} if ($params->{user_id} eq $params->{user}); - - my $del = $db->deleteUser($params->{user_id}); - die "User $params->{user_id} could not be deleted\n" unless $del; - - addLog($ce, "User $params->{user_id} deleted from $ce->{courseName}"); - return { text => "User $params->{user_id} deleted from $ce->{courseName}" }; -} - -sub editUser { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $ce = $self->ce; - - # Make sure course actions are enabled - die "Course actions disabled by configuration.\n" unless $ce->{webservices}{enableCourseActions}; - - die "The user_id parameter is required\n" unless ($params->{user_id} && $params->{user_id} =~ /\S/); - - my $User = $db->getUser($params->{user_id}); - die "User $params->{user_id} not found.\n" unless ($User); - - # It has already been checked that the user has permission to modify user data. Get the permission level here, so - # that it can be verified that the permission level of the user being edited is less than or equal to that of the - # one doing the editing. - my $callerPermission = $db->getPermissionLevel($params->{user}); - my $permissionLevel = $db->getPermissionLevel($params->{user_id}); - - die "You do not have permission to edit $params->{user_id}\n" - unless $callerPermission && $permissionLevel && $callerPermission->permission >= $permissionLevel->permission; - - my $out = { text => '', ra_out => {} }; - - for my $field ($User->NONKEYFIELDS()) { - $User->$field($params->{$field}) if defined $params->{$field}; - } - $db->putUser($User); - $out->{text} = 'User data updated.'; - $out->{ra_out}{user} = unbless($User); - - if (defined $params->{permission} && $params->{permission} =~ /\d*/) { - if ($params->{user_id} eq $params->{user}) { - $out->{text} .= ' You cannot change your own permissions.'; - $out->{ra_out}{permission_changed} = \0; - } else { - $permissionLevel->permission($params->{permission}); - $db->putPermissionLevel($permissionLevel); - $out->{text} .= ' Permissions updated.'; - $out->{ra_out}{user}{permission} = $permissionLevel->{permission}; - } - } else { - $out->{ra_out}{permission_changed} = \0; - } - - $out->{ra_out}{password_changed} = \0; - - # If the new_password param is set and not equal to the empty string and not all spaces, - # then change the password or set the password if it is not set. - if (defined $params->{new_password} && $params->{new_password} =~ /\S/) { - my $password = cryptPassword($params->{new_password} =~ s/^\s*|\s*$//gr); - my $dbPassword = $db->getPassword($params->{user_id}); - if ($dbPassword) { - $dbPassword->password($password); - $db->putPassword($dbPassword); - } else { - $dbPassword = $db->newPassword(user_id => $params->{user_id}, password => $password); - $db->addPassword($dbPassword); - } - $out->{text} .= ' Password changed.'; - $out->{ra_out}{password_changed} = \1; - } - - addLog($ce, "User edited: $out->{text}"); - return $out; -} - -sub changeUserPassword { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $ce = $self->ce; - - # Make sure course actions are enabled - die "Course actions disabled by configuration.\n" unless $ce->{webservices}{enableCourseActions}; - - # Check parameters. - die "The user_id parameter is required\n" unless ($params->{user_id} && $params->{user_id} =~ /\S/); - die "The new_password parameter is required\n" - unless defined $params->{new_password} && $params->{new_password} =~ /\S/; - - my $User = $db->getUser($params->{user_id}); - die "User $params->{user_id} not found.\n" unless $User; - - # It has already been checked that the user has permission to modify user data. Get the permission level here, so - # that it can be verified that the permission level of the user being edited is less than or equal to that of the - # one doing the editing. - my $callerPermission = $db->getPermissionLevel($params->{user}); - my $permissionLevel = $db->getPermissionLevel($params->{user_id}); - die "You do not have permission to change the password for $params->{user_id}\n" - unless ($callerPermission - && $permissionLevel - && $callerPermission->{permission} >= $permissionLevel->{permission}); - - my $password = cryptPassword($params->{new_password} =~ s/^\s*|\s*$//gr); - - # Change the password or set the password if it is not set. - my $dbPassword = $db->getPassword($User->user_id); - if ($dbPassword) { - $dbPassword->password($password); - $db->putPassword($dbPassword); - } else { - $dbPassword = $db->newPassword(user_id => $params->{user_id}, password => $password); - $db->addPassword($dbPassword); - } - - addLog($ce, "New password set for $params->{user_id}"); - return { text => "New password set for $params->{user_id}" }; -} - -sub addLog { - my ($ce, $msg) = @_; - return unless $ce->{webservices}{enableCourseActionsLog}; - - my ($sec, $msec) = gettimeofday; - my $date = time2str("%a %b %d %H:%M:%S.$msec %Y", $sec); - - if (open my $f, '>>', $ce->{webservices}{courseActionsLogfile}) { - print $f "[$date] $msg\n"; - close $f; - } else { - debug(qq{Error: Unable to open web services log file "$ce->{webservices}{courseActionsLogfile}": $!}); - } - return; -} - -sub assignVisibleSets { - my ($db, $userID) = @_; - my @globalSetIDs = $db->listGlobalSets; - my @GlobalSets = $db->getGlobalSets(@globalSetIDs); - - my $i = -1; - for my $GlobalSet (@GlobalSets) { - $i++; - if (not defined $GlobalSet) { - debug("Record not found for global set $globalSetIDs[$i]"); - next; - } - if (!$GlobalSet->visible) { - next; - } - - # assign set to user - my $setID = $GlobalSet->set_id; - my $UserSet = $db->newUserSet; - $UserSet->user_id($userID); - $UserSet->set_id($setID); - my @results; - my $set_assigned = 0; - eval { $db->addUserSet($UserSet) }; - - return 0 if $@ && !WeBWorK::DB::Ex::RecordExists->caught; - - # assign problem - my @GlobalProblems = grep { defined $_ } $db->getAllGlobalProblems($setID); - for my $GlobalProblem (@GlobalProblems) { - my $seed = int(rand(2423)) + 36; - my $UserProblem = $db->newUserProblem; - $UserProblem->user_id($userID); - $UserProblem->set_id($GlobalProblem->set_id); - $UserProblem->problem_id($GlobalProblem->problem_id); - initializeUserProblem($UserProblem, $seed); - eval { $db->addUserProblem($UserProblem) }; - return 0 if $@ && !WeBWorK::DB::Ex::RecordExists->caught; - } - } - - return 0; -} - -sub getCourseSettings { - my ($invocant, $self, $params) = @_; - my $ce = $self->ce; - my $ConfigValues = getConfigValues($ce); - - for my $oneConfig (@$ConfigValues) { - for my $hash (@$oneConfig) { - next unless ref $hash eq 'HASH'; - my $value; - if (defined $hash->{var}) { - my @keys = $hash->{var} =~ m/([^{}]+)/g; - next unless @keys; - - $value = $ce; - for (@keys) { $value = $value->{$_}; } - } else { - $value = $self->db->getSettingValue($self->{setting}); - } - $hash->{value} = $value if defined $value; - } - } - - push( - @$ConfigValues, - [ - 'tz_abbr', - DateTime::TimeZone->new(name => $ce->{siteDefaults}->{timezone})->short_name_for_datetime(DateTime->now) - ] - ); - - return { - ra_out => $ConfigValues, - text => 'Successfully found the course settings' - }; -} - -sub updateSetting { - my ($invocant, $self, $params) = @_; - my $ce = $self->ce; - - # FIXME: There is no check in this method that the var and value passed in are valid. - my $setVar = $params->{var}; - my $setValue = $params->{value}; - - my $filename = "$ce->{courseDirs}{root}/simple.conf"; - - my $fileoutput = "#!perl -# This file is automatically generated by WeBWorK's web-based -# configuration module. Do not make changes directly to this -# file. It will be overwritten the next time configuration -# changes are saved.\n\n"; - - # Read in the file - open(my $DAT, '<', $filename) - or die "Unable to read $filename. " - . "Ensure that the file exists and the server has write permission for this file.\n"; - my @raw_data = <$DAT>; - close($DAT); - - my $varFound = 0; - - for my $line (@raw_data) { - chomp $line; - if ($line =~ /^\$/) { - my @tmp = split(/\$/, $line); - my ($var, $value) = split(/\s+=\s+/, $tmp[1]); - if ($var eq $setVar) { - $fileoutput .= "\$$var = $setValue;\n"; - $varFound = 1; - } else { - # The value includes the semicolon that hopefully was in the file. - $fileoutput .= "\$$var = $value\n"; - } - } - } - - if (!$varFound) { - $fileoutput .= "\$$setVar = $setValue;\n"; - } - - open(my $OUTPUTFILE, '>', $filename) - or die "Unable to write to $filename. Ensure that the server has write permission for this file.\n"; - print $OUTPUTFILE $fileoutput; - close $OUTPUTFILE; - - return { text => 'Successfully updated course setting' }; -} - -# This saves a file to the course's templates directory. -sub saveFile { - my ($invocant, $self, $params) = @_; - - my $c = $self->c; - my $ce = $self->ce; - - my $outputFilePath = $params->{outputFilePath}; - - my $writeFileErrors; - if ($outputFilePath && $outputFilePath =~ /\S/) { - return { - ra_out => 0, - text => $c->maketext( - 'File not saved. The file "[_1]" is not contained in the templates directory!', - $outputFilePath - ) - } - unless path_is_subdir($outputFilePath, $ce->{courseDirs}{templates}, 1); - - $outputFilePath = "$ce->{courseDirs}{templates}/$outputFilePath" unless $outputFilePath =~ m|^/|; - - # Make sure any missing directories are created. - surePathToFile($ce->{courseDirs}{templates}, $outputFilePath); - - # Save the file. - open(my $outfile, '>:encoding(UTF-8)', $outputFilePath) - or return { - ra_out => 0, - text => $c->maketext('File not saved. Failed to open "[_1]" for writing.', $outputFilePath) - }; - print $outfile $params->{fileContents}; - close $outfile; - } - - return { - ra_out => 1, - text => $c->maketext('Saved to file "[_1]"', $outputFilePath =~ s/$ce->{courseDirs}{templates}/[TMPL]/r) - }; -} - -sub getCurrentServerTime { - my ($invocant, $self, $params) = @_; - - return { - ra_out => { currentServerTime => $self->c->submitTime }, - text => 'Current server time' - }; -} - -1; diff --git a/lib/WebworkWebservice/LibraryActions.pm b/lib/WebworkWebservice/LibraryActions.pm deleted file mode 100644 index e55cfb30a6..0000000000 --- a/lib/WebworkWebservice/LibraryActions.pm +++ /dev/null @@ -1,200 +0,0 @@ -# Web service which fetches WeBWorK problems from a library. -package WebworkWebservice::LibraryActions; - -use strict; -use warnings; - -use File::Find; - -use WeBWorK::Utils::ListingDB; -use WeBWorK::CourseEnvironment; - -# Idea from http://www.perlmonks.org/index.pl?node=How%20to%20map%20a%20directory%20tree%20to%20a%20perl%20hash%20tree -sub build_tree { - my ($dirPath) = @_; - my $tree = {}; - my $node = $tree; - my @s; - find( - { - wanted => sub { - unless ($File::Find::dir =~ /.svn/ || $File::Find::name =~ /.svn/) { - $node = (pop @s)->[1] while @s and $File::Find::dir ne $s[-1][0]; - return $node->{$_} = -s if -f; - push @s, [ $File::Find::name, $node ]; - $node = $node->{$_} = {}; - } - }, - follow_fast => 1 - }, - $dirPath - ); - return { $dirPath => $tree->{'.'} }; -} - -sub listLib { - my ($invocant, $self, $rh) = @_; - my $out = {}; - $rh->{library_name} =~ s|^/||; - my $dirPath = $self->ce->{courseDirs}{templates} . '/' . $rh->{library_name}; - my $maxdepth = $rh->{maxdepth}; - my $dirPath2 = $dirPath . (($rh->{dirPath}) ? '/' . $rh->{dirPath} : ''); - - my @tare = $dirPath2 =~ m|/|g; - my $tare = @tare; # counts number of '/' in dirPath prefix - my @outListLib; - my %libDirectoryList; - my $depthfinder = sub { # counts depth below the current directory - my $path = shift; - my @count = $path =~ m|/|g; - my $depth = @count; - return $depth - $tare; - }; - my $wanted = sub { # find .pg files - unless ($File::Find::dir =~ /.svn/) { - my $name = $File::Find::name; - if ($name =~ /\S/) { - push(@outListLib, $name) if $name =~ /\.pg/; - } - } - }; - - my $wanted_directory = sub { - $File::Find::prune = 1 if &$depthfinder($File::Find::dir) > $maxdepth; - unless ($File::Find::dir =~ /.svn/) { - my $dir = $File::Find::dir; - if ($dir =~ /\S/) { - $dir =~ s|^$dirPath2/*||; # cut the first directory - - $libDirectoryList{$dir} = {}; - } - } - }; - - my $command = $rh->{command}; - - $command = 'all' unless defined($command); - - $command eq 'all' && do { - $out->{command} = "all -- list all pg files in $dirPath"; - find({ wanted => $wanted, follow_fast => 1 }, $dirPath); - @outListLib = sort @outListLib; - $out->{ra_out} = \@outListLib; - $out->{text} = join("\n", @outListLib); - return $out; - }; - $command eq 'dirOnly' && do { - if (-e $dirPath2 && $dirPath2 !~ m|//|) { - # it turns out that when // occur in path -e will work - # but find will not :-( - find({ wanted => $wanted_directory, follow_fast => 1 }, $dirPath2); - delete $libDirectoryList{''}; - $out->{ra_out} = \%libDirectoryList; - $out->{text} = 'Loaded libraries'; - return $out; - } else { - $out->{error} = "Can't open directory $dirPath2"; - } - }; - $command eq 'buildtree' && do { - my $tree = build_tree($dirPath); - $out->{ra_out} = $tree; - $out->{text} = 'Loaded libraries'; - return $out; - }; - - $command eq 'files' && do { - @outListLib = (); - - if (-e $dirPath2 and $dirPath2 !~ m|//|) { - find({ wanted => $wanted, follow_fast => 1 }, $dirPath2); - @outListLib = sort @outListLib; - $out->{text} = 'Problems loaded'; - $out->{ra_out} = \@outListLib; - } else { - $out->{error} = "Can't open directory $dirPath2"; - } - return $out; - }; - - $out->{error} = "Unrecognized command $command"; - return $out; -} - -# API for searching the OPL database -sub searchLib { - my ($invocant, $self, $rh) = @_; - my $out = {}; - my $ce = $self->ce; - my $subcommand = $rh->{command}; - if ($rh->{library_levels}) { - $self->{level} = [ split(//, $rh->{library_levels}) ]; - } - 'getDBTextbooks' eq $subcommand && do { - my @textbooks = WeBWorK::Utils::ListingDB::getDBTextbooks($self->c); - $out->{ra_out} = \@textbooks; - return $out; - }; - 'getAllDBsubjects' eq $subcommand && do { - my @subjects = WeBWorK::Utils::ListingDB::getAllDBsubjects($self->c); - $out->{ra_out} = \@subjects; - $out->{text} = 'Subjects loaded.'; - return $out; - }; - 'getAllDBchapters' eq $subcommand && do { - my @chaps = WeBWorK::Utils::ListingDB::getAllDBchapters($self->c); - $out->{ra_out} = \@chaps; - $out->{text} = 'Chapters loaded.'; - - return $out; - }; - 'getDBListings' eq $subcommand && do { - my @listings = WeBWorK::Utils::ListingDB::getDBListings($self->c); - my @output = map {"$self->ce->{courseDirs}{templates}/$_->{filepath}"} @listings; - $out->{ra_out} = \@output; - return $out; - }; - 'getSectionListings' eq $subcommand && do { - my @section_listings = WeBWorK::Utils::ListingDB::getAllDBsections($self->c); - $out->{ra_out} = \@section_listings; - $out->{text} = 'Sections loaded.'; - - return $out; - }; - - 'countDBListings' eq $subcommand && do { - my $count = WeBWorK::Utils::ListingDB::countDBListings($self->c); - $out->{text} = 'Count done.'; - $out->{ra_out} = [$count]; - return $out; - }; - - $out->{error} = "Unrecognized command $subcommand"; - return $out; -} - -sub getProblemTags { - my ($invocant, $self, $rh) = @_; - my $out = {}; - my $path = $rh->{command}; - # Get a pointer to a hash of DBchapter, ..., DBsection - my $tags = WeBWorK::Utils::ListingDB::getProblemTags($path); - $out->{ra_out} = $tags; - $out->{text} = 'Tags loaded.'; - - return $out; -} - -sub setProblemTags { - my ($invocant, $self, $rh) = @_; - # result is [success, message] with success = 0 or 1 - my $result = WeBWorK::Utils::ListingDB::setProblemTags( - $rh->{command}, $rh->{library_subject}, $rh->{library_chapter}, - $rh->{library_section}, $rh->{library_levels}, $rh->{library_status} - ); - my $out = {}; - $out->{text} = $result->[1]; - return $out; -} - -1; diff --git a/lib/WebworkWebservice/ProblemActions.pm b/lib/WebworkWebservice/ProblemActions.pm deleted file mode 100644 index f58460857d..0000000000 --- a/lib/WebworkWebservice/ProblemActions.pm +++ /dev/null @@ -1,183 +0,0 @@ -# Web service which manipulates problems and user problems. -package WebworkWebservice::ProblemActions; - -use strict; -use warnings; - -use Data::Structure::Util qw(unbless); - -use WeBWorK::PG::Tidy qw(pgtidy); -use WeBWorK::PG::ConvertToPGML qw(convertToPGML); -use WeBWorK::PG::Critic qw(critiquePGCode); - -sub getUserProblem { - my ($invocant, $self, $params) = @_; - - my $db = $self->db; - - my $userProblem = $db->getUserProblem($params->{user_id}, $params->{set_id}, $params->{problem_id}); - - return { - ra_out => unbless($userProblem), - text => "Loaded problem $params->{problem_id} of set $params->{set_id} for " - . "user $params->{user_id} in course " - . $self->ce->{courseName} . '.' - }; -} - -sub putUserProblem { - my ($invocant, $self, $params) = @_; - - my $db = $self->db; - - my $userProblem = $db->getUserProblem($params->{user_id}, $params->{set_id}, $params->{problem_id}); - if (!$userProblem) { return { text => 'User problem not found.' }; } - - if ($self->c->authz->hasPermissions($self->authen->{user_id}, 'modify_student_data')) { - for ( - 'source_file', 'value', 'max_attempts', 'showMeAnother', - 'showMeAnotherCount', 'prPeriod', 'prCount', 'problem_seed', - 'attempted', 'last_answer', 'num_correct', 'num_incorrect', - 'att_to_open_children', 'counts_parent_grade', 'flags' - ) - { - $userProblem->{$_} = $params->{$_} if defined $params->{$_}; - } - } - - # The status and sub_status are the only things that users with the problem_grader permission can change. - # This method cannot be called without the problem_grader permission. - $userProblem->{status} = $params->{status} if defined $params->{status}; - $userProblem->{sub_status} = $params->{sub_status} if defined $params->{sub_status}; - - # Remove the needs_grading flag if the mark_graded parameter is set. - $userProblem->{flags} =~ s/:needs_grading$// if $params->{mark_graded}; - - eval { $db->putUserProblem($userProblem) }; - if ($@) { return { text => "putUserProblem: $@" }; } - - return { - ra_out => unbless($userProblem), - text => "Updated problem $params->{problem_id} of $params->{set_id} for " - . "user $params->{user_id} in course " - . $self->ce->{courseName} . '.' - }; -} - -sub putProblemVersion { - my ($invocant, $self, $params) = @_; - - my $db = $self->db; - - my $problemVersion = - $db->getProblemVersion($params->{user_id}, $params->{set_id}, $params->{version_id}, $params->{problem_id}); - if (!$problemVersion) { return { text => 'Problem version not found.' }; } - - if ($self->c->authz->hasPermissions($self->authen->{user_id}, 'modify_student_data')) { - for ( - 'source_file', 'value', 'max_attempts', 'showMeAnother', - 'showMeAnotherCount', 'prPeriod', 'prCount', 'problem_seed', - 'attempted', 'last_answer', 'num_correct', 'num_incorrect', - 'att_to_open_children', 'counts_parent_grade', 'flags' - ) - { - $problemVersion->{$_} = $params->{$_} if defined($params->{$_}); - } - } - - # The status and sub_status are the only things that users with the problem_grader permission can change. - # This method cannot be called without the problem_grader permission. - $problemVersion->{status} = $params->{status} if defined $params->{status}; - $problemVersion->{sub_status} = $params->{sub_status} if defined $params->{sub_status}; - - # Remove the needs_grading flag if the mark_graded parameter is set. - $problemVersion->{flags} =~ s/:needs_grading$// if $params->{mark_graded}; - - eval { $db->putProblemVersion($problemVersion) }; - if ($@) { return { text => "putProblemVersion: $@" }; } - - return { - ra_out => unbless($problemVersion), - text => "Updated problem $params->{problem_id} of $params->{set_id},v$params->{version_id} " - . "for user $params->{user_id} in course " - . $self->ce->{courseName} . '.' - }; -} - -sub putPastAnswer { - my ($invocant, $self, $params) = @_; - - my $db = $self->db; - - my $pastAnswer = $db->getPastAnswer($params->{answer_id}); - if (!$pastAnswer) { return { text => 'Past answer not found.' }; } - - $pastAnswer->{user_id} = $params->{user_id} if $params->{user_id}; - - if ($self->c->authz->hasPermissions($self->authen->{user_id}, 'modify_student_data')) { - for ( - 'set_id', 'problem_id', 'source_file', 'timestamp', - 'scores', 'answer_string', 'comment_string', 'problem_seed' - ) - { - $pastAnswer->{$_} = $params->{$_} if defined($params->{$_}); - } - } - - # The comment_string is the only thing that users with the problem_grader permission can change. - # This method cannot be called without the problem_grader permission. - $pastAnswer->{comment_string} = $params->{comment_string} if defined $params->{comment_string}; - - eval { $db->putPastAnswer($pastAnswer) }; - if ($@) { return { text => "putPastAnswer $@" }; } - - return { - ra_out => unbless($pastAnswer), - text => - "Updated answer $params->{answer_id} for problem $pastAnswer->{problem_id} of $pastAnswer->{set_id} " - . "for user $pastAnswer->{user_id} in course " - . $self->ce->{courseName} . '.' - }; -} - -sub tidyPGCode { - my ($invocant, $self, $params) = @_; - - local @ARGV = (); - - my $code = $params->{pgCode}; - my $tidiedPGCode; - my $errors; - - my $result = pgtidy(source => \$code, destination => \$tidiedPGCode, errorfile => \$errors); - - return { - ra_out => { tidiedPGCode => $tidiedPGCode, status => $result, errors => $errors }, - text => 'Tidied code' - }; -} - -sub convertCodeToPGML { - my ($invocant, $self, $params) = @_; - - return { - ra_out => convertToPGML($params->{pgCode}), - text => 'Converted to PGML' - }; -} - -sub runPGCritic { - my ($invocant, $self, $params) = @_; - - return { - ra_out => { - html => $self->c->render_to_string( - template => 'ContentGenerator/Instructor/PGProblemEditor/pg_critic', - violations => [ critiquePGCode($params->{pgCode}) ] - ) - }, - text => 'The script pg-critic has been run successfully.' - }; -} - -1; diff --git a/lib/WebworkWebservice/RenderProblem.pm b/lib/WebworkWebservice/RenderProblem.pm deleted file mode 100644 index 6640283ea8..0000000000 --- a/lib/WebworkWebservice/RenderProblem.pm +++ /dev/null @@ -1,338 +0,0 @@ -package WebworkWebservice::RenderProblem; - -use strict; -use warnings; - -use Future::AsyncAwait; -use Benchmark; -use Mojo::Util qw(url_unescape); - -use WeBWorK::Debug qw(debug); -use WeBWorK::CourseEnvironment; -use WeBWorK::DB; -use WeBWorK::DB::Utils qw(global2user fake_set fake_problem); -use WeBWorK::Utils qw(decode_utf8_base64); -use WeBWorK::Utils::Files qw(readFile path_is_subdir); -use WeBWorK::Utils::Rendering qw(renderPG); - -our $UNIT_TESTS_ON = 0; - -async sub renderProblem { - my ($invocant, $ws) = @_; - - my $rh = $ws->{inputs_ref}; - - # $WeBWorK::Debug::Enabled needs to be checked, otherwise pretty_print_rh($rh) is called regardless of if debgging - # is enabled. That is an expensive method to always call here. - debug(pretty_print_rh($rh)) if $WeBWorK::Debug::Enabled; - - my $ce = $ws->ce; - - if ($rh->{problemSource} || $rh->{rawProblemSource} || $rh->{uriEncodedProblemSource}) { - # If the problem source is provided, check user is allow to render problem source. - unless ($ws->authz->hasPermissions($rh->{user}, 'webservice_render_source')) { - $ws->error_string(__PACKAGE__ . ": User $rh->{user} does not have permission to render problem source."); - return {}; - } - } elsif (defined $rh->{sourceFilePath} && $rh->{sourceFilePath} =~ /\S/) { - # If the source file path is provided, ensure it is contained in the course's templates directory. - unless (path_is_subdir( - $ce->{courseDirs}{templates} . '/' . $rh->{sourceFilePath}, - $ce->{courseDirs}{templates} - )) - { - $ws->error_string(__PACKAGE__ . ": Source file path is unsafe."); - return {}; - } - } - - my $problemSeed = $rh->{problemSeed} // '1234'; - - my $beginTime = Benchmark->new; - - my $db = $ws->db; - - # Determine an effective user for this interaction or create one if it is not given. - # Use effectiveUser if given, and $rh->{user} otherwise. - my $effectiveUserName; - if (defined $rh->{effectiveUser} && $rh->{effectiveUser} =~ /\S/) { - $effectiveUserName = $rh->{effectiveUser}; - } else { - $effectiveUserName = $rh->{user}; - } - - if ($UNIT_TESTS_ON) { - print STDERR "RenderProblem.pm: user = $rh->{user}\n"; - print STDERR "RenderProblem.pm: courseName = $rh->{courseID}\n"; - print STDERR "RenderProblem.pm: effectiveUserName = $effectiveUserName\n"; - print STDERR 'environment fileName', $rh->{fileName}, "\n"; - } - - # The effectiveUser is the student this problem version was written for - # The user might also be the effective user but it could be - # an instructor checking out how well the problem is working. - - my $effectiveUser = $db->getUser($effectiveUserName); - my $effectiveUserPermissionLevel; - my $effectiveUserPassword; - unless (defined $effectiveUser) { - $effectiveUser = $db->newUser; - $effectiveUserPermissionLevel = $db->newPermissionLevel; - $effectiveUserPassword = $db->newPassword; - $effectiveUser->user_id($effectiveUserName); - $effectiveUserPermissionLevel->user_id($effectiveUserName); - $effectiveUserPassword->user_id($effectiveUserName); - $effectiveUserPassword->password(''); - $effectiveUser->last_name($rh->{studentName} || 'foobar'); - $effectiveUser->first_name(''); - $effectiveUser->student_id($rh->{studentID} || 'foobar'); - $effectiveUser->email_address($rh->{email} || ''); - $effectiveUser->section($rh->{section} || ''); - $effectiveUser->recitation($rh->{recitation} || ''); - $effectiveUser->comment(''); - $effectiveUser->status('C'); - $effectiveUserPermissionLevel->permission(0); - } - - # Insure that set and problem are defined. Define the set and problem information from data in the environment if - # necessary. - my $setName = $rh->{set_id} // $rh->{setNumber} // ''; - - my $setVersionId = $rh->{version_id} || 0; - - my $problemNumber = $rh->{probNum} // 0; - my $psvn = $rh->{psvn} // 1234; - my $problemValue = $rh->{problemValue} // 1; - my $lastAnswer = ''; - - debug('effectiveUserName: ' . $effectiveUserName); - debug('setName: ' . $setName); - debug('setVersionId: ' . $setVersionId); - debug('problemNumber: ' . $problemNumber); - debug('problemSeed:' . $problemSeed); - debug('psvn: ' . $psvn); - debug('problemValue: ' . $problemValue); - - my $setRecord = - $setVersionId - ? $db->getMergedSetVersion($effectiveUserName, $setName, $setVersionId) - : $db->getMergedSet($effectiveUserName, $setName); - - if (defined $setRecord && ref $setRecord) { - # If an actual set from the database is used, the passed in psvn is ignored. - # So save the actual psvn used and pass that on to the renderer. - $psvn = $setRecord->psvn; - } else { - # if a User Set does not exist for this user and this set - # then we check the Global Set - # if that does not exist we create a fake set - # if it does, we add fake user data - my $userSetClass = $db->{set_user}{record}; - my $globalSet = $db->getGlobalSet($setName); - - if (!defined $globalSet) { - $setRecord = fake_set($db); - } else { - $setRecord = global2user($userSetClass, $globalSet); - } - - # Initializations - $setRecord->set_id($setName); - $setRecord->set_header(''); - $setRecord->hardcopy_header('defaultHeader'); - $setRecord->open_date(time - 60 * 60 * 24 * 7); # one week ago - $setRecord->due_date(time + 60 * 60 * 24 * 7 * 2); # in two weeks - $setRecord->answer_date(time + 60 * 60 * 24 * 7 * 3); # in three weeks - $setRecord->psvn($rh->{psvn} // 1234); - } - - # obtain the merged problem for $effectiveUser - my $problemRecord = - !$problemNumber ? undef - : $setVersionId ? $db->getMergedProblemVersion($effectiveUserName, $setName, $setVersionId, $problemNumber) - : $db->getMergedProblem($effectiveUserName, $setName, $problemNumber); - - if (defined $problemRecord) { - # If a problem from the database is used, the passed in problem seed is ignored. - # So save the actual seed used and pass that on to the renderer. - $problemSeed = $problemRecord->problem_seed; - } else { - # If that is not yet defined obtain the global problem, - # convert it to a user problem, and add fake user data - my $userProblemClass = $db->{problem_user}{record}; - my $globalProblem = $db->getGlobalProblem($setName, $problemNumber); - # if the global problem doesn't exist either, bail! - if (not defined $globalProblem) { - $problemRecord = fake_problem($db); - } else { - $problemRecord = global2user($userProblemClass, $globalProblem); - } - # initializations - $problemRecord->user_id($effectiveUserName); - $problemRecord->problem_id($problemNumber); - $problemRecord->set_id($setName); - $problemRecord->problem_seed($problemSeed); - $problemRecord->status(0); - $problemRecord->value($problemValue); - # We are faking it - $problemRecord->attempted(2000); - $problemRecord->num_correct(1000); - $problemRecord->num_incorrect(1000); - $problemRecord->last_answer($lastAnswer); - } - - if ($UNIT_TESTS_ON) { - print STDERR 'setRecord is ', pretty_print_rh($setRecord); - print STDERR 'template directory path ', $ce->{courseDirs}{templates}, "\n"; - print STDERR 'RenderProblem.pm: source file is ', $rh->{sourceFilePath}, "\n"; - print STDERR "RenderProblem.pm: problem source is included in the request \n" - if defined($rh->{problemSource}) && $rh->{problemSource}; - } - - # Initialize problem source - my $r_problem_source; - if ($rh->{problemSource}) { - $r_problem_source = \(decode_utf8_base64($rh->{problemSource}) =~ tr/\r/\n/r); - $problemRecord->source_file($rh->{fileName} ? $rh->{fileName} : $rh->{sourceFilePath}); - } elsif ($rh->{rawProblemSource}) { - $r_problem_source = \$rh->{rawProblemSource}; - $problemRecord->source_file($rh->{fileName} ? $rh->{fileName} : $rh->{sourceFilePath}); - } elsif ($rh->{uriEncodedProblemSource}) { - $r_problem_source = \(url_unescape($rh->{uriEncodedProblemSource})); - $problemRecord->source_file($rh->{fileName} ? $rh->{fileName} : $rh->{sourceFilePath}); - } elsif (defined $rh->{sourceFilePath} && $rh->{sourceFilePath} =~ /\S/) { - $problemRecord->source_file($rh->{sourceFilePath}); - $r_problem_source = \(readFile($ce->{courseDirs}{templates} . '/' . $rh->{sourceFilePath})); - } - - if ($UNIT_TESTS_ON) { - print STDERR 'template directory path ', $ce->{courseDirs}{templates}, "\n"; - print STDERR 'RenderProblem.pm: source file is ', $problemRecord->source_file, "\n"; - print STDERR "RenderProblem.pm: problem source is included in the request \n" if defined($rh->{problemSource}); - } - # now we're sure we have valid UserSet and UserProblem objects - - # Other initializations - my $translationOptions = { - displayMode => $rh->{displayMode} // 'MathJax', - showHints => $rh->{showHints}, - showSolutions => $rh->{showSolutions}, - processAnswers => $rh->{processAnswers} // 1, - catchWarnings => 1, - r_source => $r_problem_source, - problemUUID => $rh->{problemUUID} // 0, - permissionLevel => $rh->{permissionLevel} || 0, - effectivePermissionLevel => $rh->{effectivePermissionLevel} || $rh->{permissionLevel} || 0, - useMathQuill => $ce->{pg}{specialPGEnvironmentVars}{entryAssist} eq 'MathQuill', - useMathView => $ce->{pg}{specialPGEnvironmentVars}{entryAssist} eq 'MathView', - isInstructor => $rh->{isInstructor} // 0, - forceScaffoldsOpen => $rh->{WWcorrectAnsOnly} ? 1 : ($rh->{forceScaffoldsOpen} // 0), - QUIZ_PREFIX => $rh->{answerPrefix}, - showFeedback => $rh->{previewAnswers} || $rh->{WWsubmit} || $rh->{WWcorrectAns}, - showAttemptAnswers => $rh->{WWcorrectAnsOnly} ? 0 - : ($rh->{showAttemptAnswers} // $ce->{pg}{options}{showEvaluatedAnswers}), - showAttemptPreviews => ( - $rh->{WWcorrectAnsOnly} ? 0 - : ($rh->{showAttemptPreviews} // ($rh->{previewAnswers} || $rh->{WWsubmit} || $rh->{WWcorrectAns})) - ), - showAttemptResults => $rh->{showAttemptResults} // ($rh->{WWsubmit} || $rh->{WWcorrectAns}), - forceShowAttemptResults => ( - $rh->{WWcorrectAnsOnly} ? 1 - : ( - $rh->{forceShowAttemptResults} - || ($rh->{isInstructor} - && ($rh->{showAttemptResults} // ($rh->{WWsubmit} || $rh->{WWcorrectAns}))) - ) - ), - showMessages => ( - $rh->{WWcorrectAnsOnly} ? 0 - : ($rh->{showMessages} // ($rh->{previewAnswers} || $rh->{WWsubmit} || $rh->{WWcorrectAns})) - ), - showCorrectAnswers => - ($rh->{WWcorrectAnsOnly} ? 1 : ($rh->{showCorrectAnswers} // ($rh->{WWcorrectAns} ? 2 : 0))), - debuggingOptions => { - show_resource_info => $rh->{show_resource_info} // 0, - view_problem_debugging_info => $rh->{view_problem_debugging_info} // 0, - show_pg_info => $rh->{show_pg_info} // 0, - show_answer_hash_info => $rh->{show_answer_hash_info} // 0, - show_answer_group_info => $rh->{show_answer_group_info} // 0 - }, - defined $rh->{problem_data} && $rh->{problem_data} ne '' ? (problemData => $rh->{problem_data}) : () - }; - - $ce->{pg}{specialPGEnvironmentVars}{problemPreamble} = { TeX => '', HTML => '' } if $rh->{noprepostambles}; - $ce->{pg}{specialPGEnvironmentVars}{problemPostamble} = { TeX => '', HTML => '' } if $rh->{noprepostambles}; - - my $pg = - await renderPG($ws->c, $effectiveUser, $setRecord, $problemRecord, $setRecord->psvn, $rh, $translationOptions); - - # New version of output: - return { - text => $pg->{body_text}, - header_text => $pg->{head_text}, - post_header_text => $pg->{post_header_text}, - answers => $pg->{answers}, - errors => $pg->{errors}, - pg_warnings => $pg->{warnings}, - PG_ANSWERS_HASH => $pg->{PG_ANSWERS_HASH}, - PERSISTENCE_HASH => $pg->{PERSISTENCE_HASH}, - problem_result => $pg->{result}, - problem_state => $pg->{state}, - flags => $pg->{flags}, - psvn => $psvn, - problem_seed => $problemSeed, - resource_list => $pg->{resource_list}, - warning_messages => ref $pg->{warning_messages} eq 'ARRAY' ? $pg->{warning_messages} : [], - debug_messages => ref $pg->{debug_messages} eq 'ARRAY' ? $pg->{debug_messages} : [], - compute_time => logTimingInfo($beginTime, Benchmark->new), - }; -} - -sub logTimingInfo { - my ($beginTime, $endTime) = @_; - return Benchmark::timestr(Benchmark::timediff($endTime, $beginTime)); -} - -sub pretty_print_rh { - shift if UNIVERSAL::isa($_[0] => __PACKAGE__); - my $rh = shift; - return '' unless defined $rh; - my $indent = shift || 0; - - my $out = ''; - return $out if $indent > 10; - my $type = ref($rh); - - if (defined($type) && $type) { - $out .= " type = $type; "; - } elsif (not defined($rh)) { - $out .= ' type = scalar; '; - } - if (ref $rh eq 'HASH' || eval { %$rh && 1 }) { - $out .= "{\n"; - $indent++; - foreach my $key (sort keys %{$rh}) { - $out .= ' ' x $indent . "$key => " . pretty_print_rh($rh->{$key}, $indent) . "\n"; - } - $indent--; - $out .= "\n" . ' ' x $indent . "}\n"; - - } elsif (ref($rh) =~ /ARRAY/ || "$rh" =~ /ARRAY/) { - $out .= ' ( '; - foreach my $elem (@{$rh}) { - $out .= pretty_print_rh($elem, $indent); - - } - $out .= " ) \n"; - } elsif (ref($rh) =~ /SCALAR/) { - $out .= 'scalar reference ' . ${$rh}; - } elsif (ref($rh) =~ /Base64/) { - $out .= 'base64 reference ' . $$rh; - } else { - $out .= $rh; - } - - return $out . ' '; -} - -1; diff --git a/lib/WebworkWebservice/SetActions.pm b/lib/WebworkWebservice/SetActions.pm deleted file mode 100644 index f43ea5a1c8..0000000000 --- a/lib/WebworkWebservice/SetActions.pm +++ /dev/null @@ -1,522 +0,0 @@ -# Web service which fetches, adds, removes and moves WeBWorK problems when working with a Set. -package WebworkWebservice::SetActions; - -use strict; -use warnings; - -use Carp; -use Mojo::JSON qw(from_json to_json); -use Data::Structure::Util qw(unbless); - -use WeBWorK::Utils qw(max); -use WeBWorK::Utils::Instructor qw(assignProblemToAllSetUsers assignSetToGivenUsers); -use WeBWorK::Utils::JITAR qw(seq_to_jitar_id jitar_id_to_seq); -use WeBWorK::Debug qw(debug); -use WeBWorK::DB::Utils qw(initializeUserProblem); - -sub listGlobalSets { - my ($invocant, $self) = @_; - - debug('in listGlobalSets'); - - my @found_sets = $self->db->listGlobalSets; - return { ra_out => \@found_sets, text => 'Loaded sets for course: ' . $self->ce->{courseName} }; -} - -# This returns an array of problems (path,value,problem_id, which is weight) -sub listGlobalSetProblems { - my ($invocant, $self, $params) = @_; - - debug('listGlobalSetProblems loading problems for ' . $params->{set_id}); - - my $db = $self->db; - - # If a command is passed, then we want relative paths rather than absolute paths. - # Do that by setting templateDir to the empty string. - my $templateDir = $params->{command} ? '' : ($self->ce->{courseDirs}{templates} . '/'); - - my @found_problems = $db->listGlobalProblems($params->{set_id}); - - my @problems; - for my $problem (@found_problems) { - my $problemRecord = $db->getGlobalProblem($params->{set_id}, $problem); - return { text => "global $problem for set $params->{set_id} not found." } unless $problemRecord; - push @problems, - { - path => $templateDir . $problemRecord->source_file, - problem_id => $problemRecord->{problem_id}, - value => $problemRecord->{value} - }; - } - - return { ra_out => \@problems, text => "Loaded Problems for set: $params->{set_id}" }; -} - -# This returns all problem sets of a course. -sub getSets { - my ($invocant, $self, $params) = @_; - - debug('in getSets'); - - my $db = $self->db; - - my @found_sets = $db->listGlobalSets; - my @all_sets = map { unbless($_) } $db->getGlobalSets(@found_sets); - - # Add a list of set users to the return data. - for my $set (@all_sets) { - my @users = $db->listSetUsers($set->{set_id}); - $set->{assigned_users} = \@users; - } - - return { ra_out => \@all_sets, text => 'Sets for course: ' . $self->ce->{courseName} }; -} - -# This returns all problem sets of a course for a given user. -# The set is stored in the set_id and the user in user_id -sub getUserSets { - my ($invocant, $self, $params) = @_; - - debug('in getUserSets'); - - my $db = $self->db; - my @userSetNames = $db->listUserSets($params->{user_id}); - my @userSets = map { unbless($_) } $db->getGlobalSets(@userSetNames); - - return { - ra_out => \@userSets, - text => "User sets for user $params->{user_id} in course " . $self->ce->{courseName} - }; -} - -# This returns a single problem set with name stored in set_id -sub getSet { - my ($invocant, $self, $params) = @_; - - my $db = $self->db; - my $set = unbless($db->getGlobalSet($params->{set_id})); - - return { ra_out => $set, text => "Loaded set $params->{set_id} in " . $self->ce->{courseName} }; -} - -sub updateSetProperties { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - - my $set = $db->getGlobalSet($params->{set_id}); - $set->set_header($params->{set_header}); - $set->hardcopy_header($params->{hardcopy_header}); - $set->open_date($params->{open_date}); - $set->due_date($params->{due_date}); - $set->answer_date($params->{answer_date}); - $set->visible($params->{visible}); - $set->enable_reduced_scoring($params->{enable_reduced_scoring}); - $set->assignment_type($params->{assignment_type}); - $set->attempts_per_version($params->{attempts_per_version}); - $set->time_interval($params->{time_interval}); - $set->versions_per_interval($params->{versions_per_interval}); - $set->version_time_limit($params->{version_time_limit}); - $set->version_creation_time($params->{version_creation_time}); - $set->problem_randorder($params->{problem_randorder}); - $set->version_last_attempt_time($params->{version_last_attempt_time}); - $set->problems_per_page($params->{problems_per_page}); - $set->hide_score($params->{hide_score}); - $set->hide_score_by_problem($params->{hide_score_by_problem}); - $set->hide_work($params->{hide_work}); - $set->time_limit_cap($params->{time_limit_cap}); - $set->restrict_ip($params->{restrict_ip}); - $set->relax_restrict_ip($params->{relax_restrict_ip}); - $set->restricted_login_proctor($params->{restricted_login_proctor}); - - $db->putGlobalSet($set); - - # Next update the assigned_users list - - # first, get the current list of users. - - my @usersForTheSetBefore = $db->listSetUsers($params->{set_id}); - - debug(to_json(\@usersForTheSetBefore)); - - # then determine those currently in the list. - - my @usersForTheSetNow = split(/,/, $params->{assigned_users}); - - # The following seems to work if there are only additions or subtractions from the assigned_users field. - # Perhaps a better way to do this is to check users that are new or missing and add or delete them. - - # if the number of users have grown, then add them. - - debug(to_json(\@usersForTheSetNow)); - - # determine users to be added - - for my $user (@usersForTheSetNow) { - if (!(grep {/^$user$/} @usersForTheSetBefore)) { - my $userSet = $db->newUserSet; - $userSet->user_id($user); - $userSet->set_id($params->{set_id}); - $db->addUserSet($userSet); - } - } - - # delete users that are in the set before but not now. - - for my $user (@usersForTheSetBefore) { - if (!(grep {/^$user$/} @usersForTheSetNow)) { - $db->deleteUserSet($user, $params->{set_id}); - } - } - - return { ra_out => unbless($set), text => "Successfully updated set $params->{set_id}" }; -} - -sub listSetUsers { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - - my @users = $db->listSetUsers($params->{set_id}); - return { ra_out => \@users, text => "Successfully returned the users for set $params->{set_id}" }; -} - -sub createNewSet { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $out = {}; - - debug('in createNewSet'); - - if ($params->{set_id} !~ /^[\w .-]*$/) { - $out->{text} = 'Invalid set name'; - $out->{ra_out} = { success => \0 }; - } else { - my $newSetName = $params->{set_id}; - $newSetName =~ s/\s/_/g; - - if (defined($db->getGlobalSet($newSetName))) { - $out->{text} = "The set name '$newSetName' is already in use. " - . 'Pick a different name if you would like to start a new set.'; - $out->{ra_out} = { success => \0 }; - } else { - my $now = time; - my $newSetRecord = $db->newGlobalSet; - $newSetRecord->set_id($newSetName); - $newSetRecord->set_header('defaultHeader'); - $newSetRecord->hardcopy_header('defaultHeader'); - $newSetRecord->open_date($params->{open_date} // $now); - $newSetRecord->due_date($params->{due_date} // ($now + 1209600)); - $newSetRecord->answer_date($params->{answer_date} // ($now + 1209600)); - $newSetRecord->reduced_scoring_date($params->{reduced_scoring_date} // ($now + 1209600)); - $newSetRecord->visible($params->{visible} // 1); - $newSetRecord->enable_reduced_scoring($params->{enable_reduced_scoring} // 0); - $newSetRecord->assignment_type($params->{assignment_type} // 'default'); - $newSetRecord->description($params->{description}); - $newSetRecord->restricted_release($params->{restricted_release}); - $newSetRecord->restricted_status($params->{restricted_status} // 1); - $newSetRecord->attempts_per_version($params->{attempts_per_version} // 0); - $newSetRecord->time_interval($params->{time_interval} // 0); - $newSetRecord->versions_per_interval($params->{versions_per_interval} // 0); - $newSetRecord->version_time_limit($params->{version_time_limit} // 0); - $newSetRecord->version_creation_time($params->{version_creation_time}); - $newSetRecord->problem_randorder($params->{problem_randorder}); - $newSetRecord->version_last_attempt_time($params->{version_last_attempt_time}); - $newSetRecord->problems_per_page($params->{problems_per_page} // 0); - $newSetRecord->hide_score($params->{hide_score}); - $newSetRecord->hide_score_by_problem($params->{hide_score_by_problem}); - $newSetRecord->hide_work($params->{hide_work}); - $newSetRecord->time_limit_cap($params->{time_limit_cap}); - $newSetRecord->restrict_ip($params->{restrict_ip} // 'No'); - $newSetRecord->relax_restrict_ip($params->{relax_restrict_ip} // 'No'); - $newSetRecord->hide_hint($params->{hide_hint} // 0); - $newSetRecord->restrict_prob_progression($params->{restrict_prob_progression} // 0); - $newSetRecord->email_instructor($params->{email_instructor} // 0); - - $db->addGlobalSet($newSetRecord); - $out->{text} = "Successfully created new set $newSetName"; - $out->{ra_out} = { success => \1 }; - - my $selfassign = $params->{selfassign} // ''; - debug("selfassign: $selfassign"); - $selfassign = '' if ($selfassign =~ /false/i); # deal with javascript false - if ($selfassign) { - debug("Assigning to user: $params->{user}"); - my $userSet = $db->newUserSet; - $userSet->user_id($params->{user}); - $userSet->set_id($newSetName); - $db->addUserSet($userSet); - $out->{text} .= " Set was assigned to $params->{user}."; - } - } - } - return $out; -} - -sub assignSetToUsers { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - - my $setID = $params->{set_id}; - my $GlobalSet = $db->getGlobalSet($params->{set_id}); - - my %setUsers = map { $_ => 1 } $db->listSetUsers($setID); - - debug("users: " . $params->{users}); - my @users = split(',', $params->{users}); - my @usersToAdd; - my @results; - for my $user (@users) { - if ($setUsers{$user}) { - push @results, "set $setID is already assigned to user $user."; - - } else { - push @usersToAdd, $user; - } - } - assignSetToGivenUsers($db, $self->ce, $setID, 1, $db->getUsers(@usersToAdd)); - - return { ra_out => \@results, text => "Successfully assigned users to set $params->{set_id}" }; -} - -sub deleteProblemSet { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $setID = $params->{set_id}; - my $result = $db->deleteGlobalSet($setID); - - # check the result - debug("in deleteProblemSet"); - debug("deleted set: $setID"); - debug($result); - - return { text => "Deleted Problem Set $setID" }; -} - -sub reorderProblems { - my ($invocant, $self, $params) = @_; - - my $db = $self->db; - my $setID = $params->{set_id}; - my @problemList = split(/,/, $params->{probList}); - my $topdir = $self->ce->{courseDirs}{templates}; - - # get all the problems - my @allProblems = $db->getAllGlobalProblems($setID); - - my @probOrder = (); - - for my $problem (@allProblems) { - my $recordFound = 0; - - for (my $i = 0; $i < scalar(@problemList); $i++) { - $problemList[$i] =~ s|^$topdir/*||; - - if ($problem->{source_file} eq $problemList[$i]) { - push(@probOrder, $i + 1); - if ($db->existsGlobalProblem($setID, $i + 1)) { - $problem->problem_id($i + 1); - $db->putGlobalProblem($problem); - debug("updating problem " . $problemList[$i] . " and setting the index to " . ($i + 1)); - - } else { - # delete the problem with the old problem_id and create a new one - $db->deleteGlobalProblem($setID, $problem->{problem_id}); - $problem->problem_id($i + 1); - $db->addGlobalProblem($problem); - - debug("adding new problem " . $problemList[$i] . " and setting the index to " . ($i + 1)); - } - } - $recordFound = 1; - } - die "global " . $problem->{source_file} . " for set $setID not found." unless $recordFound; - - } - - return { text => 'Successfully reordered problems' }; -} - -sub updateProblem { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $setID = $params->{set_id}; - my $path = $params->{problemPath}; - my $topdir = $self->ce->{courseDirs}{templates}; - $path =~ s|^$topdir/*||; - - my @problems = $db->getAllGlobalProblems($setID); - for my $problem (@problems) { - if ($problem->{source_file} eq $path) { - debug($params->{value}); - $problem->value($params->{value}); - $db->putGlobalProblem($problem); - } - } - - return { text => "Updated Problem Set $setID" }; -} - -# This updates the userSet for a problem set (just the open, due and answer dates) -sub updateUserSet { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my @users = split(',', $params->{users}); - - debug($params->{open_date}); - debug($params->{due_date}); - debug($params->{answer_date}); - - for my $userID (@users) { - my $set = $db->getUserSet($userID, $params->{set_id}); - if ($set) { - $set->open_date($params->{open_date}); - $set->due_date($params->{due_date}); - $set->answer_date($params->{answer_date}); - $db->putUserSet($set); - } else { - my $newSet = $db->newUserSet; - $newSet->user_id($userID); - $newSet->set_id($params->{set_id}); - $newSet->open_date($params->{open_date}); - $newSet->due_date($params->{due_date}); - $newSet->answer_date($params->{answer_date}); - - $newSet = $db->addUserSet($newSet); - } - } - - return { - #ra_out => $set, - text => "Successfully updated set $params->{set_id} for users $params->{users}" - }; -} - -sub getSetUserSets { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - - my @setUserIDs = $db->listSetUsers($params->{set_id}); - - my @userData = (); - - for my $user_id (@setUserIDs) { - push(@userData, unbless($db->getUserSet($user_id, $params->{set_id}))); - } - - return { ra_out => \@userData, text => "Returning all users sets for set $params->{set_id}" }; -} - -sub saveUserSets { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - debug($params->{overrides}); - - my @overrides = @{ from_json($params->{overrides}) }; - for my $override (@overrides) { - my $set = $db->getUserSet($override->{user_id}, $params->{set_id}); - if ($override->{open_date}) { $set->{open_date} = $override->{open_date}; } - if ($override->{due_date}) { $set->{due_date} = $override->{due_date}; } - if ($override->{answer_date}) { $set->{answer_date} = $override->{answer_date}; } - $db->putUserSet($set); - } - - return { ra_out => '', text => "Updating the overrides for set $params->{set_id}" }; -} - -sub addProblem { - my ($invocant, $self, $params) = @_; - my $db = $self->db; - my $setName = $params->{set_id}; - - my $file = $params->{problemPath}; - my $topdir = $self->ce->{courseDirs}{templates}; - $file =~ s|^$topdir/*||; - - my $freeProblemID; - my $set = $db->getGlobalSet($setName); - warn "record not found for global set $setName" unless $set; - - # for jitar sets the next problem id is the next top level problem - if ($set->assignment_type eq 'jitar') { - my @problemIDs = $db->listGlobalProblems($setName); - my @seq = (0); - if ($#problemIDs != -1) { - @seq = jitar_id_to_seq($problemIDs[-1]); - } - - $freeProblemID = seq_to_jitar_id($seq[0] + 1); - } else { - $freeProblemID = max($db->listGlobalProblems($setName)) + 1; - } - - my $value_default = $self->ce->{problemDefaults}->{value}; - my $max_attempts_default = $self->ce->{problemDefaults}->{max_attempts}; - my $showMeAnother_default = $self->ce->{problemDefaults}->{showMeAnother}; - my $showHintsAfter_default = $self->ce->{problemDefaults}{showHintsAfter}; - my $att_to_open_children_default = $self->ce->{problemDefaults}->{att_to_open_children}; - my $counts_parent_grade_default = $self->ce->{problemDefaults}->{counts_parent_grade}; - # showMeAnotherCount is the number of times that showMeAnother has been clicked; initially 0 - my $showMeAnotherCount = 0; - - my $prPeriod_default = $self->ce->{problemDefaults}->{prPeriod}; - - my $value = $value_default; - if (defined($params->{value}) and length($params->{value})) { - $value = $params->{value}; - } # 0 is a valid value for $params{value} but we don't want emptystring - - my $maxAttempts = $params->{maxAttempts} || $max_attempts_default; - my $showMeAnother = $params->{showMeAnother} || $showMeAnother_default; - my $showHintsAfter = $params->{showHintsAfter} || $showHintsAfter_default; - my $problemID = $params->{problemID}; - my $countsParentGrade = $params->{counts_parent_grade} || $counts_parent_grade_default; - my $attToOpenChildren = $params->{att_to_open_children} || $att_to_open_children_default; - - my $prPeriod = $prPeriod_default; - if (defined($params->{prPeriod})) { - $prPeriod = $params->{prPeriod}; - } - - unless ($problemID) { - $problemID = $freeProblemID; - } - - my $problemRecord = $db->newGlobalProblem; - $problemRecord->problem_id($problemID); - $problemRecord->set_id($setName); - $problemRecord->source_file($file); - $problemRecord->value($value); - $problemRecord->max_attempts($maxAttempts); - $problemRecord->showMeAnother($showMeAnother); - $problemRecord->showHintsAfter($showHintsAfter); - $problemRecord->{showMeAnotherCount} = $showMeAnotherCount; - $problemRecord->{att_to_open_children} = $attToOpenChildren; - $problemRecord->{counts_parent_grade} = $countsParentGrade; - $problemRecord->prPeriod($prPeriod); - $problemRecord->prCount(0); - $db->addGlobalProblem($problemRecord); - - assignProblemToAllSetUsers($db, $problemRecord); - - return { text => "Problem added to $setName" }; -} - -sub deleteProblem { - my ($invocant, $self, $params) = @_; - - my $db = $self->db; - my $setName = $params->{set_id}; - - my $file = $params->{problemPath}; - my $topdir = $self->ce->{courseDirs}{templates}; - $file =~ s|^$topdir/*||; - - my @setGlobalProblems = $db->getGlobalProblemsWhere({ set_id => $setName }); - for my $problemRecord (@setGlobalProblems) { - if ($problemRecord->source_file eq $file) { - $db->deleteGlobalProblem($setName, $problemRecord->problem_id); - } - } - return { text => "Problem removed from $setName" }; -} - -1; diff --git a/templates/RPCRenderFormats/default.html.ep b/templates/RPCRenderFormats/default.html.ep index 245c24b30a..7eefce473c 100644 --- a/templates/RPCRenderFormats/default.html.ep +++ b/templates/RPCRenderFormats/default.html.ep @@ -116,13 +116,9 @@ %= hidden_field showFooter => $showFooter %= hidden_field extra_header_text => $extra_header_text %= hidden_field problem_data => $problem_data - % if ($ws->{inputs_ref}{answerPrefix}) { - %= hidden_field answerPrefix => $ws->{inputs_ref}{answerPrefix} + % if ($c->req->param('answerPrefix')) { + %= hidden_field answerPrefix => $c->req->param('answerPrefix') % } - % if ($formatName eq 'debug' && $ws->{inputs_ref}{clientDebug}) { - %= hidden_field clientDebug => $ws->{inputs_ref}{clientDebug} - % } - % % if ($displayMode ne 'PTX' && $displayMode ne 'tex') {
% # Submit buttons (all are shown by default except the correct answer only button) @@ -161,13 +157,6 @@ : [], debug_messages => $formatName eq 'debug' && ref $rh_result->{debug_messages} eq 'ARRAY' ? $rh_result->{debug_messages} : [] =%> - % # Additional information output only for the debug format. - % if ($formatName eq 'debug') { - % if ($ws->{inputs_ref}{clientDebug}) { -

Webwork client data

- %== $pretty_print->($ws) - % } - % } % # Translator errors. % if ($rh_result->{flags}{error_flag}) {
diff --git a/templates/RPCRenderFormats/default.json.ep b/templates/RPCRenderFormats/default.json.ep index c9060cc620..5a0950f6ab 100644 --- a/templates/RPCRenderFormats/default.json.ep +++ b/templates/RPCRenderFormats/default.json.ep @@ -72,7 +72,7 @@ % }, % % # Add the current score to the json output - % score => $ws->{inputs_ref}{WWsubmit} && $rh_result->{problem_result} + % score => $c->req->param('WWsubmit') && $rh_result->{problem_result} % ? wwRound(0, $rh_result->{problem_result}{score} * 100) % : 0, %