1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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::Record - Base class for RT record objects
75 our $_TABLE_ATTR = { };
76 use base RT->Config->Get('RecordBaseClass');
82 $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
83 $self->CurrentUser(@_);
90 The primary keys for RT classes is 'id'
94 sub _PrimaryKeys { return ['id'] }
95 # short circuit many, many thousands of calls from searchbuilder
96 sub _PrimaryKey { 'id' }
100 Override L<DBIx::SearchBuilder/Id> to avoid a few lookups RT doesn't do
101 on a very common codepath
103 C<id> is an alias to C<Id> and is the preferred way to call this method.
108 return shift->{'values'}->{id};
115 Delete this record object from the database.
121 my ($rv) = $self->SUPER::Delete;
123 return ($rv, $self->loc("Object deleted"));
126 return(0, $self->loc("Object could not be deleted"))
132 Returns a string which is this object's type. The type is the class,
133 without the "RT::" prefix.
140 if (ref($self) =~ /^.*::(\w+)$/) {
141 return $self->loc($1);
143 return $self->loc(ref($self));
149 Return this object's attributes as an RT::Attributes object
155 unless ($self->{'attributes'}) {
156 $self->{'attributes'} = RT::Attributes->new($self->CurrentUser);
157 $self->{'attributes'}->LimitToObject($self);
158 $self->{'attributes'}->OrderByCols({FIELD => 'id'});
160 return ($self->{'attributes'});
164 =head2 AddAttribute { Name, Description, Content }
166 Adds a new attribute for this object.
172 my %args = ( Name => undef,
173 Description => undef,
177 my $attr = RT::Attribute->new( $self->CurrentUser );
178 my ( $id, $msg ) = $attr->Create(
180 Name => $args{'Name'},
181 Description => $args{'Description'},
182 Content => $args{'Content'} );
185 # XXX TODO: Why won't RedoSearch work here?
186 $self->Attributes->_DoSearch;
192 =head2 SetAttribute { Name, Description, Content }
194 Like AddAttribute, but replaces all existing attributes with the same Name.
200 my %args = ( Name => undef,
201 Description => undef,
205 my @AttributeObjs = $self->Attributes->Named( $args{'Name'} )
206 or return $self->AddAttribute( %args );
208 my $AttributeObj = pop( @AttributeObjs );
209 $_->Delete foreach @AttributeObjs;
211 $AttributeObj->SetDescription( $args{'Description'} );
212 $AttributeObj->SetContent( $args{'Content'} );
214 $self->Attributes->RedoSearch;
218 =head2 DeleteAttribute NAME
220 Deletes all attributes with the matching name for this object.
224 sub DeleteAttribute {
227 my ($val,$msg) = $self->Attributes->DeleteEntry( Name => $name );
228 $self->ClearAttributes;
232 =head2 FirstAttribute NAME
234 Returns the first attribute with the matching name for this object (as an
235 L<RT::Attribute> object), or C<undef> if no such attributes exist.
236 If there is more than one attribute with the matching name on the
237 object, the first value that was set is returned.
244 return ($self->Attributes->Named( $name ))[0];
248 sub ClearAttributes {
250 delete $self->{'attributes'};
254 sub _Handle { return $RT::Handle }
258 =head2 Create PARAMHASH
260 Takes a PARAMHASH of Column -> Value pairs.
261 If any Column has a Validate$PARAMNAME subroutine defined and the
262 value provided doesn't pass validation, this routine returns
265 If this object's table has any of the following atetributes defined as
266 'Auto', this routine will automatically fill in their values.
285 foreach my $key ( keys %attribs ) {
286 if (my $method = $self->can("Validate$key")) {
287 if (! $method->( $self, $attribs{$key} ) ) {
289 return ( 0, $self->loc('Invalid value for [_1]', $key) );
300 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) = gmtime();
303 sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $mday, $hour, $min, $sec);
305 $attribs{'Created'} = $now_iso if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'});
307 if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) {
308 $attribs{'Creator'} = $self->CurrentUser->id || '0';
310 $attribs{'LastUpdated'} = $now_iso
311 if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'});
313 $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0'
314 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'});
316 my $id = $self->SUPER::Create(%attribs);
317 if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) {
321 $self->loc( "Internal Error: [_1]", $id->{error_message} ) );
328 # If the object was created in the database,
329 # load it up now, so we're sure we get what the database
330 # has. Arguably, this should not be necessary, but there
331 # isn't much we can do about it.
335 return ( $id, $self->loc('Object could not be created') );
343 if (UNIVERSAL::isa('errno',$id)) {
347 $self->Load($id) if ($id);
352 return ( $id, $self->loc('Object created') );
364 Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the
372 # We don't want to hang onto this
373 $self->ClearAttributes;
375 return $self->SUPER::LoadByCols( @_ ) unless $self->_Handle->CaseSensitive;
377 # If this database is case sensitive we need to uncase objects for
380 foreach my $key ( keys %hash ) {
382 # If we've been passed an empty value, we can't do the lookup.
383 # We don't need to explicitly downcase integers or an id.
384 if ( $key ne 'id' && defined $hash{ $key } && $hash{ $key } !~ /^\d+$/ ) {
385 my ($op, $val, $func);
386 ($key, $op, $val, $func) =
387 $self->_Handle->_MakeClauseCaseInsensitive( $key, '=', delete $hash{ $key } );
388 $hash{$key}->{operator} = $op;
389 $hash{$key}->{value} = $val;
390 $hash{$key}->{function} = $func;
393 return $self->SUPER::LoadByCols( %hash );
398 # There is room for optimizations in most of those subs:
403 my $obj = RT::Date->new( $self->CurrentUser );
405 $obj->Set( Format => 'sql', Value => $self->LastUpdated );
413 my $obj = RT::Date->new( $self->CurrentUser );
415 $obj->Set( Format => 'sql', Value => $self->Created );
422 # TODO: This should be deprecated
426 return ( $self->CreatedObj->AgeAsString() );
431 # TODO this should be deprecated
433 sub LastUpdatedAsString {
435 if ( $self->LastUpdated ) {
436 return ( $self->LastUpdatedObj->AsString() );
446 # TODO This should be deprecated
448 sub CreatedAsString {
450 return ( $self->CreatedObj->AsString() );
455 # TODO This should be deprecated
457 sub LongSinceUpdateAsString {
459 if ( $self->LastUpdated ) {
461 return ( $self->LastUpdatedObj->AgeAsString() );
482 #if the user is trying to modify the record
483 # TODO: document _why_ this code is here
485 if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) {
489 my $old_val = $self->__Value($args{'Field'});
490 $self->_SetLastUpdated();
491 my $ret = $self->SUPER::_Set(
492 Field => $args{'Field'},
493 Value => $args{'Value'},
494 IsSQL => $args{'IsSQL'}
496 my ($status, $msg) = $ret->as_array();
498 # @values has two values, a status code and a message.
500 # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool
501 # we want to change the standard "success" message
505 "[_1] changed from [_2] to [_3]",
506 $self->loc( $args{'Field'} ),
507 ( $old_val ? '"' . $old_val . '"' : $self->loc("(no value)") ),
508 '"' . $self->__Value( $args{'Field'}) . '"'
512 $msg = $self->CurrentUser->loc_fuzzy($msg);
514 return wantarray ? ($status, $msg) : $ret;
520 =head2 _SetLastUpdated
522 This routine updates the LastUpdated and LastUpdatedBy columns of the row in question
523 It takes no options. Arguably, this is a bug
527 sub _SetLastUpdated {
530 my $now = RT::Date->new( $self->CurrentUser );
533 if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) {
534 my ( $msg, $val ) = $self->__Set(
535 Field => 'LastUpdated',
539 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) {
540 my ( $msg, $val ) = $self->__Set(
541 Field => 'LastUpdatedBy',
542 Value => $self->CurrentUser->id
551 Returns an RT::User object with the RT account of the creator of this row
557 unless ( exists $self->{'CreatorObj'} ) {
559 $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser );
560 $self->{'CreatorObj'}->Load( $self->Creator );
562 return ( $self->{'CreatorObj'} );
567 =head2 LastUpdatedByObj
569 Returns an RT::User object of the last user to touch this object
573 sub LastUpdatedByObj {
575 unless ( exists $self->{LastUpdatedByObj} ) {
576 $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser );
577 $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy );
579 return $self->{'LastUpdatedByObj'};
586 Returns this record's URI
592 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
593 return($uri->URIForObject($self));
597 =head2 ValidateName NAME
599 Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name
606 if (defined $value && $value=~ /^\d+$/) {
615 =head2 SQLType attribute
617 return the SQL type for the attribute 'attribute' as stored in _ClassAccessible
625 return ($self->_Accessible($field, 'type'));
633 my %args = ( decode_utf8 => 1, @_ );
636 $RT::Logger->error("__Value called with undef field");
639 my $value = $self->SUPER::__Value($field);
641 return undef if (!defined $value);
643 if ( $args{'decode_utf8'} ) {
644 if ( !utf8::is_utf8($value) ) {
645 utf8::decode($value);
649 if ( utf8::is_utf8($value) ) {
650 utf8::encode($value);
658 # Set up defaults for DBIx::SearchBuilder::Record::Cachable
663 'cache_for_sec' => 30,
669 sub _BuildTableAttributes {
671 my $class = ref($self) || $self;
674 if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) {
675 $attributes = $self->_CoreAccessible();
676 } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) {
677 $attributes = $self->_ClassAccessible();
681 foreach my $column (keys %$attributes) {
682 foreach my $attr ( keys %{ $attributes->{$column} } ) {
683 $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr};
686 foreach my $method ( qw(_OverlayAccessible _VendorAccessible _LocalAccessible) ) {
687 next unless UNIVERSAL::can( $self, $method );
688 $attributes = $self->$method();
690 foreach my $column ( keys %$attributes ) {
691 foreach my $attr ( keys %{ $attributes->{$column} } ) {
692 $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr};
699 =head2 _ClassAccessible
701 Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in
702 DBIx::SearchBuilder::Record
706 sub _ClassAccessible {
708 return $_TABLE_ATTR->{ref($self) || $self};
711 =head2 _Accessible COLUMN ATTRIBUTE
713 returns the value of ATTRIBUTE for COLUMN
721 my $attribute = lc(shift);
722 return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column});
723 return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0;
727 =head2 _EncodeLOB BODY MIME_TYPE
729 Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database
736 my $MIMEType = shift || '';
737 my $Filename = shift;
739 my $ContentEncoding = 'none';
741 #get the max attachment length from RT
742 my $MaxSize = RT->Config->Get('MaxAttachmentSize');
744 #if the current attachment contains nulls and the
745 #database doesn't support embedded nulls
747 if ( ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
749 # set a flag telling us to mimencode the attachment
750 $ContentEncoding = 'base64';
752 #cut the max attchment size by 25% (for mime-encoding overhead.
753 $RT::Logger->debug("Max size is $MaxSize");
754 $MaxSize = $MaxSize * 3 / 4;
755 # Some databases (postgres) can't handle non-utf8 data
756 } elsif ( !$RT::Handle->BinarySafeBLOBs
757 && $MIMEType !~ /text\/plain/gi
758 && !Encode::is_utf8( $Body, 1 ) ) {
759 $ContentEncoding = 'quoted-printable';
762 #if the attachment is larger than the maximum size
763 if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
765 # if we're supposed to truncate large attachments
766 if (RT->Config->Get('TruncateLongAttachments')) {
768 # truncate the attachment to that length.
769 $Body = substr( $Body, 0, $MaxSize );
773 # elsif we're supposed to drop large attachments on the floor,
774 elsif (RT->Config->Get('DropLongAttachments')) {
776 # drop the attachment on the floor
777 $RT::Logger->info( "$self: Dropped an attachment of size "
779 $RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) );
780 $Filename .= ".txt" if $Filename;
781 return ("none", "Large attachment dropped", "plain/text", $Filename );
785 # if we need to mimencode the attachment
786 if ( $ContentEncoding eq 'base64' ) {
788 # base64 encode the attachment
789 Encode::_utf8_off($Body);
790 $Body = MIME::Base64::encode_base64($Body);
792 } elsif ($ContentEncoding eq 'quoted-printable') {
793 Encode::_utf8_off($Body);
794 $Body = MIME::QuotedPrint::encode($Body);
798 return ($ContentEncoding, $Body, $MIMEType, $Filename );
804 my $ContentType = shift || '';
805 my $ContentEncoding = shift || 'none';
808 if ( $ContentEncoding eq 'base64' ) {
809 $Content = MIME::Base64::decode_base64($Content);
811 elsif ( $ContentEncoding eq 'quoted-printable' ) {
812 $Content = MIME::QuotedPrint::decode($Content);
814 elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) {
815 return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) );
817 if ( RT::I18N::IsTextualContentType($ContentType) ) {
818 $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content);
823 # A helper table for links mapping to make it easier
824 # to build and parse links between tickets
826 use vars '%LINKDIRMAP';
829 MemberOf => { Base => 'MemberOf',
830 Target => 'HasMember', },
831 RefersTo => { Base => 'RefersTo',
832 Target => 'ReferredToBy', },
833 DependsOn => { Base => 'DependsOn',
834 Target => 'DependedOnBy', },
835 MergedInto => { Base => 'MergedInto',
836 Target => 'MergedInto', },
840 =head2 Update ARGSHASH
842 Updates fields on an object for you using the proper Set methods,
843 skipping unchanged values.
845 ARGSRef => a hashref of attributes => value for the update
846 AttributesRef => an arrayref of keys in ARGSRef that should be updated
847 AttributePrefix => a prefix that should be added to the attributes in AttributesRef
848 when looking up values in ARGSRef
849 Bare attributes are tried before prefixed attributes
851 Returns a list of localized results of the update
860 AttributesRef => undef,
861 AttributePrefix => undef,
865 my $attributes = $args{'AttributesRef'};
866 my $ARGSRef = $args{'ARGSRef'};
869 # gather all new values
870 foreach my $attribute (@$attributes) {
872 if ( defined $ARGSRef->{$attribute} ) {
873 $value = $ARGSRef->{$attribute};
876 defined( $args{'AttributePrefix'} )
878 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
881 $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
888 $value =~ s/\r\n/\n/gs;
890 # If Queue is 'General', we want to resolve the queue name for
893 # This is in an eval block because $object might not exist.
894 # and might not have a Name method. But "can" won't find autoloaded
895 # items. If it fails, we don't care
897 no warnings "uninitialized";
900 my $object = $attribute . "Obj";
901 my $name = $self->$object->Name;
902 next if $name eq $value || $name eq ($value || 0);
904 next if $value eq $self->$attribute();
905 next if ($value || 0) eq $self->$attribute();
908 $new_values{$attribute} = $value;
911 return $self->_UpdateAttributes(
912 Attributes => $attributes,
913 NewValues => \%new_values,
917 sub _UpdateAttributes {
927 foreach my $attribute (@{ $args{Attributes} }) {
928 next if !exists($args{NewValues}{$attribute});
930 my $value = $args{NewValues}{$attribute};
931 my $method = "Set$attribute";
932 my ( $code, $msg ) = $self->$method($value);
933 my ($prefix) = ref($self) =~ /RT(?:.*)::(\w+)/;
935 # Default to $id, but use name if we can get it.
936 my $label = $self->id;
937 $label = $self->Name if (UNIVERSAL::can($self,'Name'));
938 # this requires model names to be loc'ed.
949 push @results, $self->loc( $prefix ) . " $label: ". $msg;
953 "[_1] could not be set to [_2].", # loc
954 "That is already the current value", # loc
955 "No value sent to _Set!", # loc
956 "Illegal value for [_1]", # loc
957 "The new value has been set.", # loc
958 "No column specified", # loc
959 "Immutable field", # loc
960 "Nonexistant field?", # loc
961 "Invalid data", # loc
962 "Couldn't find row", # loc
963 "Missing a primary key?: [_1]", # loc
964 "Found Object", # loc
978 This returns an RT::Links object which references all the tickets
979 which are 'MembersOf' this ticket
985 return ( $self->_Links( 'Target', 'MemberOf' ) );
992 This returns an RT::Links object which references all the tickets that this
993 ticket is a 'MemberOf'
999 return ( $self->_Links( 'Base', 'MemberOf' ) );
1006 This returns an RT::Links object which shows all references for which this ticket is a base
1012 return ( $self->_Links( 'Base', 'RefersTo' ) );
1019 This returns an L<RT::Links> object which shows all references for which this ticket is a target
1025 return ( $self->_Links( 'Target', 'RefersTo' ) );
1032 This returns an RT::Links object which references all the tickets that depend on this one
1038 return ( $self->_Links( 'Target', 'DependsOn' ) );
1044 =head2 HasUnresolvedDependencies
1046 Takes a paramhash of Type (default to '__any'). Returns the number of
1047 unresolved dependencies, if $self->UnresolvedDependencies returns an
1048 object with one or more members of that type. Returns false
1053 sub HasUnresolvedDependencies {
1060 my $deps = $self->UnresolvedDependencies;
1063 $deps->Limit( FIELD => 'Type',
1065 VALUE => $args{Type});
1071 if ($deps->Count > 0) {
1072 return $deps->Count;
1081 =head2 UnresolvedDependencies
1083 Returns an RT::Tickets object of tickets which this ticket depends on
1084 and which have a status of new, open or stalled. (That list comes from
1085 RT::Queue->ActiveStatusArray
1090 sub UnresolvedDependencies {
1092 my $deps = RT::Tickets->new($self->CurrentUser);
1094 my @live_statuses = RT::Queue->ActiveStatusArray();
1095 foreach my $status (@live_statuses) {
1096 $deps->LimitStatus(VALUE => $status);
1098 $deps->LimitDependedOnBy($self->Id);
1106 =head2 AllDependedOnBy
1108 Returns an array of RT::Ticket objects which (directly or indirectly)
1109 depends on this ticket; takes an optional 'Type' argument in the param
1110 hash, which will limit returned tickets to that type, as well as cause
1111 tickets with that type to serve as 'leaf' nodes that stops the recursive
1116 sub AllDependedOnBy {
1118 return $self->_AllLinkedTickets( LinkType => 'DependsOn',
1119 Direction => 'Target', @_ );
1124 Returns an array of RT::Ticket objects which this ticket (directly or
1125 indirectly) depends on; takes an optional 'Type' argument in the param
1126 hash, which will limit returned tickets to that type, as well as cause
1127 tickets with that type to serve as 'leaf' nodes that stops the
1128 recursive dependency search.
1134 return $self->_AllLinkedTickets( LinkType => 'DependsOn',
1135 Direction => 'Base', @_ );
1138 sub _AllLinkedTickets {
1150 my $dep = $self->_Links( $args{Direction}, $args{LinkType});
1151 while (my $link = $dep->Next()) {
1152 my $uri = $args{Direction} eq 'Target' ? $link->BaseURI : $link->TargetURI;
1153 next unless ($uri->IsLocal());
1154 my $obj = $args{Direction} eq 'Target' ? $link->BaseObj : $link->TargetObj;
1155 next if $args{_found}{$obj->Id};
1158 $args{_found}{$obj->Id} = $obj;
1159 $obj->_AllLinkedTickets( %args, _top => 0 );
1161 elsif ($obj->Type and $obj->Type eq $args{Type}) {
1162 $args{_found}{$obj->Id} = $obj;
1165 $obj->_AllLinkedTickets( %args, _top => 0 );
1170 return map { $args{_found}{$_} } sort keys %{$args{_found}};
1181 This returns an RT::Links object which references all the tickets that this ticket depends on
1187 return ( $self->_Links( 'Base', 'DependsOn' ) );
1195 =head2 Links DIRECTION [TYPE]
1197 Return links (L<RT::Links>) to/from this object.
1199 DIRECTION is either 'Base' or 'Target'.
1201 TYPE is a type of links to return, it can be omitted to get
1206 sub Links { shift->_Links(@_) }
1211 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1214 my $type = shift || "";
1216 unless ( $self->{"$field$type"} ) {
1217 $self->{"$field$type"} = RT::Links->new( $self->CurrentUser );
1218 # at least to myself
1219 $self->{"$field$type"}->Limit( FIELD => $field,
1220 VALUE => $self->URI,
1221 ENTRYAGGREGATOR => 'OR' );
1222 $self->{"$field$type"}->Limit( FIELD => 'Type',
1226 return ( $self->{"$field$type"} );
1234 Takes a Type and returns a string that is more human readable.
1240 my %args = ( Type => '',
1243 $args{Type} =~ s/([A-Z])/" " . lc $1/ge;
1244 $args{Type} =~ s/^\s+//;
1253 Takes either a Target or a Base and returns a string of human friendly text.
1259 my %args = ( Object => undef,
1263 my $text = "URI " . $args{FallBack};
1264 if ($args{Object} && $args{Object}->isa("RT::Ticket")) {
1265 $text = "Ticket " . $args{Object}->id;
1274 Takes a paramhash of Type and one of Base or Target. Adds that link to this object.
1276 Returns C<link id>, C<message> and C<exist> flag.
1283 my %args = ( Target => '',
1290 # Remote_link is the URI of the object that is not this ticket
1294 if ( $args{'Base'} and $args{'Target'} ) {
1295 $RT::Logger->debug( "$self tried to create a link. both base and target were specified" );
1296 return ( 0, $self->loc("Can't specifiy both base and target") );
1298 elsif ( $args{'Base'} ) {
1299 $args{'Target'} = $self->URI();
1300 $remote_link = $args{'Base'};
1301 $direction = 'Target';
1303 elsif ( $args{'Target'} ) {
1304 $args{'Base'} = $self->URI();
1305 $remote_link = $args{'Target'};
1306 $direction = 'Base';
1309 return ( 0, $self->loc('Either base or target must be specified') );
1312 # Check if the link already exists - we don't want duplicates
1314 my $old_link = RT::Link->new( $self->CurrentUser );
1315 $old_link->LoadByParams( Base => $args{'Base'},
1316 Type => $args{'Type'},
1317 Target => $args{'Target'} );
1318 if ( $old_link->Id ) {
1319 $RT::Logger->debug("$self Somebody tried to duplicate a link");
1320 return ( $old_link->id, $self->loc("Link already exists"), 1 );
1326 # Storing the link in the DB.
1327 my $link = RT::Link->new( $self->CurrentUser );
1328 my ($linkid, $linkmsg) = $link->Create( Target => $args{Target},
1329 Base => $args{Base},
1330 Type => $args{Type} );
1333 $RT::Logger->error("Link could not be created: ".$linkmsg);
1334 return ( 0, $self->loc("Link could not be created") );
1337 my $basetext = $self->FormatLink(Object => $link->BaseObj,
1338 FallBack => $args{Base});
1339 my $targettext = $self->FormatLink(Object => $link->TargetObj,
1340 FallBack => $args{Target});
1341 my $typetext = $self->FormatType(Type => $args{Type});
1343 "$basetext $typetext $targettext.";
1344 return ( $linkid, $TransString ) ;
1351 Delete a link. takes a paramhash of Base, Target and Type.
1352 Either Base or Target must be null. The null value will
1353 be replaced with this ticket\'s id
1366 #we want one of base and target. we don't care which
1367 #but we only want _one_
1372 if ( $args{'Base'} and $args{'Target'} ) {
1373 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target");
1374 return ( 0, $self->loc("Can't specifiy both base and target") );
1376 elsif ( $args{'Base'} ) {
1377 $args{'Target'} = $self->URI();
1378 $remote_link = $args{'Base'};
1379 $direction = 'Target';
1381 elsif ( $args{'Target'} ) {
1382 $args{'Base'} = $self->URI();
1383 $remote_link = $args{'Target'};
1387 $RT::Logger->error("Base or Target must be specified");
1388 return ( 0, $self->loc('Either base or target must be specified') );
1391 my $link = RT::Link->new( $self->CurrentUser );
1392 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} );
1395 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
1399 my $basetext = $self->FormatLink(Object => $link->BaseObj,
1400 FallBack => $args{Base});
1401 my $targettext = $self->FormatLink(Object => $link->TargetObj,
1402 FallBack => $args{Target});
1403 my $typetext = $self->FormatType(Type => $args{Type});
1404 my $linkid = $link->id;
1406 my $TransString = "$basetext no longer $typetext $targettext.";
1407 return ( 1, $TransString);
1410 #if it's not a link we can find
1412 $RT::Logger->debug("Couldn't find that link");
1413 return ( 0, $self->loc("Link not found") );
1418 =head1 LockForUpdate
1420 In a database transaction, gains an exclusive lock on the row, to
1421 prevent race conditions. On SQLite, this is a "RESERVED" lock on the
1429 my $pk = $self->_PrimaryKey;
1430 my $id = @_ ? $_[0] : $self->$pk;
1431 $self->_expire if $self->isa("DBIx::SearchBuilder::Record::Cachable");
1432 if (RT->Config->Get('DatabaseType') eq "SQLite") {
1433 # SQLite does DB-level locking, upgrading the transaction to
1434 # "RESERVED" on the first UPDATE/INSERT/DELETE. Do a no-op
1435 # UPDATE to force the upgade.
1436 return RT->DatabaseHandle->dbh->do(
1437 "UPDATE " .$self->Table.
1438 " SET $pk = $pk WHERE 1 = 0");
1440 return $self->_LoadFromSQL(
1441 "SELECT * FROM ".$self->Table
1442 ." WHERE $pk = ? FOR UPDATE",
1448 =head2 _NewTransaction PARAMHASH
1450 Private function to create a new RT::Transaction object for this ticket update
1454 sub _NewTransaction {
1461 OldReference => undef,
1462 NewReference => undef,
1463 ReferenceType => undef,
1467 ActivateScrips => 1,
1469 SquelchMailTo => undef,
1473 my $in_txn = RT->DatabaseHandle->TransactionDepth;
1474 RT->DatabaseHandle->BeginTransaction unless $in_txn;
1476 $self->LockForUpdate;
1478 my $old_ref = $args{'OldReference'};
1479 my $new_ref = $args{'NewReference'};
1480 my $ref_type = $args{'ReferenceType'};
1481 if ($old_ref or $new_ref) {
1482 $ref_type ||= ref($old_ref) || ref($new_ref);
1484 $RT::Logger->error("Reference type not specified for transaction");
1487 $old_ref = $old_ref->Id if ref($old_ref);
1488 $new_ref = $new_ref->Id if ref($new_ref);
1491 require RT::Transaction;
1492 my $trans = RT::Transaction->new( $self->CurrentUser );
1493 my ( $transaction, $msg ) = $trans->Create(
1494 ObjectId => $self->Id,
1495 ObjectType => ref($self),
1496 TimeTaken => $args{'TimeTaken'},
1497 Type => $args{'Type'},
1498 Data => $args{'Data'},
1499 Field => $args{'Field'},
1500 NewValue => $args{'NewValue'},
1501 OldValue => $args{'OldValue'},
1502 NewReference => $new_ref,
1503 OldReference => $old_ref,
1504 ReferenceType => $ref_type,
1505 MIMEObj => $args{'MIMEObj'},
1506 ActivateScrips => $args{'ActivateScrips'},
1507 CommitScrips => $args{'CommitScrips'},
1508 SquelchMailTo => $args{'SquelchMailTo'},
1511 # Rationalize the object since we may have done things to it during the caching.
1512 $self->Load($self->Id);
1514 $RT::Logger->warning($msg) unless $transaction;
1516 $self->_SetLastUpdated;
1518 if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) {
1519 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
1521 if ( RT->Config->Get('UseTransactionBatch') and $transaction ) {
1522 push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'};
1525 RT->DatabaseHandle->Commit unless $in_txn;
1527 return ( $transaction, $msg, $trans );
1534 Returns an RT::Transactions object of all transactions on this record object
1541 use RT::Transactions;
1542 my $transactions = RT::Transactions->new( $self->CurrentUser );
1544 #If the user has no rights, return an empty object
1545 $transactions->Limit(
1546 FIELD => 'ObjectId',
1549 $transactions->Limit(
1550 FIELD => 'ObjectType',
1551 VALUE => ref($self),
1554 return ($transactions);
1561 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1563 $cfs->SetContextObject( $self );
1564 # XXX handle multiple types properly
1565 $cfs->LimitToLookupType( $self->CustomFieldLookupType );
1566 $cfs->LimitToGlobalOrObjectId(
1567 $self->_LookupId( $self->CustomFieldLookupType )
1569 $cfs->ApplySortOrder;
1574 # TODO: This _only_ works for RT::Class classes. it doesn't work, for example,
1575 # for RT::IR classes.
1580 my @classes = ($lookup =~ /RT::(\w+)-/g);
1583 foreach my $class (reverse @classes) {
1584 my $method = "${class}Obj";
1585 $object = $object->$method;
1592 =head2 CustomFieldLookupType
1594 Returns the path RT uses to figure out which custom fields apply to this object.
1598 sub CustomFieldLookupType {
1604 =head2 AddCustomFieldValue { Field => FIELD, Value => VALUE }
1606 VALUE should be a string. FIELD can be any identifier of a CustomField
1607 supported by L</LoadCustomFieldByIdentifier> method.
1609 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
1610 deletes the old value.
1611 If VALUE is not a valid value for the custom field, returns
1612 (0, 'Error message' ) otherwise, returns ($id, 'Success Message') where
1613 $id is ID of created L<ObjectCustomFieldValue> object.
1617 sub AddCustomFieldValue {
1619 $self->_AddCustomFieldValue(@_);
1622 sub _AddCustomFieldValue {
1627 LargeContent => undef,
1628 ContentType => undef,
1629 RecordTransaction => 1,
1633 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1634 unless ( $cf->Id ) {
1635 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1638 my $OCFs = $self->CustomFields;
1639 $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id );
1640 unless ( $OCFs->Count ) {
1644 "Custom field [_1] does not apply to this object",
1645 ref $args{'Field'} ? $args{'Field'}->id : $args{'Field'}
1650 # empty string is not correct value of any CF, so undef it
1651 foreach ( qw(Value LargeContent) ) {
1652 $args{ $_ } = undef if defined $args{ $_ } && !length $args{ $_ };
1655 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
1656 return ( 0, $self->loc("Invalid value for custom field") );
1659 # If the custom field only accepts a certain # of values, delete the existing
1660 # value and record a "changed from foo to bar" transaction
1661 unless ( $cf->UnlimitedValues ) {
1663 # Load up a ObjectCustomFieldValues object for this custom field and this ticket
1664 my $values = $cf->ValuesForObject($self);
1666 # We need to whack any old values here. In most cases, the custom field should
1667 # only have one value to delete. In the pathalogical case, this custom field
1668 # used to be a multiple and we have many values to whack....
1669 my $cf_values = $values->Count;
1671 if ( $cf_values > $cf->MaxValues ) {
1672 my $i = 0; #We want to delete all but the max we can currently have , so we can then
1673 # execute the same code to "change" the value from old to new
1674 while ( my $value = $values->Next ) {
1676 if ( $i < $cf_values ) {
1677 my ( $val, $msg ) = $cf->DeleteValueForObject(
1679 Content => $value->Content
1684 my ( $TransactionId, $Msg, $TransactionObj ) =
1685 $self->_NewTransaction(
1686 Type => 'CustomField',
1688 OldReference => $value,
1692 $values->RedoSearch if $i; # redo search if have deleted at least one value
1695 my ( $old_value, $old_content );
1696 if ( $old_value = $values->First ) {
1697 $old_content = $old_value->Content;
1698 $old_content = undef if defined $old_content && !length $old_content;
1700 my $is_the_same = 1;
1701 if ( defined $args{'Value'} ) {
1702 $is_the_same = 0 unless defined $old_content
1703 && lc $old_content eq lc $args{'Value'};
1705 $is_the_same = 0 if defined $old_content;
1707 if ( $is_the_same ) {
1708 my $old_content = $old_value->LargeContent;
1709 if ( defined $args{'LargeContent'} ) {
1710 $is_the_same = 0 unless defined $old_content
1711 && $old_content eq $args{'LargeContent'};
1713 $is_the_same = 0 if defined $old_content;
1717 return $old_value->id if $is_the_same;
1720 my ( $new_value_id, $value_msg ) = $cf->AddValueForObject(
1722 Content => $args{'Value'},
1723 LargeContent => $args{'LargeContent'},
1724 ContentType => $args{'ContentType'},
1727 unless ( $new_value_id ) {
1728 return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg ) );
1731 my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1732 $new_value->Load( $new_value_id );
1734 # now that adding the new value was successful, delete the old one
1736 my ( $val, $msg ) = $old_value->Delete();
1737 return ( 0, $msg ) unless $val;
1740 if ( $args{'RecordTransaction'} ) {
1741 my ( $TransactionId, $Msg, $TransactionObj ) =
1742 $self->_NewTransaction(
1743 Type => 'CustomField',
1745 OldReference => $old_value,
1746 NewReference => $new_value,
1750 my $new_content = $new_value->Content;
1752 # For datetime, we need to display them in "human" format in result message
1753 #XXX TODO how about date without time?
1754 if ($cf->Type eq 'DateTime') {
1755 my $DateObj = RT::Date->new( $self->CurrentUser );
1758 Value => $new_content,
1760 $new_content = $DateObj->AsString;
1762 if ( defined $old_content && length $old_content ) {
1765 Value => $old_content,
1767 $old_content = $DateObj->AsString;
1771 unless ( defined $old_content && length $old_content ) {
1772 return ( $new_value_id, $self->loc( "[_1] [_2] added", $cf->Name, $new_content ));
1774 elsif ( !defined $new_content || !length $new_content ) {
1775 return ( $new_value_id,
1776 $self->loc( "[_1] [_2] deleted", $cf->Name, $old_content ) );
1779 return ( $new_value_id, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content, $new_content));
1784 # otherwise, just add a new value and record "new value added"
1786 my ($new_value_id, $msg) = $cf->AddValueForObject(
1788 Content => $args{'Value'},
1789 LargeContent => $args{'LargeContent'},
1790 ContentType => $args{'ContentType'},
1793 unless ( $new_value_id ) {
1794 return ( 0, $self->loc( "Could not add new custom field value: [_1]", $msg ) );
1796 if ( $args{'RecordTransaction'} ) {
1797 my ( $tid, $msg ) = $self->_NewTransaction(
1798 Type => 'CustomField',
1800 NewReference => $new_value_id,
1801 ReferenceType => 'RT::ObjectCustomFieldValue',
1804 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $msg ) );
1807 return ( $new_value_id, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name ) );
1813 =head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
1815 Deletes VALUE as a value of CustomField FIELD.
1817 VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue.
1819 If VALUE is not a valid value for the custom field, returns
1820 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1824 sub DeleteCustomFieldValue {
1833 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1834 unless ( $cf->Id ) {
1835 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1838 my ( $val, $msg ) = $cf->DeleteValueForObject(
1840 Id => $args{'ValueId'},
1841 Content => $args{'Value'},
1847 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
1848 Type => 'CustomField',
1850 OldReference => $val,
1851 ReferenceType => 'RT::ObjectCustomFieldValue',
1853 unless ($TransactionId) {
1854 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1857 my $old_value = $TransactionObj->OldValue;
1858 # For datetime, we need to display them in "human" format in result message
1859 if ( $cf->Type eq 'DateTime' ) {
1860 my $DateObj = RT::Date->new( $self->CurrentUser );
1863 Value => $old_value,
1865 $old_value = $DateObj->AsString;
1870 "[_1] is no longer a value for custom field [_2]",
1871 $old_value, $cf->Name
1878 =head2 FirstCustomFieldValue FIELD
1880 Return the content of the first value of CustomField FIELD for this ticket
1881 Takes a field id or name
1885 sub FirstCustomFieldValue {
1889 my $values = $self->CustomFieldValues( $field );
1890 return undef unless my $first = $values->First;
1891 return $first->Content;
1894 =head2 CustomFieldValuesAsString FIELD
1896 Return the content of the CustomField FIELD for this ticket.
1897 If this is a multi-value custom field, values will be joined with newlines.
1899 Takes a field id or name as the first argument
1901 Takes an optional Separator => "," second and third argument
1902 if you want to join the values using something other than a newline
1906 sub CustomFieldValuesAsString {
1910 my $separator = $args{Separator} || "\n";
1912 my $values = $self->CustomFieldValues( $field );
1913 return join ($separator, grep { defined $_ }
1914 map { $_->Content } @{$values->ItemsArrayRef});
1919 =head2 CustomFieldValues FIELD
1921 Return a ObjectCustomFieldValues object of all values of the CustomField whose
1922 id or Name is FIELD for this record.
1924 Returns an RT::ObjectCustomFieldValues object
1928 sub CustomFieldValues {
1933 my $cf = $self->LoadCustomFieldByIdentifier( $field );
1935 # we were asked to search on a custom field we couldn't find
1936 unless ( $cf->id ) {
1937 $RT::Logger->warning("Couldn't load custom field by '$field' identifier");
1938 return RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1940 return ( $cf->ValuesForObject($self) );
1943 # we're not limiting to a specific custom field;
1944 my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1945 $ocfs->LimitToObject( $self );
1949 =head2 LoadCustomFieldByIdentifier IDENTIFER
1951 Find the custom field has id or name IDENTIFIER for this object.
1953 If no valid field is found, returns an empty RT::CustomField object.
1957 sub LoadCustomFieldByIdentifier {
1962 if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) {
1963 $cf = RT::CustomField->new($self->CurrentUser);
1964 $cf->SetContextObject( $self );
1965 $cf->LoadById( $field->id );
1967 elsif ($field =~ /^\d+$/) {
1968 $cf = RT::CustomField->new($self->CurrentUser);
1969 $cf->SetContextObject( $self );
1970 $cf->LoadById($field);
1973 my $cfs = $self->CustomFields($self->CurrentUser);
1974 $cfs->SetContextObject( $self );
1975 $cfs->Limit(FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0);
1976 $cf = $cfs->First || RT::CustomField->new($self->CurrentUser);
1981 sub ACLEquivalenceObjects { }
1983 sub BasicColumns { }
1986 return RT->Config->Get('WebPath'). "/index.html?q=";
1989 RT::Base->_ImportOverlays();