give plugins a way to inject alternate grade export formats

Change-Id: Icd4ddc52cc3190ba28fb1d93821642ef2b119a3c
Reviewed-on: https://gerrit.instructure.com/3323
Reviewed-by: JT Olds <jt@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
This commit is contained in:
JT Olds 2011-04-27 18:16:51 -06:00
parent 9ab40ec477
commit 128fc92e32
4 changed files with 105 additions and 24 deletions

View File

@ -823,7 +823,7 @@ class Course < ActiveRecord::Base
return "unpublished" unless statuses.size > 0 return "unpublished" unless statuses.size > 0
# to fake a course-level grade publishing status, we look at all possible # to fake a course-level grade publishing status, we look at all possible
# enrollments, and return statuses if we find any, in this order. # enrollments, and return statuses if we find any, in this order.
["error", "unpublished", "pending", "publishing", "published"].each do |status| ["error", "unpublished", "pending", "publishing", "published", "unpublishable"].each do |status|
return status if statuses.has_key?(status) return status if statuses.has_key?(status)
end end
return "error" return "error"
@ -842,7 +842,18 @@ class Course < ActiveRecord::Base
:last_publish_attempt_at => last_publish_attempt_at :last_publish_attempt_at => last_publish_attempt_at
send_later_if_production(:send_final_grades_to_endpoint, publishing_user) send_later_if_production(:send_final_grades_to_endpoint, publishing_user)
send_at(last_publish_attempt_at + settings[:success_timeout].to_i.seconds, :expire_pending_grade_publishing_statuses, last_publish_attempt_at) if settings[:success_timeout].present? && settings[:wait_for_success] send_at(last_publish_attempt_at + settings[:success_timeout].to_i.seconds, :expire_pending_grade_publishing_statuses, last_publish_attempt_at) if settings[:success_timeout].present? && settings[:wait_for_success] && Rails.env.production?
end
def self.valid_grade_export_types
@valid_grade_export_types ||= {
"instructure_csv" => {
:name => "Instructure formatted CSV",
:callback => lambda { |course, enrollments, publishing_pseudonym|
course.generate_grade_publishing_csv_output(enrollments, publishing_pseudonym)
}
}
}
end end
def send_final_grades_to_endpoint(publishing_user) def send_final_grades_to_endpoint(publishing_user)
@ -854,35 +865,46 @@ class Course < ActiveRecord::Base
raise "endpoint undefined" if settings[:publish_endpoint].nil? || settings[:publish_endpoint].empty? raise "endpoint undefined" if settings[:publish_endpoint].nil? || settings[:publish_endpoint].empty?
enrollments = self.student_enrollments.scoped({:include => [:user, :course_section]}).find(:all, :order => "users.sortable_name") enrollments = self.student_enrollments.scoped({:include => [:user, :course_section]}).find(:all, :order => "users.sortable_name")
enrollment_ids = []
publishing_pseudonym = publishing_user.pseudonyms.active.find_by_account_id(self.root_account_id, :order => "sis_user_id DESC") publishing_pseudonym = publishing_user.pseudonyms.active.find_by_account_id(self.root_account_id, :order => "sis_user_id DESC")
errors = []
posts_to_make = []
ignored_enrollment_ids = []
if Course.valid_grade_export_types.has_key?(settings[:format_type])
callback = Course.valid_grade_export_types[settings[:format_type]][:callback]
posts_to_make, ignored_enrollments_ids = callback.call(self, enrollments,
publishing_pseudonym)
end
Enrollment.update ignored_enrollment_ids, [{ :grade_publishing_status => "unpublishable" }] * ignored_enrollment_ids.size
posts_to_make.each do |enrollment_ids, res, mime_type|
begin
SSLCommon.post_data(settings[:publish_endpoint], res, mime_type)
Enrollment.update enrollment_ids, [{ :grade_publishing_status => (settings[:wait_for_success] == "yes" ? "publishing" : "published") }] * enrollment_ids.size
rescue => e
errors << e
Enrollment.update enrollment_ids, [{ :grade_publishing_status => "error" }] * enrollment_ids.size
end
end
raise errors[0] if errors.size > 0
end
def generate_grade_publishing_csv_output(enrollments, publishing_pseudonym)
enrollment_ids = []
res = FasterCSV.generate do |csv| res = FasterCSV.generate do |csv|
csv << ["publisher_id", "publisher_sis_id", "section_id", "section_sis_id", "student_id", "student_sis_id", "enrollment_id", "enrollment_status", "grade"] csv << ["publisher_id", "publisher_sis_id", "section_id", "section_sis_id", "student_id", "student_sis_id", "enrollment_id", "enrollment_status", "grade"]
enrollments.each do |enrollment| enrollments.each do |enrollment|
enrollment_ids << enrollment.id enrollment_ids << enrollment.id
enrollment.user.pseudonyms.active.find_all_by_account_id(self.root_account_id).each do |user_pseudonym| enrollment.user.pseudonyms.active.find_all_by_account_id(self.root_account_id).each do |user_pseudonym|
csv << [publishing_pseudonym.try(:id), publishing_pseudonym.try(:sis_user_id), enrollment.course_section.id, enrollment.course_section.sis_source_id, user_pseudonym.id, user_pseudonym.sis_user_id, enrollment.id, enrollment.workflow_state, enrollment.computed_final_grade] csv << [publishing_pseudonym.try(:id), publishing_pseudonym.try(:sis_user_id), enrollment.course_section.id, enrollment.course_section.sis_source_id, user_pseudonym.id, user_pseudonym.sis_user_id, enrollment.id, enrollment.workflow_state, enrollment.computed_final_grade]
end end
end end
end end
return [[enrollment_ids, res, "text/csv"]], []
publish_status = "error"
error = nil
begin
SSLCommon.post_data(settings[:publish_endpoint], res, 'text/csv')
publish_status = (settings[:wait_for_success] == "yes" ? "publishing" : "published")
rescue => e
error = e
end
Enrollment.update enrollment_ids, [{ :grade_publishing_status => publish_status }] * enrollment_ids.size
raise error unless error.nil?
end end
def expire_pending_grade_publishing_statuses(last_publish_attempt_at) def expire_pending_grade_publishing_statuses(last_publish_attempt_at)

View File

@ -1,16 +1,20 @@
<% fields_for :settings, OpenObject.new(settings) do |f| %> <% fields_for :settings, OpenObject.new(settings) do |f| %>
<table style="width: 500px;" class="formtable"> <table style="width: 500px;" class="formtable">
<tr> <tr>
<td><%= f.label :enabled, "Enabled:" %></td> <td colspan=2><%= f.check_box :enabled, {}, 'true', 'false' %>
<td><%= f.check_box :enabled, {}, 'true', 'false' %></td> <%= f.label :enabled, "Enabled:" %></td>
</tr>
<tr>
<td><%= f.label :format_type, "Output format type:" %></td>
<td><%= f.select :format_type, Course.valid_grade_export_types.keys.map{|k| [Course.valid_grade_export_types[k][:name], k]}, {} %>
</tr> </tr>
<tr> <tr>
<td><%= f.label :publish_endpoint, "Endpoint to publish to:" %></td> <td><%= f.label :publish_endpoint, "Endpoint to publish to:" %></td>
<td><%= f.text_field :publish_endpoint %></td> <td><%= f.text_field :publish_endpoint %></td>
</tr> </tr>
<tr> <tr>
<td><%= f.label :wait_for_success, "Wait for success notifications:" %></td> <td colspan=2><%= f.check_box :wait_for_success, {}, 'yes', 'no' %>
<td><%= f.check_box :wait_for_success, {}, 'yes', 'no' %></td> <%= f.label :wait_for_success, "Wait for success notifications:" %></td>
</tr> </tr>
<tr> <tr>
<td><%= f.label :success_timeout, "Success notification timeout (in seconds):" %></td> <td><%= f.label :success_timeout, "Success notification timeout (in seconds):" %></td>

View File

@ -62,6 +62,7 @@ Canvas::Plugin.register 'common_cartridge_importer', :export_system, {
} }
} }
Canvas::Plugin.register('grade_export', :sis, { Canvas::Plugin.register('grade_export', :sis, {
:name => "Grade Export",
:description => 'Grade Export for SIS', :description => 'Grade Export for SIS',
:website => 'http://www.instructure.com', :website => 'http://www.instructure.com',
:author => 'Instructure', :author => 'Instructure',
@ -71,5 +72,6 @@ Canvas::Plugin.register('grade_export', :sis, {
:settings => { :enabled => "false", :settings => { :enabled => "false",
:publish_endpoint => "", :publish_endpoint => "",
:wait_for_success => "no", :wait_for_success => "no",
:success_timeout => "600" } :success_timeout => "600",
:format_type => "instructure_csv" }
}) })

View File

@ -17,6 +17,7 @@
# #
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb') require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
require 'socket'
describe Course do describe Course do
before(:each) do before(:each) do
@ -456,6 +457,7 @@ describe Course, "backup" do
end end
end end
end end
def course_to_backup def course_to_backup
@course = course @course = course
group = @course.assignment_groups.create!(:name => "Some Assignment Group") group = @course.assignment_groups.create!(:name => "Some Assignment Group")
@ -466,3 +468,54 @@ def course_to_backup
topic.discussion_entries.create!(:message => "just a test") topic.discussion_entries.create!(:message => "just a test")
@course @course
end end
describe Course, 'grade_publishing' do
before(:each) do
@course = Course.new
end
after(:each) do
Course.valid_grade_export_types.delete("test_export")
PluginSetting.settings_for_plugin('grade_export')[:format_type] = "instructure_csv"
PluginSetting.settings_for_plugin('grade_export')[:enabled] = "false"
PluginSetting.settings_for_plugin('grade_export')[:publish_endpoint] = ""
end
it 'should pass a quick sanity check' do
user = User.new
Course.valid_grade_export_types["test_export"] = {
:name => "test export",
:callback => lambda {|course, enrollments, publishing_pseudonym|
course.should == @course
publishing_pseudonym.should == nil
return [[[], "test-jt-data\r\n\r\n", "application/jtmimetype"]], []
}}
PluginSetting.settings_for_plugin('grade_export')[:enabled] = "true"
PluginSetting.settings_for_plugin('grade_export')[:format_type] = "test_export"
server = TCPServer.open(0)
port = server.addr[1]
PluginSetting.settings_for_plugin('grade_export')[:publish_endpoint] = "http://localhost:#{port}/endpoint"
post_lines = []
server_thread = Thread.new(server, post_lines) { |server, post_lines|
client = server.accept
loop do
line = client.readline
post_lines << line.strip unless line =~ /^Host: localhost:/
break if line =~ /test-jt-data/
end
client.puts("HTTP/1.1 200 OK\nContent-Length: 0\n\n")
client.close
server.close
}
@course.publish_final_grades(user)
server_thread.join
post_lines.should == [
"POST /endpoint HTTP/1.1",
"Accept: */*",
"Content-Type: application/jtmimetype",
"Content-Length: 16",
"",
"test-jt-data"]
end
end