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.
- checksums.yaml +4 -4
- data/README.md +45 -1
- data/lib/canvas_sync/concerns/sync_mapping.rb +112 -0
- data/lib/canvas_sync/generators/install_generator.rb +1 -0
- data/lib/canvas_sync/generators/templates/migrations/create_grading_period_groups.rb +22 -0
- data/lib/canvas_sync/generators/templates/migrations/create_grading_periods.rb +22 -0
- data/lib/canvas_sync/generators/templates/migrations/create_user_observers.rb +17 -0
- data/lib/canvas_sync/generators/templates/models/grading_period.rb +8 -0
- data/lib/canvas_sync/generators/templates/models/grading_period_group.rb +9 -0
- data/lib/canvas_sync/generators/templates/models/user_observer.rb +11 -0
- data/lib/canvas_sync/importers/bulk_importer.rb +27 -16
- data/lib/canvas_sync/job_batches/batch.rb +9 -0
- data/lib/canvas_sync/job_batches/chain_builder.rb +9 -1
- data/lib/canvas_sync/job_batches/hier_batch_ids.lua +25 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/css/styles.less +178 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/batch_tree.js +106 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/util.js +2 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_batch_tree.erb +6 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_common.erb +13 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +15 -88
- data/lib/canvas_sync/job_batches/sidekiq/web.rb +93 -0
- data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +1 -1
- data/lib/canvas_sync/jobs/report_checker.rb +37 -4
- data/lib/canvas_sync/jobs/report_starter.rb +2 -2
- data/lib/canvas_sync/processors/assignment_groups_processor.rb +1 -7
- data/lib/canvas_sync/processors/assignments_processor.rb +1 -7
- data/lib/canvas_sync/processors/context_module_items_processor.rb +1 -7
- data/lib/canvas_sync/processors/context_modules_processor.rb +1 -7
- data/lib/canvas_sync/processors/model_mappings.yml +68 -0
- data/lib/canvas_sync/processors/normal_processor.rb +3 -3
- data/lib/canvas_sync/processors/provisioning_report_processor.rb +21 -63
- data/lib/canvas_sync/processors/report_processor.rb +14 -9
- data/lib/canvas_sync/processors/submissions_processor.rb +1 -7
- data/lib/canvas_sync/record.rb +4 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +4 -1
- data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +40 -0
- data/spec/dummy/app/models/grading_period.rb +14 -0
- data/spec/dummy/app/models/grading_period_group.rb +15 -0
- data/spec/dummy/app/models/user_observer.rb +17 -0
- data/spec/dummy/db/migrate/20210907233329_create_user_observers.rb +23 -0
- data/spec/dummy/db/migrate/20210907233330_create_grading_periods.rb +28 -0
- data/spec/dummy/db/migrate/20210907233331_create_grading_period_groups.rb +28 -0
- data/spec/dummy/db/schema.rb +42 -1
- data/spec/dummy/log/development.log +1167 -0
- data/spec/dummy/log/test.log +2775 -0
- data/spec/support/fixtures/reports/grading_period_groups.csv +2 -0
- data/spec/support/fixtures/reports/grading_periods.csv +3 -0
- data/spec/support/fixtures/reports/user_observers.csv +3 -0
- metadata +38 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6fe925c97d18da5736681b55e51a1702d727ef2900097c9058de6f247748af9f
|
4
|
+
data.tar.gz: 27c0bd4eb8d57efc705cbe3affdfa6ce4af9580b1b2374e11a8e7e84ddf2b8bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82f682b5e2b713ba2adc45483f215c1de8103dc16bdd3872a1656649527053a8819de35b50d1f4bc8e9961bedbc647fd7a03dbebbe4f2ad0fb3235d6754788be
|
7
|
+
data.tar.gz: 674159253800095ba66e15d9d14f58fd41d46f7fa70a33a8303160488544d721f7e8c83700c8a5e4978436e01d34e070b7bc22ff7433219e68fe00d4620dc38f
|
data/README.md
CHANGED
@@ -190,7 +190,30 @@ Overrides are useful for two scenarios:
|
|
190
190
|
- You have an existing application where the column names do not match up with what CanvasSync expects
|
191
191
|
- You want to sync some other column in the report that CanvasSync is not configured to sync
|
192
192
|
|
193
|
-
|
193
|
+
Mappings can be modified by editing the Model class like such:
|
194
|
+
```ruby
|
195
|
+
class User < ApplicationRecord
|
196
|
+
include CanvasSync::Record
|
197
|
+
|
198
|
+
sync_mapping(reset: false) do # `reset: false` is the default
|
199
|
+
# The mapping can be totally cleared with `reset: true` in the `sync_mapping` call, or like such:
|
200
|
+
reset_links
|
201
|
+
|
202
|
+
# Add a new column:
|
203
|
+
link_column :column_in_report => :column_in_database, type: :datetime
|
204
|
+
|
205
|
+
# If the column name on the report and in the DB are the same, a shorthand can be used:
|
206
|
+
link_column :omit_from_final_grade, type: :datetime
|
207
|
+
|
208
|
+
# If the defaults define a column you don't want synced, you can remove it from the mapping:
|
209
|
+
unlink_column :column_in_database
|
210
|
+
end
|
211
|
+
|
212
|
+
# ...
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
You can also create a file called `canvas_sync_provisioning_mapping.yml` in your Rails `config` directory. However, this approach requires you to re-specify the complete table in order to modify a table. Define the tables and columns you want to override using the following format:
|
194
217
|
|
195
218
|
```ruby
|
196
219
|
users:
|
@@ -383,6 +406,27 @@ Available config options (if you add more, please update this!):
|
|
383
406
|
|
384
407
|
* `config.classes_to_only_log_errors_on` - use this if you are utilizing the `CanvasSync::JobLog` table, but want certain classes to only persist in the `job_logs` table if an error is encountered. This is useful if you've got a very frequently used job that's filling up your database, and only really care about tracking failures.
|
385
408
|
|
409
|
+
## Global Options
|
410
|
+
You can pass in global_options to a job chain. Global options are added to the batch_context and referenced by
|
411
|
+
various internal processes.
|
412
|
+
|
413
|
+
Pass global options into a job chain, using the options param nested in a :global key.
|
414
|
+
options: { global: {...} }
|
415
|
+
|
416
|
+
report_timeout (integer): Number of days until a Canvas report should timeout. Default is 1.
|
417
|
+
report_compilation_timeout (integer): Number of days until a Canvas report should timeout. Default is 1 hour.
|
418
|
+
You can likely pass a float to achieve sub-day timeouts, but not tested.
|
419
|
+
report_max_tries (integer): The number of times to attempt a report before giving up. A report is considered failed
|
420
|
+
if it has an 'error' status in Canvas or is deleted.
|
421
|
+
|
422
|
+
This is an example job chain with global options:
|
423
|
+
job_chain = CanvasSync.default_provisioning_report_chain(
|
424
|
+
MODELS_TO_SYNC,
|
425
|
+
term_scope: :active,
|
426
|
+
full_sync_every: 'sunday',
|
427
|
+
options: { global: { report_timeout: 2 } }
|
428
|
+
)
|
429
|
+
|
386
430
|
## Handling Job errors
|
387
431
|
|
388
432
|
If you need custom handling for when a CanvasSync Job fails, you can add an `:on_failure` option to you Job Chain's `:global_options`.
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module CanvasSync::Concerns
|
2
|
+
module SyncMapping
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def sync_mapping(key = nil, reset: false, &blk)
|
7
|
+
key ||= Mapping.normalize_model_name(self)
|
8
|
+
key = key.to_s
|
9
|
+
existing_map = get_sync_mapping(key)
|
10
|
+
mapper = Mapping.new(existing_map&.deep_dup || {}.with_indifferent_access)
|
11
|
+
mapper.reset_links if reset
|
12
|
+
mapper.instance_exec(&blk)
|
13
|
+
@sync_mappings[key] = mapper.map_def.freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_sync_mapping(key = nil)
|
17
|
+
key ||= Mapping.normalize_model_name(self)
|
18
|
+
key = key.to_s
|
19
|
+
@sync_mappings ||= {}
|
20
|
+
@sync_mappings[key] || superclass.try(:get_sync_mapping, key) || Mapping.default_for(key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Mapping
|
25
|
+
attr_reader :map_def
|
26
|
+
|
27
|
+
def initialize(map_def = {}, model: nil)
|
28
|
+
@map_def = map_def
|
29
|
+
@model = model
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.normalize_model_name(model)
|
33
|
+
model = model.name unless model.is_a?(String)
|
34
|
+
model.pluralize.underscore
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.default_for(key)
|
38
|
+
default_mappings[key]
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.default_mappings
|
42
|
+
@mappings ||= begin
|
43
|
+
maps = {}
|
44
|
+
default_v1_mappings.each do |mname, legacy|
|
45
|
+
m = maps[mname] = {}
|
46
|
+
|
47
|
+
m[:conflict_target] = Array(legacy[:conflict_target]).map(&:to_sym).map do |lct|
|
48
|
+
legacy[:report_columns][lct][:database_column_name]
|
49
|
+
end
|
50
|
+
|
51
|
+
m[:report_columns] = {}
|
52
|
+
legacy[:report_columns].each do |rcol, opts|
|
53
|
+
m[:report_columns][opts[:database_column_name]] = opts.except(:database_column_name).merge!(
|
54
|
+
report_column: rcol,
|
55
|
+
).freeze
|
56
|
+
end
|
57
|
+
end
|
58
|
+
maps.with_indifferent_access.freeze
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.default_v1_mappings
|
63
|
+
@legacy_mappings ||= begin
|
64
|
+
mapping = YAML.load_file(File.join(__dir__, '../processors', "model_mappings.yml")).deep_symbolize_keys!
|
65
|
+
override_filepath = Rails.root.join("config/canvas_sync_provisioning_mapping.yml")
|
66
|
+
|
67
|
+
if File.file?(override_filepath)
|
68
|
+
override = YAML.load_file(override_filepath).deep_symbolize_keys!
|
69
|
+
mapping = mapping.merge(override)
|
70
|
+
end
|
71
|
+
|
72
|
+
mapping.freeze
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def conflict_target(*columns)
|
77
|
+
if columns.count == 0
|
78
|
+
@map_def[:conflict_target]
|
79
|
+
else
|
80
|
+
@map_def[:conflict_target] = columns.flatten.compact
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def reset_links
|
85
|
+
@map_def = {}
|
86
|
+
end
|
87
|
+
|
88
|
+
def unlink_column(key)
|
89
|
+
@map_def.delete(key)
|
90
|
+
end
|
91
|
+
|
92
|
+
def link_column(m, type: nil, &blk)
|
93
|
+
if m.is_a?(Hash)
|
94
|
+
raise "Hash should have exactly 1 entry" if m && m.count != 1
|
95
|
+
@map_def[:report_columns][m.values[0]] = {
|
96
|
+
report_column: m.keys[0],
|
97
|
+
type: type,
|
98
|
+
transform: blk,
|
99
|
+
}
|
100
|
+
elsif m.is_a?(Symbol)
|
101
|
+
@map_def[:report_columns][m] = {
|
102
|
+
report_column: m,
|
103
|
+
type: type,
|
104
|
+
transform: blk,
|
105
|
+
}
|
106
|
+
else
|
107
|
+
raise "Cannot handle argument of type #{m.class}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# <%= autogenerated_migration_warning %>
|
2
|
+
|
3
|
+
class CreateGradingPeriodGroups < ActiveRecord::Migration[5.1]
|
4
|
+
def change
|
5
|
+
create_table :grading_period_groups do |t|
|
6
|
+
t.bigint :canvas_id, null: false
|
7
|
+
t.bigint :canvas_course_id
|
8
|
+
t.bigint :canvas_account_id
|
9
|
+
t.string :title
|
10
|
+
t.boolean :weighted
|
11
|
+
t.boolean :display_totals_for_all_grading_periods
|
12
|
+
|
13
|
+
t.string :workflow_state
|
14
|
+
|
15
|
+
t.timestamps
|
16
|
+
end
|
17
|
+
|
18
|
+
add_index :grading_period_groups, :canvas_id, unique: true
|
19
|
+
add_index :grading_period_groups, :canvas_course_id
|
20
|
+
add_index :grading_period_groups, :canvas_account_id
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# <%= autogenerated_migration_warning %>
|
2
|
+
|
3
|
+
class CreateGradingPeriods < ActiveRecord::Migration[5.1]
|
4
|
+
def change
|
5
|
+
create_table :grading_periods do |t|
|
6
|
+
t.bigint :canvas_id, null: false
|
7
|
+
t.string :title
|
8
|
+
t.float :weight
|
9
|
+
t.datetime :start_date
|
10
|
+
t.datetime :end_date
|
11
|
+
t.datetime :close_date
|
12
|
+
t.bigint :canvas_grading_period_group_id
|
13
|
+
|
14
|
+
t.string :workflow_state
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
|
19
|
+
add_index :grading_periods, :canvas_id, unique: true
|
20
|
+
add_index :grading_periods, :canvas_grading_period_group_id
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# <%= autogenerated_migration_warning %>
|
2
|
+
|
3
|
+
class CreateUserObservers < ActiveRecord::Migration[5.1]
|
4
|
+
def change
|
5
|
+
create_table :user_observers do |t|
|
6
|
+
t.bigint :observing_user_id
|
7
|
+
t.bigint :observed_user_id
|
8
|
+
t.string :workflow_state
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :user_observers, [:observed_user_id, :observing_user_id], unique: true
|
14
|
+
add_index :user_observers, :observing_user_id
|
15
|
+
add_index :user_observers, :observed_user_id
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# <%= autogenerated_model_warning %>
|
2
|
+
|
3
|
+
class GradingPeriod < ApplicationRecord
|
4
|
+
include CanvasSync::Record
|
5
|
+
|
6
|
+
validates :canvas_id, uniqueness: true, presence: true
|
7
|
+
belongs_to :grading_period_group, primary_key: :canvas_id, foreign_key: :canvas_grading_period_group_id, optional: true
|
8
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# <%= autogenerated_model_warning %>
|
2
|
+
|
3
|
+
class GradingPeriodGroup < ApplicationRecord
|
4
|
+
include CanvasSync::Record
|
5
|
+
|
6
|
+
validates :canvas_id, uniqueness: true, presence: true
|
7
|
+
belongs_to :course, primary_key: :canvas_id, foreign_key: :canvas_course_id, optional: true
|
8
|
+
belongs_to :account, primary_key: :canvas_id, foreign_key: :canvas_account_id, optional: true
|
9
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# <%= autogenerated_model_warning %>
|
2
|
+
|
3
|
+
class UserObserver < ApplicationRecord
|
4
|
+
include CanvasSync::Record
|
5
|
+
include CanvasSync::Concerns::ApiSyncable
|
6
|
+
|
7
|
+
validates :canvas_id, uniqueness: true, presence: true
|
8
|
+
|
9
|
+
belongs_to :observing_user, primary_key: :canvas_id, foreign_key: :observing_user_id, class_name: 'User', optional: true
|
10
|
+
belongs_to :observed_user, primary_key: :canvas_id, foreign_key: :observed_user_id, class_name: 'User', optional: true
|
11
|
+
end
|
@@ -24,30 +24,41 @@ module CanvasSync
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.perform_in_batches(report_file_path, mapping, klass, conflict_target, import_args: {})
|
27
|
-
csv_column_names = mapping.
|
28
|
-
database_column_names = mapping.
|
29
|
-
|
27
|
+
csv_column_names = mapping.values.map { |value| value[:report_column].to_s }
|
28
|
+
database_column_names = mapping.keys
|
29
|
+
|
30
|
+
conflict_target = Array(conflict_target).map(&:to_s)
|
31
|
+
conflict_target_indices = conflict_target.map{|ct| database_column_names.index(ct) }
|
30
32
|
|
31
33
|
row_ids = {}
|
32
34
|
batcher = CanvasSync::BatchProcessor.new(of: batch_size) do |batch|
|
33
35
|
row_ids = {}
|
34
|
-
perform_import(klass, database_column_names, batch,
|
36
|
+
perform_import(klass, database_column_names, batch, conflict_target, import_args)
|
35
37
|
end
|
36
38
|
|
37
39
|
row_buffer_out = ->(row) {
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
formatted_row = mapping.map do |db_col, col_def|
|
41
|
+
value = nil
|
42
|
+
value = row[col_def[:report_column]] if col_def[:report_column]
|
43
|
+
|
44
|
+
if col_def[:type]
|
45
|
+
if col_def[:type].to_sym == :datetime
|
46
|
+
# TODO: add some timezone config to the mapping.
|
47
|
+
# In cases where the timestamp or date doesn't include a timezone, you should be able to specify one
|
48
|
+
value = DateTime.parse(value).utc rescue nil # rubocop:disable Style/RescueModifier
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
value = col_def[:transform].call(value, row) if col_def[:transform]
|
53
|
+
|
54
|
+
value
|
41
55
|
end
|
42
56
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
else
|
49
|
-
row[column]
|
50
|
-
end
|
57
|
+
if conflict_target.present?
|
58
|
+
key = conflict_target_indices.map{|ct| formatted_row[ct] }
|
59
|
+
next if row_ids[key]
|
60
|
+
|
61
|
+
row_ids[key] = true
|
51
62
|
end
|
52
63
|
|
53
64
|
batcher << formatted_row
|
@@ -79,7 +90,7 @@ module CanvasSync
|
|
79
90
|
condition: condition_sql(klass, columns, import_args[:sync_start_time]),
|
80
91
|
columns: columns
|
81
92
|
}
|
82
|
-
update_conditions[:conflict_target] = conflict_target if conflict_target
|
93
|
+
update_conditions[:conflict_target] = conflict_target if conflict_target.present?
|
83
94
|
|
84
95
|
options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
|
85
96
|
options.delete(:on_duplicate_key_update) if options.key?(:on_duplicate_key_ignore)
|
@@ -28,6 +28,7 @@ module CanvasSync
|
|
28
28
|
|
29
29
|
BID_EXPIRE_TTL = 2_592_000
|
30
30
|
SCHEDULE_CALLBACK = RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
|
31
|
+
BID_HIERARCHY = RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
|
31
32
|
|
32
33
|
attr_reader :bid
|
33
34
|
|
@@ -423,6 +424,14 @@ module CanvasSync
|
|
423
424
|
def push_callbacks(args, queue)
|
424
425
|
Batch::Callback::worker_class.enqueue_all(args, queue)
|
425
426
|
end
|
427
|
+
|
428
|
+
def bid_hierarchy(bid, depth: 4, per_depth: 5, slice: nil)
|
429
|
+
args = [bid, depth, per_depth]
|
430
|
+
args << slice if slice
|
431
|
+
redis do |r|
|
432
|
+
BID_HIERARCHY.call(r, [], args)
|
433
|
+
end
|
434
|
+
end
|
426
435
|
end
|
427
436
|
end
|
428
437
|
|
@@ -40,7 +40,7 @@ module CanvasSync
|
|
40
40
|
def insert_at(position, new_jobs)
|
41
41
|
chain = self.class.get_chain_parameter(base_job)
|
42
42
|
new_jobs = [new_jobs] unless new_jobs.is_a?(Array)
|
43
|
-
chain.insert(
|
43
|
+
chain.insert(position, *new_jobs)
|
44
44
|
end
|
45
45
|
|
46
46
|
def insert(new_jobs, **kwargs)
|
@@ -172,6 +172,14 @@ module CanvasSync
|
|
172
172
|
mapper[key] ||= []
|
173
173
|
end
|
174
174
|
|
175
|
+
# TODO: Add a Chain progress web View
|
176
|
+
# Augment Batch tree-view with Chain data
|
177
|
+
# > [DONE] Tree view w/o Chain will only show Parent/Current batches and Job Counts
|
178
|
+
# > If augmented with Chain data, the above will be annotated with Chain-related info and will be able to show Jobs defined in the Chain
|
179
|
+
# > Chain-jobs will be supplied chain_id and chain_step_id metadata
|
180
|
+
# > Using server-middleware, if a Chain-job (has chain_id and chain_step_id) creates a Batch, tag the Batch w/ the chain_id and chain_step_id
|
181
|
+
# > UI will map Batches to Chain-steps using the chain_step_id. UI will add entries for any Chain-steps that were not tied to a Batch
|
182
|
+
# > [DONE] Use a Lua script to find child batch IDs. Support max_depth, items_per_depth, top_depth_slice parameters
|
175
183
|
def enqueue_job(job_def)
|
176
184
|
job_class = job_def[:job].constantize
|
177
185
|
job_options = job_def[:parameters] || []
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
local function add_bids(root, depth)
|
3
|
+
local result_data = {}
|
4
|
+
|
5
|
+
if depth > 0 then
|
6
|
+
local sbids
|
7
|
+
if depth == tonumber(ARGV[2]) and ARGV[4] then
|
8
|
+
local min, max = ARGV[4]:match('(%d+):(%d+)')
|
9
|
+
sbids = redis.call('ZRANGE', 'BID-' .. root .. '-bids', min, max)
|
10
|
+
else
|
11
|
+
sbids = redis.call('ZRANGE', 'BID-' .. root .. '-bids', 0, tonumber(ARGV[3]) - 1)
|
12
|
+
end
|
13
|
+
|
14
|
+
local sub_data = {}
|
15
|
+
for _,v in ipairs(sbids) do
|
16
|
+
table.insert(sub_data, add_bids(v, depth - 1))
|
17
|
+
end
|
18
|
+
|
19
|
+
return { root, sub_data }
|
20
|
+
end
|
21
|
+
|
22
|
+
return { root, result_data}
|
23
|
+
end
|
24
|
+
|
25
|
+
return add_bids(ARGV[1], tonumber(ARGV[2]))
|
@@ -0,0 +1,178 @@
|
|
1
|
+
|
2
|
+
@color-green: #25c766;
|
3
|
+
@color-red: #c7254e;
|
4
|
+
@color-yellow: #c4c725;
|
5
|
+
|
6
|
+
.code-wrap.batch-context .args-extended {
|
7
|
+
white-space: pre;
|
8
|
+
|
9
|
+
.key {
|
10
|
+
white-space: pre-wrap;
|
11
|
+
margin-left: 2em;
|
12
|
+
text-indent: -2em;
|
13
|
+
display: inline-block;
|
14
|
+
}
|
15
|
+
|
16
|
+
.own {
|
17
|
+
color: @color-green;
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
|
22
|
+
.batch-tree {
|
23
|
+
.status-block {
|
24
|
+
.tree-stat {
|
25
|
+
margin: 0 4px;
|
26
|
+
|
27
|
+
&.pending {
|
28
|
+
color: @color-yellow;
|
29
|
+
}
|
30
|
+
&.failed {
|
31
|
+
color: @color-red;
|
32
|
+
}
|
33
|
+
&.success {
|
34
|
+
color: @color-green;
|
35
|
+
}
|
36
|
+
&.total {
|
37
|
+
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
.text-inactive {
|
43
|
+
color: darken(#fff, 35%);
|
44
|
+
font-size: 80%;
|
45
|
+
}
|
46
|
+
|
47
|
+
.tree-header {
|
48
|
+
position: relative;
|
49
|
+
|
50
|
+
.status-block {
|
51
|
+
position: absolute;
|
52
|
+
bottom: 0;
|
53
|
+
width: 100%;
|
54
|
+
|
55
|
+
margin-right: 8px;
|
56
|
+
font-size: 90%;
|
57
|
+
text-align: right;
|
58
|
+
|
59
|
+
.tree-stat {
|
60
|
+
font-style: italic;
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
.tree-entry {
|
66
|
+
> .header {
|
67
|
+
display: flex;
|
68
|
+
align-items: center;
|
69
|
+
|
70
|
+
.header-inner {
|
71
|
+
padding: 4px 0;
|
72
|
+
border-bottom: 1px dashed white;
|
73
|
+
display: flex;
|
74
|
+
align-items: center;
|
75
|
+
flex: 1;
|
76
|
+
}
|
77
|
+
|
78
|
+
&:hover {
|
79
|
+
background-color: rgba(0,0,0,0.20);
|
80
|
+
border-radius: 3px;
|
81
|
+
}
|
82
|
+
|
83
|
+
.row-toggle {
|
84
|
+
width: 16px;
|
85
|
+
height: 16px;
|
86
|
+
text-align: center;
|
87
|
+
align-self: center;
|
88
|
+
border-radius: 50%;
|
89
|
+
border: 1px solid #999;
|
90
|
+
text-decoration: none;
|
91
|
+
margin: 0 4px;
|
92
|
+
font-size: 16px;
|
93
|
+
line-height: 15px;
|
94
|
+
|
95
|
+
&.not_applicable {
|
96
|
+
opacity: 0;
|
97
|
+
pointer-events: none;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
.main {
|
102
|
+
flex: 1;
|
103
|
+
display: flex;
|
104
|
+
align-items: baseline;
|
105
|
+
|
106
|
+
.bid {
|
107
|
+
font-family: monospace;
|
108
|
+
padding: 3px 6px;
|
109
|
+
background: rgba(0,0,0,0.2);
|
110
|
+
border-radius: 3px;
|
111
|
+
font-size: 12px;
|
112
|
+
margin: 0 12px 0 0;
|
113
|
+
|
114
|
+
&:hover {
|
115
|
+
.bid-goto {
|
116
|
+
display: inline-block;
|
117
|
+
padding: 0 0 0 4px;
|
118
|
+
font-size: 200%;
|
119
|
+
line-height: 10px;
|
120
|
+
vertical-align: sub;
|
121
|
+
text-decoration: dotted;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
.bid-goto {
|
126
|
+
display: none;
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
.goto-link {
|
132
|
+
margin: 0 8px;
|
133
|
+
display: inline-block;
|
134
|
+
height: 16px;
|
135
|
+
font-size: 90%;
|
136
|
+
border-bottom: 1px dotted white;
|
137
|
+
}
|
138
|
+
|
139
|
+
.status-label {
|
140
|
+
font-family: monospace;
|
141
|
+
padding: 3px 6px;
|
142
|
+
background: rgba(0,0,0,0.2);
|
143
|
+
border-radius: 3px;
|
144
|
+
font-size: 12px;
|
145
|
+
margin: 0 12px 0 0;
|
146
|
+
|
147
|
+
&.deleted {
|
148
|
+
background: #99999933;
|
149
|
+
}
|
150
|
+
&.failed, &.complete {
|
151
|
+
background: #99000033;
|
152
|
+
}
|
153
|
+
&.success {
|
154
|
+
background: #00990033;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
.status-block {
|
159
|
+
width: 10em;
|
160
|
+
text-align: center;
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
> .subitems {
|
165
|
+
padding-left: 16px;
|
166
|
+
|
167
|
+
>.load-more {
|
168
|
+
padding: 4px 0;
|
169
|
+
text-align: center;
|
170
|
+
border-bottom: 1px dashed white;
|
171
|
+
a {
|
172
|
+
border-bottom: 1px dotted white;
|
173
|
+
text-decoration: none;
|
174
|
+
}
|
175
|
+
}
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|