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') {