1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 RT::SearchBuilder - a baseclass for RT collection objects
65 package RT::SearchBuilder;
71 use base qw(DBIx::SearchBuilder RT::Base);
74 use DBIx::SearchBuilder "1.50";
76 use Scalar::Util qw/blessed/;
81 $self->{'user'} = shift;
82 unless(defined($self->CurrentUser)) {
84 Carp::confess("$self was created without a CurrentUser");
85 $RT::Logger->err("$self was created without a CurrentUser");
88 $self->SUPER::_Init( 'Handle' => $RT::Handle);
91 sub _Handle { return $RT::Handle }
95 $self->{'_sql_aliases'} = {};
96 delete $self->{'handled_disabled_column'};
97 delete $self->{'find_disabled_rows'};
98 return $self->SUPER::CleanSlate(@_);
105 $args{'DISTINCT'} = 1 if
106 !exists $args{'DISTINCT'}
107 && $args{'TABLE2'} && lc($args{'FIELD2'}||'') eq 'id';
109 return $self->SUPER::Join( %args );
112 sub JoinTransactions {
114 my %args = ( New => 0, @_ );
116 return $self->{'_sql_aliases'}{'transactions'}
117 if !$args{'New'} && $self->{'_sql_aliases'}{'transactions'};
119 my $alias = $self->Join(
122 TABLE2 => 'Transactions',
123 FIELD2 => 'ObjectId',
126 # NewItem is necessary here because of RT::Report::Tickets and RT::Report::Tickets::Entry
127 my $item = $self->NewItem;
128 my $object_type = $item->can('ObjectType') ? $item->ObjectType : ref $item;
130 $self->RT::SearchBuilder::Limit(
132 FIELD => 'ObjectType',
133 VALUE => $object_type,
135 $self->{'_sql_aliases'}{'transactions'} = $alias
143 my ($row, $cfkey, $cf) = @_;
145 $cfkey .= ".ordering" if !blessed($cf) || ($cf->MaxValues||0) != 1;
146 my ($ocfvs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
147 # this is described in _LimitCustomField
151 OPERATOR => 'IS NOT',
153 ENTRYAGGREGATOR => 'AND',
154 SUBCLAUSE => ".ordering",
156 my $CFvs = $self->Join(
159 FIELD1 => 'CustomField',
160 TABLE2 => 'CustomFieldValues',
161 FIELD2 => 'CustomField',
167 VALUE => "$ocfvs.Content",
168 ENTRYAGGREGATOR => 'AND'
171 return { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' },
172 { %$row, ALIAS => $ocfvs, FIELD => 'Content' };
179 next if defined $s->{FIELD} and $s->{FIELD} =~ /\W/;
180 $s->{FIELD} = $s->{FUNCTION} if $s->{FUNCTION};
183 return $self->SUPER::OrderByCols( @sort );
186 # If we're setting RowsPerPage or FirstRow, ensure we get a natural number or undef.
189 return if @_ and defined $_[0] and $_[0] =~ /\D/;
190 return $self->SUPER::RowsPerPage(@_);
195 return if @_ and defined $_[0] and $_[0] =~ /\D/;
196 return $self->SUPER::FirstRow(@_);
199 =head2 LimitToEnabled
201 Only find items that haven't been disabled
208 $self->{'handled_disabled_column'} = 1;
209 $self->Limit( FIELD => 'Disabled', VALUE => '0' );
212 =head2 LimitToDeleted
214 Only find items that have been deleted.
221 $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
222 $self->Limit( FIELD => 'Disabled', VALUE => '1' );
227 Find all matching rows, regardless of whether they are disabled or not
232 shift->{'find_disabled_rows'} = 1;
235 =head2 LimitCustomField
237 Takes a paramhash of key/value pairs with the following keys:
241 =item CUSTOMFIELD - CustomField id. Optional
243 =item OPERATOR - The usual Limit operators
245 =item VALUE - The value to compare against
253 my $class = ref($self) || $self;
254 $class =~ s/s$// or die "Cannot deduce SingularClass for $class";
260 Returns class name of records in this collection. This generic implementation
261 just strips trailing 's'.
266 $_[0]->_SingularClass
269 =head2 RegisterCustomFieldJoin
271 Takes a pair of arguments, the first a class name and the second a callback
272 function. The class will be used to call
273 L<RT::Record/CustomFieldLookupType>. The callback will be called when
274 limiting a collection of the caller's class by a CF of the passed class's
277 The callback is passed a single argument, the current collection object (C<$self>).
279 An example from L<RT::Tickets>:
281 __PACKAGE__->RegisterCustomFieldJoin(
282 "RT::Transaction" => sub { $_[0]->JoinTransactions }
285 Returns true on success, undef on failure.
289 sub RegisterCustomFieldJoin {
291 my ($type, $callback) = @_;
293 $type = $type->CustomFieldLookupType if $type;
295 die "Unknown LookupType '$type'"
296 unless $type and grep { $_ eq $type } RT::CustomField->LookupTypes;
298 die "Custom field join callbacks must be CODE references"
299 unless ref($callback) eq 'CODE';
301 warn "Another custom field join callback is already registered for '$type'"
302 if $class->_JOINS_FOR_LOOKUP_TYPES->{$type};
304 # Stash the callback on ourselves
305 $class->_JOINS_FOR_LOOKUP_TYPES->{ $type } = $callback;
310 =head2 _JoinForLookupType
312 Takes an L<RT::CustomField> LookupType and joins this collection as
313 appropriate to reach the object records to which LookupType applies. The
314 object records will be of the class returned by
315 L<RT::CustomField/ObjectTypeFromLookupType>.
317 Returns the join alias suitable for further limiting against object
320 Returns undef on failure.
322 Used by L</_CustomFieldJoin>.
326 sub _JoinForLookupType {
328 my $type = shift or return;
330 # Convenience shortcut so that classes don't need to register a handler
331 # for their native lookup type
332 return "main" if $type eq $self->RecordClass->CustomFieldLookupType
333 and grep { $_ eq $type } RT::CustomField->LookupTypes;
335 my $JOINS = $self->_JOINS_FOR_LOOKUP_TYPES;
336 return $JOINS->{$type}->($self)
337 if ref $JOINS->{$type} eq 'CODE';
342 sub _JOINS_FOR_LOOKUP_TYPES {
343 my $class = blessed($_[0]) || $_[0];
345 return $JOINS{$class} ||= {};
348 =head2 _CustomFieldJoin
350 Factor out the Join of custom fields so we can use it for sorting too
354 sub _CustomFieldJoin {
355 my ($self, $cfkey, $cf, $type) = @_;
356 $type ||= $self->RecordClass->CustomFieldLookupType;
358 # Perform one Join per CustomField
359 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
360 $self->{_sql_cf_alias}{$cfkey} )
362 return ( $self->{_sql_object_cfv_alias}{$cfkey},
363 $self->{_sql_cf_alias}{$cfkey} );
366 my $ObjectAlias = $self->_JoinForLookupType($type)
367 or die "We don't know how to join for LookupType $type";
369 my ($ocfvalias, $CFs);
370 if ( blessed($cf) ) {
371 $ocfvalias = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
373 ALIAS1 => $ObjectAlias,
375 TABLE2 => 'ObjectCustomFieldValues',
376 FIELD2 => 'ObjectId',
377 $cf->SingleValue? (DISTINCT => 1) : (),
380 LEFTJOIN => $ocfvalias,
381 FIELD => 'CustomField',
383 ENTRYAGGREGATOR => 'AND'
387 ($ocfvalias, $CFs) = $self->_CustomFieldJoinByName( $ObjectAlias, $cf, $type );
388 $self->{_sql_cf_alias}{$cfkey} = $CFs;
389 $self->{_sql_object_cfv_alias}{$cfkey} = $ocfvalias;
392 LEFTJOIN => $ocfvalias,
393 FIELD => 'ObjectType',
394 VALUE => RT::CustomField->ObjectTypeFromLookupType($type),
395 ENTRYAGGREGATOR => 'AND'
398 LEFTJOIN => $ocfvalias,
402 ENTRYAGGREGATOR => 'AND'
405 return ($ocfvalias, $CFs);
408 sub _CustomFieldJoinByName {
410 my ($ObjectAlias, $cf, $type) = @_;
411 my $ocfalias = $self->Join(
413 EXPRESSION => q|'0'|,
414 TABLE2 => 'ObjectCustomFields',
415 FIELD2 => 'ObjectId',
418 my $CFs = $self->Join(
421 FIELD1 => 'CustomField',
422 TABLE2 => 'CustomFields',
427 ENTRYAGGREGATOR => 'AND',
428 FIELD => 'LookupType',
433 ENTRYAGGREGATOR => 'AND',
439 my $ocfvalias = $self->Join(
443 TABLE2 => 'ObjectCustomFieldValues',
444 FIELD2 => 'CustomField',
447 LEFTJOIN => $ocfvalias,
449 VALUE => "$ObjectAlias.id",
451 ENTRYAGGREGATOR => 'AND',
454 return ($ocfvalias, $CFs, $ocfalias);
457 sub LimitCustomField {
459 return $self->_LimitCustomField( @_ );
462 use Regexp::Common qw(RE_net_IPv4);
463 use Regexp::Common::net::CIDR;
465 sub _LimitCustomField {
467 my %args = ( VALUE => undef,
468 CUSTOMFIELD => undef,
474 my $op = delete $args{OPERATOR};
475 my $value = delete $args{VALUE};
476 my $ltype = delete $args{LOOKUPTYPE} || $self->RecordClass->CustomFieldLookupType;
477 my $cf = delete $args{CUSTOMFIELD};
478 my $column = delete $args{COLUMN};
479 my $cfkey = delete $args{KEY};
480 if (blessed($cf) and $cf->id) {
482 } elsif ($cf =~ /^\d+$/) {
483 # Intentionally load as the system user, so we can build better
484 # queries; this is necessary as we don't have a context object
485 # which might grant the user rights to see the CF. This object
486 # is only used to inspect the properties of the CF itself.
487 my $obj = RT::CustomField->new( RT->SystemUser );
493 $cfkey ||= "$ltype-$cf";
496 $cfkey ||= "$ltype-$cf";
499 $args{SUBCLAUSE} ||= "cf-$cfkey";
503 return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
506 return %args unless $args{'FIELD'} eq 'LargeContent';
508 my $op = $args{'OPERATOR'};
510 $args{'OPERATOR'} = 'MATCHES';
512 elsif ( $op eq '!=' ) {
513 $args{'OPERATOR'} = 'NOT MATCHES';
515 elsif ( $op =~ /^[<>]=?$/ ) {
516 $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
521 # Special Limit (we can exit early)
522 # IS NULL and IS NOT NULL checks
523 if ( $op =~ /^IS( NOT)?$/i ) {
524 my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf, $ltype );
525 $self->_OpenParen( $args{SUBCLAUSE} );
529 FIELD => ($column || 'id'),
533 # See below for an explanation of this limit
537 OPERATOR => 'IS NOT',
539 ENTRYAGGREGATOR => 'AND',
540 SUBCLAUSE => $args{SUBCLAUSE},
542 $self->_CloseParen( $args{SUBCLAUSE} );
546 ########## Content pre-parsing if we know things about the CF
547 if ( blessed($cf) and delete $args{PREPARSE} ) {
548 my $type = $cf->Type;
549 if ( $type eq 'IPAddress' ) {
550 my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
554 $RT::Logger->warn("$value is not a valid IPAddress");
556 } elsif ( $type eq 'IPAddressRange' ) {
557 my ( $start_ip, $end_ip ) =
558 RT::ObjectCustomFieldValue->ParseIPRange($value);
559 if ( $start_ip && $end_ip ) {
560 if ( $op =~ /^<=?$/ ) {
562 } elsif ($op =~ /^>=?$/ ) {
565 $value = join '-', $start_ip, $end_ip;
568 $RT::Logger->warn("$value is not a valid IPAddressRange");
571 # Recurse if they want a range comparison
572 if ( $op !~ /^[<>]=?$/ ) {
573 my ($start_ip, $end_ip) = split /-/, $value;
574 $self->_OpenParen( $args{SUBCLAUSE} );
575 # Ideally we would limit >= 000.000.000.000 and <=
576 # 255.255.255.255 so DB optimizers could use better
577 # estimations and scan less rows, but this breaks with IPv6.
578 if ( $op !~ /NOT|!=|<>/i ) { # positive equation
579 $self->_LimitCustomField(
583 LOOKUPTYPE => $ltype,
588 $self->_LimitCustomField(
592 LOOKUPTYPE => $ltype,
594 COLUMN => 'LargeContent',
595 ENTRYAGGREGATOR => 'AND',
598 } else { # negative equation
599 $self->_LimitCustomField(
603 LOOKUPTYPE => $ltype,
608 $self->_LimitCustomField(
612 LOOKUPTYPE => $ltype,
614 COLUMN => 'LargeContent',
615 ENTRYAGGREGATOR => 'OR',
619 $self->_CloseParen( $args{SUBCLAUSE} );
622 } elsif ( $type =~ /^Date(?:Time)?$/ ) {
623 my $date = RT::Date->new( $self->CurrentUser );
624 $date->Set( Format => 'unknown', Value => $value );
625 if ( $date->IsSet ) {
628 # Heuristics to determine if a date, and not
629 # a datetime, was entered:
630 || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
631 || ( $value !~ /midnight|\d+:\d+:\d+/i
632 && $date->Time( Timezone => 'user' ) eq '00:00:00' )
635 $value = $date->Date( Timezone => 'user' );
637 $value = $date->DateTime;
640 $RT::Logger->warn("$value is not a valid date string");
643 # Recurse if day equality is being checked on a datetime
644 if ( $type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
645 my $date = RT::Date->new( $self->CurrentUser );
646 $date->Set( Format => 'unknown', Value => $value );
647 my $daystart = $date->ISO;
649 my $dayend = $date->ISO;
651 $self->_OpenParen( $args{SUBCLAUSE} );
652 $self->_LimitCustomField(
656 LOOKUPTYPE => $ltype,
659 ENTRYAGGREGATOR => 'AND',
663 $self->_LimitCustomField(
667 LOOKUPTYPE => $ltype,
670 ENTRYAGGREGATOR => 'AND',
673 $self->_CloseParen( $args{SUBCLAUSE} );
681 my $single_value = !blessed($cf) || $cf->SingleValue;
682 my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
683 my $value_is_long = (length( Encode::encode( "UTF-8", $value)) > 255) ? 1 : 0;
685 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++
686 if not $single_value and $op =~ /^(!?=|(NOT )?LIKE)$/i;
687 my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf, $ltype );
689 # A negative limit on a multi-value CF means _none_ of the values
690 # are the given value
691 if ( $negative_op and not $single_value ) {
692 # Reverse the limit we apply to the join, and check IS NULL
693 $op =~ s/!|NOT\s+//i;
695 # Ideally we would check both Content and LargeContent here, as
696 # the positive searches do below -- however, we cannot place
697 # complex limits inside LEFTJOINs due to searchbuilder
698 # limitations. Guessing which to check based on the value's
699 # string length is sufficient for !=, but sadly insufficient for
700 # NOT LIKE checks, giving false positives.
701 $column ||= $value_is_long ? 'LargeContent' : 'Content';
702 $self->Limit( $fix_op->(
703 LEFTJOIN => $ocfvalias,
720 # If column is defined, then we just search it that, with no magic
722 $self->_OpenParen( $args{SUBCLAUSE} );
723 $self->Limit( $fix_op->(
736 ENTRYAGGREGATOR => 'OR',
737 SUBCLAUSE => $args{SUBCLAUSE},
739 $self->_CloseParen( $args{SUBCLAUSE} );
743 $self->_OpenParen( $args{SUBCLAUSE} ); # For negative_op "OR it is null" clause
744 $self->_OpenParen( $args{SUBCLAUSE} ); # NAME IS NOT NULL clause
746 $self->_OpenParen( $args{SUBCLAUSE} ); # Check Content / LargeContent
747 if ($value_is_long and $op eq "=") {
748 # Doesn't matter what Content contains, as it cannot match the
749 # too-long value; we just look in LargeContent, below.
750 } elsif ($value_is_long and $op =~ /^(!=|<>)$/) {
751 # If Content is non-null, that's a valid way to _not_ contain the too-long value.
756 OPERATOR => 'IS NOT',
760 # Otherwise, go looking at the Content
771 if (!$value_is_long and $op eq "=") {
772 # Doesn't matter what LargeContent contains, as it cannot match
774 } elsif (!$value_is_long and $op =~ /^(!=|<>)$/) {
775 # If LargeContent is non-null, that's a valid way to _not_
776 # contain the too-short value.
780 FIELD => 'LargeContent',
781 OPERATOR => 'IS NOT',
783 ENTRYAGGREGATOR => 'OR',
786 $self->_OpenParen( $args{SUBCLAUSE} ); # LargeContent check
787 $self->_OpenParen( $args{SUBCLAUSE} ); # Content is null?
793 ENTRYAGGREGATOR => 'OR',
794 SUBCLAUSE => $args{SUBCLAUSE},
801 ENTRYAGGREGATOR => 'OR',
802 SUBCLAUSE => $args{SUBCLAUSE},
804 $self->_CloseParen( $args{SUBCLAUSE} ); # Content is null?
805 $self->Limit( $fix_op->(
807 FIELD => 'LargeContent',
810 ENTRYAGGREGATOR => 'AND',
811 SUBCLAUSE => $args{SUBCLAUSE},
814 $self->_CloseParen( $args{SUBCLAUSE} ); # LargeContent check
817 $self->_CloseParen( $args{SUBCLAUSE} ); # Check Content/LargeContent
819 # XXX: if we join via CustomFields table then
820 # because of order of left joins we get NULLs in
821 # CF table and then get nulls for those records
822 # in OCFVs table what result in wrong results
823 # as decifer method now tries to load a CF then
824 # we fall into this situation only when there
825 # are more than one CF with the name in the DB.
826 # the same thing applies to order by call.
827 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
828 # we want treat IS NULL as (not applies or has
833 OPERATOR => 'IS NOT',
835 ENTRYAGGREGATOR => 'AND',
836 SUBCLAUSE => $args{SUBCLAUSE},
838 $self->_CloseParen( $args{SUBCLAUSE} ); # Name IS NOT NULL clause
840 # If we were looking for != or NOT LIKE, we need to include the
841 # possibility that the row had no value.
847 ENTRYAGGREGATOR => 'OR',
848 SUBCLAUSE => $args{SUBCLAUSE},
850 $self->_CloseParen( $args{SUBCLAUSE} ); # negative_op clause
853 =head2 Limit PARAMHASH
855 This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus
856 making sure that by default lots of things don't do extra work trying to
857 match lower(colname) agaist lc($val);
859 We also force VALUE to C<NULL> when the OPERATOR is C<IS> or C<IS NOT>.
860 This ensures that we don't pass invalid SQL to the database or allow SQL
861 injection attacks when we pass through user specified values.
865 my %check_case_sensitivity = (
866 groups => { 'name' => 1, domain => 1 },
867 queues => { 'name' => 1 },
868 users => { 'name' => 1, emailaddress => 1 },
869 customfields => { 'name' => 1 },
876 principals => { objectid => 'id' },
886 # We use the same regex here that DBIx::SearchBuilder uses to exclude
887 # values from quoting
888 if ( $ARGS{'OPERATOR'} =~ /IS/i ) {
889 # Don't pass anything but NULL for IS and IS NOT
890 $ARGS{'VALUE'} = 'NULL';
893 if (($ARGS{FIELD}||'') =~ /\W/
894 or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>=
896 |(NOT\s*)?(STARTS|ENDS)WITH
902 $RT::Logger->crit("Possible SQL injection attack: $ARGS{FIELD} $ARGS{OPERATOR}");
912 ($table) = $ARGS{'ALIAS'} && $ARGS{'ALIAS'} ne 'main'
913 ? ($ARGS{'ALIAS'} =~ /^(.*)_\d+$/)
917 if ( $table and $ARGS{FIELD} and my $instead = $deprecated{ lc $table }{ lc $ARGS{'FIELD'} } ) {
919 Message => "$table.$ARGS{'FIELD'} column is deprecated",
920 Instead => $instead, Remove => '4.4'
924 unless ( exists $ARGS{CASESENSITIVE} or (exists $ARGS{QUOTEVALUE} and not $ARGS{QUOTEVALUE}) ) {
925 if ( $ARGS{FIELD} and $ARGS{'OPERATOR'} !~ /IS/i
926 && $table && $check_case_sensitivity{ lc $table }{ lc $ARGS{'FIELD'} }
929 "Case sensitive search by $table.$ARGS{'FIELD'}"
930 ." at ". (caller)[1] . " line ". (caller)[2]
933 $ARGS{'CASESENSITIVE'} = 1;
936 return $self->SUPER::Limit( %ARGS );
941 If it has a SortOrder attribute, sort the array by SortOrder.
942 Otherwise, if it has a "Name" attribute, sort alphabetically by Name
943 Otherwise, just give up and return it in the order it came from the
952 if ($self->RecordClass->_Accessible('SortOrder','read')) {
953 $items = [ sort { $a->SortOrder <=> $b->SortOrder } @{$items} ];
955 elsif ($self->RecordClass->_Accessible('Name','read')) {
956 $items = [ sort { lc($a->Name) cmp lc($b->Name) } @{$items} ];
964 Return this object's ItemsArray, in the order that ItemsOrderBy sorts
971 return $self->ItemsOrderBy($self->SUPER::ItemsArrayRef());
974 # make sure that Disabled rows never get seen unless
975 # we're explicitly trying to see them.
980 if ( $self->{'with_disabled_column'}
981 && !$self->{'handled_disabled_column'}
982 && !$self->{'find_disabled_rows'}
984 $self->LimitToEnabled;
986 return $self->SUPER::_DoSearch(@_);
991 if ( $self->{'with_disabled_column'}
992 && !$self->{'handled_disabled_column'}
993 && !$self->{'find_disabled_rows'}
995 $self->LimitToEnabled;
997 return $self->SUPER::_DoCount(@_);
1000 =head2 ColumnMapClassName
1002 ColumnMap needs a Collection name to load the correct list display.
1003 Depluralization is hard, so provide an easy way to correct the naive
1004 algorithm that this code uses.
1008 sub ColumnMapClassName {
1010 my $Class = $self->_SingularClass;
1017 Returns a new item based on L</RecordClass> using the current user.
1023 return $self->RecordClass->new($self->CurrentUser);
1026 =head2 NotSetDateToNullFunction
1028 Takes a paramhash with an optional FIELD key whose value is the name of a date
1029 column. If no FIELD is provided, a literal C<?> placeholder is used so the
1030 caller can fill in the field later.
1032 Returns a SQL function which evaluates to C<NULL> if the FIELD is set to the
1033 Unix epoch; otherwise it evaluates to FIELD. This is useful because RT
1034 currently stores unset dates as a Unix epoch timestamp instead of NULL, but
1035 NULLs are often more desireable.
1039 sub NotSetDateToNullFunction {
1041 my %args = ( FIELD => undef, @_ );
1043 my $res = "CASE WHEN ? BETWEEN '1969-12-31 11:59:59' AND '1970-01-01 12:00:01' THEN NULL ELSE ? END";
1044 if ( $args{FIELD} ) {
1045 $res = $self->CombineFunctionWithField( %args, FUNCTION => $res );
1050 RT::Base->_ImportOverlays();