1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
6 # <jesse@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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
49 RT::Record - Base class for RT record objects
59 ok (require RT::Record);
72 use DBIx::SearchBuilder::Record::Cachable;
75 use vars qw/@ISA $_TABLE_ATTR/;
79 if ($RT::DontCacheSearchBuilderRecords ) {
80 push (@ISA, 'DBIx::SearchBuilder::Record');
82 push (@ISA, 'DBIx::SearchBuilder::Record::Cachable');
90 $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
91 $self->CurrentUser(@_);
100 The primary keys for RT classes is 'id'
113 Delete this record object from the database.
119 my ($rv) = $self->SUPER::Delete;
121 return ($rv, $self->loc("Object deleted"));
124 return(0, $self->loc("Object could not be deleted"))
130 Returns a string which is this object's type. The type is the class,
131 without the "RT::" prefix.
135 my $ticket = RT::Ticket->new($RT::SystemUser);
136 my $group = RT::Group->new($RT::SystemUser);
137 is($ticket->ObjectTypeStr, 'Ticket', "Ticket returns correct typestring");
138 is($group->ObjectTypeStr, 'Group', "Group returns correct typestring");
146 if (ref($self) =~ /^.*::(\w+)$/) {
147 return $self->loc($1);
149 return $self->loc(ref($self));
155 Return this object's attributes as an RT::Attributes object
162 unless ($self->{'attributes'}) {
163 $self->{'attributes'} = RT::Attributes->new($self->CurrentUser);
164 $self->{'attributes'}->LimitToObject($self);
166 return ($self->{'attributes'});
171 =head2 AddAttribute { Name, Description, Content }
173 Adds a new attribute for this object.
179 my %args = ( Name => undef,
180 Description => undef,
184 my $attr = RT::Attribute->new( $self->CurrentUser );
185 my ( $id, $msg ) = $attr->Create(
187 Name => $args{'Name'},
188 Description => $args{'Description'},
189 Content => $args{'Content'} );
192 # XXX TODO: Why won't RedoSearch work here?
193 $self->Attributes->_DoSearch;
199 =head2 SetAttribute { Name, Description, Content }
201 Like AddAttribute, but replaces all existing attributes with the same Name.
207 my %args = ( Name => undef,
208 Description => undef,
212 my @AttributeObjs = $self->Attributes->Named( $args{'Name'} )
213 or return $self->AddAttribute( %args );
215 my $AttributeObj = pop( @AttributeObjs );
216 $_->Delete foreach @AttributeObjs;
218 $AttributeObj->SetDescription( $args{'Description'} );
219 $AttributeObj->SetContent( $args{'Content'} );
221 $self->Attributes->RedoSearch;
225 =head2 DeleteAttribute NAME
227 Deletes all attributes with the matching name for this object.
231 sub DeleteAttribute {
234 return $self->Attributes->DeleteEntry( Name => $name );
237 =head2 FirstAttribute NAME
239 Returns the value of the first attribute with the matching name
240 for this object, or C<undef> if no such attributes exist.
247 return ($self->Attributes->Named( $name ))[0];
254 return ($RT::Handle);
261 =head2 Create PARAMHASH
263 Takes a PARAMHASH of Column -> Value pairs.
264 If any Column has a Validate$PARAMNAME subroutine defined and the
265 value provided doesn't pass validation, this routine returns
268 If this object's table has any of the following atetributes defined as
269 'Auto', this routine will automatically fill in their values.
276 foreach my $key ( keys %attribs ) {
277 my $method = "Validate$key";
278 unless ( $self->$method( $attribs{$key} ) ) {
280 return ( 0, $self->loc('Invalid value for [_1]', $key) );
287 my $now = RT::Date->new( $self->CurrentUser );
288 $now->Set( Format => 'unix', Value => time );
289 $attribs{'Created'} = $now->ISO() if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'});
291 if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) {
292 $attribs{'Creator'} = $self->CurrentUser->id || '0';
294 $attribs{'LastUpdated'} = $now->ISO()
295 if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'});
297 $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0'
298 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'});
300 my $id = $self->SUPER::Create(%attribs);
301 if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) {
305 $self->loc( "Internal Error: [_1]", $id->{error_message} ) );
312 # If the object was created in the database,
313 # load it up now, so we're sure we get what the database
314 # has. Arguably, this should not be necessary, but there
315 # isn't much we can do about it.
319 return ( $id, $self->loc('Object could not be created') );
327 if (UNIVERSAL::isa('errno',$id)) {
333 $self->Load($id) if ($id);
338 return ( $id, $self->loc('Object created') );
352 Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the
361 # We don't want to hang onto this
362 delete $self->{'attributes'};
364 # If this database is case sensitive we need to uncase objects for
366 if ( $self->_Handle->CaseSensitive ) {
368 foreach my $key ( keys %hash ) {
370 # If we've been passed an empty value, we can't do the lookup.
371 # We don't need to explicitly downcase integers or an id.
373 || !defined( $hash{$key} )
374 || $hash{$key} =~ /^\d+$/
377 $newhash{$key} = $hash{$key};
380 my ($op, $val, $func);
381 ($key, $op, $val, $func) = $self->_Handle->_MakeClauseCaseInsensitive($key, '=', $hash{$key});
382 $newhash{$key}->{operator} = $op;
383 $newhash{$key}->{value} = $val;
384 $newhash{$key}->{function} = $func;
388 # We've clobbered everything we care about. bash the old hash
389 # and replace it with the new hash
392 $self->SUPER::LoadByCols(%hash);
399 # There is room for optimizations in most of those subs:
405 my $obj = new RT::Date( $self->CurrentUser );
407 $obj->Set( Format => 'sql', Value => $self->LastUpdated );
417 my $obj = new RT::Date( $self->CurrentUser );
419 $obj->Set( Format => 'sql', Value => $self->Created );
428 # TODO: This should be deprecated
432 return ( $self->CreatedObj->AgeAsString() );
437 # {{{ LastUpdatedAsString
439 # TODO this should be deprecated
441 sub LastUpdatedAsString {
443 if ( $self->LastUpdated ) {
444 return ( $self->LastUpdatedObj->AsString() );
454 # {{{ CreatedAsString
456 # TODO This should be deprecated
458 sub CreatedAsString {
460 return ( $self->CreatedObj->AsString() );
465 # {{{ LongSinceUpdateAsString
467 # TODO This should be deprecated
469 sub LongSinceUpdateAsString {
471 if ( $self->LastUpdated ) {
473 return ( $self->LastUpdatedObj->AgeAsString() );
497 #if the user is trying to modify the record
498 # TODO: document _why_ this code is here
500 if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) {
504 my $old_val = $self->__Value($args{'Field'});
505 $self->_SetLastUpdated();
506 my $ret = $self->SUPER::_Set(
507 Field => $args{'Field'},
508 Value => $args{'Value'},
509 IsSQL => $args{'IsSQL'}
511 my ($status, $msg) = $ret->as_array();
513 # @values has two values, a status code and a message.
515 # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool
516 # we want to change the standard "success" message
520 "[_1] changed from [_2] to [_3]",
522 ( $old_val ? "'$old_val'" : $self->loc("(no value)") ),
523 '"' . $self->__Value( $args{'Field'}) . '"'
527 $msg = $self->CurrentUser->loc_fuzzy($msg);
529 return wantarray ? ($status, $msg) : $ret;
535 # {{{ sub _SetLastUpdated
537 =head2 _SetLastUpdated
539 This routine updates the LastUpdated and LastUpdatedBy columns of the row in question
540 It takes no options. Arguably, this is a bug
544 sub _SetLastUpdated {
547 my $now = new RT::Date( $self->CurrentUser );
550 if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) {
551 my ( $msg, $val ) = $self->__Set(
552 Field => 'LastUpdated',
556 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) {
557 my ( $msg, $val ) = $self->__Set(
558 Field => 'LastUpdatedBy',
559 Value => $self->CurrentUser->id
570 Returns an RT::User object with the RT account of the creator of this row
576 unless ( exists $self->{'CreatorObj'} ) {
578 $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser );
579 $self->{'CreatorObj'}->Load( $self->Creator );
581 return ( $self->{'CreatorObj'} );
586 # {{{ sub LastUpdatedByObj
588 =head2 LastUpdatedByObj
590 Returns an RT::User object of the last user to touch this object
594 sub LastUpdatedByObj {
596 unless ( exists $self->{LastUpdatedByObj} ) {
597 $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser );
598 $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy );
600 return $self->{'LastUpdatedByObj'};
609 Returns this record's URI
615 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
616 return($uri->URIForObject($self));
621 =head2 ValidateName NAME
623 Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name
630 if ($value && $value=~ /^\d+$/) {
639 =head2 SQLType attribute
641 return the SQL type for the attribute 'attribute' as stored in _ClassAccessible
649 return ($self->_Accessible($field, 'type'));
654 require Encode::compat if $] < 5.007001;
663 my %args = ( decode_utf8 => 1,
666 unless (defined $field && $field) {
667 $RT::Logger->error("$self __Value called with undef field");
669 my $value = $self->SUPER::__Value($field);
671 return('') if ( !defined($value) || $value eq '');
673 if( $args{'decode_utf8'} ) {
674 # XXX: is_utf8 check should be here unless Encode bug would be fixed
675 # see http://rt.cpan.org/NoAuth/Bug.html?id=14559
676 return Encode::decode_utf8($value) unless Encode::is_utf8($value);
678 # check is_utf8 here just to be shure
679 return Encode::encode_utf8($value) if Encode::is_utf8($value);
684 # Set up defaults for DBIx::SearchBuilder::Record::Cachable
689 'cache_for_sec' => 30,
695 sub _BuildTableAttributes {
699 if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) {
700 $attributes = $self->_CoreAccessible();
701 } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) {
702 $attributes = $self->_ClassAccessible();
706 foreach my $column (%$attributes) {
707 foreach my $attr ( %{ $attributes->{$column} } ) {
708 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
711 if ( UNIVERSAL::can( $self, '_OverlayAccessible' ) ) {
712 $attributes = $self->_OverlayAccessible();
714 foreach my $column (%$attributes) {
715 foreach my $attr ( %{ $attributes->{$column} } ) {
716 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
720 if ( UNIVERSAL::can( $self, '_VendorAccessible' ) ) {
721 $attributes = $self->_VendorAccessible();
723 foreach my $column (%$attributes) {
724 foreach my $attr ( %{ $attributes->{$column} } ) {
725 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
729 if ( UNIVERSAL::can( $self, '_LocalAccessible' ) ) {
730 $attributes = $self->_LocalAccessible();
732 foreach my $column (%$attributes) {
733 foreach my $attr ( %{ $attributes->{$column} } ) {
734 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
742 =head2 _ClassAccessible
744 Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in
745 DBIx::SearchBuilder::Record
749 sub _ClassAccessible {
751 return $_TABLE_ATTR->{ ref($self) || $self };
754 =head2 _Accessible COLUMN ATTRIBUTE
756 returns the value of ATTRIBUTE for COLUMN
764 my $attribute = lc(shift);
765 return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column});
766 return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0;
770 =head2 _EncodeLOB BODY MIME_TYPE
772 Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database
779 my $MIMEType = shift;
781 my $ContentEncoding = 'none';
783 #get the max attachment length from RT
784 my $MaxSize = $RT::MaxAttachmentSize;
786 #if the current attachment contains nulls and the
787 #database doesn't support embedded nulls
789 if ( $RT::AlwaysUseBase64 or
790 ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
792 # set a flag telling us to mimencode the attachment
793 $ContentEncoding = 'base64';
795 #cut the max attchment size by 25% (for mime-encoding overhead.
796 $RT::Logger->debug("Max size is $MaxSize\n");
797 $MaxSize = $MaxSize * 3 / 4;
798 # Some databases (postgres) can't handle non-utf8 data
799 } elsif ( !$RT::Handle->BinarySafeBLOBs
800 && $MIMEType !~ /text\/plain/gi
801 && !Encode::is_utf8( $Body, 1 ) ) {
802 $ContentEncoding = 'quoted-printable';
805 #if the attachment is larger than the maximum size
806 if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
808 # if we're supposed to truncate large attachments
809 if ($RT::TruncateLongAttachments) {
811 # truncate the attachment to that length.
812 $Body = substr( $Body, 0, $MaxSize );
816 # elsif we're supposed to drop large attachments on the floor,
817 elsif ($RT::DropLongAttachments) {
819 # drop the attachment on the floor
820 $RT::Logger->info( "$self: Dropped an attachment of size "
821 . length($Body) . "\n"
822 . "It started: " . substr( $Body, 0, 60 ) . "\n"
824 return ("none", "Large attachment dropped" );
828 # if we need to mimencode the attachment
829 if ( $ContentEncoding eq 'base64' ) {
831 # base64 encode the attachment
832 Encode::_utf8_off($Body);
833 $Body = MIME::Base64::encode_base64($Body);
835 } elsif ($ContentEncoding eq 'quoted-printable') {
836 Encode::_utf8_off($Body);
837 $Body = MIME::QuotedPrint::encode($Body);
841 return ($ContentEncoding, $Body);
847 my $ContentType = shift;
848 my $ContentEncoding = shift;
851 if ( $ContentEncoding eq 'base64' ) {
852 $Content = MIME::Base64::decode_base64($Content);
854 elsif ( $ContentEncoding eq 'quoted-printable' ) {
855 $Content = MIME::QuotedPrint::decode($Content);
857 elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) {
858 return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) );
860 if ( $ContentType eq 'text/plain' ) {
861 $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content);
867 # A helper table for links mapping to make it easier
868 # to build and parse links between tickets
870 use vars '%LINKDIRMAP';
873 MemberOf => { Base => 'MemberOf',
874 Target => 'HasMember', },
875 RefersTo => { Base => 'RefersTo',
876 Target => 'ReferredToBy', },
877 DependsOn => { Base => 'DependsOn',
878 Target => 'DependedOnBy', },
879 MergedInto => { Base => 'MergedInto',
880 Target => 'MergedInto', },
889 AttributesRef => undef,
890 AttributePrefix => undef,
894 my $attributes = $args{'AttributesRef'};
895 my $ARGSRef = $args{'ARGSRef'};
898 foreach my $attribute (@$attributes) {
900 if ( defined $ARGSRef->{$attribute} ) {
901 $value = $ARGSRef->{$attribute};
904 defined( $args{'AttributePrefix'} )
906 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
909 $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
916 $value =~ s/\r\n/\n/gs;
919 # If Queue is 'General', we want to resolve the queue name for
922 # This is in an eval block because $object might not exist.
923 # and might not have a Name method. But "can" won't find autoloaded
924 # items. If it fails, we don't care
926 my $object = $attribute . "Obj";
927 next if ($self->$object->Name eq $value);
929 next if ( $value eq $self->$attribute() );
930 my $method = "Set$attribute";
931 my ( $code, $msg ) = $self->$method($value);
932 my ($prefix) = ref($self) =~ /RT::(\w+)/;
934 # Default to $id, but use name if we can get it.
935 my $label = $self->id;
936 $label = $self->Name if (UNIVERSAL::can($self,'Name'));
937 push @results, $self->loc( "$prefix [_1]", $label ) . ': '. $msg;
941 "[_1] could not be set to [_2].", # loc
942 "That is already the current value", # loc
943 "No value sent to _Set!\n", # loc
944 "Illegal value for [_1]", # loc
945 "The new value has been set.", # loc
946 "No column specified", # loc
947 "Immutable field", # loc
948 "Nonexistant field?", # loc
949 "Invalid data", # loc
950 "Couldn't find row", # loc
951 "Missing a primary key?: [_1]", # loc
952 "Found Object", # loc
961 # {{{ Routines dealing with Links
963 # {{{ Link Collections
969 This returns an RT::Links object which references all the tickets
970 which are 'MembersOf' this ticket
976 return ( $self->_Links( 'Target', 'MemberOf' ) );
985 This returns an RT::Links object which references all the tickets that this
986 ticket is a 'MemberOf'
992 return ( $self->_Links( 'Base', 'MemberOf' ) );
1001 This returns an RT::Links object which shows all references for which this ticket is a base
1007 return ( $self->_Links( 'Base', 'RefersTo' ) );
1016 This returns an RT::Links object which shows all references for which this ticket is a target
1022 return ( $self->_Links( 'Target', 'RefersTo' ) );
1031 This returns an RT::Links object which references all the tickets that depend on this one
1037 return ( $self->_Links( 'Target', 'DependsOn' ) );
1044 =head2 HasUnresolvedDependencies
1046 Takes a paramhash of Type (default to '__any'). Returns true if
1047 $self->UnresolvedDependencies returns an object with one or more members
1048 of that type. Returns false otherwise
1053 my $t1 = RT::Ticket->new($RT::SystemUser);
1054 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
1055 ok($id, "Created dep test 1 - $msg");
1057 my $t2 = RT::Ticket->new($RT::SystemUser);
1058 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
1059 ok($id2, "Created dep test 2 - $msg2");
1060 my $t3 = RT::Ticket->new($RT::SystemUser);
1061 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
1062 ok($id3, "Created dep test 3 - $msg3");
1063 my ($addid, $addmsg);
1064 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t2->id));
1065 ok ($addid, $addmsg);
1066 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t3->id));
1068 ok ($addid, $addmsg);
1069 my $link = RT::Link->new($RT::SystemUser);
1070 my ($rv, $msg) = $link->Load($addid);
1072 ok ($link->LocalTarget == $t3->id, "Link LocalTarget is correct");
1073 ok ($link->LocalBase == $t1->id, "Link LocalBase is correct");
1075 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
1076 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
1077 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
1078 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
1081 my ($rid, $rmsg)= $t1->Resolve();
1083 my ($rid2, $rmsg2) = $t2->Resolve();
1085 ($rid, $rmsg)= $t1->Resolve();
1087 my ($rid3,$rmsg3) = $t3->Resolve;
1089 ($rid, $rmsg)= $t1->Resolve();
1097 sub HasUnresolvedDependencies {
1104 my $deps = $self->UnresolvedDependencies;
1107 $deps->Limit( FIELD => 'Type',
1109 VALUE => $args{Type});
1115 if ($deps->Count > 0) {
1124 # {{{ UnresolvedDependencies
1126 =head2 UnresolvedDependencies
1128 Returns an RT::Tickets object of tickets which this ticket depends on
1129 and which have a status of new, open or stalled. (That list comes from
1130 RT::Queue->ActiveStatusArray
1135 sub UnresolvedDependencies {
1137 my $deps = RT::Tickets->new($self->CurrentUser);
1139 my @live_statuses = RT::Queue->ActiveStatusArray();
1140 foreach my $status (@live_statuses) {
1141 $deps->LimitStatus(VALUE => $status);
1143 $deps->LimitDependedOnBy($self->Id);
1151 # {{{ AllDependedOnBy
1153 =head2 AllDependedOnBy
1155 Returns an array of RT::Ticket objects which (directly or indirectly)
1156 depends on this ticket; takes an optional 'Type' argument in the param
1157 hash, which will limit returned tickets to that type, as well as cause
1158 tickets with that type to serve as 'leaf' nodes that stops the recursive
1163 sub AllDependedOnBy {
1165 my $dep = $self->DependedOnBy;
1173 while (my $link = $dep->Next()) {
1174 next unless ($link->BaseURI->IsLocal());
1175 next if $args{_found}{$link->BaseObj->Id};
1178 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1179 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1181 elsif ($link->BaseObj->Type eq $args{Type}) {
1182 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1185 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1190 return map { $args{_found}{$_} } sort keys %{$args{_found}};
1203 This returns an RT::Links object which references all the tickets that this ticket depends on
1209 return ( $self->_Links( 'Base', 'DependsOn' ) );
1219 =head2 Links DIRECTION TYPE
1221 return links to/from this object.
1230 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1233 my $type = shift || "";
1235 unless ( $self->{"$field$type"} ) {
1236 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
1237 # at least to myself
1238 $self->{"$field$type"}->Limit( FIELD => $field,
1239 VALUE => $self->URI,
1240 ENTRYAGGREGATOR => 'OR' );
1241 $self->{"$field$type"}->Limit( FIELD => 'Type',
1245 return ( $self->{"$field$type"} );
1256 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
1264 my %args = ( Target => '',
1271 # Remote_link is the URI of the object that is not this ticket
1275 if ( $args{'Base'} and $args{'Target'} ) {
1276 $RT::Logger->debug( "$self tried to create a link. both base and target were specified\n" );
1277 return ( 0, $self->loc("Can't specifiy both base and target") );
1279 elsif ( $args{'Base'} ) {
1280 $args{'Target'} = $self->URI();
1281 my $class = ref($self);
1282 $remote_link = $args{'Base'};
1283 $direction = 'Target';
1285 elsif ( $args{'Target'} ) {
1286 $args{'Base'} = $self->URI();
1287 my $class = ref($self);
1288 $remote_link = $args{'Target'};
1289 $direction = 'Base';
1292 return ( 0, $self->loc('Either base or target must be specified') );
1295 # {{{ Check if the link already exists - we don't want duplicates
1297 my $old_link = RT::Link->new( $self->CurrentUser );
1298 $old_link->LoadByParams( Base => $args{'Base'},
1299 Type => $args{'Type'},
1300 Target => $args{'Target'} );
1301 if ( $old_link->Id ) {
1302 $RT::Logger->debug("$self Somebody tried to duplicate a link");
1303 return ( $old_link->id, $self->loc("Link already exists") );
1309 # Storing the link in the DB.
1310 my $link = RT::Link->new( $self->CurrentUser );
1311 my ($linkid, $linkmsg) = $link->Create( Target => $args{Target},
1312 Base => $args{Base},
1313 Type => $args{Type} );
1316 $RT::Logger->error("Link could not be created: ".$linkmsg);
1317 return ( 0, $self->loc("Link could not be created") );
1321 "Record $args{'Base'} $args{Type} record $args{'Target'}.";
1323 return ( $linkid, $self->loc( "Link created ([_1])", $TransString ) );
1328 # {{{ sub _DeleteLink
1332 Delete a link. takes a paramhash of Base, Target and Type.
1333 Either Base or Target must be null. The null value will
1334 be replaced with this ticket\'s id
1347 #we want one of base and target. we don't care which
1348 #but we only want _one_
1353 if ( $args{'Base'} and $args{'Target'} ) {
1354 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
1355 return ( 0, $self->loc("Can't specifiy both base and target") );
1357 elsif ( $args{'Base'} ) {
1358 $args{'Target'} = $self->URI();
1359 $remote_link = $args{'Base'};
1360 $direction = 'Target';
1362 elsif ( $args{'Target'} ) {
1363 $args{'Base'} = $self->URI();
1364 $remote_link = $args{'Target'};
1368 $RT::Logger->debug("$self: Base or Target must be specified\n");
1369 return ( 0, $self->loc('Either base or target must be specified') );
1372 my $link = new RT::Link( $self->CurrentUser );
1373 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
1376 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
1380 my $linkid = $link->id;
1383 my $TransString = "Record $args{'Base'} no longer $args{Type} record $args{'Target'}.";
1384 return ( 1, $self->loc("Link deleted ([_1])", $TransString));
1387 #if it's not a link we can find
1389 $RT::Logger->debug("Couldn't find that link\n");
1390 return ( 0, $self->loc("Link not found") );
1398 # {{{ Routines dealing with transactions
1400 # {{{ sub _NewTransaction
1402 =head2 _NewTransaction PARAMHASH
1404 Private function to create a new RT::Transaction object for this ticket update
1408 sub _NewTransaction {
1415 OldReference => undef,
1416 NewReference => undef,
1417 ReferenceType => undef,
1421 ActivateScrips => 1,
1426 my $old_ref = $args{'OldReference'};
1427 my $new_ref = $args{'NewReference'};
1428 my $ref_type = $args{'ReferenceType'};
1429 if ($old_ref or $new_ref) {
1430 $ref_type ||= ref($old_ref) || ref($new_ref);
1432 $RT::Logger->error("Reference type not specified for transaction");
1435 $old_ref = $old_ref->Id if ref($old_ref);
1436 $new_ref = $new_ref->Id if ref($new_ref);
1439 require RT::Transaction;
1440 my $trans = new RT::Transaction( $self->CurrentUser );
1441 my ( $transaction, $msg ) = $trans->Create(
1442 ObjectId => $self->Id,
1443 ObjectType => ref($self),
1444 TimeTaken => $args{'TimeTaken'},
1445 Type => $args{'Type'},
1446 Data => $args{'Data'},
1447 Field => $args{'Field'},
1448 NewValue => $args{'NewValue'},
1449 OldValue => $args{'OldValue'},
1450 NewReference => $new_ref,
1451 OldReference => $old_ref,
1452 ReferenceType => $ref_type,
1453 MIMEObj => $args{'MIMEObj'},
1454 ActivateScrips => $args{'ActivateScrips'},
1455 CommitScrips => $args{'CommitScrips'},
1458 # Rationalize the object since we may have done things to it during the caching.
1459 $self->Load($self->Id);
1461 $RT::Logger->warning($msg) unless $transaction;
1463 $self->_SetLastUpdated;
1465 if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) {
1466 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
1468 if ( $RT::UseTransactionBatch and $transaction ) {
1469 push @{$self->{_TransactionBatch}}, $trans;
1471 return ( $transaction, $msg, $trans );
1476 # {{{ sub Transactions
1480 Returns an RT::Transactions object of all transactions on this record object
1487 use RT::Transactions;
1488 my $transactions = RT::Transactions->new( $self->CurrentUser );
1490 #If the user has no rights, return an empty object
1491 $transactions->Limit(
1492 FIELD => 'ObjectId',
1495 $transactions->Limit(
1496 FIELD => 'ObjectType',
1497 VALUE => ref($self),
1500 return ($transactions);
1506 # {{{ Routines dealing with custom fields
1510 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1512 # XXX handle multiple types properly
1513 $cfs->LimitToLookupType( $self->CustomFieldLookupType );
1514 $cfs->LimitToGlobalOrObjectId(
1515 $self->_LookupId( $self->CustomFieldLookupType ) );
1520 # TODO: This _only_ works for RT::Class classes. it doesn't work, for example, for RT::FM classes.
1525 my @classes = ($lookup =~ /RT::(\w+)-/g);
1528 foreach my $class (reverse @classes) {
1529 my $method = "${class}Obj";
1530 $object = $object->$method;
1537 =head2 CustomFieldLookupType
1539 Returns the path RT uses to figure out which custom fields apply to this object.
1543 sub CustomFieldLookupType {
1548 #TODO Deprecated API. Destroy in 3.6
1551 $RT::Logger->warning("_LookupTypes call is deprecated at (". join(":",caller)."). Replace with CustomFieldLookupType");
1553 return($self->CustomFieldLookupType);
1557 # {{{ AddCustomFieldValue
1559 =head2 AddCustomFieldValue { Field => FIELD, Value => VALUE }
1561 VALUE should be a string.
1562 FIELD can be a CustomField object OR a CustomField ID.
1565 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
1566 deletes the old value.
1567 If VALUE is not a valid value for the custom field, returns
1568 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1572 sub AddCustomFieldValue {
1574 $self->_AddCustomFieldValue(@_);
1577 sub _AddCustomFieldValue {
1582 RecordTransaction => 1,
1586 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1588 unless ( $cf->Id ) {
1589 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1592 my $OCFs = $self->CustomFields;
1593 $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id );
1594 unless ( $OCFs->Count ) {
1598 "Custom field [_1] does not apply to this object",
1603 # Load up a ObjectCustomFieldValues object for this custom field and this ticket
1604 my $values = $cf->ValuesForObject($self);
1606 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
1607 return ( 0, $self->loc("Invalid value for custom field") );
1610 # If the custom field only accepts a certain # of values, delete the existing
1611 # value and record a "changed from foo to bar" transaction
1612 unless ( $cf->UnlimitedValues) {
1614 # We need to whack any old values here. In most cases, the custom field should
1615 # only have one value to delete. In the pathalogical case, this custom field
1616 # used to be a multiple and we have many values to whack....
1617 my $cf_values = $values->Count;
1619 if ( $cf_values > $cf->MaxValues ) {
1620 my $i = 0; #We want to delete all but the max we can currently have , so we can then
1621 # execute the same code to "change" the value from old to new
1622 while ( my $value = $values->Next ) {
1624 if ( $i < $cf_values ) {
1625 my ( $val, $msg ) = $cf->DeleteValueForObject(
1627 Content => $value->Content
1632 my ( $TransactionId, $Msg, $TransactionObj ) =
1633 $self->_NewTransaction(
1634 Type => 'CustomField',
1636 OldReference => $value,
1640 $values->RedoSearch if $i; # redo search if have deleted at least one value
1643 my ( $old_value, $old_content );
1644 if ( $old_value = $values->First ) {
1645 $old_content = $old_value->Content();
1646 return (1) if( $old_content eq $args{'Value'} && $old_value->LargeContent eq $args{'LargeContent'});;
1649 my ( $new_value_id, $value_msg ) = $cf->AddValueForObject(
1651 Content => $args{'Value'},
1652 LargeContent => $args{'LargeContent'},
1653 ContentType => $args{'ContentType'},
1656 unless ($new_value_id) {
1657 return ( 0, $self->loc( "Could not add new custom field value. [_1] ",, $value_msg));
1660 my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1661 $new_value->Load($new_value_id);
1663 # now that adding the new value was successful, delete the old one
1665 my ( $val, $msg ) = $old_value->Delete();
1671 if ( $args{'RecordTransaction'} ) {
1672 my ( $TransactionId, $Msg, $TransactionObj ) =
1673 $self->_NewTransaction(
1674 Type => 'CustomField',
1676 OldReference => $old_value,
1677 NewReference => $new_value,
1681 if ( $old_value eq '' ) {
1682 return ( 1, $self->loc( "[_1] [_2] added", $cf->Name, $new_value->Content ));
1684 elsif ( $new_value->Content eq '' ) {
1686 $self->loc( "[_1] [_2] deleted", $cf->Name, $old_value->Content ) );
1689 return ( 1, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content, $new_value->Content));
1694 # otherwise, just add a new value and record "new value added"
1696 my ($new_value_id) = $cf->AddValueForObject(
1698 Content => $args{'Value'},
1699 LargeContent => $args{'LargeContent'},
1700 ContentType => $args{'ContentType'},
1703 unless ($new_value_id) {
1704 return ( 0, $self->loc("Could not add new custom field value. ") );
1706 if ( $args{'RecordTransaction'} ) {
1707 my ( $TransactionId, $Msg, $TransactionObj ) =
1708 $self->_NewTransaction(
1709 Type => 'CustomField',
1711 NewReference => $new_value_id,
1712 ReferenceType => 'RT::ObjectCustomFieldValue',
1714 unless ($TransactionId) {
1716 $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1719 return ( 1, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name));
1726 # {{{ DeleteCustomFieldValue
1728 =head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
1730 Deletes VALUE as a value of CustomField FIELD.
1732 VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue.
1734 If VALUE is not a valid value for the custom field, returns
1735 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1739 sub DeleteCustomFieldValue {
1748 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1750 unless ( $cf->Id ) {
1751 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1753 my ( $val, $msg ) = $cf->DeleteValueForObject(
1755 Id => $args{'ValueId'},
1756 Content => $args{'Value'},
1761 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
1762 Type => 'CustomField',
1764 OldReference => $val,
1765 ReferenceType => 'RT::ObjectCustomFieldValue',
1767 unless ($TransactionId) {
1768 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1774 "[_1] is no longer a value for custom field [_2]",
1775 $TransactionObj->OldValue, $cf->Name
1782 # {{{ FirstCustomFieldValue
1784 =head2 FirstCustomFieldValue FIELD
1786 Return the content of the first value of CustomField FIELD for this ticket
1787 Takes a field id or name
1791 sub FirstCustomFieldValue {
1794 my $values = $self->CustomFieldValues($field);
1795 if ($values->First) {
1796 return $values->First->Content;
1805 # {{{ CustomFieldValues
1807 =head2 CustomFieldValues FIELD
1809 Return a ObjectCustomFieldValues object of all values of the CustomField whose
1810 id or Name is FIELD for this record.
1812 Returns an RT::ObjectCustomFieldValues object
1816 sub CustomFieldValues {
1821 my $cf = $self->LoadCustomFieldByIdentifier($field);
1823 # we were asked to search on a custom field we couldn't fine
1824 unless ( $cf->id ) {
1825 return RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1827 return ( $cf->ValuesForObject($self) );
1830 # we're not limiting to a specific custom field;
1831 my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1832 $ocfs->LimitToObject($self);
1837 =head2 CustomField IDENTIFER
1839 Find the custom field has id or name IDENTIFIER for this object.
1841 If no valid field is found, returns an empty RT::CustomField object.
1845 sub LoadCustomFieldByIdentifier {
1849 my $cf = RT::CustomField->new($self->CurrentUser);
1851 if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) {
1852 $cf->LoadById( $field->id );
1854 elsif ($field =~ /^\d+$/) {
1855 $cf = RT::CustomField->new($self->CurrentUser);
1859 my $cfs = $self->CustomFields($self->CurrentUser);
1860 $cfs->Limit(FIELD => 'Name', VALUE => $field);
1861 $cf = $cfs->First || RT::CustomField->new($self->CurrentUser);
1876 eval "require RT::Record_Vendor";
1877 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Vendor.pm});
1878 eval "require RT::Record_Local";
1879 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Local.pm});