[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