Source

api/degree-plan/AcademicYearInstanceCollection.ts

import { Meteor } from 'meteor/meteor';
import SimpleSchema from 'simpl-schema';
import _ from 'lodash';
import moment from 'moment';
import { AcademicTerms } from '../academic-term/AcademicTermCollection';
import { ROLE } from '../role/Role';
import { Users } from '../user/UserCollection';
import BaseCollection from '../base/BaseCollection';
import { AcademicYearInstanceDefine } from '../../typings/radgrad';
import { RadGradProperties } from '../radgrad/RadGradProperties';

/**
 * Each AcademicYearInstance represents a sequence of three or four academic terms for a given student.
 * It is used to control the display of academic terms for a given student in the Degree Planner.
 * @extends api/base.BaseCollection
 * @memberOf api/degree-plan
 */
class AcademicYearInstanceCollection extends BaseCollection {
  /**
   * Creates the AcademicYearInstance collection.
   */
  constructor() {
    super('AcademicYearInstance', new SimpleSchema({
      year: { type: Number },
      springYear: { type: Number },
      studentID: { type: SimpleSchema.RegEx.Id },
      termIDs: [SimpleSchema.RegEx.Id],
      retired: { type: Boolean, optional: true },
    }));
    if (Meteor.isServer) {
      this.collection.rawCollection().createIndex({ studentID: 1, year: 1 });
    }
    this.defineSchema = new SimpleSchema({
      year: {
        type: SimpleSchema.Integer,
        min: moment().year() - 5,
        max: moment().year() + 10,
        defaultValue: moment().year(),
      },
      student: String,
    });
    // year?: number; springYear?: number; studentID?: string; termIDs?: string[];
    this.updateSchema = new SimpleSchema({
      year: { type: SimpleSchema.Integer, min: moment().year() - 10, max: moment().year() + 10, optional: true },
      springYear: {
        type: SimpleSchema.Integer,
        min: moment().year() - 10,
        max: moment().year() + 10,
        optional: true,
      },
      studentID: { type: String, optional: true },
      termIDs: { type: Array, optional: true },
      'termIDs.$': String,
      retired: { type: Boolean, optional: true },
    });
  }

  /**
   * Defines a new AcademicYearInstance.
   * @example
   * To define the 2016 - 2017 academic year for Joe Smith.
   *     AcademicYearInstances.define({ year: 2016,
   *                                    student: 'joesmith' });
   * @param { Object } Object with keys year and student.
   * @throws {Meteor.Error} If the definition includes an undefined student or a year that is out of bounds.
   * @returns The newly created docID.
   */
  public define({ year, student }: AcademicYearInstanceDefine) {
    const studentID = Users.getID(student);
    const quarterSystem = RadGradProperties.getQuarterSystem();
    let termIDs = [];
    // check for gaps
    const prevYears = this.collection.find({ year: { $lt: year }, studentID }, { sort: { year: 1 } }).fetch();
    if (prevYears.length > 0) {
      const lastYear = prevYears[prevYears.length - 1].year;
      for (let y = lastYear + 1; y < year; y++) {
        if (this.collection.find({ year: y, studentID }).fetch().length === 0) {
          termIDs = [];
          termIDs.push(AcademicTerms.getID(`${AcademicTerms.FALL}-${y}`));
          if (quarterSystem) {
            termIDs.push(AcademicTerms.getID(`${AcademicTerms.WINTER}-${y + 1}`));
          }
          termIDs.push(AcademicTerms.getID(`${AcademicTerms.SPRING}-${y + 1}`));
          termIDs.push(AcademicTerms.getID(`${AcademicTerms.SUMMER}-${y + 1}`));
          this.collection.insert({ year: y, springYear: y + 1, studentID, termIDs });
        }
      }
    }
    const nextYears = this.collection.find({ year: { $gt: year }, studentID }, { sort: { year: 1 } }).fetch();
    if (nextYears.length > 0) {
      const nextYear = nextYears[0].year;
      for (let y = year + 1; y < nextYear; y++) {
        if (this.collection.find({ year: y, studentID }).fetch().length === 0) {
          termIDs = [];
          termIDs.push(AcademicTerms.getID(`${AcademicTerms.FALL}-${y}`));
          if (quarterSystem) {
            termIDs.push(AcademicTerms.getID(`${AcademicTerms.WINTER}-${y + 1}`));
          }
          termIDs.push(AcademicTerms.getID(`${AcademicTerms.SPRING}-${y + 1}`));
          termIDs.push(AcademicTerms.getID(`${AcademicTerms.SUMMER}-${y + 1}`));
          this.collection.insert({ year: y, springYear: y + 1, studentID, termIDs });
        }
      }
    }
    const doc = this.collection.find({ year, studentID }).fetch();
    if (doc.length > 0) {
      return doc[0]._id;
    }
    termIDs = [];
    termIDs.push(AcademicTerms.getID(`${AcademicTerms.FALL}-${year}`));
    if (quarterSystem) {
      termIDs.push(AcademicTerms.getID(`${AcademicTerms.WINTER}-${year + 1}`));
    }
    termIDs.push(AcademicTerms.getID(`${AcademicTerms.SPRING}-${year + 1}`));
    termIDs.push(AcademicTerms.getID(`${AcademicTerms.SUMMER}-${year + 1}`));

    // Define and return the docID
    return this.collection.insert({ year, springYear: year + 1, studentID, termIDs });
  }

  /**
   * Update an AcademicYear.
   * @param docID The docID associated with this academic year.
   * @param year the fall year.
   * @param springYear the spring year
   * @param studentID the student's ID.
   * @param termIDs the 3 or 4 academic terms in the year.
   */
  public update(docID: string, { year, retired }:
  { year?: number; springYear?: number; studentID?: string; termIDs?: string[]; retired?: boolean }) {
    this.assertDefined(docID);
    const termIDs = [];
    const updateData: { year?: number; springYear?: number; termIDs?: string[]; retired?: boolean } = {};
    if (_.isNumber(year)) {
      updateData.year = year;
      updateData.springYear = year + 1;
      termIDs.push(AcademicTerms.getID(`${AcademicTerms.FALL}-${year}`));
      if (RadGradProperties.getQuarterSystem()) {
        termIDs.push(AcademicTerms.getID(`${AcademicTerms.WINTER}-${year + 1}`));
      }
      termIDs.push(AcademicTerms.getID(`${AcademicTerms.SPRING}-${year + 1}`));
      termIDs.push(AcademicTerms.getID(`${AcademicTerms.SUMMER}-${year + 1}`));
      updateData.termIDs = termIDs;
    }
    if (_.isBoolean(retired)) {
      updateData.retired = retired;
    }
    this.collection.update(docID, { $set: updateData });
  }

  /**
   * Remove the academic year.
   * @param docID The docID of the academic year.
   */
  public removeIt(docID: string) {
    this.assertDefined(docID);
    // OK, clear to delete.
    return super.removeIt(docID);
  }

  /**
   * Removes all AcademicYearInstance documents referring to user.
   * @param user The student, either the ID or the username.
   * @throws { Meteor.Error } If user is not an ID or username.
   */
  public removeUser(user: string): void {
    const studentID = Users.getID(user);
    this.collection.remove({ studentID });
  }

  /**
   * Implementation of assertValidRoleForMethod. Asserts that userId is logged in as an Admin, Advisor or
   * Student.
   * 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 an Admin or Advisor.
   */
  public assertValidRoleForMethod(userId: string): void {
    this.assertRole(userId, [ROLE.ADMIN, ROLE.ADVISOR, ROLE.STUDENT]);
  }

  /**
   * Depending on the logged in user publish only their AcademicYears. If
   * the user is an Admin or Advisor then publish all AcademicYears.
   */
  public publish(): void {
    if (Meteor.isServer) {
      const collection = this.collection;
      Meteor.publish(this.collectionName, function filterStudentID(studentID) { // eslint-disable-line meteor/audit-argument-checks
        if (_.isNil(studentID)) {
          return this.ready();
        }
        const profile = Users.getProfile(studentID);
        if (profile.role === ROLE.ADMIN || Meteor.isAppTest) {
          return collection.find();
        }
        return collection.find({ studentID, retired: { $not: { $eq: true } } });
      });
    }
  }

  /**
   * @returns {String} A formatted string representing the academic year instance.
   * @param academicYearInstanceID The academic year instance ID.
   * @throws {Meteor.Error} If not a valid ID.
   */
  public toString(academicYearInstanceID: string): string {
    this.assertDefined(academicYearInstanceID);
    const doc = this.findDoc(academicYearInstanceID);
    const username = Users.getProfile(doc.studentID).username;
    return `[AY ${doc.year}-${doc.year + 1} ${username}]`;
  }

  /**
   * Returns an array of strings, each one representing an integrity problem with this collection.
   * Returns an empty array if no problems were found.
   * Checks studentID, termIDs
   * @returns {Array} A (possibly empty) array of strings indicating integrity issues.
   */
  public checkIntegrity(): string[] {
    const problems = [];
    this.find().forEach((doc) => {
      if (!Users.isDefined(doc.studentID)) {
        problems.push(`Bad studentID: ${doc.studentID}`);
      }
      _.forEach(doc.termIDs, (termID) => {
        if (!AcademicTerms.isDefined(termID)) {
          problems.push(`Bad termID: ${termID}`);
        }
      });
    });
    return problems;
  }

  /**
   * Returns an object representing the AcademicYearInstance docID in a format acceptable to define().
   * @param docID The docID of an AcademicYearInstance.
   * @returns { object } An object representing the definition of docID.
   */
  public dumpOne(docID: string): AcademicYearInstanceDefine {
    const doc = this.findDoc(docID);
    const student = Users.getProfile(doc.studentID).username;
    const year = doc.year;
    return { student, year };
  }
}

/**
 * Provides the singleton instance of this class to all other entities.
 * @memberOf api/degree-plan
 */
export const AcademicYearInstances = new AcademicYearInstanceCollection();
// We are not going to persist AcademicYearInstances