Task Tree Render - TISTATechnologies/caseflow GitHub Wiki
Task Tree Render
An easier way to inspect task trees with fewer keystrokes.
Motivations:
- easier column-aligned printout for comparing attributes across tasks,
- attribute dereference chaining and flexible transforms,
- customizable column headings,
- and more visually appealing.
The code is located in app/models/concerns/task_tree_render_module.rb,
which is included in app/models/concerns/prints_task_tree.rb.
This code uses the console-tree-renderer gem.
Usage is like structure_render
Use tree like structure_render on a Task object like so:
> task = Task.find(9)
> puts task.tree
┌────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) ──── │ ID │ STATUS │ ASGN_BY │ ASGN_TO │ UPDATED_AT │
InformalHearingPresentationTask │ 9 │ on_hold │ │ Vso │ 2020-01-30 14:19:33 UTC │
└── InformalHearingPresentationTask │ 11 │ assigned │ MICHAEL_VSO │ MICHAEL_VSO │ 2020-01-30 14:19:33 UTC │
└────────────────────────────────────────────────────────────────────────┘
The appeal associated with the task is printed on the header line: appeal_id followed with either docket_type or docket_name whichever exists first.
Customize the appeal label by changing config.heading_label_template -- search for instructions below.
Default attributes are shown as columns.
Customize the default attributes by changing config.default_atts -- search for instructions below.
Run tree on an Appeal like so:
> puts task.appeal.tree
# OR
> task.appeal.treee # <-- avoids having to prepend `puts`
┌────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) ──────────── │ ID │ STATUS │ ASGN_BY │ ASGN_TO │ UPDATED_AT │
└── RootTask │ 8 │ on_hold │ │ Bva │ 2020-01-30 14:19:33 UTC │
├── InformalHearingPresentationTask │ 9 │ on_hold │ │ Vso │ 2020-01-30 14:19:33 UTC │
│ └── InformalHearingPresentationTask │ 11 │ assigned │ MICHAEL_VSO │ MICHAEL_VSO │ 2020-01-30 14:19:33 UTC │
└── TrackVeteranTask │ 10 │ in_progress │ CSS_ID19 │ Vso │ 2020-01-30 14:19:33 UTC │
└────────────────────────────────────────────────────────────────────────┘
Note that the values under the ASGN_BY and ASGN_TO columns are derived using predefined lambdas -- see the "Attribute Transforms" section below for more description.
Specify columns as usual:
> appeal = Appeal.find(3);
> appeal.treee(:id, :status)
# OR
> appeal.treee :id, :status
┌──────────────────┐
Appeal 3 (evidence_submission) ──────────── │ ID │ STATUS │
└── RootTask │ 8 │ on_hold │
├── InformalHearingPresentationTask │ 9 │ on_hold │
│ └── InformalHearingPresentationTask │ 11 │ assigned │
└── TrackVeteranTask │ 10 │ in_progress │
└──────────────────┘
ASCII and Compact output
The default output is ANSI, which is more visually appealing. For compatibility with text editors and other tools, change output to use ASCII characters:
> appeal.global_renderer.ascii_mode;
> appeal.treee :id, :status, :assigned_to_id
+-----------------------------------+
Appeal 3 (evidence_submission) ------------ | ID | STATUS | ASSIGNED_TO_ID |
└── RootTask | 8 | on_hold | 4 |
├── InformalHearingPresentationTask | 9 | on_hold | 7 |
│ └── InformalHearingPresentationTask | 11 | assigned | 32 |
└── TrackVeteranTask | 10 | in_progress | 7 |
+-----------------------------------+
For more compact output without vertical lines and borders:
> appeal.global_renderer.compact_mode;
> appeal.treee
Appeal 3 (evidence_submission) ID STATUS ASGN_BY ASGN_TO UPDATED_AT
└── RootTask 8 on_hold Bva 2020-01-30 14:19:33 UTC
├── InformalHearingPresentationTask 9 on_hold Vso 2020-01-30 14:19:33 UTC
│ └── InformalHearingPresentationTask 11 assigned MICHAEL_VSO MICHAEL_VSO 2020-01-30 14:19:33 UTC
└── TrackVeteranTask 10 in_progress CSS_ID19 Vso 2020-01-30 14:19:33 UTC
Restore ANSI outlines by calling appeal.global_renderer.ansi_mode.
Attribute Dereferencing and Custom Column Headers
Task attributes can be dereferenced by using an array representing a chain of methods to call.
For example, to show the type of the assigned_to object, use [:assigned_to, :type].
By default the column heading will be [:ASSIGNED_TO, :TYPE].
To manually set the column headings, use the col_labels named parameter as follows:
> atts = [:id, :status, :assigned_to_type, :parent_id, [:assigned_to, :type], :created_at]
> col_labels = ["\#", "Status", "AssignToType", "P_ID", "ASGN_TO", "Created"]
> appeal.treee *atts, col_labels: col_labels
Appeal 3 (evidence_submission) # Status AssignToType P_ID ASGN_TO Created
└── RootTask 8 on_hold Organization Bva 2020-01-30 14:19:33 UTC
├── InformalHearingPresentationTask 9 on_hold Organization 8 Vso 2020-01-30 14:19:33 UTC
│ └── InformalHearingPresentationTask 11 assigned User 9 2020-01-30 14:19:33 UTC
└── TrackVeteranTask 10 in_progress Organization 8 Vso 2020-01-30 14:19:33 UTC
Attribute Transforms (using Lambdas)
A more flexible alternative to attribute dereferencing is to use a Ruby lambda to derive values for a column.
In the example below, new columns with column headings ASGN_TO.TYPE and ASGN_TO_CSSID will be included
when the columns are specified as parameters to tree.
Note that a string or symbol can be used for the new column.
:ASGN_TO is a built-in lambda defined in task_tree_renderer.rb. Others are defined in task_tree_render_module.rb.
Call appeal.global_renderer.config.value_funcs_hash to show a list of available attribute transforms.
For each task in the tree, the specified lambda is called to populate the values under the column.
> appeal.global_renderer.config.value_funcs_hash["ASGN_TO.TYPE"] = ->(task){ task.assigned_to.type }
> appeal.global_renderer.config.value_funcs_hash[:ASGN_TO_CSSID] = ->(task){ task.assigned_to.css_id }
> appeal.global_renderer.ansi_mode
> appeal.treee(:assigned_to_type, "ASGN_TO.TYPE", :ASGN_TO_CSSID, :ASGN_TO)
┌───────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) ──────────── │ ASSIGNED_TO_TYPE │ ASGN_TO.TYPE │ ASGN_TO_CSSID │ ASGN_TO │
└── RootTask │ Organization │ Bva │ - │ Bva │
├── InformalHearingPresentationTask │ Organization │ Vso │ - │ Vso │
│ └── InformalHearingPresentationTask │ User │ - │ MICHAEL_VSO │ MICHAEL_VSO │
└── TrackVeteranTask │ Organization │ Vso │ - │ Vso │
└───────────────────────────────────────────────────────────────┘
Note the - character is used to denote an error occurred when calling the lambda (because type or css_id are undefined). Set appeal.global_renderer.config.func_error_char to change that character.
To prevent the error, the first lambda can be changed to ->(task){ TaskTreeRenderer.send_chain(task, [:assigned_to, :type]) } or ->(task){ task.assigned_to.respond_to?(:type) ? task.assigned_to.type : "" }.
Highlight Specific Task
To highlight a specific task with an asterisk *, add the " " attribute as follows:
> Task.find(8).treee " ", :id, :status
┌──────────────────────┐
Appeal 3 (evidence_submission) ──────── │ │ ID │ STATUS │
RootTask │ * │ 8 │ on_hold │
├── InformalHearingPresentationTask │ │ 9 │ on_hold │
│ └── InformalHearingPresentationTask │ │ 11 │ assigned │
└── TrackVeteranTask │ │ 10 │ in_progress │
└──────────────────────┘
By default, the calling task is highlighted -- in this case, the task with id 8.
To highlight a different task, set the named parameter highlight with the task id like so:
> appeal = Appeal.find(3)
> appeal.treee " ", :id, :status, highlight: 11
# Or leave off the " " argument since it is implied when the highlight parameter is specified:
> appeal.treee :id, :status, highlight: 11
┌──────────────────────┐
Appeal 3 (evidence_submission) ──────────── │ │ ID │ STATUS │
└── RootTask │ │ 8 │ on_hold │
├── InformalHearingPresentationTask │ │ 9 │ on_hold │
│ └── InformalHearingPresentationTask │ * │ 11 │ assigned │
└── TrackVeteranTask │ │ 10 │ in_progress │
└──────────────────────┘
Change the default highlight character (*) by setting appeal.global_renderer.config.highlight_char.
global_renderer.config
In the code snippets below, aort is any instance of an appeal or task.
Calling aort.global_renderer or TaskTreeRenderModule.global_renderer returns the default TaskTreeRenderer,
a singleton which is used when the renderer: parameter is not provided.
:heading_label_template
The heading_label_template is a lambda that generates the heading line.
The heading_label_template is evaluated with self being the associated Appeal or LegacyAppeal object.
> aort.global_renderer.config.heading_label_template = ->(appeal){ "#{appeal.class.name} #{appeal.id}, #{appeal.created_at}" }
> Appeal.find(3).treee :id, :status
┌──────────────────┐
Appeal 3, 2019-12-17 23:38:15 UTC────────── │ ID │ STATUS │
└── RootTask │ 8 │ on_hold │
├── InformalHearingPresentationTask │ 9 │ on_hold │
│ └── InformalHearingPresentationTask │ 11 │ assigned │
└── TrackVeteranTask │ 10 │ in_progress │
└──────────────────┘
When used on a LegacyAppeal, LegacyAppeal.find(10).created_at returns nil and hence the date does not appear on the header line.
> LegacyAppeal.find(10).treee :id, :status
┌────────────────┐
LegacyAppeal 10, ────────────── │ ID │ STATUS │
└── RootTask │ 816 │ on_hold │
└── HearingTask │ 817 │ on_hold │
└── ScheduleHearingTask │ 818 │ assigned │
└────────────────┘
Column Headings Transforms
If named parameter col_labels is not specified when calling tree, a column heading transform is used to create column labels. To see predefined heading transforms, run aort.global_renderer.config.heading_transform_funcs_hash.
For example, run the following to use the clipped_upcase_headings transform:
> Appeal.find(3).global_renderer.config.heading_transform = :clipped_upcase_headings;
> Appeal.find(3).treee :id, :status, :assigned_to_type
┌─────────────────────────────────┐
Appeal 3 (evidence_submission) ──────────── │ ID │ STATUS │ ASSIGNED_TO_ │
└── RootTask │ 8 │ on_hold │ Organization │
├── InformalHearingPresentationTask │ 9 │ on_hold │ Organization │
│ └── InformalHearingPresentationTask │ 11 │ assigned │ User │
└── TrackVeteranTask │ 10 │ in_progress │ Organization │
└─────────────────────────────────┘
Note the ASSIGNED_TO_ column heading has been clipped to the max length of the strings under that column.
Add your own heading transform like this:
> Appeal.find(3).global_renderer.config.heading_transform_funcs_hash[:downcase_headings] = ->(key, _col_obj) { key.downcase };
> Appeal.find(3).global_renderer.config.heading_transform = :downcase_headings
> Appeal.find(3).treee
┌────────────────────────────────────────────────────────────────────────┐
Appeal 3, 2020-01-30 14:19:33 UTC────────── │ id │ status │ asgn_by │ asgn_to │ updated_at │
└── RootTask │ 8 │ on_hold │ │ Bva │ 2020-01-30 14:19:33 UTC │
├── InformalHearingPresentationTask │ 9 │ on_hold │ │ Vso │ 2020-01-30 14:19:33 UTC │
│ └── InformalHearingPresentationTask │ 11 │ assigned │ MICHAEL_VSO │ MICHAEL_VSO │ 2020-01-30 14:19:33 UTC │
└── TrackVeteranTask │ 10 │ in_progress │ CSS_ID19 │ Vso │ 2020-01-30 14:19:33 UTC │
└────────────────────────────────────────────────────────────────────────┘
:heading_fill_str
A heading_fill_str is used in the top line between the appeal label and the column headings.
Change it by setting aort.global_renderer.config.heading_fill_str like so:
> appeal.global_renderer.config.heading_fill_str = ". "
> appeal.treee :id, :status
┌──────────────────┐
Appeal 3 (evidence_submission) . . . . . . .│ ID │ STATUS │
└── RootTask │ 8 │ on_hold │
├── InformalHearingPresentationTask │ 9 │ on_hold │
│ └── InformalHearingPresentationTask │ 11 │ assigned │
└── TrackVeteranTask │ 10 │ in_progress │
└──────────────────┘
Customize Drawing Characters
To customize column separators, internal margins, and customize borders modify other config keys.
- Change the column separator by setting
:col_sep. - Adjust internal margins by setting
:cell_margin_charto""(empty string) or" "(a longer string). - Exclude top and bottom borders by setting
:include_borderto false. - The
:top_charsand:bottom_charssettings are used for the top and bottom borders respectively.- The first and fourth characters are used for the corners.
- The second character is used to draw horizontal lines.
- The third character is used at the column separator locations.
- (See
TaskTreeRenderer.write_dividerfor details).
- Change the highlight character by setting
:highlight_char.
TaskTreeRenderModule.global_renderer.config.tap do |conf|
conf.col_sep = " "
conf.cell_margin_char = ""
conf.include_border = true
conf.top_chars = "+-++"
conf.bottom_chars = "+-++"
conf.highlight_char = "#"
end
See the TaskTreeRenderer.ansi_mode and TaskTreeRenderer.ascii_mode functions for reference.
Customize default renderer in .pryrc
You can customize settings by modifying TaskTreeRenderModule.global_renderer.config in your .pryrc
so that it is loaded each time the Rails console is run.
For example, if your .pryrc file in the Caseflow top-level directory contains the following:
TaskTreeRenderModule.global_renderer.config.tap do |conf|
conf.default_atts = [:id, :status, :updated_at, :assigned_to_type, :ASGN_TO]
conf.heading_fill_str = ". "
end
puts "TaskTreeRenderer customized"
Then in a Rails console, running the following will use your customizations.
> Appeal.find(3).treee
┌─────────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) . . . . . . │ ID │ STATUS │ UPDATED_AT │ ASSIGNED_TO_TYPE │ ASGN_TO │
└── RootTask │ 8 │ on_hold │ 2020-01-30 14:19:33 UTC │ Organization │ Bva │
├── InformalHearingPresentationTask │ 9 │ on_hold │ 2020-01-30 14:19:33 UTC │ Organization │ Vso │
│ └── InformalHearingPresentationTask │ 11 │ assigned │ 2020-01-30 14:19:33 UTC │ User │ MICHAEL_VSO │
└── TrackVeteranTask │ 10 │ in_progress │ 2020-01-30 14:19:33 UTC │ Organization │ Vso │
└─────────────────────────────────────────────────────────────────────────────┘
Create your own TaskTreeRenderer
You can define your own functions in .pryrc that use customized TaskTreeRenderers.
The tree1 method creates a TaskTreeRenderer instance, customizes it, and
pass it as part of kwargs to the treee function.
The tree2 method creates a TaskTreeRenderer instance, customizes it, and
uses it directly to call the tree_str function.
def tree1(obj, *atts, **kwargs)
kwargs[:highlight_row] = Task.find(kwargs.delete(:highlight)) if kwargs[:highlight]
kwargs[:renderer] ||= TaskTreeRenderModule.new_renderer
kwargs[:renderer].tap do |r|
r.compact_mode
r.config.default_atts = [:id, :status, :ASGN_TO, :UPD_DATE]
end
obj.treee(*atts, **kwargs)
end
def tree2(obj, *atts, **kwargs)
kwargs.delete(:renderer) && fail("Use tree1 to allow 'renderer' named parameter!")
kwargs[:highlight_row] = Task.find(kwargs.delete(:highlight)) if kwargs[:highlight]
renderer = TaskTreeRenderModule.new_renderer.tap do |r|
r.compact_mode
r.config.default_atts = [:id, :status, :ASGN_TO, :UPD_DATE]
end
puts renderer.tree_str(obj, *atts, **kwargs)
end
Then use it like so:
> tree1 Task.find(8)
Appeal 3 (evidence_submission) ID STATUS ASGN_TO UPD_DATE
RootTask 8 on_hold Bva 2020-01-09
├── InformalHearingPresentationTask 9 on_hold Vso 2020-01-09
│ └── InformalHearingPresentationTask 11 assigned MICHAEL_VSO 2020-01-09
└── TrackVeteranTask 10 in_progress Vso 2020-01-09
> tree2 Appeal.find(3), :id, :status, :UPD_TIME, highlight: 9
Appeal 3 (evidence_submission) ID STATUS UPD_TIME
└── RootTask 8 on_hold 19-00-00
├── InformalHearingPresentationTask * 9 on_hold 19-00-00
│ └── InformalHearingPresentationTask 11 assigned 19-00-00
└── TrackVeteranTask 10 in_progress 19-00-00
Byebug
Within byebug, run load ".pryrc" to load the customizations into the current byebug environment.