[Rt-commit] rt branch, 4.4/custom-date-ranges, created. rt-4.4.1rc1-20-g5abbfee

Shawn Moore shawn at bestpractical.com
Tue Jun 28 20:11:18 EDT 2016


The branch, 4.4/custom-date-ranges has been created
        at  5abbfee762ad74a6b92fe53b61e8ec9a57afa6d3 (commit)

- Log -----------------------------------------------------------------
commit e0d120026b566260e86aa14837f394c708b5e85a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Jun 28 23:23:26 2016 +0000

    RT::Record support for CustomDateRanges

diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 6d24385..354328b 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2408,6 +2408,155 @@ sub WikiBase {
     return RT->Config->Get('WebPath'). "/index.html?q=";
 }
 
+# matches one field in "field - field" style range specs. subclasses
+# that can participate in custom date ranges should override this method
+# to match their additional date fields (e.g. RT::Ticket adds "due").
+# be sure to call this superclass method to get "now" and CF parsing.
+sub _CustomDateRangeFieldParser {
+    my $self = shift;
+    return qr{
+        now
+        | cf\. (?: \{ .*? \} | \S+ )
+    }xi;
+}
+
+# returns an RT::Date instantiated with this record's value for the parsed
+# field name. includes the $range_name parameter only for diagnostics
+# subclasses should override this to instantiate the fields they added in
+# _CustomDateRangeFieldParser
+sub _DateForCustomDateRangeField {
+    my $self       = shift;
+    my $field      = shift;
+    my $range_name = shift;
+
+    my $date = RT::Date->new($self->CurrentUser);
+
+    if (lc($field) eq 'now') {
+        $date->Set(Format => 'unix', Value => time);
+    }
+    elsif ($field =~ /^CF\.(?:\{(.*)\}|(.*))$/) {
+        my $name = $1 || $2;
+        $date->Set(Format => 'sql', Value => $self->FirstCustomFieldValue($name));
+    }
+    else {
+        RT->Logger->error("Unable to parse '$field' as a field name in CustomDateRanges '$range_name'");
+        return;
+    }
+
+    return $date;
+}
+
+# parses "field - field" and returns a four-element list containing the end
+# date field name, the operator (right now always "-" for subtraction), the
+# start date field name, and either a custom duration formatter coderef or
+# undef. returns the empty list if there's an error
+sub _ParseCustomDateRangeSpec {
+    my $self = shift;
+    my $name = shift;
+    my $spec = shift;
+
+    my $calculation;
+    my $format;
+
+    if (ref($spec)) {
+        $calculation = $spec->{value};
+        $format = $spec->{format};
+    }
+    else {
+        $calculation = $spec;
+    }
+
+    if (!$calculation || ref($calculation)) {
+        RT->Logger->error("CustomDateRanges '$name' 'value' must be a string");
+        return;
+    }
+
+    if ($format && ref($format) ne 'CODE') {
+        RT->Logger->error("CustomDateRanges '$name' 'format' must be a CODE reference");
+        return;
+    }
+
+    # class-specific matcher for now, created, CF.{foo bar}, CF.baz, etc.
+    my $field_parser = $self->_CustomDateRangeFieldParser;
+
+    # 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
+        $
+    }x;
+
+    my @matches = $calculation =~ $calculation_parser;
+
+    if (!@matches) {
+        RT->Logger->error("Unable to parse '$calculation' as a calculated value in CustomDateRanges '$name'");
+        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);
+}
+
+=head2 CustomDateRange name, spec
+
+Takes a L<RT_Config/%CustomDateRanges>-style spec string and its name (for
+diagnostics). Returns a localized string evaluating the calculation. If either
+date is unset, or anything fails to parse, this returns C<undef>.
+
+=cut
+
+sub CustomDateRange {
+    my $self = shift;
+    my $name = shift;
+    my $spec = shift;
+
+    my ($end, $op, $start, $format) = $self->_ParseCustomDateRangeSpec($name, $spec);
+
+    # parse failed; render no value
+    return unless $start && $end;
+
+    my $end_dt = $self->_DateForCustomDateRangeField($end, $name);
+    my $start_dt = $self->_DateForCustomDateRangeField($start, $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 '-') {
+        $duration = $end_dt->Diff($start_dt);
+    }
+    else {
+        RT->Logger->error("Unexpected operator in CustomDateRanges '$name' spec '$spec'. Got '$op', expected '-'.");
+        return;
+    }
+
+    # _ParseCustomDateRangeSpec guarantees $format is a coderef
+    if ($format) {
+        return $format->($duration, $end_dt, $start_dt, $self);
+    }
+    else {
+        # "x days ago" is strongly suggestive of comparing with the current
+        # time; but if we're comparing two arbitrary times, "x days prior"
+        # reads better
+        if ($duration < 0) {
+            $duration *= -1;
+            return $self->loc('[_1] prior', $end_dt->DurationAsString($duration));
+        }
+        else {
+            return $end_dt->DurationAsString($duration);
+        }
+    }
+}
+
 sub UID {
     my $self = shift;
     return undef unless defined $self->Id;

commit 49101084d8569aeada9f5f135863318b91c38b18
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Jun 28 23:23:41 2016 +0000

    RT::Ticket support for CustomDateRanges

diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index 3fa0557..8abfd7a 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -3041,7 +3041,36 @@ sub CurrentUserCanSeeTime {
            !RT->Config->Get('HideTimeFieldsFromUnprivilegedUsers');
 }
 
-1;
+sub _DateForCustomDateRangeField {
+    my $self  = shift;
+    my $field = shift;
+
+       if (lc($field) eq 'created')  { return $self->CreatedObj }
+    elsif (lc($field) eq 'starts')   { return $self->StartsObj }
+    elsif (lc($field) eq 'started')  { return $self->StartedObj }
+    elsif (lc($field) eq 'told')     { return $self->ToldObj }
+    elsif (lc($field) eq 'due')      { return $self->DueObj }
+    elsif (lc($field) eq 'resolved') { return $self->ResolvedObj }
+    elsif ($field =~ /^last ?updated$/i) {
+        return $self->LastUpdatedObj;
+    }
+    else {
+        return $self->SUPER::_DateForCustomDateRangeField($field, @_);
+    }
+}
+
+sub _CustomDateRangeFieldParser {
+    my $self = shift;
+    return $self->SUPER::_CustomDateRangeFieldParser . '|' . qr{
+          created
+        | starts
+        | started
+        | last \ ? updated
+        | told
+        | due
+        | resolved
+    }xi;
+}
 
 =head1 AUTHOR
 

commit 69d746eda9459a6603d49327f7a7664dd833f8c1
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Jun 28 23:23:55 2016 +0000

    RT::Asset support for CustomDateRanges

diff --git a/lib/RT/Asset.pm b/lib/RT/Asset.pm
index b2e75bb..a700d13 100644
--- a/lib/RT/Asset.pm
+++ b/lib/RT/Asset.pm
@@ -627,6 +627,29 @@ sub _Value {
     return $self->SUPER::_Value(@_);
 }
 
+sub _CustomDateRangeFieldParser {
+    my $self = shift;
+    return $self->SUPER::_CustomDateRangeFieldParser . '|' . qr{
+          created
+        | last \ ? updated
+    }xi;
+}
+
+sub _DateForCustomDateRangeField {
+    my $self  = shift;
+    my $field = shift;
+
+    if (lc($field) eq 'created') {
+        return $self->CreatedObj;
+    }
+    elsif ($field =~ /^last ?updated$/i) {
+        return $self->LastUpdatedObj;
+    }
+    else {
+        return $self->SUPER::_DateForCustomDateRangeField($field, @_);
+    }
+}
+
 sub Table { "Assets" }
 
 sub _CoreAccessible {

commit 8b50d923798831837e4684c4943db1d712461f7f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Jun 28 23:24:10 2016 +0000

    Add RT::Ticket CustomDateRanges to query builder

diff --git a/share/html/Elements/RT__Ticket/ColumnMap b/share/html/Elements/RT__Ticket/ColumnMap
index c7303d9..48f3483 100644
--- a/share/html/Elements/RT__Ticket/ColumnMap
+++ b/share/html/Elements/RT__Ticket/ColumnMap
@@ -326,6 +326,16 @@ $COLUMN_MAP = {
         },
     },
 };
+
+my %ranges = %{ RT->Config->Get('CustomDateRanges')->{'RT::Ticket'} || {} };
+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 4cbcde4..e3c2d9a 100644
--- a/share/html/Search/Elements/BuildFormatString
+++ b/share/html/Search/Elements/BuildFormatString
@@ -128,6 +128,9 @@ while ( my $Role = $CustomRoles->Next ) {
     push @fields, "CustomRole.{" . $Role->Name . "}";
 }
 
+my %ranges = %{ RT->Config->Get('CustomDateRanges')->{'RT::Ticket'} || {} };
+push @fields, sort keys %ranges;
+
 $m->callback( Fields => \@fields, ARGSRef => \%ARGS );
 
 my ( @seen);

commit f6e59bdd3aefd23dc4d903bcce6028a6a2f7b2af
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Jun 29 00:09:10 2016 +0000

    Add config validation for CustomDateRanges

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 70df38f..d1c569d 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -981,6 +981,37 @@ our %META;
             $config->Set( CustomFieldGroupings => %$groups );
         },
     },
+    CustomDateRanges => {
+        Type            => 'HASH',
+        PostLoadCheck   => sub {
+            my $config = shift;
+            # use scalar context intentionally to avoid not a hash error
+            my $ranges = $config->Get('CustomDateRanges') || {};
+
+            unless (ref($ranges) eq 'HASH') {
+                RT->Logger->error("Config option \%CustomDateRanges is a @{[ref $ranges]} not a HASH");
+                return;
+            }
+
+            for my $class (keys %$ranges) {
+                if (ref($ranges->{$class}) eq 'HASH') {
+                    for my $name (keys %{ $ranges->{$class} }) {
+                        my $spec = $ranges->{$class}{$name};
+                        if (!ref($spec) || ref($spec) eq 'HASH') {
+                            # this will produce error messages if parsing fails
+                            $class->require;
+                            $class->_ParseCustomDateRangeSpec($name, $spec);
+                        }
+                        else {
+                            RT->Logger->error("Config option \%CustomDateRanges{$class}{$name} is not a string or HASH");
+                        }
+                    }
+                } else {
+                    RT->Logger->error("Config option \%CustomDateRanges{$class} is not a HASH");
+                }
+            }
+        },
+    },
     ExternalStorage => {
         Type            => 'HASH',
         PostLoadCheck   => sub {

commit 5abbfee762ad74a6b92fe53b61e8ec9a57afa6d3
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Jun 29 00:09:26 2016 +0000

    RT_Config doc for CustomDateRanges

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 7722558..6f213cc 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -951,6 +951,81 @@ For C<RT::User>: C<Identity>, C<Access control>, C<Location>, C<Phones>
 Extensions may also add their own built-in groupings, refer to the individual
 extension documentation for those.
 
+=item C<%CustomDateRanges>
+
+This option lets you declare additional date ranges to be calculated
+and displayed in search results. Durations between any two core fields,
+as well as custom fields, are supported. Each custom date range is
+added as an additional display column in the search builder.
+
+Set C<%CustomDateRanges> to a nested structure similar to the following:
+
+    Set(%CustomDateRanges,
+        'RT::Ticket' => {
+            'Containment' => 'Resolved - Created',
+
+            'Downtime' => 'CF.{First Alert} - CF.Recovered',
+
+            'Time Til Beta' => {
+                value => 'CF.Beta - now',
+
+                format => sub {
+                    my ($duration, $beta, $now, $ticket) = @_;
+                    my $days = int($duration / (24*60*60));
+                    if ($days < 0) {
+                        $ticket->loc('[quant,_1,day,days] ago', -$days);
+                    }
+                    else {
+                        $ticket->loc('in [quant,_1,day,days]', $days);
+                    }
+                },
+            },
+        },
+    );
+
+The first level keys are record types. Each record type's value must be a
+hash reference. Each pair in the second-level hash defines a new range. The
+key is the range's name (which is displayed to users in the UI), and its
+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.
+
+The calculation is expected to be of the format C<"field - field"> where each
+field may be:
+
+=over 4
+
+=item * a core field
+
+For example, L<RT::Ticket> supports: created, starts, started, last updated,
+told, due, resolved.
+
+=item * a custom field
+
+You may use either C<CF.Name> or C<CF.{Longer Name}> syntax.
+
+=item * the word C<now>
+
+=back
+
+Operations besides subtraction are unsupported. There must be whitespace
+between each field and the C<-> substraction operator.
+
+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>.
+
 =item C<$CanonicalizeRedirectURLs>
 
 Set C<$CanonicalizeRedirectURLs> to 1 to use C<$WebURL> when

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


More information about the rt-commit mailing list