Source

api/user/StudentProfileCollection.methods.ts

import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { CallPromiseMixin } from 'meteor/didericis:callpromise-mixin';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import { StudentProfile } from '../../typings/radgrad';
import { AcademicTerms } from '../academic-term/AcademicTermCollection';
import { CourseInstances } from '../course/CourseInstanceCollection';
import { OpportunityInstances } from '../opportunity/OpportunityInstanceCollection';
import { RadGradProperties } from '../radgrad/RadGradProperties';
import { Reviews } from '../review/ReviewCollection';
import { VerificationRequests } from '../verification/VerificationRequestCollection';
import { Users } from './UserCollection';
import { CareerGoals } from '../career/CareerGoalCollection';
import { StudentProfiles } from './StudentProfileCollection';
import { FacultyProfiles } from './FacultyProfileCollection';
import { AdvisorProfiles } from './AdvisorProfileCollection';
import { AdminProfiles } from './AdminProfileCollection';
import { ProfileCareerGoals } from './profile-entries/ProfileCareerGoalCollection';
import { ProfileInterests } from './profile-entries/ProfileInterestCollection';
import { Interests } from '../interest/InterestCollection';
import { ProfileCourses } from './profile-entries/ProfileCourseCollection';
import { Courses } from '../course/CourseCollection';
import { ProfileOpportunities } from './profile-entries/ProfileOpportunityCollection';
import { Opportunities } from '../opportunity/OpportunityCollection';
import { ROLE } from '../role/Role';

export interface PublicProfileData {
  website?: string;
  picture?: string;
  level?: number;
  ice?: { i; c; e };
  careerGoals?: string[];
  interests?: string[];
  courses?: string[];
  opportunities?: string[];
}

/**
 * Helper function to create and return an object with fields for all the publicly shared profile data for username.
 * @param username The user who's public data is to be shared.
 * @returns {PublicProfileData}
 */
const generatePublicProfileDataObject = (username) => {
  const publicData: PublicProfileData = {};
  if (Meteor.isServer) {
    const profile = Users.getProfile(username);
    const userID = Users.getID(username);
    // Advisors, Faculty, Admins should share everything by default
    // when their accounts are created, but can manually opt-out if they want.
    if (profile.sharePicture) {
      publicData.picture = profile.picture;
    }
    if (profile.shareWebsite) {
      publicData.website = profile.website;
    }
    if (profile.shareLevel) {
      publicData.level = profile.level;
    }
    if (profile.shareICE) {
      // if shareICE exists, then the user must be a student.
      publicData.ice = StudentProfiles.getEarnedICE(username);
    }
    if (profile.shareCareerGoals) {
      const profileDocs = ProfileCareerGoals.findNonRetired({ userID });
      const careerGoalSlugs = profileDocs.map((doc) => CareerGoals.findSlugByID(doc.careerGoalID));
      publicData.careerGoals = CareerGoals.sort(careerGoalSlugs);
    }
    if (profile.shareInterests) {
      const profileDocs = ProfileInterests.findNonRetired({ userID });
      const interestSlugs = profileDocs.map((doc) => Interests.findSlugByID(doc.interestID));
      publicData.interests = Interests.sort(interestSlugs);
    }
    if (profile.shareCourses) {
      const profileDocs = ProfileCourses.findNonRetired({ userID });
      const courseSlugs = profileDocs.map((doc) => Courses.findSlugByID(doc.courseID));
      publicData.courses = Courses.sort(courseSlugs);
    }
    if (profile.shareOpportunities) {
      const profileDocs = ProfileOpportunities.findNonRetired({ userID });
      const opportunitySlugs = profileDocs.map((doc) => Opportunities.findSlugByID(doc.opportunityID));
      publicData.opportunities = Opportunities.sort(opportunitySlugs);
    }
  }
  return publicData;
};

/**
 * Meteor method used to retrieve public data for a student profile card.
 * Returns an object with fields containing the visible profile data.
 */
export const getPublicProfileData = new ValidatedMethod({
  name: 'ProfileCollection.getPublicProfileData',
  mixins: [CallPromiseMixin],
  validate: null,
  run({ username }) {
    if (Meteor.isServer) {
      return generatePublicProfileDataObject(username);
    }
    return null;
  },
});

/**
 * Meteor method to set a Profile field (usually a "share" field).
 * After setting the share value, generates and returns an object with fields containing the visible profile data.
 */
export const setPublicProfileData = new ValidatedMethod({
  name: 'ProfileCollection.setPublicProfileData',
  mixins: [CallPromiseMixin],
  validate: null,
  run({ username, fieldName, fieldValue }) {
    // console.log(username, fieldName, fieldValue);
    if (Meteor.isServer) {
      const profile = Users.getProfile(username);
      let profileCollection;
      if (profile.role === ROLE.STUDENT) {
        profileCollection = StudentProfiles;
      } else if (profile.role === ROLE.FACULTY) {
        profileCollection = FacultyProfiles;
      } else if (profile.role === ROLE.ADVISOR) {
        profileCollection = AdvisorProfiles;
      } else {
        profileCollection = AdminProfiles;
      }
      const updateObject = {};
      updateObject[fieldName] = fieldValue;
      profileCollection.update(profile._id, updateObject);
    }
    // Now that we've updated the profile collection's share field, generate the new set of public data and return.
    return generatePublicProfileDataObject(username);
  },
});

export const getLastAcademicTermMethod = new ValidatedMethod({
  name: 'StudentProfile.getLastAcademicTerm',
  mixins: [CallPromiseMixin],
  validate: null,
  run(user: string) {
    if (Meteor.isServer) {
      return StudentProfiles.getLastAcademicTerm(user);
    }
    return null;
  },
});

const updateStudentToAlumni = (student: StudentProfile): boolean => {
  const oneYearInTerms = RadGradProperties.getQuarterSystem() ? 4 : 3;
  const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(student.username).termNumber;
  if (currentTermNumber > studentLastTermNumber + oneYearInTerms) {
    StudentProfiles.update(student._id, { isAlumni: true });
    return true;
  }
  return false;
};

export const updateStudentToAlumniMethod = new ValidatedMethod({
  name: 'StudentProfile.updateStudentToAlumni',
  mixins: [CallPromiseMixin],
  validate: null,
  run(user: string) {
    if (Meteor.isServer) {
      const profile = Users.getProfile(user);
      if (updateStudentToAlumni(profile)) {
        return `${user}'s last academic term was over a year ago.`;
      }
      return `${user} is still current`;
    }
    return '';
  },
});

export const updateAllStudentsToAlumniMethod = new ValidatedMethod({
  name: 'StudentProfile.updateAllStudentsToAlumni',
  mixins: [CallPromiseMixin],
  validate: null,
  run(user: string) {
    if (Meteor.isServer) {
      const students = StudentProfiles.find({ isAlumni: false }).fetch();
      let count = 0;
      students.forEach((student) => {
        if (updateStudentToAlumni(student)) {
          count++;
        }
      });
      return `${count} students moved to alumni`;
    }
    return '';
  },
});

const retireOldStudent = (student: StudentProfile): boolean => {
  const twoYearInTerms = RadGradProperties.getQuarterSystem() ? 8 : 6;
  const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(student.username).termNumber;
  if (currentTermNumber > studentLastTermNumber + twoYearInTerms) {
    StudentProfiles.update(student._id, { retired: true });
    return true;
  }
  return false;
};

export const retireOldStudentMethod = new ValidatedMethod({
  name: 'StudentProfile.retireOldStudent',
  mixins: [CallPromiseMixin],
  validate: null,
  run(student: string) {
    if (Meteor.isServer) {
      const profile = Users.getProfile(student);
      if (retireOldStudent(profile)) {
        return `Retired ${student} because their last academic term was over two years ago.`;
      }
      return `${student} is still current.`;
    }
    return '';
  },
});

export const retireAllOldStudentsMethod = new ValidatedMethod({
  name: 'StudentProfile.retireAllOldStudents',
  mixins: [CallPromiseMixin],
  validate: null,
  run() {
    if (Meteor.isServer) {
      const students = StudentProfiles.find({}).fetch();
      let count = 0;
      students.forEach((student) => {
        if (retireOldStudent(student)) {
          count++;
        }
      });
      return `Retired ${count} students.`;
    }
    return '';
  },
});

export const matriculateStudentMethod = new ValidatedMethod({
  name: 'StudentProfile.matriculateStudent',
  mixins: [CallPromiseMixin],
  validate: null,
  run(user: string) {
    if (Meteor.isServer) {
      StudentProfiles.assertValidRoleForMethod(this.userId);
      const profile = Users.getProfile(user);
      if (profile.role !== ROLE.STUDENT && profile.role !== ROLE.ALUMNI) {
        throw new Meteor.Error(`${profile.username} isn't a student`, 'You can only matriculate students.');
      }
      if (!profile.retired) {
        throw new Meteor.Error(`${profile.username} isn't retired`, 'Retire the student first.');
      }
      StudentProfiles.removeIt(profile.username);
    }
  },
});

const buildStudentDumpObject = (studentUsernameOrID: string) => {
  const timestamp = new Date();
  const collections = [];
  const studentProfileCollection = {
    name: StudentProfiles.getCollectionName(),
    contents: [],
  };
  const profile = Users.getProfile(studentUsernameOrID);
  studentProfileCollection.contents.push(StudentProfiles.dumpOne(profile._id));
  collections.push(studentProfileCollection);
  const courseInstanceCollection = {
    name: CourseInstances.getCollectionName(),
    contents: CourseInstances.dumpUser(studentUsernameOrID),
  };
  collections.push(courseInstanceCollection);
  const opportunityInstanceCollection = {
    name: OpportunityInstances.getCollectionName(),
    contents: OpportunityInstances.dumpUser(studentUsernameOrID),
  };
  collections.push(opportunityInstanceCollection);
  const profileCareerGoalCollection = {
    name: ProfileCareerGoals.getCollectionName(),
    contents: ProfileCareerGoals.dumpUser(studentUsernameOrID),
  };
  collections.push(profileCareerGoalCollection);
  const profileCourseCollection = {
    name: ProfileCourses.getCollectionName(),
    contents: ProfileCourses.dumpUser(studentUsernameOrID),
  };
  collections.push(profileCourseCollection);
  const profileInterestCollection = {
    name: ProfileInterests.getCollectionName(),
    contents: ProfileInterests.dumpUser(studentUsernameOrID),
  };
  collections.push(profileInterestCollection);
  const profileOpportunityCollection = {
    name: ProfileOpportunities.getCollectionName(),
    contents: ProfileOpportunities.dumpUser(studentUsernameOrID),
  };
  collections.push(profileOpportunityCollection);
  const reviewCollection = {
    name: Reviews.getCollectionName(),
    contents: Reviews.dumpUser(studentUsernameOrID),
  };
  collections.push(reviewCollection);
  const verificationCollection = {
    name: VerificationRequests.getCollectionName(),
    contents: VerificationRequests.dumpUser(studentUsernameOrID),
  };
  collections.push(verificationCollection);
  return { timestamp, collections };
};

export const dumpStudentMethod = new ValidatedMethod({
  name: 'StudentProfile.dumpStudent',
  mixins: [CallPromiseMixin],
  validate: null,
  run(studentUsernameOrID: string) {
    if (Meteor.isServer) {
      StudentProfiles.assertValidRoleForMethod(this.userId);
      const profile = Users.getProfile(studentUsernameOrID);
      if (profile.role !== ROLE.STUDENT && profile.role !== ROLE.ALUMNI) {
        throw new Meteor.Error(`${profile.username} isn't a student`, 'You can only dump students.');
      }
      return buildStudentDumpObject(studentUsernameOrID);
    }
    return {};
  },
});

const shouldMatriculate = (studentProfile: StudentProfile): boolean => {
  const threeYearInTerms = RadGradProperties.getQuarterSystem() ? 12 : 9;
  const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(studentProfile.username).termNumber;
  return currentTermNumber > studentLastTermNumber + threeYearInTerms;
};

const getStudentsToMatriculate = (): string[] => {
  const students = StudentProfiles.find({}).fetch();
  const threeYearInTerms = RadGradProperties.getQuarterSystem() ? 12 : 9;
  const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  const retVal = [];
  students.forEach((student) => {
    const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(student.username).termNumber;
    if (currentTermNumber > studentLastTermNumber + threeYearInTerms) {
      retVal.push(student.username);
    }
  });
  return retVal;
};

export const matriculateAllOldStudentsMethod = new ValidatedMethod({
  name: 'StudentProfiles.matriculateAllOldStudents',
  mixins: [CallPromiseMixin],
  validate: null,
  run() {
    if (Meteor.isServer) {
      StudentProfiles.assertValidRoleForMethod(this.userId);
      const studentsToMatriculate = getStudentsToMatriculate();
      const retVal = [];
      const mDate = moment().format('YYYY-MM-DD');
      studentsToMatriculate.forEach((student) => {
        const fileName = `${student}-${mDate}`;
        const record = {
          fileName,
          contents: buildStudentDumpObject(student),
        };
        retVal.push(record);
        StudentProfiles.removeIt(student);
      });
    }
  },
});

export const updateAllStudentsStatusMethod = new ValidatedMethod({
  name: 'StudentProfiles.updateAllStudentsStatus',
  mixins: [CallPromiseMixin],
  validate: null,
  run() {
    if (Meteor.isServer) {
      StudentProfiles.assertValidRoleForMethod(this.userId);
      const students = StudentProfiles.find({}).fetch();
      let alumniCount = 0;
      let retiredCount = 0;
      let matriculatedCount = 0;
      const studentRecords = [];
      const mDate = moment().format('YYYY-MM-DD');
      students.forEach((student) => {
        if (updateStudentToAlumni(student)) {
          alumniCount++;
        }
        if (retireOldStudent(student)) {
          retiredCount++;
        }
        if (shouldMatriculate(student)) {
          matriculatedCount++;
          const fileName = `${student.username}-${mDate}`;
          const record = {
            fileName,
            contents: buildStudentDumpObject(student.username),
          };
          studentRecords.push(record);
          StudentProfiles.removeIt(student);
        }
      });
      return {
        alumniCount,
        retiredCount,
        matriculatedCount,
        studentRecords,
      };
    }
    return {};
  },
});

export const findOddStudentsMethod = new ValidatedMethod({
  name: 'StudentProfiles.findOddStudents',
  mixins: [CallPromiseMixin],
  validate: null,
  run() {
    const retVal = [];
    if (Meteor.isServer) {
      StudentProfiles.assertValidRoleForMethod(this.userId);
      const students = StudentProfiles.find({}).fetch();
      students.forEach((student) => {
        const studentID = student.userID;
        const cis = CourseInstances.find({ studentID }).fetch();
        const ois = OpportunityInstances.find({ studentID }).fetch();
        if (cis.length === 0 && ois.length === 0) {
          retVal.push(student);
        }
      });
      return retVal;
    }
    return retVal;
  },
});