Skip to content

Commit

Permalink
Merge branch 'Simon-Initiative:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
dtiwarATS committed Apr 26, 2024
2 parents 6817d94 + ad5afc4 commit d6c90f0
Show file tree
Hide file tree
Showing 24 changed files with 499 additions and 458 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const _TorusAudioBrowser: MediaBrowserComponent = ({ id, label, value, onChange,
)}
</Button>
)}
<a href="#" style={{ marginLeft: '5px', textDecoration: 'underline' }} onClick={openPicker}>
Upload or Link Audio
</a>
</ButtonGroup>

{pickerOpen && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const _TorusImageBrowser: MediaBrowserComponent = ({ id, label, value, onChange,
>
<i className="fa-solid fa-image"></i>
</Button>

<a href="#" style={{ marginLeft: '5px', textDecoration: 'underline' }} onClick={openPicker}>
Upload or Link Image
</a>
Expand Down
14 changes: 8 additions & 6 deletions assets/src/components/activities/logic_lab/LogicLabAuthoring.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ const Authoring: FC<LogicLabAuthoringProps> = (props: LogicLabAuthoringProps) =>
setActivityId(model.activity);
}, [model]);

const labServer = getLabServer(authoringContext);

// Current loading state.
const [loading, setLoading] = useState<'loading' | 'loaded' | 'error'>('loading');
const [servletError, setServletError] = useState(''); // last error from servlet call
Expand All @@ -136,7 +134,8 @@ const Authoring: FC<LogicLabAuthoringProps> = (props: LogicLabAuthoringProps) =>
const signal = controller.signal;
const getActivities = async () => {
setLoading('loading');
const url = new URL('api/v1/activities', labServer);
const server = getLabServer(authoringContext);
const url = new URL('api/v1/activities', server);
const response = await fetch(
url.toString(), // tsc does not allow URL as parameter, contrary to MDM spec.
{
Expand All @@ -160,7 +159,9 @@ const Authoring: FC<LogicLabAuthoringProps> = (props: LogicLabAuthoringProps) =>
getActivities().catch((err) => {
console.error(err);
setLoading('error');
if (err instanceof Error) {
if (err instanceof ReferenceError) {
setServletError('LogicLab server is not configured for this Torus instance.');
} else if (err instanceof Error) {
setServletError(err.message);
} else if (typeof err === 'string') {
setServletError(err);
Expand All @@ -171,7 +172,7 @@ const Authoring: FC<LogicLabAuthoringProps> = (props: LogicLabAuthoringProps) =>

// abort load if component rendering interupted.
return () => controller.abort();
}, []);
}, [authoringContext]);

// Set activity data when there are activities an an id.
useEffect(() => {
Expand Down Expand Up @@ -378,7 +379,8 @@ const Preview: FC<LogicLabAuthoringProps> = ({
const signal = controller.signal;
const getActivity = async () => {
setLoading('loading');
const url = new URL(`api/v1/activities/${model.activity}`, getLabServer(authoringContext));
const server = getLabServer(authoringContext);
const url = new URL(`api/v1/activities/${model.activity}`, server);
const response = await fetch(url.toString(), {
signal,
headers: { Accept: 'application/json' },
Expand Down
228 changes: 136 additions & 92 deletions assets/src/components/activities/logic_lab/LogicLabDelivery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,112 +54,156 @@ const LogicLab: React.FC<LogicLabDeliveryProps> = () => {
let partGuid = activityState.parts[0].attemptGuid; // Moving to higher scope which helps state saving to work.

const onMessage = async (e: MessageEvent) => {
const lab = new URL(getLabServer(context));
const origin = new URL(e.origin);
// filter so we do not process torus events.
if (origin.host === lab.host) {
const msg = e.data;
// only lab messages from this activity for eventual support of multiple problems on a page.
if (isLabMessage(msg) && msg.attemptGuid === activityState.attemptGuid) {
const attemptGuid = activityState.attemptGuid;
switch (msg.messageType) {
// respond to lab score request.
case 'score':
if (mode === 'delivery') {
// only when in delivery
try {
// .dateEvaluated seems to not work as a check to see if part is evaluatable
// Always resetting and saving seems to fix the issue with a second attempt
// not registering grading.
// if (activityState.parts[0].dateEvaluated) {
// if the part has already been evaluated, then
// it is necessary to reset the part to get a new
// partGuid as there can only ever be one evaluation
// per partGuid.
const partResponse = await onResetPart(attemptGuid, partGuid);
partGuid = partResponse.attemptState.attemptGuid;
// import state to new part, luckily the current state
// is already included in the score message so no need
// to maintain it in state.
await onSaveActivity(attemptGuid, [
{
attemptGuid: partGuid,
response: { input: msg.score.input },
},
]);
onSubmitEvaluations(attemptGuid, [
{
score: msg.score.score,
outOf: msg.score.outOf,
feedback: model.feedback[Number(msg.score.complete)],
response: { input: msg.score.input },
attemptGuid: partGuid,
},
]);
} catch (err) {
console.error(err);
try {
const lab = new URL(getLabServer(context));
const origin = new URL(e.origin);
// filter so we do not process torus events.
if (origin.host === lab.host) {
const msg = e.data;
// only lab messages from this activity for eventual support of multiple problems on a page.
if (isLabMessage(msg) && msg.attemptGuid === activityState.attemptGuid) {
const attemptGuid = activityState.attemptGuid;
switch (msg.messageType) {
// respond to lab score request.
case 'score':
if (mode === 'delivery') {
// only when in delivery
try {
// .dateEvaluated seems to not work as a check to see if part is evaluatable
// Always resetting and saving seems to fix the issue with a second attempt
// not registering grading.
// if (activityState.parts[0].dateEvaluated) {
// if the part has already been evaluated, then
// it is necessary to reset the part to get a new
// partGuid as there can only ever be one evaluation
// per partGuid.
const partResponse = await onResetPart(attemptGuid, partGuid);
partGuid = partResponse.attemptState.attemptGuid;
// import state to new part, luckily the current state
// is already included in the score message so no need
// to maintain it in state.
await onSaveActivity(attemptGuid, [
{
attemptGuid: partGuid,
response: { input: msg.score.input },
},
]);
onSubmitEvaluations(attemptGuid, [
{
score: msg.score.score,
outOf: msg.score.outOf,
feedback: model.feedback[Number(msg.score.complete)],
response: { input: msg.score.input },
attemptGuid: partGuid,
},
]);
} catch (err) {
console.error(err);
}
}
}
break;
// respond to lab request to save state.
case 'save':
// it seems to only save/restore properly with score
if (mode === 'delivery') {
// only update in delivery mode.
try {
await onSaveActivity(attemptGuid, [
{
attemptGuid: partGuid,
response: {
input: msg.state,
break;
// respond to lab request to save state.
case 'save':
// it seems to only save/restore properly with score
if (mode === 'delivery') {
// only update in delivery mode.
try {
await onSaveActivity(attemptGuid, [
{
attemptGuid: partGuid,
response: {
input: msg.state,
},
},
},
]);
} catch (err) {
console.error(err);
]);
} catch (err) {
console.error(err);
}
}
}
break;
// lab is requesting activity state
case 'load':
if (mode !== 'preview') {
const saved = activityState?.parts[0].response?.input;
if (saved && e.source) {
// post saved state back to lab.
break;
// lab is requesting activity state
case 'load':
if (mode !== 'preview') {
const saved = activityState?.parts[0].response?.input;
if (saved && e.source) {
// post saved state back to lab.

e.source.postMessage(saved, { targetOrigin: lab.origin });
}
} // TODO if in preview, load appropriate content
// Preview feature in lab servlet is not complete.
break;
case 'log':
// Currently logging to console, TODO link into torus/oli logging
console.log(msg.content);
break;
default:
console.warn('Unknown message type, skipped...', e);
e.source.postMessage(saved, { targetOrigin: lab.origin });
}
} // TODO if in preview, load appropriate content
// Preview feature in lab servlet is not complete.
break;
case 'log':
// Currently logging to console, TODO link into torus/oli logging
console.log(msg.content);
break;
default:
console.warn('Unknown message type, skipped...', e);
}
}
}
} catch (err) {
console.error(err);
}
};
window.addEventListener('message', onMessage);

return () => window.removeEventListener('message', onMessage);
}, [activityState, model, context]);

const url = new URL(`api/v1/activities/lab/${activity}`, getLabServer(context));
// url.searchParams.append('activity', activity); // Used in development environment
url.searchParams.append('mode', mode);
url.searchParams.append('attemptGuid', activityState.attemptGuid);
const [loading, setLoading] = useState<'loading' | 'loaded' | 'error'>('loading');
const [baseUrl, setBaseUrl] = useState<string>('');
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
try {
const server = getLabServer(context);
const url = new URL(`api/v1/activities/lab/${activity}`, server);
url.searchParams.append('activity', activity);
url.searchParams.append('mode', mode);
url.searchParams.append('attemptGuid', activityState.attemptGuid);
// Using promise because react's useEffect does not handle async.
// toString because tsc does not accept the valid URL.
fetch(url.toString(), { signal, method: 'HEAD' })
.then((response) => {
if (!response.ok) {
throw new Error(response.statusText);
}
setLoading('loaded');
setBaseUrl(url.toString());
})
.catch(() => setLoading('error'));
} catch (err) {
console.error(err);
setLoading('error');
}
return () => controller.abort();
}, [context, activity, mode, activityState]);

return (
<iframe
title={`LogicLab Activity ${modelContext?.title}`}
src={url.toString()}
allow="fullscreen"
height="800"
width="100%"
data-activity-mode={mode}
></iframe>
<>
{loading === 'loading' && (
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden sr-only">Loading...</span>
</div>
)}
{loading === 'error' && (
<div className="alert alert-danger">
The LogicLab server is unreachable or not properly configured. Please contact support if
this issue persists.
</div>
)}
{loading === 'loaded' && (
<iframe
title={`LogicLab Activity ${modelContext?.title}`}
src={baseUrl}
allow="fullscreen"
height="800"
width="100%"
data-activity-mode={mode}
></iframe>
)}
</>
);
};

Expand Down
17 changes: 10 additions & 7 deletions assets/src/components/activities/logic_lab/LogicLabModelSchema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
*/
import { ActivityModelSchema, CreationContext, Feedback, Part, Transformation } from '../types';

// Typing and type checking for existence of variables in a context.
type ContextVariables = { variables: Record<string, string> };
function contextHasVariables(ctx: ContextVariables | unknown): ctx is ContextVariables {
return !!ctx && ctx instanceof Object && 'variables' in ctx;
}
/**
* Extract the LogicLab url from a context with null safety.
* @param context deploy context or activity edit context
* @returns url to use as a base for logiclab service calls
*/
export function getLabServer(context: unknown): string {
if (context instanceof Object && 'variables' in context) {
const ctx = context as { variables: Record<string, string> };
const variables = ctx.variables;
if ('ACTIVITY_LOGICLAB_URL' in variables) {
export function getLabServer(context: ContextVariables | unknown): string {
if (contextHasVariables(context)) {
const variables = context.variables;
if ('ACTIVITY_LOGICLAB_URL' in variables && variables.ACTIVITY_LOGICLAB_URL) {
return variables.ACTIVITY_LOGICLAB_URL;
}
}
const local = new URL('/logiclab/', window.location.origin); // default Torus base URI
return local.toString();
throw new ReferenceError('ACTIVITY_LOGICLAB_URL is not set.');
}

export interface LogicLabModelSchema extends ActivityModelSchema {
Expand Down
3 changes: 0 additions & 3 deletions assets/src/phoenix/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { finalize } from './finalize';
import { showModal } from './modal';
import { enableSubmitWhenTitleMatches } from './package_delete';
import { onReady } from './ready';
import { sessionTimeout } from './session_timeout';

(window as any).Alert = Alert;
(window as any).Button = Button;
Expand Down Expand Up @@ -96,7 +95,6 @@ window.OLI = {
onReady,
finalize,
initCountdownTimer,
sessionTimeout,
initEndDateTimer,
CreateAccountPopup: (node: any, props: any) => mount(CreateAccountPopup, node, props),
};
Expand Down Expand Up @@ -177,7 +175,6 @@ declare global {
toggleAudio: (element: HTMLAudioElement) => void;
OLI: {
initActivityBridge: typeof initActivityBridge;
sessionTimeout: typeof sessionTimeout;
initPreviewActivityBridge: typeof initPreviewActivityBridge;
showModal: typeof showModal;
enableSubmitWhenTitleMatches: typeof enableSubmitWhenTitleMatches;
Expand Down
Loading

0 comments on commit d6c90f0

Please sign in to comment.