Source

api/interest/InterestCollection.ts

import SimpleSchema from 'simpl-schema';
import { Meteor } from 'meteor/meteor';
import _ from 'lodash';
import { Internships } from '../internship/InternshipCollection';
import { Slugs } from '../slug/SlugCollection';
import { ProfileInterests } from '../user/profile-entries/ProfileInterestCollection';
import { InterestKeywords } from './InterestKeywordCollection';
import { InterestTypes } from './InterestTypeCollection';
import { Courses } from '../course/CourseCollection';
import { CareerGoals } from '../career/CareerGoalCollection';
import { Opportunities } from '../opportunity/OpportunityCollection';
import { Teasers } from '../teaser/TeaserCollection';
import BaseSlugCollection from '../base/BaseSlugCollection';
import { CareerGoal, Course, InterestDefine, InterestUpdate, Internship, Opportunity } from '../../typings/radgrad';

/**
 * Represents a specific interest, such as "Software Engineering".
 * Note that all Interests must have an associated InterestType.
 * @extends api/base.BaseSlugCollection
 * @memberOf api/interest
 */
class InterestCollection extends BaseSlugCollection {

  /**
   * Creates the Interest collection.
   */
  constructor() {
    super('Interest', new SimpleSchema({
      name: { type: String },
      slugID: { type: SimpleSchema.RegEx.Id },
      description: { type: String },
      interestTypeID: { type: SimpleSchema.RegEx.Id },
      retired: { type: Boolean, optional: true },
      picture: { type: String, optional: true, defaultValue: 'images/header-panel/header-interests.png' },
    }));
    this.defineSchema = new SimpleSchema({
      name: String,
      slug: String,
      description: String,
      interestType: String,
      retired: { type: Boolean, optional: true },
      picture: { type: String, optional: true },
      keywords: { type: Array },
      'keywords.$': { type: String },
    });
    this.updateSchema = new SimpleSchema({
      name: { type: String, optional: true },
      slug: { type: String, optional: true },
      description: { type: String, optional: true },
      interestType: { type: String, optional: true },
      retired: { type: Boolean, optional: true },
      picture: { type: String, optional: true },
      keywords: { type: Array, optional: true },
      'keywords.$': { type: String },
    });
  }

  /**
   * Defines a new Interest and its associated Slug.
   * @example
   * Interests.define({ name: 'Software Engineering',
   *                    slug: 'software-engineering',
   *                    description: 'Methods for group development of large, high quality software systems',
   *                    interestType: 'cs-disciplines' });
   * @param { Object } description Object with keys name, slug, description, interestType.
   * Slug must be previously undefined.
   * InterestType must be an InterestType slug or ID.
   * @throws {Meteor.Error} If the interest definition includes a defined slug or undefined interestType.
   * @returns The newly created docID.
   */
  public define({ name, slug, description, interestType, retired = false, picture, keywords = [] }: InterestDefine): string {
    // console.log(`${this.collectionName}.define(${name}, ${slug}, ${description}, ${interestType}`);
    // Get InterestTypeID, throw error if not found.
    const interestTypeID = InterestTypes.getID(interestType);
    // Get SlugID, throw error if found.
    const slugID = Slugs.define({ name: slug, entityName: this.getType() });
    // Define the Interest and get its ID
    const interestID = this.collection.insert({ name, description, slugID, interestTypeID, retired, picture });
    // Connect the Slug to this Interest
    Slugs.updateEntityID(slugID, interestID);
    // Update the InterestKeywords
    if (keywords.length > 0) {
      InterestKeywords.removeInterest(interestID);
      keywords.forEach((word) => InterestKeywords.define({ interest: slug, keyword: word, retired }));
    }
    return interestID;
  }

  /**
   * Update an Interest.
   * @param docID The docID to be updated.
   * @param name The new name (optional).
   * @param description The new description (optional).
   * @param interestType The new interestType slug or ID (optional).
   * @throws { Meteor.Error } If docID is not defined, or if interestType is not valid.
   */
  public update(docID: string, { name, description, interestType, retired, picture, keywords }: InterestUpdate) {
    this.assertDefined(docID);
    const updateData: { name?: string, description?: string, interestTypeID?: string, retired?: boolean, picture?:string } = {};
    if (name) {
      updateData.name = name;
    }
    if (description) {
      updateData.description = description;
    }
    if (picture) {
      updateData.picture = picture;
    }
    if (interestType) {
      const interestTypeID = InterestTypes.getID(interestType);
      updateData.interestTypeID = interestTypeID;
    }
    if (_.isBoolean(retired)) {
      updateData.retired = retired;
      const profileInterests = ProfileInterests.find({ interestID: docID }).fetch();
      profileInterests.forEach((pi) => ProfileInterests.update(pi._id, { retired }));
      const interest = this.findDoc(docID);
      const teasers = Teasers.find({ targetSlugID: interest.slugID }).fetch();
      teasers.forEach((teaser) => Teasers.update(teaser._id, { retired }));
    }
    this.collection.update(docID, { $set: updateData });
    if (keywords) {
      InterestKeywords.removeInterest(docID);
      const interestDoc = this.findDoc(docID);
      const interest = Slugs.getNameFromID(interestDoc.slugID);
      keywords.forEach(word => InterestKeywords.define({ interest, keyword: word }));
    }
    return true;
  }

  /**
   * Throws an error if docID (an interest) is referenced in any of the passed collections.
   * @param collectionList A list of collection class instances.
   * @param docID The docID of an interest.
   * @throws { Meteor.Error } If the interest is referenced in any of the collections.
   */
  public assertUnusedInterest(collectionList, docID: string) {
    const interest = this.findDoc(docID);
    collectionList.forEach((collection) => {
      collection.find().map((doc) => {
        if (collection.hasInterest(doc, docID)) {
          throw new Meteor.Error(`Interest ${interest.name} is referenced by collection ${collection.collectionName}.`);
        }
        return true;
      });
    });
  }

  /**
   * Remove the Interest.
   * @param instance The docID or slug of the entity to be removed.
   * @throws { Meteor.Error } If Interest is associated with any User, Course, Career Goal, or Opportunity.
   */
  public removeIt(instance: string) {
    const docID = this.getID(instance);
    // Remove any InterestKeywords
    InterestKeywords.removeInterest(docID);
    // Check that this interest is not referenced by any User.
    // TODO Should the profile collections be included below?
    // No, but we need to check ProfileInterests
    this.assertUnusedInterest([Courses, CareerGoals, Opportunities, Teasers], docID);
    // OK, clear to delete.
    return super.removeIt(docID);
  }

  /**
   * Returns a list of Interest names corresponding to the passed list of Interest docIDs.
   * @param instanceIDs A list of Interest docIDs.
   * @returns { Array }
   * @throws { Meteor.Error} If any of the instanceIDs cannot be found.
   */
  public findNames(instanceIDs: string[]) {
    // console.log('Interests.findNames(%o)', instanceIDs);
    return instanceIDs.map((instanceID) => this.findDoc(instanceID).name);
  }

  /**
   * Returns a list of the CareerGoals that have the given interest.
   * @param {string} docIdOrSlug an interest ID or slug.
   * @return {CareerGoal[]} CareerGoals that have the given interest.
   */
  public findRelatedCareerGoals(docIdOrSlug: string): CareerGoal[] {
    const interestID = this.getID(docIdOrSlug);
    const careerGoals = CareerGoals.findNonRetired();
    return careerGoals.filter((goal) => goal.interestIDs.includes(interestID));
  }

  /**
   * Returns a list of Courses that have the given interest.
   * @param {string} docIdOrSlug an interest ID or slug.
   * @return {Course[]} Courses that have the given interest.
   */
  public findRelatedCourses(docIdOrSlug: string): Course[] {
    const interestID = this.getID(docIdOrSlug);
    const courses = Courses.findNonRetired();
    return courses.filter((course) => course.interestIDs.includes(interestID));
  }

  /**
   * Returns a list of Internships that have the given interest.
   * @param {string} docIdOrSlug an interest ID or slug.
   * @return {Internship[]} Internships that have the given interest.
   */
  public findRelatedInternships(docIdOrSlug: string): Internship[] {
    const interestID = this.getID(docIdOrSlug);
    const internships = Internships.findNonRetired();
    return internships.filter((internship) => internship.interestIDs.includes(interestID));
  }

  /**
   * Returns a list of the Opportunities that have the given interest.
   * @param {string} docIdOrSlug an interest ID or slug.
   * @return {Opportunity[]} Opportunities that have the given interest.
   */
  public findRelatedOpportunities(docIdOrSlug: string): Opportunity[] {
    const interestID = this.getID(docIdOrSlug);
    const opportunities = Opportunities.findNonRetired();
    return opportunities.filter((opp) => opp.interestIDs.includes(interestID));
  }

  /**
   * 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 interestTypeID.
   * @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}`);
      }
      if (!InterestTypes.isDefined(doc.interestTypeID)) {
        problems.push(`Bad interestTypeID: ${doc.interestTypeID}`);
      }
    });
    return problems;
  }

  /**
   * Returns an object representing the Interest docID in a format acceptable to define().
   * @param docID The docID of an Interest.
   * @returns { Object } An object representing the definition of docID.
   */
  public dumpOne(docID): InterestDefine {
    const doc = this.findDoc(docID);
    const name = doc.name;
    const slug = Slugs.getNameFromID(doc.slugID);
    const description = doc.description;
    const picture = doc.picture;
    const interestType = InterestTypes.findSlugByID(doc.interestTypeID);
    const retired = doc.retired;
    const keywords = InterestKeywords.getKeywords(slug);
    return { name, slug, description, interestType, retired, picture, keywords };
  }
}

/**
 * Provides the singleton instance of this class to all other entities.
 * @type {api/interest.InterestCollection}
 * @memberOf api/interest
 */
export const Interests = new InterestCollection();