foundation for ember-based screenreader gradebook
this is the initial work on the screenreader interface for gradebook and is very much a work in progress at this point all that's expected to work is the initial fetching of data and the template binding that updates when choosing a student or assignment from their respective dropdowns merging to allow work to continue in parallel closes CNVS-9478 Change-Id: I38db029a86f52b89e0889c7e9189911e5519348b Reviewed-on: https://gerrit.instructure.com/26938 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Aaron Shafovaloff <ashafovaloff@instructure.com> Product-Review: Matthew Irish <mirish@instructure.com> QA-Review: Matthew Irish <mirish@instructure.com>
This commit is contained in:
parent
ac6fafdde3
commit
a4ec80a26e
|
@ -0,0 +1,89 @@
|
|||
#converted to coffeescript from:
|
||||
#https://gist.github.com/kselden/7758990
|
||||
|
||||
define [
|
||||
'ember'
|
||||
], ({Component, get, set}) ->
|
||||
doc = document
|
||||
|
||||
FastSelectComponent = Component.extend
|
||||
|
||||
initialized: false
|
||||
items: null
|
||||
valuePath: 'value'
|
||||
labelPath: 'label'
|
||||
labelDefault: null
|
||||
valueDefault: ''
|
||||
value: null
|
||||
selected: null
|
||||
|
||||
tagName: 'select'
|
||||
|
||||
didInsertElement: ->
|
||||
self = this
|
||||
@$().on('change', ->
|
||||
set(self, 'value', @value)
|
||||
)
|
||||
|
||||
valueDidChange: (->
|
||||
items = @items
|
||||
value = @value
|
||||
selected = null
|
||||
if (value && items)
|
||||
selected = items.findBy(@valuePath, value)
|
||||
set(this, 'selected', selected)
|
||||
).observes('value').on('init')
|
||||
|
||||
itemsWillChange: (->
|
||||
items = @items
|
||||
if (items)
|
||||
items.removeArrayObserver(this)
|
||||
@arrayWillChange(items, 0, get(items, 'length'), 0)
|
||||
).observesBefore('items').on('willDestroyElement')
|
||||
|
||||
itemsDidChange: (->
|
||||
items = @items
|
||||
if (items)
|
||||
items.addArrayObserver(this)
|
||||
@arrayDidChange(items, 0, 0, get(items, 'length'))
|
||||
@insertDefaultOption()
|
||||
).observes('items').on('didInsertElement')
|
||||
|
||||
arrayWillChange: (items, start, removeCount, addCount) ->
|
||||
select = get(this, 'element')
|
||||
options = select.childNodes
|
||||
i = start + removeCount - 1
|
||||
while i >= start
|
||||
select.removeChild(options[i])
|
||||
i--
|
||||
|
||||
arrayDidChange: (items, start, removeCount, addCount) ->
|
||||
select = get(this, 'element')
|
||||
i = start
|
||||
l = start + addCount
|
||||
|
||||
while i < l
|
||||
item = items.objectAt(i)
|
||||
value = get(item, @valuePath)
|
||||
label = get(item, @labelPath)
|
||||
option = doc.createElement("option")
|
||||
option.textContent = label
|
||||
option.value = value
|
||||
if (@value == value)
|
||||
option.selected = true
|
||||
set(this, 'selected', item)
|
||||
select.appendChild(option)
|
||||
i++
|
||||
|
||||
set(this, 'value', select.value)
|
||||
|
||||
insertDefaultOption: ->
|
||||
return unless @labelDefault and not @isInitialized
|
||||
select = get(this, 'element')
|
||||
option = doc.createElement("option")
|
||||
option.textContent = @labelDefault
|
||||
option.value = @valueDefault
|
||||
select.appendChild(option)
|
||||
|
||||
set(@, 'isInitialized', true)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
define ['ember'], (Ember) ->
|
||||
|
||||
Ember.Application.extend
|
||||
|
||||
rootElement: '#content'
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
define ->
|
||||
route = ->
|
||||
@resource "screenreader_gradebook", path: '/'
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
define ['i18n!sr_gradebook', 'ember', 'underscore'], (I18n, Ember, _) ->
|
||||
|
||||
# http://emberjs.com/guides/controllers/
|
||||
# http://emberjs.com/api/classes/Ember.Controller.html
|
||||
# http://emberjs.com/api/classes/Ember.ArrayController.html
|
||||
# http://emberjs.com/api/classes/Ember.ObjectController.html
|
||||
|
||||
ScreenreaderGradebookController = Ember.ObjectController.extend
|
||||
|
||||
sectionSelectDefaultLabel: I18n.t "all_sections", "All Sections"
|
||||
studentSelectDefaultLabel: I18n.t "no_student", "No Student Selected"
|
||||
assignmentSelectDefaultLabel: I18n.t "no_assignment", "No Assignment Selected"
|
||||
|
||||
students: (->
|
||||
@get('enrollments').map (enrollment) -> enrollment.user
|
||||
).property('enrollments.@each')
|
||||
|
||||
assignments: (->
|
||||
_.flatten(@get('assignment_groups').map (ag) -> ag.assignments)
|
||||
).property('assignment_groups.@each')
|
||||
|
||||
selectedSection: (->
|
||||
@get('sections')[0]
|
||||
).property('sections')
|
||||
|
||||
selectedStudent: (->
|
||||
@get('students')[0]
|
||||
).property('students')
|
||||
|
||||
selectedAssignment: (->
|
||||
@get('assignments')[0]
|
||||
).property('assignments')
|
||||
|
||||
selectedSubmission: (->
|
||||
return null unless @get('selectedStudent')? and @get('selectedAssignment')?
|
||||
student = @get 'selectedStudent'
|
||||
sub = @get('submissions').findBy('user_id', student.id).submissions?.find (submission) =>
|
||||
submission.user_id == @get('selectedStudent').id and
|
||||
submission.assignment_id == @get('selectedAssignment').id
|
||||
sub or {
|
||||
user_id: @get('selectedStudent').id
|
||||
assignment_id: @get('selectedAssignment').id
|
||||
}
|
||||
).property('selectedStudent', 'selectedAssignment')
|
||||
|
||||
selectedSubmissionGrade: (->
|
||||
@get('selectedSubmission')?.grade or '-'
|
||||
).property('selectedSubmission')
|
|
@ -0,0 +1,16 @@
|
|||
define [
|
||||
'ember',
|
||||
'ic-ajax',
|
||||
'../../shared/xhr/fetch_all_pages',
|
||||
'underscore'
|
||||
]
|
||||
, (Ember, ajax, fetchAllPages, _) ->
|
||||
|
||||
ScreenreaderGradebookRoute = Ember.Route.extend
|
||||
|
||||
model: ->
|
||||
#TODO figure out why submissions isn't paginating
|
||||
enrollments: fetchAllPages(ENV.GRADEBOOK_OPTIONS.students_url)
|
||||
assignment_groups: fetchAllPages(ENV.GRADEBOOK_OPTIONS.assignment_groups_url)
|
||||
submissions: fetchAllPages(ENV.GRADEBOOK_OPTIONS.submissions_url, student_ids: 'all')
|
||||
sections: fetchAllPages(ENV.GRADEBOOK_OPTIONS.sections_url)
|
|
@ -0,0 +1,231 @@
|
|||
<h1>Screenreader Gradebook</h1>
|
||||
|
||||
<!-- global -->
|
||||
<!-- -------- -->
|
||||
|
||||
<h2>Global Settings</h2>
|
||||
<!-- filter by section -->
|
||||
<div>
|
||||
<label for="section_select">Select a section</label>
|
||||
{{
|
||||
fast-select
|
||||
id="section_select"
|
||||
class="section_select"
|
||||
items=sections
|
||||
valuePath="id"
|
||||
labelPath="name"
|
||||
labelDefault=sectionSelectDefaultLabel
|
||||
selected=selectedSection
|
||||
}}
|
||||
</div>
|
||||
|
||||
<!-- filter by name or secondary id -->
|
||||
|
||||
<!-- don't think this makes sense for this interface -->
|
||||
|
||||
<!-- grading history -->
|
||||
|
||||
<a href="#history">View Grading History</a>
|
||||
|
||||
<!-- download scores -->
|
||||
|
||||
<a href="#download_scores">Download Scores (.csv)</a>
|
||||
|
||||
<!-- upload scores -->
|
||||
|
||||
<a href="#upload_scores">Upload Scores (.csv)</a>
|
||||
|
||||
<!-- set group weights -->
|
||||
|
||||
<a href="#set_group_weights">Set Group Weiths</a>
|
||||
|
||||
<!-- show/hide student names -->
|
||||
|
||||
<!-- not sure this makes sense in this interface -->
|
||||
|
||||
<!-- arrange columns by due date -->
|
||||
|
||||
<fieldset>
|
||||
<legend>Arrange Assignments</legend>
|
||||
<input type="radio" name="arrange_assignments" id="assigns_alphabetical" value="assigns_alphabetical">
|
||||
<label for="assigns_alphabetical">Alphabetically</label><br />
|
||||
<input type="radio" name="arrange_assignments" id="assigns_position" value="assigns_position" checked>
|
||||
<label for="assigns_position">By Assignment Group and Position</label><br />
|
||||
<input type="radio" name="arrange_assignments" id="assigns_due_date" value="assigns_due_date">
|
||||
<label for="assigns_due_date">By Due Date</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- treat ungraded as 0 -->
|
||||
|
||||
<div>
|
||||
<label for="ungraded">Treat Ungraded as 0</label>
|
||||
<input type="checkbox" name="ungraded" id="ungraded" value="ungraded">
|
||||
</div>
|
||||
|
||||
<!-- show concluded enrollments -->
|
||||
|
||||
<div>
|
||||
<label for="concluded_enrollments">Show Concluded Enrollments</label>
|
||||
<input type="checkbox" name="concluded_enrollments" id="concluded_enrollments" value="concluded_enrollments">
|
||||
</div>
|
||||
|
||||
<!-- -------- -->
|
||||
|
||||
<h2>Content Selection</h2>
|
||||
|
||||
<div>
|
||||
<label for="student_select">Select a student</label>
|
||||
{{
|
||||
fast-select id="student_select"
|
||||
class="student_select"
|
||||
items=students
|
||||
valuePath="id"
|
||||
labelPath="name"
|
||||
labelDefault=studentSelectDefaultLabel
|
||||
selected=selectedStudent
|
||||
}}
|
||||
</div>
|
||||
|
||||
<!-- not sure we need this
|
||||
<div>
|
||||
<label for="assignment_group_select">Select an assignment group</label>
|
||||
<select
|
||||
id="assignment_group_select"
|
||||
class="assignment_group_select"
|
||||
name="assignment_group">
|
||||
<option value="">All</option>
|
||||
<option value="2">Test Assignment Group</option>
|
||||
</select>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div>
|
||||
<label for="assignment_select">Select an assignment</label>
|
||||
{{
|
||||
fast-select id="assignment_select"
|
||||
class="assignment_select"
|
||||
items=assignments
|
||||
valuePath="id"
|
||||
labelPath="name"
|
||||
labelDefault=assignmentSelectDefaultLabel
|
||||
selected=selectedAssignment
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div id="student_navigation">
|
||||
<a href="#prev_student" id="prev_student">Previous Student</a> <a href="#next_student" id="next_student">Next Student</a>
|
||||
</div>
|
||||
<div id="assignment_navigation">
|
||||
<a href="#prev_assignment" id="prev_assignment">Previous Assignment</a> <a href="#next_assignment" id="next_assignment">Next Assignment</a>
|
||||
</div>
|
||||
|
||||
<!-- student + assignment -->
|
||||
<!-- -------------------- -->
|
||||
|
||||
{{#if selectedSubmission}}
|
||||
|
||||
<div id="grading">
|
||||
|
||||
<h2>Grading</h2>
|
||||
|
||||
<!-- see/change a student's grade for a specific assignment -->
|
||||
|
||||
<div>
|
||||
<label for="student_and_assignment_grade">Grade</label>
|
||||
{{
|
||||
input
|
||||
id="student_and_assignment_grade"
|
||||
name=grade
|
||||
valueBinding=selectedSubmissionGrade
|
||||
}}
|
||||
|
||||
</div>
|
||||
|
||||
<a href="#details">Submission Details</a>
|
||||
|
||||
</div>
|
||||
|
||||
{{/if}}
|
||||
|
||||
<!-- student -->
|
||||
<!-- -------- -->
|
||||
|
||||
{{#if selectedStudent}}
|
||||
{{#with selectedStudent}}
|
||||
|
||||
<div id="student_information">
|
||||
|
||||
<h2>Student Information</h2>
|
||||
|
||||
<div>Selected Student: {{name}}</div>
|
||||
|
||||
<!-- what section they are in -->
|
||||
|
||||
<div>Section: Section 3</div>
|
||||
|
||||
<!-- secondary id -->
|
||||
|
||||
<div>Secondary ID: {{login_id}}</div>
|
||||
|
||||
<!-- final score/grade -->
|
||||
|
||||
<div>Final Grade: {{enrollment.grades.final_grade}} ({{enrollment.grades.final_score}})</div>
|
||||
|
||||
<!-- sub-total grade for each assignment group (percentage + points) -->
|
||||
|
||||
<div>Assignment Group Grade: 30% (50% of grade)</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{/with}}
|
||||
{{/if}}
|
||||
|
||||
<!-- assignment -->
|
||||
<!-- ------------ -->
|
||||
|
||||
{{#if selectedAssignment}}
|
||||
{{#with selectedAssignment}}
|
||||
|
||||
<div id="assignment_information">
|
||||
|
||||
<h2>Assignment Information</h2>
|
||||
|
||||
<div>Selected Assignment: {{name}}</div>
|
||||
|
||||
<!-- how many points is the assignment worth -->
|
||||
|
||||
<div>Points possible: {{points_possible}}</div>
|
||||
|
||||
<!-- is the assignment muted -->
|
||||
|
||||
<div>
|
||||
<label for="muted">Muted</label>
|
||||
<input type="checkbox" name="muted" id="muted" value="muted"><br>
|
||||
</div>
|
||||
|
||||
<!-- assignment stats: average/high/low score + submission count -->
|
||||
|
||||
<div>Average Score: 5</div>
|
||||
<div>High Score: 10</div>
|
||||
<div>Low Score: 1</div>
|
||||
|
||||
<!-- go to assignment in speedgrader -->
|
||||
|
||||
<a href="#speedgrader">See this assignment in speedgrader</a>
|
||||
|
||||
<!-- message students who -->
|
||||
|
||||
<a href="#message_student_who">Message students who...</a>
|
||||
|
||||
<!-- set default grades -->
|
||||
|
||||
<a href="#set_default_grade">Set default grade</a>
|
||||
|
||||
<!-- curve grades -->
|
||||
|
||||
<a href="#curve_grades">Curve Grades</a>
|
||||
|
||||
</div>
|
||||
|
||||
{{/with}}
|
||||
{{/if}}
|
|
@ -0,0 +1,81 @@
|
|||
define [
|
||||
'./start_app'
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
], (startApp, Ember, ajax) ->
|
||||
|
||||
App = null
|
||||
|
||||
window.ENV.GRADEBOOK_OPTIONS = {
|
||||
students_url: '/api/v1/enrollments'
|
||||
assignment_groups_url: '/api/v1/assignment_groups'
|
||||
submissions_url: '/api/v1/submissions'
|
||||
sections_url: '/api/v1/sections'
|
||||
}
|
||||
|
||||
ajax.defineFixture window.ENV.GRADEBOOK_OPTIONS.students_url,
|
||||
response: [
|
||||
{
|
||||
user: { id: 1, name: 'Bob' }
|
||||
}
|
||||
{
|
||||
user: { id: 2, name: 'Fred' }
|
||||
}
|
||||
]
|
||||
jqXHR: { getResponseHeader: -> {} }
|
||||
textStatus: ''
|
||||
|
||||
ajax.defineFixture window.ENV.GRADEBOOK_OPTIONS.assignment_groups_url,
|
||||
response: [
|
||||
{
|
||||
id: 1
|
||||
name: 'AG1'
|
||||
assignments: [
|
||||
{ id: 1, name: 'Eat Soup', points_possible: 5 }
|
||||
{ id: 2, name: 'Drink Water', points_possible: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
jqXHR: { getResponseHeader: -> {} }
|
||||
textStatus: ''
|
||||
|
||||
ajax.defineFixture window.ENV.GRADEBOOK_OPTIONS.submissions_url,
|
||||
response: [
|
||||
{ id: 1, user_id: 1, assignment_id: 1, grade: '3' }
|
||||
{ id: 2, user_id: 1, assignment_id: 2, grade: null }
|
||||
]
|
||||
jqXHR: { getResponseHeader: -> {} }
|
||||
textStatus: ''
|
||||
|
||||
ajax.defineFixture window.ENV.GRADEBOOK_OPTIONS.sections_url,
|
||||
response: [
|
||||
{ id: 1, name: 'Section 1' }
|
||||
{ id: 2, name: 'Section 2' }
|
||||
]
|
||||
jqXHR: { getResponseHeader: -> {} }
|
||||
textStatus: ''
|
||||
|
||||
module 'screenreader_gradebook',
|
||||
setup: ->
|
||||
App = startApp()
|
||||
teardown: ->
|
||||
Ember.run App, 'destroy'
|
||||
|
||||
test 'fetches enrollments', ->
|
||||
controller = App.__container__.lookup('controller:screenreader_gradebook')
|
||||
equal controller.get('enrollments').objectAt(0).user.name, 'Bob'
|
||||
equal controller.get('enrollments').objectAt(1).user.name, 'Fred'
|
||||
|
||||
test 'fetches assignment_groups', ->
|
||||
controller = App.__container__.lookup('controller:screenreader_gradebook')
|
||||
equal controller.get('assignment_groups').objectAt(0).name, 'AG1'
|
||||
|
||||
test 'fetches submissions', ->
|
||||
controller = App.__container__.lookup('controller:screenreader_gradebook')
|
||||
equal controller.get('submissions').objectAt(0).grade, '3'
|
||||
equal controller.get('submissions').objectAt(1).grade, null
|
||||
|
||||
test 'fetches sections', ->
|
||||
controller = App.__container__.lookup('controller:screenreader_gradebook')
|
||||
equal controller.get('sections').objectAt(0).name, 'Section 1'
|
||||
equal controller.get('sections').objectAt(1).name, 'Section 2'
|
|
@ -0,0 +1,19 @@
|
|||
define ['../main'], (Application) ->
|
||||
startApp = () ->
|
||||
App = null
|
||||
Ember.run.join ->
|
||||
App = Application.create
|
||||
LOG_ACTIVE_GENERATION: yes
|
||||
LOG_MODULE_RESOLVER: yes
|
||||
LOG_TRANSITIONS: yes
|
||||
LOG_TRANSITIONS_INTERNAL: yes
|
||||
LOG_VIEW_LOOKUPS: yes
|
||||
rootElement: '#fixtures'
|
||||
App.Router.reopen history: 'none'
|
||||
App.setupForTesting()
|
||||
App.injectTestHelpers()
|
||||
App.advanceReadiness()
|
||||
window.App = App
|
||||
App
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
define [
|
||||
'ember'
|
||||
'ic-ajax'
|
||||
'./parse_link_header'
|
||||
], ({$, ArrayProxy}, ajax, parseLinkHeader) ->
|
||||
|
||||
fetch = (url, records, data) ->
|
||||
opts = $.extend({dataType: "json"}, {data: data})
|
||||
ajax(url, opts).then (result) ->
|
||||
records.pushObjects result.response
|
||||
meta = parseLinkHeader result.jqXHR
|
||||
if meta.next
|
||||
fetch meta.next, records, data
|
||||
|
||||
fetchAllPages = (url, data) ->
|
||||
records = ArrayProxy.create({content: []})
|
||||
fetch url, records, data
|
||||
records
|
|
@ -5,8 +5,18 @@ class Gradebook2Controller < ApplicationController
|
|||
|
||||
def show
|
||||
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
||||
@gradebook_is_editable = @context.grants_right?(@current_user, session, :manage_grades)
|
||||
set_js_env
|
||||
end
|
||||
end
|
||||
|
||||
def screenreader
|
||||
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
||||
set_js_env
|
||||
end
|
||||
end
|
||||
|
||||
def set_js_env
|
||||
@gradebook_is_editable = @context.grants_right?(@current_user, session, :manage_grades)
|
||||
per_page = Setting.get('api_max_per_page', '50').to_i
|
||||
js_env :GRADEBOOK_OPTIONS => {
|
||||
:chunk_size => Setting.get('gradebook2.submissions_chunk_size', '35').to_i,
|
||||
|
@ -29,5 +39,4 @@ class Gradebook2Controller < ApplicationController
|
|||
:draft_state_enabled => @context.feature_enabled?(:draft_state)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<% js_bundle :screenreader_gradebook %>
|
||||
<% content_for :page_title, t(:page_titles, "Screenreader Gradebook") %>
|
|
@ -242,7 +242,11 @@ routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resource :gradebook2, :controller => :gradebook2
|
||||
resource :gradebook2, :controller => :gradebook2 do
|
||||
collection do
|
||||
get :screenreader
|
||||
end
|
||||
end
|
||||
match 'attendance' => 'gradebooks#attendance', :as => :attendance
|
||||
match 'attendance/:user_id' => 'gradebooks#attendance', :as => :attendance_user
|
||||
concerns :zip_file_imports
|
||||
|
|
Loading…
Reference in New Issue