diff --git a/spec/coffeescripts/collections/OutcomeResultCollectionSpec.js b/spec/coffeescripts/collections/OutcomeResultCollectionSpec.js index 153c08e3534..daa1c8890b6 100644 --- a/spec/coffeescripts/collections/OutcomeResultCollectionSpec.js +++ b/spec/coffeescripts/collections/OutcomeResultCollectionSpec.js @@ -18,7 +18,7 @@ import Backbone from '@canvas/backbone' import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' -import OutcomeResultCollection from 'ui/features/grade_summary/backbone/collections/OutcomeResultCollection.coffee' +import OutcomeResultCollection from 'ui/features/grade_summary/backbone/collections/OutcomeResultCollection' import fakeENV from 'helpers/fakeENV' import tz from '@canvas/timezone' @@ -96,6 +96,7 @@ QUnit.module('OutcomeResultCollectionSpec', { }) test('default params reflect aligned outcome', function () { + // eslint-disable-next-line new-cap const collectionModel = new this.outcomeResultCollection.model() deepEqual(collectionModel.get('mastery_points'), 8) deepEqual(collectionModel.get('points_possible'), 10) diff --git a/spec/coffeescripts/views/grade_summary/OutcomeDetailViewSpec.js b/spec/coffeescripts/views/grade_summary/OutcomeDetailViewSpec.js index 78136b9ca17..e85e6706052 100644 --- a/spec/coffeescripts/views/grade_summary/OutcomeDetailViewSpec.js +++ b/spec/coffeescripts/views/grade_summary/OutcomeDetailViewSpec.js @@ -18,9 +18,9 @@ import Backbone from '@canvas/backbone' import CollectionView from '@canvas/backbone-collection-view' -import OutcomeResultCollection from 'ui/features/grade_summary/backbone/collections/OutcomeResultCollection.coffee' +import OutcomeResultCollection from 'ui/features/grade_summary/backbone/collections/OutcomeResultCollection' import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' -import Group from 'ui/features/grade_summary/backbone/models/Group.coffee' +import Group from 'ui/features/grade_summary/backbone/models/Group' import OutcomeDetailView from 'ui/features/grade_summary/backbone/views/OutcomeDetailView' import fakeENV from 'helpers/fakeENV' diff --git a/spec/coffeescripts/views/grade_summary/OutcomeDialogViewSpec.js b/spec/coffeescripts/views/grade_summary/OutcomeDialogViewSpec.js index db2895995f4..504f8574a03 100644 --- a/spec/coffeescripts/views/grade_summary/OutcomeDialogViewSpec.js +++ b/spec/coffeescripts/views/grade_summary/OutcomeDialogViewSpec.js @@ -16,10 +16,11 @@ * with this program. If not, see . */ +import $ from 'jquery' import _ from 'underscore' import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' -import OutcomeDialogView from 'ui/features/grade_summary/backbone/views/OutcomeDialogView.coffee' -import OutcomeLineGraphView from 'ui/features/grade_summary/backbone/views/OutcomeLineGraphView.coffee' +import OutcomeDialogView from 'ui/features/grade_summary/backbone/views/OutcomeDialogView' +import OutcomeLineGraphView from 'ui/features/grade_summary/backbone/views/OutcomeLineGraphView' QUnit.module('OutcomeDialogViewSpec', { setup() { diff --git a/spec/coffeescripts/views/grade_summary/OutcomeLineGraphViewSpec.js b/spec/coffeescripts/views/grade_summary/OutcomeLineGraphViewSpec.js index 62082a2c488..c381870cb17 100644 --- a/spec/coffeescripts/views/grade_summary/OutcomeLineGraphViewSpec.js +++ b/spec/coffeescripts/views/grade_summary/OutcomeLineGraphViewSpec.js @@ -16,10 +16,11 @@ * with this program. If not, see . */ +import $ from 'jquery' import {isUndefined} from 'lodash' import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' -import OutcomeResultCollection from 'ui/features/grade_summary/backbone/collections/OutcomeResultCollection.coffee' -import OutcomeLineGraphView from 'ui/features/grade_summary/backbone/views/OutcomeLineGraphView.coffee' +import OutcomeResultCollection from 'ui/features/grade_summary/backbone/collections/OutcomeResultCollection' +import OutcomeLineGraphView from 'ui/features/grade_summary/backbone/views/OutcomeLineGraphView' import tz from '@canvas/timezone' import fakeENV from 'helpers/fakeENV' diff --git a/spec/coffeescripts/views/grade_summary/OutcomePopoverViewSpec.js b/spec/coffeescripts/views/grade_summary/OutcomePopoverViewSpec.js index 71451061e44..5b1a89a4777 100644 --- a/spec/coffeescripts/views/grade_summary/OutcomePopoverViewSpec.js +++ b/spec/coffeescripts/views/grade_summary/OutcomePopoverViewSpec.js @@ -20,7 +20,7 @@ import $ from 'jquery' import {isUndefined} from 'lodash' import Popover from 'jquery-popover' import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' -import OutcomePopoverView from 'ui/features/grade_summary/backbone/views/OutcomePopoverView.coffee' +import OutcomePopoverView from 'ui/features/grade_summary/backbone/views/OutcomePopoverView' import template from '@canvas/outcomes/jst/outcomePopover.handlebars' QUnit.module('OutcomePopoverViewSpec', { @@ -42,7 +42,7 @@ QUnit.module('OutcomePopoverViewSpec', { }) test('closePopover', function () { - ok(isUndefined(this.popoverView.popover, 'precondition')) + ok(isUndefined(this.popoverView.popover), 'precondition') ok(this.popoverView.closePopover()) this.popoverView.popover = new Popover(this.e('mouseleave'), this.popoverView.render(), { verticalSide: 'bottom', diff --git a/spec/coffeescripts/views/grade_summary/OutcomeViewSpec.js b/spec/coffeescripts/views/grade_summary/OutcomeViewSpec.js index 40e9ee9764e..7a0a8cefb54 100644 --- a/spec/coffeescripts/views/grade_summary/OutcomeViewSpec.js +++ b/spec/coffeescripts/views/grade_summary/OutcomeViewSpec.js @@ -16,12 +16,13 @@ * with this program. If not, see . */ +import $ from 'jquery' import {isUndefined} from 'lodash' import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' -import OutcomePopoverView from 'ui/features/grade_summary/backbone/views/OutcomePopoverView.coffee' -import OutcomeDialogView from 'ui/features/grade_summary/backbone/views/OutcomeDialogView.coffee' -import OutcomeView from 'ui/features/grade_summary/backbone/views/OutcomeView.coffee' -import ProgressBarView from 'ui/features/grade_summary/backbone/views/ProgressBarView.coffee' +import OutcomePopoverView from 'ui/features/grade_summary/backbone/views/OutcomePopoverView' +import OutcomeDialogView from 'ui/features/grade_summary/backbone/views/OutcomeDialogView' +import OutcomeView from 'ui/features/grade_summary/backbone/views/OutcomeView' +import ProgressBarView from 'ui/features/grade_summary/backbone/views/ProgressBarView' import assertions from 'helpers/assertions' QUnit.module('OutcomeViewSpec', { @@ -36,6 +37,7 @@ QUnit.module('OutcomeViewSpec', { }, }) +// eslint-disable-next-line qunit/resolve-async test('should be accessible', function (assert) { const done = assert.async() assertions.isAccessible(this.outcomeView, done, {a11yReport: true}) @@ -46,7 +48,7 @@ test('assign instance of ProgressBarView on init', function () { }) test('have after render behavior', function () { - ok(isUndefined(this.outcomeView.popover, 'precondition')) + ok(isUndefined(this.outcomeView.popover), 'precondition') this.outcomeView.render() ok(this.outcomeView.popover instanceof OutcomePopoverView) ok(this.outcomeView.dialog instanceof OutcomeDialogView) diff --git a/ui/features/grade_summary/backbone/collections/OutcomeResultCollection.coffee b/ui/features/grade_summary/backbone/collections/OutcomeResultCollection.coffee deleted file mode 100644 index cd9519e539d..00000000000 --- a/ui/features/grade_summary/backbone/collections/OutcomeResultCollection.coffee +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright (C) 2015 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import Backbone from '@canvas/backbone' -import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' -import WrappedCollection from './WrappedCollection.coffee' - -export default class OutcomeResultCollection extends WrappedCollection - key: 'outcome_results' - model: Outcome - @optionProperty 'outcome' - url: -> "/api/v1/courses/#{@course_id}/outcome_results?user_ids[]=#{@user_id}&outcome_ids[]=#{@outcome.id}&include[]=alignments&per_page=100" - loadAll: true - - comparator: (model) -> - return -1 * model.get('submitted_or_assessed_at').getTime() - - initialize: -> - super - @model = Outcome.extend defaults: { - points_possible: @outcome.get('points_possible'), - mastery_points: @outcome.get('mastery_points') - } - @course_id = ENV.context_asset_string?.replace('course_', '') - @user_id = ENV.student_id - @on('reset', @handleReset) - @on('add', @handleAdd) - - handleReset: => - @each @handleAdd - - handleAdd: (model) => - alignment_id = model.get('links').alignment - model.set('alignment_name', @alignments.get(alignment_id)?.get('name')) - if model.get('points_possible') > 0 - model.set('score', model.get('points_possible') * model.get('percent')) - else - model.set('score', model.get('mastery_points') * model.get('percent')) - - parse: (response) -> - @alignments ?= new Backbone.Collection([]) - @alignments.add(response?.linked?.alignments || []) - response[@key] diff --git a/ui/features/grade_summary/backbone/collections/OutcomeResultCollection.js b/ui/features/grade_summary/backbone/collections/OutcomeResultCollection.js new file mode 100644 index 00000000000..81fa98b5fa9 --- /dev/null +++ b/ui/features/grade_summary/backbone/collections/OutcomeResultCollection.js @@ -0,0 +1,74 @@ +// +// Copyright (C) 2015 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +import Backbone from '@canvas/backbone' +import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' +import WrappedCollection from './WrappedCollection' + +class OutcomeResultCollection extends WrappedCollection { + constructor(...args) { + super(...args) + this.handleReset = this.handleReset.bind(this) + this.handleAdd = this.handleAdd.bind(this) + } + + url = () => + `/api/v1/courses/${this.course_id}/outcome_results?user_ids[]=${this.user_id}&outcome_ids[]=${this.outcome.id}&include[]=alignments&per_page=100` + + comparator = model => -1 * model.get('submitted_or_assessed_at').getTime() + + initialize() { + super.initialize(...arguments) + this.model = Outcome.extend({ + defaults: { + points_possible: this.outcome.get('points_possible'), + mastery_points: this.outcome.get('mastery_points'), + }, + }) + this.course_id = ENV.context_asset_string?.replace('course_', '') + this.user_id = ENV.student_id + this.on('reset', this.handleReset) + this.on('add', this.handleAdd) + } + + handleReset = () => this.each(this.handleAdd) + + handleAdd(model) { + const alignment_id = model.get('links').alignment + model.set('alignment_name', this.alignments.get(alignment_id)?.get('name')) + if (model.get('points_possible') > 0) { + model.set('score', model.get('points_possible') * model.get('percent')) + } else { + model.set('score', model.get('mastery_points') * model.get('percent')) + } + } + + parse(response) { + if (this.alignments === null || typeof this.alignments === 'undefined') { + this.alignments = new Backbone.Collection([]) + } + this.alignments.add(response?.linked?.alignments || []) + return response[this.key] + } +} + +OutcomeResultCollection.prototype.key = 'outcome_results' +OutcomeResultCollection.prototype.model = Outcome +OutcomeResultCollection.optionProperty('outcome') +OutcomeResultCollection.prototype.loadAll = true + +export default OutcomeResultCollection diff --git a/ui/features/grade_summary/backbone/collections/OutcomeSummaryCollection.js b/ui/features/grade_summary/backbone/collections/OutcomeSummaryCollection.js index b0af501e0a5..56faa9bf0ef 100644 --- a/ui/features/grade_summary/backbone/collections/OutcomeSummaryCollection.js +++ b/ui/features/grade_summary/backbone/collections/OutcomeSummaryCollection.js @@ -17,13 +17,12 @@ import $ from 'jquery' -import _ from 'underscore' import {Collection} from '@canvas/backbone' -import Section from '../models/Section.coffee' -import Group from '../models/Group.coffee' +import Section from '../models/Section' +import Group from '../models/Group' import Outcome from '@canvas/grade-summary/backbone/models/Outcome.coffee' import PaginatedCollection from '@canvas/pagination/backbone/collections/PaginatedCollection.coffee' -import WrappedCollection from './WrappedCollection.coffee' +import WrappedCollection from './WrappedCollection' import natcompare from '@canvas/util/natcompare' class GroupCollection extends PaginatedCollection { @@ -63,7 +62,7 @@ export default class OutcomeSummaryCollection extends Collection { fetch = () => { const dfd = $.Deferred() - const requests = _.values(this.rawCollections).map(collection => { + const requests = Object.values(this.rawCollections).map(collection => { collection.loadAll = true return collection.fetch() }) @@ -73,8 +72,7 @@ export default class OutcomeSummaryCollection extends Collection { rollups() { const studentRollups = this.rawCollections.rollups.at(0).get('scores') - const pairs = studentRollups.map(x => [x.links.outcome, x]) - return _.object(pairs) + return Object.fromEntries(studentRollups.map(x => [x.links.outcome, x])) } populateGroupOutcomes() { diff --git a/ui/features/grade_summary/backbone/collections/WrappedCollection.coffee b/ui/features/grade_summary/backbone/collections/WrappedCollection.coffee deleted file mode 100644 index 43cc1e0117e..00000000000 --- a/ui/features/grade_summary/backbone/collections/WrappedCollection.coffee +++ /dev/null @@ -1,25 +0,0 @@ -# -# Copyright (C) 2014 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import PaginatedCollection from '@canvas/pagination/backbone/collections/PaginatedCollection.coffee' - -export default class WrappedCollection extends PaginatedCollection - @optionProperty 'key' - - parse: (response) -> - @linked = response.linked - response[@key] diff --git a/ui/features/grade_summary/sum.js b/ui/features/grade_summary/backbone/collections/WrappedCollection.js similarity index 62% rename from ui/features/grade_summary/sum.js rename to ui/features/grade_summary/backbone/collections/WrappedCollection.js index 1ebfe1d92d7..1aef3ef07a1 100644 --- a/ui/features/grade_summary/sum.js +++ b/ui/features/grade_summary/backbone/collections/WrappedCollection.js @@ -1,5 +1,5 @@ // -// Copyright (C) 2015 - present Instructure, Inc. +// Copyright (C) 2014 - present Instructure, Inc. // // This file is part of Canvas. // @@ -15,19 +15,15 @@ // You should have received a copy of the GNU Affero General Public License along // with this program. If not, see . -// Adds _.sum method. -// -// Use like: -// -// _.sum([2,3,4]) #=> 9 -// -// or with a custom accessor: -// -// _.sum([[2,3], [3,4]], (a) -> a[0]) #=> 5 -import _ from 'underscore' +import PaginatedCollection from '@canvas/pagination/backbone/collections/PaginatedCollection.coffee' -export default _.mixin({ - sum(array, accessor = null, start = 0) { - return _.reduce(array, (memo, el) => (accessor != null ? accessor(el) : el) + memo, start) - }, -}) +class WrappedCollection extends PaginatedCollection { + parse(response) { + this.linked = response.linked + return response[this.key] + } +} + +WrappedCollection.optionProperty('key') + +export default WrappedCollection diff --git a/ui/features/grade_summary/backbone/models/Group.coffee b/ui/features/grade_summary/backbone/models/Group.coffee deleted file mode 100644 index 07635ffc2dc..00000000000 --- a/ui/features/grade_summary/backbone/models/Group.coffee +++ /dev/null @@ -1,62 +0,0 @@ -# -# Copyright (C) 2014 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import _ from 'underscore' -import {Model, Collection} from '@canvas/backbone' -import natcompare from '@canvas/util/natcompare' - -export default class Group extends Model - initialize: -> - @set('outcomes', new Collection([], comparator: natcompare.byGet('friendly_name'))) - - count: -> @get('outcomes').length - - - statusCount: (status) -> - @get('outcomes').filter((x) -> - x.status() == status - ).length - - mastery_count: -> - @statusCount('mastery') + @statusCount('exceeds') - - remedialCount: -> - @statusCount('remedial') - - undefinedCount: -> - @statusCount('undefined') - - status: -> - if @remedialCount() > 0 - "remedial" - else - if @mastery_count() == @count() - "mastery" - else if @undefinedCount() == @count() - "undefined" - else - "near" - - started: -> - true - - toJSON: -> - _.extend super, - count: @count() - mastery_count: @mastery_count() - started: @started() - status: @status() diff --git a/ui/features/grade_summary/backbone/models/Group.js b/ui/features/grade_summary/backbone/models/Group.js new file mode 100644 index 00000000000..b26b3ebdae0 --- /dev/null +++ b/ui/features/grade_summary/backbone/models/Group.js @@ -0,0 +1,71 @@ +// +// Copyright (C) 2014 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +import {Model, Collection} from '@canvas/backbone' +import natcompare from '@canvas/util/natcompare' + +export default class Group extends Model { + initialize() { + return this.set('outcomes', new Collection([], {comparator: natcompare.byGet('friendly_name')})) + } + + count() { + return this.get('outcomes').length + } + + statusCount(status) { + return this.get('outcomes').filter(x => x.status() === status).length + } + + mastery_count() { + return this.statusCount('mastery') + this.statusCount('exceeds') + } + + remedialCount() { + return this.statusCount('remedial') + } + + undefinedCount() { + return this.statusCount('undefined') + } + + status() { + if (this.remedialCount() > 0) { + return 'remedial' + } else if (this.mastery_count() === this.count()) { + return 'mastery' + } else if (this.undefinedCount() === this.count()) { + return 'undefined' + } else { + return 'near' + } + } + + started() { + return true + } + + toJSON() { + return { + ...super.toJSON(...arguments), + count: this.count(), + mastery_count: this.mastery_count(), + started: this.started(), + status: this.status(), + } + } +} diff --git a/ui/features/grade_summary/backbone/models/Section.coffee b/ui/features/grade_summary/backbone/models/Section.coffee deleted file mode 100644 index a29fdfb197e..00000000000 --- a/ui/features/grade_summary/backbone/models/Section.coffee +++ /dev/null @@ -1,23 +0,0 @@ -# -# Copyright (C) 2014 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import {Model, Collection} from '@canvas/backbone' -import natcompare from '@canvas/util/natcompare' - -export default class Section extends Model - initialize: -> - @set('groups', new Collection([], comparator: natcompare.byGet('title'))) diff --git a/ui/features/grade_summary/backbone/models/Section.js b/ui/features/grade_summary/backbone/models/Section.js new file mode 100644 index 00000000000..f228671e222 --- /dev/null +++ b/ui/features/grade_summary/backbone/models/Section.js @@ -0,0 +1,25 @@ +// +// Copyright (C) 2014 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +import {Model, Collection} from '@canvas/backbone' +import natcompare from '@canvas/util/natcompare' + +export default class Section extends Model { + initialize() { + return this.set('groups', new Collection([], {comparator: natcompare.byGet('title')})) + } +} diff --git a/ui/features/grade_summary/backbone/views/AlignmentView.js b/ui/features/grade_summary/backbone/views/AlignmentView.js index 41ecef7d03b..dd2291c7376 100644 --- a/ui/features/grade_summary/backbone/views/AlignmentView.js +++ b/ui/features/grade_summary/backbone/views/AlignmentView.js @@ -16,7 +16,7 @@ // with this program. If not, see . import Backbone from '@canvas/backbone' -import ProgressBarView from './ProgressBarView.coffee' +import ProgressBarView from './ProgressBarView' import template from '../../jst/alignment.handlebars' export default class AlignmentView extends Backbone.View { diff --git a/ui/features/grade_summary/backbone/views/GroupView.coffee b/ui/features/grade_summary/backbone/views/GroupView.coffee deleted file mode 100644 index 01963dd70a8..00000000000 --- a/ui/features/grade_summary/backbone/views/GroupView.coffee +++ /dev/null @@ -1,82 +0,0 @@ -# -# Copyright (C) 2014 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import {useScope as useI18nScope} from '@canvas/i18n' -import {View, Collection} from '@canvas/backbone' -import _ from 'underscore' -import CollectionView from '@canvas/backbone-collection-view' -import OutcomeView from './OutcomeView.coffee' -import template from '../../jst/group.handlebars' - -I18n = useI18nScope('grade_summaryGroupView') - -export default class GroupView extends View - tagName: 'li' - className: 'group' - - els: - '.outcomes': '$outcomes' - - events: - 'click .group-description': 'expand' - 'keyclick .group-description': 'expand' - - template: template - - render: -> - super - outcomesView = new CollectionView - el: @$outcomes - collection: @model.get('outcomes') - itemView: OutcomeView - outcomesView.render() - - expand: -> - @$el.toggleClass('expanded') - if @$el.hasClass("expanded") - @$el.children("div.group-description").attr("aria-expanded", "true") - else - @$el.children("div.group-description").attr("aria-expanded", "false") - - $collapseToggle = $('div.outcome-toggles a.icon-collapse') - if $('li.group.expanded').length == 0 - $collapseToggle.attr('disabled', 'disabled') - $collapseToggle.attr('aria-disabled', 'true') - else - $collapseToggle.removeAttr('disabled') - $collapseToggle.attr('aria-disabled', 'false') - - $expandToggle = $('div.outcome-toggles a.icon-expand') - if $('li.group:not(.expanded)').length == 0 - $expandToggle.attr('disabled', 'disabled') - $expandToggle.attr('aria-disabled', 'true') - else - $expandToggle.removeAttr('disabled') - $expandToggle.attr('aria-disabled', 'false') - - statusTooltip: -> - switch @model.status() - when 'undefined' then I18n.t('Unstarted') - when 'remedial' then I18n.t('Well Below Mastery') - when 'near' then I18n.t('Near Mastery') - when 'mastery' then I18n.t('Meets Mastery') - when 'exceeds' then I18n.t('Exceeds Mastery') - - toJSON: -> - json = super - _.extend json, - statusTooltip: @statusTooltip() diff --git a/ui/features/grade_summary/backbone/views/GroupView.js b/ui/features/grade_summary/backbone/views/GroupView.js new file mode 100644 index 00000000000..b0df833ed22 --- /dev/null +++ b/ui/features/grade_summary/backbone/views/GroupView.js @@ -0,0 +1,97 @@ +// +// Copyright (C) 2014 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +import {useScope as useI18nScope} from '@canvas/i18n' +import $ from 'jquery' +import {View} from '@canvas/backbone' +import CollectionView from '@canvas/backbone-collection-view' +import OutcomeView from './OutcomeView' +import template from '../../jst/group.handlebars' + +const I18n = useI18nScope('grade_summaryGroupView') + +class GroupView extends View { + render() { + super.render(...arguments) + const outcomesView = new CollectionView({ + el: this.$outcomes, + collection: this.model.get('outcomes'), + itemView: OutcomeView, + }) + return outcomesView.render() + } + + expand() { + this.$el.toggleClass('expanded') + if (this.$el.hasClass('expanded')) { + this.$el.children('div.group-description').attr('aria-expanded', 'true') + } else { + this.$el.children('div.group-description').attr('aria-expanded', 'false') + } + + const $collapseToggle = $('div.outcome-toggles a.icon-collapse') + if ($('li.group.expanded').length === 0) { + $collapseToggle.attr('disabled', 'disabled') + $collapseToggle.attr('aria-disabled', 'true') + } else { + $collapseToggle.removeAttr('disabled') + $collapseToggle.attr('aria-disabled', 'false') + } + + const $expandToggle = $('div.outcome-toggles a.icon-expand') + if ($('li.group:not(.expanded)').length === 0) { + $expandToggle.attr('disabled', 'disabled') + return $expandToggle.attr('aria-disabled', 'true') + } else { + $expandToggle.removeAttr('disabled') + return $expandToggle.attr('aria-disabled', 'false') + } + } + + statusTooltip() { + switch (this.model.status()) { + case 'undefined': + return I18n.t('Unstarted') + case 'remedial': + return I18n.t('Well Below Mastery') + case 'near': + return I18n.t('Near Mastery') + case 'mastery': + return I18n.t('Meets Mastery') + case 'exceeds': + return I18n.t('Exceeds Mastery') + } + } + + toJSON() { + return { + ...super.toJSON(...arguments), + statusTooltip: this.statusTooltip(), + } + } +} + +GroupView.prototype.template = template +GroupView.prototype.tagName = 'li' +GroupView.prototype.className = 'group' +GroupView.prototype.els = {'.outcomes': '$outcomes'} +GroupView.prototype.events = { + 'click .group-description': 'expand', + 'keyclick .group-description': 'expand', +} + +export default GroupView diff --git a/ui/features/grade_summary/backbone/views/OutcomeDetailView.js b/ui/features/grade_summary/backbone/views/OutcomeDetailView.js index 786e0fab7ed..a79e175c93f 100644 --- a/ui/features/grade_summary/backbone/views/OutcomeDetailView.js +++ b/ui/features/grade_summary/backbone/views/OutcomeDetailView.js @@ -16,18 +16,14 @@ // with this program. If not, see . import Backbone from '@canvas/backbone' -import OutcomeResultCollection from '../collections/OutcomeResultCollection.coffee' +import OutcomeResultCollection from '../collections/OutcomeResultCollection' import DialogBaseView from '@canvas/dialog-base-view' import CollectionView from '@canvas/backbone-collection-view' import AlignmentView from './AlignmentView' -import ProgressBarView from './ProgressBarView.coffee' +import ProgressBarView from './ProgressBarView' import template from '../../jst/outcome_detail.handlebars' -export default class OutcomeDetailView extends DialogBaseView { - static initClass() { - this.prototype.template = template - } - +class OutcomeDetailView extends DialogBaseView { dialogOptions() { return { containerId: 'outcome_detail', @@ -77,4 +73,7 @@ export default class OutcomeDetailView extends DialogBaseView { return {...json, progress: this.progress} } } -OutcomeDetailView.initClass() + +OutcomeDetailView.prototype.template = template + +export default OutcomeDetailView diff --git a/ui/features/grade_summary/backbone/views/OutcomeDialogView.coffee b/ui/features/grade_summary/backbone/views/OutcomeDialogView.coffee deleted file mode 100644 index 5b931b54d81..00000000000 --- a/ui/features/grade_summary/backbone/views/OutcomeDialogView.coffee +++ /dev/null @@ -1,68 +0,0 @@ -# -# Copyright (C) 2015 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import $ from 'jquery' -import _ from 'underscore' -import DialogBaseView from '@canvas/dialog-base-view' -import OutcomeLineGraphView from './OutcomeLineGraphView.coffee' -import template from '@canvas/outcomes/jst/outcomePopover.handlebars' - -export default class OutcomeResultsDialogView extends DialogBaseView - @optionProperty 'model' - $target: null - template: template - - initialize: -> - super - @outcomeLineGraphView = new OutcomeLineGraphView({ - model: @model - }) - - afterRender: -> - @outcomeLineGraphView.setElement(@$("div.line-graph")) - @outcomeLineGraphView.render() - - dialogOptions: -> - containerId: "outcome_results_dialog" - close: @onClose - buttons: [] - width: 460 - - show: (e) -> - return unless (e.type == "click" || @_getKey(e.keyCode)) - @$target = $(e.target) - e.preventDefault() - @$el.dialog('option', 'title', @model.get('title')) - super - @render() - - onClose: => - @$target.focus() - delete @$target - - toJSON: -> - json = super - _.extend json, - dialog: true - - # Private - _getKey: (keycode) => - keys = { - 13 : "enter" - 32 : "spacebar" - } - keys[keycode] diff --git a/ui/features/grade_summary/backbone/views/OutcomeDialogView.js b/ui/features/grade_summary/backbone/views/OutcomeDialogView.js new file mode 100644 index 00000000000..bd0447b0b59 --- /dev/null +++ b/ui/features/grade_summary/backbone/views/OutcomeDialogView.js @@ -0,0 +1,86 @@ +// +// Copyright (C) 2015 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +import $ from 'jquery' +import DialogBaseView from '@canvas/dialog-base-view' +import OutcomeLineGraphView from './OutcomeLineGraphView' +import template from '@canvas/outcomes/jst/outcomePopover.handlebars' + +class OutcomeResultsDialogView extends DialogBaseView { + constructor(...args) { + super(...args) + this.onClose = this.onClose.bind(this) + this._getKey = this._getKey.bind(this) + } + + initialize() { + super.initialize(...arguments) + return (this.outcomeLineGraphView = new OutcomeLineGraphView({ + model: this.model, + })) + } + + afterRender() { + this.outcomeLineGraphView.setElement(this.$('div.line-graph')) + return this.outcomeLineGraphView.render() + } + + dialogOptions() { + return { + containerId: 'outcome_results_dialog', + close: this.onClose, + buttons: [], + width: 460, + } + } + + show(e) { + if (e.type !== 'click' && !this._getKey(e.keyCode)) return + this.$target = $(e.target) + e.preventDefault() + this.$el.dialog('option', 'title', this.model.get('title')) + super.show(...arguments) + return this.render() + } + + onClose() { + this.$target.focus() + delete this.$target + } + + toJSON() { + return { + ...super.toJSON(...arguments), + dialog: true, + } + } + + // Private + _getKey(keycode) { + const keys = { + 13: 'enter', + 32: 'spacebar', + } + return keys[keycode] + } +} + +OutcomeResultsDialogView.optionProperty('model') +OutcomeResultsDialogView.prototype.$target = null +OutcomeResultsDialogView.prototype.template = template + +export default OutcomeResultsDialogView diff --git a/ui/features/grade_summary/backbone/views/OutcomeLineGraphView.coffee b/ui/features/grade_summary/backbone/views/OutcomeLineGraphView.coffee deleted file mode 100644 index 3aa4547c9a5..00000000000 --- a/ui/features/grade_summary/backbone/views/OutcomeLineGraphView.coffee +++ /dev/null @@ -1,285 +0,0 @@ -# -# Copyright (C) 2015 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import _ from 'underscore' -import Backbone from '@canvas/backbone' -import I18n from '@canvas/i18n' -import OutcomeResultCollection from '../collections/OutcomeResultCollection.coffee' -import d3 from 'd3/d3' -import accessibleTemplate from '../../jst/accessibleLineGraph.handlebars' -import '../../sum' - -# Trend class based on formulae found here: -# http://classroom.synonym.com/calculate-trendline-2709.html -class Trend - constructor: (@rawData) -> - - # Returns: [[x1, y1], [x2, y2]] - data: -> - [[ - @xValue(@rawData[0]) - @yIntercept() - @xValue(_.last(@rawData)) - (@slope() * @xValue(_.last(@rawData))) + @yIntercept() - ]] - - slope: -> - (@a() - @b()) / (@c() - @d()) - - yIntercept: -> - (@e() - @f()) / @n() - - # The number of points of data. - n: -> - @rawData.length - - # `n` times the sum of the products of each x & y pair. - a: -> - @n() * _.sum(@rawData, (point) => (@xValue(point) * @yValue(point))) - - # The product of the sum of all x values and all y values. - b: -> - _.sum(@rawData, @xValue) * _.sum(@rawData, @yValue) - - # `n` times the sum of all x values individually squared. - c: -> - @n() * _.sum(@rawData, (point) => Math.pow(@xValue(point), 2)) - - # The sum of all x values squared. - d: -> - Math.pow(_.sum(@rawData, @xValue), 2) - - # The sum of all y values. - e: -> - _.sum(@rawData, @yValue) - - # The slope times the sum of all x values. - f: -> - @slope() * _.sum(@rawData, @xValue) - - xValue: (point) -> - point.x - - yValue: (point) -> - point.y - -export default class OutcomeLineGraphView extends Backbone.View - @optionProperty 'el' - @optionProperty 'height' - @optionProperty 'limit' - @optionProperty 'margin' - @optionProperty 'model' - @optionProperty 'timeFormat' - defaults: - height: 200 - limit: 8 - margin: {top: 20, right: 20, bottom: 30, left: 40} - # 2015-02-06T17:49:08Z - timeFormat: "%Y-%m-%dT%XZ" - - initialize: -> - super - @deferred = $.Deferred() - @collection = new OutcomeResultCollection([], { - outcome: @model - }) - @collection.on 'fetched:last', => - @deferred.resolve() - @collection.fetch() - - render: -> - if @deferred.isResolved() - return @ if @collection.isEmpty() - - @_prepareScales() - @_prepareAxes() - @_prepareLines() - - @svg = d3.select(@el) - .append("svg") - .attr("width", @width() + @margin.left + @margin.right) - .attr("height", @height + @margin.top + @margin.bottom) - .attr("aria-hidden", true) - .append("g") - .attr("transform", "translate(#{@margin.left}, #{@margin.top})") - - @_appendAxes() - @_appendLines() - - @$('.screenreader-only').append(accessibleTemplate(@toJSON())) - else - @deferred.done(@render) - - - @ - - toJSON: -> - current_user_name: ENV.current_user.display_name - data: @data() - outcome_name: @model.get('friendly_name') - - # Data helpers - data: -> - @_data ?= @collection.chain() - .last(@limit) - .map((outcomeResult, i) => - x: i - y: @percentageFor(outcomeResult.get('score')) - date: outcomeResult.get('submitted_or_assessed_at') - ).value() - - masteryPercentage: -> - if @model.get('points_possible') > 0 - (@model.get('mastery_points') / @model.get('points_possible')) * 100 - else - 100 - - percentageFor: (score) -> - if @model.get('points_possible') > 0 - ((score / @model.get('points_possible')) * 100) - else - ((score / @model.get('mastery_points')) * 100) - - xValue: (point) => - @x(point.x) - - yValue: (point) => - @y(point.y) - - # View helpers - _appendAxes: -> - @svg.append("g") - .attr("class", "x axis") - .attr("transform", "translate(0,#{@height})") - .call(@xAxis) - - @svg.append("g") - .attr("class", "date-guides") - .attr("transform", "translate(0,#{@height})") - .call(@dateGuides) - - @svg.append("g") - .attr("class", "y axis") - .call(@yAxis) - - @svg.append("g") - .attr("class", "guides") - .call(@yGuides) - - @svg.append("g") - .attr("class", "mastery-percentage-guide") - .style("stroke-dasharray", ("3, 3")) - .call(@masteryPercentageGuide) - - _appendLines: -> - @svg.selectAll("circle") - .data(@data()) - .enter().append("circle") - .attr("fill", "black") - .attr("r", 3) - .attr("cx", @xValue) - .attr("cy", @yValue) - - @svg.append("path") - .datum(@data()) - .attr("d", @line) - .attr("class", "line") - .attr("stroke", "black") - .attr("stroke-width", 1) - .attr("fill", "none") - - if @trend? - @svg.selectAll(".trendline") - .data(@trend.data()) - .enter() - .append("line") - .attr("class", "trendline") - .attr("x1", (d) => @x(d[0])) - .attr("y1", (d) => @y(d[1])) - .attr("x2", (d) => @x(d[2])) - .attr("y2", (d) => @y(d[3])) - .attr("stroke-width", 1) - - @svg - - - _prepareAxes: -> - @xAxis = d3.svg.axis() - .scale(@x) - .tickFormat('') - @dateGuides = d3.svg.axis() - .scale(@xTimeScale) - .tickValues([ - _.first(@data()).date - _.last(@data()).date - ]) - .tickFormat((d) -> Intl.DateTimeFormat(I18n.currentLocale(), { day: 'numeric', month: 'numeric'}).format(d)) - @yAxis = d3.svg.axis() - .scale(@y) - .orient("left") - .tickFormat((d) -> I18n.n(d, { percentage: true })) - .tickValues([0, 50, 100]) - @yGuides = d3.svg.axis() - .scale(@y) - .orient("left") - .tickValues([50, 100]) - .tickSize(-@width(), 0, 0) - .tickFormat("") - @masteryPercentageGuide = d3.svg.axis() - .scale(@y) - .orient("left") - .tickValues([@masteryPercentage()]) - .tickSize(-@width(), 0, 0) - .tickFormat("") - - _prepareLines: -> - if @data().length >=3 - @trend = new Trend(@data()) - - @line = d3.svg.line() - .x(@xValue) - .y(@yValue) - .interpolate('linear') - - _prepareScales: -> - @x = d3.scale.linear() - .range([0, @width()]) - .domain([0, @limit - 1]) - @xTimeScale = d3.time.scale() - .range([0, @xTimeScaleWidth()]) - .domain([ - _.first(@data()).date - _.last(@data()).date - ]) - @y = d3.scale.linear() - .range([@height, @margin.bottom]) - .domain([0, 100]) - - width: -> - @$el.width() - @margin.left - @margin.right - 10 - - # The width of the axis used to display the first and last date of scores - # displayed has to be different than the full width, in case the number - # of points is fewer than the limit (8). What we want is the width of the - # element reduced by the difference between the limit and the number of - # points we actually have, multiplied by the width each point represents, - # based on the element's width and the limit. - xTimeScaleWidth: -> - (@width() - ( - (@width() / (@limit - 1)) * - (@limit - @data().length) - )) diff --git a/ui/features/grade_summary/backbone/views/OutcomeLineGraphView.js b/ui/features/grade_summary/backbone/views/OutcomeLineGraphView.js new file mode 100644 index 00000000000..646d89e16e9 --- /dev/null +++ b/ui/features/grade_summary/backbone/views/OutcomeLineGraphView.js @@ -0,0 +1,340 @@ +// +// Copyright (C) 2015 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + +import $ from 'jquery' +import {sumBy} from 'lodash' +import Backbone from '@canvas/backbone' +import I18n from '@canvas/i18n' +import OutcomeResultCollection from '../collections/OutcomeResultCollection' +import d3 from 'd3/d3' +import accessibleTemplate from '../../jst/accessibleLineGraph.handlebars' + +const first = array => array.at(0) +const last = array => array.at(-1) + +// Trend class based on formulae found here: +// http://classroom.synonym.com/calculate-trendline-2709.html +class Trend { + constructor(rawData) { + this.rawData = rawData + } + + // Returns: [[x1, y1], [x2, y2]] + data() { + return [ + [ + this.rawData[0].x, + this.yIntercept(), + last(this.rawData).x, + this.slope() * last(this.rawData).x + this.yIntercept(), + ], + ] + } + + slope() { + return (this.a() - this.b()) / (this.c() - this.d()) + } + + yIntercept() { + return (this.e() - this.f()) / this.n() + } + + // The number of points of data. + n() { + return this.rawData.length + } + + // `n` times the sum of the products of each x & y pair. + a() { + return this.n() * sumBy(this.rawData, p => p.x * p.y) + } + + // The product of the sum of all x values and all y values. + b() { + return sumBy(this.rawData, p => p.x) * sumBy(this.rawData, p => p.y) + } + + // `n` times the sum of all x values individually squared. + c() { + return this.n() * sumBy(this.rawData, p => p.x * p.x) + } + + // The sum of all x values squared. + d() { + return Math.pow( + sumBy(this.rawData, p => p.x), + 2 + ) + } + + // The sum of all y values. + e() { + return sumBy(this.rawData, p => p.y) + } + + // The slope times the sum of all x values. + f() { + return this.slope() * sumBy(this.rawData, p => p.x) + } +} + +class OutcomeLineGraphView extends Backbone.View { + constructor(...args) { + super(...args) + this.xValue = this.xValue.bind(this) + this.yValue = this.yValue.bind(this) + } + + initialize() { + super.initialize(...arguments) + this.deferred = $.Deferred() + this.collection = new OutcomeResultCollection([], { + outcome: this.model, + }) + this.collection.on('fetched:last', () => { + return this.deferred.resolve() + }) + return this.collection.fetch() + } + + render() { + if (this.deferred.isResolved()) { + if (this.collection.isEmpty()) { + return this + } + + this._prepareScales() + this._prepareAxes() + this._prepareLines() + + this.svg = d3 + .select(this.el) + .append('svg') + .attr('width', this.width() + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.top + this.margin.bottom) + .attr('aria-hidden', true) + .append('g') + .attr('transform', `translate(${this.margin.left}, ${this.margin.top})`) + + this._appendAxes() + this._appendLines() + + this.$('.screenreader-only').append(accessibleTemplate(this.toJSON())) + } else { + this.deferred.done(this.render) + } + + return this + } + + toJSON() { + return { + current_user_name: ENV.current_user.display_name, + data: this.data(), + outcome_name: this.model.get('friendly_name'), + } + } + + // Data helpers + data() { + if (this._data === null || typeof this._data === 'undefined') { + this._data = this.collection + .chain() + .last(this.limit) + .map((outcomeResult, i) => ({ + x: i, + y: this.percentageFor(outcomeResult.get('score')), + date: outcomeResult.get('submitted_or_assessed_at'), + })) + .value() + } + return this._data + } + + masteryPercentage() { + if (this.model.get('points_possible') > 0) { + return (this.model.get('mastery_points') / this.model.get('points_possible')) * 100 + } else { + return 100 + } + } + + percentageFor(score) { + if (this.model.get('points_possible') > 0) { + return (score / this.model.get('points_possible')) * 100 + } else { + return (score / this.model.get('mastery_points')) * 100 + } + } + + xValue(point) { + return this.x(point.x) + } + + yValue(point) { + return this.y(point.y) + } + + // View helpers + _appendAxes() { + this.svg + .append('g') + .attr('class', 'x axis') + .attr('transform', `translate(0,${this.height})`) + .call(this.xAxis) + + this.svg + .append('g') + .attr('class', 'date-guides') + .attr('transform', `translate(0,${this.height})`) + .call(this.dateGuides) + + this.svg.append('g').attr('class', 'y axis').call(this.yAxis) + + this.svg.append('g').attr('class', 'guides').call(this.yGuides) + + return this.svg + .append('g') + .attr('class', 'mastery-percentage-guide') + .style('stroke-dasharray', '3, 3') + .call(this.masteryPercentageGuide) + } + + _appendLines() { + this.svg + .selectAll('circle') + .data(this.data()) + .enter() + .append('circle') + .attr('fill', 'black') + .attr('r', 3) + .attr('cx', this.xValue) + .attr('cy', this.yValue) + + this.svg + .append('path') + .datum(this.data()) + .attr('d', this.line) + .attr('class', 'line') + .attr('stroke', 'black') + .attr('stroke-width', 1) + .attr('fill', 'none') + + if (this.trend != null) { + this.svg + .selectAll('.trendline') + .data(this.trend.data()) + .enter() + .append('line') + .attr('class', 'trendline') + .attr('x1', d => this.x(d[0])) + .attr('y1', d => this.y(d[1])) + .attr('x2', d => this.x(d[2])) + .attr('y2', d => this.y(d[3])) + .attr('stroke-width', 1) + } + + return this.svg + } + + _prepareAxes() { + this.xAxis = d3.svg.axis().scale(this.x).tickFormat('') + this.dateGuides = d3.svg + .axis() + .scale(this.xTimeScale) + .tickValues([first(this.data()).date, last(this.data()).date]) + .tickFormat(d => + Intl.DateTimeFormat(I18n.currentLocale(), {day: 'numeric', month: 'numeric'}).format(d) + ) + this.yAxis = d3.svg + .axis() + .scale(this.y) + .orient('left') + .tickFormat(d => I18n.n(d, {percentage: true})) + .tickValues([0, 50, 100]) + this.yGuides = d3.svg + .axis() + .scale(this.y) + .orient('left') + .tickValues([50, 100]) + .tickSize(-this.width(), 0, 0) + .tickFormat('') + return (this.masteryPercentageGuide = d3.svg + .axis() + .scale(this.y) + .orient('left') + .tickValues([this.masteryPercentage()]) + .tickSize(-this.width(), 0, 0) + .tickFormat('')) + } + + _prepareLines() { + if (this.data().length >= 3) { + this.trend = new Trend(this.data()) + } + + return (this.line = d3.svg.line().x(this.xValue).y(this.yValue).interpolate('linear')) + } + + _prepareScales() { + this.x = d3.scale + .linear() + .range([0, this.width()]) + .domain([0, this.limit - 1]) + this.xTimeScale = d3.time + .scale() + .range([0, this.xTimeScaleWidth()]) + .domain([first(this.data()).date, last(this.data()).date]) + return (this.y = d3.scale.linear().range([this.height, this.margin.bottom]).domain([0, 100])) + } + + width() { + return this.$el.width() - this.margin.left - this.margin.right - 10 + } + + // The width of the axis used to display the first and last date of scores + // displayed has to be different than the full width, in case the number + // of points is fewer than the limit (8). What we want is the width of the + // element reduced by the difference between the limit and the number of + // points we actually have, multiplied by the width each point represents, + // based on the element's width and the limit. + xTimeScaleWidth() { + return this.width() - (this.width() / (this.limit - 1)) * (this.limit - this.data().length) + } +} + +OutcomeLineGraphView.optionProperty('el') +OutcomeLineGraphView.optionProperty('height') +OutcomeLineGraphView.optionProperty('limit') +OutcomeLineGraphView.optionProperty('margin') +OutcomeLineGraphView.optionProperty('model') +OutcomeLineGraphView.optionProperty('timeFormat') +OutcomeLineGraphView.prototype.defaults = { + height: 200, + limit: 8, + margin: {top: 20, right: 20, bottom: 30, left: 40}, + // 2015-02-06T17:49:08Z + timeFormat: '%Y-%m-%dT%XZ', +} + +export default OutcomeLineGraphView diff --git a/ui/features/grade_summary/backbone/views/OutcomePopoverView.coffee b/ui/features/grade_summary/backbone/views/OutcomePopoverView.coffee deleted file mode 100644 index 1eafd132b28..00000000000 --- a/ui/features/grade_summary/backbone/views/OutcomePopoverView.coffee +++ /dev/null @@ -1,71 +0,0 @@ -# -# Copyright (C) 2015 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import Backbone from '@canvas/backbone' -import Popover from 'jquery-popover' -import OutcomeLineGraphView from './OutcomeLineGraphView.coffee' -import template from '@canvas/outcomes/jst/outcomePopover.handlebars' - -export default class OutcomePopoverView extends Backbone.View - TIMEOUT_LENGTH: 50 - - @optionProperty 'el' - @optionProperty 'model' - - events: - 'click i': 'mouseleave' - 'mouseenter i': 'mouseenter' - 'mouseleave i': 'mouseleave' - inside: false - - initialize: -> - super - @outcomeLineGraphView = new OutcomeLineGraphView({ - model: @model - }) - - # Overrides - render: -> - template(@toJSON()) - - # Instance methods - closePopover: (e) -> - e?.preventDefault() - return true unless @popover? - @popover.hide() - delete @popover - - mouseenter: (e) => - @openPopover(e) - @inside = true - - mouseleave: (e) => - @inside = false - setTimeout => - return if @inside || !@popover - @closePopover() - , @TIMEOUT_LENGTH - - openPopover: (e) -> - if @closePopover() - @popover = new Popover(e, @render(), { - verticalSide: 'bottom' - manualOffset: 14 - }) - @outcomeLineGraphView.setElement(@popover.el.find("div.line-graph")) - @outcomeLineGraphView.render() - diff --git a/ui/features/grade_summary/backbone/views/OutcomePopoverView.js b/ui/features/grade_summary/backbone/views/OutcomePopoverView.js new file mode 100644 index 00000000000..1d4322e799e --- /dev/null +++ b/ui/features/grade_summary/backbone/views/OutcomePopoverView.js @@ -0,0 +1,95 @@ +// +// Copyright (C) 2015 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +/* + * decaffeinate suggestions: + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + +import Backbone from '@canvas/backbone' +import Popover from 'jquery-popover' +import OutcomeLineGraphView from './OutcomeLineGraphView' +import template from '@canvas/outcomes/jst/outcomePopover.handlebars' + +const TIMEOUT_LENGTH = 50 + +class OutcomePopoverView extends Backbone.View { + constructor(...args) { + super(...args) + this.mouseenter = this.mouseenter.bind(this) + this.mouseleave = this.mouseleave.bind(this) + } + + initialize() { + super.initialize(...arguments) + return (this.outcomeLineGraphView = new OutcomeLineGraphView({ + model: this.model, + })) + } + + // Overrides + render() { + return template(this.toJSON()) + } + + // Instance methods + closePopover(e) { + e?.preventDefault() + if (this.popover === null || typeof this.popover === 'undefined') return true + this.popover.hide() + return delete this.popover + } + + mouseenter(e) { + this.openPopover(e) + this.inside = true + return true + } + + mouseleave(_e) { + this.inside = false + setTimeout(() => { + if (!this.inside && this.popover) this.closePopover() + }, TIMEOUT_LENGTH) + } + + openPopover(e) { + if (this.closePopover()) { + this.popover = new Popover(e, this.render(), { + verticalSide: 'bottom', + manualOffset: 14, + }) + } + this.outcomeLineGraphView.setElement(this.popover.el.find('div.line-graph')) + return this.outcomeLineGraphView.render() + } +} + +OutcomePopoverView.prototype.events = { + 'click i': 'mouseleave', + 'mouseenter i': 'mouseenter', + 'mouseleave i': 'mouseleave', +} +OutcomePopoverView.prototype.inside = false +OutcomePopoverView.prototype.TIMEOUT_LENGTH = TIMEOUT_LENGTH + +OutcomePopoverView.optionProperty('el') +OutcomePopoverView.optionProperty('model') + +export default OutcomePopoverView diff --git a/ui/features/grade_summary/backbone/views/OutcomeView.coffee b/ui/features/grade_summary/backbone/views/OutcomeView.coffee deleted file mode 100644 index 821f41637f9..00000000000 --- a/ui/features/grade_summary/backbone/views/OutcomeView.coffee +++ /dev/null @@ -1,53 +0,0 @@ -# -# Copyright (C) 2014 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import _ from 'underscore' -import Backbone from '@canvas/backbone' -import ProgressBarView from './ProgressBarView.coffee' -import OutcomePopoverView from './OutcomePopoverView.coffee' -import OutcomeDialogView from './OutcomeDialogView.coffee' -import template from '../../jst/outcome.handlebars' - -export default class OutcomeView extends Backbone.View - className: 'outcome' - events: - 'click .more-details' : 'show' - 'keydown .more-details' : 'show' - tagName: 'li' - template: template - - initialize: -> - super - @progress = new ProgressBarView(model: @model) - - afterRender: -> - @popover = new OutcomePopoverView({ - el: @$('.more-details') - model: @model - }) - @dialog = new OutcomeDialogView({ - model: @model - }) - - show: (e) -> - @dialog.show e - - toJSON: -> - json = super - _.extend json, - progress: @progress - diff --git a/ui/features/grade_summary/backbone/views/OutcomeView.js b/ui/features/grade_summary/backbone/views/OutcomeView.js new file mode 100644 index 00000000000..6c3b4a27d5e --- /dev/null +++ b/ui/features/grade_summary/backbone/views/OutcomeView.js @@ -0,0 +1,60 @@ +// +// Copyright (C) 2014 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +import Backbone from '@canvas/backbone' +import ProgressBarView from './ProgressBarView' +import OutcomePopoverView from './OutcomePopoverView' +import OutcomeDialogView from './OutcomeDialogView' +import template from '../../jst/outcome.handlebars' + +class OutcomeView extends Backbone.View { + initialize() { + super.initialize(...arguments) + return (this.progress = new ProgressBarView({model: this.model})) + } + + afterRender() { + this.popover = new OutcomePopoverView({ + el: this.$('.more-details'), + model: this.model, + }) + return (this.dialog = new OutcomeDialogView({ + model: this.model, + })) + } + + show(e) { + return this.dialog.show(e) + } + + toJSON() { + return { + ...super.toJSON(...arguments), + progress: this.progress, + } + } +} + +OutcomeView.prototype.className = 'outcome' +OutcomeView.prototype.tagName = 'li' +OutcomeView.prototype.template = template +OutcomeView.prototype.events = { + 'click .more-details': 'show', + 'keydown .more-details': 'show', +} + +export default OutcomeView diff --git a/ui/features/grade_summary/backbone/views/ProgressBarView.coffee b/ui/features/grade_summary/backbone/views/ProgressBarView.coffee deleted file mode 100644 index ddb5ab5e119..00000000000 --- a/ui/features/grade_summary/backbone/views/ProgressBarView.coffee +++ /dev/null @@ -1,23 +0,0 @@ -# -# Copyright (C) 2014 - present Instructure, Inc. -# -# This file is part of Canvas. -# -# Canvas is free software: you can redistribute it and/or modify it under -# the terms of the GNU Affero General Public License as published by the Free -# Software Foundation, version 3 of the License. -# -# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -# details. -# -# You should have received a copy of the GNU Affero General Public License along -# with this program. If not, see . - -import Backbone from '@canvas/backbone' -import template from '../../jst/progress_bar.handlebars' - -export default class ProgressBarView extends Backbone.View - className: 'bar' - template: template diff --git a/ui/features/grade_summary/backbone/views/ProgressBarView.js b/ui/features/grade_summary/backbone/views/ProgressBarView.js new file mode 100644 index 00000000000..769f82a688b --- /dev/null +++ b/ui/features/grade_summary/backbone/views/ProgressBarView.js @@ -0,0 +1,26 @@ +// +// Copyright (C) 2014 - present Instructure, Inc. +// +// This file is part of Canvas. +// +// Canvas is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, version 3 of the License. +// +// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . + +import Backbone from '@canvas/backbone' +import template from '../../jst/progress_bar.handlebars' + +class ProgressBarView extends Backbone.View {} + +ProgressBarView.prototype.className = 'bar' +ProgressBarView.prototype.template = template + +export default ProgressBarView diff --git a/ui/features/grade_summary/backbone/views/SectionView.js b/ui/features/grade_summary/backbone/views/SectionView.js index e7926f66a60..e234ccd6ea3 100644 --- a/ui/features/grade_summary/backbone/views/SectionView.js +++ b/ui/features/grade_summary/backbone/views/SectionView.js @@ -17,7 +17,7 @@ import {View} from '@canvas/backbone' import CollectionView from '@canvas/backbone-collection-view' -import GroupView from './GroupView.coffee' +import GroupView from './GroupView' import template from '../../jst/section.handlebars' export default class SectionView extends View {