learning mastery student view: outcome hierarchy
fixes CNVS-13307 test plan: * enable learning mastery student view * open a student's grades page * open the learning mastery tab * verify that outcome groups are presented in a hierarchy * verify that very deep outcome trees are compressed to paths Change-Id: I1f74dca2876b39b01a6ec70935f60d5e233fe02b Reviewed-on: https://gerrit.instructure.com/35481 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Drew Bowman <dbowman@instructure.com> QA-Review: Steven Shepherd <sshepherd@instructure.com> Product-Review: Braden Anderson <banderson@instructure.com>
This commit is contained in:
parent
47434d86ae
commit
805510f0ad
|
@ -4,11 +4,12 @@ require [
|
|||
'Backbone'
|
||||
'compiled/views/CollectionView'
|
||||
'compiled/userSettings'
|
||||
'compiled/views/gradebook/StudentOutcomeView'
|
||||
'compiled/collections/OutcomeSummaryCollection'
|
||||
'compiled/views/grade_summary/SectionView'
|
||||
'jqueryui/tabs'
|
||||
'jquery.disableWhileLoading'
|
||||
'grade_summary'
|
||||
], ($, _, Backbone, CollectionView, userSettings, StudentOutcomeView) ->
|
||||
], ($, _, Backbone, CollectionView, userSettings, OutcomeSummaryCollection, SectionView) ->
|
||||
class GradebookSummaryRouter extends Backbone.Router
|
||||
routes:
|
||||
'': 'tab'
|
||||
|
@ -18,6 +19,11 @@ require [
|
|||
return unless ENV.student_outcome_gradebook_enabled
|
||||
$('#content').tabs(activate: @activate)
|
||||
|
||||
course_id = ENV.context_asset_string.replace('course_', '')
|
||||
user_id = ENV.student_id
|
||||
@outcomes = new OutcomeSummaryCollection([], course_id: course_id, user_id: user_id)
|
||||
@outcomeView = new CollectionView(el: $('#outcomes'), collection: @outcomes, itemView: SectionView)
|
||||
|
||||
tab: (tab) ->
|
||||
if tab != 'outcomes' && tab != 'assignments'
|
||||
tab = userSettings.contextGet('grade_summary_tab') || 'assignments'
|
||||
|
@ -26,30 +32,12 @@ require [
|
|||
activate: (event, ui) =>
|
||||
tab = ui.newPanel.attr('id')
|
||||
router.navigate("#tab-#{tab}")
|
||||
@loadOutcomes() if tab == 'outcomes'
|
||||
@fetchOutcomes() if tab == 'outcomes'
|
||||
userSettings.contextSet('grade_summary_tab', tab)
|
||||
|
||||
loadOutcomes: ->
|
||||
@loadOutcomes = $.noop
|
||||
course_id = ENV.context_asset_string.replace('course_', '')
|
||||
user_id = ENV.student_id
|
||||
url = "/api/v1/courses/#{course_id}/outcome_rollups?user_ids[]=#{user_id}&include[]=outcomes"
|
||||
whenLoaded = $.getJSON(url)
|
||||
$('#outcomes').disableWhileLoading(whenLoaded)
|
||||
whenLoaded.done(@handleOutcomes)
|
||||
|
||||
handleOutcomes: (response) =>
|
||||
scores = _.object(_.map(response.rollups[0].scores, (score) ->
|
||||
[score.links.outcome, score.score]
|
||||
))
|
||||
outcomes = new Backbone.Collection(_.map(response.linked.outcomes, (outcome) ->
|
||||
new Backbone.Model(_.extend({score: scores[outcome.id]}, outcome))
|
||||
))
|
||||
new CollectionView(
|
||||
el: $('#outcomes')
|
||||
itemView: StudentOutcomeView
|
||||
collection: outcomes
|
||||
).render()
|
||||
fetchOutcomes: ->
|
||||
@fetchOutcomes = $.noop
|
||||
@outcomes.fetch()
|
||||
|
||||
@router = new GradebookSummaryRouter
|
||||
Backbone.history.start()
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
define [
|
||||
'jquery'
|
||||
'underscore'
|
||||
'Backbone'
|
||||
'compiled/models/grade_summary/Section'
|
||||
'compiled/models/grade_summary/Group'
|
||||
'compiled/models/grade_summary/Outcome'
|
||||
'compiled/collections/PaginatedCollection'
|
||||
'compiled/collections/WrappedCollection'
|
||||
'compiled/util/natcompare'
|
||||
], ($, _, {Collection}, Section, Group, Outcome, PaginatedCollection, WrappedCollection, natcompare) ->
|
||||
class GroupCollection extends PaginatedCollection
|
||||
@optionProperty 'course_id'
|
||||
model: Group
|
||||
url: -> "/api/v1/courses/#{@course_id}/outcome_groups"
|
||||
|
||||
class LinkCollection extends PaginatedCollection
|
||||
@optionProperty 'course_id'
|
||||
url: -> "/api/v1/courses/#{@course_id}/outcome_group_links?outcome_style=full"
|
||||
|
||||
class ResultCollection extends WrappedCollection
|
||||
@optionProperty 'course_id'
|
||||
@optionProperty 'user_id'
|
||||
key: 'rollups'
|
||||
url: -> "/api/v1/courses/#{@course_id}/outcome_rollups?user_ids[]=#{@user_id}"
|
||||
|
||||
class OutcomeSummaryCollection extends Collection
|
||||
@optionProperty 'course_id'
|
||||
@optionProperty 'user_id'
|
||||
|
||||
comparator: natcompare.byGet('path')
|
||||
|
||||
initialize: ->
|
||||
super
|
||||
@rawCollections =
|
||||
groups: new GroupCollection([], course_id: @course_id)
|
||||
links: new LinkCollection([], course_id: @course_id)
|
||||
results: new ResultCollection([], course_id: @course_id, user_id: @user_id)
|
||||
|
||||
fetch: ->
|
||||
dfd = $.Deferred()
|
||||
requests = _.values(@rawCollections).map (collection) -> collection.loadAll = true; collection.fetch()
|
||||
$.when.apply($, requests).done(=> @processCollections(dfd))
|
||||
dfd
|
||||
|
||||
scores: ->
|
||||
studentResults = @rawCollections.results.at(0).get('scores')
|
||||
pairs = studentResults.map((x) -> [x.links.outcome, x.score])
|
||||
_.object(pairs)
|
||||
|
||||
populateGroupOutcomes: ->
|
||||
scores = @scores()
|
||||
@rawCollections.links.each (link) =>
|
||||
outcome = new Outcome(link.get('outcome'))
|
||||
outcome.set('score', scores[outcome.id])
|
||||
parent = @rawCollections.groups.get(link.get('outcome_group').id)
|
||||
parent.get('outcomes').add(outcome)
|
||||
|
||||
populateSectionGroups: ->
|
||||
tmp = new Collection()
|
||||
@rawCollections.groups.each (group) =>
|
||||
return unless group.get('outcomes').length
|
||||
parentObj = group.get('parent_outcome_group')
|
||||
parentId = if parentObj then parentObj.id else group
|
||||
unless parent = tmp.get(parentId)
|
||||
parent = tmp.add(new Section(id: parentId, path: @getPath(parentId)))
|
||||
parent.get('groups').add(group)
|
||||
@reset(tmp.models)
|
||||
|
||||
processCollections: (dfd) =>
|
||||
@populateGroupOutcomes()
|
||||
@populateSectionGroups()
|
||||
dfd.resolve(@models)
|
||||
|
||||
getPath: (id) ->
|
||||
group = @rawCollections.groups.get(id)
|
||||
parent = group.get('parent_outcome_group')
|
||||
return '' unless parent
|
||||
parentPath = @getPath(parent.id)
|
||||
(if parentPath then parentPath + ': ' else '') + group.get('title')
|
|
@ -66,6 +66,7 @@ define [
|
|||
@_setStateAfterFetch(xhr, options)
|
||||
data
|
||||
|
||||
dfd = options.dfd || $.Deferred()
|
||||
xhr = super(options).done (response, text, xhr) =>
|
||||
@trigger 'fetch', this, response, options
|
||||
@trigger "fetch:#{options.page}", this, response, options if options.page?
|
||||
|
@ -74,7 +75,13 @@ define [
|
|||
@loadedAll = true
|
||||
if @loadAll and @urls.next?
|
||||
setTimeout =>
|
||||
@fetch page: 'next' # next tick so we can show loading indicator, etc.
|
||||
@fetch page: 'next', dfd: dfd # next tick so we can show loading indicator, etc.
|
||||
else
|
||||
dfd.resolve(response, text, xhr)
|
||||
dfd.abort = xhr.abort
|
||||
dfd.success = dfd.done
|
||||
dfd.error = dfd.fail
|
||||
dfd
|
||||
|
||||
canFetch: (page) ->
|
||||
@urls? and @urls[page]?
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
define [
|
||||
'jquery'
|
||||
'Backbone'
|
||||
'compiled/collections/PaginatedCollection'
|
||||
], ($, Backbone, PaginatedCollection) ->
|
||||
class WrappedCollection extends PaginatedCollection
|
||||
@optionProperty 'key'
|
||||
|
||||
parse: (response) ->
|
||||
@linked = response.linked
|
||||
response[@key]
|
|
@ -0,0 +1,18 @@
|
|||
define [
|
||||
'underscore'
|
||||
'Backbone'
|
||||
'compiled/util/natcompare'
|
||||
], (_, {Model, Collection}, natcompare) ->
|
||||
class Group extends Model
|
||||
initialize: ->
|
||||
@set('outcomes', new Collection([], comparator: natcompare.byGet('title')))
|
||||
|
||||
count: -> @get('outcomes').length
|
||||
|
||||
mastery_count: ->
|
||||
@get('outcomes').filter((x) ->
|
||||
x.status() == 'mastery'
|
||||
).length
|
||||
|
||||
toJSON: ->
|
||||
_.extend(super, count: @count(), mastery_count: @mastery_count())
|
|
@ -0,0 +1,17 @@
|
|||
define [
|
||||
'underscore'
|
||||
'Backbone'
|
||||
], (_, {Model, Collection}) ->
|
||||
class Outcome extends Model
|
||||
status: ->
|
||||
score = @get('score')
|
||||
mastery = @get('mastery_points')
|
||||
if score >= mastery
|
||||
'mastery'
|
||||
else if score >= mastery / 2
|
||||
'near'
|
||||
else
|
||||
'remedial'
|
||||
|
||||
toJSON: ->
|
||||
_.extend(super, status: @status())
|
|
@ -0,0 +1,8 @@
|
|||
define [
|
||||
'underscore'
|
||||
'Backbone'
|
||||
'compiled/util/natcompare'
|
||||
], (_, {Model, Collection}, natcompare) ->
|
||||
class Section extends Model
|
||||
initialize: ->
|
||||
@set('groups', new Collection([], comparator: natcompare.byGet('title')))
|
|
@ -0,0 +1,11 @@
|
|||
define [], ->
|
||||
strings: (x, y) ->
|
||||
x.localeCompare(y, window.I18n.locale, { sensitivity: 'accent', ignorePunctuation: true, numeric: true})
|
||||
|
||||
by: (f) ->
|
||||
return (x, y) =>
|
||||
@strings(f(x), f(y))
|
||||
|
||||
byKey: (key) -> @by((x) -> x[key])
|
||||
|
||||
byGet: (key) -> @by((x) -> x.get(key))
|
|
@ -0,0 +1,24 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'underscore'
|
||||
'compiled/views/CollectionView'
|
||||
'compiled/views/grade_summary/OutcomeView'
|
||||
'jst/grade_summary/group'
|
||||
], ({View, Collection}, _, CollectionView, OutcomeView, template) ->
|
||||
|
||||
class GroupView extends View
|
||||
tagName: 'li'
|
||||
className: 'group'
|
||||
|
||||
els:
|
||||
'.outcomes': '$outcomes'
|
||||
|
||||
template: template
|
||||
|
||||
render: ->
|
||||
super
|
||||
outcomesView = new CollectionView
|
||||
el: @$outcomes
|
||||
collection: @model.get('outcomes')
|
||||
itemView: OutcomeView
|
||||
outcomesView.render()
|
|
@ -1,10 +1,11 @@
|
|||
define [
|
||||
'underscore'
|
||||
'Backbone'
|
||||
'jst/gradebook2/student_outcome_view'
|
||||
'jst/grade_summary/outcome'
|
||||
], (_, Backbone, template) ->
|
||||
class StudentOutcomesView extends Backbone.View
|
||||
class OutcomeView extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'outcome'
|
||||
template: template
|
||||
toJSON: ->
|
||||
json = super
|
|
@ -0,0 +1,24 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'underscore'
|
||||
'compiled/views/CollectionView'
|
||||
'compiled/views/grade_summary/GroupView'
|
||||
'jst/grade_summary/section'
|
||||
], ({View, Collection}, _, CollectionView, GroupView, template) ->
|
||||
|
||||
class SectionView extends View
|
||||
tagName: 'li'
|
||||
className: 'section'
|
||||
|
||||
els:
|
||||
'.groups': '$groups'
|
||||
|
||||
template: template
|
||||
|
||||
render: ->
|
||||
super
|
||||
groupsView = new CollectionView
|
||||
el: @$groups
|
||||
collection: @model.get('groups')
|
||||
itemView: GroupView
|
||||
groupsView.render()
|
|
@ -122,7 +122,7 @@
|
|||
# },
|
||||
# "outcome": {
|
||||
# "description": "an abbreviated Outcome object representing the outcome linked into the containing outcome group.",
|
||||
# "$ref": "OutcomeGroup"
|
||||
# "$ref": "Outcome"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
@ -162,13 +162,23 @@ class OutcomeGroupsApiController < ApplicationController
|
|||
# @API Get all outcome links for context
|
||||
# @beta
|
||||
#
|
||||
# @argument outcome_style [Optional, String]
|
||||
# The detail level of the outcomes. Defaults to "abbrev".
|
||||
# Specify "full" for more information.
|
||||
#
|
||||
# @argument outcome_group_style [Optional, String]
|
||||
# The detail level of the outcome groups. Defaults to "abbrev".
|
||||
# Specify "full" for more information.
|
||||
#
|
||||
# @returns [OutcomeLink]
|
||||
def link_index
|
||||
return unless can_read_outcomes
|
||||
|
||||
url = polymorphic_url [:api_v1, @context || :global, :outcome_group_links]
|
||||
links = Api.paginate(context_outcome_links, self, url)
|
||||
render json: links.map { |link| outcome_link_json(link, @current_user, session) }
|
||||
render json: links.map { |link|
|
||||
outcome_link_json(link, @current_user, session, params.slice(:outcome_style, :outcome_group_style))
|
||||
}
|
||||
end
|
||||
|
||||
# @API Show an outcome group
|
||||
|
|
|
@ -432,7 +432,7 @@ class OutcomeResultsController < ApplicationController
|
|||
|
||||
if params[:user_ids]
|
||||
user_ids = Api.value_to_array(params[:user_ids]).map(&:to_i).uniq
|
||||
@users = users_for_outcome_context.where(id: user_ids)
|
||||
@users = users_for_outcome_context.where(id: user_ids).uniq
|
||||
reject!( "can only include id's of users in the outcome context") if @users.count != user_ids.count
|
||||
elsif params[:section_id]
|
||||
@section = @context.course_sections.where(id: params[:section_id].to_i).first
|
||||
|
|
|
@ -295,32 +295,54 @@ div.rubric-toggle
|
|||
|
||||
$outcome-border: 1px solid #BCC2CA
|
||||
|
||||
#outcomes ul
|
||||
background-color: #f4f4f4
|
||||
border: $outcome-border
|
||||
border-radius: 3px
|
||||
overflow: hidden
|
||||
#outcomes
|
||||
h2
|
||||
font-size: 1.4em
|
||||
font-weight: bold
|
||||
|
||||
#outcomes li
|
||||
list-style-type: none
|
||||
overflow: hidden
|
||||
border-top: $outcome-border
|
||||
.group
|
||||
border: $outcome-border
|
||||
border-radius: 3px
|
||||
margin-top: 1em
|
||||
|
||||
&:first-child
|
||||
border-top: none
|
||||
|
||||
.outcome
|
||||
h3
|
||||
font-size: 1.2em
|
||||
margin-left: 1em
|
||||
float: left
|
||||
width: 80%
|
||||
padding: 1em
|
||||
border-right: $outcome-border
|
||||
box-sizing: border-box
|
||||
|
||||
.description
|
||||
color: #b4b4b4
|
||||
.group-description
|
||||
overflow: hidden
|
||||
.status
|
||||
float: right
|
||||
margin: 1em
|
||||
|
||||
.score
|
||||
float: right
|
||||
width: 20%
|
||||
padding: 1em
|
||||
box-sizing: border-box
|
||||
.outcomes
|
||||
background-color: #f7f7f7
|
||||
|
||||
ul
|
||||
margin-left: 0
|
||||
|
||||
li
|
||||
list-style-type: none
|
||||
overflow: hidden
|
||||
|
||||
li.outcome
|
||||
border-top: $outcome-border
|
||||
|
||||
.outcome-properties
|
||||
float: left
|
||||
width: 80%
|
||||
padding: 1em
|
||||
box-sizing: border-box
|
||||
|
||||
.description
|
||||
color: #b4b4b4
|
||||
|
||||
.title
|
||||
font-weight: bold
|
||||
|
||||
.score
|
||||
float: right
|
||||
width: 20%
|
||||
padding: 1em
|
||||
box-sizing: border-box
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<div class="group-description">
|
||||
<h3>{{title}}</h3>
|
||||
<div class="status">
|
||||
{{mastery_count}} / {{count}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="outcomes">
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
<div class="outcome-properties">
|
||||
<div class="title">{{title}}</div>
|
||||
<div class="description">{{{description}}}</div>
|
||||
</div>
|
||||
<div class="score">
|
||||
{{#if score_defined}}{{score}}{{else}}-{{/if}}/{{mastery_points}}
|
||||
</div>
|
|
@ -0,0 +1,3 @@
|
|||
<h2>{{path}}</h2>
|
||||
<div class="groups">
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
<div class="outcome">
|
||||
<div class="title">{{title}}</div>
|
||||
<div class="description">{{{description}}}</div>
|
||||
</div>
|
||||
<div class="score">{{#if score_defined}}
|
||||
{{score}}/{{points_possible}}
|
||||
{{/if}}</div>
|
|
@ -75,16 +75,18 @@ module Api::V1::Outcome
|
|||
end
|
||||
end
|
||||
|
||||
def outcome_link_json(outcome_link, user, session)
|
||||
def outcome_link_json(outcome_link, user, session, opts={})
|
||||
opts[:outcome_style] ||= :abbrev
|
||||
opts[:outcome_group_style] ||= :abbrev
|
||||
api_json(outcome_link, user, session, :only => %w(context_type context_id)).tap do |hash|
|
||||
hash['url'] = polymorphic_path [:api_v1, outcome_link.context || :global, :outcome_link],
|
||||
:id => outcome_link.associated_asset_id,
|
||||
:outcome_id => outcome_link.content_id
|
||||
hash['outcome_group'] = outcome_group_json(outcome_link.associated_asset, user, session, :abbrev)
|
||||
hash['outcome_group'] = outcome_group_json(outcome_link.associated_asset, user, session, opts[:outcome_group_style])
|
||||
# use learning_outcome_content vs. content in case
|
||||
# learning_outcome_content has been preloaded (e.g. by
|
||||
# ContentTag.order_by_outcome_title)
|
||||
hash['outcome'] = outcome_json(outcome_link.learning_outcome_content, user, session, :abbrev)
|
||||
hash['outcome'] = outcome_json(outcome_link.learning_outcome_content, user, session, opts[:outcome_style])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -106,7 +106,7 @@ describe "grades" do
|
|||
f('#navpills').should_not be_nil
|
||||
f('a[href="#outcomes"]').click
|
||||
wait_for_ajaximations
|
||||
ff('#outcomes li').count.should == @course.learning_outcome_links.count
|
||||
ff('#outcomes li.outcome').count.should == @course.learning_outcome_links.count
|
||||
end
|
||||
|
||||
context 'student view' do
|
||||
|
|
Loading…
Reference in New Issue