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:
Matthew Irish 2013-12-09 15:39:05 -06:00
parent ac6fafdde3
commit a4ec80a26e
12 changed files with 552 additions and 24 deletions

View File

@ -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)

View File

@ -0,0 +1,7 @@
define ['ember'], (Ember) ->
Ember.Application.extend
rootElement: '#content'

View File

@ -0,0 +1,4 @@
define ->
route = ->
@resource "screenreader_gradebook", path: '/'

View File

@ -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')

View File

@ -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)

View File

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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
<% js_bundle :screenreader_gradebook %>
<% content_for :page_title, t(:page_titles, "Screenreader Gradebook") %>

View File

@ -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