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.
structure_render
Usage is like 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_char
to""
(empty string) or" "
(a longer string). - Exclude top and bottom borders by setting
:include_border
to false. - The
:top_chars
and:bottom_chars
settings 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_divider
for 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.