Task Tree Render - department-of-veterans-affairs/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_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.