Merge branch 'main' of https://github.com/microsoft/AzureLTIAssessmentApp into main
This commit is contained in:
Коммит
b193269a5c
|
@ -16,8 +16,10 @@ namespace Assessment.App.Database.Model
|
|||
|
||||
public List<string> Options { get; set; }
|
||||
|
||||
public int Answer { get; set; }
|
||||
public List<string> Answer { get; set; }
|
||||
|
||||
public string TextType{get;set;}
|
||||
|
||||
public string QuestionType{get;set;}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,6 @@
|
|||
{
|
||||
public class QuestionResponseInfo
|
||||
{
|
||||
public int ChosenOption { get; set; }
|
||||
public string[] ChosenOption { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,8 +9,11 @@ namespace Assessment.App.Functions.Student.Dto
|
|||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public List<string> Options { get; set; }
|
||||
public int ChosenOption { get; set; }
|
||||
public string[] ChosenOption { get; set; }
|
||||
|
||||
public string TextType{get;set;}
|
||||
|
||||
|
||||
public string QuestionType{get;set;}
|
||||
}
|
||||
}
|
|
@ -118,7 +118,7 @@ namespace Assessment.App.Functions.Student
|
|||
foreach (var questionId in assessmentItem.QuestionIds)
|
||||
{
|
||||
var item = questionItems[questionId];
|
||||
var chosenOption = -1;
|
||||
string[] chosenOption = new string[]{};
|
||||
if (studentResponse.Responses.TryGetValue(questionId, out var responseInfo))
|
||||
{
|
||||
chosenOption = responseInfo.ChosenOption;
|
||||
|
@ -131,7 +131,8 @@ namespace Assessment.App.Functions.Student
|
|||
Description = item.Description,
|
||||
Options = item.Options,
|
||||
ChosenOption = chosenOption,
|
||||
TextType=item.TextType
|
||||
TextType=item.TextType,
|
||||
QuestionType = item.QuestionType
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -177,10 +178,13 @@ namespace Assessment.App.Functions.Student
|
|||
{
|
||||
if (requestData.Responses.TryGetValue(questionId, out var responseInfo))
|
||||
{
|
||||
if (responseInfo.ChosenOption == questionItems[questionId].Answer)
|
||||
{
|
||||
correctAnswers += 1;
|
||||
}
|
||||
var questionType = questionItems[questionId].QuestionType;
|
||||
var studentAns = responseInfo.ChosenOption;
|
||||
studentAns = studentAns.Select(s => s.ToLowerInvariant()).ToArray();
|
||||
var correctAns = questionItems[questionId].Answer;
|
||||
correctAns = correctAns.ConvertAll(d => d.ToLower());
|
||||
var res = getResult(studentAns, correctAns.ToArray(), questionType);
|
||||
correctAnswers = correctAnswers + res;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,5 +229,27 @@ namespace Assessment.App.Functions.Student
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int getResult(string[] studentResponse, string[] correctAns, string questionType){
|
||||
if (questionType == "MCQ" || questionType == "TF"){
|
||||
Array.Sort(studentResponse);
|
||||
Array.Sort(correctAns);
|
||||
if (studentResponse.SequenceEqual(correctAns)){
|
||||
return 1;
|
||||
}
|
||||
else{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (questionType == "QA"){
|
||||
foreach(string answer in studentResponse ){
|
||||
if (correctAns.Contains(answer)){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -180,7 +180,8 @@ namespace Assessment.App.Functions.Teacher
|
|||
Name = questionItem.Name,
|
||||
Options = questionItem.Options,
|
||||
LastModified = DateTime.UtcNow,
|
||||
TextType = questionItem.TextType
|
||||
TextType = questionItem.TextType,
|
||||
QuestionType = questionItem.QuestionType,
|
||||
|
||||
});
|
||||
return new OkObjectResult(new CreateQuestionResponse() {Id = id});
|
||||
|
@ -213,6 +214,8 @@ namespace Assessment.App.Functions.Teacher
|
|||
Name = questionItem.Name,
|
||||
Options = questionItem.Options,
|
||||
LastModified = DateTime.UtcNow,
|
||||
TextType = questionItem.TextType,
|
||||
QuestionType = questionItem.QuestionType
|
||||
});
|
||||
return new OkResult();
|
||||
}
|
||||
|
|
|
@ -140,7 +140,8 @@ export const AssessmentStatisticsComponent = (
|
|||
incorrect: 0,
|
||||
}
|
||||
}
|
||||
if (value.chosenOption === question.answer) {
|
||||
var correctAnswers = new Set(question.answer)
|
||||
if (correctAnswers.has(value.chosenOption.toString())) {
|
||||
data[questionId].correct += 1;
|
||||
} else {
|
||||
data[questionId].incorrect += 1;
|
||||
|
|
|
@ -6,31 +6,40 @@ import {
|
|||
Label,
|
||||
mergeStyles,
|
||||
PrimaryButton,
|
||||
TextField
|
||||
TextField,
|
||||
Dropdown,
|
||||
ICheckboxProps,
|
||||
Checkbox,
|
||||
} from "@fluentui/react";
|
||||
import {Question} from "../model/Question";
|
||||
import {Col, Container, Row} from "react-grid-system";
|
||||
|
||||
const optionRootClass = mergeStyles({display: 'flex', alignItems: 'baseline'});
|
||||
const textFieldStyles: Partial<ITextFieldStyles> = {fieldGroup: {width: 350}};
|
||||
|
||||
|
||||
|
||||
|
||||
interface EditQuestionComponentProps {
|
||||
question: Question;
|
||||
setQuestion: (f: (oldValue: Question) => Question) => void;
|
||||
question: any;
|
||||
setQuestion: (f: (oldValue: any ) => any) => void;
|
||||
}
|
||||
|
||||
|
||||
export const EditQuestionComponent = (
|
||||
{question, setQuestion}: EditQuestionComponentProps
|
||||
) => {
|
||||
const createOptions = (): IChoiceGroupOption[] => {
|
||||
const createOneOption = (optionId: number): IChoiceGroupOption => {
|
||||
) => {
|
||||
|
||||
const shouldCheckBoxBeChecked = (index:number):boolean => {
|
||||
if (question.answer.find((x: string) => x === index.toString())){
|
||||
return true;
|
||||
}
|
||||
else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const createOptions = (): ICheckboxProps[] => {
|
||||
const createOneOption = (optionId: number): ICheckboxProps => {
|
||||
return {
|
||||
key: optionId.toString(),
|
||||
text: '',
|
||||
onRenderField: (props, render) => {
|
||||
id: optionId.toString(),
|
||||
onRenderLabel: (props, render) => {
|
||||
return (
|
||||
<div className={optionRootClass}>
|
||||
{render!(props)}
|
||||
|
@ -58,24 +67,129 @@ export const EditQuestionComponent = (
|
|||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
onChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
var previousArray:string[] = Object.assign([], question.answer)
|
||||
if (ev?.currentTarget.id !== undefined){
|
||||
if (checked){
|
||||
if (previousArray.includes(ev?.currentTarget.id) === false){
|
||||
previousArray.push(ev?.currentTarget.id)
|
||||
}
|
||||
}
|
||||
else{
|
||||
const index = previousArray.indexOf(ev?.currentTarget.id, 0);
|
||||
if (index > -1) {
|
||||
previousArray.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
setQuestion({...question, answer:previousArray})
|
||||
},
|
||||
defaultChecked: shouldCheckBoxBeChecked(optionId)
|
||||
};
|
||||
};
|
||||
return question.options.map((_: any, index: number) => createOneOption(index));
|
||||
};
|
||||
const updateCorrectAnswer = (_: any, option?: IChoiceGroupOption) => {
|
||||
var answer: number;
|
||||
var answer: string[];
|
||||
if (option == null) {
|
||||
answer = -1;
|
||||
answer = [];
|
||||
} else {
|
||||
answer = Number(option.key);
|
||||
answer = [option.key.toString()];
|
||||
}
|
||||
setQuestion(q => ({...q, answer: answer}))
|
||||
}
|
||||
|
||||
function mcqDisplay(isOn:boolean){
|
||||
if (isOn){
|
||||
return <> <Row>
|
||||
<Col md={2}>
|
||||
<Label style={{ textAlign: "left" }}>Options</Label>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
{createOptions().map((value:ICheckboxProps , index: number) => <Checkbox {...value}/>)}
|
||||
</Col>
|
||||
</Row><br /><Row>
|
||||
<Col md={2} />
|
||||
<Col md={6}>
|
||||
<PrimaryButton text="Add option" onClick={() => setQuestion(
|
||||
q => ({ ...q, options: [...q.options, ''] })
|
||||
)} />
|
||||
</Col>
|
||||
</Row></>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function tfDisplay(isOn:Boolean){
|
||||
if (isOn){
|
||||
return <> <Row>
|
||||
<Col md={2}>
|
||||
<Label style={{ textAlign: "left" }}>Options</Label>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<ChoiceGroup
|
||||
options={[{key:'0', text:'True'}, {key:'1', text:'False'}]}
|
||||
required={true}
|
||||
selectedKey={`${question.answer}`}
|
||||
onChange={updateCorrectAnswer}/>
|
||||
</Col>
|
||||
</Row></>
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
function qaDisplay(isOn:boolean){
|
||||
if (isOn){
|
||||
return <>
|
||||
<Row>
|
||||
<Col md={2}>
|
||||
<Label style={{textAlign: "left"}}>Correct answer/s</Label>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
{question.answer.map((_: any, index: number) => ( <>
|
||||
<div className={optionRootClass}>
|
||||
<TextField
|
||||
id={`question-answer-${index}`}
|
||||
styles={textFieldStyles}
|
||||
value={question.answer[index]}
|
||||
onChange={(_: any, newValue?: string) => setQuestion(q => {
|
||||
var result = { ...q, answer: [...q.answer] };
|
||||
result.answer[index] = newValue || '';
|
||||
return result;
|
||||
})} /><IconButton
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
onClick={() => {
|
||||
setQuestion(q => {
|
||||
let result = { ...q, answer: [...q.answer] };
|
||||
result.answer.splice(index, 1);
|
||||
return result;
|
||||
});
|
||||
} }
|
||||
disabled={question.answer.length <= 1} />
|
||||
</div>
|
||||
<br></br>
|
||||
</>))}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={2} />
|
||||
<Col md={6}>
|
||||
<PrimaryButton text="Add answer" onClick={() => setQuestion(
|
||||
q => ({ ...q, answer: [...q.answer, ''] })
|
||||
)} />
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (<Container style={{margin: '30px', position: 'relative'}}>
|
||||
<Row>
|
||||
<Col md={2}>
|
||||
<Label style={{textAlign: "left"}}>Name</Label>
|
||||
<Label style={{textAlign: "left"}}>Question Name</Label>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<TextField
|
||||
|
@ -90,7 +204,7 @@ export const EditQuestionComponent = (
|
|||
<br/>
|
||||
<Row>
|
||||
<Col md={2}>
|
||||
<Label style={{textAlign: "left"}}>Description</Label>
|
||||
<Label style={{textAlign: "left"}}>Question Description</Label>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<TextField
|
||||
|
@ -108,42 +222,31 @@ export const EditQuestionComponent = (
|
|||
<br/>
|
||||
<Row>
|
||||
<Col md={2}>
|
||||
<Label style={{textAlign: "left"}}>Text format</Label>
|
||||
<Label style={{textAlign: "left"}}>Question format</Label>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<TextField
|
||||
id="textType-input"
|
||||
rows={1}
|
||||
value={question.textType}
|
||||
onChange={(_: any, newValue?: string) =>
|
||||
setQuestion(q => ({...q, textType: newValue || ''}))
|
||||
}
|
||||
/>
|
||||
|
||||
<Col md = {6}>
|
||||
<Dropdown
|
||||
defaultSelectedKey={question.textType}
|
||||
options={[{text:"Text", key: 'text'}, {text:"HTML", key:'html'}]}
|
||||
onChange={(_, key)=> setQuestion({...question, textType:key?.key})} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br/>
|
||||
<Row>
|
||||
<Col md={2}>
|
||||
<Label style={{textAlign: "left"}}>Options</Label>
|
||||
<Label style={{textAlign: "left"}}>Question Type</Label>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<ChoiceGroup
|
||||
options={createOptions()}
|
||||
required={true}
|
||||
selectedKey={`${question.answer}`}
|
||||
onChange={updateCorrectAnswer}/>
|
||||
<Dropdown
|
||||
defaultSelectedKey={question.questionType}
|
||||
options={[{text:"Multiple Choice", key: 'MCQ'}, {text:"True/False", key:'TF'}, {text:"Question/Answer", key:'QA'}]}
|
||||
onChange={(_, key)=> setQuestion({...question, questionType: key?.key.toString() || '', answer:[], options:[]})} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br/>
|
||||
<Row>
|
||||
<Col md={2}/>
|
||||
<Col md={6}>
|
||||
<PrimaryButton text="Add option" onClick={() => setQuestion(
|
||||
q => ({...q, options: [...q.options, '']})
|
||||
)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
{mcqDisplay(question.questionType === "MCQ")}
|
||||
{qaDisplay(question.questionType === "QA")}
|
||||
{tfDisplay(question.questionType === "TF")}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,63 +1,123 @@
|
|||
import React from "react";
|
||||
import {StudentQuestion} from '../model/StudentQuestion';
|
||||
import {ChoiceGroup, IChoiceGroupOption} from '@fluentui/react/lib/ChoiceGroup';
|
||||
import { Label } from "@fluentui/react";
|
||||
import { IChoiceGroupOption} from '@fluentui/react/lib/ChoiceGroup';
|
||||
import { Checkbox, ICheckboxProps, Label, TextField } from "@fluentui/react";
|
||||
import { getTheme } from '@fluentui/react';
|
||||
import parse from 'html-react-parser';
|
||||
|
||||
interface StudentQuestionComponentProps {
|
||||
question: StudentQuestion;
|
||||
selectedOption: number;
|
||||
setSelectedOption: (choice: number) => void;
|
||||
selectedOption: string[];
|
||||
setSelectedOption: (choice: string[]) => void;
|
||||
}
|
||||
var getDisplay = (x:string, texttype:string)=> {
|
||||
console.log("text type is");
|
||||
console.log(texttype);
|
||||
if (texttype === "html" || texttype === "text/html"){
|
||||
var parseDisplay = (x:string, texttype:string)=> {
|
||||
if (texttype === "html" || texttype === "text/html" || texttype === "HTML"){
|
||||
x = x.replaceAll('\\n','<br>')
|
||||
return parse(x)
|
||||
}
|
||||
// Need to handle case where texttype is markdown
|
||||
return x
|
||||
}
|
||||
|
||||
|
||||
export const StudentQuestionComponent = (
|
||||
{question, selectedOption, setSelectedOption}: StudentQuestionComponentProps
|
||||
) => {
|
||||
const theme = getTheme();
|
||||
const options: IChoiceGroupOption[] = question.options.map((value, index) => ({
|
||||
key: index.toString(),
|
||||
text: value,
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<br/>
|
||||
<div style={{
|
||||
backgroundColor: '#faf9f8',
|
||||
width: '50%',
|
||||
margin: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: theme.effects.elevation8
|
||||
}}>
|
||||
<br/>
|
||||
<div style={{margin: '30px', textAlign: 'left'}}>
|
||||
<Label style={{textAlign: 'left', fontSize: '25px'}}>Question</Label>
|
||||
<p>{getDisplay(question.description, question.textType)}</p>
|
||||
{console.log(question)}
|
||||
<ChoiceGroup
|
||||
selectedKey={selectedOption.toString()}
|
||||
onChange={(_: any, option) => {
|
||||
if (option) {
|
||||
setSelectedOption(Number(option.key));
|
||||
|
||||
const shouldCheckBoxBeChecked = (index:number):boolean => {
|
||||
if (index in selectedOption){
|
||||
return true;
|
||||
}
|
||||
else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const createOptions = (): ICheckboxProps[] => {
|
||||
const createOneOption = (optionId: number): ICheckboxProps => {
|
||||
return {
|
||||
id: optionId.toString().concat(question.id) ,
|
||||
defaultChecked: shouldCheckBoxBeChecked(optionId),
|
||||
onChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
|
||||
var previousArray:string[] = Object.assign([], selectedOption)
|
||||
if (ev?.currentTarget.id !== undefined){
|
||||
if (checked){
|
||||
if (previousArray.includes(optionId.toString()) === false){
|
||||
previousArray.push(optionId.toString())
|
||||
}
|
||||
}
|
||||
else{
|
||||
const index = previousArray.indexOf(optionId.toString(), 0);
|
||||
if (index > -1) {
|
||||
previousArray.splice(index, 1);
|
||||
}
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
}
|
||||
}
|
||||
setSelectedOption(previousArray)
|
||||
},
|
||||
label:question.options[optionId]
|
||||
};
|
||||
};
|
||||
return question.options.map((_: any, index: number) => createOneOption(index));
|
||||
};
|
||||
|
||||
function displayMCQOrTF(){
|
||||
return <div style={{margin: '30px', textAlign: 'left'}}>
|
||||
<Label style={{textAlign: 'left', fontSize: '25px'}}>Question</Label>
|
||||
<p>
|
||||
{parseDisplay(question.description, question.textType)}
|
||||
</p>
|
||||
<p>
|
||||
{
|
||||
createOptions().map((value:ICheckboxProps , index: number) =>
|
||||
<>
|
||||
<Checkbox {...value}/>
|
||||
<br/>
|
||||
</>)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
function displayQA(){
|
||||
return <div style={{margin: '30px', textAlign: 'left'}}>
|
||||
<Label style={{textAlign: 'left', fontSize: '25px'}}>Question</Label>
|
||||
<p>{parseDisplay(question.description, question.textType)}</p>
|
||||
<TextField
|
||||
id="LA-input"
|
||||
onChange={(_: any, newValue?: string) =>
|
||||
setSelectedOption([newValue|| ''])}
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
if (question.questionType == "QA") {
|
||||
return <>
|
||||
<br />
|
||||
<div style={{backgroundColor: '#faf9f8', width: '50%',margin: 'auto', padding: '10px', boxShadow: theme.effects.elevation8}}>
|
||||
<br />
|
||||
{displayQA()}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
else{
|
||||
return (
|
||||
<>
|
||||
<br/>
|
||||
<div style={{
|
||||
backgroundColor: '#faf9f8',
|
||||
width: '50%',
|
||||
margin: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: theme.effects.elevation8
|
||||
}}>
|
||||
<br/>
|
||||
{displayMCQOrTF()}
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -187,8 +187,9 @@ export class FakeRepository implements IRepository {
|
|||
description: "Applications of machine learning are all around us",
|
||||
lastModified: new Date(),
|
||||
options: ["True", "False"],
|
||||
answer: 0,
|
||||
answer: ['0'],
|
||||
textType:"text",
|
||||
questionType:"TF"
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
|
@ -200,8 +201,9 @@ export class FakeRepository implements IRepository {
|
|||
"The use of neural networks",
|
||||
"Deep learning is used in robots",
|
||||
],
|
||||
answer: 1,
|
||||
textType:"text"
|
||||
answer: ['1'],
|
||||
textType:"text",
|
||||
questionType:"MCQ",
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
|
@ -213,8 +215,9 @@ export class FakeRepository implements IRepository {
|
|||
"To customise a shopping experience based on the type of the customer",
|
||||
"Both of the above",
|
||||
],
|
||||
answer: 2,
|
||||
textType:"text"
|
||||
answer: ['2'],
|
||||
textType:"text",
|
||||
questionType:"MCQ"
|
||||
},
|
||||
'3': {
|
||||
id: '3',
|
||||
|
@ -227,8 +230,9 @@ export class FakeRepository implements IRepository {
|
|||
"Documentations",
|
||||
"All of the above",
|
||||
],
|
||||
answer: 3,
|
||||
textType:"text"
|
||||
answer: ['3'],
|
||||
textType:"text",
|
||||
questionType:"MCQ",
|
||||
},
|
||||
'4': {
|
||||
id: '4',
|
||||
|
@ -241,8 +245,9 @@ export class FakeRepository implements IRepository {
|
|||
"System Models",
|
||||
"Software Models",
|
||||
],
|
||||
answer: 1,
|
||||
textType:"text"
|
||||
answer: ['1'],
|
||||
textType:"text",
|
||||
questionType:"MCQ"
|
||||
},
|
||||
'5': {
|
||||
id: '5',
|
||||
|
@ -255,8 +260,9 @@ export class FakeRepository implements IRepository {
|
|||
"3",
|
||||
"5",
|
||||
],
|
||||
answer: 2,
|
||||
textType:"text"
|
||||
answer: ['2'],
|
||||
textType:"text",
|
||||
questionType:"MCQ"
|
||||
},
|
||||
'6': {
|
||||
id: '6',
|
||||
|
@ -269,8 +275,9 @@ export class FakeRepository implements IRepository {
|
|||
"Increasing complexity",
|
||||
"Self-regulation",
|
||||
],
|
||||
answer: 0,
|
||||
textType:"text"
|
||||
answer:['0'],
|
||||
textType:"text",
|
||||
questionType:"MCQ"
|
||||
},
|
||||
'7': {
|
||||
id: '7',
|
||||
|
@ -283,8 +290,9 @@ export class FakeRepository implements IRepository {
|
|||
"Maintenance",
|
||||
"All of the above",
|
||||
],
|
||||
answer: 3,
|
||||
textType:"text"
|
||||
answer: ['3'],
|
||||
textType:"text",
|
||||
questionType:"MCQ"
|
||||
},
|
||||
'8': {
|
||||
id: '8',
|
||||
|
@ -297,8 +305,9 @@ export class FakeRepository implements IRepository {
|
|||
"Software Quality Management",
|
||||
"All of the above",
|
||||
],
|
||||
answer: 3,
|
||||
textType:"text"
|
||||
answer: ['3'],
|
||||
textType:"text",
|
||||
questionType:"MCQ"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -363,7 +372,7 @@ export class FakeRepository implements IRepository {
|
|||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
public async submitStudentAssessment(assessmentId: string, chosenOptions: { [id: string]: number }) {}
|
||||
public async submitStudentAssessment(assessmentId: string, chosenOptions: { [id: string]: string[] }) {}
|
||||
|
||||
public isReady(): boolean {
|
||||
return true;
|
||||
|
|
|
@ -22,7 +22,7 @@ export interface IRepository {
|
|||
getAssessmentStats(assessmentId: string): Promise<AssessmentStatistics>;
|
||||
getStudentAssessment(assessmentId: string): Promise<StudentAssessment>;
|
||||
getStudentQuestions(assessmentId: string): Promise<StudentAssessmentQuestions>;
|
||||
submitStudentAssessment(assessmentId: string, chosenOptions: { [id: string]: number }): void;
|
||||
submitStudentAssessment(assessmentId: string, chosenOptions: { [id: string]: string[] }): void;
|
||||
deleteQuestions(questionIds: string[]): void;
|
||||
deleteQuestionBanks(questionBankIds: string[]): void;
|
||||
isReady(): boolean;
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
export interface Question {
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
lastModified: Date,
|
||||
options: string[],
|
||||
answer: number,
|
||||
textType:string,
|
||||
export abstract class Question {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
lastModified: Date;
|
||||
answer: string[];
|
||||
textType:string;
|
||||
questionType:string;
|
||||
options:string[]
|
||||
|
||||
constructor(id:string, name:string, description:string, lastModified:Date
|
||||
,answer:any, textType:string, questionType:string){
|
||||
this.id= id;
|
||||
this.name=name;
|
||||
this.description=description;
|
||||
this.lastModified=lastModified;
|
||||
this.textType="text";
|
||||
this.answer=[''];
|
||||
this.questionType=questionType
|
||||
this.options=['', '']
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export interface QuestionResponseInfo {
|
||||
chosenOption: number,
|
||||
chosenOption: string[],
|
||||
}
|
|
@ -220,7 +220,7 @@ export class Repository implements IRepository {
|
|||
}
|
||||
|
||||
|
||||
public async submitStudentAssessment(assessmentId: string, chosenOptions: { [id: string]: number }) {
|
||||
public async submitStudentAssessment(assessmentId: string, chosenOptions: { [id: string]: string[] }) {
|
||||
const accessToken = await this.getAccessToken();
|
||||
const request: SubmitStudentAssessmentRequest = {
|
||||
responses: Object.entries(chosenOptions).reduce((a, item) => ({
|
||||
|
|
|
@ -3,6 +3,7 @@ export interface StudentQuestion {
|
|||
name: string,
|
||||
description: string,
|
||||
options: string[],
|
||||
chosenOption: number,
|
||||
chosenOption: string[],
|
||||
textType:string,
|
||||
questionType:string
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ import { Question } from "../Question";
|
|||
export class GiftParser extends AssessmentAppParser{
|
||||
|
||||
public parse(): void {
|
||||
|
||||
|
||||
var questionBankTitle:string = "Not defined yet";
|
||||
var questions:Question[] = [];
|
||||
|
||||
|
@ -24,12 +22,13 @@ export class GiftParser extends AssessmentAppParser{
|
|||
|
||||
var choices:TextChoice[] = q.choices;
|
||||
var answerTexts = Array();
|
||||
var correctAnswer = 0;
|
||||
var correctAnswer = [];
|
||||
for (var choice in choices){
|
||||
var details:TextChoice = choices[choice];
|
||||
answerTexts.push(this.removeTags(details.text['text']));
|
||||
if (details.isCorrect){
|
||||
correctAnswer = +choice; // plus operator converts to number
|
||||
var weight = details.weight
|
||||
if (weight != null && weight > 0){
|
||||
correctAnswer.push(choice); // plus operator converts to number
|
||||
}
|
||||
}
|
||||
var stem:TextFormat = q.stem;
|
||||
|
@ -40,7 +39,8 @@ export class GiftParser extends AssessmentAppParser{
|
|||
lastModified: new Date (),
|
||||
options: answerTexts,
|
||||
answer: correctAnswer,
|
||||
textType:stem.format
|
||||
textType:stem.format,
|
||||
questionType: "MCQ",
|
||||
}
|
||||
questions.push(question);
|
||||
|
||||
|
@ -55,13 +55,27 @@ export class GiftParser extends AssessmentAppParser{
|
|||
description: stem.text,
|
||||
lastModified: new Date (),
|
||||
options: ["True", "False"],
|
||||
answer: ans? 0:1,
|
||||
textType:stem.format
|
||||
answer: ans? ['0']:['1'],
|
||||
textType:stem.format,
|
||||
questionType: "TF",
|
||||
}
|
||||
questions.push(question);
|
||||
}
|
||||
if (q.type === "Description"|| q.type === "Essay" || q.type === "Short"){
|
||||
var stem:TextFormat = q.stem;
|
||||
const question: Question = {
|
||||
id: "",
|
||||
name: this.removeTags(stem.text),
|
||||
description: stem.text,
|
||||
lastModified: new Date (),
|
||||
options: ["", ""],
|
||||
answer: ["This is an example"], // Gift format does not give "ideal" anwer for these types
|
||||
textType:stem.format,
|
||||
questionType:"QA",
|
||||
}
|
||||
questions.push(question);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var qb: ParsedQuestionBank = {
|
||||
|
|
|
@ -15,12 +15,12 @@ export class MicrosoftOSCParser extends AssessmentAppParser{
|
|||
var questions:Question[] = [];
|
||||
for (let question of rawQuestion.quiz){
|
||||
var answerTexts = Array();
|
||||
var correctAnswer = 0;
|
||||
var correctAnswer = [];
|
||||
var counter = 0;
|
||||
for (let option of question.answerOptions){
|
||||
answerTexts.push(option.answerText)
|
||||
if (option.isCorrect == "true"){
|
||||
correctAnswer = counter ;
|
||||
correctAnswer.push(counter.toString());
|
||||
}
|
||||
counter = counter + 1;
|
||||
|
||||
|
@ -28,12 +28,13 @@ export class MicrosoftOSCParser extends AssessmentAppParser{
|
|||
|
||||
const questionToSave: Question = {
|
||||
id: "",
|
||||
name: question.questionText,
|
||||
name: question.questionText,
|
||||
description: question.questionText,
|
||||
lastModified: new Date (),
|
||||
lastModified: new Date(),
|
||||
options: answerTexts,
|
||||
answer: correctAnswer,
|
||||
textType:"text"
|
||||
textType: "text",
|
||||
questionType: "MCQ"
|
||||
}
|
||||
questions.push(questionToSave);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ParsedQuestionBank } from "./ParsedQuestionBank";
|
|||
import { AssessmentAppParser } from "./Parser";
|
||||
import { Question } from "../Question";
|
||||
|
||||
// Currently only supports MCQs and TFs
|
||||
export class OriginalAppParser extends AssessmentAppParser{
|
||||
public parse() {
|
||||
const rawData = JSON.parse(this.raw);
|
||||
|
@ -13,10 +14,11 @@ export class OriginalAppParser extends AssessmentAppParser{
|
|||
id: "",
|
||||
name: rawQuestion.name,
|
||||
description: rawQuestion.description,
|
||||
lastModified: new Date (),
|
||||
lastModified: new Date(),
|
||||
options: rawQuestion.options,
|
||||
answer: rawQuestion.answer,
|
||||
textType: rawQuestion.textType
|
||||
textType: rawQuestion.textType,
|
||||
questionType: rawQuestion.questionType
|
||||
}
|
||||
questions.push(question);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export class AssessmentAppParser {
|
|||
str = str.toString();
|
||||
str = str.replace( /(<([^>]+)>)/ig, '')
|
||||
str = str.replaceAll('\\n','')
|
||||
str = str.replace(/\ /g, "")
|
||||
return str;}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,10 +3,16 @@ import { ParsedQuestionBank } from "./ParsedQuestionBank";
|
|||
import { AssessmentAppParser } from "./Parser";
|
||||
import { Question } from "../Question";
|
||||
|
||||
|
||||
interface questionTypeSpecificParse {
|
||||
correctAnswer:string[]
|
||||
options:string[]
|
||||
};
|
||||
export class QTIParser extends AssessmentAppParser{
|
||||
options = {
|
||||
ignoreAttributes:false
|
||||
};
|
||||
|
||||
public parse() {
|
||||
var questionBankTitle:string = "Not defined yet";
|
||||
var questions:Question[] = [];
|
||||
|
@ -19,40 +25,57 @@ export class QTIParser extends AssessmentAppParser{
|
|||
|
||||
for (let questionId in questionsSection){
|
||||
var currQuestion = questionsSection[questionId];
|
||||
// Get question title
|
||||
var questionTitle = currQuestion['@_title'];
|
||||
|
||||
// Get question type
|
||||
var qMetaDataField = currQuestion['itemmetadata']['qtimetadata']['qtimetadatafield'];
|
||||
var metaData = qMetaDataField[0]; // 0 position contains question type
|
||||
if (metaData['fieldentry'] != 'multiple_choice_question'){
|
||||
continue; // As we currently only support MCQs
|
||||
}
|
||||
var questionType = 'NA';
|
||||
|
||||
// Get question description
|
||||
var questionText = currQuestion['presentation']['material']['mattext']['#text'];
|
||||
questionText = questionText.split('\n')[1];
|
||||
|
||||
// Get all options
|
||||
var responseLabels = currQuestion['presentation']['response_lid']['render_choice']['response_label'];
|
||||
var answerTexts = Array();
|
||||
var correctAnswer = currQuestion['resprocessing']['respcondition']['conditionvar']['varequal']['#text']
|
||||
for (let responseId in responseLabels){
|
||||
var response = responseLabels[responseId];
|
||||
if (correctAnswer == response['@_ident']){
|
||||
correctAnswer = responseId;
|
||||
}
|
||||
answerTexts.push(this.removeTags(response['material']['mattext']['#text']))
|
||||
// Get text type
|
||||
var cleanedTextType:string = 'text'
|
||||
if (currQuestion['presentation']['material']['mattext']['@_texttype'] === "text/html"){
|
||||
cleanedTextType = "html";
|
||||
}
|
||||
// Finally creating the question
|
||||
// Get answer and options
|
||||
|
||||
var result:questionTypeSpecificParse = {options:[], correctAnswer:[]}
|
||||
if (metaData['fieldentry'] === 'multiple_choice_question' ){
|
||||
result = this.parseMCQ(currQuestion);
|
||||
questionType = "MCQ";
|
||||
}
|
||||
else if (metaData['fieldentry'] === 'true_false_question'){
|
||||
result = this.parseMCQ(currQuestion);
|
||||
questionType = "TF";
|
||||
}
|
||||
else if (metaData['fieldentry'] === 'multiple_answers_question'){
|
||||
result= this.parseMAQ(currQuestion);
|
||||
questionType = "MCQ";
|
||||
}
|
||||
else if (metaData['fieldentry'] === 'numerical_question'){
|
||||
result= this.parseQA(currQuestion);
|
||||
questionType = "QA"
|
||||
}
|
||||
else{
|
||||
continue;
|
||||
}
|
||||
|
||||
const question:Question = {
|
||||
id: "",
|
||||
name: questionTitle,
|
||||
description: questionText,
|
||||
lastModified: new Date (),
|
||||
options: answerTexts,
|
||||
answer: correctAnswer,
|
||||
textType:currQuestion['presentation']['material']['mattext']['@_texttype'],
|
||||
}
|
||||
id: "",
|
||||
name: questionTitle,
|
||||
description: questionText,
|
||||
lastModified: new Date(),
|
||||
options: result.options,
|
||||
answer: result.correctAnswer,
|
||||
textType: cleanedTextType,
|
||||
questionType: questionType
|
||||
}
|
||||
questions.push(question);
|
||||
|
||||
}
|
||||
|
||||
var qb: ParsedQuestionBank = {
|
||||
questionBankTitle: questionBankTitle,
|
||||
questions:questions
|
||||
|
@ -61,6 +84,63 @@ export class QTIParser extends AssessmentAppParser{
|
|||
|
||||
}
|
||||
|
||||
private parseMCQ(currQuestion:any){
|
||||
// Get all options
|
||||
var responseLabels = currQuestion['presentation']['response_lid']['render_choice']['response_label'];
|
||||
var answerTexts = Array();
|
||||
var correctAnswerId = currQuestion['resprocessing']['respcondition']['conditionvar']['varequal']['#text'].toString();
|
||||
var correctAnswer = ['-1'];
|
||||
for (let responseId in responseLabels){
|
||||
var response = responseLabels[responseId];
|
||||
if (correctAnswerId === response['@_ident'].toString()){
|
||||
correctAnswer = [responseId.toString()];
|
||||
}
|
||||
answerTexts.push(this.removeTags(response['material']['mattext']['#text']))
|
||||
}
|
||||
var result: questionTypeSpecificParse = {options:answerTexts, correctAnswer:correctAnswer}
|
||||
return result
|
||||
}
|
||||
|
||||
private parseMAQ(currQuestion:any){
|
||||
|
||||
// Get all options
|
||||
var responseLabels = currQuestion['presentation']['response_lid']['render_choice']['response_label'];
|
||||
var answerTexts = Array();
|
||||
var correctAnswerQTIIds = currQuestion['resprocessing']['respcondition']['conditionvar']['and']['varequal']
|
||||
var correctAnsIds = new Set();
|
||||
|
||||
for (let id in correctAnswerQTIIds){
|
||||
var answerId = correctAnswerQTIIds[id]['#text'];
|
||||
correctAnsIds.add(answerId.toString());
|
||||
}
|
||||
var correctAns:string[] = Array();
|
||||
for (let responseId in responseLabels){
|
||||
var response = responseLabels[responseId];
|
||||
if (correctAnsIds.has(response['@_ident'].toString())){
|
||||
var correctAnswer = +responseId
|
||||
correctAns.push(correctAnswer.toString())
|
||||
}
|
||||
answerTexts.push(this.removeTags(response['material']['mattext']['#text']))
|
||||
}
|
||||
|
||||
var result: questionTypeSpecificParse = {options:answerTexts, correctAnswer:correctAns}
|
||||
return result
|
||||
}
|
||||
private parseQA(currQuestion:any){
|
||||
|
||||
//Get all allowed answers
|
||||
var correctAnswerQTIIds = currQuestion['resprocessing']['respcondition']
|
||||
var correctAns = new Array();
|
||||
for (let index in correctAnswerQTIIds){
|
||||
var curr = correctAnswerQTIIds[index]['conditionvar']['or']['varequal']['#text'];
|
||||
correctAns.push(curr.toString());
|
||||
}
|
||||
var result: questionTypeSpecificParse = {options:[], correctAnswer:correctAns}
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {PrimaryButton, Spinner} from '@fluentui/react';
|
||||
import * as React from 'react';
|
||||
import {useContext, useEffect, useState} from 'react';
|
||||
import {StudentQuestionComponent} from '../../components/StudentQuestionComponent';
|
||||
import {StudentQuestion} from '../../model/StudentQuestion';
|
||||
|
@ -17,7 +16,7 @@ export const StudentQuiz = () => {
|
|||
const {id} = useParams<StudentQuizParams>();
|
||||
const repositoryContext = useContext(RepositoryContext);
|
||||
const [questions, setQuestions] = useState<StudentQuestion[]>([])
|
||||
const [chosenOptions, setChosenOptions] = useState<{ [id: string]: number }>({});
|
||||
const [chosenOptions, setChosenOptions] = useState<{ [id: string]: any }>({});
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [studentAssessment, setStudentAssessment] = useState<StudentAssessment | null>(null);
|
||||
const history = useHistory();
|
||||
|
|
|
@ -20,8 +20,9 @@ export const NewQuestionPage = () => {
|
|||
description: "",
|
||||
lastModified: new Date(),
|
||||
options: ['', ''],
|
||||
answer: -1,
|
||||
textType:""
|
||||
answer: [],
|
||||
textType:"",
|
||||
questionType:"MCQ",
|
||||
});
|
||||
const {bankId} = useParams<NewQuestionPageParams>();
|
||||
const repositoryContext = React.useContext(RepositoryContext);
|
||||
|
@ -46,7 +47,7 @@ export const NewQuestionPage = () => {
|
|||
padding: '10px',
|
||||
boxShadow: theme.effects.elevation8
|
||||
}}>
|
||||
<EditQuestionComponent question={question} setQuestion={setQuestion}/>
|
||||
<EditQuestionComponent question={question} setQuestion={setQuestion}/>
|
||||
<Container style={{margin: '30px', position: 'relative'}}>
|
||||
<Row>
|
||||
<Col md={2}/>
|
||||
|
|
|
@ -21,18 +21,20 @@ export const QuestionPage = () => {
|
|||
name: "",
|
||||
description: "",
|
||||
lastModified: new Date(),
|
||||
options: ['', ''],
|
||||
answer: -1,
|
||||
textType:"text"
|
||||
questionType: "",
|
||||
answer: ['-1'],
|
||||
textType:"text",
|
||||
options:['', '']
|
||||
});
|
||||
const [savedQuestion, setSavedQuestion] = useState<Question>({
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
lastModified: new Date(),
|
||||
options: ['', ''],
|
||||
answer: -1,
|
||||
textType:"text"
|
||||
questionType:"",
|
||||
answer: [],
|
||||
textType:"text",
|
||||
options:['', '']
|
||||
})
|
||||
const {id} = useParams<QuestionPageParams>();
|
||||
const repositoryContext = React.useContext(RepositoryContext);
|
||||
|
|
|
@ -105,7 +105,7 @@ export const QuestionBankPage = () => {
|
|||
if (repositoryContext == null) {
|
||||
return <p>Question Bank cannot be found</p>
|
||||
}
|
||||
const redirectToNewQuestion = () => {
|
||||
const redirectToNewQuestion = () => {
|
||||
history.push(`/spa/new-question/bank=${id}`);
|
||||
}
|
||||
const _items: ICommandBarItemProps[] = [
|
||||
|
|
Загрузка…
Ссылка в новой задаче