diff options
Diffstat (limited to 'rt/lib/RT/Record.pm.orig')
-rwxr-xr-x | rt/lib/RT/Record.pm.orig | 2102 |
1 files changed, 0 insertions, 2102 deletions
diff --git a/rt/lib/RT/Record.pm.orig b/rt/lib/RT/Record.pm.orig deleted file mode 100755 index 7adfc2678..000000000 --- a/rt/lib/RT/Record.pm.orig +++ /dev/null @@ -1,2102 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC -# <sales@bestpractical.com> -# -# (Except where explicitly superseded by other copyright notices) -# -# -# LICENSE: -# -# This work is made available to you under the terms of Version 2 of -# the GNU General Public License. A copy of that license should have -# been provided with this software, but in any event can be snarfed -# from www.gnu.org. -# -# This work is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301 or visit their web page on the internet at -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# -# CONTRIBUTION SUBMISSION POLICY: -# -# (The following paragraph is not intended to limit the rights granted -# to you to modify and distribute this software under the terms of -# the GNU General Public License and is only of importance to you if -# you choose to contribute your changes and enhancements to the -# community by submitting them to Best Practical Solutions, LLC.) -# -# By intentionally submitting any modifications, corrections or -# derivatives to this work, or any other work intended for use with -# Request Tracker, to Best Practical Solutions, LLC, you confirm that -# you are the copyright holder for those contributions and you grant -# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, -# royalty-free, perpetual, license to use, copy, create derivative -# works based on those contributions, and sublicense and distribute -# those contributions and any derivatives thereof. -# -# END BPS TAGGED BLOCK }}} - -=head1 NAME - - RT::Record - Base class for RT record objects - -=head1 SYNOPSIS - - -=head1 DESCRIPTION - - - -=head1 METHODS - -=cut - -package RT::Record; - -use strict; -use warnings; - - -use RT::Date; -use RT::I18N; -use RT::User; -use RT::Attributes; -use Encode qw(); - -our $_TABLE_ATTR = { }; -use base RT->Config->Get('RecordBaseClass'); -use base 'RT::Base'; - - -sub _Init { - my $self = shift; - $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)}); - $self->CurrentUser(@_); -} - - - -=head2 _PrimaryKeys - -The primary keys for RT classes is 'id' - -=cut - -sub _PrimaryKeys { return ['id'] } -# short circuit many, many thousands of calls from searchbuilder -sub _PrimaryKey { 'id' } - -=head2 Id - -Override L<DBIx::SearchBuilder/Id> to avoid a few lookups RT doesn't do -on a very common codepath - -C<id> is an alias to C<Id> and is the preferred way to call this method. - -=cut - -sub Id { - return shift->{'values'}->{id}; -} - -*id = \&Id; - -=head2 Delete - -Delete this record object from the database. - -=cut - -sub Delete { - my $self = shift; - my ($rv) = $self->SUPER::Delete; - if ($rv) { - return ($rv, $self->loc("Object deleted")); - } else { - - return(0, $self->loc("Object could not be deleted")) - } -} - -=head2 ObjectTypeStr - -Returns a string which is this object's type. The type is the class, -without the "RT::" prefix. - - -=cut - -sub ObjectTypeStr { - my $self = shift; - if (ref($self) =~ /^.*::(\w+)$/) { - return $self->loc($1); - } else { - return $self->loc(ref($self)); - } -} - -=head2 Attributes - -Return this object's attributes as an RT::Attributes object - -=cut - -sub Attributes { - my $self = shift; - unless ($self->{'attributes'}) { - $self->{'attributes'} = RT::Attributes->new($self->CurrentUser); - $self->{'attributes'}->LimitToObject($self); - $self->{'attributes'}->OrderByCols({FIELD => 'id'}); - } - return ($self->{'attributes'}); -} - - -=head2 AddAttribute { Name, Description, Content } - -Adds a new attribute for this object. - -=cut - -sub AddAttribute { - my $self = shift; - my %args = ( Name => undef, - Description => undef, - Content => undef, - @_ ); - - my $attr = RT::Attribute->new( $self->CurrentUser ); - my ( $id, $msg ) = $attr->Create( - Object => $self, - Name => $args{'Name'}, - Description => $args{'Description'}, - Content => $args{'Content'} ); - - - # XXX TODO: Why won't RedoSearch work here? - $self->Attributes->_DoSearch; - - return ($id, $msg); -} - - -=head2 SetAttribute { Name, Description, Content } - -Like AddAttribute, but replaces all existing attributes with the same Name. - -=cut - -sub SetAttribute { - my $self = shift; - my %args = ( Name => undef, - Description => undef, - Content => undef, - @_ ); - - my @AttributeObjs = $self->Attributes->Named( $args{'Name'} ) - or return $self->AddAttribute( %args ); - - my $AttributeObj = pop( @AttributeObjs ); - $_->Delete foreach @AttributeObjs; - - $AttributeObj->SetDescription( $args{'Description'} ); - $AttributeObj->SetContent( $args{'Content'} ); - - $self->Attributes->RedoSearch; - return 1; -} - -=head2 DeleteAttribute NAME - -Deletes all attributes with the matching name for this object. - -=cut - -sub DeleteAttribute { - my $self = shift; - my $name = shift; - my ($val,$msg) = $self->Attributes->DeleteEntry( Name => $name ); - $self->ClearAttributes; - return ($val,$msg); -} - -=head2 FirstAttribute NAME - -Returns the first attribute with the matching name for this object (as an -L<RT::Attribute> object), or C<undef> if no such attributes exist. -If there is more than one attribute with the matching name on the -object, the first value that was set is returned. - -=cut - -sub FirstAttribute { - my $self = shift; - my $name = shift; - return ($self->Attributes->Named( $name ))[0]; -} - - -sub ClearAttributes { - my $self = shift; - delete $self->{'attributes'}; - -} - -sub _Handle { return $RT::Handle } - - - -=head2 Create PARAMHASH - -Takes a PARAMHASH of Column -> Value pairs. -If any Column has a Validate$PARAMNAME subroutine defined and the -value provided doesn't pass validation, this routine returns -an error. - -If this object's table has any of the following atetributes defined as -'Auto', this routine will automatically fill in their values. - -=over - -=item Created - -=item Creator - -=item LastUpdated - -=item LastUpdatedBy - -=back - -=cut - -sub Create { - my $self = shift; - my %attribs = (@_); - foreach my $key ( keys %attribs ) { - if (my $method = $self->can("Validate$key")) { - if (! $method->( $self, $attribs{$key} ) ) { - if (wantarray) { - return ( 0, $self->loc('Invalid value for [_1]', $key) ); - } - else { - return (0); - } - } - } - } - - - - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) = gmtime(); - - my $now_iso = - sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $mday, $hour, $min, $sec); - - $attribs{'Created'} = $now_iso if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'}); - - if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) { - $attribs{'Creator'} = $self->CurrentUser->id || '0'; - } - $attribs{'LastUpdated'} = $now_iso - if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'}); - - $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0' - if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'}); - - my $id = $self->SUPER::Create(%attribs); - if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) { - if ( $id->errno ) { - if (wantarray) { - return ( 0, - $self->loc( "Internal Error: [_1]", $id->{error_message} ) ); - } - else { - return (0); - } - } - } - # If the object was created in the database, - # load it up now, so we're sure we get what the database - # has. Arguably, this should not be necessary, but there - # isn't much we can do about it. - - unless ($id) { - if (wantarray) { - return ( $id, $self->loc('Object could not be created') ); - } - else { - return ($id); - } - - } - - if (UNIVERSAL::isa('errno',$id)) { - return(undef); - } - - $self->Load($id) if ($id); - - - - if (wantarray) { - return ( $id, $self->loc('Object created') ); - } - else { - return ($id); - } - -} - - - -=head2 LoadByCols - -Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the -DB is case sensitive - -=cut - -sub LoadByCols { - my $self = shift; - - # We don't want to hang onto this - $self->ClearAttributes; - - return $self->SUPER::LoadByCols( @_ ) unless $self->_Handle->CaseSensitive; - - # If this database is case sensitive we need to uncase objects for - # explicit loading - my %hash = (@_); - foreach my $key ( keys %hash ) { - - # If we've been passed an empty value, we can't do the lookup. - # We don't need to explicitly downcase integers or an id. - if ( $key ne 'id' && defined $hash{ $key } && $hash{ $key } !~ /^\d+$/ ) { - my ($op, $val, $func); - ($key, $op, $val, $func) = - $self->_Handle->_MakeClauseCaseInsensitive( $key, '=', delete $hash{ $key } ); - $hash{$key}->{operator} = $op; - $hash{$key}->{value} = $val; - $hash{$key}->{function} = $func; - } - } - return $self->SUPER::LoadByCols( %hash ); -} - - - -# There is room for optimizations in most of those subs: - - -sub LastUpdatedObj { - my $self = shift; - my $obj = RT::Date->new( $self->CurrentUser ); - - $obj->Set( Format => 'sql', Value => $self->LastUpdated ); - return $obj; -} - - - -sub CreatedObj { - my $self = shift; - my $obj = RT::Date->new( $self->CurrentUser ); - - $obj->Set( Format => 'sql', Value => $self->Created ); - - return $obj; -} - - -# -# TODO: This should be deprecated -# -sub AgeAsString { - my $self = shift; - return ( $self->CreatedObj->AgeAsString() ); -} - - - -# TODO this should be deprecated - -sub LastUpdatedAsString { - my $self = shift; - if ( $self->LastUpdated ) { - return ( $self->LastUpdatedObj->AsString() ); - - } - else { - return "never"; - } -} - - -# -# TODO This should be deprecated -# -sub CreatedAsString { - my $self = shift; - return ( $self->CreatedObj->AsString() ); -} - - -# -# TODO This should be deprecated -# -sub LongSinceUpdateAsString { - my $self = shift; - if ( $self->LastUpdated ) { - - return ( $self->LastUpdatedObj->AgeAsString() ); - - } - else { - return "never"; - } -} - - - -# -sub _Set { - my $self = shift; - - my %args = ( - Field => undef, - Value => undef, - IsSQL => undef, - @_ - ); - - #if the user is trying to modify the record - # TODO: document _why_ this code is here - - if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) { - $args{'Value'} = 0; - } - - my $old_val = $self->__Value($args{'Field'}); - $self->_SetLastUpdated(); - my $ret = $self->SUPER::_Set( - Field => $args{'Field'}, - Value => $args{'Value'}, - IsSQL => $args{'IsSQL'} - ); - my ($status, $msg) = $ret->as_array(); - - # @values has two values, a status code and a message. - - # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool - # we want to change the standard "success" message - if ($status) { - if ($self->SQLType( $args{'Field'}) =~ /text/) { - $msg = $self->loc( - "[_1] updated", - $self->loc( $args{'Field'} ), - ); - } else { - $msg = $self->loc( - "[_1] changed from [_2] to [_3]", - $self->loc( $args{'Field'} ), - ( $old_val ? '"' . $old_val . '"' : $self->loc("(no value)") ), - '"' . $self->__Value( $args{'Field'}) . '"', - ); - } - } else { - $msg = $self->CurrentUser->loc_fuzzy($msg); - } - - return wantarray ? ($status, $msg) : $ret; -} - - - -=head2 _SetLastUpdated - -This routine updates the LastUpdated and LastUpdatedBy columns of the row in question -It takes no options. Arguably, this is a bug - -=cut - -sub _SetLastUpdated { - my $self = shift; - use RT::Date; - my $now = RT::Date->new( $self->CurrentUser ); - $now->SetToNow(); - - if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) { - my ( $msg, $val ) = $self->__Set( - Field => 'LastUpdated', - Value => $now->ISO - ); - } - if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) { - my ( $msg, $val ) = $self->__Set( - Field => 'LastUpdatedBy', - Value => $self->CurrentUser->id - ); - } -} - - - -=head2 CreatorObj - -Returns an RT::User object with the RT account of the creator of this row - -=cut - -sub CreatorObj { - my $self = shift; - unless ( exists $self->{'CreatorObj'} ) { - - $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser ); - $self->{'CreatorObj'}->Load( $self->Creator ); - } - return ( $self->{'CreatorObj'} ); -} - - - -=head2 LastUpdatedByObj - - Returns an RT::User object of the last user to touch this object - -=cut - -sub LastUpdatedByObj { - my $self = shift; - unless ( exists $self->{LastUpdatedByObj} ) { - $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser ); - $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy ); - } - return $self->{'LastUpdatedByObj'}; -} - - - -=head2 URI - -Returns this record's URI - -=cut - -sub URI { - my $self = shift; - my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser); - return($uri->URIForObject($self)); -} - - -=head2 ValidateName NAME - -Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name - -=cut - -sub ValidateName { - my $self = shift; - my $value = shift; - if (defined $value && $value=~ /^\d+$/) { - return(0); - } else { - return(1); - } -} - - - -=head2 SQLType attribute - -return the SQL type for the attribute 'attribute' as stored in _ClassAccessible - -=cut - -sub SQLType { - my $self = shift; - my $field = shift; - - return ($self->_Accessible($field, 'type')); - - -} - -sub __Value { - my $self = shift; - my $field = shift; - my %args = ( decode_utf8 => 1, @_ ); - - unless ($field) { - $RT::Logger->error("__Value called with undef field"); - } - - my $value = $self->SUPER::__Value($field); - - return undef if (!defined $value); - - if ( $args{'decode_utf8'} ) { - if ( !utf8::is_utf8($value) ) { - utf8::decode($value); - } - } - else { - if ( utf8::is_utf8($value) ) { - utf8::encode($value); - } - } - - return $value; - -} - -# Set up defaults for DBIx::SearchBuilder::Record::Cachable - -sub _CacheConfig { - { - 'cache_p' => 1, - 'cache_for_sec' => 30, - } -} - - - -sub _BuildTableAttributes { - my $self = shift; - my $class = ref($self) || $self; - - my $attributes; - if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) { - $attributes = $self->_CoreAccessible(); - } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) { - $attributes = $self->_ClassAccessible(); - - } - - foreach my $column (keys %$attributes) { - foreach my $attr ( keys %{ $attributes->{$column} } ) { - $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr}; - } - } - foreach my $method ( qw(_OverlayAccessible _VendorAccessible _LocalAccessible) ) { - next unless UNIVERSAL::can( $self, $method ); - $attributes = $self->$method(); - - foreach my $column ( keys %$attributes ) { - foreach my $attr ( keys %{ $attributes->{$column} } ) { - $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr}; - } - } - } -} - - -=head2 _ClassAccessible - -Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in -DBIx::SearchBuilder::Record - -=cut - -sub _ClassAccessible { - my $self = shift; - return $_TABLE_ATTR->{ref($self) || $self}; -} - -=head2 _Accessible COLUMN ATTRIBUTE - -returns the value of ATTRIBUTE for COLUMN - - -=cut - -sub _Accessible { - my $self = shift; - my $column = shift; - my $attribute = lc(shift); - return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column}); - return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0; - -} - -=head2 _EncodeLOB BODY MIME_TYPE FILENAME - -Takes a potentially large attachment. Returns (ContentEncoding, -EncodedBody, MimeType, Filename) based on system configuration and -selected database. Returns a custom (short) text/plain message if -DropLongAttachments causes an attachment to not be stored. - -Encodes your data as base64 or Quoted-Printable as needed based on your -Databases's restrictions and the UTF-8ness of the data being passed in. Since -we are storing in columns marked UTF8, we must ensure that binary data is -encoded on databases which are strict. - -This function expects to receive an octet string in order to properly -evaluate and encode it. It will return an octet string. - -=cut - -sub _EncodeLOB { - my $self = shift; - my $Body = shift; - my $MIMEType = shift || ''; - my $Filename = shift; - - my $ContentEncoding = 'none'; - - #get the max attachment length from RT - my $MaxSize = RT->Config->Get('MaxAttachmentSize'); - - #if the current attachment contains nulls and the - #database doesn't support embedded nulls - - if ( ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) { - - # set a flag telling us to mimencode the attachment - $ContentEncoding = 'base64'; - - #cut the max attchment size by 25% (for mime-encoding overhead. - $RT::Logger->debug("Max size is $MaxSize"); - $MaxSize = $MaxSize * 3 / 4; - # Some databases (postgres) can't handle non-utf8 data - } elsif ( !$RT::Handle->BinarySafeBLOBs - && $Body =~ /\P{ASCII}/ - && !Encode::is_utf8( $Body, 1 ) ) { - $ContentEncoding = 'quoted-printable'; - } - - #if the attachment is larger than the maximum size - if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) { - - # if we're supposed to truncate large attachments - if (RT->Config->Get('TruncateLongAttachments')) { - - # truncate the attachment to that length. - $Body = substr( $Body, 0, $MaxSize ); - - } - - # elsif we're supposed to drop large attachments on the floor, - elsif (RT->Config->Get('DropLongAttachments')) { - - # drop the attachment on the floor - $RT::Logger->info( "$self: Dropped an attachment of size " - . length($Body)); - $RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) ); - $Filename .= ".txt" if $Filename; - return ("none", "Large attachment dropped", "text/plain", $Filename ); - } - } - - # if we need to mimencode the attachment - if ( $ContentEncoding eq 'base64' ) { - - # base64 encode the attachment - Encode::_utf8_off($Body); - $Body = MIME::Base64::encode_base64($Body); - - } elsif ($ContentEncoding eq 'quoted-printable') { - Encode::_utf8_off($Body); - $Body = MIME::QuotedPrint::encode($Body); - } - - - return ($ContentEncoding, $Body, $MIMEType, $Filename ); - -} - -=head2 _DecodeLOB - -Unpacks data stored in the database, which may be base64 or QP encoded -because of our need to store binary and badly encoded data in columns -marked as UTF-8. Databases such as PostgreSQL and Oracle care that you -are feeding them invalid UTF-8 and will refuse the content. This -function handles unpacking the encoded data. - -It returns textual data as a UTF-8 string which has been processed by Encode's -PERLQQ filter which will replace the invalid bytes with \x{HH} so you can see -the invalid byte but won't run into problems treating the data as UTF-8 later. - -This is similar to how we filter all data coming in via the web UI in -RT::Interface::Web::DecodeARGS. This filter should only end up being -applied to old data from less UTF-8-safe versions of RT. - -Important Note - This function expects an octet string and returns a -character string for non-binary data. - -=cut - -sub _DecodeLOB { - my $self = shift; - my $ContentType = shift || ''; - my $ContentEncoding = shift || 'none'; - my $Content = shift; - - if ( $ContentEncoding eq 'base64' ) { - $Content = MIME::Base64::decode_base64($Content); - } - elsif ( $ContentEncoding eq 'quoted-printable' ) { - $Content = MIME::QuotedPrint::decode($Content); - } - elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) { - return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) ); - } - if ( RT::I18N::IsTextualContentType($ContentType) ) { - $Content = Encode::decode('UTF-8',$Content,Encode::FB_PERLQQ) unless Encode::is_utf8($Content); - } - return ($Content); -} - -# A helper table for links mapping to make it easier -# to build and parse links between tickets - -use vars '%LINKDIRMAP'; - -%LINKDIRMAP = ( - MemberOf => { Base => 'MemberOf', - Target => 'HasMember', }, - RefersTo => { Base => 'RefersTo', - Target => 'ReferredToBy', }, - DependsOn => { Base => 'DependsOn', - Target => 'DependedOnBy', }, - MergedInto => { Base => 'MergedInto', - Target => 'MergedInto', }, - -); - -=head2 Update ARGSHASH - -Updates fields on an object for you using the proper Set methods, -skipping unchanged values. - - ARGSRef => a hashref of attributes => value for the update - AttributesRef => an arrayref of keys in ARGSRef that should be updated - AttributePrefix => a prefix that should be added to the attributes in AttributesRef - when looking up values in ARGSRef - Bare attributes are tried before prefixed attributes - -Returns a list of localized results of the update - -=cut - -sub Update { - my $self = shift; - - my %args = ( - ARGSRef => undef, - AttributesRef => undef, - AttributePrefix => undef, - @_ - ); - - my $attributes = $args{'AttributesRef'}; - my $ARGSRef = $args{'ARGSRef'}; - my %new_values; - - # gather all new values - foreach my $attribute (@$attributes) { - my $value; - if ( defined $ARGSRef->{$attribute} ) { - $value = $ARGSRef->{$attribute}; - } - elsif ( - defined( $args{'AttributePrefix'} ) - && defined( - $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute } - ) - ) { - $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }; - - } - else { - next; - } - - $value =~ s/\r\n/\n/gs; - - my $truncated_value = $self->TruncateValue($attribute, $value); - - # If Queue is 'General', we want to resolve the queue name for - # the object. - - # This is in an eval block because $object might not exist. - # and might not have a Name method. But "can" won't find autoloaded - # items. If it fails, we don't care - do { - no warnings "uninitialized"; - local $@; - eval { - my $object = $attribute . "Obj"; - my $name = $self->$object->Name; - next if $name eq $value || $name eq ($value || 0); - }; - - my $current = $self->$attribute(); - # RT::Queue->Lifecycle returns a Lifecycle object instead of name - $current = eval { $current->Name } if ref $current; - next if $truncated_value eq $current; - next if ( $truncated_value || 0 ) eq $current; - }; - - $new_values{$attribute} = $value; - } - - return $self->_UpdateAttributes( - Attributes => $attributes, - NewValues => \%new_values, - ); -} - -sub _UpdateAttributes { - my $self = shift; - my %args = ( - Attributes => [], - NewValues => {}, - @_, - ); - - my @results; - - foreach my $attribute (@{ $args{Attributes} }) { - next if !exists($args{NewValues}{$attribute}); - - my $value = $args{NewValues}{$attribute}; - my $method = "Set$attribute"; - my ( $code, $msg ) = $self->$method($value); - my ($prefix) = ref($self) =~ /RT(?:.*)::(\w+)/; - - # Default to $id, but use name if we can get it. - my $label = $self->id; - $label = $self->Name if (UNIVERSAL::can($self,'Name')); - # this requires model names to be loc'ed. - -=for loc - - "Ticket" # loc - "User" # loc - "Group" # loc - "Queue" # loc - -=cut - - push @results, $self->loc( $prefix ) . " $label: ". $msg; - -=for loc - - "[_1] could not be set to [_2].", # loc - "That is already the current value", # loc - "No value sent to _Set!", # loc - "Illegal value for [_1]", # loc - "The new value has been set.", # loc - "No column specified", # loc - "Immutable field", # loc - "Nonexistant field?", # loc - "Invalid data", # loc - "Couldn't find row", # loc - "Missing a primary key?: [_1]", # loc - "Found Object", # loc - -=cut - - } - - return @results; -} - - - - -=head2 Members - - This returns an RT::Links object which references all the tickets -which are 'MembersOf' this ticket - -=cut - -sub Members { - my $self = shift; - return ( $self->_Links( 'Target', 'MemberOf' ) ); -} - - - -=head2 MemberOf - - This returns an RT::Links object which references all the tickets that this -ticket is a 'MemberOf' - -=cut - -sub MemberOf { - my $self = shift; - return ( $self->_Links( 'Base', 'MemberOf' ) ); -} - - - -=head2 RefersTo - - This returns an RT::Links object which shows all references for which this ticket is a base - -=cut - -sub RefersTo { - my $self = shift; - return ( $self->_Links( 'Base', 'RefersTo' ) ); -} - - - -=head2 ReferredToBy - -This returns an L<RT::Links> object which shows all references for which this ticket is a target - -=cut - -sub ReferredToBy { - my $self = shift; - return ( $self->_Links( 'Target', 'RefersTo' ) ); -} - - - -=head2 DependedOnBy - - This returns an RT::Links object which references all the tickets that depend on this one - -=cut - -sub DependedOnBy { - my $self = shift; - return ( $self->_Links( 'Target', 'DependsOn' ) ); -} - - - - -=head2 HasUnresolvedDependencies - -Takes a paramhash of Type (default to '__any'). Returns the number of -unresolved dependencies, if $self->UnresolvedDependencies returns an -object with one or more members of that type. Returns false -otherwise. - -=cut - -sub HasUnresolvedDependencies { - my $self = shift; - my %args = ( - Type => undef, - @_ - ); - - my $deps = $self->UnresolvedDependencies; - - if ($args{Type}) { - $deps->Limit( FIELD => 'Type', - OPERATOR => '=', - VALUE => $args{Type}); - } - else { - $deps->IgnoreType; - } - - if ($deps->Count > 0) { - return $deps->Count; - } - else { - return (undef); - } -} - - - -=head2 UnresolvedDependencies - -Returns an RT::Tickets object of tickets which this ticket depends on -and which have a status of new, open or stalled. (That list comes from -RT::Queue->ActiveStatusArray - -=cut - - -sub UnresolvedDependencies { - my $self = shift; - my $deps = RT::Tickets->new($self->CurrentUser); - - my @live_statuses = RT::Queue->ActiveStatusArray(); - foreach my $status (@live_statuses) { - $deps->LimitStatus(VALUE => $status); - } - $deps->LimitDependedOnBy($self->Id); - - return($deps); - -} - - - -=head2 AllDependedOnBy - -Returns an array of RT::Ticket objects which (directly or indirectly) -depends on this ticket; takes an optional 'Type' argument in the param -hash, which will limit returned tickets to that type, as well as cause -tickets with that type to serve as 'leaf' nodes that stops the recursive -dependency search. - -=cut - -sub AllDependedOnBy { - my $self = shift; - return $self->_AllLinkedTickets( LinkType => 'DependsOn', - Direction => 'Target', @_ ); -} - -=head2 AllDependsOn - -Returns an array of RT::Ticket objects which this ticket (directly or -indirectly) depends on; takes an optional 'Type' argument in the param -hash, which will limit returned tickets to that type, as well as cause -tickets with that type to serve as 'leaf' nodes that stops the -recursive dependency search. - -=cut - -sub AllDependsOn { - my $self = shift; - return $self->_AllLinkedTickets( LinkType => 'DependsOn', - Direction => 'Base', @_ ); -} - -sub _AllLinkedTickets { - my $self = shift; - - my %args = ( - LinkType => undef, - Direction => undef, - Type => undef, - _found => {}, - _top => 1, - @_ - ); - - my $dep = $self->_Links( $args{Direction}, $args{LinkType}); - while (my $link = $dep->Next()) { - my $uri = $args{Direction} eq 'Target' ? $link->BaseURI : $link->TargetURI; - next unless ($uri->IsLocal()); - my $obj = $args{Direction} eq 'Target' ? $link->BaseObj : $link->TargetObj; - next if $args{_found}{$obj->Id}; - - if (!$args{Type}) { - $args{_found}{$obj->Id} = $obj; - $obj->_AllLinkedTickets( %args, _top => 0 ); - } - elsif ($obj->Type and $obj->Type eq $args{Type}) { - $args{_found}{$obj->Id} = $obj; - } - else { - $obj->_AllLinkedTickets( %args, _top => 0 ); - } - } - - if ($args{_top}) { - return map { $args{_found}{$_} } sort keys %{$args{_found}}; - } - else { - return 1; - } -} - - - -=head2 DependsOn - - This returns an RT::Links object which references all the tickets that this ticket depends on - -=cut - -sub DependsOn { - my $self = shift; - return ( $self->_Links( 'Base', 'DependsOn' ) ); -} - -# }}} - -# {{{ Customers - -=head2 Customers - - This returns an RT::Links object which references all the customers that - this object is a member of. This includes both explicitly linked customers - and links implied by services. - -=cut - -sub Customers { - my( $self, %opt ) = @_; - my $Debug = $opt{'Debug'}; - - unless ( $self->{'Customers'} ) { - - $self->{'Customers'} = $self->MemberOf->Clone; - - for my $fstable (qw(cust_main cust_svc)) { - - $self->{'Customers'}->Limit( - FIELD => 'Target', - OPERATOR => 'STARTSWITH', - VALUE => "freeside://freeside/$fstable", - ENTRYAGGREGATOR => 'OR', - SUBCLAUSE => 'customers', - ); - } - } - - warn "->Customers method called on $self; returning ". - ref($self->{'Customers'}). ' object' - if $Debug; - - return $self->{'Customers'}; -} - -# }}} - -# {{{ Services - -=head2 Services - - This returns an RT::Links object which references all the services this - object is a member of. - -=cut - -sub Services { - my( $self, %opt ) = @_; - - unless ( $self->{'Services'} ) { - - $self->{'Services'} = $self->MemberOf->Clone; - - $self->{'Services'}->Limit( - FIELD => 'Target', - OPERATOR => 'STARTSWITH', - VALUE => "freeside://freeside/cust_svc", - ); - } - - return $self->{'Services'}; -} - - - - - - -=head2 Links DIRECTION [TYPE] - -Return links (L<RT::Links>) to/from this object. - -DIRECTION is either 'Base' or 'Target'. - -TYPE is a type of links to return, it can be omitted to get -links of any type. - -=cut - -sub Links { shift->_Links(@_) } - -sub _Links { - my $self = shift; - - #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic --- - #tobias meant by $f - my $field = shift; - my $type = shift || ""; - - unless ( $self->{"$field$type"} ) { - $self->{"$field$type"} = RT::Links->new( $self->CurrentUser ); - # at least to myself - $self->{"$field$type"}->Limit( FIELD => $field, - VALUE => $self->URI, - ENTRYAGGREGATOR => 'OR' ); - $self->{"$field$type"}->Limit( FIELD => 'Type', - VALUE => $type ) - if ($type); - } - return ( $self->{"$field$type"} ); -} - - - - -=head2 FormatType - -Takes a Type and returns a string that is more human readable. - -=cut - -sub FormatType{ - my $self = shift; - my %args = ( Type => '', - @_ - ); - $args{Type} =~ s/([A-Z])/" " . lc $1/ge; - $args{Type} =~ s/^\s+//; - return $args{Type}; -} - - - - -=head2 FormatLink - -Takes either a Target or a Base and returns a string of human friendly text. - -=cut - -sub FormatLink { - my $self = shift; - my %args = ( Object => undef, - FallBack => '', - @_ - ); - my $text = "URI " . $args{FallBack}; - if ($args{Object} && $args{Object}->isa("RT::Ticket")) { - $text = "Ticket " . $args{Object}->id; - } - return $text; -} - - - -=head2 _AddLink - -Takes a paramhash of Type and one of Base or Target. Adds that link to this object. - -Returns C<link id>, C<message> and C<exist> flag. - - -=cut - -sub _AddLink { - my $self = shift; - my %args = ( Target => '', - Base => '', - Type => '', - Silent => undef, - @_ ); - - - # Remote_link is the URI of the object that is not this ticket - my $remote_link; - my $direction; - - if ( $args{'Base'} and $args{'Target'} ) { - $RT::Logger->debug( "$self tried to create a link. both base and target were specified" ); - return ( 0, $self->loc("Can't specify both base and target") ); - } - elsif ( $args{'Base'} ) { - $args{'Target'} = $self->URI(); - $remote_link = $args{'Base'}; - $direction = 'Target'; - } - elsif ( $args{'Target'} ) { - $args{'Base'} = $self->URI(); - $remote_link = $args{'Target'}; - $direction = 'Base'; - } - else { - return ( 0, $self->loc('Either base or target must be specified') ); - } - - # Check if the link already exists - we don't want duplicates - use RT::Link; - my $old_link = RT::Link->new( $self->CurrentUser ); - $old_link->LoadByParams( Base => $args{'Base'}, - Type => $args{'Type'}, - Target => $args{'Target'} ); - if ( $old_link->Id ) { - $RT::Logger->debug("$self Somebody tried to duplicate a link"); - return ( $old_link->id, $self->loc("Link already exists"), 1 ); - } - - # }}} - - - # Storing the link in the DB. - my $link = RT::Link->new( $self->CurrentUser ); - my ($linkid, $linkmsg) = $link->Create( Target => $args{Target}, - Base => $args{Base}, - Type => $args{Type} ); - - unless ($linkid) { - $RT::Logger->error("Link could not be created: ".$linkmsg); - return ( 0, $self->loc("Link could not be created") ); - } - - my $basetext = $self->FormatLink(Object => $link->BaseObj, - FallBack => $args{Base}); - my $targettext = $self->FormatLink(Object => $link->TargetObj, - FallBack => $args{Target}); - my $typetext = $self->FormatType(Type => $args{Type}); - my $TransString = - "$basetext $typetext $targettext."; - return ( $linkid, $TransString ) ; -} - - - -=head2 _DeleteLink - -Delete a link. takes a paramhash of Base, Target and Type. -Either Base or Target must be null. The null value will -be replaced with this ticket's id - -=cut - -sub _DeleteLink { - my $self = shift; - my %args = ( - Base => undef, - Target => undef, - Type => undef, - @_ - ); - - #we want one of base and target. we don't care which - #but we only want _one_ - - my $direction; - my $remote_link; - - if ( $args{'Base'} and $args{'Target'} ) { - $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target"); - return ( 0, $self->loc("Can't specify both base and target") ); - } - elsif ( $args{'Base'} ) { - $args{'Target'} = $self->URI(); - $remote_link = $args{'Base'}; - $direction = 'Target'; - } - elsif ( $args{'Target'} ) { - $args{'Base'} = $self->URI(); - $remote_link = $args{'Target'}; - $direction='Base'; - } - else { - $RT::Logger->error("Base or Target must be specified"); - return ( 0, $self->loc('Either base or target must be specified') ); - } - - my $link = RT::Link->new( $self->CurrentUser ); - $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} ); - - - $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} ); - #it's a real link. - - if ( $link->id ) { - my $basetext = $self->FormatLink(Object => $link->BaseObj, - FallBack => $args{Base}); - my $targettext = $self->FormatLink(Object => $link->TargetObj, - FallBack => $args{Target}); - my $typetext = $self->FormatType(Type => $args{Type}); - my $linkid = $link->id; - $link->Delete(); - my $TransString = "$basetext no longer $typetext $targettext."; - return ( 1, $TransString); - } - - #if it's not a link we can find - else { - $RT::Logger->debug("Couldn't find that link"); - return ( 0, $self->loc("Link not found") ); - } -} - - -=head1 LockForUpdate - -In a database transaction, gains an exclusive lock on the row, to -prevent race conditions. On SQLite, this is a "RESERVED" lock on the -entire database. - -=cut - -sub LockForUpdate { - my $self = shift; - - my $pk = $self->_PrimaryKey; - my $id = @_ ? $_[0] : $self->$pk; - $self->_expire if $self->isa("DBIx::SearchBuilder::Record::Cachable"); - if (RT->Config->Get('DatabaseType') eq "SQLite") { - # SQLite does DB-level locking, upgrading the transaction to - # "RESERVED" on the first UPDATE/INSERT/DELETE. Do a no-op - # UPDATE to force the upgade. - return RT->DatabaseHandle->dbh->do( - "UPDATE " .$self->Table. - " SET $pk = $pk WHERE 1 = 0"); - } else { - return $self->_LoadFromSQL( - "SELECT * FROM ".$self->Table - ." WHERE $pk = ? FOR UPDATE", - $id, - ); - } -} - -=head2 _NewTransaction PARAMHASH - -Private function to create a new RT::Transaction object for this ticket update - -=cut - -sub _NewTransaction { - my $self = shift; - my %args = ( - TimeTaken => undef, - Type => undef, - OldValue => undef, - NewValue => undef, - OldReference => undef, - NewReference => undef, - ReferenceType => undef, - Data => undef, - Field => undef, - MIMEObj => undef, - ActivateScrips => 1, - CommitScrips => 1, - SquelchMailTo => undef, - CustomFields => {}, - @_ - ); - - my $in_txn = RT->DatabaseHandle->TransactionDepth; - RT->DatabaseHandle->BeginTransaction unless $in_txn; - - $self->LockForUpdate; - - my $old_ref = $args{'OldReference'}; - my $new_ref = $args{'NewReference'}; - my $ref_type = $args{'ReferenceType'}; - if ($old_ref or $new_ref) { - $ref_type ||= ref($old_ref) || ref($new_ref); - if (!$ref_type) { - $RT::Logger->error("Reference type not specified for transaction"); - return; - } - $old_ref = $old_ref->Id if ref($old_ref); - $new_ref = $new_ref->Id if ref($new_ref); - } - - require RT::Transaction; - my $trans = RT::Transaction->new( $self->CurrentUser ); - my ( $transaction, $msg ) = $trans->Create( - ObjectId => $self->Id, - ObjectType => ref($self), - TimeTaken => $args{'TimeTaken'}, - Type => $args{'Type'}, - Data => $args{'Data'}, - Field => $args{'Field'}, - NewValue => $args{'NewValue'}, - OldValue => $args{'OldValue'}, - NewReference => $new_ref, - OldReference => $old_ref, - ReferenceType => $ref_type, - MIMEObj => $args{'MIMEObj'}, - ActivateScrips => $args{'ActivateScrips'}, - CommitScrips => $args{'CommitScrips'}, - SquelchMailTo => $args{'SquelchMailTo'}, - CustomFields => $args{'CustomFields'}, - ); - - # Rationalize the object since we may have done things to it during the caching. - $self->Load($self->Id); - - $RT::Logger->warning($msg) unless $transaction; - - $self->_SetLastUpdated; - - if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) { - $self->_UpdateTimeTaken( $args{'TimeTaken'} ); - } - if ( RT->Config->Get('UseTransactionBatch') and $transaction ) { - push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'}; - } - - RT->DatabaseHandle->Commit unless $in_txn; - - return ( $transaction, $msg, $trans ); -} - - - -=head2 Transactions - - Returns an RT::Transactions object of all transactions on this record object - -=cut - -sub Transactions { - my $self = shift; - - use RT::Transactions; - my $transactions = RT::Transactions->new( $self->CurrentUser ); - - #If the user has no rights, return an empty object - $transactions->Limit( - FIELD => 'ObjectId', - VALUE => $self->id, - ); - $transactions->Limit( - FIELD => 'ObjectType', - VALUE => ref($self), - ); - - return ($transactions); -} - -# - -sub CustomFields { - my $self = shift; - my $cfs = RT::CustomFields->new( $self->CurrentUser ); - - $cfs->SetContextObject( $self ); - # XXX handle multiple types properly - $cfs->LimitToLookupType( $self->CustomFieldLookupType ); - $cfs->LimitToGlobalOrObjectId( $self->CustomFieldLookupId ); - $cfs->ApplySortOrder; - - return $cfs; -} - -# TODO: This _only_ works for RT::Foo classes. it doesn't work, for -# example, for RT::IR::Foo classes. - -sub CustomFieldLookupId { - my $self = shift; - my $lookup = shift || $self->CustomFieldLookupType; - my @classes = ($lookup =~ /RT::(\w+)-/g); - - # Work on "RT::Queue", for instance - return $self->Id unless @classes; - - my $object = $self; - # Save a ->Load call by not calling ->FooObj->Id, just ->Foo - my $final = shift @classes; - foreach my $class (reverse @classes) { - my $method = "${class}Obj"; - $object = $object->$method; - } - - my $id = $object->$final; - unless (defined $id) { - my $method = "${final}Obj"; - $id = $object->$method->Id; - } - return $id; -} - - -=head2 CustomFieldLookupType - -Returns the path RT uses to figure out which custom fields apply to this object. - -=cut - -sub CustomFieldLookupType { - my $self = shift; - return ref($self) || $self; -} - - -=head2 AddCustomFieldValue { Field => FIELD, Value => VALUE } - -VALUE should be a string. FIELD can be any identifier of a CustomField -supported by L</LoadCustomFieldByIdentifier> method. - -Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field, -deletes the old value. -If VALUE is not a valid value for the custom field, returns -(0, 'Error message' ) otherwise, returns ($id, 'Success Message') where -$id is ID of created L<ObjectCustomFieldValue> object. - -=cut - -sub AddCustomFieldValue { - my $self = shift; - $self->_AddCustomFieldValue(@_); -} - -sub _AddCustomFieldValue { - my $self = shift; - my %args = ( - Field => undef, - Value => undef, - LargeContent => undef, - ContentType => undef, - RecordTransaction => 1, - @_ - ); - - my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'}); - unless ( $cf->Id ) { - return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) ); - } - - my $OCFs = $self->CustomFields; - $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id ); - unless ( $OCFs->Count ) { - return ( - 0, - $self->loc( - "Custom field [_1] does not apply to this object", - ref $args{'Field'} ? $args{'Field'}->id : $args{'Field'} - ) - ); - } - - # empty string is not correct value of any CF, so undef it - foreach ( qw(Value LargeContent) ) { - $args{ $_ } = undef if defined $args{ $_ } && !length $args{ $_ }; - } - - unless ( $cf->ValidateValue( $args{'Value'} ) ) { - return ( 0, $self->loc("Invalid value for custom field") ); - } - - # If the custom field only accepts a certain # of values, delete the existing - # value and record a "changed from foo to bar" transaction - unless ( $cf->UnlimitedValues ) { - - # Load up a ObjectCustomFieldValues object for this custom field and this ticket - my $values = $cf->ValuesForObject($self); - - # We need to whack any old values here. In most cases, the custom field should - # only have one value to delete. In the pathalogical case, this custom field - # used to be a multiple and we have many values to whack.... - my $cf_values = $values->Count; - - if ( $cf_values > $cf->MaxValues ) { - my $i = 0; #We want to delete all but the max we can currently have , so we can then - # execute the same code to "change" the value from old to new - while ( my $value = $values->Next ) { - $i++; - if ( $i < $cf_values ) { - my ( $val, $msg ) = $cf->DeleteValueForObject( - Object => $self, - Id => $value->id, - ); - unless ($val) { - return ( 0, $msg ); - } - my ( $TransactionId, $Msg, $TransactionObj ) = - $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - OldReference => $value, - ); - } - } - $values->RedoSearch if $i; # redo search if have deleted at least one value - } - - if ( my $entry = $values->HasEntry($args{'Value'}, $args{'LargeContent'}) ) { - return $entry->id; - } - - my $old_value = $values->First; - my $old_content; - $old_content = $old_value->Content if $old_value; - - my ( $new_value_id, $value_msg ) = $cf->AddValueForObject( - Object => $self, - Content => $args{'Value'}, - LargeContent => $args{'LargeContent'}, - ContentType => $args{'ContentType'}, - ); - - unless ( $new_value_id ) { - return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg ) ); - } - - my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser ); - $new_value->Load( $new_value_id ); - - # now that adding the new value was successful, delete the old one - if ( $old_value ) { - my ( $val, $msg ) = $old_value->Delete(); - return ( 0, $msg ) unless $val; - } - - if ( $args{'RecordTransaction'} ) { - my ( $TransactionId, $Msg, $TransactionObj ) = - $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - OldReference => $old_value, - NewReference => $new_value, - ); - } - - my $new_content = $new_value->Content; - - # For datetime, we need to display them in "human" format in result message - #XXX TODO how about date without time? - if ($cf->Type eq 'DateTime') { - my $DateObj = RT::Date->new( $self->CurrentUser ); - $DateObj->Set( - Format => 'ISO', - Value => $new_content, - ); - $new_content = $DateObj->AsString; - - if ( defined $old_content && length $old_content ) { - $DateObj->Set( - Format => 'ISO', - Value => $old_content, - ); - $old_content = $DateObj->AsString; - } - } - - unless ( defined $old_content && length $old_content ) { - return ( $new_value_id, $self->loc( "[_1] [_2] added", $cf->Name, $new_content )); - } - elsif ( !defined $new_content || !length $new_content ) { - return ( $new_value_id, - $self->loc( "[_1] [_2] deleted", $cf->Name, $old_content ) ); - } - else { - return ( $new_value_id, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content, $new_content)); - } - - } - - # otherwise, just add a new value and record "new value added" - else { - if ( !$cf->Repeated ) { - my $values = $cf->ValuesForObject($self); - if ( my $entry = $values->HasEntry($args{'Value'}, $args{'LargeContent'}) ) { - return $entry->id; - } - } - - my ($new_value_id, $msg) = $cf->AddValueForObject( - Object => $self, - Content => $args{'Value'}, - LargeContent => $args{'LargeContent'}, - ContentType => $args{'ContentType'}, - ); - - unless ( $new_value_id ) { - return ( 0, $self->loc( "Could not add new custom field value: [_1]", $msg ) ); - } - if ( $args{'RecordTransaction'} ) { - my ( $tid, $msg ) = $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - NewReference => $new_value_id, - ReferenceType => 'RT::ObjectCustomFieldValue', - ); - unless ( $tid ) { - return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $msg ) ); - } - } - return ( $new_value_id, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name ) ); - } -} - - - -=head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE } - -Deletes VALUE as a value of CustomField FIELD. - -VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue. - -If VALUE is not a valid value for the custom field, returns -(0, 'Error message' ) otherwise, returns (1, 'Success Message') - -=cut - -sub DeleteCustomFieldValue { - my $self = shift; - my %args = ( - Field => undef, - Value => undef, - ValueId => undef, - @_ - ); - - my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'}); - unless ( $cf->Id ) { - return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) ); - } - - my ( $val, $msg ) = $cf->DeleteValueForObject( - Object => $self, - Id => $args{'ValueId'}, - Content => $args{'Value'}, - ); - unless ($val) { - return ( 0, $msg ); - } - - my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - OldReference => $val, - ReferenceType => 'RT::ObjectCustomFieldValue', - ); - unless ($TransactionId) { - return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) ); - } - - my $old_value = $TransactionObj->OldValue; - # For datetime, we need to display them in "human" format in result message - if ( $cf->Type eq 'DateTime' ) { - my $DateObj = RT::Date->new( $self->CurrentUser ); - $DateObj->Set( - Format => 'ISO', - Value => $old_value, - ); - $old_value = $DateObj->AsString; - } - return ( - $TransactionId, - $self->loc( - "[_1] is no longer a value for custom field [_2]", - $old_value, $cf->Name - ) - ); -} - - - -=head2 FirstCustomFieldValue FIELD - -Return the content of the first value of CustomField FIELD for this ticket -Takes a field id or name - -=cut - -sub FirstCustomFieldValue { - my $self = shift; - my $field = shift; - - my $values = $self->CustomFieldValues( $field ); - return undef unless my $first = $values->First; - return $first->Content; -} - -=head2 CustomFieldValuesAsString FIELD - -Return the content of the CustomField FIELD for this ticket. -If this is a multi-value custom field, values will be joined with newlines. - -Takes a field id or name as the first argument - -Takes an optional Separator => "," second and third argument -if you want to join the values using something other than a newline - -=cut - -sub CustomFieldValuesAsString { - my $self = shift; - my $field = shift; - my %args = @_; - my $separator = $args{Separator} || "\n"; - - my $values = $self->CustomFieldValues( $field ); - return join ($separator, grep { defined $_ } - map { $_->Content } @{$values->ItemsArrayRef}); -} - - - -=head2 CustomFieldValues FIELD - -Return a ObjectCustomFieldValues object of all values of the CustomField whose -id or Name is FIELD for this record. - -Returns an RT::ObjectCustomFieldValues object - -=cut - -sub CustomFieldValues { - my $self = shift; - my $field = shift; - - if ( $field ) { - my $cf = $self->LoadCustomFieldByIdentifier( $field ); - - # we were asked to search on a custom field we couldn't find - unless ( $cf->id ) { - $RT::Logger->warning("Couldn't load custom field by '$field' identifier"); - return RT::ObjectCustomFieldValues->new( $self->CurrentUser ); - } - return ( $cf->ValuesForObject($self) ); - } - - # we're not limiting to a specific custom field; - my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser ); - $ocfs->LimitToObject( $self ); - return $ocfs; -} - -=head2 LoadCustomFieldByIdentifier IDENTIFER - -Find the custom field has id or name IDENTIFIER for this object. - -If no valid field is found, returns an empty RT::CustomField object. - -=cut - -sub LoadCustomFieldByIdentifier { - my $self = shift; - my $field = shift; - - my $cf; - if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) { - $cf = RT::CustomField->new($self->CurrentUser); - $cf->SetContextObject( $self ); - $cf->LoadById( $field->id ); - } - elsif ($field =~ /^\d+$/) { - $cf = RT::CustomField->new($self->CurrentUser); - $cf->SetContextObject( $self ); - $cf->LoadById($field); - } else { - - my $cfs = $self->CustomFields($self->CurrentUser); - $cfs->SetContextObject( $self ); - $cfs->Limit(FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0); - $cf = $cfs->First || RT::CustomField->new($self->CurrentUser); - } - return $cf; -} - -sub ACLEquivalenceObjects { } - -sub BasicColumns { } - -sub WikiBase { - return RT->Config->Get('WebPath'). "/index.html?q="; -} - -RT::Base->_ImportOverlays(); - -1; |