ilios/frontend

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

Summary

Maintainability
A
2 hrs
Test Coverage
import Model, { hasMany, belongsTo, attr } from '@ember-data/model';
import sortableByPosition from 'ilios-common/utils/sortable-by-position';
import { TrackedAsyncData } from 'ember-async-data';
import { cached } from '@glimmer/tracking';
import { map } from 'rsvp';
import { DateTime } from 'luxon';
import { sortBy, uniqueValues } from 'ilios-common/utils/array-helpers';

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

  @attr('number')
  level;

  @attr('number')
  year;

  @attr('date')
  startDate;

  @attr('date')
  endDate;

  @attr('string')
  externalId;

  @attr('boolean')
  locked;

  @attr('boolean')
  archived;

  @attr('boolean')
  publishedAsTbd;

  @attr('boolean')
  published;

  @belongsTo('course-clerkship-type', { async: true, inverse: 'courses' })
  clerkshipType;

  @belongsTo('school', { async: true, inverse: 'courses' })
  school;

  @cached
  get _schoolData() {
    return new TrackedAsyncData(this.school);
  }

  @hasMany('user', {
    async: true,
    inverse: 'directedCourses',
  })
  directors;

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

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

  @hasMany('cohort', { async: true, inverse: 'courses' })
  cohorts;

  @cached
  get _cohortsData() {
    return new TrackedAsyncData(this.cohorts);
  }

  @hasMany('course-objective', { async: true, inverse: 'course' })
  courseObjectives;

  @cached
  get _courseObjectivesData() {
    return new TrackedAsyncData(this.courseObjectives);
  }

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

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

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

  @cached
  get _sessionsData() {
    return new TrackedAsyncData(this.sessions);
  }

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

  @hasMany('course', {
    inverse: 'ancestor',
    async: true,
  })
  descendants;

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

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

  @cached
  get _publishedSessionOfferings() {
    if (!this._sessionsData.isResolved) {
      return null;
    }
    return new TrackedAsyncData(
      Promise.all(this._sessionsData.value.filter((s) => s.isPublished).map((s) => s.offerings)),
    );
  }

  @cached
  get publishedOfferingCount() {
    if (!this._publishedSessionOfferings?.isResolved) {
      return 0;
    }
    return uniqueValues(this._publishedSessionOfferings.value.flat()).length;
  }

  get allTreeCompetencies() {
    if (!this._courseObjectivesData.isResolved) {
      return null;
    }
    return this._courseObjectivesData.value.map((co) => co.treeCompetencies);
  }

  get competencies() {
    if (!this._courseObjectivesData.isResolved) {
      return [];
    }

    const treeCompetencies = this._courseObjectivesData.value.map((o) => o.treeCompetencies);

    return uniqueValues(treeCompetencies.flat()).filter(Boolean);
  }

  @cached
  get _competencyDomains() {
    return new TrackedAsyncData(Promise.all(this.competencies?.map((c) => c.getDomain())));
  }

  @cached
  get _unsortedDomainsWithSubcompetencies() {
    if (!this._competencyDomains.isResolved || this.competencies === null) {
      return null;
    }

    return new TrackedAsyncData(
      map(uniqueValues(this._competencyDomains.value), async (domain) => {
        let subCompetencies = (await domain.children).filter((competency) => {
          return this.competencies.includes(competency);
        });

        subCompetencies = sortBy(subCompetencies, 'title');

        return {
          title: domain.title,
          id: domain.id,
          subCompetencies,
        };
      }),
    );
  }

  get domainsWithSubcompetencies() {
    if (
      !this._unsortedDomainsWithSubcompetencies ||
      !this._unsortedDomainsWithSubcompetencies.isResolved
    ) {
      return [];
    }

    return sortBy(this._unsortedDomainsWithSubcompetencies.value, 'title');
  }

  get requiredPublicationIssues() {
    const issues = [];
    if (!this.startDate) {
      issues.push('startDate');
    }
    if (!this.endDate) {
      issues.push('endDate');
    }

    if (!this.cohorts.length) {
      issues.push('cohorts');
    }

    return issues;
  }

  get optionalPublicationIssues() {
    const issues = [];
    if (!this.terms.length) {
      issues.push('terms');
    }
    if (!this.courseObjectives.length) {
      issues.push('courseObjectives');
    }
    if (!this.meshDescriptors.length) {
      issues.push('meshDescriptors');
    }

    return issues;
  }

  @cached
  get _programSchoolsData() {
    if (!this._cohortsData.isResolved) {
      return null;
    }

    return new TrackedAsyncData(
      map(this._cohortsData.value, async (cohort) => {
        const programYear = await cohort.programYear;
        const program = await programYear.program;
        return program.school;
      }),
    );
  }

  get schools() {
    if (!this._programSchoolsData?.isResolved || !this._schoolData.isResolved) {
      return [];
    }

    return uniqueValues([...this._programSchoolsData.value, this._schoolData.value]);
  }

  @cached
  get _schoolVocabulariesData() {
    return new TrackedAsyncData(Promise.all(this.schools.map((s) => s.vocabularies)));
  }

  get assignableVocabularies() {
    if (!this._schoolVocabulariesData?.isResolved) {
      return [];
    }
    return sortBy(this._schoolVocabulariesData.value.flat(), ['school.title', 'title']);
  }

  /**
   * A list of course objectives, sorted by position (asc) and then id (desc).
   */
  get sortedCourseObjectives() {
    if (!this._courseObjectivesData.isResolved) {
      return null;
    }
    return this._courseObjectivesData.value.slice().sort(sortableByPosition);
  }

  get hasMultipleCohorts() {
    return this.cohorts.length > 1;
  }

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

    return new TrackedAsyncData(Promise.all(this._termsData.value.map((t) => t.vocabulary)));
  }
  /**
   * 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;
  }

  setDatesBasedOnYear() {
    const today = DateTime.now();
    const firstDayOfYear = DateTime.fromObject({
      year: this.year,
      month: 7,
      day: 1,
    });
    const startDate = today < firstDayOfYear ? firstDayOfYear : today;
    const endDate = startDate.plus({ weeks: 8 });
    this.startDate = startDate.toJSDate();
    this.endDate = endDate.toJSDate();
  }

  get xObjectives() {
    return this.courseObjectives;
  }

  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;
  }
}