ilios/frontend

View on GitHub
packages/ilios-common/addon/models/session.js

Summary

Maintainability
C
1 day
Test Coverage
import Model, { hasMany, belongsTo, attr } from '@ember-data/model';
import { DateTime } from 'luxon';
import sortableByPosition from 'ilios-common/utils/sortable-by-position';
import striptags from 'striptags';
import { filter } from 'rsvp';
import { TrackedAsyncData } from 'ember-async-data';
import { cached } from '@glimmer/tracking';
import { mapBy, sortBy, uniqueValues } from 'ilios-common/utils/array-helpers';

export default class SessionModel extends Model {
  @attr('string')
  title;

  @attr('string')
  description;

  @attr('boolean')
  attireRequired;

  @attr('boolean')
  equipmentRequired;

  @attr('boolean')
  supplemental;

  @attr('boolean')
  attendanceRequired;

  @attr('string')
  instructionalNotes;

  @attr('date')
  updatedAt;

  @attr('boolean')
  publishedAsTbd;

  @attr('boolean')
  published;

  @belongsTo('session-type', { async: true, inverse: 'sessions' })
  sessionType;

  @belongsTo('course', { async: true, inverse: 'sessions' })
  course;

  @cached
  get _courseData() {
    return new TrackedAsyncData(this.course);
  }

  @belongsTo('ilm-session', { async: true, inverse: 'session' })
  ilmSession;

  @cached
  get _ilmSessionData() {
    return new TrackedAsyncData(this.ilmSession);
  }

  get _ilmSession() {
    if (!this._ilmSessionData.isResolved) {
      return null;
    }

    return this._ilmSessionData.value;
  }

  @hasMany('session-objective', { async: true, inverse: 'session' })
  sessionObjectives;

  @cached
  get _sessionObjectivesData() {
    return new TrackedAsyncData(this.sessionObjectives);
  }

  @hasMany('mesh-descriptor', { async: true, inverse: 'sessions' })
  meshDescriptors;

  @hasMany('session-learning-material', { async: true, inverse: 'session' })
  learningMaterials;

  @hasMany('offering', { async: true, inverse: 'session' })
  offerings;

  @cached
  get _offeringsData() {
    return new TrackedAsyncData(this.offerings);
  }

  @hasMany('user', {
    async: true,
    inverse: 'administeredSessions',
  })
  administrators;

  @hasMany('user', {
    async: true,
    inverse: 'studentAdvisedSessions',
  })
  studentAdvisors;

  @belongsTo('session', {
    inverse: 'prerequisites',
    async: true,
  })
  postrequisite;

  @cached
  get _postrequisiteData() {
    return new TrackedAsyncData(this.postrequisite);
  }

  @hasMany('session', {
    inverse: 'postrequisite',
    async: true,
  })
  prerequisites;

  @hasMany('term', { async: true, inverse: 'sessions' })
  terms;

  @cached
  get _termsData() {
    return new TrackedAsyncData(this.terms);
  }

  @cached
  get _ilmLearnerGroupsData() {
    if (!this._ilmSessionData.isResolved) {
      return null;
    }
    return new TrackedAsyncData(this._ilmSessionData.value?.learnerGroups);
  }

  @cached
  get _ilmInstructorsData() {
    if (!this._ilmSessionData.isResolved) {
      return null;
    }
    return new TrackedAsyncData(this._ilmSessionData.value.instructors);
  }

  @cached
  get _ilmSessionInstructorGroupsData() {
    if (!this._ilmSessionData.isResolved) {
      return null;
    }
    return new TrackedAsyncData(this._ilmSessionData.value.instructorGroups);
  }

  @cached
  get _ilmSessionInstructorGroupsInstructorsData() {
    if (!this._ilmSessionInstructorGroupsData?.isResolved) {
      return null;
    }
    return new TrackedAsyncData(
      Promise.all(this._ilmSessionInstructorGroupsData.value.map((i) => i.users)),
    );
  }

  @cached
  get _offeringLearnerGroupsData() {
    if (!this._offeringsData.isResolved) {
      return null;
    }
    return new TrackedAsyncData(Promise.all(this._offeringsData.value.map((o) => o.learnerGroups)));
  }

  @cached
  get _offeringInstructorsData() {
    if (!this._offeringsData.isResolved) {
      return null;
    }
    return new TrackedAsyncData(Promise.all(this._offeringsData.value.map((o) => o.instructors)));
  }

  @cached
  get _offeringInstructorGroups() {
    if (!this._offeringsData.isResolved) {
      return null;
    }
    return new TrackedAsyncData(
      Promise.all(this._offeringsData.value.map((o) => o.instructorGroups)),
    );
  }

  @cached
  get _offeringInstructorGroupsInstructors() {
    if (!this._offeringInstructorGroups?.isResolved) {
      return null;
    }
    return new TrackedAsyncData(
      Promise.all(this._offeringInstructorGroups.value.flat().map((o) => o.users)),
    );
  }

  @cached
  get _termVocabularies() {
    if (!this._termsData.isResolved) {
      return null;
    }

    return new TrackedAsyncData(Promise.all(this._termsData.value.map((t) => t.vocabulary)));
  }

  @cached
  get _sessionObjectiveCourseObjectives() {
    if (!this._sessionObjectivesData.isResolved) {
      return null;
    }

    return new TrackedAsyncData(
      Promise.all(this._sessionObjectivesData.value.map((so) => so.courseObjectives)),
    );
  }

  get showUnlinkIcon() {
    if (!this._sessionObjectivesData.isResolved) {
      return false;
    }

    const unlinkedSessionObectives = this._sessionObjectivesData.value.find(
      (so) => so.hasMany('courseObjectives').ids().length === 0,
    );

    return Boolean(unlinkedSessionObectives);
  }

  get learnerGroupCount() {
    return this.associatedOfferingLearnerGroups.length;
  }

  get assignableVocabularies() {
    if (!this._courseData.isResolved) {
      return [];
    }
    return this._courseData.value.assignableVocabularies;
  }

  get xObjectives() {
    return this.sessionObjectives;
  }

  get isIndependentLearning() {
    if (this.belongsTo('ilmSession').id()) {
      return true;
    }

    if (this._ilmSessionData.isResolved) {
      return Boolean(this._ilmSessionData.value);
    }

    return false;
  }

  /**
   * All offerings for this session, sorted by offering start date in ascending order.
   */
  get sortedOfferingsByDate() {
    if (!this._offeringsData.isResolved) {
      return [];
    }
    const filteredOfferings = this._offeringsData.value.filter((offering) => offering.startDate);
    return filteredOfferings.sort((a, b) => {
      const aDate = DateTime.fromJSDate(a.startDate);
      const bDate = DateTime.fromJSDate(b.startDate);
      if (aDate === bDate) {
        return 0;
      }
      return aDate > bDate ? 1 : -1;
    });
  }

  /**
   * The earliest start date of all offerings in this session, or, if this is an ILM session, the ILM's due date.
   */
  get firstOfferingDate() {
    if (this.isIndependentLearning) {
      return this._ilmSessionData.isResolved ? this._ilmSessionData.value.dueDate : undefined;
    }

    if (!this.hasMany('offerings').ids().length) {
      return null;
    }

    return this.sortedOfferingsByDate[0]?.startDate;
  }

  /**
   * The maximum duration in hours (incl. fractions) of any session offerings.
   */
  get maxSingleOfferingDuration() {
    if (!this.hasMany('offerings').ids().length || !this._offeringsData.isResolved) {
      return 0;
    }
    const sortedOfferings = this._offeringsData.value.slice().sort(function (a, b) {
      const diffA = DateTime.fromJSDate(a.endDate).diff(
        DateTime.fromJSDate(a.startDate),
        'minutes',
      ).minutes;
      const diffB = DateTime.fromJSDate(b.endDate).diff(
        DateTime.fromJSDate(b.startDate),
        'minutes',
      ).minutes;
      if (diffA > diffB) {
        return -1;
      } else if (diffA < diffB) {
        return 1;
      }
      return 0;
    });

    const offering = sortedOfferings[0];
    const duration = DateTime.fromJSDate(offering.endDate).diff(
      DateTime.fromJSDate(offering.startDate),
      'hours',
    );
    return duration.hours.toFixed(2);
  }

  /**
   * The total duration in hours (incl. fractions) of all session offerings.
   */
  get totalSumOfferingsDuration() {
    if (!this._offeringsData.isResolved || this._offeringsData.value.length === 0) {
      return 0;
    }

    return this._offeringsData.value
      .reduce((total, offering) => {
        return (
          total +
          DateTime.fromJSDate(offering.endDate).diff(
            DateTime.fromJSDate(offering.startDate),
            'hours',
          ).hours
        );
      }, 0)
      .toFixed(2);
  }

  /**
   * Total duration in hours for offerings and ILM Sessions
   * If both ILM and offerings are present sum them
   */
  get totalSumDuration() {
    if (!this.isIndependentLearning) {
      return this.totalSumOfferingsDuration;
    }

    if (!this._ilmSessionData.isResolved) {
      return 0;
    }

    return (
      parseFloat(this._ilmSessionData.value.hours) + parseFloat(this.totalSumOfferingsDuration)
    );
  }

  /**
   * The maximum duration in hours (incl. fractions) of any session offerings, plus any ILM hours.
   * If both ILM and offerings are present sum them
   */
  get maxDuration() {
    if (!this.isIndependentLearning) {
      return this.maxSingleOfferingDuration;
    }

    if (!this._ilmSessionData.isResolved) {
      return 0;
    }

    return (
      parseFloat(this._ilmSessionData.value.hours) + parseFloat(this.maxSingleOfferingDuration)
    );
  }

  /**
   * A list of all vocabularies that are associated via terms.
   */
  get associatedVocabularies() {
    if (!this._termVocabularies?.isResolved) {
      return [];
    }
    return sortBy(uniqueValues(this._termVocabularies.value), 'title');
  }

  get termCount() {
    return this.hasMany('terms').ids().length;
  }

  get offeringCount() {
    return this.hasMany('offerings').ids().length;
  }

  get objectiveCount() {
    return this.hasMany('sessionObjectives').ids().length;
  }

  get associatedOfferingLearnerGroups() {
    if (!this._offeringLearnerGroupsData?.isResolved) {
      return [];
    }
    return sortBy(uniqueValues(this._offeringLearnerGroupsData.value.flat()), 'title');
  }

  get associatedIlmLearnerGroups() {
    if (!this._ilmLearnerGroupsData?.isResolved) {
      return [];
    }

    return this._ilmLearnerGroupsData.value ?? [];
  }

  get associatedLearnerGroups() {
    if (!this._ilmLearnerGroupsData?.isResolved || !this._offeringLearnerGroupsData?.isResolved) {
      return [];
    }

    const ilmLearnerGroups = this.isIndependentLearning ? this._ilmLearnerGroupsData.value : [];
    return sortBy(
      uniqueValues([...this._offeringLearnerGroupsData.value.flat(), ...ilmLearnerGroups]),
      'title',
    );
  }

  get sortedSessionObjectives() {
    if (!this._sessionObjectivesData.isResolved) {
      return null;
    }
    return this._sessionObjectivesData.value.slice().sort(sortableByPosition);
  }

  get ilmSessionInstructors() {
    if (!this.isIndependentLearning) {
      return [];
    }

    if (!this._ilmSessionInstructors || !this._ilmSessionInstructorGroupInstructors) {
      return [];
    }

    return [...this._ilmSessionInstructors, ...this._ilmSessionInstructorGroupInstructors];
  }

  get allInstructors() {
    if (
      !this._offeringInstructorsData?.isResolved ||
      !this._offeringInstructorGroupsInstructors?.isResolved ||
      (this.isIndependentLearning && !this._ilmInstructorsData?.isResolved) ||
      (this.isIndependentLearning && !this._ilmSessionInstructorGroupsInstructorsData?.isResolved)
    ) {
      return [];
    }

    const ilmInstructors = this.isIndependentLearning ? this._ilmInstructorsData.value : [];
    const ilmInstructorGroupInstructors = this.isIndependentLearning
      ? this._ilmSessionInstructorGroupsInstructorsData.value.flat()
      : [];

    return uniqueValues([
      ...this._offeringInstructorsData.value.flat(),
      ...this._offeringInstructorGroupsInstructors.value.flat(),
      ...ilmInstructors,
      ...ilmInstructorGroupInstructors,
    ]);
  }

  get hasPrerequisites() {
    return this.prerequisites.length > 0;
  }

  get prerequisiteCount() {
    return this.hasMany('prerequisites').ids().length;
  }

  get hasPostrequisite() {
    return !!this.belongsTo('postrequisite')?.id();
  }

  get requiredPublicationIssues() {
    const issues = [];
    if (this.isIndependentLearning) {
      if (!this._ilmSession?.dueDate) {
        issues.push('dueDate');
      }
    } else {
      if (this.offerings.length === 0) {
        issues.push('offerings');
      }
    }
    if (!this.title) {
      issues.push('title');
    }

    return issues;
  }
  get optionalPublicationIssues() {
    const issues = [];
    if (this.terms.length === 0) {
      issues.push('terms');
    }

    if (this.sessionObjectives.length === 0) {
      issues.push('sessionObjectives');
    }

    if (this.meshDescriptors.length === 0) {
      issues.push('meshDescriptors');
    }

    return issues;
  }

  get isPublished() {
    return this.published;
  }

  get isNotPublished() {
    return !this.isPublished;
  }

  get isScheduled() {
    return this.publishedAsTbd;
  }

  get isPublishedOrScheduled() {
    return this.publishedAsTbd || this.isPublished;
  }

  get allPublicationIssuesLength() {
    return this.requiredPublicationIssues.length + this.optionalPublicationIssues.length;
  }

  get textDescription() {
    return striptags(this.description);
  }

  async getAllIlmSessionInstructors() {
    const ilmSession = await this.ilmSession;
    if (!ilmSession) {
      return [];
    }
    return ilmSession.getAllInstructors();
  }

  async getAllOfferingInstructors() {
    const offerings = await this.offerings;
    if (!offerings.length) {
      return [];
    }
    const allOfferingInstructors = await Promise.all(
      offerings.map(async (offering) => {
        return await offering.getAllInstructors();
      }),
    );

    return uniqueValues(allOfferingInstructors.flat());
  }

  async getAllInstructors() {
    const allIlmSessionInstructors = await this.getAllIlmSessionInstructors();
    const allOfferingInstructors = await this.getAllOfferingInstructors();
    return uniqueValues([...allOfferingInstructors, ...allIlmSessionInstructors]);
  }

  async getTotalSumOfferingsDuration() {
    const offerings = await this.offerings;

    if (!offerings.length) {
      return 0;
    }

    return offerings
      .reduce((total, offering) => {
        return (
          total +
          DateTime.fromJSDate(offering.endDate).diff(
            DateTime.fromJSDate(offering.startDate),
            'hours',
          ).hours
        );
      }, 0)
      .toFixed(2);
  }

  async getTotalSumDuration() {
    const ilmSession = await this.ilmSession;
    const totalSumOfferingDuration = await this.getTotalSumOfferingsDuration();
    if (!ilmSession) {
      return totalSumOfferingDuration;
    }
    return parseFloat(ilmSession.hours) + parseFloat(totalSumOfferingDuration);
  }

  async getTotalSumOfferingsDurationByInstructor(user) {
    const offerings = await this.offerings;
    const offeringsWithUser = await filter(offerings, async (offering) => {
      const instructors = await offering.getAllInstructors();
      return mapBy(instructors, 'id').includes(user.id);
    });
    const offeringHours = offeringsWithUser
      .reduce((total, offering) => {
        return (
          total +
          DateTime.fromJSDate(offering.endDate).diff(
            DateTime.fromJSDate(offering.startDate),
            'hours',
          ).hours
        );
      }, 0)
      .toFixed(2);
    return Math.round(offeringHours * 60);
  }

  async getTotalSumIlmDurationByInstructor(user) {
    const ilmSession = await this.ilmSession;
    let ilmMinutes = 0;
    if (ilmSession) {
      const instructors = await ilmSession.getAllInstructors();
      if (mapBy(instructors, 'id').includes(user.id)) {
        ilmMinutes = Math.round(parseFloat(ilmSession.hours) * 60);
      }
    }
    return ilmMinutes;
  }

  async getTotalSumDurationByInstructor(user) {
    const offeringMinutes = await this.getTotalSumOfferingsDurationByInstructor(user);
    const ilmMinutes = await this.getTotalSumIlmDurationByInstructor(user);
    return parseFloat(offeringMinutes) + parseFloat(ilmMinutes);
  }
}