senkyoshi 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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