import shortid from 'shortid' ; import { ILog , IQuizDefaultSettings , TQuestionFull , TQuestionPartial , TSelectionQuestionPartial } from '../types' ; import { generateInputQuestionAnswers , generateSelectionQuestionAnswers , isPrimitive } from './' ; function setObjectValues ( parent , arr ) { arr . forEach ( ( entry ) => { parent [ entry [ 0 ] ] = parent [ entry [ 0 ] ] ?? entry [ 1 ] ; } ) ; } function generateDefaultSettingsContexts ( contexts ) { return typeof contexts === 'number' ? [ contexts ] : Array . isArray ( contexts ) ? contexts : [ ] ; } export function generateCompleteQuestion ( question , contexts , defaultSettings ) { const logs = { warns : [ ] , errors : [ ] } ; const completeQuestion = JSON . parse ( JSON . stringify ( question ) ) ; ( [ 'question' , 'answers' ] as const ) . forEach ( ( field ) => { completeQuestion [ field ] ?? logs . errors . push ( `Question ${ field } is required` ) ; } ) ; if ( completeQuestion . options ) { completeQuestion . options = completeQuestion . options . map ( ( option , i ) => ( { text : option . toString ( ) . trim ( ) , index : `${ i }` } ) ) ; } let time_allocated = defaultSettings . time_allocated ?? 60 ; const dummyQuestion = completeQuestion ; if ( logs . errors . length === 0 ) { if ( isPrimitive ( completeQuestion . answers ) || Object . getPrototypeOf ( completeQuestion . answers ) === Object . prototype || completeQuestion . answers . length === 1 ) completeQuestion . type = completeQuestion . type || ( dummyQuestion . options ? 'MCQ' : 'Snippet' ) ; else completeQuestion . type = completeQuestion . type || ( dummyQuestion . options ? 'MS' : 'FIB' ) ; switch ( completeQuestion . type ) { case 'MCQ' : completeQuestion . answers = generateSelectionQuestionAnswers ( ( question as TSelectionQuestionPartial ) . answers ) ; time_allocated = 15 ; if ( ! dummyQuestion . options ) logs . errors . push ( `Options must be provided for ${ dummyQuestion . type } questions` ) ; else { if ( parseInt ( dummyQuestion . answers [ 0 ] . text . trim ( ) ) < 0 || parseInt ( dummyQuestion . answers [ 0 ] . text . trim ( ) ) > dummyQuestion . options . length - 1 ) logs . errors . push ( `MCQ Answer must be within 0-${ dummyQuestion . options . length - 1 }, provided ${ dummyQuestion . answers [ 0 ] . text . trim ( ) }` ) ; if ( dummyQuestion . options . length < 2 ) logs . errors . push ( `MCQ Question must have at least 2 options` ) ; } break ; case 'MS' : completeQuestion . answers = generateSelectionQuestionAnswers ( ( question as TSelectionQuestionPartial ) . answers ) ; if ( ! dummyQuestion . options ) logs . errors . push ( `Options must be provided for ${ dummyQuestion . type } questions` ) ; else { if ( dummyQuestion . options . length < 2 ) logs . errors . push ( `MS Question must have at least 2 options` ) ; if ( dummyQuestion . answers . length > dummyQuestion . options . length ) { logs . errors . push ( `Provided more answers than options, given ${ dummyQuestion . options . length } options, but provided ${ dummyQuestion . answers . length } answers` ) ; } completeQuestion . answers . forEach ( ( answer ) => { if ( parseInt ( answer . text . trim ( ) ) < 0 || parseInt ( answer . text . trim ( ) ) > dummyQuestion . options . length - 1 ) logs . errors . push ( `MS Answer must be within 0-${ dummyQuestion . options . length - 1 }, provided ${ answer . text . trim ( ) }` ) ; } ) ; } time_allocated = 30 ; break ; case 'Snippet' : { const [ generatedInputQuestionAnswers , generatedLogs ] = generateInputQuestionAnswers ( question . answers ) ; completeQuestion . answers = generatedInputQuestionAnswers ; logs . errors . concat ( generatedLogs . errors ) ; logs . warns . concat ( generatedLogs . warns ) ; time_allocated = 45 ; break ; } case 'FIB' : { const [ generatedInputQuestionAnswers , generatedLogs ] = generateInputQuestionAnswers ( question . answers ) ; completeQuestion . answers = generatedInputQuestionAnswers ; logs . errors . concat ( generatedLogs . errors ) ; logs . warns . concat ( generatedLogs . warns ) ; if ( completeQuestion . answers . length + 1 !== completeQuestion . question . length ) { logs . errors . push ( `Unmatched blanks in question, given ${ completeQuestion . answers . length }, required ${ completeQuestion . question . length - 1 }` ) ; } time_allocated = 60 ; completeQuestion . format = completeQuestion . format ?? 'text' ; break ; } } setObjectValues ( completeQuestion , [ [ 'image' , null ] , [ 'weight' , defaultSettings . weight ?? 1 ] , [ 'difficulty' , defaultSettings . difficulty ?? 'Beginner' ] , [ 'explanation' , 'No explanation available' ] , [ 'hints' , [ ] ] , [ 'time_allocated' , defaultSettings . time_allocated ?? time_allocated ] , [ 'contexts' , generateDefaultSettingsContexts ( defaultSettings . contexts ) ] ] ) ; completeQuestion . _id = shortid ( ) ; if ( completeQuestion . time_allocated < 10 || completeQuestion . time_allocated > 120 ) { logs . warns . push ( `Question time allocated must be within 10-120 but given ${ completeQuestion . time_allocated }, changing to 60` ) ; completeQuestion . time_allocated = 60 ; } if ( ! [ 'Beginner' , 'Intermediate' , 'Advanced' ] . includes ( completeQuestion . difficulty ) ) { logs . warns . push ( `Question difficulty must be one of Beginner, Intermediate or Advanced, but given ${ completeQuestion . difficulty }, changing to Beginner` ) ; completeQuestion . difficulty = 'Beginner' ; } completeQuestion . contexts = generateDefaultSettingsContexts ( completeQuestion . contexts ) ; completeQuestion . contexts = completeQuestion . contexts . filter ( ( context ) => { if ( context < 0 || context > contexts . length - 1 ) { logs . warns . push ( `Question referred a context ${ context } that is not within the range 0-${ contexts . length - 1 }. Removing it.` ) ; return false ; } return true ; } ) ; } return [ completeQuestion , logs ] as const ; }