1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 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
48 RT::Record - Base class for RT record objects
58 ok (require RT::Record);
71 use DBIx::SearchBuilder::Record::Cachable;
74 use vars qw/@ISA $_TABLE_ATTR/;
78 if ($RT::DontCacheSearchBuilderRecords ) {
79 push (@ISA, 'DBIx::SearchBuilder::Record');
81 push (@ISA, 'DBIx::SearchBuilder::Record::Cachable');
89 $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
90 $self->CurrentUser(@_);
99 The primary keys for RT classes is 'id'
112 Return this object's attributes as an RT::Attributes object
119 unless ($self->{'attributes'}) {
120 $self->{'attributes'} = RT::Attributes->new($self->CurrentUser);
121 $self->{'attributes'}->LimitToObject($self);
123 return ($self->{'attributes'});
128 =head2 AddAttribute { Name, Description, Content }
130 Adds a new attribute for this object.
136 my %args = ( Name => undef,
137 Description => undef,
141 my $attr = RT::Attribute->new( $self->CurrentUser );
142 my ( $id, $msg ) = $attr->Create(
144 Name => $args{'Name'},
145 Description => $args{'Description'},
146 Content => $args{'Content'} );
148 $self->Attributes->RedoSearch;
154 =head2 SetAttribute { Name, Description, Content }
156 Like AddAttribute, but replaces all existing attributes with the same Name.
162 my %args = ( Name => undef,
163 Description => undef,
167 my @AttributeObjs = $self->Attributes->Named( $args{'Name'} )
168 or return $self->AddAttribute( %args );
170 my $AttributeObj = pop( @AttributeObjs );
171 $_->Delete foreach @AttributeObjs;
173 $AttributeObj->SetDescription( $args{'Description'} );
174 $AttributeObj->SetContent( $args{'Content'} );
176 $self->Attributes->RedoSearch;
180 =head2 DeleteAttribute NAME
182 Deletes all attributes with the matching name for this object.
186 sub DeleteAttribute {
189 return $self->Attributes->DeleteEntry( Name => $name );
192 =head2 FirstAttribute NAME
194 Returns the value of the first attribute with the matching name
195 for this object, or C<undef> if no such attributes exist.
202 return ($self->Attributes->Named( $name ))[0];
209 return ($RT::Handle);
216 =item Create PARAMHASH
218 Takes a PARAMHASH of Column -> Value pairs.
219 If any Column has a Validate$PARAMNAME subroutine defined and the
220 value provided doesn't pass validation, this routine returns
223 If this object's table has any of the following atetributes defined as
224 'Auto', this routine will automatically fill in their values.
231 foreach my $key ( keys %attribs ) {
232 my $method = "Validate$key";
233 unless ( $self->$method( $attribs{$key} ) ) {
235 return ( 0, $self->loc('Invalid value for [_1]', $key) );
242 my $now = RT::Date->new( $self->CurrentUser );
243 $now->Set( Format => 'unix', Value => time );
244 $attribs{'Created'} = $now->ISO() if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'});
246 if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) {
247 $attribs{'Creator'} = $self->CurrentUser->id || '0';
249 $attribs{'LastUpdated'} = $now->ISO()
250 if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'});
252 $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0'
253 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'});
255 my $id = $self->SUPER::Create(%attribs);
256 if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) {
260 $self->loc( "Internal Error: [_1]", $id->{error_message} ) );
267 # If the object was created in the database,
268 # load it up now, so we're sure we get what the database
269 # has. Arguably, this should not be necessary, but there
270 # isn't much we can do about it.
274 return ( $id, $self->loc('Object could not be created') );
282 if (UNIVERSAL::isa('errno',$id)) {
288 $self->Load($id) if ($id);
293 return ( $id, $self->loc('Object created') );
307 Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the
316 # We don't want to hang onto this
317 delete $self->{'attributes'};
319 # If this database is case sensitive we need to uncase objects for
321 if ( $self->_Handle->CaseSensitive ) {
323 foreach my $key ( keys %hash ) {
325 # If we've been passed an empty value, we can't do the lookup.
326 # We don't need to explicitly downcase integers or an id.
328 || !defined( $hash{$key} )
329 || $hash{$key} =~ /^\d+$/
332 $newhash{$key} = $hash{$key};
336 ($key, $op, $val) = $self->_Handle->_MakeClauseCaseInsensitive($key, '=', $hash{$key});
337 $newhash{$key}->{operator} = $op;
338 $newhash{$key}->{value} = $val;
342 # We've clobbered everything we care about. bash the old hash
343 # and replace it with the new hash
346 $self->SUPER::LoadByCols(%hash);
353 # There is room for optimizations in most of those subs:
359 my $obj = new RT::Date( $self->CurrentUser );
361 $obj->Set( Format => 'sql', Value => $self->LastUpdated );
371 my $obj = new RT::Date( $self->CurrentUser );
373 $obj->Set( Format => 'sql', Value => $self->Created );
382 # TODO: This should be deprecated
386 return ( $self->CreatedObj->AgeAsString() );
391 # {{{ LastUpdatedAsString
393 # TODO this should be deprecated
395 sub LastUpdatedAsString {
397 if ( $self->LastUpdated ) {
398 return ( $self->LastUpdatedObj->AsString() );
408 # {{{ CreatedAsString
410 # TODO This should be deprecated
412 sub CreatedAsString {
414 return ( $self->CreatedObj->AsString() );
419 # {{{ LongSinceUpdateAsString
421 # TODO This should be deprecated
423 sub LongSinceUpdateAsString {
425 if ( $self->LastUpdated ) {
427 return ( $self->LastUpdatedObj->AgeAsString() );
450 #if the user is trying to modify the record
451 # TODO: document _why_ this code is here
453 if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) {
457 $self->_SetLastUpdated();
458 my ( $val, $msg ) = $self->SUPER::_Set(
459 Field => $args{'Field'},
460 Value => $args{'Value'},
461 IsSQL => $args{'IsSQL'}
467 # {{{ sub _SetLastUpdated
469 =head2 _SetLastUpdated
471 This routine updates the LastUpdated and LastUpdatedBy columns of the row in question
472 It takes no options. Arguably, this is a bug
476 sub _SetLastUpdated {
479 my $now = new RT::Date( $self->CurrentUser );
482 if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) {
483 my ( $msg, $val ) = $self->__Set(
484 Field => 'LastUpdated',
488 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) {
489 my ( $msg, $val ) = $self->__Set(
490 Field => 'LastUpdatedBy',
491 Value => $self->CurrentUser->id
502 Returns an RT::User object with the RT account of the creator of this row
508 unless ( exists $self->{'CreatorObj'} ) {
510 $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser );
511 $self->{'CreatorObj'}->Load( $self->Creator );
513 return ( $self->{'CreatorObj'} );
518 # {{{ sub LastUpdatedByObj
520 =head2 LastUpdatedByObj
522 Returns an RT::User object of the last user to touch this object
526 sub LastUpdatedByObj {
528 unless ( exists $self->{LastUpdatedByObj} ) {
529 $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser );
530 $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy );
532 return $self->{'LastUpdatedByObj'};
541 Returns this record's URI
547 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
548 return($uri->URIForObject($self));
557 =head2 SQLType attribute
559 return the SQL type for the attribute 'attribute' as stored in _ClassAccessible
567 return ($self->_Accessible($field, 'type'));
572 require Encode::compat if $] < 5.007001;
581 my %args = ( decode_utf8 => 1,
584 unless (defined $field && $field) {
585 $RT::Logger->error("$self __Value called with undef field");
587 my $value = $self->SUPER::__Value($field);
589 return('') if ( !defined($value) || $value eq '');
591 return Encode::decode_utf8($value) || $value if $args{'decode_utf8'};
595 # Set up defaults for DBIx::SearchBuilder::Record::Cachable
600 'cache_for_sec' => 30,
606 sub _BuildTableAttributes {
610 if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) {
611 $attributes = $self->_CoreAccessible();
612 } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) {
613 $attributes = $self->_ClassAccessible();
617 foreach my $column (%$attributes) {
618 foreach my $attr ( %{ $attributes->{$column} } ) {
619 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
622 if ( UNIVERSAL::can( $self, '_OverlayAccessible' ) ) {
623 $attributes = $self->_OverlayAccessible();
625 foreach my $column (%$attributes) {
626 foreach my $attr ( %{ $attributes->{$column} } ) {
627 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
631 if ( UNIVERSAL::can( $self, '_VendorAccessible' ) ) {
632 $attributes = $self->_VendorAccessible();
634 foreach my $column (%$attributes) {
635 foreach my $attr ( %{ $attributes->{$column} } ) {
636 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
640 if ( UNIVERSAL::can( $self, '_LocalAccessible' ) ) {
641 $attributes = $self->_LocalAccessible();
643 foreach my $column (%$attributes) {
644 foreach my $attr ( %{ $attributes->{$column} } ) {
645 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
653 =head2 _ClassAccessible
655 Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in
656 DBIx::SearchBuilder::Record
660 sub _ClassAccessible {
662 return $_TABLE_ATTR->{ref($self)};
665 =head2 _Accessible COLUMN ATTRIBUTE
667 returns the value of ATTRIBUTE for COLUMN
675 my $attribute = lc(shift);
676 return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column});
677 return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0;
681 =head2 _EncodeLOB BODY MIME_TYPE
683 Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database
690 my $MIMEType = shift;
692 my $ContentEncoding = 'none';
694 #get the max attachment length from RT
695 my $MaxSize = $RT::MaxAttachmentSize;
697 #if the current attachment contains nulls and the
698 #database doesn't support embedded nulls
700 if ( $RT::AlwaysUseBase64 or
701 ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
703 # set a flag telling us to mimencode the attachment
704 $ContentEncoding = 'base64';
706 #cut the max attchment size by 25% (for mime-encoding overhead.
707 $RT::Logger->debug("Max size is $MaxSize\n");
708 $MaxSize = $MaxSize * 3 / 4;
709 # Some databases (postgres) can't handle non-utf8 data
710 } elsif ( !$RT::Handle->BinarySafeBLOBs
711 && $MIMEType !~ /text\/plain/gi
712 && !Encode::is_utf8( $Body, 1 ) ) {
713 $ContentEncoding = 'quoted-printable';
716 #if the attachment is larger than the maximum size
717 if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
719 # if we're supposed to truncate large attachments
720 if ($RT::TruncateLongAttachments) {
722 # truncate the attachment to that length.
723 $Body = substr( $Body, 0, $MaxSize );
727 # elsif we're supposed to drop large attachments on the floor,
728 elsif ($RT::DropLongAttachments) {
730 # drop the attachment on the floor
731 $RT::Logger->info( "$self: Dropped an attachment of size " . length($Body) . "\n" . "It started: " . substr( $Body, 0, 60 ) . "\n" );
732 return ("none", "Large attachment dropped" );
736 # if we need to mimencode the attachment
737 if ( $ContentEncoding eq 'base64' ) {
739 # base64 encode the attachment
740 Encode::_utf8_off($Body);
741 $Body = MIME::Base64::encode_base64($Body);
743 } elsif ($ContentEncoding eq 'quoted-printable') {
744 Encode::_utf8_off($Body);
745 $Body = MIME::QuotedPrint::encode($Body);
749 return ($ContentEncoding, $Body);
755 # A helper table for links mapping to make it easier
756 # to build and parse links between tickets
758 use vars '%LINKDIRMAP';
761 MemberOf => { Base => 'MemberOf',
762 Target => 'HasMember', },
763 RefersTo => { Base => 'RefersTo',
764 Target => 'ReferredToBy', },
765 DependsOn => { Base => 'DependsOn',
766 Target => 'DependedOnBy', },
767 MergedInto => { Base => 'MergedInto',
768 Target => 'MergedInto', },
777 AttributesRef => undef,
778 AttributePrefix => undef,
782 my $attributes = $args{'AttributesRef'};
783 my $ARGSRef = $args{'ARGSRef'};
786 foreach my $attribute (@$attributes) {
788 if ( defined $ARGSRef->{$attribute} ) {
789 $value = $ARGSRef->{$attribute};
792 defined( $args{'AttributePrefix'} )
794 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
798 $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
805 $value =~ s/\r\n/\n/gs;
808 # If Queue is 'General', we want to resolve the queue name for
811 # This is in an eval block because $object might not exist.
812 # and might not have a Name method. But "can" won't find autoloaded
813 # items. If it fails, we don't care
815 my $object = $attribute . "Obj";
816 next if ($self->$object->Name eq $value);
818 next if ( $value eq $self->$attribute() );
819 my $method = "Set$attribute";
820 my ( $code, $msg ) = $self->$method($value);
822 my ($prefix) = ref($self) =~ /RT::(\w+)/;
824 $self->loc( "$prefix [_1]", $self->id ) . ': '
825 . $self->loc($attribute) . ': '
826 . $self->CurrentUser->loc_fuzzy($msg);
829 "[_1] could not be set to [_2].", # loc
830 "That is already the current value", # loc
831 "No value sent to _Set!\n", # loc
832 "Illegal value for [_1]", # loc
833 "The new value has been set.", # loc
834 "No column specified", # loc
835 "Immutable field", # loc
836 "Nonexistant field?", # loc
837 "Invalid data", # loc
838 "Couldn't find row", # loc
839 "Missing a primary key?: [_1]", # loc
840 "Found Object", # loc
848 # {{{ Routines dealing with Links between tickets
850 # {{{ Link Collections
856 This returns an RT::Links object which references all the tickets
857 which are 'MembersOf' this ticket
863 return ( $self->_Links( 'Target', 'MemberOf' ) );
872 This returns an RT::Links object which references all the tickets that this
873 ticket is a 'MemberOf'
879 return ( $self->_Links( 'Base', 'MemberOf' ) );
888 This returns an RT::Links object which shows all references for which this ticket is a base
894 return ( $self->_Links( 'Base', 'RefersTo' ) );
903 This returns an RT::Links object which shows all references for which this ticket is a target
909 return ( $self->_Links( 'Target', 'RefersTo' ) );
918 This returns an RT::Links object which references all the tickets that depend on this one
924 return ( $self->_Links( 'Target', 'DependsOn' ) );
931 =head2 HasUnresolvedDependencies
933 Takes a paramhash of Type (default to '__any'). Returns true if
934 $self->UnresolvedDependencies returns an object with one or more members
935 of that type. Returns false otherwise
940 my $t1 = RT::Ticket->new($RT::SystemUser);
941 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
942 ok($id, "Created dep test 1 - $msg");
944 my $t2 = RT::Ticket->new($RT::SystemUser);
945 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
946 ok($id2, "Created dep test 2 - $msg2");
947 my $t3 = RT::Ticket->new($RT::SystemUser);
948 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
949 ok($id3, "Created dep test 3 - $msg3");
950 my ($addid, $addmsg);
951 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t2->id));
952 ok ($addid, $addmsg);
953 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t3->id));
955 ok ($addid, $addmsg);
956 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
957 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
958 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
959 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
962 my ($rid, $rmsg)= $t1->Resolve();
965 ($rid, $rmsg)= $t1->Resolve();
968 ($rid, $rmsg)= $t1->Resolve();
976 sub HasUnresolvedDependencies {
983 my $deps = $self->UnresolvedDependencies;
986 $deps->Limit( FIELD => 'Type',
988 VALUE => $args{Type});
994 if ($deps->Count > 0) {
1003 # {{{ UnresolvedDependencies
1005 =head2 UnresolvedDependencies
1007 Returns an RT::Tickets object of tickets which this ticket depends on
1008 and which have a status of new, open or stalled. (That list comes from
1009 RT::Queue->ActiveStatusArray
1014 sub UnresolvedDependencies {
1016 my $deps = RT::Tickets->new($self->CurrentUser);
1018 my @live_statuses = RT::Queue->ActiveStatusArray();
1019 foreach my $status (@live_statuses) {
1020 $deps->LimitStatus(VALUE => $status);
1022 $deps->LimitDependedOnBy($self->Id);
1030 # {{{ AllDependedOnBy
1032 =head2 AllDependedOnBy
1034 Returns an array of RT::Ticket objects which (directly or indirectly)
1035 depends on this ticket; takes an optional 'Type' argument in the param
1036 hash, which will limit returned tickets to that type, as well as cause
1037 tickets with that type to serve as 'leaf' nodes that stops the recursive
1042 sub AllDependedOnBy {
1044 my $dep = $self->DependedOnBy;
1052 while (my $link = $dep->Next()) {
1053 next unless ($link->BaseURI->IsLocal());
1054 next if $args{_found}{$link->BaseObj->Id};
1057 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1058 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1060 elsif ($link->BaseObj->Type eq $args{Type}) {
1061 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1064 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1069 return map { $args{_found}{$_} } sort keys %{$args{_found}};
1082 This returns an RT::Links object which references all the tickets that this ticket depends on
1088 return ( $self->_Links( 'Base', 'DependsOn' ) );
1101 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1104 my $type = shift || "";
1106 unless ( $self->{"$field$type"} ) {
1107 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
1108 # at least to myself
1109 $self->{"$field$type"}->Limit( FIELD => $field,
1110 VALUE => $self->URI,
1111 ENTRYAGGREGATOR => 'OR' );
1112 $self->{"$field$type"}->Limit( FIELD => 'Type',
1116 return ( $self->{"$field$type"} );
1127 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
1135 my %args = ( Target => '',
1142 # Remote_link is the URI of the object that is not this ticket
1146 if ( $args{'Base'} and $args{'Target'} ) {
1148 "$self tried to delete a link. both base and target were specified\n" );
1149 return ( 0, $self->loc("Can't specifiy both base and target") );
1151 elsif ( $args{'Base'} ) {
1152 $args{'Target'} = $self->URI();
1153 my $class = ref($self);
1154 $remote_link = $args{'Base'};
1155 $direction = 'Target';
1157 elsif ( $args{'Target'} ) {
1158 $args{'Base'} = $self->URI();
1159 my $class = ref($self);
1160 $remote_link = $args{'Target'};
1161 $direction = 'Base';
1164 return ( 0, $self->loc('Either base or target must be specified') );
1167 # {{{ Check if the link already exists - we don't want duplicates
1169 my $old_link = RT::Link->new( $self->CurrentUser );
1170 $old_link->LoadByParams( Base => $args{'Base'},
1171 Type => $args{'Type'},
1172 Target => $args{'Target'} );
1173 if ( $old_link->Id ) {
1174 $RT::Logger->debug("$self Somebody tried to duplicate a link");
1175 return ( $old_link->id, $self->loc("Link already exists"), 0 );
1181 # Storing the link in the DB.
1182 my $link = RT::Link->new( $self->CurrentUser );
1183 my ($linkid, $linkmsg) = $link->Create( Target => $args{Target},
1184 Base => $args{Base},
1185 Type => $args{Type} );
1188 $RT::Logger->error("Link could not be created: ".$linkmsg);
1189 return ( 0, $self->loc("Link could not be created") );
1193 "Record $args{'Base'} $args{Type} record $args{'Target'}.";
1195 return ( 1, $self->loc( "Link created ([_1])", $TransString ) );
1200 # {{{ sub _DeleteLink
1204 Delete a link. takes a paramhash of Base, Target and Type.
1205 Either Base or Target must be null. The null value will
1206 be replaced with this ticket\'s id
1219 #we want one of base and target. we don't care which
1220 #but we only want _one_
1225 if ( $args{'Base'} and $args{'Target'} ) {
1226 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
1227 return ( 0, $self->loc("Can't specifiy both base and target") );
1229 elsif ( $args{'Base'} ) {
1230 $args{'Target'} = $self->URI();
1231 $remote_link = $args{'Base'};
1232 $direction = 'Target';
1234 elsif ( $args{'Target'} ) {
1235 $args{'Base'} = $self->URI();
1236 $remote_link = $args{'Target'};
1240 $RT::Logger->debug("$self: Base or Target must be specified\n");
1241 return ( 0, $self->loc('Either base or target must be specified') );
1244 my $link = new RT::Link( $self->CurrentUser );
1245 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
1248 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
1252 my $linkid = $link->id;
1255 my $TransString = "Record $args{'Base'} no longer $args{Type} record $args{'Target'}.";
1256 return ( 1, $self->loc("Link deleted ([_1])", $TransString));
1259 #if it's not a link we can find
1261 $RT::Logger->debug("Couldn't find that link\n");
1262 return ( 0, $self->loc("Link not found") );
1268 eval "require RT::Record_Vendor";
1269 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Vendor.pm});
1270 eval "require RT::Record_Local";
1271 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Local.pm});