Source

api/whats-new/WhatsNew.ts

import _ from 'lodash';
import moment from 'moment';
import { USER_INTERACTIONS, UserInteractions } from '../user-interaction/UserInteractionCollection';
import { CareerGoals } from '../career/CareerGoalCollection';
import { Courses } from '../course/CourseCollection';
import { Interests } from '../interest/InterestCollection';
import { Opportunities } from '../opportunity/OpportunityCollection';
import { AdvisorProfiles } from '../user/AdvisorProfileCollection';
import { FacultyProfiles } from '../user/FacultyProfileCollection';
import { StudentProfiles } from '../user/StudentProfileCollection';

/**
 * What's New is implemented via the following mechanisms:
 *   * This server-side class, which stores the current state of What's New along with updateData() and getData() methods.
 *   * The WhatsNew method, which is called by the client to obtain what's new data via the getData() method.
 *   * The WhatsNew server-side cron job, which updates what's new once a day via updateData().
 */

export enum WHATS_NEW_FIELDS {
  INTERESTS = 'InterestCollection',
  CAREER_GOALS = 'CareerGoalCollection',
  COURSES = 'CourseCollection',
  OPPORTUNITIES = 'OpportunityCollection',
  STUDENTS = 'StudentProfileCollection',
  FACULTY = 'FacultyProfileCollection',
  ADVISORS = 'AdvisorProfileCollection',
}

export interface WhatsNewData {
  newEntities?: Record<string, any>;
  updatedEntities?: Record<string, any>;
  logins?: number;
  levelUps?: number,
  lastUpdate?: Date;
}

class WhatsNew {
  private newEntities = {};
  private updatedEntities = {};
  private oneWeekAgo = moment().subtract(1, 'weeks');
  private lastUpdate = new Date();
  private logins = 0;
  private levelUps = 0;

  constructor() {
    this.initialize();
    this.updateData();
  }

  private initialize() {
    // Initialize the slug objects with fields whose names are the collection names, and value is an empty array.
    Object.values(WHATS_NEW_FIELDS).forEach(fieldName => {
      this.newEntities[fieldName] = [];
      this.updatedEntities[fieldName] = [];
    });
    this.oneWeekAgo = moment().subtract(1, 'weeks');
  }

  public getData(): WhatsNewData {
    return { newEntities: this.newEntities, updatedEntities: this.updatedEntities, logins: this.logins, levelUps: this.levelUps, lastUpdate: this.lastUpdate };
  }

  public updateData() {
    this.initialize();
    this.lastUpdate = new Date();
    this.updateEntities();
    this.updateUsers();
    this.updateLogins();
    this.updateLevelUps();
  }

  /** Sets the logins field with the number of unique users accessing the system during the past week. */
  private updateLogins() {
    const loginDocs = UserInteractions.find({ type: USER_INTERACTIONS.LOGIN, createdAt: { $gte: this.oneWeekAgo.toDate() } }).fetch();
    this.logins = _.uniq(loginDocs.map(doc => doc.username)).length;
  }

  /** Sets the levelUps field with the number of Level Up events during the past week. OK if one user levels up twice. */
  private updateLevelUps() {
    const levelUpDocs = UserInteractions.find({ type: USER_INTERACTIONS.LEVEL_UP, createdAt: { $gte: this.oneWeekAgo.toDate() } }).fetch();
    this.levelUps = _.uniq(levelUpDocs.map(doc => doc.username)).length;
  }

  /** Sets the newEntities and updateEntities fields for Interests, Career Goals, Opportunities, and Courses. */
  private updateEntities() {
    const entityCollections = [Interests, CareerGoals, Courses, Opportunities];
    entityCollections.forEach(collection => {
      collection.collection.find().fetch().forEach(document => {
        const slug = collection.findSlugByID(document._id);
        this.addIfNew(slug, document, collection);
      });
    });
  }

  /** Sets the newEntities and updateEntities fields for all users except Admin. */
  private updateUsers() {
    const userCollections = [StudentProfiles, FacultyProfiles, AdvisorProfiles];
    userCollections.forEach(collection => {
      collection.collection.find().fetch().forEach(document => {
        const username = document.username;
        this.addIfNew(username, document, collection);
      });
    });
  }

  /** Helper for updateEntities and updateUsers. */
  private addIfNew(name, document, collection) {
    if (document.createdAt && this.oneWeekAgo.isBefore(moment(document.createdAt))) {
      this.newEntities[collection.getCollectionName()].push(name);
    }
    if (document.updatedAt && this.oneWeekAgo.isBefore(moment(document.updatedAt))) {
      this.updatedEntities[collection.getCollectionName()].push(name);
    }
  }
}

// A singleton, storing state regarding what's new during the past week.
export const whatsNew = new WhatsNew();