senkyoshi 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 13edfb731484992561073e292a28f75a15bfaa5d
4
- data.tar.gz: 528e83f4e9f0a60f88a40cd8d503e35b07fa8655
3
+ metadata.gz: d7187188584379d9e573651fe9147e7bb50497ca
4
+ data.tar.gz: e595d29609ad508dfc443cf1543b0eecdd382ea5
5
5
  SHA512:
6
- metadata.gz: 470eafc042455cf9221aac1dca5544fb2ad335c5c9ab5f4fc5c368bd270c6ee40d25ffcb4991f5625351f21ead8220f3178315bf9076d2c455f1d421b9debb1d
7
- data.tar.gz: 57c7176e3b952dbf02dff18ba3546ac02a2ebe35b72bafd3f8b52f220cb9cf759f21c25ab9518d10ebadaafa4fe1eb6f739888badba46d5909de7f39a403cc12
6
+ metadata.gz: c16a2de94714d3b58e2acaa22e17eb58fae79c1e17a13d8c987660ae60ffc3bf8e17d733b7ae1bc116e9d8e672a37d910c0efcce2952caad8d00f9032f94766f
7
+ data.tar.gz: 4d333e8b3a2e0b6000a41d3b49d28a2a2fe17f3add5a1b0248ecda417fa333df4a0aaae9e0c4d980d9bbf599c971ac449dd41adcb840b4726cdbbcc97c715343
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Senkyoshi Converter [![Build Status](https://travis-ci.org/atomicjolt/senkyoshi.svg?branch=master)](https://travis-ci.org/atomicjolt/senkyoshi)
2
2
 
3
- TODO: Describe the gem
3
+ Senkyoshi converts exported Blackboard packages into Canvas .imscc packages. It also allows you to upload those converted packages to a Canvas instance.
4
4
 
5
5
  ## Installation
6
6
 
@@ -21,13 +21,13 @@ Or install it yourself as:
21
21
  $ gem install senkyoshi
22
22
  ```
23
23
 
24
- Create a `Rakefile` and add
24
+ Create a `Rakefile` and add:
25
25
  ```ruby
26
26
  require "senkyoshi/tasks"
27
27
  Senkyoshi::Tasks.install_tasks
28
28
  ```
29
29
 
30
- Create a `senkyoshi.yml` and add credentials
30
+ Create a `senkyoshi.yml` and add credentials:
31
31
  ```yaml
32
32
  # Generally looks like https://< mycanvas_instance >/api
33
33
  :canvas_url: <canvas instance api url>
@@ -55,23 +55,23 @@ Create a `senkyoshi.yml` and add credentials
55
55
 
56
56
  ## Usage
57
57
 
58
- Run the rake task to convert from .zip to .imscc
58
+ Run the rake task to convert from .zip to .imscc:
59
59
  ```sh
60
60
  rake senkyoshi:imscc
61
61
  ```
62
- This will take all your files in your source folder and convert them to your outputs folder
62
+ This will take all your files in your source folder and convert them to your outputs folder.
63
63
 
64
- Run converting files in parallel
64
+ Run converting files in parallel:
65
65
  ```sh
66
66
  time rake senkyoshi:imscc -m
67
67
  ```
68
68
 
69
- Delete entire outputs folder
69
+ Delete entire outputs folder:
70
70
  ```sh
71
71
  rake clean
72
72
  ```
73
73
 
74
- Upload to canvas to process
74
+ Upload to Canvas to process:
75
75
  ```sh
76
76
  rake senkyoshi:upload
77
77
  ```
@@ -84,7 +84,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
84
84
 
85
85
  ## Contributing
86
86
 
87
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/senkyoshi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/atomicjolt/senkyoshi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
88
88
 
89
89
 
90
90
  ## License
@@ -92,6 +92,5 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN
92
92
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
93
93
 
94
94
 
95
- ### Things not quite implemented
96
- People Group Sets
97
- Blogs
95
+ ### Things Not Quite Implemented
96
+ People, Groups, Sets, Blogs
@@ -1,11 +1,7 @@
1
1
  require "pandarus"
2
- require "senkyoshi/config"
3
2
  require "senkyoshi/models/scorm_package"
4
3
  require "rest-client"
5
4
 
6
- require "openssl"
7
- OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
8
-
9
5
  module Senkyoshi
10
6
  ##
11
7
  # This class represents a canvas course for which we are uploading data to
@@ -43,8 +39,8 @@ module Senkyoshi
43
39
  ##
44
40
  def self.client
45
41
  @client ||= Pandarus::Client.new(
46
- prefix: Senkyoshi.canvas_url,
47
- token: Senkyoshi.canvas_token,
42
+ prefix: Senkyoshi.configuration.canvas_url,
43
+ token: Senkyoshi.configuration.canvas_token,
48
44
  )
49
45
  end
50
46
 
@@ -54,7 +50,7 @@ module Senkyoshi
54
50
  def self.from_metadata(metadata, blackboard_export = nil)
55
51
  course_name = metadata[:name] || metadata[:title]
56
52
  canvas_course = client.create_new_course(
57
- Senkyoshi.account_id,
53
+ Senkyoshi.configuration.account_id,
58
54
  course: {
59
55
  name: course_name,
60
56
  },
@@ -66,9 +62,49 @@ module Senkyoshi
66
62
  # Creates a canvas assignment from a scorm package that has already been
67
63
  # uploaded to a scorm manager
68
64
  ##
69
- def create_scorm_assignment(scorm_package, course_id)
70
- url = Senkyoshi.scorm_launch_url +
71
- "?course_id=#{scorm_package['package_id']}"
65
+ def create_scorm_assignment(scorm_package, course_id, local)
66
+ if local
67
+ _create_scorm_assignment_local(scorm_package)
68
+ else
69
+ _create_scorm_assignment_external(scorm_package, course_id)
70
+ end
71
+ end
72
+
73
+ ##
74
+ # Assembles the launch url with the course_id
75
+ ##
76
+ def _scorm_launch_url(package_id)
77
+ "#{Senkyoshi.configuration.scorm_launch_url}?course_id=#{package_id}"
78
+ end
79
+
80
+ ##
81
+ # Creates a scorm assignment from a Canvas course object
82
+ ##
83
+ def _create_scorm_assignment_local(scorm_package)
84
+ url = _scorm_launch_url(scorm_package["package_id"])
85
+
86
+ payload = {
87
+ title: scorm_package["title"],
88
+ submission_types: "external_tool",
89
+ integration_id: scorm_package["package_id"],
90
+ integration_data: {
91
+ provider: "atomic-scorm",
92
+ },
93
+ external_tool_tag_attributes: {
94
+ url: url,
95
+ },
96
+ points_possible: scorm_package["points_possible"],
97
+ }
98
+
99
+ # @course_resource in this case is a Canvas course object
100
+ @course_resource.assignments.create(payload)
101
+ end
102
+
103
+ ##
104
+ # Creates a scorm assignment using the Canvas api
105
+ ##
106
+ def _create_scorm_assignment_external(scorm_package, course_id)
107
+ url = _scorm_launch_url(scorm_package["package_id"])
72
108
 
73
109
  payload = {
74
110
  assignment__submission_types__: ["external_tool"],
@@ -97,13 +133,13 @@ module Senkyoshi
97
133
  zip = scorm_package.write_zip tmp_name
98
134
  File.open(zip, "rb") do |file|
99
135
  RestClient.post(
100
- "#{Senkyoshi.scorm_url}/api/scorm_courses",
136
+ "#{Senkyoshi.configuration.scorm_url}/api/scorm_courses",
101
137
  {
102
138
  oauth_consumer_key: "scorm-player",
103
139
  lms_course_id: course_id,
104
140
  file: file,
105
141
  },
106
- SharedAuthorization: Senkyoshi.scorm_shared_auth,
142
+ SharedAuthorization: Senkyoshi.configuration.scorm_shared_auth,
107
143
  ) do |resp|
108
144
  result = JSON.parse(resp.body)["response"]
109
145
  result["points_possible"] = scorm_package.points_possible
@@ -115,8 +151,10 @@ module Senkyoshi
115
151
  ##
116
152
  # Creates assignments from all previously uploaded scorm packages
117
153
  ##
118
- def create_scorm_assignments(scorm_packages, course_id)
119
- scorm_packages.each { |pack| create_scorm_assignment(pack, course_id) }
154
+ def create_scorm_assignments(scorm_packages, course_id, local)
155
+ scorm_packages.each do |pack|
156
+ create_scorm_assignment(pack, course_id, local)
157
+ end
120
158
  end
121
159
 
122
160
  ##
@@ -132,6 +170,14 @@ module Senkyoshi
132
170
  end
133
171
  end
134
172
 
173
+ def process_scorm(local: false)
174
+ create_scorm_assignments(
175
+ upload_scorm_packages(@scorm_packages),
176
+ @course_resource.id,
177
+ local,
178
+ )
179
+ end
180
+
135
181
  ##
136
182
  # Create a migration for the course
137
183
  # and upload the imscc file to be imported into the course
@@ -152,10 +198,7 @@ module Senkyoshi
152
198
  puts "Done uploading: #{name}"
153
199
 
154
200
  puts "Creating Scorm: #{name}"
155
- create_scorm_assignments(
156
- upload_scorm_packages(@scorm_packages),
157
- @course_resource.id,
158
- )
201
+ process_scorm
159
202
  puts "Done creating scorm: #{name}"
160
203
  end
161
204
 
@@ -176,7 +219,7 @@ module Senkyoshi
176
219
  RestClient.post(
177
220
  response.headers[:location],
178
221
  nil,
179
- Authorization: "Bearer #{Senkyoshi.canvas_token}",
222
+ Authorization: "Bearer #{Senkyoshi.configuration.canvas_token}",
180
223
  )
181
224
  end
182
225
  end
@@ -0,0 +1,29 @@
1
+ require "yaml"
2
+
3
+ module Senkyoshi
4
+ class Configuration
5
+ attr_accessor :canvas_url
6
+ attr_accessor :canvas_token
7
+ attr_accessor :account_id
8
+ attr_accessor :scorm_url
9
+ attr_accessor :scorm_launch_url
10
+ attr_accessor :scorm_shared_auth
11
+
12
+ def initialize
13
+ @canvas_url = Configuration._config[:canvas_url]
14
+ @canvas_token = Configuration._config[:canvas_token]
15
+ @account_id = Configuration._config[:account_id] || :self
16
+ @scorm_url = Configuration._config[:scorm_url]
17
+ @scorm_launch_url = Configuration._config[:scorm_launch_url]
18
+ @scorm_shared_auth = Configuration._config[:scorm_shared_auth]
19
+ end
20
+
21
+ def self._config
22
+ @config ||= if File.exists? "senkyoshi.yml"
23
+ YAML::load(File.read("senkyoshi.yml"))
24
+ else
25
+ {}
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,13 +1,13 @@
1
- require "senkyoshi/models/resource"
1
+ require "senkyoshi/models/file_resource"
2
2
 
3
3
  module Senkyoshi
4
- class Announcement < Resource
5
- def initialize
4
+ class Announcement < FileResource
5
+ def initialize(resource_id)
6
+ super(resource_id)
6
7
  @title = ""
7
8
  @text = ""
8
9
  @delayed_post = ""
9
10
  @posted_at = ""
10
- @identifier = Senkyoshi.create_random_hex
11
11
  @dependency = Senkyoshi.create_random_hex
12
12
  @type = "announcement"
13
13
  end
@@ -21,13 +21,13 @@ module Senkyoshi
21
21
  self
22
22
  end
23
23
 
24
- def canvas_conversion(course, _resources = nil)
24
+ def canvas_conversion(course, resources)
25
25
  announcement = CanvasCc::CanvasCC::Models::Announcement.new
26
26
  announcement.title = @title
27
- announcement.text = @text
27
+ announcement.text = fix_html(@text, resources)
28
28
  announcement.delayed_post = @delayed_post
29
29
  announcement.posted_at = @posted_at
30
- announcement.identifier = @identifier
30
+ announcement.identifier = @id
31
31
  announcement.dependency = @dependency
32
32
  course.announcements << announcement
33
33
  course
@@ -16,7 +16,9 @@ module Senkyoshi
16
16
  assignment.submission_types << "online_upload"
17
17
  assignment.grading_type = "points"
18
18
 
19
- @files.each { |f| assignment.body << f.canvas_conversion }
19
+ @files.each do |file|
20
+ assignment.body << file.canvas_conversion(resources)
21
+ end
20
22
  course = create_module(course)
21
23
  course.assignments << assignment
22
24
  end
@@ -3,21 +3,40 @@ require "senkyoshi/models/resource"
3
3
  module Senkyoshi
4
4
  class AssignmentGroup < Resource
5
5
  attr_reader :id
6
- def initialize(name, group_id)
6
+ def initialize(name, id)
7
7
  @title = name
8
8
  @group_weight = ""
9
9
  @rules = {}
10
- @id = group_id
10
+ @id = id
11
11
  end
12
12
 
13
- def canvas_conversion(course, _resources = nil)
13
+ def canvas_conversion
14
14
  assignment_group = CanvasCc::CanvasCC::Models::AssignmentGroup.new
15
15
  assignment_group.identifier = @id
16
16
  assignment_group.title = @title
17
17
  assignment_group.group_weight = @group_weight
18
18
  assignment_group.rules = @rules
19
- course.assignment_groups << assignment_group
20
- course
19
+ assignment_group
20
+ end
21
+
22
+ def self.create_assignment_group(group_name)
23
+ id = Senkyoshi.create_random_hex
24
+ group = AssignmentGroup.new(group_name, id)
25
+ group.canvas_conversion
26
+ end
27
+
28
+ def self.find_group(course, category)
29
+ course.assignment_groups.
30
+ detect { |a| a.title == category }
31
+ end
32
+
33
+ def self.find_or_create(course, category)
34
+ assignment_group = find_group(course, category)
35
+ if !assignment_group
36
+ assignment_group = AssignmentGroup.create_assignment_group(category)
37
+ course.assignment_groups << assignment_group
38
+ end
39
+ assignment_group
21
40
  end
22
41
  end
23
42
  end
@@ -1,8 +1,9 @@
1
- require "senkyoshi/models/resource"
1
+ require "senkyoshi/models/file_resource"
2
2
 
3
3
  module Senkyoshi
4
- class Blog < Resource
5
- def initialize
4
+ class Blog < FileResource
5
+ def initialize(resource_id)
6
+ super(resource_id)
6
7
  @title = ""
7
8
  @description = ""
8
9
  @is_public = true
@@ -1,8 +1,8 @@
1
- require "senkyoshi/models/resource"
1
+ require "senkyoshi/models/file_resource"
2
2
  require "senkyoshi/models/content_file"
3
3
 
4
4
  module Senkyoshi
5
- class Content < Resource
5
+ class Content < FileResource
6
6
  CONTENT_TYPES = {
7
7
  "x-bb-asmt-test-link" => "Quiz",
8
8
  "x-bb-asmt-survey-link" => "Quiz",
@@ -30,12 +30,13 @@ module Senkyoshi
30
30
  "Senkyoshi::Quiz" => "Quizzes::Quiz",
31
31
  }.freeze
32
32
 
33
- attr_accessor(:title, :body, :id, :files, :url)
33
+ attr_accessor(:title, :body, :files, :url)
34
34
  attr_reader(:extendeddata)
35
35
 
36
36
  def self.from(xml, pre_data, resource_xids)
37
37
  type = xml.xpath("/CONTENT/CONTENTHANDLER/@value").first.text
38
38
  type.slice! "resource/"
39
+
39
40
  xml.xpath("//FILES/FILE").each do |file|
40
41
  file_name = ContentFile.clean_xid file.at("NAME").text
41
42
  is_attachment = CONTENT_TYPES[type] == "Attachment"
@@ -44,10 +45,10 @@ module Senkyoshi
44
45
  break
45
46
  end
46
47
  end
48
+
47
49
  if content_type = CONTENT_TYPES[type]
48
50
  content_class = Senkyoshi.const_get content_type
49
- content = content_class.new
50
- content.iterate_xml(xml, pre_data)
51
+ content_class.new(pre_data[:file_name]).iterate_xml(xml, pre_data)
51
52
  end
52
53
  end
53
54
 
@@ -64,10 +65,11 @@ module Senkyoshi
64
65
  @type = xml.xpath("/CONTENT/RENDERTYPE/@value").first.text
65
66
  @parent_id = pre_data[:parent_id]
66
67
  @module_type = MODULE_TYPES[self.class.name]
67
- @id = xml.xpath("/CONTENT/@id").first.text
68
+
68
69
  if pre_data[:assignment_id] && !pre_data[:assignment_id].empty?
69
70
  @id = pre_data[:assignment_id]
70
71
  end
72
+
71
73
  @files = xml.xpath("//FILES/FILE").map do |file|
72
74
  ContentFile.new(file)
73
75
  end
@@ -80,7 +82,7 @@ module Senkyoshi
80
82
  module_item.canvas_conversion
81
83
  end
82
84
 
83
- def get_pre_data(xml, file_name)
85
+ def self.get_pre_data(xml, file_name)
84
86
  id = xml.xpath("/CONTENT/@id").first.text
85
87
  parent_id = xml.xpath("/CONTENT/PARENTID/@value").first.text
86
88
  {
@@ -26,10 +26,15 @@ module Senkyoshi
26
26
  canvas_file.file_path.split("/").last
27
27
  end
28
28
 
29
- def canvas_conversion(canvas_file = nil)
30
- path = canvas_file ? canvas_file.file_path : @linkname
29
+ def canvas_conversion(resources, canvas_file = nil)
30
+ path = if canvas_file
31
+ canvas_file.file_path
32
+ else
33
+ resource = resources.detect_xid(@name)
34
+ resource.path if resource
35
+ end
31
36
  query = "?canvas_download=1&amp;canvas_qs_wrap=1"
32
- href = "$IMS_CC_FILEBASE$/#{path}#{query}"
37
+ href = "#{FILE_BASE}/#{path}#{query}"
33
38
  %{
34
39
  <a
35
40
  class="instructure_scribd_file instructure_file_link"
@@ -1,14 +1,14 @@
1
- require "senkyoshi/models/resource"
1
+ require "senkyoshi/models/file_resource"
2
2
 
3
3
  module Senkyoshi
4
- class Course < Resource
4
+ class Course < FileResource
5
5
  ##
6
6
  # This class represents a reader for one zip file, and allows the usage of
7
7
  # individual files within zip file
8
8
  ##
9
- def initialize
9
+ def initialize(resource_id = nil)
10
+ super(resource_id)
10
11
  @course_code = ""
11
- @identifier = ""
12
12
  @title = ""
13
13
  @description = ""
14
14
  @is_public = false
@@ -17,7 +17,6 @@ module Senkyoshi
17
17
  end
18
18
 
19
19
  def iterate_xml(data, _)
20
- @identifier = data["id"]
21
20
  @title = Senkyoshi.get_attribute_value(data, "TITLE")
22
21
  @description = Senkyoshi.get_description(data)
23
22
  @is_public = Senkyoshi.get_attribute_value(data, "ISAVAILABLE")
@@ -27,7 +26,7 @@ module Senkyoshi
27
26
  end
28
27
 
29
28
  def canvas_conversion(course, _resources = nil)
30
- course.identifier = @identifier
29
+ course.identifier = @id
31
30
  course.title = @title
32
31
  course.description = @description
33
32
  course.is_public = @is_public
@@ -18,10 +18,6 @@ module Senkyoshi
18
18
  Senkyoshi.create_random_hex
19
19
  end
20
20
 
21
- def strip_xid(name)
22
- name.gsub(/__xid-[0-9]+_[0-9]+/, "")
23
- end
24
-
25
21
  def matches_xid?(xid)
26
22
  @xid == xid
27
23
  end
@@ -0,0 +1,23 @@
1
+ require "senkyoshi/models/resource"
2
+
3
+ module Senkyoshi
4
+ ##
5
+ # Class to represent a resource constructed from a single 'dat' file.
6
+ ##
7
+ class FileResource < Resource
8
+ attr_reader(:id)
9
+
10
+ def initialize(id = nil)
11
+ @id = id
12
+ end
13
+
14
+ def self.from(xml, pre_data, _resource_xids = nil)
15
+ resource = new(pre_data[:file_name])
16
+ resource.iterate_xml(xml, pre_data)
17
+ end
18
+
19
+ def iterate_xml(_xml, _pre_data)
20
+ self
21
+ end
22
+ end
23
+ end
@@ -1,11 +1,11 @@
1
- require "senkyoshi/models/resource"
1
+ require "senkyoshi/models/file_resource"
2
2
 
3
3
  module Senkyoshi
4
- class Forum < Resource
5
- def initialize
4
+ class Forum < FileResource
5
+ def initialize(resource_id)
6
+ super(resource_id)
6
7
  @title = ""
7
8
  @text = ""
8
- @identifier = Senkyoshi.create_random_hex
9
9
  @discussion_type = "threaded"
10
10
  end
11
11
 
@@ -19,7 +19,7 @@ module Senkyoshi
19
19
  discussion = CanvasCc::CanvasCC::Models::Discussion.new
20
20
  discussion.title = @title
21
21
  discussion.text = @text
22
- discussion.identifier = @identifier
22
+ discussion.identifier = @id
23
23
  discussion.discussion_type = @discussion_type
24
24
  course.discussions << discussion
25
25
  course
@@ -1,6 +1,23 @@
1
+ require "senkyoshi/models/outcome_definition"
2
+ require "senkyoshi/models/file_resource"
3
+
1
4
  module Senkyoshi
2
- class Gradebook
3
- def get_pre_data(data, _)
5
+ class Gradebook < FileResource
6
+ attr_reader(:outcome_definitions, :categories)
7
+
8
+ def initialize(resource_id = nil, categories = [], outcome_definitions = [])
9
+ super(resource_id)
10
+ @categories = categories
11
+ @outcome_definitions = outcome_definitions
12
+ end
13
+
14
+ def iterate_xml(xml_data, _)
15
+ @categories = Gradebook.get_categories(xml_data)
16
+ @outcome_definitions = get_outcome_definitions(xml_data).compact
17
+ self
18
+ end
19
+
20
+ def self.get_pre_data(data, _)
4
21
  categories = get_categories(data)
5
22
  data.search("OUTCOMEDEFINITIONS").children.map do |outcome|
6
23
  category_id = outcome.at("CATEGORYID").attributes["value"].value
@@ -14,7 +31,7 @@ module Senkyoshi
14
31
  end
15
32
  end
16
33
 
17
- def get_categories(data)
34
+ def self.get_categories(data)
18
35
  data.at("CATEGORIES").children.
19
36
  each_with_object({}) do |category, categories|
20
37
  id = category.attributes["id"].value
@@ -23,5 +40,29 @@ module Senkyoshi
23
40
  categories[id] = title
24
41
  end
25
42
  end
43
+
44
+ def get_outcome_definitions(xml)
45
+ xml.xpath("//OUTCOMEDEFINITION").map do |outcome_def|
46
+ category_id = outcome_def.xpath("CATEGORYID/@value").first.value
47
+ OutcomeDefinition.from(outcome_def, @categories[category_id])
48
+ end
49
+ end
50
+
51
+ def convert_categories(course)
52
+ @categories.each do |category|
53
+ if AssignmentGroup.find_group(course, category.last).nil?
54
+ course.assignment_groups <<
55
+ AssignmentGroup.create_assignment_group(category.last)
56
+ end
57
+ end
58
+ end
59
+
60
+ def canvas_conversion(course, resources = nil)
61
+ convert_categories(course)
62
+ @outcome_definitions.
63
+ select { |outcome_def| OutcomeDefinition.orphan? outcome_def }.
64
+ each { |outcome_def| outcome_def.canvas_conversion course, resources }
65
+ course
66
+ end
26
67
  end
27
68
  end
@@ -1,8 +1,9 @@
1
- require "senkyoshi/models/resource"
1
+ require "senkyoshi/models/file_resource"
2
2
 
3
3
  module Senkyoshi
4
- class Group < Resource
5
- def initialize
4
+ class Group < FileResource
5
+ def initialize(resource_id)
6
+ super(resource_id)
6
7
  @name = ""
7
8
  @description = ""
8
9
  @is_public = true
@@ -0,0 +1,52 @@
1
+ require "senkyoshi/models/resource"
2
+ require "senkyoshi/models/assignment_group"
3
+
4
+ module Senkyoshi
5
+ class OutcomeDefinition < Resource
6
+ include Senkyoshi
7
+ attr_reader :id, :content_id, :asidataid, :is_user_created
8
+ def self.from(xml, category)
9
+ outcome_definition = OutcomeDefinition.new(category)
10
+ outcome_definition.iterate_xml(xml)
11
+ end
12
+
13
+ def initialize(category)
14
+ @category = category
15
+ end
16
+
17
+ def iterate_xml(xml)
18
+ @content_id = xml.xpath("./CONTENTID/@value").text
19
+ @asidataid = xml.xpath("./ASIDATAID/@value").text
20
+ @id = xml.xpath("./@id").text
21
+ @title = xml.xpath("./TITLE/@value").text
22
+ @points_possible = xml.xpath("./POINTSPOSSIBLE/@value").text
23
+ @is_user_created = true? xml.xpath("./ISUSERCREATED/@value").text
24
+ self
25
+ end
26
+
27
+ ##
28
+ # Determine if an outcome definition is user created and linked to any
29
+ # other 'CONTENT' or assignments
30
+ ##
31
+ def self.orphan?(outcome_def)
32
+ outcome_def.content_id.empty? &&
33
+ outcome_def.asidataid.empty? &&
34
+ outcome_def.is_user_created
35
+ end
36
+
37
+ def canvas_conversion(course, _ = nil)
38
+ assignment_group = AssignmentGroup.find_or_create(course, @category)
39
+ assignment = CanvasCc::CanvasCC::Models::Assignment.new
40
+ assignment.identifier = Senkyoshi.create_random_hex
41
+ assignment.assignment_group_identifier_ref = assignment_group.identifier
42
+ assignment.title = @title
43
+ assignment.position = 1
44
+ assignment.points_possible = @points_possible
45
+ assignment.workflow_state = "published"
46
+ assignment.grading_type = "points"
47
+
48
+ course.assignments << assignment
49
+ course
50
+ end
51
+ end
52
+ end
@@ -1,7 +1,7 @@
1
1
  require "senkyoshi/models/assignment_group"
2
2
  require "senkyoshi/models/assignment"
3
3
  require "senkyoshi/models/question"
4
- require "senkyoshi/models/resource"
4
+ require "senkyoshi/models/file_resource"
5
5
 
6
6
  QTI_TYPE = {
7
7
  "Test" => "Assessment",
@@ -10,15 +10,16 @@ QTI_TYPE = {
10
10
  }.freeze
11
11
 
12
12
  module Senkyoshi
13
- class QTI < Resource
14
- def self.from(data, pre_data)
13
+ class QTI < FileResource
14
+ def self.from(data, pre_data, _resource_xids = nil)
15
15
  type = data.at("bbmd_assessmenttype").text
16
16
  qti_class = Senkyoshi.const_get QTI_TYPE[type]
17
- qti = qti_class.new
17
+ qti = qti_class.new(pre_data[:file_name])
18
18
  qti.iterate_xml(data, pre_data)
19
19
  end
20
20
 
21
- def initialize
21
+ def initialize(resource_id = nil)
22
+ super(resource_id)
22
23
  @title = ""
23
24
  @description = ""
24
25
  @quiz_type = ""
@@ -85,7 +86,7 @@ module Senkyoshi
85
86
  end
86
87
  end
87
88
 
88
- def get_pre_data(xml, _)
89
+ def self.get_pre_data(xml, _)
89
90
  {
90
91
  original_file_name: xml.xpath("/COURSEASSESSMENT/
91
92
  ASMTID/@value").first.text,
@@ -109,8 +110,8 @@ module Senkyoshi
109
110
  def canvas_conversion(course, resources)
110
111
  assessment = CanvasCc::CanvasCC::Models::Assessment.new
111
112
  assessment.identifier = @id
112
- course = create_assignment_group(course, resources)
113
- assignment = create_assignment
113
+ assignment_group = AssignmentGroup.find_or_create(course, @group_name)
114
+ assignment = create_assignment(assignment_group.identifier)
114
115
  assignment.quiz_identifier_ref = assessment.identifier
115
116
  course.assignments << assignment
116
117
  assessment = setup_assessment(assessment, assignment, resources)
@@ -187,22 +188,10 @@ module Senkyoshi
187
188
  question_group
188
189
  end
189
190
 
190
- def create_assignment_group(course, resources)
191
- group = course.assignment_groups.detect { |a| a.title == @group_name }
192
- if group
193
- @group_id = group.identifier
194
- else
195
- @group_id = Senkyoshi.create_random_hex
196
- assignment_group = AssignmentGroup.new(@group_name, @group_id)
197
- course = assignment_group.canvas_conversion(course, resources)
198
- end
199
- course
200
- end
201
-
202
- def create_assignment
191
+ def create_assignment(group_id)
203
192
  assignment = CanvasCc::CanvasCC::Models::Assignment.new
204
193
  assignment.identifier = Senkyoshi.create_random_hex
205
- assignment.assignment_group_identifier_ref = @group_id
194
+ assignment.assignment_group_identifier_ref = group_id
206
195
  assignment.title = @title
207
196
  assignment.position = 1
208
197
  assignment.submission_types << "online_quiz"
@@ -2,6 +2,12 @@ require "senkyoshi/models/qti"
2
2
 
3
3
  module Senkyoshi
4
4
  class QuestionBank < QTI
5
+ TAGS = {
6
+ "<p><span size=\"2\" style=\"font-size: small;\">.</span></p>" => "",
7
+ "<p>.</p>" => "",
8
+ }.freeze
9
+ TAGS_RE = Regexp.union(TAGS.keys)
10
+
5
11
  def canvas_conversion(course, resources)
6
12
  question_bank = CanvasCc::CanvasCC::Models::QuestionBank.new
7
13
  question_bank.identifier = @id
@@ -28,7 +34,7 @@ module Senkyoshi
28
34
  question_bank.questions = []
29
35
  questions.each do |item|
30
36
  question = item.canvas_conversion(question_bank, resources)
31
- question.material = clean_up_material(question)
37
+ question.material = clean_up_material(question.material)
32
38
  question_bank.questions << question
33
39
  end
34
40
  question_bank
@@ -36,11 +42,12 @@ module Senkyoshi
36
42
 
37
43
  # This is to remove the random extra <p>.</p> included in the
38
44
  # description that is just randomly there
39
- def clean_up_material(question)
40
- tag = "<p><span size=\"2\" style=\"font-size: small;\">.</span></p>"
41
- question.material.gsub!(tag, "")
42
- question.material.gsub!("<p>.</p>", "")
43
- question.material.strip!
45
+ def clean_up_material(material)
46
+ if material
47
+ material = material.gsub(TAGS_RE, TAGS)
48
+ material = material.strip
49
+ end
50
+ material
44
51
  end
45
52
  end
46
53
  end
@@ -2,6 +2,17 @@ require "senkyoshi/models/question"
2
2
 
3
3
  module Senkyoshi
4
4
  class Matching < Question
5
+ TAGS_PATTERN = Regexp.union(
6
+ /<\/?p[^>]*>/i, # <p> tags
7
+ /<\/?b[^>]*>/i, # <b> tags
8
+ /<\/?strong[^>]*>/i, # <strong> tags
9
+ /<\/?em[^>]*>/i, # <em> tags
10
+ /<\/?span[^>]*>/i, # <span> tags
11
+ /<\/?font[^>]*>/i, # <font> tags
12
+ /<\/?i(?!mg)[^>]*>/i, # <i> tags, no <img>
13
+ / style=("|')[^"']*("|')/i, # inline styles
14
+ )
15
+
5
16
  def initialize
6
17
  super
7
18
  @matches = []
@@ -9,6 +20,10 @@ module Senkyoshi
9
20
  @distractors = []
10
21
  end
11
22
 
23
+ def strip_select_html(text)
24
+ text.gsub(TAGS_PATTERN, "")
25
+ end
26
+
12
27
  def iterate_xml(data)
13
28
  super
14
29
  resprocessing = data.at("resprocessing")
@@ -18,20 +33,23 @@ module Senkyoshi
18
33
  if match_block = data.at("flow[@class=RIGHT_MATCH_BLOCK]")
19
34
  matches_array = match_block.
20
35
  search("flow[@class=FORMATTED_TEXT_BLOCK]").
21
- map(&:text)
36
+ map { |match| strip_select_html(match.text) }
22
37
  end
38
+
23
39
  if response_block = data.at("flow[@class=RESPONSE_BLOCK]")
24
40
  response_block.children.each do |response|
25
41
  id = response.at("response_lid").attributes["ident"].value
26
42
  question = response.at("mat_formattedtext").text
27
43
  answer_id = @matching_answers[id]
28
44
  answer = ""
45
+
29
46
  flow_label = response.at("flow_label")
30
47
  flow_label.children.each_with_index do |label, index|
31
48
  if label.attributes["ident"].value == answer_id
32
49
  answer = matches_array[index]
33
50
  end
34
51
  end
52
+
35
53
  answers << answer
36
54
  @matches << { id: id, question_text: question, answer_text: answer }
37
55
  end
@@ -40,7 +58,12 @@ module Senkyoshi
40
58
  self
41
59
  end
42
60
 
43
- def canvas_conversion(assessment, _resources = nil)
61
+ def canvas_conversion(assessment, resources)
62
+ @matches.each do |match|
63
+ match[:question_text] = fix_html(match[:question_text], resources)
64
+ match[:answer_text] = fix_html(match[:answer_text], resources)
65
+ end
66
+
44
67
  @question.matches = @matches
45
68
  @question.distractors = @distractors
46
69
  super
@@ -19,17 +19,53 @@ module Senkyoshi
19
19
  false
20
20
  end
21
21
 
22
+ def strip_xid(name)
23
+ name.gsub(/__xid-[0-9]+_[0-9]+/, "")
24
+ end
25
+
26
+ def _matches_directory_xid?(xid, directory)
27
+ dir_xid = directory[/xid-[0-9]+_[0-9]+\z/]
28
+ xid == dir_xid
29
+ end
30
+
31
+ def _find_directories(resources)
32
+ resources.resources.map do |resource|
33
+ if resource.respond_to?(:location)
34
+ File.dirname(resource.location)[/csfiles.*/]
35
+ end
36
+ end.uniq.compact
37
+ end
38
+
39
+ def _fix_path(original_src, resources, dir_names)
40
+ xid = original_src.split("/").last
41
+ file_resource = resources.detect_xid(xid)
42
+
43
+ if file_resource
44
+ "#{FILE_BASE}/#{file_resource.path}"
45
+ else
46
+ matching_dir = dir_names.detect do |dir|
47
+ _matches_directory_xid?(xid, dir)
48
+ end
49
+
50
+ if matching_dir
51
+ "#{DIR_BASE}/#{strip_xid(matching_dir)}"
52
+ end
53
+ end
54
+ end
55
+
22
56
  def _search_and_replace(resources, node_html, tag, attr)
57
+ dir_names = _find_directories(resources)
58
+
23
59
  node_html.search(tag).each do |element|
24
60
  original_src = element[attr]
61
+
25
62
  if original_src
26
- xid = original_src.split("/").last
27
- file_resource = resources.detect_xid(xid)
28
- if file_resource
29
- element[attr] = "#{BASE}/#{file_resource.path}"
30
- end
63
+ path = _fix_path(original_src, resources, dir_names)
64
+ element[attr] = path if path
31
65
  end
32
66
  end
33
67
  end
68
+
69
+ def self.get_pre_data(_xml, _file_name); end
34
70
  end
35
71
  end
@@ -1,15 +1,12 @@
1
- require "senkyoshi/models/resource"
1
+ require "senkyoshi/models/file_resource"
2
2
  require "active_support/core_ext/string"
3
3
 
4
4
  module Senkyoshi
5
- class StaffInfo < Resource
6
- attr_reader(
7
- :id,
8
- :title,
9
- :entries,
10
- )
5
+ class StaffInfo < FileResource
6
+ attr_reader(:title, :entries)
11
7
 
12
- def initialize
8
+ def initialize(resource_id = nil)
9
+ super(resource_id)
13
10
  @entries = []
14
11
  end
15
12
 
@@ -19,10 +19,10 @@ module Senkyoshi
19
19
  # Add page links to page body
20
20
  @files.each do |file|
21
21
  if canvas_file = course.files.detect { |f| f.identifier == file.name }
22
- page.body << file.canvas_conversion(canvas_file)
22
+ page.body << file.canvas_conversion(resources, canvas_file)
23
23
  else
24
- page.body << "<p>File: " + file.linkname +
25
- " -- doesn't exist in blackboard</p>"
24
+ page.body <<
25
+ "<p>File: #{file.linkname} -- doesn't exist in blackboard</p>"
26
26
  end
27
27
  end
28
28
  course.pages << page
@@ -54,6 +54,17 @@ module Senkyoshi
54
54
  ##
55
55
  def self.install_tasks
56
56
  namespace :senkyoshi do
57
+ desc "Convert a single given blackboard cartridge to a canvas cartridge"
58
+ task :imscc_single, [:file_path] do |_t, args|
59
+ file_path = args.file_path
60
+ if file_path
61
+ imscc_path = file_path.clone.ext(".imscc")
62
+ Senkyoshi.parse_and_process_single(file_path, imscc_path)
63
+ else
64
+ puts "No file given"
65
+ end
66
+ end
67
+
57
68
  desc "Convert blackboard cartridges to canvas cartridges"
58
69
  task imscc: SOURCE_FILES.pathmap(
59
70
  "%{^#{SOURCE_NAME}/,#{OUTPUT_DIR}/}X.imscc",
@@ -1,3 +1,3 @@
1
1
  module Senkyoshi
2
- VERSION = "1.0.2".freeze
2
+ VERSION = "1.0.3".freeze
3
3
  end
@@ -56,6 +56,7 @@ module Senkyoshi
56
56
  questestinterop: "QTI",
57
57
  content: "Content",
58
58
  staffinfo: "StaffInfo",
59
+ gradebook: "Gradebook",
59
60
  }.freeze
60
61
 
61
62
  PRE_RESOURCE_TYPE = {
@@ -80,16 +81,10 @@ module Senkyoshi
80
81
  single_pre_data = get_single_pre_data(pre_data, file)
81
82
  res_class = Senkyoshi.const_get RESOURCE_TYPE[type.to_sym]
82
83
  case type
83
- when "content"
84
- Content.from(xml_data, single_pre_data, resource_xids)
85
- when "questestinterop"
86
- single_pre_data ||= { file_name: file }
87
- QTI.from(xml_data, single_pre_data)
88
84
  when "staffinfo"
89
85
  staff_info.iterate_xml(xml_data, single_pre_data)
90
86
  else
91
- resource = res_class.new
92
- resource.iterate_xml(xml_data, single_pre_data)
87
+ res_class.from(xml_data, single_pre_data, resource_xids)
93
88
  end
94
89
  end
95
90
  end
@@ -98,7 +93,7 @@ module Senkyoshi
98
93
  def self.get_single_pre_data(pre_data, file)
99
94
  pre_data.detect do |d|
100
95
  d[:file_name] == file || d[:assignment_id] == file
101
- end
96
+ end || { file_name: file }
102
97
  end
103
98
 
104
99
  def self.iterator_master(resources, zip_file)
@@ -119,9 +114,9 @@ module Senkyoshi
119
114
  iterator_master(resources, zip_file) do |xml_data, type, file|
120
115
  if PRE_RESOURCE_TYPE[type.to_sym]
121
116
  res_class = Senkyoshi.const_get PRE_RESOURCE_TYPE[type.to_sym]
122
- resource_class = res_class.new
123
117
  pre_data[type] ||= []
124
- pre_data[type].push(resource_class.get_pre_data(xml_data, file))
118
+ data = res_class.get_pre_data(xml_data, file)
119
+ pre_data[type].push(data) if data
125
120
  end
126
121
  end
127
122
  pre_data = connect_content(pre_data)
@@ -130,13 +125,16 @@ module Senkyoshi
130
125
 
131
126
  def self.connect_content(pre_data)
132
127
  pre_data["content"].each do |content|
133
- gradebook = pre_data["gradebook"].first.
134
- detect { |g| g[:content_id] == content[:file_name] }
135
- content.merge!(gradebook) if gradebook
136
-
137
- course_assessment = pre_data["courseassessment"].
138
- detect { |ca| ca[:original_file_name] == content[:assignment_id] }
139
- content.merge!(course_assessment) if course_assessment
128
+ if pre_data["gradebook"]
129
+ gradebook = pre_data["gradebook"].first.
130
+ detect { |g| g[:content_id] == content[:file_name] }
131
+ content.merge!(gradebook) if gradebook
132
+ end
133
+ if pre_data["courseassessment"]
134
+ course_assessment = pre_data["courseassessment"].
135
+ detect { |ca| ca[:original_file_name] == content[:assignment_id] }
136
+ content.merge!(course_assessment) if course_assessment
137
+ end
140
138
  end
141
139
  pre_data["content"]
142
140
  end
data/lib/senkyoshi.rb CHANGED
@@ -2,6 +2,7 @@ require "senkyoshi/version"
2
2
  require "senkyoshi/xml_parser"
3
3
  require "senkyoshi/canvas_course"
4
4
  require "senkyoshi/collection"
5
+ require "senkyoshi/configuration"
5
6
 
6
7
  require "canvas_cc"
7
8
  require "optparse"
@@ -12,7 +13,24 @@ require "zip"
12
13
  require "senkyoshi/exceptions"
13
14
 
14
15
  module Senkyoshi
15
- BASE = "$IMS-CC-FILEBASE$".freeze
16
+ FILE_BASE = "$IMS-CC-FILEBASE$".freeze
17
+ DIR_BASE = "$CANVAS_COURSE_REFERENCE$/files/folder".freeze
18
+
19
+ class << self
20
+ attr_writer :configuration
21
+ end
22
+
23
+ def self.configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def self.reset
28
+ @configuration = Configuration.new
29
+ end
30
+
31
+ def self.configure
32
+ yield configuration
33
+ end
16
34
 
17
35
  def self.parse(zip_path, imscc_path)
18
36
  Zip::File.open(zip_path) do |file|
@@ -30,6 +48,10 @@ module Senkyoshi
30
48
  end
31
49
  end
32
50
 
51
+ def self.parse_and_process_single(zip_path, imscc_path)
52
+ Senkyoshi.parse(zip_path, imscc_path)
53
+ end
54
+
33
55
  def self.read_file(zip_file, file_name)
34
56
  zip_file.find_entry(file_name).get_input_stream.read
35
57
  rescue NoMethodError
@@ -37,7 +59,7 @@ module Senkyoshi
37
59
  end
38
60
 
39
61
  def self.build_file(course, imscc_path, resources)
40
- folder = imscc_path.split("/").first
62
+ folder = File.dirname(imscc_path)
41
63
  file = CanvasCc::CanvasCC::CartridgeCreator.new(course).create(folder)
42
64
  File.rename(file, imscc_path)
43
65
  cleanup resources
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: senkyoshi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Atomic Jolt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-11 00:00:00.000000000 Z
11
+ date: 2017-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry-byebug
@@ -166,7 +166,7 @@ files:
166
166
  - lib/senkyoshi.rb
167
167
  - lib/senkyoshi/canvas_course.rb
168
168
  - lib/senkyoshi/collection.rb
169
- - lib/senkyoshi/config.rb
169
+ - lib/senkyoshi/configuration.rb
170
170
  - lib/senkyoshi/exceptions.rb
171
171
  - lib/senkyoshi/models/announcement.rb
172
172
  - lib/senkyoshi/models/answer.rb
@@ -180,11 +180,13 @@ files:
180
180
  - lib/senkyoshi/models/course.rb
181
181
  - lib/senkyoshi/models/external_url.rb
182
182
  - lib/senkyoshi/models/file.rb
183
+ - lib/senkyoshi/models/file_resource.rb
183
184
  - lib/senkyoshi/models/forum.rb
184
185
  - lib/senkyoshi/models/gradebook.rb
185
186
  - lib/senkyoshi/models/group.rb
186
187
  - lib/senkyoshi/models/module.rb
187
188
  - lib/senkyoshi/models/module_item.rb
189
+ - lib/senkyoshi/models/outcome_definition.rb
188
190
  - lib/senkyoshi/models/qti.rb
189
191
  - lib/senkyoshi/models/question.rb
190
192
  - lib/senkyoshi/models/question_bank.rb
@@ -1,35 +0,0 @@
1
- require "yaml"
2
-
3
- module Senkyoshi
4
- def self.canvas_url
5
- Senkyoshi._config[:canvas_url]
6
- end
7
-
8
- def self.canvas_token
9
- Senkyoshi._config[:canvas_token]
10
- end
11
-
12
- def self.scorm_launch_url
13
- Senkyoshi._config[:scorm_launch_url]
14
- end
15
-
16
- def self.scorm_url
17
- Senkyoshi._config[:scorm_url]
18
- end
19
-
20
- def self.scorm_shared_auth
21
- Senkyoshi._config[:scorm_shared_auth]
22
- end
23
-
24
- def self.account_id
25
- Senkyoshi._config[:account_id] || :self
26
- end
27
-
28
- def self._config
29
- @config ||= if File.exists? "senkyoshi.yml"
30
- YAML::load(File.read("senkyoshi.yml"))
31
- else
32
- {}
33
- end
34
- end
35
- end