@memberjunction/actions-bizapps-lms
Version:
LMS system integration actions for MemberJunction
388 lines • 18.7 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { RegisterClass } from '@memberjunction/global';
import { LearnWorldsBaseAction } from '../learnworlds-base.action.js';
import { BaseAction } from '@memberjunction/actions';
/**
* Action to retrieve quiz/assessment results from LearnWorlds
*/
let GetQuizResultsAction = class GetQuizResultsAction extends LearnWorldsBaseAction {
// ----------------------------------------------------------------
// Typed public method – can be called directly from code
// ----------------------------------------------------------------
/**
* Get quiz results for a user, course, or specific quiz.
* Throws on any error.
*/
async GetQuizResults(params, contextUser) {
this.SetCompanyContext(params.CompanyID);
const { UserID: userId, CourseID: courseId, QuizID: quizId, IncludeQuestions: includeQuestionsRaw, IncludeAnswers: includeAnswersRaw, PassingOnly: passingOnly = false, DateFrom: dateFrom, DateTo: dateTo, SortBy: sortBy = 'completed_at', SortOrder: sortOrder = 'desc', MaxResults: maxResults = 100, } = params;
const includeQuestions = includeQuestionsRaw !== false;
const includeAnswers = includeAnswersRaw !== false;
// Require at least one identifier
if (!userId && !courseId && !quizId) {
throw new Error('At least one of UserID, CourseID, or QuizID is required');
}
// Build query parameters
const queryParams = this.buildQuizQueryParams(userId, quizId, passingOnly, dateFrom, dateTo, sortBy, sortOrder, maxResults);
// Validate path segments before URL interpolation
if (userId)
this.validatePathSegment(userId, 'UserID');
if (courseId)
this.validatePathSegment(courseId, 'CourseID');
if (quizId)
this.validatePathSegment(quizId, 'QuizID');
// Determine endpoint
const endpoint = this.buildQuizEndpoint(userId, courseId, quizId);
// Build query string
const queryString = this.buildQueryString(queryParams);
// Fetch quiz results
const resultsResponse = await this.makeLearnWorldsRequest(endpoint + queryString, 'GET', null, contextUser);
if (resultsResponse.success === false) {
throw new Error(resultsResponse.message || 'Failed to retrieve quiz results');
}
// Normalize results
const rawResults = this.extractQuizResultsArray(resultsResponse);
// Process each quiz result
const formattedResults = await this.processQuizResults(rawResults, userId, courseId, quizId, includeQuestions, includeAnswers, contextUser);
// Calculate summary
const summary = this.buildQuizResultsSummary(formattedResults, userId, courseId, quizId, dateFrom, dateTo);
return {
QuizResults: formattedResults,
TotalCount: formattedResults.length,
Summary: summary,
};
}
// ----------------------------------------------------------------
// Framework wrapper – thin delegation to the public method
// ----------------------------------------------------------------
async InternalRunAction(params) {
const { Params, ContextUser } = params;
this.params = Params;
try {
const typedParams = this.extractQuizResultsParams(Params);
const result = await this.GetQuizResults(typedParams, ContextUser);
this.setOutputParam(Params, 'QuizResults', result.QuizResults);
this.setOutputParam(Params, 'TotalCount', result.TotalCount);
this.setOutputParam(Params, 'Summary', result.Summary);
return this.buildSuccessResult(`Retrieved ${result.TotalCount} quiz result(s)`, Params);
}
catch (error) {
const msg = error instanceof Error ? error.message : 'Unknown error';
return this.buildErrorResult('ERROR', `Error retrieving quiz results: ${msg}`, Params);
}
}
// ----------------------------------------------------------------
// Private helpers – parameter extraction
// ----------------------------------------------------------------
extractQuizResultsParams(params) {
return {
CompanyID: this.getRequiredStringParam(params, 'CompanyID'),
UserID: this.getOptionalStringParam(params, 'UserID'),
CourseID: this.getOptionalStringParam(params, 'CourseID'),
QuizID: this.getOptionalStringParam(params, 'QuizID'),
IncludeQuestions: this.getOptionalBooleanParam(params, 'IncludeQuestions', true),
IncludeAnswers: this.getOptionalBooleanParam(params, 'IncludeAnswers', true),
PassingOnly: this.getOptionalBooleanParam(params, 'PassingOnly', false),
DateFrom: this.getOptionalStringParam(params, 'DateFrom'),
DateTo: this.getOptionalStringParam(params, 'DateTo'),
SortBy: this.getOptionalStringParam(params, 'SortBy'),
SortOrder: (this.getOptionalStringParam(params, 'SortOrder') || 'desc'),
MaxResults: this.getOptionalNumberParam(params, 'MaxResults', LearnWorldsBaseAction.LW_MAX_PAGE_SIZE),
};
}
// ----------------------------------------------------------------
// Private helpers – query building
// ----------------------------------------------------------------
buildQuizQueryParams(userId, quizId, passingOnly, dateFrom, dateTo, sortBy, sortOrder, maxResults) {
const queryParams = {
limit: Math.min(maxResults, LearnWorldsBaseAction.LW_MAX_PAGE_SIZE),
sort: sortBy,
order: sortOrder,
};
if (passingOnly) {
queryParams.passed = true;
}
const parsedFrom = this.safeParseDateToISO(dateFrom);
if (parsedFrom) {
queryParams.completed_after = parsedFrom;
}
const parsedTo = this.safeParseDateToISO(dateTo);
if (parsedTo) {
queryParams.completed_before = parsedTo;
}
if (quizId && userId) {
queryParams.user_id = userId;
}
return queryParams;
}
buildQuizEndpoint(userId, courseId, quizId) {
if (quizId) {
return `/quizzes/${quizId}/results`;
}
else if (userId && courseId) {
return `/users/${userId}/courses/${courseId}/quiz-results`;
}
else if (userId) {
return `/users/${userId}/quiz-results`;
}
else if (courseId) {
return `/courses/${courseId}/quiz-results`;
}
return '/quiz-results';
}
buildQueryString(queryParams) {
const filtered = {};
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
filtered[key] = String(value);
}
}
const keys = Object.keys(filtered);
if (keys.length === 0)
return '';
return '?' + new URLSearchParams(filtered).toString();
}
extractQuizResultsArray(response) {
const data = response.data;
if (Array.isArray(data)) {
return data;
}
if (data && typeof data === 'object' && 'data' in data && Array.isArray(data.data)) {
return data.data;
}
return [];
}
// ----------------------------------------------------------------
// Private helpers – processing
// ----------------------------------------------------------------
async processQuizResults(rawResults, userId, courseId, quizId, includeQuestions, includeAnswers, contextUser) {
const formatted = [];
for (const result of rawResults) {
const formattedResult = this.buildBaseQuizResult(result, userId, courseId, quizId);
// Attach user info if available
if (result.user) {
formattedResult.user = {
id: result.user.id || '',
email: result.user.email || '',
name: result.user.name || `${result.user.first_name || ''} ${result.user.last_name || ''}`.trim(),
};
}
// Attach quiz info
formattedResult.quiz = await this.resolveQuizInfo(result, quizId, contextUser);
// Attach questions & answers if requested
if (includeQuestions || includeAnswers) {
await this.attachDetailedResults(formattedResult, includeQuestions, includeAnswers, contextUser);
}
formatted.push(formattedResult);
}
return formatted;
}
buildBaseQuizResult(result, userId, courseId, quizId) {
const score = result.score || 0;
const maxScore = result.max_score || result.total_points || 100;
return {
id: result.id || result.result_id || '',
userId: result.user_id || userId || '',
courseId: result.course_id || courseId || '',
quizId: result.quiz_id || quizId || '',
attemptNumber: result.attempt_number || result.attempt || 1,
score,
maxScore,
percentage: result.percentage || (score / (maxScore || 100)) * 100,
passed: result.passed || result.is_passing || false,
passingScore: result.passing_score || result.passing_percentage || 70,
startedAt: result.started_at,
completedAt: result.completed_at || result.submitted_at,
duration: result.duration || result.time_spent || 0,
durationText: this.formatDuration(result.duration || result.time_spent || 0),
};
}
async resolveQuizInfo(result, filterQuizId, contextUser) {
if (result.quiz) {
return {
id: result.quiz.id || '',
title: result.quiz.title || result.quiz.name || '',
type: result.quiz.type || 'quiz',
questionCount: result.quiz.question_count || result.quiz.total_questions,
};
}
if (!filterQuizId && result.quiz_id) {
const quizResponse = await this.makeLearnWorldsRequest(`/quizzes/${result.quiz_id}`, 'GET', null, contextUser);
if (quizResponse.success !== false && quizResponse.data) {
const quiz = quizResponse.data;
return {
id: quiz.id || '',
title: quiz.title || quiz.name || '',
type: quiz.type || 'quiz',
questionCount: quiz.question_count || quiz.total_questions,
};
}
}
return undefined;
}
async attachDetailedResults(formattedResult, includeQuestions, includeAnswers, contextUser) {
const detailsResponse = await this.makeLearnWorldsRequest(`/quiz-results/${formattedResult.id}/details`, 'GET', null, contextUser);
if (detailsResponse.success === false || !detailsResponse.data) {
return;
}
const details = detailsResponse.data;
if (includeQuestions) {
formattedResult.questions = this.formatQuestions(details.questions || details.answers || []);
}
if (includeAnswers) {
formattedResult.answers = this.formatAnswers(details.answers || details.questions || []);
// Calculate additional metrics
const answers = formattedResult.answers;
formattedResult.metrics = this.calculateQuizMetrics(answers);
}
}
// ----------------------------------------------------------------
// Private helpers – formatting
// ----------------------------------------------------------------
/**
* Format quiz questions
*/
formatQuestions(questions) {
return questions.map((q, index) => ({
questionNumber: q.question_number || index + 1,
questionId: q.question_id || q.id || '',
questionText: q.question_text || q.question || q.text || '',
questionType: q.question_type || q.type || 'multiple-choice',
points: q.points || q.score || 1,
difficulty: q.difficulty || 'medium',
}));
}
/**
* Format user answers
*/
formatAnswers(answers) {
return answers.map((a, index) => ({
questionNumber: a.question_number || index + 1,
questionId: a.question_id,
userAnswer: this.coerceAnswerValue(a.user_answer || a.answer || a.selected_answer),
correctAnswer: this.coerceAnswerValue(a.correct_answer),
isCorrect: a.is_correct || a.correct || false,
pointsEarned: a.points_earned || (a.is_correct ? a.points || 1 : 0),
pointsPossible: a.points_possible || a.points || 1,
feedback: a.feedback,
timeSpent: a.time_spent,
answeredAt: a.answered_at,
}));
}
/**
* Coerce a raw answer value from the API to a typed QuizAnswerValue.
*/
coerceAnswerValue(value) {
if (value === undefined || value === null)
return undefined;
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
return value;
if (Array.isArray(value))
return value.map(String);
return String(value);
}
calculateQuizMetrics(answers) {
const correctCount = answers.filter((a) => a.isCorrect).length;
return {
correctAnswers: correctCount,
incorrectAnswers: answers.filter((a) => !a.isCorrect).length,
totalQuestions: answers.length,
accuracyRate: answers.length > 0 ? Number(((correctCount / answers.length) * 100).toFixed(1)) : 0,
};
}
// ----------------------------------------------------------------
// Private helpers – summary
// ----------------------------------------------------------------
buildQuizResultsSummary(formattedResults, userId, courseId, quizId, dateFrom, dateTo) {
const totalResults = formattedResults.length;
const passedResults = formattedResults.filter((r) => r.passed).length;
const averageScore = totalResults > 0 ? formattedResults.reduce((sum, r) => sum + r.percentage, 0) / totalResults : 0;
const averageDuration = totalResults > 0 ? formattedResults.reduce((sum, r) => sum + (r.duration || 0), 0) / totalResults : 0;
// Group results by quiz if multiple quizzes
const quizBreakdown = !quizId ? this.buildQuizBreakdown(formattedResults) : null;
const filterType = userId ? (courseId ? 'user-course' : 'user') : courseId ? 'course' : 'all';
return {
totalResults,
passedResults,
failedResults: totalResults - passedResults,
passRate: totalResults > 0 ? Number(((passedResults / totalResults) * 100).toFixed(1)) : 0,
averageScore: Number(averageScore.toFixed(1)),
averageDuration,
averageDurationText: this.formatDuration(Math.round(averageDuration)),
dateRange: {
from: dateFrom || 'all-time',
to: dateTo || 'current',
},
filterType,
quizBreakdown,
};
}
buildQuizBreakdown(formattedResults) {
const resultsByQuiz = {};
for (const result of formattedResults) {
const quizTitle = result.quiz?.title || 'Unknown Quiz';
if (!resultsByQuiz[quizTitle]) {
resultsByQuiz[quizTitle] = {
results: [],
stats: { attempts: 0, averageScore: 0, passRate: 0 },
};
}
resultsByQuiz[quizTitle].results.push(result);
}
// Calculate stats for each quiz
for (const quizTitle of Object.keys(resultsByQuiz)) {
const quizResults = resultsByQuiz[quizTitle].results;
const qTotal = quizResults.length;
const qPassed = quizResults.filter((r) => r.passed).length;
const qAvgScore = quizResults.reduce((sum, r) => sum + r.percentage, 0) / qTotal;
resultsByQuiz[quizTitle].stats = {
attempts: qTotal,
averageScore: Number(qAvgScore.toFixed(1)),
passRate: Number(((qPassed / qTotal) * 100).toFixed(1)),
};
}
return resultsByQuiz;
}
// ----------------------------------------------------------------
// Params & Description metadata
// ----------------------------------------------------------------
/**
* Define the parameters this action expects
*/
get Params() {
const baseParams = this.getCommonLMSParams();
const specificParams = [
{ Name: 'UserID', Type: 'Input', Value: null },
{ Name: 'CourseID', Type: 'Input', Value: null },
{ Name: 'QuizID', Type: 'Input', Value: null },
{ Name: 'IncludeQuestions', Type: 'Input', Value: true },
{ Name: 'IncludeAnswers', Type: 'Input', Value: true },
{ Name: 'PassingOnly', Type: 'Input', Value: false },
{ Name: 'DateFrom', Type: 'Input', Value: null },
{ Name: 'DateTo', Type: 'Input', Value: null },
{ Name: 'SortBy', Type: 'Input', Value: 'completed_at' },
{ Name: 'SortOrder', Type: 'Input', Value: 'desc' },
{ Name: 'MaxResults', Type: 'Input', Value: 100 },
{ Name: 'QuizResults', Type: 'Output', Value: null },
{ Name: 'TotalCount', Type: 'Output', Value: null },
{ Name: 'Summary', Type: 'Output', Value: null },
];
return [...baseParams, ...specificParams];
}
/**
* Metadata about this action
*/
get Description() {
return 'Retrieves quiz and assessment results from LearnWorlds with detailed question/answer information';
}
};
GetQuizResultsAction = __decorate([
RegisterClass(BaseAction, 'GetQuizResultsAction')
], GetQuizResultsAction);
export { GetQuizResultsAction };
//# sourceMappingURL=get-quiz-results.action.js.map