UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

388 lines 18.7 kB
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