1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 RT::Record - Base class for RT record objects
75 our $_TABLE_ATTR = { };
76 use base RT->Config->Get('RecordBaseClass');
82 $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
83 $self->CurrentUser(@_);
90 The primary keys for RT classes is 'id'
94 sub _PrimaryKeys { return ['id'] }
95 # short circuit many, many thousands of calls from searchbuilder
96 sub _PrimaryKey { 'id' }
100 Override L<DBIx::SearchBuilder/Id> to avoid a few lookups RT doesn't do
101 on a very common codepath
103 C<id> is an alias to C<Id> and is the preferred way to call this method.
108 return shift->{'values'}->{id};
115 Delete this record object from the database.
121 my ($rv) = $self->SUPER::Delete;
123 return ($rv, $self->loc("Object deleted"));
126 return(0, $self->loc("Object could not be deleted"))
132 Returns a string which is this object's type. The type is the class,
133 without the "RT::" prefix.
140 if (ref($self) =~ /^.*::(\w+)$/) {
141 return $self->loc($1);
143 return $self->loc(ref($self));
149 Return this object's attributes as an RT::Attributes object
155 unless ($self->{'attributes'}) {
156 $self->{'attributes'} = RT::Attributes->new($self->CurrentUser);
157 $self->{'attributes'}->LimitToObject($self);
158 $self->{'attributes'}->OrderByCols({FIELD => 'id'});
160 return ($self->{'attributes'});
164 =head2 AddAttribute { Name, Description, Content }
166 Adds a new attribute for this object.
172 my %args = ( Name => undef,
173 Description => undef,
177 my $attr = RT::Attribute->new( $self->CurrentUser );
178 my ( $id, $msg ) = $attr->Create(
180 Name => $args{'Name'},
181 Description => $args{'Description'},
182 Content => $args{'Content'} );
185 # XXX TODO: Why won't RedoSearch work here?
186 $self->Attributes->_DoSearch;
192 =head2 SetAttribute { Name, Description, Content }
194 Like AddAttribute, but replaces all existing attributes with the same Name.
200 my %args = ( Name => undef,
201 Description => undef,
205 my @AttributeObjs = $self->Attributes->Named( $args{'Name'} )
206 or return $self->AddAttribute( %args );
208 my $AttributeObj = pop( @AttributeObjs );
209 $_->Delete foreach @AttributeObjs;
211 $AttributeObj->SetDescription( $args{'Description'} );
212 $AttributeObj->SetContent( $args{'Content'} );
214 $self->Attributes->RedoSearch;
218 =head2 DeleteAttribute NAME
220 Deletes all attributes with the matching name for this object.
224 sub DeleteAttribute {
227 my ($val,$msg) = $self->Attributes->DeleteEntry( Name => $name );
228 $self->ClearAttributes;
232 =head2 FirstAttribute NAME
234 Returns the first attribute with the matching name for this object (as an
235 L<RT::Attribute> object), or C<undef> if no such attributes exist.
236 If there is more than one attribute with the matching name on the
237 object, the first value that was set is returned.
244 return ($self->Attributes->Named( $name ))[0];
248 sub ClearAttributes {
250 delete $self->{'attributes'};
254 sub _Handle { return $RT::Handle }
258 =head2 Create PARAMHASH
260 Takes a PARAMHASH of Column -> Value pairs.
261 If any Column has a Validate$PARAMNAME subroutine defined and the
262 value provided doesn't pass validation, this routine returns
265 If this object's table has any of the following atetributes defined as
266 'Auto', this routine will automatically fill in their values.
285 foreach my $key ( keys %attribs ) {
286 if (my $method = $self->can("Validate$key")) {
287 if (! $method->( $self, $attribs{$key} ) ) {
289 return ( 0, $self->loc('Invalid value for [_1]', $key) );
300 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) = gmtime();
303 sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $mday, $hour, $min, $sec);
305 $attribs{'Created'} = $now_iso if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'});
307 if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) {
308 $attribs{'Creator'} = $self->CurrentUser->id || '0';
310 $attribs{'LastUpdated'} = $now_iso
311 if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'});
313 $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0'
314 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'});
316 my $id = $self->SUPER::Create(%attribs);
317 if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) {
321 $self->loc( "Internal Error: [_1]", $id->{error_message} ) );
328 # If the object was created in the database,
329 # load it up now, so we're sure we get what the database
330 # has. Arguably, this should not be necessary, but there
331 # isn't much we can do about it.
335 return ( $id, $self->loc('Object could not be created') );
343 if (UNIVERSAL::isa('errno',$id)) {
347 $self->Load($id) if ($id);
352 return ( $id, $self->loc('Object created') );
364 Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the
372 # We don't want to hang onto this
373 $self->ClearAttributes;
375 return $self->SUPER::LoadByCols( @_ ) unless $self->_Handle->CaseSensitive;
377 # If this database is case sensitive we need to uncase objects for
380 foreach my $key ( keys %hash ) {
382 # If we've been passed an empty value, we can't do the lookup.
383 # We don't need to explicitly downcase integers or an id.
384 if ( $key ne 'id' && defined $hash{ $key } && $hash{ $key } !~ /^\d+$/ ) {
385 my ($op, $val, $func);
386 ($key, $op, $val, $func) =
387 $self->_Handle->_MakeClauseCaseInsensitive( $key, '=', delete $hash{ $key } );
388 $hash{$key}->{operator} = $op;
389 $hash{$key}->{value} = $val;
390 $hash{$key}->{function} = $func;
393 return $self->SUPER::LoadByCols( %hash );
398 # There is room for optimizations in most of those subs:
403 my $obj = RT::Date->new( $self->CurrentUser );
405 $obj->Set( Format => 'sql', Value => $self->LastUpdated );
413 my $obj = RT::Date->new( $self->CurrentUser );
415 $obj->Set( Format => 'sql', Value => $self->Created );
422 # TODO: This should be deprecated
426 return ( $self->CreatedObj->AgeAsString() );
431 # TODO this should be deprecated
433 sub LastUpdatedAsString {
435 if ( $self->LastUpdated ) {
436 return ( $self->LastUpdatedObj->AsString() );
446 # TODO This should be deprecated
448 sub CreatedAsString {
450 return ( $self->CreatedObj->AsString() );
455 # TODO This should be deprecated
457 sub LongSinceUpdateAsString {
459 if ( $self->LastUpdated ) {
461 return ( $self->LastUpdatedObj->AgeAsString() );
482 #if the user is trying to modify the record
483 # TODO: document _why_ this code is here
485 if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) {
489 my $old_val = $self->__Value($args{'Field'});
490 $self->_SetLastUpdated();
491 my $ret = $self->SUPER::_Set(
492 Field => $args{'Field'},
493 Value => $args{'Value'},
494 IsSQL => $args{'IsSQL'}
496 my ($status, $msg) = $ret->as_array();
498 # @values has two values, a status code and a message.
500 # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool
501 # we want to change the standard "success" message
503 if ($self->SQLType( $args{'Field'}) =~ /text/) {
506 $self->loc( $args{'Field'} ),
510 "[_1] changed from [_2] to [_3]",
511 $self->loc( $args{'Field'} ),
512 ( $old_val ? '"' . $old_val . '"' : $self->loc("(no value)") ),
513 '"' . $self->__Value( $args{'Field'}) . '"',
517 $msg = $self->CurrentUser->loc_fuzzy($msg);
520 return wantarray ? ($status, $msg) : $ret;
525 =head2 _SetLastUpdated
527 This routine updates the LastUpdated and LastUpdatedBy columns of the row in question
528 It takes no options. Arguably, this is a bug
532 sub _SetLastUpdated {
535 my $now = RT::Date->new( $self->CurrentUser );
538 if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) {
539 my ( $msg, $val ) = $self->__Set(
540 Field => 'LastUpdated',
544 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) {
545 my ( $msg, $val ) = $self->__Set(
546 Field => 'LastUpdatedBy',
547 Value => $self->CurrentUser->id
556 Returns an RT::User object with the RT account of the creator of this row
562 unless ( exists $self->{'CreatorObj'} ) {
564 $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser );
565 $self->{'CreatorObj'}->Load( $self->Creator );
567 return ( $self->{'CreatorObj'} );
572 =head2 LastUpdatedByObj
574 Returns an RT::User object of the last user to touch this object
578 sub LastUpdatedByObj {
580 unless ( exists $self->{LastUpdatedByObj} ) {
581 $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser );
582 $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy );
584 return $self->{'LastUpdatedByObj'};
591 Returns this record's URI
597 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
598 return($uri->URIForObject($self));
602 =head2 ValidateName NAME
604 Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name
611 if (defined $value && $value=~ /^\d+$/) {
620 =head2 SQLType attribute
622 return the SQL type for the attribute 'attribute' as stored in _ClassAccessible
630 return ($self->_Accessible($field, 'type'));
638 my %args = ( decode_utf8 => 1, @_ );
641 $RT::Logger->error("__Value called with undef field");
644 my $value = $self->SUPER::__Value($field);
646 return undef if (!defined $value);
648 if ( $args{'decode_utf8'} ) {
649 if ( !utf8::is_utf8($value) ) {
650 utf8::decode($value);
654 if ( utf8::is_utf8($value) ) {
655 utf8::encode($value);
663 # Set up defaults for DBIx::SearchBuilder::Record::Cachable
668 'cache_for_sec' => 30,
674 sub _BuildTableAttributes {
676 my $class = ref($self) || $self;
679 if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) {
680 $attributes = $self->_CoreAccessible();
681 } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) {
682 $attributes = $self->_ClassAccessible();
686 foreach my $column (keys %$attributes) {
687 foreach my $attr ( keys %{ $attributes->{$column} } ) {
688 $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr};
691 foreach my $method ( qw(_OverlayAccessible _VendorAccessible _LocalAccessible) ) {
692 next unless UNIVERSAL::can( $self, $method );
693 $attributes = $self->$method();
695 foreach my $column ( keys %$attributes ) {
696 foreach my $attr ( keys %{ $attributes->{$column} } ) {
697 $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr};
704 =head2 _ClassAccessible
706 Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in
707 DBIx::SearchBuilder::Record
711 sub _ClassAccessible {
713 return $_TABLE_ATTR->{ref($self) || $self};
716 =head2 _Accessible COLUMN ATTRIBUTE
718 returns the value of ATTRIBUTE for COLUMN
726 my $attribute = lc(shift);
727 return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column});
728 return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0;
732 =head2 _EncodeLOB BODY MIME_TYPE
734 Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database
741 my $MIMEType = shift || '';
742 my $Filename = shift;
744 my $ContentEncoding = 'none';
746 #get the max attachment length from RT
747 my $MaxSize = RT->Config->Get('MaxAttachmentSize');
749 #if the current attachment contains nulls and the
750 #database doesn't support embedded nulls
752 if ( ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
754 # set a flag telling us to mimencode the attachment
755 $ContentEncoding = 'base64';
757 #cut the max attchment size by 25% (for mime-encoding overhead.
758 $RT::Logger->debug("Max size is $MaxSize");
759 $MaxSize = $MaxSize * 3 / 4;
760 # Some databases (postgres) can't handle non-utf8 data
761 } elsif ( !$RT::Handle->BinarySafeBLOBs
762 && $MIMEType !~ /text\/plain/gi
763 && !Encode::is_utf8( $Body, 1 ) ) {
764 $ContentEncoding = 'quoted-printable';
767 #if the attachment is larger than the maximum size
768 if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
770 # if we're supposed to truncate large attachments
771 if (RT->Config->Get('TruncateLongAttachments')) {
773 # truncate the attachment to that length.
774 $Body = substr( $Body, 0, $MaxSize );
778 # elsif we're supposed to drop large attachments on the floor,
779 elsif (RT->Config->Get('DropLongAttachments')) {
781 # drop the attachment on the floor
782 $RT::Logger->info( "$self: Dropped an attachment of size "
784 $RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) );
785 $Filename .= ".txt" if $Filename;
786 return ("none", "Large attachment dropped", "text/plain", $Filename );
790 # if we need to mimencode the attachment
791 if ( $ContentEncoding eq 'base64' ) {
793 # base64 encode the attachment
794 Encode::_utf8_off($Body);
795 $Body = MIME::Base64::encode_base64($Body);
797 } elsif ($ContentEncoding eq 'quoted-printable') {
798 Encode::_utf8_off($Body);
799 $Body = MIME::QuotedPrint::encode($Body);
803 return ($ContentEncoding, $Body, $MIMEType, $Filename );
809 my $ContentType = shift || '';
810 my $ContentEncoding = shift || 'none';
813 if ( $ContentEncoding eq 'base64' ) {
814 $Content = MIME::Base64::decode_base64($Content);
816 elsif ( $ContentEncoding eq 'quoted-printable' ) {
817 $Content = MIME::QuotedPrint::decode($Content);
819 elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) {
820 return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) );
822 if ( RT::I18N::IsTextualContentType($ContentType) ) {
823 $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content);
828 # A helper table for links mapping to make it easier
829 # to build and parse links between tickets
831 use vars '%LINKDIRMAP';
834 MemberOf => { Base => 'MemberOf',
835 Target => 'HasMember', },
836 RefersTo => { Base => 'RefersTo',
837 Target => 'ReferredToBy', },
838 DependsOn => { Base => 'DependsOn',
839 Target => 'DependedOnBy', },
840 MergedInto => { Base => 'MergedInto',
841 Target => 'MergedInto', },
845 =head2 Update ARGSHASH
847 Updates fields on an object for you using the proper Set methods,
848 skipping unchanged values.
850 ARGSRef => a hashref of attributes => value for the update
851 AttributesRef => an arrayref of keys in ARGSRef that should be updated
852 AttributePrefix => a prefix that should be added to the attributes in AttributesRef
853 when looking up values in ARGSRef
854 Bare attributes are tried before prefixed attributes
856 Returns a list of localized results of the update
865 AttributesRef => undef,
866 AttributePrefix => undef,
870 my $attributes = $args{'AttributesRef'};
871 my $ARGSRef = $args{'ARGSRef'};
874 # gather all new values
875 foreach my $attribute (@$attributes) {
877 if ( defined $ARGSRef->{$attribute} ) {
878 $value = $ARGSRef->{$attribute};
881 defined( $args{'AttributePrefix'} )
883 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
886 $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
893 $value =~ s/\r\n/\n/gs;
895 my $truncated_value = $self->TruncateValue($attribute, $value);
897 # If Queue is 'General', we want to resolve the queue name for
900 # This is in an eval block because $object might not exist.
901 # and might not have a Name method. But "can" won't find autoloaded
902 # items. If it fails, we don't care
904 no warnings "uninitialized";
907 my $object = $attribute . "Obj";
908 my $name = $self->$object->Name;
909 next if $name eq $value || $name eq ($value || 0);
912 my $current = $self->$attribute();
913 # RT::Queue->Lifecycle returns a Lifecycle object instead of name
914 $current = eval { $current->Name } if ref $current;
915 next if $truncated_value eq $current;
916 next if ( $truncated_value || 0 ) eq $current;
919 $new_values{$attribute} = $value;
922 return $self->_UpdateAttributes(
923 Attributes => $attributes,
924 NewValues => \%new_values,
928 sub _UpdateAttributes {
938 foreach my $attribute (@{ $args{Attributes} }) {
939 next if !exists($args{NewValues}{$attribute});
941 my $value = $args{NewValues}{$attribute};
942 my $method = "Set$attribute";
943 my ( $code, $msg ) = $self->$method($value);
944 my ($prefix) = ref($self) =~ /RT(?:.*)::(\w+)/;
946 # Default to $id, but use name if we can get it.
947 my $label = $self->id;
948 $label = $self->Name if (UNIVERSAL::can($self,'Name'));
949 # this requires model names to be loc'ed.
960 push @results, $self->loc( $prefix ) . " $label: ". $msg;
964 "[_1] could not be set to [_2].", # loc
965 "That is already the current value", # loc
966 "No value sent to _Set!", # loc
967 "Illegal value for [_1]", # loc
968 "The new value has been set.", # loc
969 "No column specified", # loc
970 "Immutable field", # loc
971 "Nonexistant field?", # loc
972 "Invalid data", # loc
973 "Couldn't find row", # loc
974 "Missing a primary key?: [_1]", # loc
975 "Found Object", # loc
989 This returns an RT::Links object which references all the tickets
990 which are 'MembersOf' this ticket
996 return ( $self->_Links( 'Target', 'MemberOf' ) );
1003 This returns an RT::Links object which references all the tickets that this
1004 ticket is a 'MemberOf'
1010 return ( $self->_Links( 'Base', 'MemberOf' ) );
1017 This returns an RT::Links object which shows all references for which this ticket is a base
1023 return ( $self->_Links( 'Base', 'RefersTo' ) );
1030 This returns an L<RT::Links> object which shows all references for which this ticket is a target
1036 return ( $self->_Links( 'Target', 'RefersTo' ) );
1043 This returns an RT::Links object which references all the tickets that depend on this one
1049 return ( $self->_Links( 'Target', 'DependsOn' ) );
1055 =head2 HasUnresolvedDependencies
1057 Takes a paramhash of Type (default to '__any'). Returns the number of
1058 unresolved dependencies, if $self->UnresolvedDependencies returns an
1059 object with one or more members of that type. Returns false
1064 sub HasUnresolvedDependencies {
1071 my $deps = $self->UnresolvedDependencies;
1074 $deps->Limit( FIELD => 'Type',
1076 VALUE => $args{Type});
1082 if ($deps->Count > 0) {
1083 return $deps->Count;
1092 =head2 UnresolvedDependencies
1094 Returns an RT::Tickets object of tickets which this ticket depends on
1095 and which have a status of new, open or stalled. (That list comes from
1096 RT::Queue->ActiveStatusArray
1101 sub UnresolvedDependencies {
1103 my $deps = RT::Tickets->new($self->CurrentUser);
1105 my @live_statuses = RT::Queue->ActiveStatusArray();
1106 foreach my $status (@live_statuses) {
1107 $deps->LimitStatus(VALUE => $status);
1109 $deps->LimitDependedOnBy($self->Id);
1117 =head2 AllDependedOnBy
1119 Returns an array of RT::Ticket objects which (directly or indirectly)
1120 depends on this ticket; takes an optional 'Type' argument in the param
1121 hash, which will limit returned tickets to that type, as well as cause
1122 tickets with that type to serve as 'leaf' nodes that stops the recursive
1127 sub AllDependedOnBy {
1129 return $self->_AllLinkedTickets( LinkType => 'DependsOn',
1130 Direction => 'Target', @_ );
1135 Returns an array of RT::Ticket objects which this ticket (directly or
1136 indirectly) depends on; takes an optional 'Type' argument in the param
1137 hash, which will limit returned tickets to that type, as well as cause
1138 tickets with that type to serve as 'leaf' nodes that stops the
1139 recursive dependency search.
1145 return $self->_AllLinkedTickets( LinkType => 'DependsOn',
1146 Direction => 'Base', @_ );
1149 sub _AllLinkedTickets {
1161 my $dep = $self->_Links( $args{Direction}, $args{LinkType});
1162 while (my $link = $dep->Next()) {
1163 my $uri = $args{Direction} eq 'Target' ? $link->BaseURI : $link->TargetURI;
1164 next unless ($uri->IsLocal());
1165 my $obj = $args{Direction} eq 'Target' ? $link->BaseObj : $link->TargetObj;
1166 next if $args{_found}{$obj->Id};
1169 $args{_found}{$obj->Id} = $obj;
1170 $obj->_AllLinkedTickets( %args, _top => 0 );
1172 elsif ($obj->Type and $obj->Type eq $args{Type}) {
1173 $args{_found}{$obj->Id} = $obj;
1176 $obj->_AllLinkedTickets( %args, _top => 0 );
1181 return map { $args{_found}{$_} } sort keys %{$args{_found}};
1192 This returns an RT::Links object which references all the tickets that this ticket depends on
1198 return ( $self->_Links( 'Base', 'DependsOn' ) );
1206 =head2 Links DIRECTION [TYPE]
1208 Return links (L<RT::Links>) to/from this object.
1210 DIRECTION is either 'Base' or 'Target'.
1212 TYPE is a type of links to return, it can be omitted to get
1217 sub Links { shift->_Links(@_) }
1222 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1225 my $type = shift || "";
1227 unless ( $self->{"$field$type"} ) {
1228 $self->{"$field$type"} = RT::Links->new( $self->CurrentUser );
1229 # at least to myself
1230 $self->{"$field$type"}->Limit( FIELD => $field,
1231 VALUE => $self->URI,
1232 ENTRYAGGREGATOR => 'OR' );
1233 $self->{"$field$type"}->Limit( FIELD => 'Type',
1237 return ( $self->{"$field$type"} );
1245 Takes a Type and returns a string that is more human readable.
1251 my %args = ( Type => '',
1254 $args{Type} =~ s/([A-Z])/" " . lc $1/ge;
1255 $args{Type} =~ s/^\s+//;
1264 Takes either a Target or a Base and returns a string of human friendly text.
1270 my %args = ( Object => undef,
1274 my $text = "URI " . $args{FallBack};
1275 if ($args{Object} && $args{Object}->isa("RT::Ticket")) {
1276 $text = "Ticket " . $args{Object}->id;
1285 Takes a paramhash of Type and one of Base or Target. Adds that link to this object.
1287 Returns C<link id>, C<message> and C<exist> flag.
1294 my %args = ( Target => '',
1301 # Remote_link is the URI of the object that is not this ticket
1305 if ( $args{'Base'} and $args{'Target'} ) {
1306 $RT::Logger->debug( "$self tried to create a link. both base and target were specified" );
1307 return ( 0, $self->loc("Can't specify both base and target") );
1309 elsif ( $args{'Base'} ) {
1310 $args{'Target'} = $self->URI();
1311 $remote_link = $args{'Base'};
1312 $direction = 'Target';
1314 elsif ( $args{'Target'} ) {
1315 $args{'Base'} = $self->URI();
1316 $remote_link = $args{'Target'};
1317 $direction = 'Base';
1320 return ( 0, $self->loc('Either base or target must be specified') );
1323 # Check if the link already exists - we don't want duplicates
1325 my $old_link = RT::Link->new( $self->CurrentUser );
1326 $old_link->LoadByParams( Base => $args{'Base'},
1327 Type => $args{'Type'},
1328 Target => $args{'Target'} );
1329 if ( $old_link->Id ) {
1330 $RT::Logger->debug("$self Somebody tried to duplicate a link");
1331 return ( $old_link->id, $self->loc("Link already exists"), 1 );
1337 # Storing the link in the DB.
1338 my $link = RT::Link->new( $self->CurrentUser );
1339 my ($linkid, $linkmsg) = $link->Create( Target => $args{Target},
1340 Base => $args{Base},
1341 Type => $args{Type} );
1344 $RT::Logger->error("Link could not be created: ".$linkmsg);
1345 return ( 0, $self->loc("Link could not be created") );
1348 my $basetext = $self->FormatLink(Object => $link->BaseObj,
1349 FallBack => $args{Base});
1350 my $targettext = $self->FormatLink(Object => $link->TargetObj,
1351 FallBack => $args{Target});
1352 my $typetext = $self->FormatType(Type => $args{Type});
1354 "$basetext $typetext $targettext.";
1355 return ( $linkid, $TransString ) ;
1362 Delete a link. takes a paramhash of Base, Target and Type.
1363 Either Base or Target must be null. The null value will
1364 be replaced with this ticket's id
1377 #we want one of base and target. we don't care which
1378 #but we only want _one_
1383 if ( $args{'Base'} and $args{'Target'} ) {
1384 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target");
1385 return ( 0, $self->loc("Can't specify both base and target") );
1387 elsif ( $args{'Base'} ) {
1388 $args{'Target'} = $self->URI();
1389 $remote_link = $args{'Base'};
1390 $direction = 'Target';
1392 elsif ( $args{'Target'} ) {
1393 $args{'Base'} = $self->URI();
1394 $remote_link = $args{'Target'};
1398 $RT::Logger->error("Base or Target must be specified");
1399 return ( 0, $self->loc('Either base or target must be specified') );
1402 my $link = RT::Link->new( $self->CurrentUser );
1403 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} );
1406 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
1410 my $basetext = $self->FormatLink(Object => $link->BaseObj,
1411 FallBack => $args{Base});
1412 my $targettext = $self->FormatLink(Object => $link->TargetObj,
1413 FallBack => $args{Target});
1414 my $typetext = $self->FormatType(Type => $args{Type});
1415 my $linkid = $link->id;
1417 my $TransString = "$basetext no longer $typetext $targettext.";
1418 return ( 1, $TransString);
1421 #if it's not a link we can find
1423 $RT::Logger->debug("Couldn't find that link");
1424 return ( 0, $self->loc("Link not found") );
1429 =head1 LockForUpdate
1431 In a database transaction, gains an exclusive lock on the row, to
1432 prevent race conditions. On SQLite, this is a "RESERVED" lock on the
1440 my $pk = $self->_PrimaryKey;
1441 my $id = @_ ? $_[0] : $self->$pk;
1442 $self->_expire if $self->isa("DBIx::SearchBuilder::Record::Cachable");
1443 if (RT->Config->Get('DatabaseType') eq "SQLite") {
1444 # SQLite does DB-level locking, upgrading the transaction to
1445 # "RESERVED" on the first UPDATE/INSERT/DELETE. Do a no-op
1446 # UPDATE to force the upgade.
1447 return RT->DatabaseHandle->dbh->do(
1448 "UPDATE " .$self->Table.
1449 " SET $pk = $pk WHERE 1 = 0");
1451 return $self->_LoadFromSQL(
1452 "SELECT * FROM ".$self->Table
1453 ." WHERE $pk = ? FOR UPDATE",
1459 =head2 _NewTransaction PARAMHASH
1461 Private function to create a new RT::Transaction object for this ticket update
1465 sub _NewTransaction {
1472 OldReference => undef,
1473 NewReference => undef,
1474 ReferenceType => undef,
1478 ActivateScrips => 1,
1480 SquelchMailTo => undef,
1484 my $in_txn = RT->DatabaseHandle->TransactionDepth;
1485 RT->DatabaseHandle->BeginTransaction unless $in_txn;
1487 $self->LockForUpdate;
1489 my $old_ref = $args{'OldReference'};
1490 my $new_ref = $args{'NewReference'};
1491 my $ref_type = $args{'ReferenceType'};
1492 if ($old_ref or $new_ref) {
1493 $ref_type ||= ref($old_ref) || ref($new_ref);
1495 $RT::Logger->error("Reference type not specified for transaction");
1498 $old_ref = $old_ref->Id if ref($old_ref);
1499 $new_ref = $new_ref->Id if ref($new_ref);
1502 require RT::Transaction;
1503 my $trans = RT::Transaction->new( $self->CurrentUser );
1504 my ( $transaction, $msg ) = $trans->Create(
1505 ObjectId => $self->Id,
1506 ObjectType => ref($self),
1507 TimeTaken => $args{'TimeTaken'},
1508 Type => $args{'Type'},
1509 Data => $args{'Data'},
1510 Field => $args{'Field'},
1511 NewValue => $args{'NewValue'},
1512 OldValue => $args{'OldValue'},
1513 NewReference => $new_ref,
1514 OldReference => $old_ref,
1515 ReferenceType => $ref_type,
1516 MIMEObj => $args{'MIMEObj'},
1517 ActivateScrips => $args{'ActivateScrips'},
1518 CommitScrips => $args{'CommitScrips'},
1519 SquelchMailTo => $args{'SquelchMailTo'},
1522 # Rationalize the object since we may have done things to it during the caching.
1523 $self->Load($self->Id);
1525 $RT::Logger->warning($msg) unless $transaction;
1527 $self->_SetLastUpdated;
1529 if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) {
1530 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
1532 if ( RT->Config->Get('UseTransactionBatch') and $transaction ) {
1533 push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'};
1536 RT->DatabaseHandle->Commit unless $in_txn;
1538 return ( $transaction, $msg, $trans );
1545 Returns an RT::Transactions object of all transactions on this record object
1552 use RT::Transactions;
1553 my $transactions = RT::Transactions->new( $self->CurrentUser );
1555 #If the user has no rights, return an empty object
1556 $transactions->Limit(
1557 FIELD => 'ObjectId',
1560 $transactions->Limit(
1561 FIELD => 'ObjectType',
1562 VALUE => ref($self),
1565 return ($transactions);
1572 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1574 $cfs->SetContextObject( $self );
1575 # XXX handle multiple types properly
1576 $cfs->LimitToLookupType( $self->CustomFieldLookupType );
1577 $cfs->LimitToGlobalOrObjectId( $self->CustomFieldLookupId );
1578 $cfs->ApplySortOrder;
1583 # TODO: This _only_ works for RT::Foo classes. it doesn't work, for
1584 # example, for RT::IR::Foo classes.
1586 sub CustomFieldLookupId {
1588 my $lookup = shift || $self->CustomFieldLookupType;
1589 my @classes = ($lookup =~ /RT::(\w+)-/g);
1591 # Work on "RT::Queue", for instance
1592 return $self->Id unless @classes;
1595 # Save a ->Load call by not calling ->FooObj->Id, just ->Foo
1596 my $final = shift @classes;
1597 foreach my $class (reverse @classes) {
1598 my $method = "${class}Obj";
1599 $object = $object->$method;
1602 my $id = $object->$final;
1603 unless (defined $id) {
1604 my $method = "${final}Obj";
1605 $id = $object->$method->Id;
1611 =head2 CustomFieldLookupType
1613 Returns the path RT uses to figure out which custom fields apply to this object.
1617 sub CustomFieldLookupType {
1619 return ref($self) || $self;
1623 =head2 AddCustomFieldValue { Field => FIELD, Value => VALUE }
1625 VALUE should be a string. FIELD can be any identifier of a CustomField
1626 supported by L</LoadCustomFieldByIdentifier> method.
1628 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
1629 deletes the old value.
1630 If VALUE is not a valid value for the custom field, returns
1631 (0, 'Error message' ) otherwise, returns ($id, 'Success Message') where
1632 $id is ID of created L<ObjectCustomFieldValue> object.
1636 sub AddCustomFieldValue {
1638 $self->_AddCustomFieldValue(@_);
1641 sub _AddCustomFieldValue {
1646 LargeContent => undef,
1647 ContentType => undef,
1648 RecordTransaction => 1,
1652 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1653 unless ( $cf->Id ) {
1654 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1657 my $OCFs = $self->CustomFields;
1658 $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id );
1659 unless ( $OCFs->Count ) {
1663 "Custom field [_1] does not apply to this object",
1664 ref $args{'Field'} ? $args{'Field'}->id : $args{'Field'}
1669 # empty string is not correct value of any CF, so undef it
1670 foreach ( qw(Value LargeContent) ) {
1671 $args{ $_ } = undef if defined $args{ $_ } && !length $args{ $_ };
1674 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
1675 return ( 0, $self->loc("Invalid value for custom field") );
1678 # If the custom field only accepts a certain # of values, delete the existing
1679 # value and record a "changed from foo to bar" transaction
1680 unless ( $cf->UnlimitedValues ) {
1682 # Load up a ObjectCustomFieldValues object for this custom field and this ticket
1683 my $values = $cf->ValuesForObject($self);
1685 # We need to whack any old values here. In most cases, the custom field should
1686 # only have one value to delete. In the pathalogical case, this custom field
1687 # used to be a multiple and we have many values to whack....
1688 my $cf_values = $values->Count;
1690 if ( $cf_values > $cf->MaxValues ) {
1691 my $i = 0; #We want to delete all but the max we can currently have , so we can then
1692 # execute the same code to "change" the value from old to new
1693 while ( my $value = $values->Next ) {
1695 if ( $i < $cf_values ) {
1696 my ( $val, $msg ) = $cf->DeleteValueForObject(
1698 Content => $value->Content
1703 my ( $TransactionId, $Msg, $TransactionObj ) =
1704 $self->_NewTransaction(
1705 Type => 'CustomField',
1707 OldReference => $value,
1711 $values->RedoSearch if $i; # redo search if have deleted at least one value
1714 my ( $old_value, $old_content );
1715 if ( $old_value = $values->First ) {
1716 $old_content = $old_value->Content;
1717 $old_content = undef if defined $old_content && !length $old_content;
1719 my $is_the_same = 1;
1720 if ( defined $args{'Value'} ) {
1721 $is_the_same = 0 unless defined $old_content
1722 && $old_content eq $args{'Value'};
1724 $is_the_same = 0 if defined $old_content;
1726 if ( $is_the_same ) {
1727 my $old_content = $old_value->LargeContent;
1728 if ( defined $args{'LargeContent'} ) {
1729 $is_the_same = 0 unless defined $old_content
1730 && $old_content eq $args{'LargeContent'};
1732 $is_the_same = 0 if defined $old_content;
1736 return $old_value->id if $is_the_same;
1739 my ( $new_value_id, $value_msg ) = $cf->AddValueForObject(
1741 Content => $args{'Value'},
1742 LargeContent => $args{'LargeContent'},
1743 ContentType => $args{'ContentType'},
1746 unless ( $new_value_id ) {
1747 return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg ) );
1750 my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1751 $new_value->Load( $new_value_id );
1753 # now that adding the new value was successful, delete the old one
1755 my ( $val, $msg ) = $old_value->Delete();
1756 return ( 0, $msg ) unless $val;
1759 if ( $args{'RecordTransaction'} ) {
1760 my ( $TransactionId, $Msg, $TransactionObj ) =
1761 $self->_NewTransaction(
1762 Type => 'CustomField',
1764 OldReference => $old_value,
1765 NewReference => $new_value,
1769 my $new_content = $new_value->Content;
1771 # For datetime, we need to display them in "human" format in result message
1772 #XXX TODO how about date without time?
1773 if ($cf->Type eq 'DateTime') {
1774 my $DateObj = RT::Date->new( $self->CurrentUser );
1777 Value => $new_content,
1779 $new_content = $DateObj->AsString;
1781 if ( defined $old_content && length $old_content ) {
1784 Value => $old_content,
1786 $old_content = $DateObj->AsString;
1790 unless ( defined $old_content && length $old_content ) {
1791 return ( $new_value_id, $self->loc( "[_1] [_2] added", $cf->Name, $new_content ));
1793 elsif ( !defined $new_content || !length $new_content ) {
1794 return ( $new_value_id,
1795 $self->loc( "[_1] [_2] deleted", $cf->Name, $old_content ) );
1798 return ( $new_value_id, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content, $new_content));
1803 # otherwise, just add a new value and record "new value added"
1805 my ($new_value_id, $msg) = $cf->AddValueForObject(
1807 Content => $args{'Value'},
1808 LargeContent => $args{'LargeContent'},
1809 ContentType => $args{'ContentType'},
1812 unless ( $new_value_id ) {
1813 return ( 0, $self->loc( "Could not add new custom field value: [_1]", $msg ) );
1815 if ( $args{'RecordTransaction'} ) {
1816 my ( $tid, $msg ) = $self->_NewTransaction(
1817 Type => 'CustomField',
1819 NewReference => $new_value_id,
1820 ReferenceType => 'RT::ObjectCustomFieldValue',
1823 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $msg ) );
1826 return ( $new_value_id, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name ) );
1832 =head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
1834 Deletes VALUE as a value of CustomField FIELD.
1836 VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue.
1838 If VALUE is not a valid value for the custom field, returns
1839 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1843 sub DeleteCustomFieldValue {
1852 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1853 unless ( $cf->Id ) {
1854 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1857 my ( $val, $msg ) = $cf->DeleteValueForObject(
1859 Id => $args{'ValueId'},
1860 Content => $args{'Value'},
1866 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
1867 Type => 'CustomField',
1869 OldReference => $val,
1870 ReferenceType => 'RT::ObjectCustomFieldValue',
1872 unless ($TransactionId) {
1873 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1876 my $old_value = $TransactionObj->OldValue;
1877 # For datetime, we need to display them in "human" format in result message
1878 if ( $cf->Type eq 'DateTime' ) {
1879 my $DateObj = RT::Date->new( $self->CurrentUser );
1882 Value => $old_value,
1884 $old_value = $DateObj->AsString;
1889 "[_1] is no longer a value for custom field [_2]",
1890 $old_value, $cf->Name
1897 =head2 FirstCustomFieldValue FIELD
1899 Return the content of the first value of CustomField FIELD for this ticket
1900 Takes a field id or name
1904 sub FirstCustomFieldValue {
1908 my $values = $self->CustomFieldValues( $field );
1909 return undef unless my $first = $values->First;
1910 return $first->Content;
1913 =head2 CustomFieldValuesAsString FIELD
1915 Return the content of the CustomField FIELD for this ticket.
1916 If this is a multi-value custom field, values will be joined with newlines.
1918 Takes a field id or name as the first argument
1920 Takes an optional Separator => "," second and third argument
1921 if you want to join the values using something other than a newline
1925 sub CustomFieldValuesAsString {
1929 my $separator = $args{Separator} || "\n";
1931 my $values = $self->CustomFieldValues( $field );
1932 return join ($separator, grep { defined $_ }
1933 map { $_->Content } @{$values->ItemsArrayRef});
1938 =head2 CustomFieldValues FIELD
1940 Return a ObjectCustomFieldValues object of all values of the CustomField whose
1941 id or Name is FIELD for this record.
1943 Returns an RT::ObjectCustomFieldValues object
1947 sub CustomFieldValues {
1952 my $cf = $self->LoadCustomFieldByIdentifier( $field );
1954 # we were asked to search on a custom field we couldn't find
1955 unless ( $cf->id ) {
1956 $RT::Logger->warning("Couldn't load custom field by '$field' identifier");
1957 return RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1959 return ( $cf->ValuesForObject($self) );
1962 # we're not limiting to a specific custom field;
1963 my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1964 $ocfs->LimitToObject( $self );
1968 =head2 LoadCustomFieldByIdentifier IDENTIFER
1970 Find the custom field has id or name IDENTIFIER for this object.
1972 If no valid field is found, returns an empty RT::CustomField object.
1976 sub LoadCustomFieldByIdentifier {
1981 if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) {
1982 $cf = RT::CustomField->new($self->CurrentUser);
1983 $cf->SetContextObject( $self );
1984 $cf->LoadById( $field->id );
1986 elsif ($field =~ /^\d+$/) {
1987 $cf = RT::CustomField->new($self->CurrentUser);
1988 $cf->SetContextObject( $self );
1989 $cf->LoadById($field);
1992 my $cfs = $self->CustomFields($self->CurrentUser);
1993 $cfs->SetContextObject( $self );
1994 $cfs->Limit(FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0);
1995 $cf = $cfs->First || RT::CustomField->new($self->CurrentUser);
2000 sub ACLEquivalenceObjects { }
2002 sub BasicColumns { }
2005 return RT->Config->Get('WebPath'). "/index.html?q=";
2008 RT::Base->_ImportOverlays();