Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/quiz module #142

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5c935a1
Quiz feature - prepared code for initial review
zeala Dec 14, 2015
ab12970
adding a simple schema to quiz - started
zeala Dec 24, 2015
5c267a7
questions schema
zeala Dec 24, 2015
e17f450
options field schema - fields set to optional
zeala Dec 24, 2015
2b36aa7
fixed typo
zeala Dec 24, 2015
42940bc
created subschemas
zeala Dec 24, 2015
ee82cbf
true/false template
zeala Dec 26, 2015
3dfe77c
work in progress
zeala Dec 26, 2015
34d6748
work in progress
zeala Dec 26, 2015
dbfd93a
quick form experiment
zeala Dec 26, 2015
de75f0d
autoform - true/false question
zeala Dec 29, 2015
c1d82fd
added hooks for question insert
zeala Dec 29, 2015
febfb5f
options for true/false questions
zeala Dec 29, 2015
867f44d
true false schema
zeala Dec 30, 2015
e0bba16
added list of all options to the schema
zeala Dec 30, 2015
bec302f
autoform work for quiz
zeala Jan 3, 2016
86f2f51
single answer question
zeala Jan 3, 2016
e1e1953
form inputs for multiple choice - multiple answers question
zeala Jan 4, 2016
2660316
added trueOrFalse and multiple answers types, code cleanup
zeala Jan 5, 2016
5b5a450
quiz questions layout
zeala Jan 5, 2016
ccd87f8
autoform - question types - work in progress
zeala Jan 5, 2016
7f02fcc
code cleanup
zeala Jan 5, 2016
44674f1
reverted quiz schema
zeala Jan 5, 2016
361f2ee
clear question on 'cancel' move question generator code
zeala Jan 5, 2016
62e3ffb
added an option to delete questions from quiz
zeala Jan 6, 2016
006058d
removed unused partials and js
zeala Jan 7, 2016
39270a7
removed debug statements
zeala Jan 7, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions both/collections/_schemas.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Schema = {};
QuizzesSchema = {};
91 changes: 91 additions & 0 deletions both/collections/questions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
Questions = new Mongo.Collection('questions');

QuestionSchema = new SimpleSchema({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attach this schema to the Questions object:

Questions.schema = new SimpleSchema({ ... });

id: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary, as all MongoDB documents have an _id property.

type: String,
optional: false,
unique: true
},
questionType: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should probably have some allowedValues so that it is rendered in a select element. E.g. we only want to allow question types that we support, such as "multiple choice".

type:String,
optional: false
},
quizId: {
type:String,
optional: false
},
title: {
type:String,
optional: false
},
description : {
type:String,
optional: false
},
numberOfOptions: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider re-organizing your options schema as follows:

E.g.

"options": {
  type: [Object],
  max: 8,
},
"options.$.title": {
  type: String
},
"options.$.isCorrectAnswer": {
  type: Boolean
}


type: Number,
optional: true,
max: 10,
min: 2
},
optionTitles: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this field, with the above refactoring.

label: "Options",
type: [Object],
optional: false,
autoform: {
type: "radio-with-text-input",
options: function(){
var optionsArray = [];

for ( var i=0; i < 8; i++){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this loop hard-coded to 8? You can use the max property of SinpleSchema arrays to set an upper limit.

var option = Quiz.generateAnswerOption("", false, i)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this line do. Please add comment in plain English.

optionsArray.push(option);
}
return optionsArray;
}
},
custom: function() {
// This custom function renders an error, if this field is not equal to
// the new Password field supplied in the form.
var titlesValid = true;
var selectionFound = false;
for (var i = 0; i < this.value.length; i++){
var obj = this.value[i];
if (!obj.title){
titlesValid = false;
return "invalidQuestionTitles";
}
if (obj.isSelected){
selectionFound = true;
}
}
if (!selectionFound){
return "invalidQuestionSelection";
}
}
},

"optionTitles.$.title": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is unnecessary, if you refactor the "options" object.

E.g.

"options.$.title": {
  type: String
},

type:String,
optional: false
},

"optionTitles.$.isSelected": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is unnecessary if you simplify the options field. E.g.

"options.$.isCorrectAnswer": {
  type: Boolean
}

type:Boolean,
optional: false
},
"optionTitles.$.index":{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field may be unnecessary, since JavaScript arrays are natively indexed.

type:Number,
optional: false
},


options:{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please refactor this object as indicated:

"options": {
  type: [Object],
  max: 8,
},
"options.$.title": {
  type: String
},
"options.$.isCorrectAnswer": {
  type: Boolean
}

label: "Options",
type: [Object],
}
})


Questions.attachSchema(QuestionSchema);
35 changes: 35 additions & 0 deletions both/collections/quizzes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Quizzes = new Mongo.Collection('quizzes');

QuizzesSchema.AnswerOptionSchema = new SimpleSchema({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be redundant, as individual options may be marked as correctAnswer directly.

title: {
type:String,
optional: true,
defaultValue: ""
},
isCorrect: {
type: Boolean,
defaultValue: false
},
});

//This schema will validate the initial creation of a quiz
QuizzesSchema.QuizzesSchema = new SimpleSchema({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define this schema directly in the Quizzes object:

Quizzes.schema = new SimpleSchema({ 
  ... 
});

title: {
type:String,
label: "Quiz Title",
min: 4,
max: 140
},
questions: {
type: [Object],
optional: true
},

lessonID: {
type:String,
},
});

Quizzes.attachSchema(QuizzesSchema.QuizzesSchema);


5 changes: 4 additions & 1 deletion both/common.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
SimpleSchema.messages({
"passwordMismatch": "Passwords do not match"
"passwordMismatch": "Passwords do not match",
"invalidQuestionSettings": "Invalid question settings",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to remember to internationalize these strings, so they can be translated.

"invalidQuestionTitles": "All options should be filled in",
"invalidQuestionSelection": "Please select an answer"
});
197 changes: 197 additions & 0 deletions both/quizModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
Quiz = function(){
var quiz = this;
this.addNewQuestion = function(val){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type of functionality is handled in a simple manner by embracing AutoForm.

//check if the question already exists
if (quiz.questions == undefined){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use === comparison here.

quiz.questions = [];
}
for (var q in quiz.questions){

};

quiz.questions.push(val);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this in to the for block above.

};

};

Quiz.convertToQuizObject = function(object){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoForm and SimpleSchema handle this automatically.


if (object == undefined) return;
var quiz = new Quiz();

quiz._id = object._id;
quiz.title = object.title;
quiz.lessonID = object.lessonID;
quiz.questions = object.questions;
quiz.userAttempts = object.userAttempts;

return quiz;
};

Quiz.generateQuestion = function(questionType, quizId ){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoForm and SimpleSchema handle this automatically.

var question = new Object();
question.quizId = quizId;
question.id = Random.id(); //assign an id to the question - need this for checking and validating answers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MongoDB automatically assigns an _id property to each document.

question.questionType = questionType;

//if the question type is true-or-false, populate the answer options
if (question.questionType == QuizOptions.TRUE_OR_FALSE){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use SimpleSchema to dynamically define what field to display, based on the question.type field.

question.optionTitles = [];
var trueOption = Quiz.generateAnswerOption("True", false, 0);

question.optionTitles.push(trueOption);
var falseOption = Quiz.generateAnswerOption("False", false, 1);
question.optionTitles.push(falseOption);
};

question.description = "";
question.title = "";
question.options = [];
return question;
};

Quiz.generateAnswerOption = function (title, isSelected, index){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleSchema and AutoForm handle this simply and automatically.

var option = {};
option.title = title;
option.isSelected = isSelected;
option.index = index;

return option
}


Object.defineProperty(Quiz, "_title", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleSchema handles this automatically. Then, we use plain JavaScript to access object properties.

get: function title(){
return this._title;
},
set: function title(val){
this._title = val;
}
});
Object.defineProperty(Quiz, "_questions", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleSchema handles this automatically. Then, we use plain JavaScript to access object properties.

get: function question(){
return this._questions;
},
set: function questions(val){
this._questions = val;
}
});

Object.defineProperty(Quiz, "_lessonID", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleSchema handles this automatically. Then, we use plain JavaScript to access object properties.

get: function lessonID(){
return this._lessonIDs
},
set: function lessonID(val){
this._lessonID = val;
}
});

Object.defineProperty(Quiz, "_userAttempts", { //an array of objects containig user ids, date and result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move userAttempts into a separate collection. E.g.

const UserAttempts = new Mongo.collection("userAttempts");

UserAttempts.schema = new SimpleSchema({
  userId: {
    type: String,
    regEx: SimpleSchema.RegEx.Id
  },
  quizId: {
    type: String,
    regEx: SimpleSchema.RegEx.Id
  },
  userAttempts: {
    type: [Object] // Hold an array of all user attempts
  },
  "userAttempts.$.successful": {
    type: Boolean // true if user passes, false if user fails
  }
});

get: function userAttempts(){
return this._userAttempts;
},
set: function userAttempts(val){
this._userAttempts = val;
}
})




QuizOptions = {};

QuizOptions.MULTIPLE_CHOICE_SINGLE_ANSWER = "Multiple Choice - single answer";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this can probably be done at the schema level.

QuizOptions.MULTIPLE_CHOICE_MULTIPLE_ANSWERS = "Multiple Choice - multiple answers";
QuizOptions.TRUE_OR_FALSE = "True or False";
QuizOptions.QUESTION_TYPES =
[ QuizOptions.MULTIPLE_CHOICE_SINGLE_ANSWER,
QuizOptions.MULTIPLE_CHOICE_MULTIPLE_ANSWERS,
QuizOptions.TRUE_OR_FALSE
];



QuizQuestion = function() {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleSchema provides the structure for Questions, while JavaScript provides simple access to the object properties without the need for getters or setters.

Object.defineProperty(QuizQuestion, "_questionType", {
get: function questionType() {
return this._quiztype;
},
set: function questionType(val) {
this._quiztype = val;
}
});

//array of lesson IDs - the quiz can be used in more than one lesson
Object.defineProperty(QuizQuestion, "_quizId", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by SimpleSchema.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quizModel class was left in the code by mistake (it was used in the previous implementation - prior to the SimpleSchema and autoform integration).

I'll remove the file.

get: function quizId(){
return this._quizId
},
set: function quizId(val){
this._lessonIDs = val;
}
});

Object.defineProperty(QuizQuestion, "_title", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by SimpleSchema. JavaScript properties are accessible on returned MongoDB document objects.

get: function title(){
return this._title;
},
set: function title(val){
this._title = val;
}
});

Object.defineProperty(QuizQuestion, "_description", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by SimpleSchema. JavaScript properties are accessible on returned MongoDB document objects.

get: function description(){
return this._description;
},
set: function description(val){
this._description = val;
}
});

Object.defineProperty(QuizQuestion, "_options", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by SimpleSchema. JavaScript properties are accessible on returned MongoDB document objects.

get: function options(){
return this._options;
},
set: function options(val){
this._questions = val;
}
});

Object.defineProperty(QuizQuestion, "_saved",{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by SimpleSchema. JavaScript properties are accessible on returned MongoDB document objects.

get: function saved(){
return this._saved;
},
set: function saved(val){
this._saved = val;
}
});

Object.defineProperty(QuizQuestion, "_answered", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by SimpleSchema. JavaScript properties are accessible on returned MongoDB document objects.

get: function answered(){
return this._answered;
},
set: function answered(val){
this._answered = val;
}
})

Object.defineProperty(QuizQuestion, "_isMultipleAnswer", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by SimpleSchema. JavaScript properties are accessible on returned MongoDB document objects.

get: function isMultipleAnswer(){
return this._questionType == QuizOptions.MULTIPLE_CHOICE_MULTIPLE_ANSWERS;
}
});

Object.defineProperty(QuizQuestion, "_isSingleAnswer", {
get: function isSingleAnswer(){
return this._questionType == QuizOptions.MULTIPLE_CHOICE_SINGLE_ANSWER;
}
});

Object.defineProperty(QuizQuestion, "_isTrueOrFalse", {
get: function isTrueOrfalse(){
return this._questionType == QuizOptions.TRUE_OR_FALSE;
}
})


17 changes: 17 additions & 0 deletions client/helpers/editMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,20 @@ Template.registerHelper('editMode', function () {
// get edit mode session variable
return Session.get('editMode');
});

Template.registerHelper('isEditingCurrentCourse', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea to move this to a general Template helper, since it was duplicated.

// Get reference to current router
var router = Router.current();

// Get Course ID from router
var currentCourseId = router.params._id;

// Get value of editing course session variable
var editingCourseId = Session.get('editingCourseId')

// See if user is editing current course
var editingCurrentCourse = (editingCourseId === currentCourseId);

// return true if user is editing this course
return editingCurrentCourse;
});
7 changes: 6 additions & 1 deletion client/templates/course/course.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Template.course.created = function () {

// Set the empty active lesson ID variable
activeLessonID = new ReactiveVar(undefined);

//Set an ampty active quiz Id var
activeQuizID = new ReactiveVar(undefined);

};

Template.course.helpers({
Expand All @@ -24,5 +28,6 @@ Template.course.helpers({
var course = Courses.findOne(instance.courseId);

return course;
}
},

});
Loading