canvas_sync 0.17.17.beta1 → 0.17.23.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -1
  3. data/lib/canvas_sync/concerns/sync_mapping.rb +112 -0
  4. data/lib/canvas_sync/generators/install_generator.rb +1 -0
  5. data/lib/canvas_sync/generators/templates/migrations/create_grading_period_groups.rb +22 -0
  6. data/lib/canvas_sync/generators/templates/migrations/create_grading_periods.rb +22 -0
  7. data/lib/canvas_sync/generators/templates/migrations/create_user_observers.rb +17 -0
  8. data/lib/canvas_sync/generators/templates/models/grading_period.rb +8 -0
  9. data/lib/canvas_sync/generators/templates/models/grading_period_group.rb +9 -0
  10. data/lib/canvas_sync/generators/templates/models/user_observer.rb +11 -0
  11. data/lib/canvas_sync/importers/bulk_importer.rb +27 -16
  12. data/lib/canvas_sync/job_batches/batch.rb +9 -0
  13. data/lib/canvas_sync/job_batches/chain_builder.rb +9 -1
  14. data/lib/canvas_sync/job_batches/hier_batch_ids.lua +25 -0
  15. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/css/styles.less +178 -0
  16. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/batch_tree.js +106 -0
  17. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/util.js +2 -0
  18. data/lib/canvas_sync/job_batches/sidekiq/web/views/_batch_tree.erb +6 -0
  19. data/lib/canvas_sync/job_batches/sidekiq/web/views/_common.erb +13 -0
  20. data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +15 -88
  21. data/lib/canvas_sync/job_batches/sidekiq/web.rb +93 -0
  22. data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +1 -1
  23. data/lib/canvas_sync/jobs/report_checker.rb +37 -4
  24. data/lib/canvas_sync/jobs/report_starter.rb +2 -2
  25. data/lib/canvas_sync/processors/assignment_groups_processor.rb +1 -7
  26. data/lib/canvas_sync/processors/assignments_processor.rb +1 -7
  27. data/lib/canvas_sync/processors/context_module_items_processor.rb +1 -7
  28. data/lib/canvas_sync/processors/context_modules_processor.rb +1 -7
  29. data/lib/canvas_sync/processors/model_mappings.yml +68 -0
  30. data/lib/canvas_sync/processors/normal_processor.rb +3 -3
  31. data/lib/canvas_sync/processors/provisioning_report_processor.rb +21 -63
  32. data/lib/canvas_sync/processors/report_processor.rb +14 -9
  33. data/lib/canvas_sync/processors/submissions_processor.rb +1 -7
  34. data/lib/canvas_sync/record.rb +4 -0
  35. data/lib/canvas_sync/version.rb +1 -1
  36. data/lib/canvas_sync.rb +4 -1
  37. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +40 -0
  38. data/spec/dummy/app/models/grading_period.rb +14 -0
  39. data/spec/dummy/app/models/grading_period_group.rb +15 -0
  40. data/spec/dummy/app/models/user_observer.rb +17 -0
  41. data/spec/dummy/db/migrate/20210907233329_create_user_observers.rb +23 -0
  42. data/spec/dummy/db/migrate/20210907233330_create_grading_periods.rb +28 -0
  43. data/spec/dummy/db/migrate/20210907233331_create_grading_period_groups.rb +28 -0
  44. data/spec/dummy/db/schema.rb +42 -1
  45. data/spec/dummy/log/development.log +1167 -0
  46. data/spec/dummy/log/test.log +2775 -0
  47. data/spec/support/fixtures/reports/grading_period_groups.csv +2 -0
  48. data/spec/support/fixtures/reports/grading_periods.csv +3 -0
  49. data/spec/support/fixtures/reports/user_observers.csv +3 -0
  50. metadata +38 -17
@@ -0,0 +1,106 @@
1
+ import { h, Component, render } from 'https://unpkg.com/preact?module';
2
+ import htm from 'https://unpkg.com/htm?module';
3
+ import { root_url } from './util.js';
4
+
5
+ // Initialize htm with Preact
6
+ const html = htm.bind(h);
7
+
8
+ const StatusBlock = (props) => html`
9
+ <div class="status-block ${props.class || ''}">
10
+ ${props.title && props.title + ':'}
11
+ <span class="tree-stat pending">${props.pending_count}</span>
12
+ |
13
+ <span class="tree-stat failed">${props.failed_count}</span>
14
+ |
15
+ <span class="tree-stat success">${props.successful_count}</span>
16
+ /
17
+ <span class="tree-stat total">${props.total_count}</span>
18
+ </div>
19
+ `
20
+
21
+ class TreeLevel extends Component {
22
+ get bid() {
23
+ return this.props.data.bid;
24
+ }
25
+
26
+ get batch() {
27
+ return this.props.data;
28
+ }
29
+
30
+ load_more = async (event) => {
31
+ event.preventDefault();
32
+ const l = this.batch.batches.items.length;
33
+ const resp = await fetch(`${root_url}batches/${this.bid}/tree?slice=${l}:${l + 5 - 1}`)
34
+ const result = await resp.json()
35
+ const newEntries = result.batches.items;
36
+ for (let ent of newEntries) {
37
+ this.batch.batches.items.push(ent)
38
+ }
39
+ this.forceUpdate()
40
+ }
41
+
42
+ toggle = (event) => {
43
+ event.preventDefault();
44
+ this.setState({ collapsed: !this.state.collapsed })
45
+ }
46
+
47
+ render() {
48
+ const { data: bd } = this.props;
49
+
50
+ let sub_entries = [];
51
+ let sub_batches = bd.batches.items;
52
+ for (let b of sub_batches) {
53
+ sub_entries.push(html`<${TreeLevel} data=${b} />`)
54
+ }
55
+
56
+ let fully_loaded = !(sub_batches.length < bd.batches.total_count);
57
+
58
+ const load_more_elem = html`<div class="load-more">
59
+ ${sub_entries.length} / ${bd.batches.total_count} Items Loaded - <a href="#" onClick=${this.load_more}>Load More</a>
60
+ </div>`
61
+
62
+ return html`<div class="tree-entry tree-batch">
63
+ <div class="header">
64
+ <a class="row-toggle ${!bd.batches.total_count && 'not_applicable'}" onClick=${this.toggle} href="#">
65
+ ${this.state.collapsed ? '+' : '-'}
66
+ </a>
67
+
68
+ <div class="header-inner">
69
+ <div class="main">
70
+ <span class="bid">
71
+ ${bd.bid}
72
+ <a class="bid-goto" href="${root_url}batches/${bd.bid}">⇢</a>
73
+ </span>
74
+ ${bd.description || (bd.status == 'deleted' && html`<i class="text-inactive">Deleted</i>`) || html`<i class="text-inactive">No Description</i>`}
75
+ </div>
76
+
77
+ <span class="status-label ${bd.status}">${bd.status}</span>
78
+
79
+ ${bd.status != 'deleted' && html`
80
+ <${StatusBlock} class="job-status" title="Jobs" ...${bd.jobs} />
81
+ <${StatusBlock} class="batch-status" title="Batches" ...${bd.batches} />
82
+ `}
83
+ </div>
84
+ </div>
85
+
86
+ <div class="subitems ${this.state.collapsed ? 'hidden' : ''}">
87
+ ${sub_entries}
88
+ ${!fully_loaded && load_more_elem}
89
+ </div>
90
+ </div>`
91
+ }
92
+ }
93
+
94
+ class TreeRoot extends Component {
95
+ render() {
96
+ const tree_data = JSON.parse(document.querySelector('#batch-tree #initial-data').innerHTML);
97
+ return html`
98
+ <div class="tree-header">
99
+ <${StatusBlock} pending_count="pending" failed_count="failed" successful_count="successful" total_count="total" />
100
+ </div>
101
+ <${TreeLevel} data=${tree_data} />
102
+ `;
103
+ }
104
+ }
105
+
106
+ render(html`<${TreeRoot} />`, document.querySelector('#batch-tree'));
@@ -0,0 +1,2 @@
1
+
2
+ export const root_url = (document.querySelector('meta[name="sidekiq-baseurl"]')).content;
@@ -0,0 +1,6 @@
1
+
2
+ <div id="batch-tree" class="batch-tree">
3
+ <script id="initial-data" type="application/json"><%= @tree_data.to_json %></script>
4
+ </div>
5
+
6
+ <script type="module" src="<%= root_path %>batches_assets/js/batch_tree.js"></script>
@@ -0,0 +1,13 @@
1
+
2
+ <% add_to_head do %>
3
+ <meta name="sidekiq-baseurl" content="<%= root_path %>" />
4
+ <% if dev_mode? %>
5
+ <link href="<%= root_path %>batches_assets/css/styles.less" media="screen" rel="stylesheet/less" type="text/css" />
6
+ <script async type="text/javascript" src="https://unpkg.com/less@4.1.1/dist/less.js" data-async="true"></script>
7
+ <% else %>
8
+ <link href="<%= root_path %>batches_assets/css/styles.less" media="screen" rel="stylesheet/less" type="text/css" />
9
+ <script async type="text/javascript" src="https://unpkg.com/less@4.1.1/dist/less.js" data-async="true"></script>
10
+ <%# TODO: Pre-compile LESS %>
11
+ <!-- <link href="<%= root_path %>batches_assets/css/styles.css" media="screen" rel="stylesheet" type="text/css" /> -->
12
+ <% end %>
13
+ <% end %>
@@ -1,3 +1,5 @@
1
+ <%= erb get_template(:_common) %>
2
+
1
3
  <h3><%= t('Batch') %></h3>
2
4
  <% status = CanvasSync::JobBatches::Batch::Status.new(@batch) %>
3
5
 
@@ -17,70 +19,13 @@
17
19
  <td><%= @batch.description %></td>
18
20
  </tr>
19
21
  <tr>
20
- <th colspan="2" scope=row><%= t('Added Context') %></td>
22
+ <th colspan="2" scope=row><%= t('Context') %></td>
21
23
  <td>
22
- <code class="code-wrap">
23
- <div class="args-extended"><%= @batch.context.own.to_json %></div>
24
+ <code class="code-wrap batch-context">
25
+ <div class="args-extended"><%= format_context(@batch) %></div>
24
26
  </code>
25
27
  </td>
26
28
  </tr>
27
- <tr>
28
- <th colspan="2" scope=row><%= t('Full Context') %></td>
29
- <td>
30
- <code class="code-wrap">
31
- <div class="args-extended"><%= @batch.context.flatten.to_json %></div>
32
- </code>
33
- </td>
34
- </tr>
35
-
36
- <tr>
37
- <th colspan="3">Jobs</th>
38
- </tr>
39
- <tr>
40
- <th></th>
41
- <th><%= t('Pending') %></th>
42
- <td><%= status.pending %></th>
43
- </tr>
44
- <tr>
45
- <th></th>
46
- <th><%= t('Failed') %></th>
47
- <td><%= status.failures %></th>
48
- </tr>
49
- <tr>
50
- <th></th>
51
- <th><%= t('Complete') %></th>
52
- <td><%= status.completed_count %></th>
53
- </tr>
54
- <tr>
55
- <th></th>
56
- <th><%= t('Total') %></th>
57
- <td><%= status.job_count %></th>
58
- </tr>
59
-
60
- <tr>
61
- <th colspan="3">Batches</th>
62
- </tr>
63
- <tr>
64
- <th></th>
65
- <th><%= t('Pending') %></th>
66
- <td><%= status.child_count - status.successful_children_count %></th>
67
- </tr>
68
- <tr>
69
- <th></th>
70
- <th><%= t('Failed') %></th>
71
- <td><%= status.failed_children_count %></th>
72
- </tr>
73
- <tr>
74
- <th></th>
75
- <th><%= t('Success') %></th>
76
- <td><%= status.successful_children_count %></th>
77
- </tr>
78
- <tr>
79
- <th></th>
80
- <th><%= t('Total') %></th>
81
- <td><%= status.child_count %></th>
82
- </tr>
83
-
84
29
  </tbody>
85
30
  </table>
86
31
  </div>
@@ -88,51 +33,33 @@
88
33
  <header class="row">
89
34
  <div class="col-sm-5">
90
35
  <h3>
91
- <%= t('Jobs') %>
36
+ <%= t('Child Batches') %>
92
37
  </h3>
93
38
  </div>
94
- <%
95
- @current_page = @current_jobs_page
96
- @total_size = @total_jobs_size
97
- %>
98
- <% if @jobs.any? && @total_size > @count.to_i %>
99
- <div class="col-sm-4">
100
- <%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{@batch.bid}", page_key: :job_page } %>
101
- </div>
102
- <% end %>
103
39
  </header>
104
40
 
105
- <% if @jobs.any? %>
106
- <div class="table_container">
107
- <%= erb get_template(:_jobs_table), locals: { jobs: @jobs } %>
108
- </div>
109
- <% end %>
41
+ <%= erb get_template(:_batch_tree), locals: { } %>
110
42
 
111
43
  <header class="row">
112
44
  <div class="col-sm-5">
113
45
  <h3>
114
- <%= t('Child Batches') %>
46
+ <%= t('Jobs') %>
47
+ <i>(Queued and Current)</i>
115
48
  </h3>
116
49
  </div>
117
50
  <%
118
- @current_page = @current_batches_page
119
- @total_size = @total_batches_size
51
+ @current_page = @current_jobs_page
52
+ @total_size = @total_jobs_size
120
53
  %>
121
- <% if @sub_batches.any? && @total_size > @count.to_i %>
54
+ <% if @jobs.any? && @total_size > @count.to_i %>
122
55
  <div class="col-sm-4">
123
- <%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{@batch.bid}", page_key: :batch_page } %>
56
+ <%= erb get_template(:_pagination), locals: { url: "#{root_path}batches/#{@batch.bid}", page_key: :job_page } %>
124
57
  </div>
125
58
  <% end %>
126
59
  </header>
127
60
 
128
- <% if @sub_batches.any? %>
61
+ <% if @jobs.any? %>
129
62
  <div class="table_container">
130
- <%= erb get_template(:_batches_table), locals: { batches: @sub_batches } %>
63
+ <%= erb get_template(:_jobs_table), locals: { jobs: @jobs } %>
131
64
  </div>
132
65
  <% end %>
133
-
134
- <form class="form-horizontal" action="<%= root_path %>batches/<%= @batch.bid %>" method="post">
135
- <%= csrf_tag %>
136
- <a class="btn btn-default" href="<%= root_path %>batches"><%= t('GoBack') %></a>
137
- <input class="btn btn-danger" type="submit" name="delete" value="<%= t('Delete') %>" data-confirm="<%= t('AreYouSure') %>" />
138
- </form>
@@ -9,11 +9,19 @@ require_relative "web/helpers"
9
9
 
10
10
  module CanvasSync::JobBatches::Sidekiq
11
11
  module Web
12
+ DEV_MODE = (defined?(Rails) && !Rails.env.production?) || !!ENV["SIDEKIQ_WEB_TESTING"]
13
+
12
14
  def self.registered(app) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
13
15
  app.helpers do
14
16
  include Web::Helpers
17
+
18
+ def dev_mode?
19
+ DEV_MODE
20
+ end
15
21
  end
16
22
 
23
+ # =============== BATCHES =============== #
24
+
17
25
  app.get "/batches" do
18
26
  @count = (params['count'] || 25).to_i
19
27
  @current_page, @total_size, @batches = page('batches', params['page'], @count)
@@ -26,6 +34,8 @@ module CanvasSync::JobBatches::Sidekiq
26
34
  @bid = params[:bid]
27
35
  @batch = CanvasSync::JobBatches::Batch.new(@bid)
28
36
 
37
+ @tree_data = tree_data(@bid)
38
+
29
39
  @count = (params['count'] || 25).to_i
30
40
  @current_batches_page, @total_batches_size, @sub_batches = page("BID-#{@batch.bid}-bids", params['batch_page'], @count)
31
41
  @sub_batches = @sub_batches.map {|b, score| CanvasSync::JobBatches::Batch.new(b) }
@@ -36,6 +46,81 @@ module CanvasSync::JobBatches::Sidekiq
36
46
  erb(get_template(:batch))
37
47
  end
38
48
 
49
+ app.get "/batches/:bid/tree" do
50
+ @bid = params[:bid]
51
+
52
+ json(tree_data(@bid, slice: params[:slice]))
53
+ end
54
+
55
+ app.helpers do
56
+ def tree_data(root_bid, slice: nil)
57
+ tree_bids = CanvasSync::JobBatches::Batch.bid_hierarchy(root_bid, slice: slice)
58
+
59
+ CanvasSync::JobBatches::Batch.redis do |r|
60
+ layer_data = ->(layer, parent = nil) {
61
+ bid = layer[0]
62
+ batch = CanvasSync::JobBatches::Batch.new(bid)
63
+
64
+ jobs_total = r.hget("BID-#{bid}", "job_count").to_i
65
+ jobs_pending = r.hget("BID-#{bid}", 'pending').to_i
66
+ jobs_failed = r.scard("BID-#{bid}-failed").to_i
67
+ jobs_success = jobs_total - jobs_pending
68
+
69
+ batches_total = r.hget("BID-#{bid}", 'children').to_i
70
+ batches_success = r.scard("BID-#{bid}-batches-success").to_i
71
+ batches_pending = batches_total - batches_success
72
+ batches_failed = r.scard("BID-#{bid}-batches-failed").to_i
73
+
74
+ status = 'in_progress'
75
+ status = 'complete' if batches_pending == batches_failed && jobs_pending == jobs_failed
76
+ status = 'success' if batches_pending == 0 && jobs_pending == 0
77
+ status = 'deleted' if bid != root_bid && !batch.parent_bid
78
+
79
+ {
80
+ bid: bid,
81
+ created_at: r.hget("BID-#{bid}", 'created_at'),
82
+ status: status,
83
+ parent_bid: parent ? parent.bid : batch.parent_bid,
84
+ description: batch.description,
85
+ jobs: {
86
+ pending_count: jobs_pending,
87
+ successful_count: jobs_success,
88
+ failed_count: jobs_failed,
89
+ total_count: jobs_total,
90
+ # items: batches.map{|b| layer_data[b] },
91
+ },
92
+ batches: {
93
+ pending_count: batches_pending,
94
+ successful_count: batches_success,
95
+ failed_count: batches_failed,
96
+ total_count: batches_total,
97
+ items: layer[1].map{|b| layer_data[b, batch] },
98
+ },
99
+ }
100
+ }
101
+
102
+ data = layer_data[tree_bids]
103
+ data[:batches][:slice] = slice if slice
104
+ data
105
+ end
106
+ end
107
+
108
+ def format_context(batch)
109
+ bits = []
110
+ own_keys = batch.context.own.keys
111
+ batch.context.flatten.each do |k,v|
112
+ added = own_keys.include? k
113
+ bits << " <span class=\"key #{added ? 'own' : 'inherited'}\">\"#{k}\": #{v.to_json},</span>"
114
+ end
115
+ bits = [
116
+ "{ // <span class=\"own\">Added</span> / <span class=\"inherited\">Inherited</span>",
117
+ *bits,
118
+ '}'
119
+ ]
120
+ bits.join("\n")
121
+ end
122
+ end
123
+
39
124
  app.post "/batches/all" do
40
125
  if params['delete']
41
126
  drain_zset('batches') do |batches|
@@ -107,6 +192,14 @@ module CanvasSync::JobBatches::Sidekiq
107
192
  end
108
193
 
109
194
  if defined?(::Sidekiq::Web)
195
+ rules = []
196
+ rules = [[:all, {"Cache-Control" => "public, max-age=86400"}]] unless CanvasSync::JobBatches::Sidekiq::Web::DEV_MODE
197
+
198
+ ::Sidekiq::Web.use Rack::Static, urls: ["/batches_assets"],
199
+ root: File.expand_path("#{File.dirname(__FILE__)}/web"),
200
+ cascade: true,
201
+ header_rules: rules
202
+
110
203
  ::Sidekiq::Web.register CanvasSync::JobBatches::Sidekiq::Web
111
204
  ::Sidekiq::Web.tabs["Batches"] = "batches"
112
205
  ::Sidekiq::Web.tabs["Pools"] = "pools"
@@ -46,7 +46,7 @@ module CanvasSync
46
46
  m = Regexp.last_match
47
47
  day = m[1]
48
48
  skip = m[2] || "1"
49
- Date.new.send(:"#{day}?") && last_full_sync.end_of_day <= (skip.to_i.weeks.ago.end_of_day)
49
+ DateTime.now.send(:"#{day}?") && last_full_sync.end_of_day <= (skip.to_i.weeks.ago.end_of_day)
50
50
  when opt.match?(%r{^(\d+)\%$})
51
51
  m = Regexp.last_match
52
52
  rand(100) < m[1].to_i
@@ -4,8 +4,9 @@ module CanvasSync
4
4
  # Re-enqueues itself if the report is still processing on Canvas.
5
5
  # Enqueues the ReportProcessor when the report has completed.
6
6
  class ReportChecker < CanvasSync::Job
7
- REPORT_TIMEOUT = 12.hours
7
+ REPORT_TIMEOUT = 24.hours
8
8
  COMPILATION_TIMEOUT = 1.hour
9
+ MAX_TRIES = 3
9
10
 
10
11
  # @param report_name [Hash] e.g., 'provisioning_csv'
11
12
  # @param report_id [Integer]
@@ -13,6 +14,7 @@ module CanvasSync
13
14
  # @param options [Hash] hash of options that will be passed to the job processor
14
15
  # @return [nil]
15
16
  def perform(report_name, report_id, processor, options, checker_context = {}) # rubocop:disable Metrics/AbcSize
17
+ max_tries = options[:report_max_tries] || batch_context[:report_max_tries] || MAX_TRIES
16
18
  account_id = options[:account_id] || batch_context[:account_id] || "self"
17
19
  report_status = CanvasSync.get_canvas_sync_client(batch_context)
18
20
  .report_status(account_id, report_name, report_id)
@@ -27,9 +29,17 @@ module CanvasSync
27
29
  report_id,
28
30
  )
29
31
  when "error", "deleted"
30
- message = "Report failed to process; status was #{report_status} for report_name: #{report_name}, report_id: #{report_id}" # rubocop:disable Metrics/LineLength
32
+ checker_context[:failed_attempts] ||= 0
33
+ checker_context[:failed_attempts] += 1
34
+ failed_attempts = checker_context[:failed_attempts]
35
+ message = "Report failed to process; status was #{report_status} for report_name: #{report_name}, report_id: #{report_id}, #{current_organization.name}. This report has now failed #{checker_context[:failed_attempts]} time." # rubocop:disable Metrics/LineLength
31
36
  Rails.logger.error(message)
32
- raise message
37
+ if failed_attempts >= max_tries
38
+ Rails.logger.error("This report has failed #{failed_attempts} times. Giving up.")
39
+ raise message
40
+ else
41
+ restart_report(options, report_name, processor, checker_context)
42
+ end
33
43
  else
34
44
  report_timeout = parse_timeout(options[:report_timeout] || batch_context[:report_timeout] || REPORT_TIMEOUT)
35
45
  if timeout_met?(options[:sync_start_time], report_timeout)
@@ -51,7 +61,7 @@ module CanvasSync
51
61
  report_id,
52
62
  processor,
53
63
  options,
54
- checker_context,
64
+ checker_context
55
65
  )
56
66
  end
57
67
  end
@@ -66,6 +76,29 @@ module CanvasSync
66
76
  def parse_timeout(val)
67
77
  val
68
78
  end
79
+
80
+ def restart_report(options, report_name, processor, checker_context)
81
+ account_id = options[:account_id] || batch_context[:account_id] || "self"
82
+ options[:sync_start_time] = DateTime.now.utc.iso8601
83
+ new_context = {}
84
+ new_context[:failed_attempts] = checker_context[:failed_attempts]
85
+ report_id = start_report(account_id, report_name, options[:report_params])
86
+ CanvasSync::Jobs::ReportChecker
87
+ .set(wait: report_checker_wait_time)
88
+ .perform_later(
89
+ report_name,
90
+ report_id,
91
+ processor,
92
+ options,
93
+ new_context
94
+ )
95
+ end
96
+
97
+ def start_report(account_id, report_name, report_params)
98
+ report = CanvasSync.get_canvas_sync_client(batch_context)
99
+ .start_report(account_id, report_name, report_params)
100
+ report["id"]
101
+ end
69
102
  end
70
103
  end
71
104
  end