import { Meteor } from 'meteor/meteor';
import _ from 'lodash';
import SimpleSchema from 'simpl-schema';
import { CareerGoals } from '../career/CareerGoalCollection';
import { Internships } from '../internship/InternshipCollection';
import { Opportunities } from '../opportunity/OpportunityCollection';
import { Reviews } from '../review/ReviewCollection';
import { Slugs } from '../slug/SlugCollection';
import { Interests } from '../interest/InterestCollection';
import { Teasers } from '../teaser/TeaserCollection';
import { ProfileCourses } from '../user/profile-entries/ProfileCourseCollection';
import { CourseInstances } from './CourseInstanceCollection';
import BaseSlugCollection from '../base/BaseSlugCollection';
import { CareerGoal, Course, CourseDefine, CourseUpdate, Internship, Opportunity } from '../../typings/radgrad';
import { validateCourseSlugFormat } from './CourseUtilities';
/**
* Represents a specific course, such as "ICS 311".
* To represent a specific course for a specific academicTerm, use CourseInstance.
* @memberOf api/course
* @extends api/base~BaseSlugCollection
*/
class CourseCollection extends BaseSlugCollection {
public unInterestingSlug: string;
/**
* Creates the Course collection.
*/
constructor() {
super('Course', new SimpleSchema({
name: { type: String },
shortName: { type: String },
slugID: { type: SimpleSchema.RegEx.Id },
num: { type: String },
description: { type: String },
creditHrs: { type: Number },
interestIDs: [SimpleSchema.RegEx.Id],
// Optional data
syllabus: { type: String, optional: true },
repeatable: { type: Boolean, optional: true },
retired: { type: Boolean, optional: true },
picture: { type: String, optional: true, defaultValue: 'images/header-panel/header-courses.png' },
}));
this.defineSchema = new SimpleSchema({
name: String,
shortName: { type: String, optional: true },
slug: String,
description: String,
creditHrs: { type: SimpleSchema.Integer, optional: true },
interests: [String],
syllabus: String,
repeatable: { type: Boolean, optional: true },
retired: { type: Boolean, optional: true },
picture: { type: String, optional: true },
});
this.updateSchema = new SimpleSchema({
name: { type: String, optional: true },
shortName: { type: String, optional: true },
description: { type: String, optional: true },
creditHrs: { type: SimpleSchema.Integer, optional: true },
interests: { type: Array, optional: true },
'interests.$': String,
syllabus: { type: String, optional: true },
repeatable: { type: Boolean, optional: true },
retired: { type: Boolean, optional: true },
picture: { type: String, optional: true },
});
this.unInterestingSlug = 'other';
}
/**
* Defines a new Course.
* @example
* Courses.define({ name: 'Introduction to the theory and practice of scripting',
* shortName: 'Intro to Scripting',
* slug: 'ics_215',
* num: 'ICS 215',
* description: 'Introduction to scripting languages for the integration of applications.',
* creditHrs: 4,
* interests: ['perl', 'javascript', 'ruby'],
* syllabus: 'http://courses.ics.hawaii.edu/syllabuses/ICS215.html',
* });
* @param { Object } description Object with keys name, shortName, slug, num, description, creditHrs,
* interests, syllabus.
* @param name is the official course name.
* @param shortName is an optional abbreviation. Defaults to name.
* @param slug must not be previously defined.
* @param num the course number.
* @param creditHrs is optional and defaults to 3. If supplied, must be a num between 1 and 15.
* @param interests is a (possibly empty) array of defined interest slugs or interestIDs.
* @param syllabus is optional. If supplied, should be a URL.
* @param repeatable is optional, defaults to false.
* @param retired is optional, defaults to false.
* @throws {Meteor.Error} If the definition includes a defined slug or undefined interest or invalid creditHrs.
* @returns The newly created docID.
*/
public define({ name, shortName = name, slug, num, description, creditHrs = 3, interests = [], syllabus, picture, retired = false, repeatable = false }: CourseDefine) {
// Make sure the slug has the right format <dept>_<number>
validateCourseSlugFormat(slug);
// check if slug is defined
if (Slugs.isSlugForEntity(slug, this.getType())) {
// console.log(`${slug} is already defined for ${this.getType()}`);
return Slugs.getEntityID(slug, this.getType());
}
// console.log(`Defining slug ${slug}`);
// Get SlugID, throw error if found.
const slugID = Slugs.define({ name: slug, entityName: this.getType() });
// Get Interests, throw error if any of them are not found.
const interestIDs = Interests.getIDs(interests);
// Make sure creditHrs is a num between 1 and 15.
if (!(typeof creditHrs === 'number') || (creditHrs < 1) || (creditHrs > 15)) {
throw new Meteor.Error(`CreditHrs ${creditHrs} is not a number between 1 and 15.`);
}
const courseID =
this.collection.insert({
name,
shortName,
slugID,
num,
description,
creditHrs,
interestIDs,
syllabus,
repeatable,
retired,
picture,
});
// Connect the Slug to this Interest
Slugs.updateEntityID(slugID, courseID);
return courseID;
}
/**
* Update a Course.
* @param instance The docID (or slug) associated with this course.
* @param name optional
* @param shortName optional
* @param num optional
* @param description optional
* @param creditHrs optional
* @param interests An array of interestIDs or slugs (optional)
* @param syllabus optional
* @param repeatable optional boolean.
* @param retired optional boolean.
*/
public update(instance: string, { name, shortName, num, description, creditHrs, interests, picture, syllabus, retired, repeatable }: CourseUpdate) {
const docID = this.getID(instance);
const updateData: {
name?: string;
description?: string;
interestIDs?: string[];
shortName?: string;
num?: string;
creditHrs?: number;
syllabus?: string;
repeatable?: boolean;
retired?: boolean;
picture?: string;
} = {};
if (name) {
updateData.name = name;
}
if (description) {
updateData.description = description;
}
if (picture) {
updateData.picture = picture;
}
if (interests) {
const interestIDs = Interests.getIDs(interests);
updateData.interestIDs = interestIDs;
}
if (shortName) {
updateData.shortName = shortName;
}
if (num) {
updateData.num = num;
}
if (creditHrs) {
updateData.creditHrs = creditHrs;
}
if (syllabus) {
updateData.syllabus = syllabus;
}
if (_.isBoolean(repeatable)) {
updateData.repeatable = repeatable;
}
if (_.isBoolean(retired)) {
updateData.retired = retired;
const profileCourses = ProfileCourses.find({ courseID: docID }).fetch();
profileCourses.forEach((pc) => ProfileCourses.update(pc._id, { retired }));
const reviews = Reviews.find({ revieweeID: docID }).fetch();
reviews.forEach((review) => Reviews.update(review._id, { retired }));
const course = this.findDoc(docID);
const teasers = Teasers.find({ targetSlugID: course.slugID }).fetch();
teasers.forEach((teaser) => Teasers.update(teaser._id, { retired }));
}
this.collection.update(docID, { $set: updateData });
}
/**
* Remove the Course.
* @param instance The docID or slug of the entity to be removed.
* @throws { Meteor.Error } If docID is not a Course, or if this course has any associated course instances.
*/
public removeIt(instance: string) {
// console.log('CourseCollection.removeIt', instance);
const docID = this.getID(instance);
// Check that this is not referenced by any Course Instance.
CourseInstances.find().map((courseInstance) => {
if (courseInstance.courseID === docID) {
throw new Meteor.Error(`Course ${instance} is referenced by a course instance ${courseInstance}.`);
}
return true;
});
// Now remove the Course.
return super.removeIt(docID);
}
/**
* Returns true if Course has the specified interest.
* @param course The user (docID or slug)
* @param interest The Interest (docID or slug).
* @returns {boolean} True if the course has the associated Interest.
* @throws { Meteor.Error } If course is not a course or interest is not a Interest.
*/
public hasInterest(course: string, interest: string) {
const interestID = Interests.getID(interest);
const doc = this.findDoc(course);
return (((doc.interestIDs).includes(interestID)));
}
/**
* Returns a list of Course names corresponding to the passed list of Course docIDs.
* @param instanceIDs A list of Course docIDs.
* @returns { Array }
* @throws { Meteor.Error} If any of the instanceIDs cannot be found.
*/
public findNames(instanceIDs: string[]) {
// console.log('Courses.findNames(%o)', instanceIDs);
return instanceIDs.map((instanceID) => this.getName(instanceID));
}
/**
* Courses have names, but they also have a method 'getName' that returns the `${num}: ${shortName}`. This method
* will return the doc that has that getName.
* @param { String | Object } name Either the docID, or an object selector, or the 'name' field value.
* @returns { Object } The document associated with name.
* @throws { Meteor.Error } If the document cannot be found.
*/
public findDoc(name: string | { [key: string]: unknown } | { name } | { _id: string; } | { username: string; }) {
if (_.isNull(name) || _.isUndefined(name)) {
throw new Meteor.Error(`${name} is not a defined ${this.type}`);
}
const doc = (
this.collection.findOne(name) ||
this.collection.findOne({ name }) ||
this.collection.findOne({ _id: name }) ||
this.collection.findOne({ username: name })) ||
// last chance we were given the getName name.
this.findDocByName(name as string);
if (!doc) {
if (typeof name !== 'string') {
throw new Meteor.Error(`${JSON.stringify(name)} is not a defined ${this.type}`);
} else {
throw new Meteor.Error(`${name} is not a defined ${this.type}`);
}
}
return doc;
}
/**
* Returns a list of CareerGoals that have common interests.
* @param {string} docIdOrSlug a course ID or slug.
* @return {CareerGoals[]} CareerGoals that share the interests.
*/
public findRelatedCareerGoals(docIdOrSlug: string): CareerGoal[] {
const docID = this.getID(docIdOrSlug);
const interestIDs = this.findDoc(docID).interestIDs;
const goals = CareerGoals.findNonRetired();
return goals.filter((goal) => goal.interestIDs.filter((x) => interestIDs.includes(x)).length > 0);
}
/**
* Returns a list of Internships that have common interests.
* @param {string} docIdOrSlug a course ID or slug.
* @return {Internship[]} Internships that share the interests.
*/
public findRelatedInternships(docIdOrSlug: string): Internship[] {
const docID = this.getID(docIdOrSlug);
const interestIDs = this.findDoc(docID).interestIDs;
const internships = Internships.findNonRetired();
return internships.filter((internship) => internship.interestIDs.filter(x => interestIDs.includes(x)).length > 0);
}
/**
* Returns a list of the Opportunities that have common interests.
* @param {string} docIdOrSlug a course ID or slug.
* @return {Opportunity[]} Opportunities that share the interests.
*/
public findRelatedOpportunities(docIdOrSlug: string): Opportunity[] {
const docID = this.getID(docIdOrSlug);
const interestIDs = this.findDoc(docID).interestIDs;
const opportunities = Opportunities.findNonRetired();
return opportunities.filter((opp) => opp.interestIDs.filter((x) => interestIDs.includes(x)).length > 0);
}
/**
* Returns an array of strings, each one representing an integrity problem with this collection.
* Returns an empty array if no problems were found.
* Checks slugID and interestIDs.
* @returns {Array} A (possibly empty) array of strings indicating integrity issues.
*/
public checkIntegrity() {
const problems = [];
this.find({}, {}).forEach((doc) => {
if (!Slugs.isDefined(doc.slugID)) {
problems.push(`Bad slugID: ${doc.slugID}`);
}
doc.interestIDs.forEach((interestID) => {
if (!Interests.isDefined(interestID)) {
problems.push(`Bad interestID: ${interestID}`);
}
});
});
return problems;
}
/**
* Returns an object representing the Course docID in a format acceptable to define().
* @param docID The docID of a Course.
* @returns { Object } An object representing the definition of docID.
*/
public dumpOne(docID): CourseDefine {
const doc = this.findDoc(docID);
const name = doc.name;
const shortName = doc.shortName;
const slug = Slugs.getNameFromID(doc.slugID);
const num = doc.num;
const description = doc.description;
const creditHrs = doc.creditHrs;
const interests = doc.interestIDs.map((interestID) => Interests.findSlugByID(interestID));
const syllabus = doc.syllabus;
const repeatable = doc.repeatable;
const retired = doc.retired;
return { name, shortName, slug, num, description, creditHrs, interests, syllabus, repeatable, retired };
}
public toString(docID: string): string {
const course = this.findDoc(docID);
return `${course.num}: ${course.name}`;
}
/**
* Returns the name of the ID or slug.
* @param {string} docIdOrSlug an ID or slug.
* @return {string} Course number and short name.
*/
public getName(docIdOrSlug: string): string {
const courseID = this.getID(docIdOrSlug);
const course = this.findDoc(courseID);
return `${course.num}: ${course.shortName}`;
}
public findDocByName(name: string): Course {
const num = this.findCourseNumberByName(name);
return this.findDoc({ num });
}
/**
* Returns the course number of the course name
* @param {string} name
* @returns {string}
*/
public findCourseNumberByName(name: string): string {
return name.substring(0, name.indexOf(':'));
}
}
/**
* Provides the singleton instance of this class to all other entities.
* @memberOf api/course
*/
export const Courses = new CourseCollection();
Source