Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/resources/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,43 @@ export interface Page extends TorusResource {
content: Record<string, unknown>;
isGraded: boolean;
isSurvey: boolean;
maxAttempts?: number;
recommendedAttempts?: number;
collabSpace: CollabSpaceDefinition;
objectives: any[];
}

// Legacy scored resources store their attempt limit as an XML attribute. Both
// "unlimited" and -1 mean unlimited in legacy content; Torus represents that as 0.
export function parseLegacyMaxAttempts(resource: any): number | undefined {
const value = resource.max_attempts?.trim().toLowerCase();
if (value === 'unlimited' || value === '-1') {
return 0;
}
if (!value) {
return undefined;
}

const parsed = Number(value);

// Leave absent or malformed values out so Torus can apply its default.
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
}

// Recommended attempts has no unlimited form, so pass through only a valid
// non-negative count and otherwise allow Torus to supply its default.
export function parseLegacyRecommendedAttempts(
resource: any
): number | undefined {
const value = resource.recommended_attempts?.trim();
if (!value) {
return undefined;
}

const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
}

export function isPage(r: TorusResource): r is Page {
return r.type === 'Page';
}
Expand Down
6 changes: 6 additions & 0 deletions src/resources/summative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Summary,
Page,
defaultCollabSpaceDefinition,
parseLegacyMaxAttempts,
parseLegacyRecommendedAttempts,
} from './resource';
import {
processCodeblock,
Expand Down Expand Up @@ -160,6 +162,10 @@ export class Summative extends Resource {
page.isGraded = true;
page.title = title;
page.unresolvedReferences = unresolvedReferences;
page.maxAttempts = parseLegacyMaxAttempts(r.children[0]);
page.recommendedAttempts = parseLegacyRecommendedAttempts(
r.children[0]
);

resolve([page, ...items]);
});
Expand Down
6 changes: 6 additions & 0 deletions src/resources/superactivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
Summary,
Page,
defaultCollabSpaceDefinition,
parseLegacyMaxAttempts,
parseLegacyRecommendedAttempts,
} from './resource';
import { guid } from 'src/utils/common';
import * as XML from 'src/utils/xml';
Expand Down Expand Up @@ -77,6 +79,10 @@ export class Superactivity extends Resource {
content: { model },
isGraded: true,
isSurvey: false,
maxAttempts: parseLegacyMaxAttempts(r.children[0]),
recommendedAttempts: parseLegacyRecommendedAttempts(
r.children[0]
),
objectives: [],
warnings: [],
collabSpace: defaultCollabSpaceDefinition(),
Expand Down
85 changes: 85 additions & 0 deletions test/max-attempts-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { convert } from 'src/convert';
import { MediaSummary } from 'src/media';
import { ProjectSummary } from 'src/project';
import {
Page,
parseLegacyMaxAttempts,
parseLegacyRecommendedAttempts,
} from 'src/resources/resource';
import { Superactivity } from 'src/resources/superactivity';

const mediaSummary: MediaSummary = {
mediaItems: {},
missing: [],
urlPrefix: '',
downloadRemote: false,
flattenedNames: {},
};

const projectSummary = new ProjectSummary(
'test/course_packages/migration-4sdfykby_v_1_0-echo',
'',
'',
mediaSummary
);

describe('assessment attempt settings', () => {
test('passes through max_attempts from a legacy assessment', async () => {
const resources = await convert(
projectSummary,
'test/course_packages/migration-4sdfykby_v_1_0-echo/content/x-oli-assessment2/newc72f87db5a5543b5ae8582d2d4cd34a7.xml',
false
);
const page = resources.find(
(resource) => typeof resource !== 'string' && resource.type === 'Page'
) as Page;

expect(page.maxAttempts).toBe(3);
expect(page.recommendedAttempts).toBe(3);
});

test('passes through max_attempts to a synthesized superactivity wrapper', async () => {
const resources = await new Superactivity(
'./test/content/x-oli-linked-activity/improvement.xml',
true
).convert(projectSummary);
const page = resources.find(
(resource) => typeof resource !== 'string' && resource.type === 'Page'
) as Page;

expect(page.maxAttempts).toBe(10);
});

test.each(['unlimited', '-1'])(
'normalizes legacy unlimited value %s to the Torus sentinel',
(max_attempts) => {
expect(parseLegacyMaxAttempts({ max_attempts })).toBe(0);
}
);

test.each([undefined, '', 'many', '1.5', '-2'])(
'omits absent or invalid legacy value %s',
(max_attempts) => {
expect(parseLegacyMaxAttempts({ max_attempts })).toBeUndefined();
}
);

test.each([
['0', 0],
['1', 1],
['100', 100],
])('passes through recommended_attempts value %s', (value, expected) => {
expect(
parseLegacyRecommendedAttempts({ recommended_attempts: value })
).toBe(expected);
});

test.each([undefined, '', 'unlimited', '1.5', '-1'])(
'omits absent or invalid recommended_attempts value %s',
(recommended_attempts) => {
expect(
parseLegacyRecommendedAttempts({ recommended_attempts })
).toBeUndefined();
}
);
});
Loading