canvas_sync 0.17.17.beta1 → 0.17.23.beta1
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 +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
|
+
}
|