[Rt-commit] rt branch, 4.6/custom-date-ranges-ui, created. rt-4.4.4-91-g4aeb898e7

? sunnavy sunnavy at bestpractical.com
Tue Jun 25 15:58:10 EDT 2019


The branch, 4.6/custom-date-ranges-ui has been created
        at  4aeb898e7174171cfda99efb16f61604d5a9a077 (commit)

- Log -----------------------------------------------------------------
commit a3424c04d8ee68c1a4e027129a98bd56362615c4
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Jun 15 02:33:20 2019 +0800

    Abstract RT::Record::CustomDateRanges to cover both config and attribute
    
    As both config and attribute(named "CustomDateRanges") could define
    custom date ranges now, it makes sense to abstract a method to return
    all the items.

diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index dc511987e..b9d7a39c9 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2617,6 +2617,32 @@ sub CustomDateRange {
     }
 }
 
+=head2 CustomDateRanges
+
+Return all of the custom date ranges of current class.
+
+=cut
+
+sub CustomDateRanges {
+    my $self = shift;
+    my $type = ref $self || $self;
+
+    my %ranges;
+
+    if ( my $config = RT->Config->Get('CustomDateRanges') ) {
+        %ranges = %{ $config->{$type} } if $config->{$type};
+    }
+
+    if ( my $attribute = RT->System->FirstAttribute('CustomDateRanges') ) {
+        if ( my $content = $attribute->Content ) {
+            for my $name ( keys %{ $content->{$type} || {} } ) {
+                $ranges{$name} ||= $content->{$type}{$name};
+            }
+        }
+    }
+    return %ranges;
+}
+
 sub UID {
     my $self = shift;
     return undef unless defined $self->Id;
diff --git a/share/html/Elements/RT__Asset/ColumnMap b/share/html/Elements/RT__Asset/ColumnMap
index d76ccd213..a4fcd0ce6 100644
--- a/share/html/Elements/RT__Asset/ColumnMap
+++ b/share/html/Elements/RT__Asset/ColumnMap
@@ -113,16 +113,14 @@ my $COLUMN_MAP = {
     },
 };
 
-if (my $config = RT->Config->Get('CustomDateRanges')) {
-    my %ranges = %{ $config->{'RT::Asset'} || {} };
-    for my $name (keys %ranges) {
-        $COLUMN_MAP->{$name} = {
-            title => $name,
-            value => sub {
-                $_[0]->CustomDateRange($name, $ranges{$name});
-            },
-        };
-    }
+my %ranges = RT::Asset->CustomDateRanges;
+for my $name (keys %ranges) {
+    $COLUMN_MAP->{$name} = {
+        title => $name,
+        value => sub {
+            $_[0]->CustomDateRange($name, $ranges{$name});
+        },
+    };
 }
 </%ONCE>
 <%init>
diff --git a/share/html/Elements/RT__Ticket/ColumnMap b/share/html/Elements/RT__Ticket/ColumnMap
index da64b1915..5f5d4b6b2 100644
--- a/share/html/Elements/RT__Ticket/ColumnMap
+++ b/share/html/Elements/RT__Ticket/ColumnMap
@@ -334,17 +334,16 @@ $COLUMN_MAP = {
     },
 };
 
-if (my $config = RT->Config->Get('CustomDateRanges')) {
-    my %ranges = %{ $config->{'RT::Ticket'} || {} };
-    for my $name (keys %ranges) {
-        $COLUMN_MAP->{$name} = {
-            title => $name,
-            value => sub {
-                $_[0]->CustomDateRange($name, $ranges{$name});
-            },
-        };
-    }
+my %ranges = RT::Ticket->CustomDateRanges;
+for my $name (keys %ranges) {
+    $COLUMN_MAP->{$name} = {
+        title => $name,
+        value => sub {
+            $_[0]->CustomDateRange($name, $ranges{$name});
+        },
+    };
 }
+
 </%ONCE>
 <%init>
 # if no encryption support, then KeyOwnerName and KeyRequestors fall back to the regular
diff --git a/share/html/Search/Elements/BuildFormatString b/share/html/Search/Elements/BuildFormatString
index e0a66168a..b35a81d31 100644
--- a/share/html/Search/Elements/BuildFormatString
+++ b/share/html/Search/Elements/BuildFormatString
@@ -132,10 +132,8 @@ while ( my $Role = $CustomRoles->Next ) {
     push @fields, "CustomRole.{" . $Role->Name . "}";
 }
 
-if (RT->Config->Get('CustomDateRanges')) {
-    my %ranges = %{ RT->Config->Get('CustomDateRanges')->{'RT::Ticket'} || {} };
-    push @fields, sort keys %ranges;
-}
+my %ranges = RT::Ticket->CustomDateRanges;
+push @fields, sort keys %ranges;
 
 $m->callback( Fields => \@fields, ARGSRef => \%ARGS );
 

commit d79f6735a201f6fe6cbf489830847978a8deca1b
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Jun 15 06:00:53 2019 +0800

    Refactor custom date range code to make it easier to expand
    
    * _ParseCustomDateRangeSpec
    
    Previously we returned a 5-elements list, which was too many and it's
    inconvenient to add more config items to the spec if we want to return
    them from _ParseCustomDateRangeSpec.
    
    Now it returns a hash instead.
    
    * $op
    
    As only '-' is supported and it's probably also the only sensible
    operator for 2 dates, there is no need to extract it.

diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index b9d7a39c9..ba1b6f59d 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2472,11 +2472,8 @@ sub _DateForCustomDateRangeField {
     return $date;
 }
 
-# Parses "field - field" and returns a five-element list containing the end
-# date field name, the operator (right now always "-" for subtraction), the
-# start date field name, either a custom duration formatter coderef or undef,
-# and a boolean to indicate if it should calculate duration as business time
-# or not. Returns the empty list if there's an error.
+# Parses custom date range spec and returns a hash containing parsed info.
+# Returns the empty list if there's an error.
 
 sub _ParseCustomDateRangeSpec {
     my $self = shift;
@@ -2485,12 +2482,10 @@ sub _ParseCustomDateRangeSpec {
 
     my $calculation;
     my $format;
-    my $business_time;
 
     if (ref($spec)) {
-        $calculation = $spec->{value};
+        $calculation = $spec->{value} || join( ' - ', $spec->{to}, $spec->{from} );
         $format = $spec->{format};
-        $business_time = $spec->{business_time};
     }
     else {
         $calculation = $spec;
@@ -2512,9 +2507,9 @@ sub _ParseCustomDateRangeSpec {
     # regex parses "field - field" (intentionally very strict)
     my $calculation_parser = qr{
         ^
-        ($field_parser)   # end field name
-        \s+ (-) \s+       # space, operator, more space
-        ($field_parser)   # start field name
+        ($field_parser)   # to field name
+        \s+ - \s+       # space, operator, more space
+        ($field_parser)   # from field name
         $
     }x;
 
@@ -2525,14 +2520,8 @@ sub _ParseCustomDateRangeSpec {
         return;
     }
 
-    if (@matches != 3) {
-        RT->Logger->error("Unexpected number of submatches for '$calculation' in CustomDateRanges '$name'. Got " . scalar(@matches) . ", expected 3.");
-        return;
-    }
-
-    my ($end, $op, $start) = @matches;
-
-    return ($end, $op, $start, $format, $business_time);
+    my %date_range_spec= ( from => $matches[1], to => $matches[0], ref $spec ? %$spec : () );
+    return %date_range_spec;
 }
 
 =head2 CustomDateRange name, spec
@@ -2548,60 +2537,54 @@ sub CustomDateRange {
     my $name = shift;
     my $spec = shift;
 
-    my ($end, $op, $start, $format, $business_time) = $self->_ParseCustomDateRangeSpec($name, $spec);
+    my %date_range_spec = $self->_ParseCustomDateRangeSpec($name, $spec);
 
     # parse failed; render no value
-    return unless $start && $end;
+    return unless $date_range_spec{from} && $date_range_spec{to};
 
-    my $end_dt = $self->_DateForCustomDateRangeField($end, $name);
-    my $start_dt = $self->_DateForCustomDateRangeField($start, $name);
+    my $end_dt = $self->_DateForCustomDateRangeField($date_range_spec{to}, $name);
+    my $start_dt = $self->_DateForCustomDateRangeField($date_range_spec{from}, $name);
 
     # RT::Date instantiation failed; render no value
     return unless $start_dt && $start_dt->IsSet
                && $end_dt && $end_dt->IsSet;
 
     my $duration;
-    if ($op eq '-') {
-        if ( $business_time && !$self->QueueObj->SLADisabled && $self->SLA ) {
-            my $config    = RT->Config->Get('ServiceAgreements');
-            my $agreement = $config->{Levels}{ $self->SLA };
-            my $timezone
-                = $config->{QueueDefault}{ $self->QueueObj->Name }{Timezone}
-                || $agreement->{Timezone}
-                || RT->Config->Get('Timezone');
-
-            {
-                local $ENV{'TZ'} = $ENV{'TZ'};
-                if ( $timezone ne ( $ENV{'TZ'} || '' ) ) {
-                    $ENV{'TZ'} = $timezone;
-                    require POSIX;
-                    POSIX::tzset();
-                }
-
-                my $bhours = RT::SLA->BusinessHours( $agreement->{BusinessHours} || 'Default' );
-                $duration = $bhours->between(
-                    $start_dt->Unix <= $end_dt->Unix
-                    ? ( $start_dt->Unix, $end_dt->Unix )
-                    : ( $end_dt->Unix, $start_dt->Unix )
-                );
-                $duration *= -1 if $start_dt->Unix > $end_dt->Unix;
-            }
-
+    if ( $date_range_spec{business_time} && !$self->QueueObj->SLADisabled && $self->SLA ) {
+        my $config    = RT->Config->Get('ServiceAgreements');
+        my $agreement = $config->{Levels}{ $self->SLA };
+        my $timezone
+          = $config->{QueueDefault}{ $self->QueueObj->Name }{Timezone}
+          || $agreement->{Timezone}
+          || RT->Config->Get('Timezone');
+
+        {
+            local $ENV{'TZ'} = $ENV{'TZ'};
             if ( $timezone ne ( $ENV{'TZ'} || '' ) ) {
+                $ENV{'TZ'} = $timezone;
+                require POSIX;
                 POSIX::tzset();
             }
+
+            my $bhours = RT::SLA->BusinessHours( $agreement->{BusinessHours} || 'Default' );
+            $duration = $bhours->between(
+                $start_dt->Unix <= $end_dt->Unix
+                ? ( $start_dt->Unix, $end_dt->Unix )
+                : ( $end_dt->Unix, $start_dt->Unix )
+            );
+            $duration *= -1 if $start_dt->Unix > $end_dt->Unix;
         }
 
-        $duration //= $end_dt->Diff($start_dt);
-    }
-    else {
-        RT->Logger->error("Unexpected operator in CustomDateRanges '$name' spec '$spec'. Got '$op', expected '-'.");
-        return;
+        if ( $timezone ne ( $ENV{'TZ'} || '' ) ) {
+            POSIX::tzset();
+        }
     }
 
+    $duration //= $end_dt->Diff($start_dt);
+
     # _ParseCustomDateRangeSpec guarantees $format is a coderef
-    if ($format) {
-        return $format->($duration, $end_dt, $start_dt, $self);
+    if ($date_range_spec{format}) {
+        return $date_range_spec{format}->($duration, $end_dt, $start_dt, $self);
     }
     else {
         # "x days ago" is strongly suggestive of comparing with the current

commit a8ddc25127b701648f2feecd0313f370e55ca676
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Jun 15 06:11:32 2019 +0800

    Support fallback dates for custom date ranges
    
    Take "Downtime" for example:
    
        'Downtime' => {
            from => 'CF.{First Alert}',
            to => 'CF.Recovered',
            to_fallback => 'now',
        },
    
    When CF.Recovered is not set, it makes more sense to use "now" instead
    to calculate the "Downtime".

diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index ba1b6f59d..a454bf49a 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2520,7 +2520,16 @@ sub _ParseCustomDateRangeSpec {
         return;
     }
 
-    my %date_range_spec= ( from => $matches[1], to => $matches[0], ref $spec ? %$spec : () );
+    if ( ref $spec ) {
+        for my $type ( qw/from to/ ) {
+            if ( $spec->{"${type}_fallback"} && $spec->{"${type}_fallback"} !~ /^$field_parser$/ ) {
+                RT->Logger->error( "Invalid ${type}_fallback field: " . $spec->{"${type}_fallback"} );
+                return;
+            }
+        }
+    }
+
+    my %date_range_spec = ( from => $matches[1], to => $matches[0], ref $spec ? %$spec : () );
     return %date_range_spec;
 }
 
@@ -2545,6 +2554,18 @@ sub CustomDateRange {
     my $end_dt = $self->_DateForCustomDateRangeField($date_range_spec{to}, $name);
     my $start_dt = $self->_DateForCustomDateRangeField($date_range_spec{from}, $name);
 
+    unless ( $start_dt && $start_dt->IsSet ) {
+        if ( ref $spec && $parsed{from_fallback} ) {
+            $start_dt = $self->_DateForCustomDateRangeField( $parsed{from_fallback}, $name );
+        }
+    }
+
+    unless ( $end_dt && $end_dt->IsSet ) {
+        if ( ref $spec && $parsed{to_fallback} ) {
+            $end_dt = $self->_DateForCustomDateRangeField( $parsed{to_fallback}, $name );
+        }
+    }
+
     # RT::Date instantiation failed; render no value
     return unless $start_dt && $start_dt->IsSet
                && $end_dt && $end_dt->IsSet;

commit e18d9c7127e40a032be3099549dffe3677b41d7f
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jun 13 02:22:56 2019 +0800

    Web UI for custom date ranges

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 5e9fbed9c..806d23888 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -513,6 +513,8 @@ sub BuildMainNav {
             title => loc('Edit Search'), path => "/Search/Build.html" . ( ($has_query) ? $args : '' ) );
         $current_search_menu->child( advanced =>
             title => loc('Advanced'),    path => "/Search/Edit.html$args" );
+        $current_search_menu->child( custom_date_ranges =>
+            title => loc('Custom Date Ranges'), path => "/Search/CustomDateRanges.html" );
         if ($has_query) {
             $current_search_menu->child( results => title => loc('Show Results'), path => "/Search/Results.html$args" );
         }
diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index a454bf49a..70a566298 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2647,6 +2647,32 @@ sub CustomDateRanges {
     return %ranges;
 }
 
+=head2 CustomDateRangeFields
+
+Return all of the fields custom date range could use for current class.
+
+=cut
+
+sub CustomDateRangeFields {
+    my $self = shift;
+    my $type = ref $self || $self;
+
+    my @fields = 'now';
+
+    for my $column ( keys %{ $_TABLE_ATTR->{ ref $self || $self } } ) {
+        my $entry = $_TABLE_ATTR->{ ref $self || $self }{$column};
+        next unless $entry->{read} && ( $entry->{type} // '' ) eq 'datetime';
+        push @fields, $column;
+    }
+
+    my $cfs = RT::CustomFields->new( ref $self ? $self->CurrentUser : RT->SystemUser );
+    $cfs->Limit( FIELD => 'Type', VALUE => [ 'Date', 'DateTime' ], OPERATOR => 'IN' );
+    while ( my $cf = $cfs->Next ) {
+        push @fields, 'CF.{' . $cf->Name . '}';
+    }
+    return sort { lc $a cmp lc $b } @fields;
+}
+
 sub UID {
     my $self = shift;
     return undef unless defined $self->Id;
diff --git a/share/html/Elements/SelectCustomDateRangeField b/share/html/Elements/SelectCustomDateRangeField
new file mode 100644
index 000000000..9a08b8488
--- /dev/null
+++ b/share/html/Elements/SelectCustomDateRangeField
@@ -0,0 +1,59 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<select name="<% $Name %>" id="<% $Name %>" class="chosen custom-date-range-field">
+<option value="" >-</option>
+% for my $field ( $ObjectType->new( $session{CurrentUser} )->CustomDateRangeFields ) {
+<option <% $field eq ($Default//'') ? 'selected="selected"' : '' |n %> value="<% $field %>" ><% $field %></option>
+% }
+</select>
+
+<%ARGS>
+$Name => ''
+$Default => ''
+$ObjectType => 'RT::Ticket'
+</%ARGS>
diff --git a/share/html/Search/CustomDateRanges.html b/share/html/Search/CustomDateRanges.html
new file mode 100644
index 000000000..0eef09ba1
--- /dev/null
+++ b/share/html/Search/CustomDateRanges.html
@@ -0,0 +1,295 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc('Custom Date Ranges') &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files') &>
+% if ( $config && keys %{$config->{'RT::Ticket'}} ) {
+  <table class="collection-as-table">
+    <tr class="collection-as-table">
+      <th class="collection-as-table"><&|/l&>Name</&></th>
+      <th class="collection-as-table"><&|/l&>From</&></th>
+      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>To</&></th>
+      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
+    </tr>
+% my $i = 0;
+% for my $name ( sort keys %{$config->{'RT::Ticket'}} ) {
+% $i++;
+    <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
+      <td class="collection-as-table"><% $name %></td>
+%     my $spec = $config->{'RT::Ticket'}{$name};
+%     my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $spec);
+      <td class="collection-as-table"><% $date_range_spec{from} %></td>
+      <td class="collection-as-table"><% $date_range_spec{from_fallback} || '' %></td>
+      <td class="collection-as-table"><% $date_range_spec{to} %></td>
+      <td class="collection-as-table"><% $date_range_spec{to_fallback} || '' %></td>
+      <td class="collection-as-table"><% $date_range_spec{business_time} ? loc('Yes') : loc('No') %></td>
+    </tr>
+% }
+  </table>
+% }
+% else {
+  <p><&|/l&>No custom date ranges in config files</&></p>
+% }
+</&>
+
+<form name="CustomDateRanges" method="POST" method="?">
+  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges') &>
+    <table class="collection-as-table">
+      <tr class="collection-as-table">
+        <th class="collection-as-table"><&|/l&>Name</&></th>
+        <th class="collection-as-table"><&|/l&>From</&></th>
+        <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
+        <th class="collection-as-table"><&|/l&>To</&></th>
+        <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
+        <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
+        <th class="collection-as-table">
+          <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
+          <&|/l&>Delete</&>
+        </th>
+      </tr>
+% my $i = 0;
+% if ( $content ) {
+% my $id = 0;
+%   for my $name ( sort keys %{$content->{'RT::Ticket'}} ) {
+% $i++;
+      <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
+        <td class="collection-as-table"><input type="text" name="<% $id %>-name" value="<% $name %>" /></td>
+%       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $content->{'RT::Ticket'}{$name});
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></td>
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></td>
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></td>
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></td>
+        <td class="collection-as-table">
+          <select name="<% $id %>-business_time">
+            <option value="1" <% $date_range_spec{business_time} ? 'selected="selected"' : '' |n%>><&|/l&>Yes</&></option>
+            <option value="0" <% $date_range_spec{business_time} ? '': 'selected="selected"' |n%>><&|/l&>No</&></option>
+          </select>
+        </td>
+        <td class="collection-as-table"><input type="checkbox" name="<% $id %>-Delete" value="1" /></td>
+      </tr>
+%     $id++;
+%   }
+% }
+
+% for ( 1 .. 3 ) {
+% $i++;
+      <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
+        <td class="collection-as-table"><input type="text" name="name" value="" /></td>
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></td>
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></td>
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></td>
+        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></td>
+        <td class="collection-as-table">
+          <select name="business_time">
+            <option value="1"><&|/l&>Yes</&></option>
+            <option value="0" selected="selected"><&|/l&>No</&></option>
+          </select>
+        </td>
+        <td class="collection-as-table"></td>
+      </tr>
+% }
+    </table>
+    <& /Elements/Submit, Name => 'Save', Label => loc('Save Changes') &>
+  </&>
+</form>
+
+<script type="text/javascript">
+jQuery(function() {
+    jQuery('select.chosen.custom-date-range-field').chosen({ width: '12em', no_results_text: ' ', search_contains: true });
+});
+</script>
+
+<%INIT>
+
+Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser');
+
+my $config = RT->Config->Get('CustomDateRanges');
+my $attribute = RT->System->FirstAttribute('CustomDateRanges') || RT::Attribute->new( $session{CurrentUser} );
+
+my $content;
+
+$content = $attribute->Content if $attribute->id;
+
+my @results;
+
+if ($Save) {
+    my %label = (
+        from => 'From', # loc
+        to => 'To', # loc
+        from_fallback => 'From Value if Unset', # loc
+        to_fallback => 'To Value if Unset', # loc
+    );
+
+    my $need_save;
+    if ($content) {
+        my @current_names = sort keys %{ $content->{'RT::Ticket'} };
+        for my $id ( 0 .. $#current_names ) {
+            my $current_name = $current_names[$id];
+            my $spec         = $content->{'RT::Ticket'}{$current_name};
+            my $name         = $ARGS{"$id-name"};
+
+            if ( $config && $config->{'RT::Ticket'}{$name} ) {
+                push @results, loc( "[_1] already exists", $name );
+                next;
+            }
+
+            if ( $ARGS{"$id-Delete"} ) {
+                delete $content->{'RT::Ticket'}{$current_name};
+                push @results, loc( 'Deleted [_1]', $current_name );
+                $need_save ||= 1;
+                next;
+            }
+
+            my $updated;
+            for my $field (qw/from from_fallback to to_fallback/) {
+                next if ( $spec->{$field} // '' ) eq $ARGS{"$id-$field"};
+                if ((   $ARGS{"$id-$field"}
+                        && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $ARGS{"$id-$field"} )
+                    )
+                    || ( !$ARGS{"$id-$field"} && $field =~ /fallback/ )
+                  )
+                {
+                    $spec->{$field} = $ARGS{"$id-$field"};
+                    $updated ||= 1;
+                }
+                else {
+                    push @results, loc( 'Invalid [_1] for [_2]', loc( $label{$field} ), $name );
+                    next;
+                }
+            }
+
+            if ( $spec->{business_time} != $ARGS{"$id-business_time"} ) {
+                $spec->{business_time} = $ARGS{"$id-business_time"};
+                $updated ||= 1;
+            }
+
+            $content->{'RT::Ticket'}{$name} = $spec;
+            if ( $name ne $current_name ) {
+                delete $content->{'RT::Ticket'}{$current_name};
+                $updated   ||= 1;
+            }
+
+            if ( $updated ) {
+                push @results, loc( 'Updated [_1]', $name );
+                $need_save ||= 1;
+            }
+        }
+    }
+
+    if ( $ARGS{name} ) {
+        for my $field (qw/from from_fallback to to_fallback business_time/) {
+            $ARGS{$field} = [ $ARGS{$field} ] unless ref $ARGS{$field};
+        }
+
+        my $i = 0;
+        for my $name ( @{ $ARGS{name} } ) {
+            if ($name) {
+                if ( $config && $config->{'RT::Ticket'}{$name} || $content && $content->{'RT::Ticket'}{$name} ) {
+                    push @results, loc( "[_1] already exists", $name );
+                    $i++;
+                    next;
+                }
+            }
+            else {
+                $i++;
+                next;
+            }
+
+            my $spec = { business_time => $ARGS{business_time}[$i] };
+            for my $field ( qw/from from_fallback to to_fallback/ ) {
+                if ( ($ARGS{$field}[$i] && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $ARGS{$field}[$i] ))
+                    || ( !$ARGS{$field}[$i] && $field =~ /fallback/  )
+                ) {
+                    $spec->{$field} = $ARGS{$field}[$i];
+                }
+                else {
+                    push @results, loc( 'Invalid [_1] for [_2]', loc($field), $name );
+                    $i++;
+                    next;
+                }
+            }
+
+            $content->{'RT::Ticket'}{$name} = $spec;
+            push @results, loc( 'Created [_1]', $name );
+            $need_save ||= 1;
+            $i++;
+        }
+    }
+
+    if ($need_save) {
+        my ( $ret, $msg );
+        if ( $attribute->id ) {
+            ( $ret, $msg ) = $attribute->SetContent($content);
+        }
+        else {
+            ( $ret, $msg )
+              = $attribute->Create( Name => 'CustomDateRanges', Object => RT->System, Content => $content );
+        }
+
+        unless ($ret) {
+            RT->Logger->error("Couldn't save content: $msg");
+            push @results, $msg;
+        }
+    }
+}
+
+MaybeRedirectForResults(
+    Actions => \@results,
+    Path    => '/Search/CustomDateRanges.html',
+);
+
+</%INIT>
+
+<%ARGS>
+$Save => undef
+</%ARGS>

commit 4b161f264a7eabaffbcf3920d69b79faba938926
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Jun 18 05:41:12 2019 +0800

    Fix supported ticket fields for custom date ranges in the doc
    
    Space separated fields like "Last Updated" and "Last Contact" are not
    supported any more.  Though case sensitivity doesn't matter here, it's a
    bit nicer to use CamelCase, at least for "multiple word" fields like
    "LastUpdated".

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 2a8d0288f..26e731d0c 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1072,8 +1072,8 @@ field may be:
 
 =item * a core field
 
-For example, L<RT::Ticket> supports: created, starts, started, last updated,
-told or last contact, due, resolved.
+For example, L<RT::Ticket> supports: Created, Starts, Started, LastUpdated,
+Told or LastContact, Due and Resolved.
 
 =item * a custom field
 

commit f4e8c113c53af41cd49ac98c3ad9260ababc5b2c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Jun 18 06:06:30 2019 +0800

    Try harder to calculate business time for custom date ranges
    
    Previously we only calculate business time if the corresponding ticket
    has an SLA, which probably is too strict.  This commit allows to
    calculate business time based on "Default" schedule defined in
    %ServiceBusinessHours, when the schedule can't be deducted from
    corresponding object.

diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 70a566298..4ef6f4d05 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2555,14 +2555,14 @@ sub CustomDateRange {
     my $start_dt = $self->_DateForCustomDateRangeField($date_range_spec{from}, $name);
 
     unless ( $start_dt && $start_dt->IsSet ) {
-        if ( ref $spec && $parsed{from_fallback} ) {
-            $start_dt = $self->_DateForCustomDateRangeField( $parsed{from_fallback}, $name );
+        if ( ref $spec && $date_range_spec{from_fallback} ) {
+            $start_dt = $self->_DateForCustomDateRangeField( $date_range_spec{from_fallback}, $name );
         }
     }
 
     unless ( $end_dt && $end_dt->IsSet ) {
-        if ( ref $spec && $parsed{to_fallback} ) {
-            $end_dt = $self->_DateForCustomDateRangeField( $parsed{to_fallback}, $name );
+        if ( ref $spec && $date_range_spec{to_fallback} ) {
+            $end_dt = $self->_DateForCustomDateRangeField( $date_range_spec{to_fallback}, $name );
         }
     }
 
@@ -2571,13 +2571,24 @@ sub CustomDateRange {
                && $end_dt && $end_dt->IsSet;
 
     my $duration;
-    if ( $date_range_spec{business_time} && !$self->QueueObj->SLADisabled && $self->SLA ) {
-        my $config    = RT->Config->Get('ServiceAgreements');
-        my $agreement = $config->{Levels}{ $self->SLA };
-        my $timezone
-          = $config->{QueueDefault}{ $self->QueueObj->Name }{Timezone}
-          || $agreement->{Timezone}
-          || RT->Config->Get('Timezone');
+    if ( $date_range_spec{business_time} ) {
+        my $schedule;
+        my $timezone;
+
+        # Prefer the schedule/timezone specified in %ServiceAgreements for current object
+        if ( $self->isa('RT::Ticket') && !$self->QueueObj->SLADisabled && $self->SLA ) {
+            if ( my $config = RT->Config->Get('ServiceAgreements') ) {
+                $timezone = $config->{QueueDefault}{ $self->QueueObj->Name }{Timezone};
+
+                # Each SLA could have its own schedule and timezone
+                if ( my $agreement = $config->{Levels}{ $self->SLA } ) {
+                    $schedule = $agreement->{BusinessHours};
+                    $timezone ||= $agreement->{Timezone};
+                }
+            }
+        }
+        $timezone ||= RT->Config->Get('Timezone');
+        $schedule ||= 'Default';
 
         {
             local $ENV{'TZ'} = $ENV{'TZ'};
@@ -2587,7 +2598,7 @@ sub CustomDateRange {
                 POSIX::tzset();
             }
 
-            my $bhours = RT::SLA->BusinessHours( $agreement->{BusinessHours} || 'Default' );
+            my $bhours = RT::SLA->BusinessHours($schedule);
             $duration = $bhours->between(
                 $start_dt->Unix <= $end_dt->Unix
                 ? ( $start_dt->Unix, $end_dt->Unix )

commit 4aeb898e7174171cfda99efb16f61604d5a9a077
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Jun 18 05:48:32 2019 +0800

    Document more config items for custom date ranges

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 26e731d0c..67d402ab8 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1055,15 +1055,13 @@ value describes the range and must be either a string or a hashref.
 
 Values that are plain strings simply describe the calculation to be made.
 
-Values that are hashrefs must include the key C<value> which must a string
-that describes the calculation to be made. This hashref may also include the
-key C<format>, which is a code reference that allows customization of how
-the duration is displayed to users. This code reference receives four
-parameters: the duration (a number of seconds), the end time (an L<RT::Date>
-object), the start time (another L<RT::Date>), and the record itself (which
-corresponds to the first-level key; in the example config above, it would be
-the L<RT::Ticket> object). The code reference should return the string to
-be displayed to the user.
+Values that are hashrefs that could include:
+
+=over 4
+
+=item value
+
+A string that describes the calculation to be made.
 
 The calculation is expected to be of the format C<"field - field"> where each
 field may be:
@@ -1086,10 +1084,47 @@ You may use either C<CF.Name> or C<CF.{Longer Name}> syntax.
 Custom date range calculations are defined using typical math operators with
 a space before and after. Subtraction (-) is currently supported.
 
-If either field is unset, nothing will be displayed for that record (and the
-C<format> code reference will not be called). If you need additional control
-over how results are calculated, see
-L<docs/customizing/search_result_columns.pod>.
+If either field and its corresponding fallback field(see blow) are both unset,
+then nothing will be displayed for that record (and the C<format> code
+reference will not be called).  If you need additional control over how
+results are calculated, see L<docs/customizing/search_result_columns.pod>.
+
+=item from and to
+
+When value is not set, C<from/to> will be used to calculate instead.
+Technically, C<Resolved - Created"> is equal to:
+
+    { from => 'Created', to => 'Resolved' }
+
+=item from_fallback and to_fallback
+
+Fallback fields when the main fields are unset, e.g.
+
+    {   from        => 'CF.{First Alert}',
+        to          => 'CF.Recovered',
+        to_fallback => 'now',
+    }
+
+When C<CF.Recovered> is unset, "now" will be used in the calculation.
+
+=item business_time
+
+A boolean value to indicate if it's a business time or not.
+
+When the schedule can't be deducted from corresponding object, the
+C<Default> one defined in C<%ServiceBusinessHours> will be used instead.
+
+=item format
+
+A code reference that allows customization of how the duration is displayed
+to users.  This code reference receives four parameters: the duration (a
+number of seconds), the end time (an L<RT::Date> object), the start time
+(another L<RT::Date>), and the record itself (which corresponds to the
+first-level key; in the example config above, it would be the L<RT::Ticket>
+object). The code reference should return the string to be displayed to the
+user.
+
+=back
 
 =item C<$CanonicalizeRedirectURLs>
 

-----------------------------------------------------------------------


More information about the rt-commit mailing list