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)};
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 " . length($Body) . "\n" . "It started: " . substr( $Body, 0, 60 ) . "\n" );
821 return ("none", "Large attachment dropped" );
825 # if we need to mimencode the attachment
826 if ( $ContentEncoding eq 'base64' ) {
828 # base64 encode the attachment
829 Encode::_utf8_off($Body);
830 $Body = MIME::Base64::encode_base64($Body);
832 } elsif ($ContentEncoding eq 'quoted-printable') {
833 Encode::_utf8_off($Body);
834 $Body = MIME::QuotedPrint::encode($Body);
838 return ($ContentEncoding, $Body);
844 my $ContentType = shift;
845 my $ContentEncoding = shift;
848 if ( $ContentEncoding eq 'base64' ) {
849 $Content = MIME::Base64::decode_base64($Content);
851 elsif ( $ContentEncoding eq 'quoted-printable' ) {
852 $Content = MIME::QuotedPrint::decode($Content);
854 elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) {
855 return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) );
857 if ( $ContentType eq 'text/plain' ) {
858 $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content);
864 # A helper table for links mapping to make it easier
865 # to build and parse links between tickets
867 use vars '%LINKDIRMAP';
870 MemberOf => { Base => 'MemberOf',
871 Target => 'HasMember', },
872 RefersTo => { Base => 'RefersTo',
873 Target => 'ReferredToBy', },
874 DependsOn => { Base => 'DependsOn',
875 Target => 'DependedOnBy', },
876 MergedInto => { Base => 'MergedInto',
877 Target => 'MergedInto', },
886 AttributesRef => undef,
887 AttributePrefix => undef,
891 my $attributes = $args{'AttributesRef'};
892 my $ARGSRef = $args{'ARGSRef'};
895 foreach my $attribute (@$attributes) {
897 if ( defined $ARGSRef->{$attribute} ) {
898 $value = $ARGSRef->{$attribute};
901 defined( $args{'AttributePrefix'} )
903 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
906 $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
913 $value =~ s/\r\n/\n/gs;
916 # If Queue is 'General', we want to resolve the queue name for
919 # This is in an eval block because $object might not exist.
920 # and might not have a Name method. But "can" won't find autoloaded
921 # items. If it fails, we don't care
923 my $object = $attribute . "Obj";
924 next if ($self->$object->Name eq $value);
926 next if ( $value eq $self->$attribute() );
927 my $method = "Set$attribute";
928 my ( $code, $msg ) = $self->$method($value);
929 my ($prefix) = ref($self) =~ /RT::(\w+)/;
931 # Default to $id, but use name if we can get it.
932 my $label = $self->id;
933 $label = $self->Name if (UNIVERSAL::can($self,'Name'));
934 push @results, $self->loc( "$prefix [_1]", $label ) . ': '. $msg;
938 "[_1] could not be set to [_2].", # loc
939 "That is already the current value", # loc
940 "No value sent to _Set!\n", # loc
941 "Illegal value for [_1]", # loc
942 "The new value has been set.", # loc
943 "No column specified", # loc
944 "Immutable field", # loc
945 "Nonexistant field?", # loc
946 "Invalid data", # loc
947 "Couldn't find row", # loc
948 "Missing a primary key?: [_1]", # loc
949 "Found Object", # loc
958 # {{{ Routines dealing with Links
960 # {{{ Link Collections
966 This returns an RT::Links object which references all the tickets
967 which are 'MembersOf' this ticket
973 return ( $self->_Links( 'Target', 'MemberOf' ) );
982 This returns an RT::Links object which references all the tickets that this
983 ticket is a 'MemberOf'
989 return ( $self->_Links( 'Base', 'MemberOf' ) );
998 This returns an RT::Links object which shows all references for which this ticket is a base
1004 return ( $self->_Links( 'Base', 'RefersTo' ) );
1013 This returns an RT::Links object which shows all references for which this ticket is a target
1019 return ( $self->_Links( 'Target', 'RefersTo' ) );
1028 This returns an RT::Links object which references all the tickets that depend on this one
1034 return ( $self->_Links( 'Target', 'DependsOn' ) );
1041 =head2 HasUnresolvedDependencies
1043 Takes a paramhash of Type (default to '__any'). Returns true if
1044 $self->UnresolvedDependencies returns an object with one or more members
1045 of that type. Returns false otherwise
1050 my $t1 = RT::Ticket->new($RT::SystemUser);
1051 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
1052 ok($id, "Created dep test 1 - $msg");
1054 my $t2 = RT::Ticket->new($RT::SystemUser);
1055 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
1056 ok($id2, "Created dep test 2 - $msg2");
1057 my $t3 = RT::Ticket->new($RT::SystemUser);
1058 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
1059 ok($id3, "Created dep test 3 - $msg3");
1060 my ($addid, $addmsg);
1061 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t2->id));
1062 ok ($addid, $addmsg);
1063 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t3->id));
1065 ok ($addid, $addmsg);
1066 my $link = RT::Link->new($RT::SystemUser);
1067 my ($rv, $msg) = $link->Load($addid);
1069 ok ($link->LocalTarget == $t3->id, "Link LocalTarget is correct");
1070 ok ($link->LocalBase == $t1->id, "Link LocalBase is correct");
1072 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
1073 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
1074 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
1075 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
1078 my ($rid, $rmsg)= $t1->Resolve();
1080 my ($rid2, $rmsg2) = $t2->Resolve();
1082 ($rid, $rmsg)= $t1->Resolve();
1084 my ($rid3,$rmsg3) = $t3->Resolve;
1086 ($rid, $rmsg)= $t1->Resolve();
1094 sub HasUnresolvedDependencies {
1101 my $deps = $self->UnresolvedDependencies;
1104 $deps->Limit( FIELD => 'Type',
1106 VALUE => $args{Type});
1112 if ($deps->Count > 0) {
1121 # {{{ UnresolvedDependencies
1123 =head2 UnresolvedDependencies
1125 Returns an RT::Tickets object of tickets which this ticket depends on
1126 and which have a status of new, open or stalled. (That list comes from
1127 RT::Queue->ActiveStatusArray
1132 sub UnresolvedDependencies {
1134 my $deps = RT::Tickets->new($self->CurrentUser);
1136 my @live_statuses = RT::Queue->ActiveStatusArray();
1137 foreach my $status (@live_statuses) {
1138 $deps->LimitStatus(VALUE => $status);
1140 $deps->LimitDependedOnBy($self->Id);
1148 # {{{ AllDependedOnBy
1150 =head2 AllDependedOnBy
1152 Returns an array of RT::Ticket objects which (directly or indirectly)
1153 depends on this ticket; takes an optional 'Type' argument in the param
1154 hash, which will limit returned tickets to that type, as well as cause
1155 tickets with that type to serve as 'leaf' nodes that stops the recursive
1160 sub AllDependedOnBy {
1162 my $dep = $self->DependedOnBy;
1170 while (my $link = $dep->Next()) {
1171 next unless ($link->BaseURI->IsLocal());
1172 next if $args{_found}{$link->BaseObj->Id};
1175 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1176 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1178 elsif ($link->BaseObj->Type eq $args{Type}) {
1179 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1182 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1187 return map { $args{_found}{$_} } sort keys %{$args{_found}};
1200 This returns an RT::Links object which references all the tickets that this ticket depends on
1206 return ( $self->_Links( 'Base', 'DependsOn' ) );
1216 =head2 Links DIRECTION TYPE
1218 return links to/from this object.
1227 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1230 my $type = shift || "";
1232 unless ( $self->{"$field$type"} ) {
1233 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
1234 # at least to myself
1235 $self->{"$field$type"}->Limit( FIELD => $field,
1236 VALUE => $self->URI,
1237 ENTRYAGGREGATOR => 'OR' );
1238 $self->{"$field$type"}->Limit( FIELD => 'Type',
1242 return ( $self->{"$field$type"} );
1253 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
1261 my %args = ( Target => '',
1268 # Remote_link is the URI of the object that is not this ticket
1272 if ( $args{'Base'} and $args{'Target'} ) {
1273 $RT::Logger->debug( "$self tried to create a link. both base and target were specified\n" );
1274 return ( 0, $self->loc("Can't specifiy both base and target") );
1276 elsif ( $args{'Base'} ) {
1277 $args{'Target'} = $self->URI();
1278 my $class = ref($self);
1279 $remote_link = $args{'Base'};
1280 $direction = 'Target';
1282 elsif ( $args{'Target'} ) {
1283 $args{'Base'} = $self->URI();
1284 my $class = ref($self);
1285 $remote_link = $args{'Target'};
1286 $direction = 'Base';
1289 return ( 0, $self->loc('Either base or target must be specified') );
1292 # {{{ Check if the link already exists - we don't want duplicates
1294 my $old_link = RT::Link->new( $self->CurrentUser );
1295 $old_link->LoadByParams( Base => $args{'Base'},
1296 Type => $args{'Type'},
1297 Target => $args{'Target'} );
1298 if ( $old_link->Id ) {
1299 $RT::Logger->debug("$self Somebody tried to duplicate a link");
1300 return ( $old_link->id, $self->loc("Link already exists") );
1306 # Storing the link in the DB.
1307 my $link = RT::Link->new( $self->CurrentUser );
1308 my ($linkid, $linkmsg) = $link->Create( Target => $args{Target},
1309 Base => $args{Base},
1310 Type => $args{Type} );
1313 $RT::Logger->error("Link could not be created: ".$linkmsg);
1314 return ( 0, $self->loc("Link could not be created") );
1318 "Record $args{'Base'} $args{Type} record $args{'Target'}.";
1320 return ( $linkid, $self->loc( "Link created ([_1])", $TransString ) );
1325 # {{{ sub _DeleteLink
1329 Delete a link. takes a paramhash of Base, Target and Type.
1330 Either Base or Target must be null. The null value will
1331 be replaced with this ticket\'s id
1344 #we want one of base and target. we don't care which
1345 #but we only want _one_
1350 if ( $args{'Base'} and $args{'Target'} ) {
1351 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
1352 return ( 0, $self->loc("Can't specifiy both base and target") );
1354 elsif ( $args{'Base'} ) {
1355 $args{'Target'} = $self->URI();
1356 $remote_link = $args{'Base'};
1357 $direction = 'Target';
1359 elsif ( $args{'Target'} ) {
1360 $args{'Base'} = $self->URI();
1361 $remote_link = $args{'Target'};
1365 $RT::Logger->debug("$self: Base or Target must be specified\n");
1366 return ( 0, $self->loc('Either base or target must be specified') );
1369 my $link = new RT::Link( $self->CurrentUser );
1370 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
1373 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
1377 my $linkid = $link->id;
1380 my $TransString = "Record $args{'Base'} no longer $args{Type} record $args{'Target'}.";
1381 return ( 1, $self->loc("Link deleted ([_1])", $TransString));
1384 #if it's not a link we can find
1386 $RT::Logger->debug("Couldn't find that link\n");
1387 return ( 0, $self->loc("Link not found") );
1395 # {{{ Routines dealing with transactions
1397 # {{{ sub _NewTransaction
1399 =head2 _NewTransaction PARAMHASH
1401 Private function to create a new RT::Transaction object for this ticket update
1405 sub _NewTransaction {
1412 OldReference => undef,
1413 NewReference => undef,
1414 ReferenceType => undef,
1418 ActivateScrips => 1,
1423 my $old_ref = $args{'OldReference'};
1424 my $new_ref = $args{'NewReference'};
1425 my $ref_type = $args{'ReferenceType'};
1426 if ($old_ref or $new_ref) {
1427 $ref_type ||= ref($old_ref) || ref($new_ref);
1429 $RT::Logger->error("Reference type not specified for transaction");
1432 $old_ref = $old_ref->Id if ref($old_ref);
1433 $new_ref = $new_ref->Id if ref($new_ref);
1436 require RT::Transaction;
1437 my $trans = new RT::Transaction( $self->CurrentUser );
1438 my ( $transaction, $msg ) = $trans->Create(
1439 ObjectId => $self->Id,
1440 ObjectType => ref($self),
1441 TimeTaken => $args{'TimeTaken'},
1442 Type => $args{'Type'},
1443 Data => $args{'Data'},
1444 Field => $args{'Field'},
1445 NewValue => $args{'NewValue'},
1446 OldValue => $args{'OldValue'},
1447 NewReference => $new_ref,
1448 OldReference => $old_ref,
1449 ReferenceType => $ref_type,
1450 MIMEObj => $args{'MIMEObj'},
1451 ActivateScrips => $args{'ActivateScrips'},
1452 CommitScrips => $args{'CommitScrips'},
1455 # Rationalize the object since we may have done things to it during the caching.
1456 $self->Load($self->Id);
1458 $RT::Logger->warning($msg) unless $transaction;
1460 $self->_SetLastUpdated;
1462 if ( defined $args{'TimeTaken'} ) {
1463 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
1465 if ( $RT::UseTransactionBatch and $transaction ) {
1466 push @{$self->{_TransactionBatch}}, $trans;
1468 return ( $transaction, $msg, $trans );
1473 # {{{ sub Transactions
1477 Returns an RT::Transactions object of all transactions on this record object
1484 use RT::Transactions;
1485 my $transactions = RT::Transactions->new( $self->CurrentUser );
1487 #If the user has no rights, return an empty object
1488 $transactions->Limit(
1489 FIELD => 'ObjectId',
1492 $transactions->Limit(
1493 FIELD => 'ObjectType',
1494 VALUE => ref($self),
1497 return ($transactions);
1503 # {{{ Routines dealing with custom fields
1507 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1509 # XXX handle multiple types properly
1510 $cfs->LimitToLookupType( $self->CustomFieldLookupType );
1511 $cfs->LimitToGlobalOrObjectId(
1512 $self->_LookupId( $self->CustomFieldLookupType ) );
1517 # TODO: This _only_ works for RT::Class classes. it doesn't work, for example, for RT::FM classes.
1522 my @classes = ($lookup =~ /RT::(\w+)-/g);
1525 foreach my $class (reverse @classes) {
1526 my $method = "${class}Obj";
1527 $object = $object->$method;
1534 =head2 CustomFieldLookupType
1536 Returns the path RT uses to figure out which custom fields apply to this object.
1540 sub CustomFieldLookupType {
1545 #TODO Deprecated API. Destroy in 3.6
1548 $RT::Logger->warning("_LookupTypes call is deprecated at (". join(":",caller)."). Replace with CustomFieldLookupType");
1550 return($self->CustomFieldLookupType);
1554 # {{{ AddCustomFieldValue
1556 =head2 AddCustomFieldValue { Field => FIELD, Value => VALUE }
1558 VALUE should be a string.
1559 FIELD can be a CustomField object OR a CustomField ID.
1562 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
1563 deletes the old value.
1564 If VALUE is not a valid value for the custom field, returns
1565 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1569 sub AddCustomFieldValue {
1571 $self->_AddCustomFieldValue(@_);
1574 sub _AddCustomFieldValue {
1579 RecordTransaction => 1,
1583 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1585 unless ( $cf->Id ) {
1586 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1589 my $OCFs = $self->CustomFields;
1590 $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id );
1591 unless ( $OCFs->Count ) {
1595 "Custom field [_1] does not apply to this object",
1600 # Load up a ObjectCustomFieldValues object for this custom field and this ticket
1601 my $values = $cf->ValuesForObject($self);
1603 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
1604 return ( 0, $self->loc("Invalid value for custom field") );
1607 # If the custom field only accepts a certain # of values, delete the existing
1608 # value and record a "changed from foo to bar" transaction
1609 unless ( $cf->UnlimitedValues) {
1611 # We need to whack any old values here. In most cases, the custom field should
1612 # only have one value to delete. In the pathalogical case, this custom field
1613 # used to be a multiple and we have many values to whack....
1614 my $cf_values = $values->Count;
1616 if ( $cf_values > $cf->MaxValues ) {
1617 my $i = 0; #We want to delete all but the max we can currently have , so we can then
1618 # execute the same code to "change" the value from old to new
1619 while ( my $value = $values->Next ) {
1621 if ( $i < $cf_values ) {
1622 my ( $val, $msg ) = $cf->DeleteValueForObject(
1624 Content => $value->Content
1629 my ( $TransactionId, $Msg, $TransactionObj ) =
1630 $self->_NewTransaction(
1631 Type => 'CustomField',
1633 OldReference => $value,
1639 my ( $old_value, $old_content );
1640 if ( $old_value = $cf->ValuesForObject($self)->First ) {
1641 $old_content = $old_value->Content();
1642 return (1) if( $old_content eq $args{'Value'} && $old_value->LargeContent eq $args{'LargeContent'});;
1645 my ( $new_value_id, $value_msg ) = $cf->AddValueForObject(
1647 Content => $args{'Value'},
1648 LargeContent => $args{'LargeContent'},
1649 ContentType => $args{'ContentType'},
1652 unless ($new_value_id) {
1653 return ( 0, $self->loc( "Could not add new custom field value. [_1] ",, $value_msg));
1656 my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1657 $new_value->Load($new_value_id);
1659 # now that adding the new value was successful, delete the old one
1661 my ( $val, $msg ) = $old_value->Delete();
1667 if ( $args{'RecordTransaction'} ) {
1668 my ( $TransactionId, $Msg, $TransactionObj ) =
1669 $self->_NewTransaction(
1670 Type => 'CustomField',
1672 OldReference => $old_value,
1673 NewReference => $new_value,
1677 if ( $old_value eq '' ) {
1678 return ( 1, $self->loc( "[_1] [_2] added", $cf->Name, $new_value->Content ));
1680 elsif ( $new_value->Content eq '' ) {
1682 $self->loc( "[_1] [_2] deleted", $cf->Name, $old_value->Content ) );
1685 return ( 1, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content, $new_value->Content));
1690 # otherwise, just add a new value and record "new value added"
1692 my ($new_value_id) = $cf->AddValueForObject(
1694 Content => $args{'Value'},
1695 LargeContent => $args{'LargeContent'},
1696 ContentType => $args{'ContentType'},
1699 unless ($new_value_id) {
1700 return ( 0, $self->loc("Could not add new custom field value. ") );
1702 if ( $args{'RecordTransaction'} ) {
1703 my ( $TransactionId, $Msg, $TransactionObj ) =
1704 $self->_NewTransaction(
1705 Type => 'CustomField',
1707 NewReference => $new_value_id,
1708 ReferenceType => 'RT::ObjectCustomFieldValue',
1710 unless ($TransactionId) {
1712 $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1715 return ( 1, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name));
1722 # {{{ DeleteCustomFieldValue
1724 =head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
1726 Deletes VALUE as a value of CustomField FIELD.
1728 VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue.
1730 If VALUE is not a valid value for the custom field, returns
1731 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1735 sub DeleteCustomFieldValue {
1744 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1746 unless ( $cf->Id ) {
1747 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1749 my ( $val, $msg ) = $cf->DeleteValueForObject(
1751 Id => $args{'ValueId'},
1752 Content => $args{'Value'},
1757 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
1758 Type => 'CustomField',
1760 OldReference => $val,
1761 ReferenceType => 'RT::ObjectCustomFieldValue',
1763 unless ($TransactionId) {
1764 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1770 "[_1] is no longer a value for custom field [_2]",
1771 $TransactionObj->OldValue, $cf->Name
1778 # {{{ FirstCustomFieldValue
1780 =head2 FirstCustomFieldValue FIELD
1782 Return the content of the first value of CustomField FIELD for this ticket
1783 Takes a field id or name
1787 sub FirstCustomFieldValue {
1790 my $values = $self->CustomFieldValues($field);
1791 if ($values->First) {
1792 return $values->First->Content;
1801 # {{{ CustomFieldValues
1803 =head2 CustomFieldValues FIELD
1805 Return a ObjectCustomFieldValues object of all values of the CustomField whose
1806 id or Name is FIELD for this record.
1808 Returns an RT::ObjectCustomFieldValues object
1812 sub CustomFieldValues {
1817 my $cf = $self->LoadCustomFieldByIdentifier($field);
1819 # we were asked to search on a custom field we couldn't fine
1820 unless ( $cf->id ) {
1821 return RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1823 return ( $cf->ValuesForObject($self) );
1826 # we're not limiting to a specific custom field;
1827 my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1828 $ocfs->LimitToObject($self);
1833 =head2 CustomField IDENTIFER
1835 Find the custom field has id or name IDENTIFIER for this object.
1837 If no valid field is found, returns an empty RT::CustomField object.
1841 sub LoadCustomFieldByIdentifier {
1845 my $cf = RT::CustomField->new($self->CurrentUser);
1847 if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) {
1848 $cf->LoadById( $field->id );
1850 elsif ($field =~ /^\d+$/) {
1851 $cf = RT::CustomField->new($self->CurrentUser);
1855 my $cfs = $self->CustomFields($self->CurrentUser);
1856 $cfs->Limit(FIELD => 'Name', VALUE => $field);
1857 $cf = $cfs->First || RT::CustomField->new($self->CurrentUser);
1872 eval "require RT::Record_Vendor";
1873 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Vendor.pm});
1874 eval "require RT::Record_Local";
1875 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Local.pm});