diff --git a/packages/frontend/app/components/reports/curriculum.gjs b/packages/frontend/app/components/reports/curriculum.gjs index c32e9e6410..ae0fae00a4 100644 --- a/packages/frontend/app/components/reports/curriculum.gjs +++ b/packages/frontend/app/components/reports/curriculum.gjs @@ -6,6 +6,7 @@ import SessionObjectives from './curriculum/session-objectives'; import SessionOfferings from './curriculum/session-offerings'; import LearnerGroups from './curriculum/learner-groups'; import InstructionalTime from './curriculum/instructional-time'; +import TaggedTerms from './curriculum/tagged-terms'; import Header from 'frontend/components/reports/curriculum/header'; import ChooseCourse from 'frontend/components/reports/curriculum/choose-course'; @@ -74,6 +75,8 @@ export default class ReportsCurriculumComponent extends Component { return ensureSafeComponent(LearnerGroups, this); case 'instructionalTime': return ensureSafeComponent(InstructionalTime, this); + case 'taggedTerms': + return ensureSafeComponent(TaggedTerms, this); } return false; diff --git a/packages/frontend/app/components/reports/curriculum/header.gjs b/packages/frontend/app/components/reports/curriculum/header.gjs index b14018fd8b..caee570a0f 100644 --- a/packages/frontend/app/components/reports/curriculum/header.gjs +++ b/packages/frontend/app/components/reports/curriculum/header.gjs @@ -93,6 +93,14 @@ export default class ReportsCurriculumHeader extends Component { schoolCount: this.countSelectedSchools, }), }, + { + value: 'taggedTerms', + label: this.intl.t('general.taggedTerms'), + summary: this.intl.t('general.taggedTermsReportSummaryMultiSchool', { + courseCount: this.args.countSelectedCourses, + schoolCount: this.countSelectedSchools, + }), + }, ]; } else { return [ @@ -124,6 +132,13 @@ export default class ReportsCurriculumHeader extends Component { courseCount: this.args.countSelectedCourses, }), }, + { + value: 'taggedTerms', + label: this.intl.t('general.taggedTerms'), + summary: this.intl.t('general.taggedTermsReportSummary', { + courseCount: this.args.countSelectedCourses, + }), + }, ]; } } diff --git a/packages/frontend/app/components/reports/curriculum/tagged-terms.gjs b/packages/frontend/app/components/reports/curriculum/tagged-terms.gjs new file mode 100644 index 0000000000..ff20c47de4 --- /dev/null +++ b/packages/frontend/app/components/reports/curriculum/tagged-terms.gjs @@ -0,0 +1,252 @@ +import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import PapaParse from 'papaparse'; +import { task, timeout } from 'ember-concurrency'; +import createDownloadFile from 'frontend/utils/create-download-file'; +import { cached, tracked } from '@glimmer/tracking'; +import { TrackedAsyncData } from 'ember-async-data'; +import { chunk } from 'ilios-common/utils/array-helpers'; +import Header from 'frontend/components/reports/curriculum/header'; +import noop from 'ilios-common/helpers/noop'; +import perform from 'ember-concurrency/helpers/perform'; +import add from 'ember-math-helpers/helpers/add'; +import t from 'ember-intl/helpers/t'; +import sortBy from 'ilios-common/helpers/sort-by'; +import { LinkTo } from '@ember/routing'; + +export default class ReportsCurriculumTaggedTermsComponent extends Component { + @service router; + @service intl; + @service store; + @service graphql; + @service reporting; + @tracked finishedBuildingReport = false; + + @cached + get queryPromises() { + const chunks = chunk(this.args.courses, 5); + const sessionData = ['id', 'title', 'terms { id, title }'].join(', '); + + const data = [ + 'id', + 'title', + 'terms { id, title }', + 'school { id, title }', + `sessions { ${sessionData} }`, + ]; + + return chunks.map((courses) => { + const courseIds = courses.map((c) => c.id); + const filters = [`ids: [${courseIds.join(', ')}]`]; + return new TrackedAsyncData(this.graphql.find('courses', filters, data.join(', '))); + }); + } + + get completedPromises() { + return this.queryPromises.filter((tad) => tad.isResolved); + } + + get reportRunning() { + return this.queryPromises.length > this.completedPromises.length; + } + + get reportResults() { + if (this.reportRunning) { + return []; + } + return this.completedPromises + .map(({ value }) => value) + .map(({ data }) => data.courses) + .flat(); + } + + get summary() { + return this.reportResults.map((c) => { + return { + schoolTitle: c.school.title, + courseId: c.id, + courseTitle: c.title, + courseTermsCount: c.terms.length, + sessionCount: c.sessions.length, + sessionTermsCount: c.sessions.reduce((acc, s) => acc + s.terms.length, 0), + }; + }); + } + + get results() { + const origin = window.location.origin; + return this.reportResults.reduce((acc, c) => { + c.sessions.forEach((s) => { + const path = this.router.urlFor('session', c.id, s.id); + s.terms.forEach(() => { + const sessionTerm = { + courseId: c.id, + courseTitle: c.title, + courseTerms: c.terms, + sessionTitle: s.title, + sessionTerms: s.terms, + link: `${origin}${path}`, + }; + + if (this.hasMultipleSchools) { + sessionTerm.schoolTitle = c.school.title; + } + + acc.push(sessionTerm); + }); + }); + return acc; + }, []); + } + + get sortedResults() { + return this.results.sort(this.sortResults); + } + + sortResults = (a, b) => { + if (a.courseTitle !== b.courseTitle) { + return a.courseTitle.localeCompare(b.courseTitle); + } + + return a.sessionTitle.localeCompare(b.sessionTitle); + }; + + get selectedSchoolIds() { + if (!this.args.courses) { + return []; + } + const schools = this.store.peekAll('school'); + let schoolIds = []; + this.args.courses.map((course) => { + const schoolForCourse = schools.find((school) => + school.hasMany('courses').ids().includes(course.id), + ); + + if (schoolForCourse) { + schoolIds = [...schoolIds, schoolForCourse.id]; + } + }); + return [...new Set(schoolIds)]; + } + + get countSelectedSchools() { + return this.selectedSchoolIds.length; + } + + get hasMultipleSchools() { + return this.countSelectedSchools > 1; + } + + get schoolTitlePlaceholder() { + return 'School'; + } + + get sessionCountPlaceholder() { + return '11'; + } + + get courseTermsCountPlaceholder() { + return '11'; + } + + get sessionTermsCountPlaceholder() { + return '84'; + } + + downloadReport = task({ drop: true }, async () => { + const data = this.sortedResults.map((o) => { + const rhett = {}; + + if (this.hasMultipleSchools) { + rhett[this.intl.t('general.school')] = o.schoolTitle; + } + rhett[this.intl.t('general.course')] = o.courseTitle; + rhett[this.intl.t('general.courseTerms')] = o.courseTerms.map((t) => t.title).join(', '); + rhett[this.intl.t('general.session')] = o.sessionTitle; + rhett[this.intl.t('general.sessionTerms')] = o.sessionTerms.map((t) => t.title).join(', '); + rhett[this.intl.t('general.link')] = o.link; + + return rhett; + }); + const csv = PapaParse.unparse(data); + this.finishedBuildingReport = true; + createDownloadFile(`terms.csv`, csv, 'text/csv'); + await timeout(2000); + this.finishedBuildingReport = false; + }); + +} diff --git a/packages/frontend/tests/integration/components/reports/curriculum/header-test.gjs b/packages/frontend/tests/integration/components/reports/curriculum/header-test.gjs index c278ea94c0..3afef9c1c7 100644 --- a/packages/frontend/tests/integration/components/reports/curriculum/header-test.gjs +++ b/packages/frontend/tests/integration/components/reports/curriculum/header-test.gjs @@ -28,7 +28,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector component is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report types selector has correct number of options', ); assert.strictEqual( @@ -67,6 +67,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector fourth option is not chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 3 courses'), 'summary includes correct number of courses', @@ -105,7 +114,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report types selector has correct number of options', ); assert.strictEqual( @@ -144,6 +153,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector fourth option is not chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 2 courses, across 2 schools'), 'summary includes correct number of courses and schools', @@ -174,7 +192,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector component is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report types selector has correct number of options', ); assert.strictEqual( @@ -213,6 +231,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector fourth option is not chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 3 courses'), 'summary includes correct number of courses', @@ -251,7 +278,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report types selector has correct number of options', ); assert.strictEqual( @@ -290,6 +317,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector fourth option is not chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 2 courses, across 2 schools'), 'summary includes correct number of courses and schools', @@ -320,7 +356,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report selector has correct number of options', ); assert.strictEqual( @@ -359,6 +395,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector fourth option is not chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 5 courses'), 'summary includes correct number of courses', @@ -392,7 +437,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report types selector has correct number of options', ); assert.strictEqual( @@ -431,6 +476,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector fourth option is not chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 5 courses, across 3 schools'), 'summary includes correct number of courses and schools', @@ -462,7 +516,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report selector has correct number of options', ); assert.strictEqual( @@ -501,6 +555,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector FOURTH option IS chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 7 courses'), 'summary includes correct number of courses', @@ -535,7 +598,7 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.reportSelector.isPresent, 'report types selector is present'); assert.strictEqual( component.reportSelector.options.length, - 4, + 5, 'report types selector has correct number of options', ); assert.strictEqual( @@ -574,6 +637,15 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { component.reportSelector.options[3].isSelected, 'report types selector FOURTH option IS chosen', ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.notOk( + component.reportSelector.options[4].isSelected, + 'report types selector fifth option is not chosen', + ); assert.ok( component.runSummaryText.includes('for 4 courses, across 4 schools'), 'summary includes correct number of courses and schools', @@ -588,6 +660,167 @@ module('Integration | Component | reports/curriculum/header', function (hooks) { assert.ok(component.copy.isPresent, 'copy report button is present'); }); + test('it renders for tagged terms and is accessible', async function (assert) { + await render( + , + ); + + assert.ok(component.reportSelector.isPresent, 'report types selector is present'); + assert.strictEqual( + component.reportSelector.options.length, + 5, + 'report selector has correct number of options', + ); + assert.strictEqual( + component.reportSelector.options[0].text, + 'Session Objectives', + 'report types selector has correct first option text', + ); + assert.notOk( + component.reportSelector.options[0].isSelected, + 'report types selector first option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[1].text, + 'Session Offerings', + 'report types selector has correct second option text', + ); + assert.notOk( + component.reportSelector.options[1].isSelected, + 'report types selector second option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[2].text, + 'Learner Groups', + 'report types selector has correct third option text', + ); + assert.notOk( + component.reportSelector.options[2].isSelected, + 'report types selector third option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[3].text, + 'Instructional Time', + 'report types selector has correct fourth option text', + ); + assert.notOk( + component.reportSelector.options[3].isSelected, + 'report types selector fourth option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.ok( + component.reportSelector.options[4].isSelected, + 'report types selector FIFTH option IS chosen', + ); + assert.ok( + component.runSummaryText.includes('for 7 courses'), + 'summary includes correct number of courses', + ); + assert.ok( + component.runSummaryText.includes( + 'Each set of attached terms is listed along with course data.', + ), + 'summary description is correct', + ); + assert.ok(component.runReport.isPresent, 'run report button is present'); + assert.ok(component.copy.isPresent, 'copy report button is present'); + await a11yAudit(this.element); + assert.ok(true, 'no a11y errors found!'); + }); + + test('it renders for tagged terms across multiple schools', async function (assert) { + await render( + , + ); + + assert.ok(component.reportSelector.isPresent, 'report types selector is present'); + assert.strictEqual( + component.reportSelector.options.length, + 5, + 'report types selector has correct number of options', + ); + assert.strictEqual( + component.reportSelector.options[0].text, + 'Session Objectives', + 'report types selector has correct first option text', + ); + assert.notOk( + component.reportSelector.options[0].isSelected, + 'report types selector first option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[1].text, + 'Session Offerings', + 'report types selector has correct second option text', + ); + assert.notOk( + component.reportSelector.options[1].isSelected, + 'report types selector second option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[2].text, + 'Learner Groups', + 'report types selector has correct fourth option text', + ); + assert.notOk( + component.reportSelector.options[2].isSelected, + 'report types selector third option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[3].text, + 'Instructional Time', + 'fourth report type option text is correct', + ); + assert.notOk( + component.reportSelector.options[3].isSelected, + 'report types selector fourth option is not chosen', + ); + assert.strictEqual( + component.reportSelector.options[4].text, + 'Tagged Terms', + 'report types selector has correct fifth option text', + ); + assert.ok( + component.reportSelector.options[4].isSelected, + 'report types selector FIFTH option IS chosen', + ); + assert.ok( + component.runSummaryText.includes('for 4 courses, across 4 schools'), + 'summary includes correct number of courses and schools', + ); + assert.ok( + component.runSummaryText.includes( + 'Each set of attached terms is listed along with course data.', + ), + 'summary description is correct', + ); + assert.ok(component.runReport.isPresent, 'run report button is present'); + assert.ok(component.copy.isPresent, 'copy report button is present'); + }); + test('it changes selected report', async function (assert) { this.set('selectedReportValue', 'sessionObjectives'); this.set('changeSelectedReport', (value) => { diff --git a/packages/frontend/tests/integration/components/reports/curriculum/tagged-terms-test.gjs b/packages/frontend/tests/integration/components/reports/curriculum/tagged-terms-test.gjs new file mode 100644 index 0000000000..41334350e1 --- /dev/null +++ b/packages/frontend/tests/integration/components/reports/curriculum/tagged-terms-test.gjs @@ -0,0 +1,134 @@ +import { module, test, skip } from 'qunit'; +import { setupRenderingTest } from 'frontend/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { setupMirage } from 'frontend/tests/test-support/mirage'; +import { component } from 'frontend/tests/pages/components/reports/curriculum/tagged-terms'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { graphQL } from 'frontend/tests/helpers/curriculum-report'; +import TaggedTerms from 'frontend/components/reports/curriculum/tagged-terms'; +import { array } from '@ember/helper'; +import noop from 'ilios-common/helpers/noop'; + +module('Integration | Component | reports/curriculum/tagged-terms', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + const school = this.server.create('school', { title: 'school 0' }); + this.vocabulary = this.server.create('vocabulary'); + + const courseTerm1 = this.server.create('term', { + vocabulary: this.vocabulary, + title: 'course term 1', + }); + const courseTerm2 = this.server.create('term', { + vocabulary: this.vocabulary, + title: 'course term 2', + }); + const sessionTerm1 = this.server.create('term', { + vocabulary: this.vocabulary, + title: 'session term 1', + }); + const sessionTerm2 = this.server.create('term', { + vocabulary: this.vocabulary, + title: 'session term 2', + }); + + this.course = this.server.create('course', { + school, + terms: [courseTerm1, courseTerm2], + }); + const sessionType = this.server.create('sessionType'); + this.session = this.server.create('session', { + course: this.course, + sessionType, + terms: [sessionTerm1, sessionTerm2], + }); + + this.server.post('api/graphql', (schema) => { + //use all the courses, getting the id filter from graphQL is a bit tricky + const courseIds = schema.db.courses.map((c) => c.id); + const rawCourses = courseIds.map((id) => graphQL.fetchCourse(schema.db, id)); + const courses = rawCourses.map((course) => { + course.terms = schema.db.terms + .filter((t) => t.courseIds?.includes(course.id)) + .map(({ id, title }) => { + (id, title); + }); + + course.sessions.forEach((session) => { + session.terms = schema.db.terms + .filter((t) => t.sessionIds?.includes(session.id)) + .map(({ id, title }) => ({ id, title })); + }); + + return course; + }); + + return { data: { courses } }; + }); + }); + + test('it renders and is accessible', async function (assert) { + const courseModels = await this.owner.lookup('service:store').findAll('course'); + this.set('courses', courseModels); + + await render( + , + ); + + assert.strictEqual( + component.header.runSummaryText, + 'Run Tagged Terms report for one course. Each set of attached terms is listed along with course data.', + 'report summary text is correct', + ); + + assert.strictEqual(component.results.length, 1, 'report results count is correct'); + assert.strictEqual( + component.results.objectAt(0).courseTitle, + 'course 0', + 'first report result course title is correct', + ); + assert.strictEqual( + component.results.objectAt(0).courseTermsCount, + '2', + 'first report result course terms count is correct', + ); + assert.strictEqual( + component.results.objectAt(0).sessionCount, + '1', + 'first report result session count is correct', + ); + assert.strictEqual( + component.results.objectAt(0).sessionTermsCount, + '2', + 'first report result session terms count is correct', + ); + + await a11yAudit(this.element); + assert.ok(true, 'no a11y errors found!'); + }); + + skip('download report', async function (assert) { + const courseModels = await this.owner.lookup('service:store').findAll('course'); + this.set('courses', courseModels); + + await render(); + + assert.strictEqual( + component.header.runSummaryText, + 'Run Tagged Terms report for one course. Each set of attached terms is listed along with course data.', + ); + + await component.header.download.click(); + assert.ok(true, 'downloaded report'); + }); +}); diff --git a/packages/frontend/tests/pages/components/reports/curriculum/tagged-terms.js b/packages/frontend/tests/pages/components/reports/curriculum/tagged-terms.js new file mode 100644 index 0000000000..ce12f2caeb --- /dev/null +++ b/packages/frontend/tests/pages/components/reports/curriculum/tagged-terms.js @@ -0,0 +1,23 @@ +import { create, collection, text } from 'ember-cli-page-object'; + +import header from './header'; + +const definition = { + header, + results: collection('[data-test-report-results] [data-test-result]', { + courseTitle: text('td', { at: 0 }), + courseTermsCount: text('td', { at: 1 }), + sessionCount: text('td', { at: 2 }), + sessionTermsCount: text('td', { at: 3 }), + }), + resultsMultiSchool: collection('[data-test-report-results] [data-test-result]', { + schoolTitle: text('td', { at: 0 }), + courseTitle: text('td', { at: 1 }), + courseTermsCount: text('td', { at: 2 }), + sessionCount: text('td', { at: 3 }), + sessionTermsCount: text('td', { at: 4 }), + }), +}; + +export default definition; +export const component = create(definition); diff --git a/packages/frontend/translations/en-us.yaml b/packages/frontend/translations/en-us.yaml index 172b760307..0505b4538c 100644 --- a/packages/frontend/translations/en-us.yaml +++ b/packages/frontend/translations/en-us.yaml @@ -90,6 +90,7 @@ general: country: Country countryCode: Country Code courseDirector: Course Director + courseTerms: Course Terms courseTitleFilterPlaceholder: Filter by course title create: Create New User createBulk: Upload Multiple Users @@ -408,6 +409,7 @@ general: sessionOfferings: Session Offerings sessionOfferingsReportSummary: "report for {courseCount, plural, one {one course} other {# courses}}. Each session offering is listed along with instructors, learner groups, and course data." sessionOfferingsReportSummaryMultiSchool: "report for {courseCount, plural, one {one course} other {# courses}}{schoolCount, plural, zero {} one {} other {, across # schools}}. Each session offering is listed along with instructors, learner groups, and course data." + sessionTerms: Session Terms sessionTitlePlaceholder: Enter a title for this session sessionTypeConfirmRemoval: Are you sure you want to delete this session type? This action cannot be undone. sessionTypeTitlePlaceholder: Enter a title for this session type @@ -445,6 +447,9 @@ general: table6ClerkshipSequenceBlockAssessmentMethods: "Table 6: Clerkship Sequence Block Assessment Methods" table7AllEventsWithAssessmentsTaggedAsFormativeOrSummative: "Table 7: All Events with Assessments Tagged as Formative or Summative" table8AllResourceTypes: "Table 8: All Resource Types" + taggedTerms: Tagged Terms + taggedTermsReportSummary: "report for {courseCount, plural, one {one course} other {# courses}}. Each set of attached terms is listed along with course data." + taggedTermsReportSummaryMultiSchool: "report for {courseCount, plural, one {one course} other {# courses}}{schoolCount, plural, zero {} one {} other {, across # schools}}. Each set of attached terms is listed along with course data." term: Term termsBySessionType: Terms by Session Type termXappliedToYSessionsWithSessionTypeZ: 'The term "{term}" from the "{vocabulary}" vocabulary is applied to {sessionsCount, plural, =1 {1 session} other {# sessions}} with session-type "{sessionType}".' diff --git a/packages/frontend/translations/es.yaml b/packages/frontend/translations/es.yaml index 07dcab1f2c..ac4eb1fec5 100644 --- a/packages/frontend/translations/es.yaml +++ b/packages/frontend/translations/es.yaml @@ -90,6 +90,7 @@ general: country: País countryCode: Código del país courseDirector: Director del Curso + courseTerms: Términos del Curso courseTitleFilterPlaceholder: Aplicar un filtro por título de curso create: Crear Nuevo Usuario createBulk: Subir Múltiples Usuarios. @@ -408,6 +409,7 @@ general: sessionOfferings: Ofrecimientos de la Sesión sessionOfferingsReportSummary: "informe para {courseCount, plural, one {un curso} other {# cursos}}. Se enumera el ofrecimiento de cada sesión junto con los instructores, los grupos de aprendedores, y los datos del curso." sessionOfferingsReportSummaryMultiSchool: "informe para {courseCount, plural, one {un curso} other {# cursos}}{schoolCount, plural, zero {} one {} other {, en # escuelas}}. Se enumera el ofrecimiento de cada sesión junto con los instructores, los grupos de aprendedores, y los datos del curso." + sessionTerms: Términos de Curso sessionTitlePlaceholder: Entre en un titulo para esta sesión sessionTypeConfirmRemoval: ¿Está seguro de que desea eliminar este tipo de sesión? Esta acción no se puede deshacer. sessionTypeTitlePlaceholder: Entre en un titulo para este tipo de sesión @@ -445,6 +447,9 @@ general: table6ClerkshipSequenceBlockAssessmentMethods: "Tabla 6: Métodos de evaluación del bloque de secuencia de la rotación" table7AllEventsWithAssessmentsTaggedAsFormativeOrSummative: "Tabla 7: Todos los eventos con evaluaciones etiquetadas como formativas o sumativas" table8AllResourceTypes: "Tabla 8: Todos los tipos de recursos" + taggedTerms: Términos Etiquetados + taggedTermsReportSummary: "informe para {courseCount, plural, one {un curso} other {# cursos}}. Se enumera cada conjunto de términos junto los datos del curso." + taggedTermsReportSummaryMultiSchool: "report for {courseCount, plural, one {un curso} other {# cursos}}{schoolCount, plural, zero {} one {} other {, en # escuelas}}. Se enumera cada conjunto de términos junto los datos del curso." term: Término termsBySessionType: Términos por Tipo de Sesión termXappliedToYSessionsWithSessionTypeZ: 'El término "{term}" del vocabulario "{vocabulary}" se aplica a {sessionsCount, plural, =1 {1 sesión} other {# sesiones}} con el tipo de sesión "{sessionType}".' diff --git a/packages/frontend/translations/fr.yaml b/packages/frontend/translations/fr.yaml index 07ddec68c4..e88aab4d77 100644 --- a/packages/frontend/translations/fr.yaml +++ b/packages/frontend/translations/fr.yaml @@ -90,6 +90,7 @@ general: country: Pays countryCode: Code du pays courseDirector: Directeur de Cours + courseTerms: Termes de Cours courseTitleFilterPlaceholder: Filtre par titre de cours create: Créez un nouvel utilisateur createBulk: Envoyez utilisateurs multiples @@ -409,6 +410,7 @@ general: sessionOfferings: Offres de séance sessionOfferingsReportSummary: "Rapport de {courseCount, plural, one {one course} other {# courses}}. Chaque offre de séance est listée avec des instructeurs, groupes d'étudiants, et cours données." sessionOfferingsReportSummaryMultiSchool: "Rapport de {courseCount, plural, one {one course} other {# courses}}{schoolCount, plural, zero {} one {} other {, dans # écoles}}. Chaque offre de séance est listée avec des instructeurs, groupes d'étudiants, et cours données." + sessionTerms: Termes de Séance sessionTitlePlaceholder: Ajoutez un titre pour ce session sessionTypeConfirmRemoval: "Voulez-vous vraiment supprimer ce type de session ? Cette action ne peut pas être annulée." sessionTypeTitlePlaceholder: Ajoutez un titre pour ce type de session @@ -446,7 +448,10 @@ general: table6ClerkshipSequenceBlockAssessmentMethods: "Tableau 6: Méthodes d’évaluation du bloc de séquences stage" table7AllEventsWithAssessmentsTaggedAsFormativeOrSummative: "Tableau 7: Tous les événements avec des évaluations marquées en tant que formatif ou sommatif" table8AllResourceTypes: "Tableau 8: Tous les types de ressources" - term: Terme + taggedTerms: Termes Marqueés + taggedTermsReportSummary: "rapport de {courseCount, plural, one {un cours} other {# cours}}. Chaque ensemble de termes est listée avec cours données." + taggedTermsReportSummaryMultiSchool: "rapport de {courseCount, plural, one {un cours} other {# cours}}{schoolCount, plural, zero {} one {} other {, dans # écoles}}. Chaque ensemble de termes est listée avec cours données." + term: Termes Étiq termsBySessionType: Termes par Type de Seance termXappliedToYSessionsWithSessionTypeZ: 'Le terme "{term}" du vocabulaire "{vocabulary}" s''applique à {sessionsCount, plural, =1 {1 session} other {# sessions}} avec le type de session "{sessionType}".' thereAreXTerms: "{count, plural, =1 {Il y a 1 terme} other {Il y a # termes}}"