Source

api/opportunity/OpportunityCollection.ts

import SimpleSchema from 'simpl-schema';
import { Meteor } from 'meteor/meteor';
import _ from 'lodash';
import moment from 'moment';
import { Slugs } from '../slug/SlugCollection';
import { AcademicTerms } from '../academic-term/AcademicTermCollection';
import { Teasers } from '../teaser/TeaserCollection';
import { Interests } from '../interest/InterestCollection';
import { ROLE } from '../role/Role';
import { Users } from '../user/UserCollection';
import { OpportunityTypes } from './OpportunityTypeCollection';
import { OpportunityInstances } from './OpportunityInstanceCollection';
import { Feeds } from '../feed/FeedCollection';
import BaseSlugCollection from '../base/BaseSlugCollection';
import { assertICE, iceSchema } from '../ice/IceProcessor';
import { OpportunityDefine, OpportunityUpdate, OpportunityUpdateData } from '../../typings/radgrad';

export const defaultProfilePicture = '/images/radgrad_logo.png';

/**
 * Represents an Opportunity, such as "LiveWire Internship".
 * To represent an Opportunity taken by a specific student in a specific academicTerm, use OpportunityInstance.
 * @extends api/base.BaseSlugCollection
 * @memberOf api/opportunity
 */
class OpportunityCollection extends BaseSlugCollection {

  /**
   * Creates the Opportunity collection.
   */
  constructor() {
    super('Opportunity', new SimpleSchema({
      name: { type: String },
      slugID: { type: String },
      description: { type: String },
      opportunityTypeID: { type: SimpleSchema.RegEx.Id },
      sponsorID: { type: SimpleSchema.RegEx.Id },
      interestIDs: [SimpleSchema.RegEx.Id],
      termIDs: [SimpleSchema.RegEx.Id],
      timestamp: { type: Date },
      // Optional data
      eventDate: { type: Date, optional: true },
      ice: { type: iceSchema, optional: true },
      picture: { type: String, optional: true },
      retired: { type: Boolean, optional: true },
    }));
    this.defineSchema = new SimpleSchema({
      name: String,
      slug: String,
      description: String,
      opportunityType: String,
      sponsor: String,
      terms: Array,
      'terms.$': String,
      timestamp: { type: Date },
      eventDate: { type: Date, optional: true },
      ice: { type: iceSchema, optional: true },
      picture: { type: String, optional: true },
      retired: { type: Boolean, optional: true },
    });
    this.updateSchema = new SimpleSchema({
      name: { type: String, optional: true },
      description: { type: String, optional: true },
      opportunityType: { type: String, optional: true },
      sponsor: { type: String, optional: true },
      terms: { type: Array, optional: true },
      'terms.$': String,
      timestamp: { type: Date, optional: true },
      eventDate: { type: Date, optional: true },
      ice: { type: iceSchema, optional: true },
      picture: { type: String, optional: true },
      retired: { type: Boolean, optional: true },
    });
  }

  /**
   * Defines a new Opportunity.
   * @example
   * Opportunities.define({ name: 'ATT Hackathon',
   *                        slug: 'att-hackathon',
   *                        description: 'Programming challenge at Sacred Hearts Academy, $10,000 prize',
   *                        opportunityType: 'event',
   *                        sponsor: 'philipjohnson',
   *                        ice: { i: 10, c: 0, e: 10},
   *                        interests: ['software-engineering'],
   *                        academicTerms: ['Fall-2016', 'Spring-2016', 'Summer-2106'],
   *                      });
   * @param { Object } description Object with keys name, slug, description, opportunityType, sponsor, interests,
   * @param name the name of the opportunity.
   * @param slug must not be previously defined.
   * @param description the description of the opportunity. Can be markdown.
   * @param opportunityType must be defined slug.
   * @param interests must be a (possibly empty) array of interest slugs or IDs.
   * @param academicTerms must be a (possibly empty) array of academicTerm slugs or IDs.
   * @param sponsor must be a User with role 'FACULTY', 'ADVISOR', or 'ADMIN'.
   * @param ice must be a valid ICE object.
   * @param eventDate optional date.
   * @param timestamp the Date timestamp that this Opportunity document was created at.
   * @param picture The URL to the opportunity picture. (optional, defaults to a default picture.)
   * @param retired optional, true if the opportunity is retired.
   * @throws {Meteor.Error} If the definition includes a defined slug or undefined interest, sponsor, opportunityType,
   * or startActive or endActive are not valid.
   * @returns The newly created docID.
   */
  public define({ name, slug, description, opportunityType, sponsor, interests, academicTerms, ice, timestamp = moment().toDate(), eventDate = null, picture = defaultProfilePicture, retired = false }: OpportunityDefine) {
    // Get instances, or throw error

    const opportunityTypeID = OpportunityTypes.getID(opportunityType);
    const sponsorID = Users.getID(sponsor);
    Users.assertInRole(sponsorID, [ROLE.FACULTY, ROLE.ADVISOR, ROLE.ADMIN]);
    assertICE(ice);
    const interestIDs = Interests.getIDs(interests);
    // 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());
    }
    const slugID = Slugs.define({ name: slug, entityName: this.getType() });
    const termIDs = AcademicTerms.getIDs(academicTerms);
    let opportunityID;
    if (eventDate !== null) {
      // Define the new Opportunity and its Slug.
      opportunityID = this.collection.insert({
        name, slugID, description, opportunityTypeID, sponsorID,
        interestIDs, termIDs, ice, timestamp, eventDate, retired, picture });
    } else {
      opportunityID = this.collection.insert({
        name, slugID, description, opportunityTypeID, sponsorID,
        interestIDs, termIDs, ice, timestamp, retired, picture });
    }
    Slugs.updateEntityID(slugID, opportunityID);

    // Return the id to the newly created Opportunity.
    return opportunityID;
  }

  /**
   * Update an Opportunity.
   * @param instance The docID or slug associated with this opportunity.
   * @param name optional.
   * @param description optional.
   * @param opportunityType docID or slug (optional.)
   * @param sponsor user in role admin, advisor, or faculty. optional.
   * @param interests optional.
   * @param academicTerms optional
   * @param eventDate a Date. (optional)
   * @param ice An ICE object (optional).
   * @param retired boolean (optional).
   * @param picture
   */
  public update(instance: string, { name, description, opportunityType, sponsor, interests, academicTerms, eventDate, ice, timestamp, retired, picture }: OpportunityUpdate) {
    const docID = this.getID(instance);
    const updateData: OpportunityUpdateData = {};
    if (name) {
      updateData.name = name;
    }
    if (description) {
      updateData.description = description;
    }
    if (opportunityType) {
      updateData.opportunityTypeID = OpportunityTypes.getID(opportunityType);
    }
    if (sponsor) {
      const sponsorID = Users.getID(sponsor);
      Users.assertInRole(sponsorID, [ROLE.FACULTY, ROLE.ADVISOR, ROLE.ADMIN]);
      updateData.sponsorID = sponsorID;
    }
    if (interests) {
      updateData.interestIDs = Interests.getIDs(interests);
    }
    if (academicTerms) {
      updateData.termIDs = AcademicTerms.getIDs(academicTerms);
    }
    if (eventDate) {
      updateData.eventDate = eventDate;
    }
    if (ice) {
      assertICE(ice);
      updateData.ice = ice;
    }
    if (timestamp) {
      updateData.timestamp = timestamp;
    }
    if (_.isBoolean(retired)) {
      updateData.retired = retired;
    }
    if (picture) {
      updateData.picture = picture;
    }
    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) {
    const docID = this.getID(instance);
    // Check that this opportunity is not referenced by any Opportunity Instance.
    OpportunityInstances.find().map((opportunityInstance) => {
      if (opportunityInstance.opportunityID === docID) {
        throw new Meteor.Error(`Opportunity ${instance} referenced by a opportunity instance ${opportunityInstance}.`);
      }
      return true;
    });
    // Check that this opportunity is not referenced by any Teaser.
    Teasers.find().map((teaser) => {
      if (Teasers.hasTarget(teaser, docID)) {
        throw new Meteor.Error(`Opportunity ${instance} referenced by a teaser ${teaser}.`);
      }
      return true;
    });
    // OK to delete. First remove any Feeds that reference this opportunity.
    Feeds.find({ opportunityID: docID }).map((feed) => Feeds.removeIt(feed._id));
    return super.removeIt(docID);
  }

  /**
   * Asserts that userId is logged in as an Admin, Faculty, or Advisor.
   * This is used in the define, update, and removeIt Meteor methods associated with each class.
   * @param userId The userId of the logged in user. Can be null or undefined
   * @throws { Meteor.Error } If there is no logged in user, or the user is not in the allowed roles.
   */
  public assertValidRoleForMethod(userId: string) {
    this.assertRole(userId, [ROLE.ADMIN, ROLE.ADVISOR, ROLE.FACULTY]);
  }

  /**
   * Returns the OpportunityType associated with the Opportunity with the given instanceID.
   * @param instanceID The id of the Opportunity.
   * @returns {Object} The associated Opportunity.
   * @throws {Meteor.Error} If instanceID is not a valid ID.
   */
  public getOpportunityTypeDoc(instanceID: string) {
    this.assertDefined(instanceID);
    const instance = this.collection.findOne({ _id: instanceID });
    return OpportunityTypes.findDoc(instance.opportunityTypeID);
  }

  /**
   * 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, opportunityTypeID, sponsorID, interestIDs, termIDs
   * @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 (!OpportunityTypes.isDefined(doc.opportunityTypeID)) {
        problems.push(`Bad opportunityTypeID: ${doc.opportunityTypeID}`);
      }
      if (!Users.isDefined(doc.sponsorID)) {
        problems.push(`Bad sponsorID: ${doc.sponsorID}`);
      }
      _.forEach(doc.interestIDs, (interestID) => {
        if (!Interests.isDefined(interestID)) {
          problems.push(`Bad interestID: ${interestID}`);
        }
      });
      _.forEach(doc.termIDs, (termID) => {
        if (!AcademicTerms.isDefined(termID)) {
          problems.push(`Bad termID: ${termID}`);
        }
      });
    });
    return problems;
  }

  /**
   * Returns true if Opportunity has the specified interest.
   * @param opportunity The opportunity(docID or slug)
   * @param interest The Interest (docID or slug).
   * @returns {boolean} True if the opportunity has the associated Interest.
   * @throws { Meteor.Error } If opportunity is not a opportunity or interest is not a Interest.
   */
  public hasInterest(opportunity: string, interest: string) {
    const interestID = Interests.getID(interest);
    const doc = this.findDoc(opportunity);
    return _.includes(doc.interestIDs, interestID);
  }

  /**
   * Returns an object representing the Opportunity docID in a format acceptable to define().
   * @param docID The docID of an Opportunity.
   * @returns { Object } An object representing the definition of docID.
   */
  public dumpOne(docID: string): OpportunityDefine {
    const doc = this.findDoc(docID);
    const name = doc.name;
    const slug = Slugs.getNameFromID(doc.slugID);
    const opportunityType = OpportunityTypes.findSlugByID(doc.opportunityTypeID);
    const sponsor = Users.getProfile(doc.sponsorID).username;
    const description = doc.description;
    const ice = doc.ice;
    const interests = _.map(doc.interestIDs, (interestID) => Interests.findSlugByID(interestID));
    const academicTerms = _.map(doc.termIDs, (termID) => AcademicTerms.findSlugByID(termID));
    const eventDate = doc.eventDate;
    const timestamp = doc.timestamp;
    const retired = doc.retired;
    return { name, slug, description, opportunityType, sponsor, ice, interests, academicTerms, eventDate, timestamp, retired };
  }
}

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