/**
* @category Backend API
* @subcategory Models
*
* @module CourseModel
*
* @description This module contains the models for courses,
* such as the course model, the course section model, the exercise model, and the question model.
*
* @requires mongoose
*/
const mongoose = require("mongoose")
const { translateDoc, translateArray } = require("../utils/crowdin")
const Schema = mongoose.Schema
const options = {
timestampsa: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
async function translate_document (doc) {
console.log(doc)
const translated_doc = await translateDoc(doc)
return await doc.updateOne(translated_doc)
}
/**
* @typedef {Object} questionSchema
*
* @description This schema is used to store questions for exercises.
*
* @property {String} type - The type of the document, "question"
* @property {ObjectId} exercise - The exercise to which the question belongs
* @property {String} question - The question
* @property {String} correct_option - The correct option
* @property {Array} options - The options for the question
*
* @see {@link module:CourseModel~exerciseSchema Exercise}
*/
/**
* @typedef {Object} exerciseSchema
*
* @description This schema is used to store exercises.
*
* @property {String} type - The type of the document, "exercise"
* @property {String} title - The title of the exercise
* @property {String} description - The description of the exercise
* @property {Number} duration - The duration of the exercise
* @property {Date} date - The date the exercise was created
*
* @property {ObjectId} course - The course to which the exercise belongs
* @property {ObjectId} course_section - The course section to which the exercise belongs
* @property {Number} order - The order of the exercise in the course section
*
* @property {MongooseVirtualType[]} questions - The questions for the exercise
*
* @see {@link module:CourseModel~courseSectionSchema CourseSection}
* @see {@link module:CourseModel~courseSchema Course}
* @see {@link module:CourseModel~questionSchema Question}
* */
/**
* @typedef {Object} videoSchema
*
* @description This schema is used to store videos.
*
* @property {String} type - The type of the document, "video"
* @property {String} title - The title of the video
* @property {String} description - The description of the video
* @property {String} video_url - The url of the video
* @property {Number} duration - The duration of the video
* @property {ObjectId} course - The course to which the video belongs
* @property {ObjectId} course_section - The course section to which the video belongs
* @property {Number} order - The order of the video in the course section
* @property {String} category - The category of the video (e.g. "Programming",
* "Mathematics", "Physics", "Chemistry")
* @property {Boolean} isAvailable - Whether the video is available to the user
*
* @see {@link module:CourseModel~courseSectionSchema CourseSection}
* @see {@link module:CourseModel~courseSchema Course}
*/
/**
* @typedef {Object} textmaterialSchema
*
* @description This schema is used to store text materials. text materials
* are documents such as pdfs, word documents, etc. that are used within the course
*
* @property {String} type - The type of the document, "text_material"
* @property {String} title - The title of the text material
* @property {String} file_url - The url of the text material
* @property {ObjectId} course - The course to which the text material belongs
* @property {ObjectId} course_section - The course section to which the text material belongs
* @property {Number} order - The order of the text material in the course section
* @property {Boolean} isAvailable - Whether the text material is available to the user
*
* @see {@link module:CourseModel~courseSectionSchema CourseSection}
* @see {@link module:CourseModel~courseSchema Course}
*/
/**
* @typedef {Object} downloadableResourceSchema
*
* @description This schema is used to store downloadable resources.
* These are resources that can be downloaded by the user. they are usually
* links to files such as pdfs, word documents, etc.
*
* @property {String} type - The type of the document, "downloadable_resource"
* @property {String} title - The title of the downloadable resource
* @property {String} file_url - The url of the downloadable resource
* @property {ObjectId} course - The course to which the downloadable resource belongs
* @property {Number} order - The order of the downloadable resource in the course section
* @property {Boolean} isAvailable - Whether the downloadable resource is available to the user
*
* @see {@link module:CourseModel~courseSchema Course}
* */
/**
* @typedef {Object} courseSectionSchema
*
* @description This schema is used to store course sections.
*
* @property {String} type - The type of the document, "course_section"
* @property {String} title - The title of the course section
* @property {Number} order - The order of the course section in the course
* @property {ObjectId} course - The course to which the course section belongs
* @property {MongooseVirtualType[]} contents - The contents of the course section
* @property {Boolean} isAvailable - Whether the course section is available to the user
* @property {MongooseVirtualType[]} exercises - The exercises in the course section
* @property {MongooseVirtualType[]} videos - The videos in the course section
* @property {MongooseVirtualType[]} textmaterials - The text materials in the course section
*
* <br>
*
* <b>NOTE:</b> The contents of the course section are stored as an array of virtuals.
* This is because the contents can be either an exercises, videos or text materials
*
* @see {@link module:CourseModel~exerciseSchema Exercise}
* @see {@link module:CourseModel~videoSchema Video}
* @see {@link module:CourseModel~textmaterialSchema TextMaterial}
* */
/**
* @typedef {Object} courseSchema
*
* @description This schema is used to store courses.
*
* @property {String} title - The title of the course
* @property {String} description - The description of the course
* @property {String} author - The author of the course
* @property {MongooseVirtualType[]} course_sections - The course sections in the course
* @property {MongooseVirtualType[]} exercises - The exercises in the course
* @property {MongooseVirtualType[]} videos - The videos in the course
* @property {MongooseVirtualType[]} textmaterials - The text materials in the course
*
* @see {@link module:CourseModel~courseSectionSchema CourseSection}
* @see {@link module:CourseModel~exerciseSchema Exercise}
* @see {@link module:CourseModel~videoSchema Video}
* @see {@link module:CourseModel~textmaterialSchema TextMaterial}
*/
/**
* @typedef {Object} submissionSchema
*
* @description This schema is used to store the record of all the submissions
* for a particular exercise made by a particular user.
*
* @property {ObjectId} user - The user who made the submission
* @property {ObjectId} exercise - The exercise for which the submission was made
* @property {Number} score - The score of the submission
* @property {Object} submission - The submission
* @property {String} submission.question - The question
* @property {String} submission.correct_option - The correct option
* @property {String} submission.submitted_option - The option selected by the user
*
* @property {Date} date - The date the submission was made
*
* @see {@link module:CourseModel~exerciseSchema Exercise}
* @see {@link module:UserModel~userSchema User}
*/
/**
* @typedef {Object} courseReportSchema
*
* @description This schema is used to track the progress of a user in a course.
*
* @property {ObjectId} user - The user
* @property {ObjectId} course - The course
* @property {ObjectId[]} completed_exercises - The exercises completed by the user
* @property {ObjectId[]} completed_videos - The videos completed by the user
* @property {ObjectId[]} completed_course_sections - The course sections completed by the user
* @property {Boolean} isCompleted - Whether the user has completed the course
*
* @see {@link module:CourseModel~exerciseSchema Exercise}
* @see {@link module:CourseModel~videoSchema Video}
* @see {@link module:CourseModel~courseSectionSchema CourseSection}
* @see {@link module:CourseModel~courseSchema Course}
* @see {@link module:UserModel~userSchema User}
*/
/**
* @type {questionSchema}
*/
const questionSchema = new Schema({
// Assuming questions are in quiz format
exercise: { type: Schema.Types.ObjectId, ref: 'Exercise', required: true },
question: {
type: String,
required: true
},
correct_option: {
type: String,
required: true,
},
options: [{ type: String }],
question_tr: {
type: String,
},
correct_option_tr: {
type: String,
},
options_tr: [{ type: String }],
type: { type: String, default: "question"},
}, options)
questionSchema.post('save', translate_document)
// questionSchema.post('update', translate_document)
/**
* @type {exerciseSchema}
*/
const exerciseSchema = new Schema({
type: { type: String, default: "exercise", minLength: 3, maxLength: 40 },
title: { type: String, required: true },
description: { type: String, required: true },
// questions: [{ type: Schema.Types.ObjectId, ref: "Question" }],
duration: { type: Number, required: true },
date: { type: Date, default: Date.now() },
course: { type: Schema.Types.ObjectId, ref: 'Course', required: true },
course_section: { type: Schema.Types.ObjectId, ref: 'CourseSection', required: true },
order: { type: Number, default: Date.now() },
title_tr: { type: String },
description_tr: { type: String },
}, options)
exerciseSchema.post('save', translate_document)
// exerciseSchema.post('update', translate_document)
exerciseSchema.virtual('questions', {
localField: '_id',
foreignField: 'exercise',
ref: 'Question'
})
/**
* @type {videoSchema}
* */
const videoSchema = new Schema({
type: { type: String, default: 'video' },
author: {
type: String,
required: true
},
title: {
type: String,
required: true,
minLength: 3, maxLength: 40
},
video_url: { type: String, required: true },
description: { type: String, required: true },
duration: { type: String, required: true },
course: { type: Schema.Types.ObjectId, ref: "Course", required: true },
course_section: { type: Schema.Types.ObjectId, ref: 'CourseSection', required: true },
category: {
type: String,
required: true
},
order: { type: Number, default: Date.now() },
isAvailable: { type: Boolean, default: true },
title_tr: {
type: String,
minLength: 3, maxLength: 40
},
description_tr: { type: String },
}, options)
videoSchema.post('save', translate_document)
// videoSchema.post('update', translate_document)
videoSchema.virtual('downloadable_resources', {
localField: '_id',
foreignField: 'video',
ref: 'DownloadableResource'
})
/**
* @type {textmaterialSchema}
* */
const textmaterialSchema = new Schema({
type: { type: String, default: "slide", minLength: 3, maxLength: 40 },
title: {
type: String,
required: true
},
description: { type: String, required: true },
file_url: {
type: String,
required: true
},
course: { type: Schema.Types.ObjectId, ref: "Course", required: true },
course_section: { type: Schema.Types.ObjectId, ref: 'CourseSection', required: true },
order: { type: Number, default: Date.now() },
isAvailable: { type: Boolean, default: true },
title_tr: {
type: String,
},
description_tr: { type: String },
}, options)
textmaterialSchema.virtual('downloadable_resources', {
localField: '_id',
foreignField: 'textmaterial',
ref: 'DownloadableResource'
})
textmaterialSchema.post('save', translate_document)
// textmaterialSchema.post('update', translate_document)
/**
* @type {downloadableResourceSchema}
*/
const downloadableResourceSchema = new Schema({
resource_type: { type: String, required: true },
file_url: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
course: { type: Schema.Types.ObjectId, ref: 'Course', required: true },
video: { type: Schema.Types.ObjectId, ref: 'Video' },
textmaterial: { type: Schema.Types.ObjectId, ref: 'TextMaterial' },
order: { type: Number, default: Date.now() },
isAvailable: { type: Boolean, default: true },
title_tr: { type: String },
description_tr: { type: String },
}, options)
downloadableResourceSchema.post('save', translate_document)
// downloadableResourceSchema.post('update', translate_document)
/**
* @type {courseSectionSchema}
* */
const courseSectionSchema = new Schema({
title: { type: String, required: true, minLength: 3, maxLength: 40 },
title_tr: { type: String, minLength: 3, maxLength: 40 },
course: { type: Schema.Types.ObjectId, ref: 'Course', required: true },
deleted: { type: Schema.Types.ObjectId, ref: 'Course' },
order: { type: Number, default: Date.now() },
}, options)
courseSectionSchema.post('save', translate_document)
// courseSectionSchema.post('update', translate_document)
courseSectionSchema.virtual('videos', {
localField: '_id',
foreignField: 'course_section',
ref: 'Video',
justOne: false
})
courseSectionSchema.virtual('exercises', {
localField: '_id',
foreignField: 'course_section',
ref: 'Exercise',
justOne: false
})
courseSectionSchema.virtual('textmaterials', {
localField: '_id',
foreignField: 'course_section',
ref: 'TextMaterial',
justOne: false
})
/**
* @description Combines the contents of the course section into one array
* and sorts them by their order
*
* @param {courseSectionSchema} courseSection
* @returns {courseSectionSchema}
*/
function combineContents(courseSection) {
courseSection.contents = [
...courseSection.videos ?? [],
...courseSection.exercises ?? [],
...courseSection.textmaterials ?? []
]
courseSection.contents.sort((a, b) => {
return a.order - b.order
}
)
return courseSection
}
courseSectionSchema.post('find', async function (courseSections) {
for (let courseSection of courseSections) {
await combineContents(courseSection)
}
})
courseSectionSchema.post('findOne', async function (courseSection) {
const doc = await combineContents(courseSection)
return doc.toObject()
})
/**
* @type {courseSchema}
*/
const courseSchema = new Schema({
author: {
type: String,
required: true
},
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
title_tr: {
type: String,
},
description_tr: {
type: String,
},
videos: [{ type: Schema.Types.ObjectId, ref: "Video" }],
enrolled_users: [{ type: Schema.Types.ObjectId, ref: "User" }],
preview_image: { type: String, required: true },
isAvailable: { type: Boolean, default: true }
}, options)
courseSchema.post('save', translate_document)
// courseSchema.pre('validate', translate_document)
courseSchema.virtual('exercises', {
localField: '_id',
foreignField: 'course',
ref: 'Exercise'
})
courseSchema.virtual('course_sections', {
localField: '_id',
foreignField: 'course',
ref: 'CourseSection'
})
/**
* @type {submissionSchema}
* */
const exerciseSubmissionSchema = new Schema({
exercise: { type: Schema.Types.ObjectId, ref: 'Exercise', required: true },
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
submission: [{
type: new Schema({
question: { type: Schema.Types.ObjectId, ref: 'Question', required: true },
submitted_option: { type: String },
correct_option: { type: String }
})
}],
score: { type: Number, default: 0 },
percentage_passed: { type: Number, default: 0 },
report: { type: Schema.Types.ObjectId, ref: "ExerciseReport", required: true }
}, options)
exerciseSubmissionSchema.pre('save', async function (next) {
// check if score was updated
if (this.isModified('score')) {
// get the exercise
const { exercise } = await this.populate({
path: 'exercise',
populate: 'questions'
})
this.percentage_passed = (this.score / exercise.questions.length) * 100
}
next()
})
const exerciseReportSchema = new Schema({
exercise: { type: Schema.Types.ObjectId, ref: 'ExerciseReport', required: true },
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
course_report: { type: Schema.Types.ObjectId, ref: 'CourseReport', required: true },
best_score: { type: Number, default: 0 },
percentage_passed: { type: Number, default: 0 },
})
exerciseReportSchema.virtual('submissions', {
localField: '_id',
foreignField: 'report',
ref: "ExerciseSubmission",
justOne: false
})
exerciseReportSchema.pre('save', async function (next) {
// check if score was updated
if (this.isModified('best_score')) {
// get the exercise
const exercise = await Exercise.findById(this.exercise).populate('questions')
this.percentage_passed = (this.best_score / exercise.questions.length) * 100
}
next()
})
/**
* @type {courseReportSchema}
*/
const courseReportSchema = new Schema(
{
course: { type: Schema.Types.ObjectId, ref: "Course", required: true },
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
percentage_passed: { type: Number, default: 0 },
isCompleted: { type: Boolean, default: false },
},
options
);
courseReportSchema.virtual('certificate', {
localField: '_id',
foreignField: 'course_report',
ref: 'Certificate',
justOne: true
})
courseReportSchema.virtual('attempted_exercises', {
localField: '_id',
foreignField: 'course_report',
ref: 'ExerciseReport',
justOne: false
})
courseReportSchema.methods.updateBestScore = async function () {
const doc = (await this.populate([
{
path: 'attempted_exercises',
},
{
path: 'course',
populate: 'exercises'
}
])).toObject();
const attempted_exercises = doc.attempted_exercises;
const required_exercises = doc.course.exercises;
let total_scores = 0
for (let i = 0; i < attempted_exercises.length; i++) {
total_scores += attempted_exercises[i].percentage_passed
}
// Calculate the average percentage passed
this.percentage_passed = total_scores / required_exercises.length
// Update the isCompleted field if the percentage passed is greater than or equal to 80
this.isCompleted = this.percentage_passed >= 80 ? true : false;
return this.save();
}
const Question = mongoose.model("Question", questionSchema);
const Exercise = mongoose.model("Exercise", exerciseSchema);
const Video = mongoose.model("Video", videoSchema);
const TextMaterial = mongoose.model("TextMaterial", textmaterialSchema);
const DownloadableResource = mongoose.model('DownloadableResource', downloadableResourceSchema)
const Course = mongoose.model("Course", courseSchema);
const CourseSection = mongoose.model("CourseSection", courseSectionSchema);
const ExerciseSubmission = mongoose.model(
"ExerciseSubmission",
exerciseSubmissionSchema
);
const ExerciseReport = mongoose.model('ExerciseReport', exerciseReportSchema)
const CourseReport = mongoose.model("CourseReport", courseReportSchema);
module.exports = {
Video, Course,
CourseSection,
Question, Exercise,
CourseReport, TextMaterial,
ExerciseSubmission, ExerciseReport,
DownloadableResource
};
Source