diff --git a/actions/loadContentModules.js b/actions/loadContentModules.js index 66f3662dc..46139c015 100644 --- a/actions/loadContentModules.js +++ b/actions/loadContentModules.js @@ -10,6 +10,7 @@ import deckContentTypeError from './error/deckContentTypeError'; import slideIdTypeError from './error/slideIdTypeError'; import { AllowedPattern } from './error/util/allowedPattern'; import serviceUnavailable from './error/serviceUnavailable'; +import PermissionsStore from '../stores/PermissionsStore'; const log = require('./log/clog'); @@ -35,6 +36,9 @@ export default function loadContentModules(context, payload, done) { // }, (callback) => { + let editPermission = (context.getStore(PermissionsStore) && context.getStore(PermissionsStore).permissions && (context.getStore(PermissionsStore).permissions.admin || context.getStore(PermissionsStore).permissions.edit)); + + payload.params.nonExamQuestionsOnly = !editPermission; context.executeAction(loadQuestionsCount, payload, callback); }, (callback) => { diff --git a/actions/loadContentQuestions.js b/actions/loadContentQuestions.js index a872e4a7d..b7de4195f 100644 --- a/actions/loadContentQuestions.js +++ b/actions/loadContentQuestions.js @@ -26,15 +26,12 @@ export default function loadContentQuestions(context, payload, done) { if (err) { log.error(context, {filepath: __filename}); context.executeAction(serviceUnavailable, payload, done); - //context.dispatch('LOAD_CONTENT_QUESTIONS_FAILURE', err); } else { context.dispatch('LOAD_CONTENT_QUESTIONS_SUCCESS', res); context.dispatch('UPDATE_MODULE_TYPE_SUCCESS', {moduleType: 'questions'}); } let pageTitle = shortTitle + ' | Content Questions | ' + payload.params.stype + ' | ' + payload.params.sid; - //context.dispatch('UPDATE_PAGE_TITLE', { - // pageTitle: pageTitle - //}); + done(); }); } diff --git a/actions/questions/invertExamListFlag.js b/actions/questions/invertExamListFlag.js new file mode 100644 index 000000000..78331244d --- /dev/null +++ b/actions/questions/invertExamListFlag.js @@ -0,0 +1,7 @@ +const log = require('../log/clog'); + +export default function invertExamListFlag(context, payload, done) { + log.info(context); + context.dispatch('INVERT_EXAM_LIST_FLAG', payload); + done(); +} diff --git a/actions/questions/loadExamQuestions.js b/actions/questions/loadExamQuestions.js new file mode 100644 index 000000000..db8dc377a --- /dev/null +++ b/actions/questions/loadExamQuestions.js @@ -0,0 +1,37 @@ +const log = require('../log/clog'); +import ContentQuestionsStore from '../../stores/ContentQuestionsStore'; +import deckContentTypeError from '../error/deckContentTypeError'; +import serviceUnavailable from '../error/serviceUnavailable'; +import slideIdTypeError from '../error/slideIdTypeError'; +import { AllowedPattern } from '../error/util/allowedPattern'; +export default function loadExamQuestions(context, payload, done) { + log.info(context); + const selector = context.getStore(ContentQuestionsStore).selector; + const sid = (selector.sid) ? selector.sid.split('-')[0] : ''; + if (payload.params.sid.split('-')[0] !== sid || payload.params.stype !== selector.stype) { + + if (!(['deck', 'slide', 'question'].indexOf(payload.params.stype) > -1 || payload.params.stype === undefined)){ + context.executeAction(deckContentTypeError, payload, done); + return; + } + + if (!(AllowedPattern.SLIDE_ID.test(payload.params.sid) || payload.params.sid === undefined)) { + context.executeAction(slideIdTypeError, payload, done); + return; + } + + context.service.read('questions.examlist', payload, {timeout: 20 * 1000}, (err, res) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + } else { + context.dispatch('LOAD_CONTENT_QUESTIONS_SUCCESS', res); + context.dispatch('UPDATE_MODULE_TYPE_SUCCESS', {moduleType: 'questions'}); + } + + done(); + }); + } else { + done(); + } +} diff --git a/actions/questions/resetExamAnswers.js b/actions/questions/resetExamAnswers.js new file mode 100644 index 000000000..a83dc1889 --- /dev/null +++ b/actions/questions/resetExamAnswers.js @@ -0,0 +1,6 @@ +import log from '../log/clog'; +export default function resetExamAnswers(context, payload, done) { + log.info(context); + context.dispatch('RESET_ANSWERS', payload); + done(); +} diff --git a/actions/questions/selectExamAnswer.js b/actions/questions/selectExamAnswer.js new file mode 100644 index 000000000..6cb57a995 --- /dev/null +++ b/actions/questions/selectExamAnswer.js @@ -0,0 +1,6 @@ +import log from '../log/clog'; +export default function selectExamAnswer(context, payload, done) { + log.info(context); + context.dispatch('QUESTION_ANSWER_SELECTED', payload); + done(); +} diff --git a/actions/questions/showCorrectExamAnswers.js b/actions/questions/showCorrectExamAnswers.js new file mode 100644 index 000000000..4120dae5b --- /dev/null +++ b/actions/questions/showCorrectExamAnswers.js @@ -0,0 +1,6 @@ +import log from '../log/clog'; +export default function showCorrectExamAnswers(context, payload, done) { + log.info(context); + context.dispatch('SHOW_CORRECT_EXAM_ANSWERS', payload); + done(); +} diff --git a/actions/questions/updateExamList.js b/actions/questions/updateExamList.js new file mode 100644 index 000000000..403acd008 --- /dev/null +++ b/actions/questions/updateExamList.js @@ -0,0 +1,17 @@ +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import ContentQuestionsStore from '../../stores/ContentQuestionsStore'; + +export default function updateExamList(context, payload, done) { + log.info(context, payload); + + context.service.update('questions.updateExamList', {modifiedSelections: payload.modifiedSelections}, {timeout: 20 * 1000}, (err, res) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + } else { + context.dispatch('UPDATE_QUESTIONS', payload); + } + done(); + }); +} diff --git a/components/Deck/ContentModulesPanel/ContentModulesPanel.js b/components/Deck/ContentModulesPanel/ContentModulesPanel.js index c3c21e097..21835c781 100644 --- a/components/Deck/ContentModulesPanel/ContentModulesPanel.js +++ b/components/Deck/ContentModulesPanel/ContentModulesPanel.js @@ -19,6 +19,7 @@ import DataSourcePanel from './DataSourcePanel/DataSourcePanel'; import TagsPanel from './TagsPanel/TagsPanel'; //import ContributorsPanel from './ContributorsPanel/ContributorsPanel'; import ContentModulesStore from '../../../stores/ContentModulesStore'; +import PermissionsStore from '../../../stores/PermissionsStore'; import { isLocalStorageOn } from '../../../common.js'; import loadCollectionsTab from '../../../actions/collections/loadCollectionsTab'; import CollectionsPanel from './CollectionsPanel/CollectionsPanel'; @@ -78,7 +79,10 @@ class ContentModulesPanel extends React.Component { handleTabClick(type, e) { switch (type) { case 'questions': - this.context.executeAction(loadContentQuestions, {params: this.props.ContentModulesStore.selector}); + let editPermission = (this.props.PermissionsStore && this.props.PermissionsStore.permissions && (this.props.PermissionsStore.permissions.admin || this.props.PermissionsStore.permissions.edit)); + let params = this.props.ContentModulesStore.selector; + this.context.executeAction(loadContentQuestions, {params: params}); + break; case 'datasource': this.context.executeAction(loadDataSources, {params: this.props.ContentModulesStore.selector}); @@ -277,9 +281,10 @@ class ContentModulesPanel extends React.Component { ContentModulesPanel.contextTypes = { executeAction: PropTypes.func.isRequired }; -ContentModulesPanel = connectToStores(ContentModulesPanel, [ContentModulesStore], (context, props) => { +ContentModulesPanel = connectToStores(ContentModulesPanel, [ContentModulesStore, PermissionsStore], (context, props) => { return { - ContentModulesStore: context.getStore(ContentModulesStore).getState() + ContentModulesStore: context.getStore(ContentModulesStore).getState(), + PermissionsStore: context.getStore(PermissionsStore).getState() }; }); export default ContentModulesPanel; diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAdd.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAdd.js index 5769fe781..b1c76db90 100644 --- a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAdd.js +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAdd.js @@ -24,6 +24,7 @@ class ContentQuestionAdd extends React.Component { userId: this.props.userId, relatedObjectId: this.props.selector.sid, relatedObject: this.props.selector.stype, + isExamQuestion: false }; this.updateQuestionTitle = this.updateQuestionTitle.bind(this); this.updateQuestionDifficulty = this.updateQuestionDifficulty.bind(this); @@ -39,6 +40,7 @@ class ContentQuestionAdd extends React.Component { this.updateCorrect4 = this.updateCorrect4.bind(this); this.updateExplanation = this.updateExplanation.bind(this); + this.updateIsExamQuestion = this.updateIsExamQuestion.bind(this); this.saveButtonClick = this.saveButtonClick.bind(this); this.cancelButtonClick = this.cancelButtonClick.bind(this); } @@ -132,6 +134,10 @@ class ContentQuestionAdd extends React.Component { updateQuestionDifficulty(e) { this.setState({difficulty: e.target.value}); } + + updateIsExamQuestion(e) { + this.setState({isExamQuestion: e.target.checked}); + } render() { //const numAnswers = this.props.question.answers.length; @@ -139,7 +145,7 @@ class ContentQuestionAdd extends React.Component { width: '680px', }; return ( -
+
@@ -213,6 +219,12 @@ class ContentQuestionAdd extends React.Component {
+
+
+ + +
+
diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAnswersItem.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAnswersItem.js index 7b9f61bb6..5b571d49b 100644 --- a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAnswersItem.js +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ContentQuestionAnswersItem.js @@ -4,24 +4,12 @@ class ContentQuestionAnswersItem extends React.Component { render() { const answer = this.props.answer; - const name = this.props.name; - - // let rightIcon = (); - // switch (answer.correct) { - // case true: - // rightIcon = (); - // break; - // } - + return ( - //
{rightIcon} - //
{answer.answer}
- //
-
-
- {/* defaultChecked={answer.correct} */} -
- ); + ) : ''; break; } let editPermission = (this.props.PermissionsStore.permissions.admin || this.props.PermissionsStore.permissions.edit); - // console.log(editPermission); - let addQuestionButton = ((editPermission) ? -
- + + let visibleQuestions = []; + questions.forEach((question) => { + if (editPermission || !question.isExamQuestion) { + visibleQuestions.push(question); + } + }); + + let examQuestionsButton = (questions.length > 0 && this.props.ContentModulesStore.selector.stype === 'deck') ? + : ''; + + + let addQuestionButton = + + ; + + let editButtons = (editPermission) ?
+ {addQuestionButton} + {examQuestionsButton}
- : ''); + : ''; + let downloadQuestionsButton = ; /* let addQuestionButton = ( -
+
); @@ -117,85 +150,87 @@ class ContentQuestionsPanel extends React.Component { }; */ let questionsHeader = ( -
-
-
+
+
+
-
-
+
+

Questions

-
- {addQuestionButton} - {downloadQuestionsButton} +
+ {editButtons} + {downloadQuestionsButton}
- {content}
); - class PaginationItem extends React.Component { - constructor(props){ - super(props); - this._onClick = this._onClick.bind(this); - } - _onClick() { - this.props.onItemClick(this.props.pageNo); - } - - render() { - let className = 'item'; - if(this.props.isActiveItem){ - className += ' active'; - } - return ( - - {this.props.pageNo} - - ); - } - } - - let getItems = () => { - let noOfQuestions = questionsCount; - let items = []; - let pageNo = 1; - for(let i = 0; i < noOfQuestions; i+=itemsPerPage) { - items.push( - - ); - } - return items; - }; - - let lastPageNo = parseInt(questionsCount / itemsPerPage) + 1; - let pagination = ( - - ); - let questionsList = (); + // class PaginationItem extends React.Component { + // constructor(props){ + // super(props); + // this._onClick = this._onClick.bind(this); + // } + // _onClick() { + // this.props.onItemClick(this.props.pageNo); + // } + // + // render() { + // let className = 'item'; + // if(this.props.isActiveItem){ + // className += ' active'; + // } + // return ( + // + // {this.props.pageNo} + // + // ); + // } + // } + + // let getItems = () => { + // let noOfQuestions = questionsCount; + // let items = []; + // let pageNo = 1; + // for(let i = 0; i < noOfQuestions; i+=itemsPerPage) { + // items.push( + // + // ); + // } + // return items; + // }; + + // let lastPageNo = parseInt(questionsCount / itemsPerPage) + 1; + // let pagination = ( + // + // ); + let questionsList = (); + let examQuestionsList = (); + let questionAdd = (); + let questionEdit = (); let content = (
- {/* {buttonBar} */} + {buttonBar} {questionsHeader} - {questions.length === 0 ? 'There are currently no questions for this ' + selector.stype + '.' : questionsList} + {visibleQuestions.length === 0 ? 'There are currently no questions for this ' + selector.stype + '.' : questionsList} {/* {pagination} */}
); return (
- { this.props.ContentQuestionsStore.showAddBox ? : this.props.ContentQuestionsStore.question ? : content } + { this.props.ContentQuestionsStore.showAddBox ? questionAdd : this.props.ContentQuestionsStore.question ? questionEdit : this.props.ContentQuestionsStore.showExamList ? examQuestionsList : content }
); } diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamAnswersItem.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamAnswersItem.js new file mode 100644 index 000000000..a46e99781 --- /dev/null +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamAnswersItem.js @@ -0,0 +1,46 @@ +import React from 'react'; +import selectExamAnswer from '../../../../actions/questions/selectExamAnswer'; +class ExamAnswersItem extends React.Component { + handleOnChange() { + if (!this.props.showCorrectAnswers) { + this.context.executeAction(selectExamAnswer, { + questionIndex: this.props.originalQIndex, + answerIndex: this.props.index, + selected: this.refs[this.props.name].checked + }); + } + } + render() { + const answer = this.props.answer; + const name = this.props.name; + const showCorrectAnswers = this.props.showCorrectAnswers; + if (answer.selectedAnswer === undefined) { + answer.selectedAnswer = false; + } + let answerIcon = (); + if (showCorrectAnswers) { + if (answer.correct && answer.selectedAnswer) { + answerIcon = (); + } else if (answer.correct && !answer.selectedAnswer) { + answerIcon = (); + } else if (!answer.correct && answer.selectedAnswer) { + answerIcon = (); + } + } + let inputCheckbox = (showCorrectAnswers) ? () : (); + return ( +
+
+ {inputCheckbox} + +
+
+ ); + } +} +ExamAnswersItem.contextTypes = { + executeAction: React.PropTypes.func.isRequired +}; +export default ExamAnswersItem; diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamAnswersList.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamAnswersList.js new file mode 100644 index 000000000..a9562a234 --- /dev/null +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamAnswersList.js @@ -0,0 +1,30 @@ +import React from 'react'; +import ExamAnswersItem from './ExamAnswersItem'; +class ExamAnswersList extends React.Component { + render() { + let wrongAnswer = false; + for (let answer of this.props.items) { + if (answer.selectedAnswer === undefined) { + answer.selectedAnswer = false; + } + if (answer.correct !== answer.selectedAnswer) { + wrongAnswer = true; + break; + } + } + let explanation = (this.props.explanation.trim() !== '') ? 'Explanation: ' + this.props.explanation : ''; + let wrongAnswerInfo = (this.props.showCorrectAnswers && wrongAnswer) ? (
{explanation}
) : ''; + let list = this.props.items.map((node, index) => { + return ( + + ); + }); + return ( +
+ {list} + {wrongAnswerInfo} +
+ ); + } +} +export default ExamAnswersList; diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamItem.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamItem.js new file mode 100644 index 000000000..163db7ca2 --- /dev/null +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamItem.js @@ -0,0 +1,55 @@ +import React from 'react'; +import ExamAnswersList from './ExamAnswersList'; +class ExamItem extends React.Component { + render() { + const question = this.props.question; + const answers = ( + + ); + + let difficultyStars = (difficulty) => { + let difficultyClass = ''; + switch (difficulty) { + case 1: + difficultyClass = 'ui small yellow star icon'; + break; + case 2: + difficultyClass = 'ui small orange star icon'; + break; + case 3: + difficultyClass = 'ui small red star icon'; + break; + } + let difficultyStars = []; + for(let i = 0; i < difficulty; i++){ + difficultyStars.push(); + } + return difficultyStars; + + }; + let questionNo = this.props.questionIndex + 1; + + return ( +
+
+
{'Question' + questionNo + 'difficulty level ' + question.difficulty}
+ +
+ ); + } +} +export default ExamItem; diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamList.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamList.js new file mode 100644 index 000000000..8d06162af --- /dev/null +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamList.js @@ -0,0 +1,109 @@ +import React from 'react'; +import {connectToStores} from 'fluxible-addons-react'; +import {navigateAction} from 'fluxible-router'; +import ExamItem from './ExamItem'; +import UserProfileStore from '../../../../stores/UserProfileStore'; +import DeckTreeStore from '../../../../stores/DeckTreeStore'; +import ContentQuestionsStore from '../../../../stores/ContentQuestionsStore'; +import Util from '../../../common/Util'; +import showCorrectExamAnswers from '../../../../actions/questions/showCorrectExamAnswers'; +import addActivity from '../../../../actions/activityfeed/addActivity'; +class ExamList extends React.Component { + getPath(){ + const selector = this.props.selector; + if (selector.id === undefined || selector.id === 'undefined') {//this can happen when exam has not been opened from the deck view + return '/' + selector.stype + '/' + selector.sid; + } else { + const flatTree = this.props.DeckTreeStore.flatTree; + let path = ''; + for (let i=0; i < flatTree.size; i++) { + if (flatTree.get(i).get('type') === selector.stype && flatTree.get(i).get('id').split('-')[0] === selector.sid.split('-')[0]) { + path = flatTree.get(i).get('path'); + let nodeSelector = {id: selector.id, stype: selector.stype, sid: selector.sid.split('-')[0], spath: path}; + let nodeURL = Util.makeNodeURL(nodeSelector, 'deck', 'view', undefined, undefined, true); + return nodeURL; + } + } + return path; + } + } + cancelButtonClick(e) { + e.preventDefault(); + this.context.executeAction(navigateAction, { + url: this.getPath() + }); + } + handleSubmitAnswers(e) { + e.preventDefault(); + let questions = this.props.items; + let errorsCount = 0; + questions.forEach((question) => { + const answers = question.answers; + for(let answer of answers) { + if (answer.selectedAnswer === undefined) { + answer.selectedAnswer = false; + } + if (answer.correct !== answer.selectedAnswer) { + errorsCount++; + break; + } + } + }); + const roundedScore = Math.round((questions.length - errorsCount) / questions.length * 100); + swal({ + title: 'Exam submitted', + text: 'Your score: ' + roundedScore + ' %', + type: 'success', + showCloseButton: false, + showCancelButton: false, + allowEscapeKey: false, + showConfirmButton: true + }); + this.context.executeAction(showCorrectExamAnswers, {}); + //create an activity + let activity = { + activity_type: 'exam', + user_id: String(this.props.UserProfileStore.userid), + content_id: this.props.selector.sid, + content_kind: this.props.selector.stype, + exam_info: { + score: roundedScore + } + }; + this.context.executeAction(addActivity, {activity: activity}); + return false; + } + render() { + let showCorrectAnswers = this.props.ContentQuestionsStore.showCorrectExamAnswers; + let list = this.props.items.map((node, index) => { + return ( + + ); + }); + return ( +
+ + {list} +
+ + + +
+ ); + } +} +ExamList.contextTypes = { + executeAction: React.PropTypes.func.isRequired +}; +ExamList = connectToStores(ExamList, [UserProfileStore, DeckTreeStore, ContentQuestionsStore], (context, props) => { + return { + UserProfileStore: context.getStore(UserProfileStore).getState(), + DeckTreeStore: context.getStore(DeckTreeStore).getState(), + ContentQuestionsStore: context.getStore(ContentQuestionsStore).getState() + }; +}); +export default ExamList; diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamPanel.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamPanel.js new file mode 100644 index 000000000..7751add76 --- /dev/null +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamPanel.js @@ -0,0 +1,80 @@ +import React from 'react'; +import {connectToStores} from 'fluxible-addons-react'; +import {navigateAction} from 'fluxible-router'; +import ContentQuestionsStore from '../../../../stores/ContentQuestionsStore'; +import DeckViewStore from '../../../../stores/DeckViewStore'; +import DeckTreeStore from '../../../../stores/DeckTreeStore'; +import Util from '../../../common/Util'; +import ExamList from './ExamList'; + +class ExamPanel extends React.Component { + getPath(){ + const selector = this.props.ContentQuestionsStore.selector; + if (selector.id === undefined || selector.id === 'undefined') {//this can happen when exam has not been opened from the deck view + return '/' + selector.stype + '/' + selector.sid; + } else { + const flatTree = this.props.DeckTreeStore.flatTree; + let path = ''; + for (let i=0; i < flatTree.size; i++) { + if (flatTree.get(i).get('type') === selector.stype && flatTree.get(i).get('id').split('-')[0] === selector.sid.split('-')[0]) { + path = flatTree.get(i).get('path'); + let nodeSelector = {id: selector.id, stype: selector.stype, sid: selector.sid.split('-')[0], spath: path}; + let nodeURL = Util.makeNodeURL(nodeSelector, 'deck', 'view', undefined, undefined, true); + return nodeURL; + } + } + return path; + } + } + cancelButtonClick(e) { + e.preventDefault(); + this.context.executeAction(navigateAction, { + url: this.getPath() + }); + } + render() { + const questions = this.props.ContentQuestionsStore.questions; + let examQuestions = []; + questions.forEach((question) => { + if (question.isExamQuestion) { + question.originalQIndex = questions.indexOf(question); + examQuestions.push(question); + } + }); + const selector = this.props.ContentQuestionsStore.selector; + let title = ''; + if (this.props.DeckViewStore.deckData && this.props.DeckViewStore.deckData.revisions) { + let revisions = this.props.DeckViewStore.deckData.revisions; + title = revisions[revisions.length - 1].title + ' - '; + } + let questionsList = (); + let noExamQuestionsNotification = ( +
+

There are currently no exam questions for this {selector.stype}.

+ +
+ ); + + return ( +
+
+

{title}Exam mode

+
+ {examQuestions.length === 0 ? noExamQuestionsNotification : questionsList} +
+ ); + } +} +ExamPanel.contextTypes = { + executeAction: React.PropTypes.func.isRequired +}; +ExamPanel = connectToStores(ExamPanel, [ContentQuestionsStore, DeckTreeStore, DeckViewStore], (context, props) => { + return { + ContentQuestionsStore: context.getStore(ContentQuestionsStore).getState(), + DeckTreeStore: context.getStore(DeckTreeStore).getState(), + DeckViewStore: context.getStore(DeckViewStore).getState() + }; +}); +export default ExamPanel; diff --git a/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamQuestionsList.js b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamQuestionsList.js new file mode 100644 index 000000000..1f3f7a34f --- /dev/null +++ b/components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamQuestionsList.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {Divider} from 'semantic-ui-react'; +import invertExamListFlag from '../../../../actions/questions/invertExamListFlag'; +import updateExamList from '../../../../actions/questions/updateExamList'; + +class ExamQuestionsList extends React.Component { + constructor(props){ + super(props); + + this.state = { + modifiedSelections: [] + }; + } + + saveButtonClick() { + this.context.executeAction(updateExamList, {modifiedSelections: this.state.modifiedSelections}); + } + + cancelButtonClick() { + this.context.executeAction(invertExamListFlag, {}); + } + + invertExamQuestionClick(index) { + let modifiedSelectionIndex = this.state.modifiedSelections.findIndex((modifiedSelection) => modifiedSelection.id === this.props.items[index].id); + if (modifiedSelectionIndex > -1) { + this.state.modifiedSelections.splice(modifiedSelectionIndex, 1); + } else { + const newValue = !this.props.items[index].isExamQuestion; + this.state.modifiedSelections.push({id: this.props.items[index].id, is_exam_question: newValue}); + } + } + + render() { + let list = this.props.items.map((node, index) => { + return ( +
+
+ + +
+
+ ); + }); + + return ( +
+

Select exam questions

+
+ {list} +
+ +
+ + +
+ +
+ ); + } +} + +ExamQuestionsList.contextTypes = { + executeAction: PropTypes.func.isRequired +}; + +export default ExamQuestionsList; diff --git a/configs/routes.js b/configs/routes.js index 55d8225de..5b688ae17 100644 --- a/configs/routes.js +++ b/configs/routes.js @@ -16,6 +16,7 @@ import loadTranslations from '../actions/loadTranslations'; import loadContentHistory from '../actions/history/loadContentHistory'; import loadContentUsage from '../actions/loadContentUsage'; import loadContentQuestions from '../actions/loadContentQuestions'; +import loadExamQuestions from '../actions/questions/loadExamQuestions'; import loadContentDiscussion from '../actions/contentdiscussion/loadContentDiscussion'; import loadSimilarContents from '../actions/loadSimilarContents'; import loadImportFile from '../actions/loadImportFile'; @@ -565,6 +566,15 @@ export default { context.executeAction(loadContentQuestions, payload, done); } }, + exam: { + path: '/exam/:stype/:sid', + method: 'get', + page: 'exam', + handler: require('../components/Deck/ContentModulesPanel/ContentQuestionsPanel/ExamPanel'), + action: (context, payload, done) => { + context.executeAction(loadExamQuestions, payload, done); + } + }, discussion: { path: '/discussion/:stype/:sid', method: 'get', diff --git a/services/questions.js b/services/questions.js index c06231c23..881eb0da3 100644 --- a/services/questions.js +++ b/services/questions.js @@ -11,14 +11,34 @@ export default { let selector= {'id': String(args.id), 'spath': args.spath, 'sid': String(args.sid), 'stype': args.stype}; if(resource === 'questions.list') { + rp.get({ - // uri: 'https://questionservice.experimental.slidewiki.org/questions', - uri: Microservices.questions.uri + '/' + args.stype + '/' + args.sid.split('-')[0] + '/' + 'questions?include_subdecks_and_slides=true' + uri: Microservices.questions.uri + '/' + args.stype + '/' + args.sid.split('-')[0] + '/' + 'questions?include_subdecks_and_slides=true', }).then((res) => { let questions = JSON.parse(res).map((item, index) => { return { - id: item.id, title: item.question, difficulty: item.difficulty, relatedObject: item.related_object, relatedObjectId: item.related_object_id, relatedObjectName: item.related_object_name, + id: item.id, title: item.question, difficulty: item.difficulty, relatedObject: item.related_object, relatedObjectId: item.related_object_id, relatedObjectName: item.related_object_name, isExamQuestion: item.is_exam_question, + answers: item.choices + .map((ans, ansIndex) => { + return {answer: ans.choice, correct: ans.is_correct}; + }), + explanation: item.explanation, + userId: item.user_id, + }; + }); + callback(null, {questions: questions, selector: selector}); + }).catch((err) => { + console.log(err); + callback(err, {}); + }); + } else if(resource === 'questions.examlist') { + rp.get({ + uri: Microservices.questions.uri + '/' + args.stype + '/' + args.sid.split('-')[0] + '/' + 'questions?include_subdecks_and_slides=true&exam_questions_only=true' + }).then((res) => { + let questions = JSON.parse(res).map((item, index) => { + return { + id: item.id, title: item.question, difficulty: item.difficulty, relatedObject: item.related_object, relatedObjectId: item.related_object_id, relatedObjectName: item.related_object_name, isExamQuestion: item.is_exam_question, answers: item.choices .map((ans, ansIndex) => { return {answer: ans.choice, correct: ans.is_correct}; @@ -33,8 +53,10 @@ export default { callback(err, {}); }); } else if(resource === 'questions.count') { + const nonExamQuestionsOnly = (args.nonExamQuestionsOnly) ? '&non_exam_questions_only=true' : ''; + rp.get({ - uri: Microservices.questions.uri + '/' + args.stype + '/' + args.sid.split('-')[0] + '/' + 'questions?metaonly=true&include_subdecks_and_slides=true', + uri: Microservices.questions.uri + '/' + args.stype + '/' + args.sid.split('-')[0] + '/' + 'questions?metaonly=true&include_subdecks_and_slides=true' + nonExamQuestionsOnly, }).then((res) => { callback(null, {count: JSON.parse(res).count}); }).catch((err) => { @@ -42,65 +64,6 @@ export default { callback(err, {}); }); } - - /* Hard coded sample work follows */ - /* - let questions = [ - {id: 12, title: 'Super exciting question', username: 'Ilya B.', userID: 66, difficulty: 2, Date: 'yesterday', - answers: [{answer: 'Yes', correct: true, explanation: 'Obvious'}, - {answer: 'No', correct: false, explanation: ''}, - {answer: 'May be', correct: true, explanation: 'May the power comes with you!'}, - {answer: 'I do not know', correct: false, explanation: ''}]}, - {id: 23, title: 'Title for question 2', username: 'Vuk M.', userID: 7, difficulty: 1, Date: '2 hours agp', - answers: [{answer: 'Answer 1', correct: false, explanation: 'Some explanation'}, - {answer: 'Answer 2', correct: false, explanation: ''}, - {answer: 'Correct answer', correct: true, explanation: ''}]}, - {id: 40, title: 'Very difficult question', username: 'Ali K', userID: 23, difficulty: 3, Date: '23 minutes ago', - answers: [{answer: 'Meh...', correct: false, explanation: ''}, - {answer: 'Make a smart look during the answering...', correct: true, explanation: 'Be simple'}]}, - {id: 22, title: 'Extriemly hard to answer', username: 'Ali K', userID: 23, difficulty: 3, Date: 'yesterday', - answers: [{answer: 'Have no idea', correct: false, explanation: ''}, - {answer: 'The correct one', correct: true, explanation: ''}, - {answer: 'Also correct', correct: true, explanation: ''}]}, - {id: 16, title: 'The easiest one for everyone!', username: 'Ilya B.', userID: 66, difficulty: 1, Date: '2 minutes ago', - answers: [{answer: 'True', correct: true, explanation: ''}, - {answer: 'False', correct: false, explanation: ''}]}, - {id: 1, title: 'Second Super exciting question', username: 'Ilya B.', userID: 66, difficulty: 2, Date: 'yesterday', - answers: [{answer: 'Yes', correct: true, explanation: 'Obvious'}, - {answer: 'No', correct: false, explanation: ''}, - {answer: 'May be', correct: true, explanation: 'May the power comes with you!'}, - {answer: 'I do not know', correct: false, explanation: ''}]}, - {id: 2, title: 'Title for question 2. Some more stuff', username: 'Vuk M.', userID: 7, difficulty: 1, Date: '2 hours agp', - answers: [{answer: 'Answer 1', correct: false, explanation: 'Some explanation'}, - {answer: 'Answer 2', correct: false, explanation: ''}, - {answer: 'Correct answer', correct: true, explanation: ''}]}, - {id: 3, title: 'Very difficult question 2', username: 'Ali K', userID: 23, difficulty: 3, Date: '23 minutes ago', - answers: [{answer: 'Meh...', correct: false, explanation: ''}, - {answer: 'Make a smart look during the answering...', correct: true, explanation: 'Be simple'}]}, - {id: 4, title: 'Extriemly hard to answer 2', username: 'Ali K', userID: 23, difficulty: 3, Date: 'yesterday', - answers: [{answer: 'Have no idea', correct: false, explanation: ''}, - {answer: 'The correct one', correct: true, explanation: ''}, - {answer: 'Also correct', correct: true, explanation: ''}]}, - {id: 5, title: 'The easiest one for everyone! 2', username: 'Ilya B.', userID: 66, difficulty: 1, Date: '2 minutes ago', - answers: [{answer: 'True', correct: true, explanation: ''}, - {answer: 'False', correct: false, explanation: ''}]}, - {id: 6, title: 'The easiest one for everyone! 3', username: 'Ilya B.', userID: 66, difficulty: 1, Date: '2 minutes ago', - answers: [{answer: 'True', correct: true, explanation: ''}, - {answer: 'False', correct: false, explanation: ''}]} - ]; - - let length = questions.length; - let lowerBound = (args.pageNum - 1) * args.maxQ; - let upperBound = lowerBound + args.maxQ; - if (upperBound > length){ - upperBound = length; - } - - questions = questions.slice(lowerBound, upperBound); - callback(null, {questions: questions, totalLength: length, selector: selector}); - } - }, - */ }, create: (req, resource, params, body, config, callback) => { @@ -132,7 +95,8 @@ export default { difficulty: parseInt(args.question.difficulty), choices: choices, question: args.question.title, - explanation: args.question.explanation}) + explanation: args.question.explanation, + is_exam_question: args.question.isExamQuestion}) }).then((res) => { let question = JSON.parse(res); const answers = question.choices @@ -140,7 +104,7 @@ export default { return {answer: ans.choice, correct: ans.is_correct}; }); callback(null, {question: { - id: question.id, title: question.question, difficulty: question.difficulty, relatedObject: question.related_object, relatedObjectId: question.related_object_id, + id: question.id, title: question.question, difficulty: question.difficulty, relatedObject: question.related_object, relatedObjectId: question.related_object_id, isExamQuestion: question.is_exam_question, answers: answers, explanation: question.explanation, userId: question.user_id @@ -158,26 +122,26 @@ export default { log.info({Id: req.reqId, Service: __filename.split('/').pop(), Resource: resource, Operation: 'update', Method: req.method}); let args = params.params? params.params : params; - let choices = []; - let answers = [];//There is a problem with different names used on the platform and service - if (args.question.answer1 !== '') { - choices.push({'choice': args.question.answer1, 'is_correct': args.question.correct1}); - answers.push({'answer': args.question.answer1, 'correct': args.question.correct1}); - } - if (args.question.answer2 !== '') { - choices.push({'choice': args.question.answer2, 'is_correct': args.question.correct2}); - answers.push({'answer': args.question.answer2, 'correct': args.question.correct2}); - } - if (args.question.answer3 !== '') { - choices.push({'choice': args.question.answer3, 'is_correct': args.question.correct3}); - answers.push({'answer': args.question.answer3, 'correct': args.question.correct3}); - } - if (args.question.answer4 !== '') { - choices.push({'choice': args.question.answer4, 'is_correct': args.question.correct4}); - answers.push({'answer': args.question.answer4, 'correct': args.question.correct4}); - } - if (resource === 'questions.update') { + let choices = []; + let answers = [];//There is a problem with different names used on the platform and service + if (args.question.answer1 !== '') { + choices.push({'choice': args.question.answer1, 'is_correct': args.question.correct1}); + answers.push({'answer': args.question.answer1, 'correct': args.question.correct1}); + } + if (args.question.answer2 !== '') { + choices.push({'choice': args.question.answer2, 'is_correct': args.question.correct2}); + answers.push({'answer': args.question.answer2, 'correct': args.question.correct2}); + } + if (args.question.answer3 !== '') { + choices.push({'choice': args.question.answer3, 'is_correct': args.question.correct3}); + answers.push({'answer': args.question.answer3, 'correct': args.question.correct3}); + } + if (args.question.answer4 !== '') { + choices.push({'choice': args.question.answer4, 'is_correct': args.question.correct4}); + answers.push({'answer': args.question.answer4, 'correct': args.question.correct4}); + } + rp.put({ uri: Microservices.questions.uri + '/question/' + args.question.qid, body:JSON.stringify({ @@ -187,11 +151,12 @@ export default { difficulty: parseInt(args.question.difficulty), choices: choices, question: args.question.title, - explanation: args.question.explanation + explanation: args.question.explanation, + is_exam_question: args.question.isExamQuestion }) }).then((res) => { const question = { - id: args.question.qid, title: args.question.title, difficulty: parseInt(args.question.difficulty), relatedObject: args.question.relatedObject, relatedObjectId: args.question.relatedObjectId.split('-')[0], + id: args.question.qid, title: args.question.title, difficulty: parseInt(args.question.difficulty), relatedObject: args.question.relatedObject, relatedObjectId: args.question.relatedObjectId.split('-')[0], isExamQuestion: args.question.isExamQuestion, answers: answers, explanation: args.question.explanation, userId: args.question.userId.toString() @@ -201,6 +166,16 @@ export default { console.log(err); callback(err, {}); }); + } else if (resource === 'questions.updateExamList') { + rp.put({ + uri: Microservices.questions.uri + '/questions/updateExamList', + body: JSON.stringify(args.modifiedSelections) + }).then(() => { + callback(null, {}); + }).catch((err) => { + console.log(err); + callback(err, {}); + }); } }, diff --git a/stores/ContentQuestionsStore.js b/stores/ContentQuestionsStore.js index be7aa9d70..c9d3453df 100644 --- a/stores/ContentQuestionsStore.js +++ b/stores/ContentQuestionsStore.js @@ -8,23 +8,37 @@ class ContentQuestionsStore extends BaseStore { this.selector = {}; this.questionsCount = 0; this.showAddBox = false; + this.showExamList = false; + this.showCorrectExamAnswers = false; this.downloadQuestions = []; } addQuestion(payload) { this.questions.push(payload.question); this.showAddBox = false; + this.showExamList = false; this.emitChange(); } updateQuestion(payload) { - // let updatedQuestion = this.questions.find((qst) => qst.id === payload.question.id); this.question.title = payload.question.title; this.question.difficulty = payload.question.difficulty; this.question.answers = payload.question.answers; this.question.explanation = payload.question.explanation; + this.question.isExamQuestion = payload.question.isExamQuestion; this.question = null; this.emitChange(); } + updateQuestions(payload) { + payload.modifiedSelections.forEach((modifiedSelection) => { + let index = this.questions.findIndex((question) => question.id === modifiedSelection.id); + if (index > -1) { + this.questions[index].isExamQuestion = !this.questions[index].isExamQuestion; + } + }); + + this.showExamList = false; + this.emitChange(); + } deleteQuestion(payload) { let index = this.questions.findIndex((qst) => {return (qst.id === payload.questionId);}); if (index !== -1) { @@ -38,6 +52,7 @@ class ContentQuestionsStore extends BaseStore { this.question = null; this.selector = payload.selector; this.questionsCount = this.questions.length; + this.showCorrectExamAnswers = false; this.emitChange(); } loadQuestion(payload) { @@ -59,10 +74,32 @@ class ContentQuestionsStore extends BaseStore { } this.emitChange(); } + resetAnswers() { + this.showCorrectExamAnswers = false; + this.questions.forEach((question) => { + const answers = question.answers; + for(let answer of answers) { + delete answer.selectedAnswer; + } + }); + } invertAddBoxFlag() { this.showAddBox = !this.showAddBox; this.emitChange(); } + + invertExamListFlag() { + this.showExamList = !this.showExamList; + this.emitChange(); + } + updateSelectedAnswer(payload) { + this.questions[payload.questionIndex].answers[payload.answerIndex].selectedAnswer = payload.selected; + } + displayCorrectExamAnswers(payload) { + this.showCorrectExamAnswers = true; + this.emitChange(); + } + updateDownloadQuestions(payload){ if((payload.downloadQuestions===[])||(typeof payload.downloadQuestions === 'undefined')){ this.downloadQuestions = []; @@ -78,6 +115,8 @@ class ContentQuestionsStore extends BaseStore { selector: this.selector, questionsCount: this.questionsCount, showAddBox: this.showAddBox, + showExamList: this.showExamList, + showCorrectExamAnswers: this.showCorrectExamAnswers, downloadQuestions: this.downloadQuestions, }; } @@ -90,6 +129,8 @@ class ContentQuestionsStore extends BaseStore { this.selector = state.selector; this.questionsCount = state.questionsCount; this.showAddBox = state.showAddBox; + this.showExamList = state.showExamList; + this.showCorrectExamAnswers = state.showCorrectExamAnswers; this.downloadQuestions = state.downloadQuestions; } } @@ -100,11 +141,16 @@ ContentQuestionsStore.handlers = { 'LOAD_QUESTION': 'loadQuestion', 'CANCEL_QUESTION': 'cancelQuestion', 'TOGGLE_ANSWERS': 'toggleAnswers', + 'RESET_ANSWERS': 'resetAnswers', 'UPDATE_QUESTION': 'updateQuestion', + 'UPDATE_QUESTIONS': 'updateQuestions', 'ADD_QUESTION': 'addQuestion', 'DELETE_QUESTION': 'deleteQuestion', 'INVERT_ADD_QUESTION_BOX_FLAG': 'invertAddBoxFlag', - 'UPDATE_DOWNLOAD_QUESTIONS': 'updateDownloadQuestions' + 'INVERT_EXAM_LIST_FLAG': 'invertExamListFlag', + 'QUESTION_ANSWER_SELECTED': 'updateSelectedAnswer', + 'SHOW_CORRECT_EXAM_ANSWERS': 'displayCorrectExamAnswers', + 'UPDATE_DOWNLOAD_QUESTIONS': 'updateDownloadQuestions', }; export default ContentQuestionsStore;