Skip to content

Task Tree Render

Yoom Lam edited this page Jan 12, 2020 · 23 revisions

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.rb, which is included in app/models/concerns/prints_task_tree.rb.

Usage is like structure_render

Use tree like structure_render on a Task:

> task = Task.find(9)
> puts task.tree
                                    ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) ──── │ ID │ STATUS      │ ASSIGNED_TO_TYPE │ ASGN_BY     │ ASGN_TO     │ CREATED_AT              │ UPDATED_AT              │
InformalHearingPresentationTask     │ 9  │ on_hold     │ Organization     │             │ Vso         │ 2019-12-17 23:38:15 UTC │ 2019-12-17 23:38:15 UTC │
└── InformalHearingPresentationTask │ 11 │ assigned    │ User             │ MICHAEL_VSO │ MICHAEL_VSO │ 2019-12-17 23:38:15 UTC │ 2019-12-17 23:38:15 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.appeal_label_template -- see instructions below.

Run tree on an Appeal:

> puts task.appeal.tree
# OR
> task.appeal.treee  # <-- when you're too lazy to prepend `puts`
                                            ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) ──────────── │ ID │ STATUS      │ ASSIGNED_TO_TYPE │ ASGN_BY     │ ASGN_TO     │ CREATED_AT              │ UPDATED_AT              │
└── RootTask                                │ 8  │ on_hold     │ Organization     │             │ Bva         │ 2019-12-17 23:38:15 UTC │ 2019-12-17 23:38:15 UTC │
    ├── InformalHearingPresentationTask     │ 9  │ on_hold     │ Organization     │             │ Vso         │ 2019-12-17 23:38:15 UTC │ 2019-12-17 23:38:15 UTC │
    │   └── InformalHearingPresentationTask │ 11 │ assigned    │ User             │ MICHAEL_VSO │ MICHAEL_VSO │ 2019-12-17 23:38:15 UTC │ 2019-12-17 23:38:15 UTC │
    └── TrackVeteranTask                    │ 10 │ in_progress │ Organization     │ CSS_ID19    │ Vso         │ 2019-12-17 23:38:15 UTC │ 2019-12-17 23:38:15 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.

Choose columns as usual:

> appeal = Appeal.find(3);
> puts appeal.tree(: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;
> puts appeal.tree(: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;
> puts appeal.tree
Appeal 3 (evidence_submission)               ID STATUS      ASSIGNED_TO_TYPE ASGN_BY     ASGN_TO     CREATED_AT              UPDATED_AT
└── RootTask                                 8  on_hold     Organization                 Bva         2019-12-17 23:38:15 UTC 2019-12-17 23:38:15 UTC
    ├── InformalHearingPresentationTask      9  on_hold     Organization                 Vso         2019-12-17 23:38:15 UTC 2019-12-17 23:38:15 UTC
    │   └── InformalHearingPresentationTask  11 assigned    User             MICHAEL_VSO MICHAEL_VSO 2019-12-17 23:38:15 UTC 2019-12-17 23:38:15 UTC
    └── TrackVeteranTask                     10 in_progress Organization     CSS_ID19    Vso         2019-12-17 23:38:15 UTC 2019-12-17 23:38:15 UTC

Restore ANSI outlines by calling appeal.global_renderer.ansi.

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"]
> puts appeal.tree(*atts, col_labels: col_labels)
Appeal 3 (evidence_submission)               #  Status      AssignToType P_ID ASGN_TO Created
└── RootTask                                 8  on_hold     Organization      Bva     2019-12-17 23:38:15 UTC
    ├── InformalHearingPresentationTask      9  on_hold     Organization 8    Vso     2019-12-17 23:38:15 UTC
    │   └── InformalHearingPresentationTask  11 assigned    User         9            2019-12-17 23:38:15 UTC
    └── TrackVeteranTask                     10 in_progress Organization 8    Vso     2019-12-17 23:38:15 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 }
> puts appeal.tree(: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         │
                                            └───────────────────────────────────────────────────────────────┘

Highlight Specific Task

To highlight a specific task with an asterisk *, add the " " attribute as follows:

> puts Task.find(8).tree(" ", :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)
> puts appeal.tree(" ", :id, :status, highlight: 11)
# Or leave off the " " argument like so:
> puts appeal.tree(: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.

TaskTreeRenderer.config

In the code snippets below, aort is any instance of an appeal or task. Calling aort.global_renderer or TaskTreeRenderModule.default_renderer returns the default TaskTreeRenderer, a singleton which is used when the renderer: parameter is not provided.

:appeal_label_template

The appeal_label_template is evaluated with self being an Appeal or LegacyAppeal object. Remember to use single quotes so that string interpolation is not immediately performed.

> aort.global_renderer.config.appeal_label_template = '#{self.class.name} #{id}, #{created_at}'
> puts Appeal.find(3).tree(: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 │
                                            └──────────────────┘
> puts LegacyAppeal.find(10).tree(: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:

> aort.global_renderer.config.heading_transform = :clipped_upcase_headings;
> puts Appeal.find(3).tree
                                            ┌─────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) . . . . . .  │ ID │ STATUS      │ UPDATED_AT              │ ASSIGNED_TO_ │ ASGN_TO     │
└── RootTask                                │ 8  │ on_hold     │ 2019-12-17 23:38:15 UTC │ Organization │ Bva         │
    ├── InformalHearingPresentationTask     │ 9  │ on_hold     │ 2019-12-17 23:38:15 UTC │ Organization │ Vso         │
    │   └── InformalHearingPresentationTask │ 11 │ assigned    │ 2019-12-17 23:38:15 UTC │ User         │ MICHAEL_VSO │
    └── TrackVeteranTask                    │ 10 │ in_progress │ 2019-12-17 23:38:15 UTC │ Organization │ Vso         │
                                            └─────────────────────────────────────────────────────────────────────────┘

Note the ASSIGNED_TO_ column heading has been clipped to the max length of the strings under that column.

Available heading transforms are in aort.global_renderer.config.heading_transform_funcs_hash.

Add your own heading transform like this:

> aort.global_renderer.config.heading_transform_funcs_hash[:downcase_headings] = ->(key, _col_obj) { key.downcase };
> aort.global_renderer.config.heading_transform = :downcase_headings
> puts Appeal.find(3).tree
                                            ┌─────────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) . . . . . .  │ id │ status      │ updated_at              │ assigned_to_type │ asgn_to     │
└── RootTask                                │ 8  │ on_hold     │ 2019-12-17 23:38:15 UTC │ Organization     │ Bva         │
    ├── InformalHearingPresentationTask     │ 9  │ on_hold     │ 2019-12-17 23:38:15 UTC │ Organization     │ Vso         │
    │   └── InformalHearingPresentationTask │ 11 │ assigned    │ 2019-12-17 23:38:15 UTC │ User             │ MICHAEL_VSO │
    └── TrackVeteranTask                    │ 10 │ in_progress │ 2019-12-17 23:38:15 UTC │ Organization     │ Vso         │
                                            └─────────────────────────────────────────────────────────────────────────────┘

: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:

> aort.global_renderer.config.heading_fill_str = ". "
> puts appeal.tree(: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.default_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 and TaskTreeRenderer.ascii functions for reference.

Customize default renderer in .pryrc

You can customize settings by modifying TaskTreeRenderModule.default_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.default_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.

> puts Appeal.find(3).tree
                                            ┌─────────────────────────────────────────────────────────────────────────────┐
Appeal 3 (evidence_submission) . . . . . .  │ ID │ STATUS      │ UPDATED_AT              │ ASSIGNED_TO_TYPE │ ASGN_TO     │
└── RootTask                                │ 8  │ on_hold     │ 2019-12-17 23:38:15 UTC │ Organization     │ Bva         │
    ├── InformalHearingPresentationTask     │ 9  │ on_hold     │ 2019-12-17 23:38:15 UTC │ Organization     │ Vso         │
    │   └── InformalHearingPresentationTask │ 11 │ assigned    │ 2019-12-17 23:38:15 UTC │ User             │ MICHAEL_VSO │
    └── TrackVeteranTask                    │ 10 │ in_progress │ 2019-12-17 23:38:15 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[:renderer] ||= TaskTreeRenderModule.new_renderer
  kwargs[:renderer].tap do |r|
    r.compact
    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 other approach to allow 'renderer' named parameter!")
  renderer = TaskTreeRenderModule.new_renderer.tap do |r|
    r.compact
    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.

Clone this wiki locally