X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FCustomField_Overlay.pm;h=5e868d1c588133e112464f23abc7b8fbb81f82da;hp=8f7c8bd555005211b14d2986732872b86ce06f23;hb=90edd8a914fd484e649fb0aa051dce7927bd6881;hpb=9509e5bfb7f9331303153cac24d7bfecbe2ea9f1 diff --git a/rt/lib/RT/CustomField_Overlay.pm b/rt/lib/RT/CustomField_Overlay.pm index 8f7c8bd55..5e868d1c5 100644 --- a/rt/lib/RT/CustomField_Overlay.pm +++ b/rt/lib/RT/CustomField_Overlay.pm @@ -1,40 +1,40 @@ # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC -# -# +# +# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# +# # (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/copyleft/gpl.html. -# -# +# 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 @@ -43,71 +43,87 @@ # 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 }}} + package RT::CustomField; use strict; no warnings qw(redefine); -use vars qw(%FieldTypes $RIGHTS %FRIENDLY_OBJECT_TYPES); - use RT::CustomFieldValues; +use RT::ObjectCustomFields; use RT::ObjectCustomFieldValues; -%FieldTypes = ( +our %FieldTypes = ( Select => [ - 'Select multiple values', # loc - 'Select one value', # loc - 'Select up to [_1] values', # loc + 'Select multiple values', # loc + 'Select one value', # loc + 'Select up to [_1] values', # loc ], Freeform => [ - 'Enter multiple values', # loc - 'Enter one value', # loc - 'Enter up to [_1] values', # loc + 'Enter multiple values', # loc + 'Enter one value', # loc + 'Enter up to [_1] values', # loc ], Text => [ - 'Fill in multiple text areas', # loc - 'Fill in one text area', # loc + 'Fill in multiple text areas', # loc + 'Fill in one text area', # loc 'Fill in up to [_1] text areas',# loc ], Wikitext => [ - 'Fill in multiple wikitext areas', # loc - 'Fill in one wikitext area', # loc + 'Fill in multiple wikitext areas', # loc + 'Fill in one wikitext area', # loc 'Fill in up to [_1] wikitext areas',# loc ], Image => [ - 'Upload multiple images', # loc - 'Upload one image', # loc - 'Upload up to [_1] images', # loc + 'Upload multiple images', # loc + 'Upload one image', # loc + 'Upload up to [_1] images', # loc ], Binary => [ - 'Upload multiple files', # loc - 'Upload one file', # loc - 'Upload up to [_1] files', # loc + 'Upload multiple files', # loc + 'Upload one file', # loc + 'Upload up to [_1] files', # loc ], Combobox => [ - 'Combobox: Select or enter multiple values', # loc - 'Combobox: Select or enter one value', # loc - 'Combobox: Select or enter up to [_1] values', # loc + 'Combobox: Select or enter multiple values', # loc + 'Combobox: Select or enter one value', # loc + 'Combobox: Select or enter up to [_1] values', # loc + ], + Autocomplete => [ + 'Enter multiple values with autocompletion', # loc + 'Enter one value with autocompletion', # loc + 'Enter up to [_1] values with autocompletion', # loc + ], + Date => [ + 'Select multiple dates', # loc + 'Select date', # loc + 'Select up to [_1] dates', # loc + ], + TimeValue => [ + 'Enter multiple time values (UNSUPPORTED)', + 'Enter a time value', + 'Enter [_1] time values (UNSUPPORTED)', ], ); -%FRIENDLY_OBJECT_TYPES = (); +our %FRIENDLY_OBJECT_TYPES = (); RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc RT::CustomField->_ForObjectType( 'RT::User' => "Users", ); #loc +RT::CustomField->_ForObjectType( 'RT::Queue' => "Queues", ); #loc RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", ); #loc -$RIGHTS = { +our $RIGHTS = { SeeCustomField => 'See custom fields', # loc_pair AdminCustomField => 'Create, delete and modify custom fields', # loc_pair + AdminCustomFieldValues => 'Create, delete and modify custom fields values', # loc_pair ModifyCustomField => 'Add, delete and modify custom field values for objects' #loc_pair - }; # Tell RT::ACE that this sort of object can get acls granted @@ -117,23 +133,34 @@ foreach my $right ( keys %{$RIGHTS} ) { $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right; } +=head2 AddRights C, C [, ...] + +Adds the given rights to the list of possible rights. This method +should be called during server startup, not at runtime. + +=cut + +sub AddRights { + my $self = shift; + my %new = @_; + $RIGHTS = { %$RIGHTS, %new }; + %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES, + map { lc($_) => $_ } keys %new); +} + sub AvailableRights { my $self = shift; - return($RIGHTS); + return $RIGHTS; } =head1 NAME - RT::CustomField_Overlay + RT::CustomField_Overlay - overlay for RT::CustomField =head1 DESCRIPTION =head1 'CORE' METHODS -=cut - - - =head2 Create PARAMHASH Create takes a hash of values and creates a row in the database: @@ -148,49 +175,48 @@ Create takes a hash of values and creates a row in the database: varchar(255) 'LookupType'. smallint(6) 'Disabled'. - 'LookupType' is generally the result of either - RT::Ticket->CustomFieldLookupType or RT::Transaction->CustomFieldLookupType +C is generally the result of either +CCustomFieldLookupType> or CCustomFieldLookupType>. =cut - - - sub Create { my $self = shift; - my %args = ( - Name => '', - Type => '', - MaxValues => '0', - Pattern => '', - Description => '', - Disabled => '0', - LookupType => '', - Repeated => '0', - - @_); - - unless ($self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField')) { + my %args = ( + Name => '', + Type => '', + MaxValues => 0, + Pattern => '', + Description => '', + Disabled => 0, + LookupType => '', + Repeated => 0, + LinkValueTo => '', + IncludeContentForValue => '', + @_, + ); + + unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) { return (0, $self->loc('Permission Denied')); } - - if ($args{TypeComposite}) { - @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2); + if ( $args{TypeComposite} ) { + @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2); } - elsif ($args{Type} =~ s/(?:(Single)|Multiple)$//) { - # old style Type string - $args{'MaxValues'} = $1 ? 1 : 0; + elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) { + # old style Type string + $args{'MaxValues'} = $1 ? 1 : 0; } - + $args{'MaxValues'} = int $args{'MaxValues'}; + if ( !exists $args{'Queue'}) { - # do nothing -- things below are strictly backward compat + # do nothing -- things below are strictly backward compat } elsif ( ! $args{'Queue'} ) { unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) { return ( 0, $self->loc('Permission Denied') ); } - $args{'LookupType'} = 'RT::Queue-RT::Ticket'; + $args{'LookupType'} = 'RT::Queue-RT::Ticket'; } else { my $queue = RT::Queue->new($self->CurrentUser); @@ -205,32 +231,55 @@ sub Create { $args{'Queue'} = $queue->Id; } - my ($ok, $msg) = $self->_IsValidRegex($args{'Pattern'}); - if (!$ok) { - return (0, $self->loc("Invalid pattern: [_1]", $msg)); + my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} ); + return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok; + + if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) { + $RT::Logger->warning("Support for 'multiple' Texts or Comboboxes is not implemented"); + $args{'MaxValues'} = 1; } - my $rv = $self->SUPER::Create( - Name => $args{'Name'}, - Type => $args{'Type'}, - MaxValues => $args{'MaxValues'}, - Pattern => $args{'Pattern'}, - Description => $args{'Description'}, - Disabled => $args{'Disabled'}, - LookupType => $args{'LookupType'}, - Repeated => $args{'Repeated'}, -); + (my $rv, $msg) = $self->SUPER::Create( + Name => $args{'Name'}, + Type => $args{'Type'}, + MaxValues => $args{'MaxValues'}, + Pattern => $args{'Pattern'}, + Description => $args{'Description'}, + Disabled => $args{'Disabled'}, + LookupType => $args{'LookupType'}, + Repeated => $args{'Repeated'}, + ); - return $rv unless exists $args{'Queue'}; + if ( exists $args{'LinkValueTo'}) { + $self->SetLinkValueTo($args{'LinkValueTo'}); + } + + if ( exists $args{'IncludeContentForValue'}) { + $self->SetIncludeContentForValue($args{'IncludeContentForValue'}); + } + + if ( exists $args{'ValuesClass'} ) { + $self->SetValuesClass( $args{'ValuesClass'} ); + } + + if ( exists $args{'BasedOn'} ) { + $self->SetBasedOn( $args{'BasedOn'} ); + } + + if ( exists $args{'UILocation'} ) { + $self->SetUILocation( $args{'UILocation'} ); + } + + return ($rv, $msg) unless exists $args{'Queue'}; # Compat code -- create a new ObjectCustomField mapping - my $OCF = RT::ObjectCustomField->new($self->CurrentUser); + my $OCF = RT::ObjectCustomField->new( $self->CurrentUser ); $OCF->Create( - CustomField => $self->Id, - ObjectId => $args{'Queue'}, + CustomField => $self->Id, + ObjectId => $args{'Queue'}, ); - return $rv; + return ($rv, $msg); } =head2 Load ID/NAME @@ -239,25 +288,27 @@ Load a custom field. If the value handed in is an integer, load by custom field =cut - sub Load { my $self = shift; - my $id = shift; + my $id = shift || ''; - if ($id =~ /^\d+$/) { - return ($self->SUPER::Load($id)); + if ( $id =~ /^\d+$/ ) { + return $self->SUPER::Load( $id ); } else { - return($self->LoadByName(Name => $id)); + return $self->LoadByName( Name => $id ); } } # {{{ sub LoadByName -=head2 LoadByName (Queue => QUEUEID, Name => NAME) +=head2 LoadByName (Queue => QUEUEID, Name => NAME) Loads the Custom field named NAME. +Will load a Disabled Custom Field even if there is a non-disabled Custom Field +with the same Name. + If a Queue parameter is specified, only look for ticket custom fields tied to that Queue. If the Queue parameter is '0', look for global ticket custom fields. @@ -281,91 +332,82 @@ sub LoadByName { @_, ); + unless ( defined $args{'Name'} && length $args{'Name'} ) { + $RT::Logger->error("Couldn't load Custom Field without Name"); + return wantarray ? (0, $self->loc("No name provided")) : 0; + } + # if we're looking for a queue by name, make it a number - if (defined $args{'Queue'} && $args{'Queue'} !~ /^\d+$/) { - my $QueueObj = RT::Queue->new($self->CurrentUser); - $QueueObj->Load($args{'Queue'}); - $args{'Queue'} = $QueueObj->Id; + if ( defined $args{'Queue'} && $args{'Queue'} =~ /\D/ ) { + my $QueueObj = RT::Queue->new( $self->CurrentUser ); + $QueueObj->Load( $args{'Queue'} ); + $args{'Queue'} = $QueueObj->Id; } # XXX - really naive implementation. Slow. - not really. still just one query - my $CFs = RT::CustomFields->new($self->CurrentUser); - - $CFs->Limit( FIELD => 'Name', VALUE => $args{'Name'} ); + my $CFs = RT::CustomFields->new( $self->CurrentUser ); + $CFs->SetContextObject( $self->ContextObject ); + my $field = $args{'Name'} =~ /\D/? 'Name' : 'id'; + $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0); # Don't limit to queue if queue is 0. Trying to do so breaks # RT::Group type CFs. - if (defined $args{'Queue'}) { - $CFs->LimitToQueue( $args{'Queue'} ); + if ( defined $args{'Queue'} ) { + $CFs->LimitToQueue( $args{'Queue'} ); } - # When loading by name, it's ok if they're disabled. That's not a big deal. - $CFs->{'find_disabled_rows'}=1; + # When loading by name, we _can_ load disabled fields, but prefer + # non-disabled fields. + $CFs->FindAllRows; + $CFs->OrderByCols( + { FIELD => "Disabled", ORDER => 'ASC' }, + ); # We only want one entry. $CFs->RowsPerPage(1); - unless ($CFs->First) { - return(0); - } - return($self->Load($CFs->First->id)); + # version before 3.8 just returns 0, so we need to test if wantarray to be + # backward compatible. + return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First; + + return $self->LoadById( $first->id ); } # }}} # {{{ Dealing with custom field values -=begin testing - -use_ok(RT::CustomField); -ok(my $cf = RT::CustomField->new($RT::SystemUser)); -ok(my ($id, $msg)= $cf->Create( Name => 'TestingCF', - Queue => '0', - SortOrder => '1', - Description => 'A Testing custom field', - Type=> 'SelectSingle'), 'Created a global CustomField'); -ok($id != 0, 'Global custom field correctly created'); -ok ($cf->SingleValue); -is($cf->Type, 'Select'); -is($cf->MaxValues, 1); - -my ($val, $msg) = $cf->SetMaxValues('0'); -ok($val, $msg); -is($cf->Type, 'Select'); -is($cf->MaxValues, 0); -ok(!$cf->SingleValue ); -ok(my ($bogus_val, $bogus_msg) = $cf->SetType('BogusType') , "Trying to set a custom field's type to a bogus type"); -ok($bogus_val == 0, "Unable to set a custom field's type to a bogus type"); - -ok(my $bad_cf = RT::CustomField->new($RT::SystemUser)); -ok(my ($bad_id, $bad_msg)= $cf->Create( Name => 'TestingCF-bad', - Queue => '0', - SortOrder => '1', - Description => 'A Testing custom field with a bogus Type', - Type=> 'SelectSingleton'), 'Created a global CustomField with a bogus type'); -ok($bad_id == 0, 'Global custom field correctly decided to not create a cf with a bogus type '); - -=end testing + +=head2 Custom field values + +=head3 Values FIELD + +Return a object (collection) of all acceptable values for this Custom Field. +Class of the object can vary and depends on the return value +of the C method. =cut -# {{{ AddValue +*ValuesObj = \&Values; -=head2 AddValue HASH +sub Values { + my $self = shift; -Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder + my $class = $self->ValuesClass || 'RT::CustomFieldValues'; + eval "require $class" or die "$@"; + my $cf_values = $class->new( $self->CurrentUser ); + # if the user has no rights, return an empty object + if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) { + $cf_values->LimitToCustomField( $self->Id ); + } + return ($cf_values); +} -=begin testing +# {{{ AddValue -ok(my $cf = RT::CustomField->new($RT::SystemUser)); -$cf->Load(1); -ok($cf->Id == 1); -ok(my ($val,$msg) = $cf->AddValue(Name => 'foo' , Description => 'TestCFValue', SortOrder => '6')); -ok($val != 0); -ok (my ($delval, $delmsg) = $cf->DeleteValue($val)); -ok ($delval,"Deleting a cf value: $delmsg"); +=head3 AddValue HASH -=end testing +Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder =cut @@ -373,17 +415,17 @@ sub AddValue { my $self = shift; my %args = @_; - unless ($self->CurrentUserHasRight('AdminCustomField')) { + unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) { return (0, $self->loc('Permission Denied')); } # allow zero value if ( !defined $args{'Name'} || $args{'Name'} eq '' ) { - return(0, $self->loc("Can't add a custom field value without a name")); + return (0, $self->loc("Can't add a custom field value without a name")); } - my $newval = RT::CustomFieldValue->new($self->CurrentUser); - return($newval->Create(%args, CustomField => $self->Id)); + my $newval = RT::CustomFieldValue->new( $self->CurrentUser ); + return $newval->Create( %args, CustomField => $self->Id ); } @@ -391,203 +433,127 @@ sub AddValue { # {{{ DeleteValue -=head2 DeleteValue ID +=head3 DeleteValue ID -Deletes a value from this custom field by id. +Deletes a value from this custom field by id. -Does not remove this value for any article which has had it selected +Does not remove this value for any article which has had it selected =cut sub DeleteValue { - my $self = shift; + my $self = shift; my $id = shift; - unless ($self->CurrentUserHasRight('AdminCustomField')) { + unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) { return (0, $self->loc('Permission Denied')); } - my $val_to_del = RT::CustomFieldValue->new($self->CurrentUser); - $val_to_del->Load($id); - unless ($val_to_del->Id) { - return (0, $self->loc("Couldn't find that value")); - } - unless ($val_to_del->CustomField == $self->Id) { - return (0, $self->loc("That is not a value for this custom field")); - } + my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser ); + $val_to_del->Load( $id ); + unless ( $val_to_del->Id ) { + return (0, $self->loc("Couldn't find that value")); + } + unless ( $val_to_del->CustomField == $self->Id ) { + return (0, $self->loc("That is not a value for this custom field")); + } - my $retval = $val_to_del->Delete(); - if ($retval) { - return ($retval, $self->loc("Custom field value deleted")); - } else { - return(0, $self->loc("Custom field value could not be deleted")); + my $retval = $val_to_del->Delete; + unless ( $retval ) { + return (0, $self->loc("Custom field value could not be deleted")); } + return ($retval, $self->loc("Custom field value deleted")); } # }}} -# {{{ Values - -=head2 Values FIELD -Return a CustomFieldeValues object of all acceptable values for this Custom Field. +=head2 ValidateQueue Queue +Make sure that the queue specified is a valid queue name =cut -*ValuesObj = \&Values; - -sub Values { +sub ValidateQueue { my $self = shift; + my $id = shift; - my $cf_values = RT::CustomFieldValues->new($self->CurrentUser); - # if the user has no rights, return an empty object - if ($self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) { - $cf_values->LimitToCustomField($self->Id); - } - return ($cf_values); -} - -# }}} - -# }}} - -# {{{ Ticket related routines - -# {{{ ValuesForTicket - -=head2 ValuesForTicket TICKET - -Returns a RT::ObjectCustomFieldValues object of this Field's values for TICKET. -TICKET is a ticket id. - -This is deprecated -- use ValuesForObject instead. - - -=cut - -sub ValuesForTicket { - my $self = shift; - my $ticket_id = shift; - - $RT::Logger->debug( ref($self) . " -> ValuesForTicket deprecated in favor of ValuesForObject at (". join(":",caller).")"); - my $ticket = RT::Ticket->new($self->CurrentUser); - $ticket->Load($ticket_id); + return undef unless defined $id; + # 0 means "Global" null would _not_ be ok. + return 1 if $id eq '0'; - return $self->ValuesForObject($ticket); + my $q = RT::Queue->new( $RT::SystemUser ); + $q->Load( $id ); + return undef unless $q->id; + return 1; } -# }}} - -# {{{ AddValueForTicket -=head2 AddValueForTicket HASH +# {{{ Types -Adds a custom field value for a ticket. Takes a param hash of Ticket and Content +=head2 Types -This is deprecated -- use AddValueForObject instead. +Retuns an array of the types of CustomField that are supported =cut -sub AddValueForTicket { - my $self = shift; - my %args = ( Ticket => undef, - Content => undef, - @_ ); - $RT::Logger->debug( ref($self) . " -> AddValueForTicket deprecated in favor of AddValueForObject at (". join(":",caller).")"); - - - my $ticket = RT::Ticket->new($self->CurrentUser); - $ticket->Load($args{'Ticket'}); - return($self->AddValueForObject(Content => $args{'Content'}, Object => $ticket,@_)); - +sub Types { + return (keys %FieldTypes); } - # }}} -# {{{ DeleteValueForTicket - -=head2 DeleteValueForTicket HASH +# {{{ IsSelectionType -Adds a custom field value for a ticket. Takes a param hash of Ticket and Content +=head2 IsSelectionType -This is deprecated -- use DeleteValueForObject instead. +Retuns a boolean value indicating whether the C method makes sense +to this Custom Field. =cut -sub DeleteValueForTicket { - my $self = shift; - my %args = ( Ticket => undef, - Content => undef, - @_ ); - - $RT::Logger->debug( ref($self) . " -> DeleteValueForTicket deprecated in favor of DeleteValueForObject at (". join(":",caller).")"); - - - my $ticket = RT::Ticket->new($self->CurrentUser); - $ticket->load($args{'Ticket'}); - return ($self->DeleteValueForObject(Object => $ticket, Content => $args{'Content'}, @_)); +sub IsSelectionType { + my $self = shift; + my $type = @_? shift : $self->Type; + return undef unless $type; + $type =~ /(?:Select|Combobox|Autocomplete)/; } # }}} -# }}} -=head2 ValidateQueue Queue - -Make sure that the queue specified is a valid queue name +=head2 IsExternalValues =cut -sub ValidateQueue { +sub IsExternalValues { my $self = shift; - my $id = shift; - - if ($id eq '0') { # 0 means "Global" null would _not_ be ok. - return (1); - } - - my $q = RT::Queue->new($RT::SystemUser); - $q->Load($id); - unless ($q->id) { - return undef; - } - return (1); - + my $selectable = $self->IsSelectionType( @_ ); + return $selectable unless $selectable; + my $class = $self->ValuesClass; + return 0 if $class eq 'RT::CustomFieldValues'; + return 1; } +sub ValuesClass { + my $self = shift; + return '' unless $self->IsSelectionType; -# {{{ Types - -=head2 Types - -Retuns an array of the types of CustomField that are supported - -=cut - -sub Types { - return (keys %FieldTypes); + my $class = $self->FirstAttribute( 'ValuesClass' ); + $class = $class->Content if $class; + return $class || 'RT::CustomFieldValues'; } -# }}} - -# {{{ IsSelectionType - -=head2 IsSelectionType - -Retuns a boolean value indicating whether the C method makes sense -to this Custom Field. - -=cut - -sub IsSelectionType { +sub SetValuesClass { my $self = shift; - $self->Type =~ /(?:Select|Combobox)/; -} + my $class = shift || 'RT::CustomFieldValues'; -# }}} + if( $class eq 'RT::CustomFieldValues' ) { + return $self->DeleteAttribute( 'ValuesClass' ); + } + return $self->SetAttribute( Name => 'ValuesClass', Content => $class ); +} =head2 FriendlyType [TYPE, MAX_VALUES] @@ -602,9 +568,10 @@ sub FriendlyType { my $type = @_ ? shift : $self->Type; my $max = @_ ? shift : $self->MaxValues; + $max = 0 unless $max; if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) { - return ( $self->loc( $friendly_type, $max ) ); + return ( $self->loc( $friendly_type, $max ) ); } else { return ( $self->loc( $type ) ); @@ -623,14 +590,6 @@ sub FriendlyTypeComposite { Takes a single string. returns true if that string is a value type of custom field -=begin testing - -ok(my $cf = RT::CustomField->new($RT::SystemUser)); -ok($cf->ValidateType('SelectSingle')); -ok($cf->ValidateType('SelectMultiple')); -ok(!$cf->ValidateType('SelectFooMultiple')); - -=end testing =cut @@ -638,12 +597,12 @@ sub ValidateType { my $self = shift; my $type = shift; - if ($type =~ s/(?:Single|Multiple)$//) { - $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")"); + if ( $type =~ s/(?:Single|Multiple)$// ) { + $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")"); } - if( $FieldTypes{$type}) { - return(1); + if ( $FieldTypes{$type} ) { + return 1; } else { return undef; @@ -655,8 +614,8 @@ sub SetType { my $self = shift; my $type = shift; if ($type =~ s/(?:(Single)|Multiple)$//) { - $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")"); - $self->SetMaxValues($1 ? 1 : 0); + $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")"); + $self->SetMaxValues($1 ? 1 : 0); } $self->SUPER::SetType($type); } @@ -693,8 +652,8 @@ sub _IsValidRegex { my $regex = shift or return (1, 'valid'); local $^W; local $@; - $SIG{__DIE__} = sub { 1 }; - $SIG{__WARN__} = sub { 1 }; + local $SIG{__DIE__} = sub { 1 }; + local $SIG{__WARN__} = sub { 1 }; if (eval { qr/$regex/; 1 }) { return (1, 'valid'); @@ -717,7 +676,7 @@ Returns false if it accepts multiple values sub SingleValue { my $self = shift; - if ($self->MaxValues == 1) { + if (($self->MaxValues||0) == 1) { return 1; } else { @@ -727,7 +686,7 @@ sub SingleValue { sub UnlimitedValues { my $self = shift; - if ($self->MaxValues == 0) { + if (($self->MaxValues||0) == 0) { return 1; } else { @@ -737,8 +696,6 @@ sub UnlimitedValues { # }}} -# {{{ sub CurrentUserHasRight - =head2 CurrentUserHasRight RIGHT Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args. @@ -750,13 +707,44 @@ sub CurrentUserHasRight { my $right = shift; return $self->CurrentUser->HasRight( - Object => $self, - Right => $right, + Object => $self, + Right => $right, ); } -# }}} +=head2 ACLEquivalenceObjects + +Returns list of objects via which users can get rights on this custom field. For custom fields +these objects can be set using L. + +=cut + +sub ACLEquivalenceObjects { + my $self = shift; + my $ctx = $self->ContextObject + or return; + return ($ctx, $ctx->ACLEquivalenceObjects); +} + +=head2 ContextObject and SetContextObject + +Set or get a context for this object. It can be ticket, queue or another object +this CF applies to. Used for ACL control, for example SeeCustomField can be granted on +queue level to allow people to see all fields applied to the queue. + +=cut + +sub SetContextObject { + my $self = shift; + return $self->{'context_object'} = shift; +} + +sub ContextObject { + my $self = shift; + return $self->{'context_object'}; +} + # {{{ sub _Set sub _Set { @@ -765,7 +753,7 @@ sub _Set { unless ( $self->CurrentUserHasRight('AdminCustomField') ) { return ( 0, $self->loc('Permission Denied') ); } - return ( $self->SUPER::_Set(@_) ); + return $self->SUPER::_Set( @_ ); } @@ -781,16 +769,18 @@ Returns its value as a string, if the user passes an ACL check =cut sub _Value { - my $self = shift; - my $field = shift; + return undef unless $self->id; # we need to do the rights check - unless ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) { - return (undef); + unless ( $self->CurrentUserHasRight('SeeCustomField') ) { + $RT::Logger->debug( + "Permission denied. User #". $self->CurrentUser->id + ." has no SeeCustomField right on CF #". $self->id + ); + return (undef); } - return ( $self->__Value($field) ); - + return $self->__Value( @_ ); } # }}} @@ -799,62 +789,39 @@ sub _Value { =head2 SetDisabled Takes a boolean. -1 will cause this custom field to no longer be avaialble for tickets. -0 will re-enable this queue +1 will cause this custom field to no longer be avaialble for objects. +0 will re-enable this field. =cut # }}} -sub Queue { - $RT::Logger->debug( ref($_[0]) . " -> Queue deprecated at (". join(":",caller).")"); - - return 0; -} - -sub SetQueue { - $RT::Logger->debug( ref($_[0]) . " -> SetQueue deprecated at (". join(":",caller).")"); - - return 0; -} - -sub QueueObj { - $RT::Logger->debug( ref($_[0]) . " -> QueueObj deprecated at (". join(":",caller).")"); - - return undef; -} - =head2 SetTypeComposite Set this custom field's type and maximum values as a composite value - =cut sub SetTypeComposite { my $self = shift; my $composite = shift; - my ($type, $max_values) = split(/-/, $composite, 2); - $self->SetType($type); - $self->SetMaxValues($max_values); -} -=head2 SetLookupType - -Autrijus: care to doc how LookupTypes work? - -=cut + my $old = $self->TypeComposite; -sub SetLookupType { - my $self = shift; - my $lookup = shift; - if ($lookup ne $self->LookupType) { - # Okay... We need to invalidate our existing relationships - my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser); - $ObjectCustomFields->LimitToCustomField($self->Id); - $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef}; + my ($type, $max_values) = split(/-/, $composite, 2); + if ( $type ne $self->Type ) { + my ($status, $msg) = $self->SetType( $type ); + return ($status, $msg) unless $status; + } + if ( ($max_values || 0) != ($self->MaxValues || 0) ) { + my ($status, $msg) = $self->SetMaxValues( $max_values ); + return ($status, $msg) unless $status; } - $self->SUPER::SetLookupType($lookup); + return 1, $self->loc( + "Type changed from '[_1]' to '[_2]'", + $self->FriendlyTypeComposite( $old ), + $self->FriendlyTypeComposite( $composite ), + ); } =head2 TypeComposite @@ -866,7 +833,7 @@ Returns a composite value composed of this object's type and maximum values sub TypeComposite { my $self = shift; - join('-', $self->Type, $self->MaxValues); + return join '-', ($self->Type || ''), ($self->MaxValues || 0); } =head2 TypeComposites @@ -877,7 +844,25 @@ Returns an array of all possible composite values for custom fields. sub TypeComposites { my $self = shift; - return grep !/(?:[Tt]ext|Combobox)-0/, map { ("$_-1", "$_-0") } $self->Types; + return grep !/(?:[Tt]ext|Combobox|Date|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types; +} + +=head2 SetLookupType + +Autrijus: care to doc how LookupTypes work? + +=cut + +sub SetLookupType { + my $self = shift; + my $lookup = shift; + if ( $lookup ne $self->LookupType ) { + # Okay... We need to invalidate our existing relationships + my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser); + $ObjectCustomFields->LimitToCustomField($self->Id); + $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef}; + } + return $self->SUPER::SetLookupType($lookup); } =head2 LookupTypes @@ -893,12 +878,14 @@ sub LookupTypes { } my @FriendlyObjectTypes = ( - "[_1] objects", # loc - "[_1]'s [_2] objects", # loc + "[_1] objects", # loc + "[_1]'s [_2] objects", # loc "[_1]'s [_2]'s [_3] objects", # loc ); -=head2 FriendlyTypeLookup +=head2 FriendlyLookupType + +Returns a localized description of the type of this custom field =cut @@ -907,7 +894,7 @@ sub FriendlyLookupType { my $lookup = shift || $self->LookupType; return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} )) - if (defined $FRIENDLY_OBJECT_TYPES{$lookup} ); + if (defined $FRIENDLY_OBJECT_TYPES{$lookup} ); my @types = map { s/^RT::// ? $self->loc($_) : $_ } grep { defined and length } @@ -916,6 +903,137 @@ sub FriendlyLookupType { return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) ); } +sub RecordClassFromLookupType { + my $self = shift; + my ($class) = ($self->LookupType =~ /^([^-]+)/); + unless ( $class ) { + $RT::Logger->error( + "Custom Field #". $self->id + ." has incorrect LookupType '". $self->LookupType ."'" + ); + return undef; + } + return $class; +} + +sub CollectionClassFromLookupType { + my $self = shift; + + my $record_class = $self->RecordClassFromLookupType; + return undef unless $record_class; + + my $collection_class; + if ( UNIVERSAL::can($record_class.'Collection', 'new') ) { + $collection_class = $record_class.'Collection'; + } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) { + $collection_class = $record_class.'es'; + } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) { + $collection_class = $record_class.'s'; + } else { + $RT::Logger->error("Can not find a collection class for record class '$record_class'"); + return undef; + } + return $collection_class; +} + +=head1 AppliedTo + +Returns collection with objects this custom field is applied to. +Class of the collection depends on L. +See all L . + +Doesn't takes into account if object is applied globally. + +=cut + +sub AppliedTo { + my $self = shift; + + my ($res, $ocfs_alias) = $self->_AppliedTo; + return $res unless $res; + + $res->Limit( + ALIAS => $ocfs_alias, + FIELD => 'id', + OPERATOR => 'IS NOT', + VALUE => 'NULL', + ); + + return $res; +} + +=head1 NotAppliedTo + +Returns collection with objects this custom field is not applied to. +Class of the collection depends on L. +See all L . + +Doesn't takes into account if object is applied globally. + +=cut + +sub NotAppliedTo { + my $self = shift; + + my ($res, $ocfs_alias) = $self->_AppliedTo; + return $res unless $res; + + $res->Limit( + ALIAS => $ocfs_alias, + FIELD => 'id', + OPERATOR => 'IS', + VALUE => 'NULL', + ); + + return $res; +} + +sub _AppliedTo { + my $self = shift; + + my ($class) = $self->CollectionClassFromLookupType; + return undef unless $class; + + my $res = $class->new( $self->CurrentUser ); + + # If CF is a Group CF, only display user-defined groups + if ( $class eq 'RT::Groups' ) { + $res->LimitToUserDefinedGroups; + } + + $res->OrderBy( FIELD => 'Name' ); + my $ocfs_alias = $res->Join( + TYPE => 'LEFT', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'ObjectCustomFields', + FIELD2 => 'ObjectId', + ); + $res->Limit( + LEFTJOIN => $ocfs_alias, + ALIAS => $ocfs_alias, + FIELD => 'CustomField', + VALUE => $self->id, + ); + return ($res, $ocfs_alias); +} + +=head2 IsApplied + +Takes object id and returns corresponding L +record if this custom field is applied to the object. Use 0 to check +if custom field is applied globally. + +=cut + +sub IsApplied { + my $self = shift; + my $id = shift; + my $ocf = RT::ObjectCustomField->new( $self->CurrentUser ); + $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 ); + return undef unless $ocf->id; + return $ocf; +} =head2 AddToObject OBJECT @@ -932,22 +1050,34 @@ sub AddToObject { my $id = $object->Id || 0; unless (index($self->LookupType, ref($object)) == 0) { - return ( 0, $self->loc('Lookup type mismatch') ); + return ( 0, $self->loc('Lookup type mismatch') ); } unless ( $object->CurrentUserHasRight('AssignCustomFields') ) { return ( 0, $self->loc('Permission Denied') ); } - my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser ); + if ( $self->IsApplied( $id ) ) { + return ( 0, $self->loc("Custom field is already applied to the object") ); + } - $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id ); - if ( $ObjectCF->Id ) { - return ( 0, $self->loc("That is already the current value") ); + if ( $id ) { + # applying locally + return (0, $self->loc("Couldn't apply custom field to an object as it's global already") ) + if $self->IsApplied( 0 ); + } + else { + my $applied = RT::ObjectCustomFields->new( $self->CurrentUser ); + $applied->LimitToCustomField( $self->id ); + while ( my $record = $applied->Next ) { + $record->Delete; + } } - my ( $oid, $msg ) = - $ObjectCF->Create( ObjectId => $id, CustomField => $self->Id ); + my $ocf = RT::ObjectCustomField->new( $self->CurrentUser ); + my ( $oid, $msg ) = $ocf->Create( + ObjectId => $id, CustomField => $self->id, + ); return ( $oid, $msg ); } @@ -960,29 +1090,26 @@ Takes an object =cut - sub RemoveFromObject { my $self = shift; my $object = shift; my $id = $object->Id || 0; unless (index($self->LookupType, ref($object)) == 0) { - return ( 0, $self->loc('Object type mismatch') ); + return ( 0, $self->loc('Object type mismatch') ); } unless ( $object->CurrentUserHasRight('AssignCustomFields') ) { return ( 0, $self->loc('Permission Denied') ); } - my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser ); - - $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id ); - unless ( $ObjectCF->Id ) { + my $ocf = $self->IsApplied( $id ); + unless ( $ocf ) { return ( 0, $self->loc("This custom field does not apply to that object") ); } - # XXX: Delete doesn't return anything - my ( $oid, $msg ) = $ObjectCF->Delete; + # XXX: Delete doesn't return anything + my ( $oid, $msg ) = $ocf->Delete; return ( $oid, $msg ); } @@ -1014,21 +1141,20 @@ sub AddValueForObject { ContentType => undef, @_ ); - my $obj = $args{'Object'} or return; + my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') ); unless ( $self->CurrentUserHasRight('ModifyCustomField') ) { return ( 0, $self->loc('Permission Denied') ); } - unless ( $self->MatchPattern($args{Content}) ) { + unless ( $self->MatchPattern($args{'Content'}) ) { return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) ); } $RT::Handle->BeginTransaction; - my $current_values = $self->ValuesForObject($obj); - if ( $self->MaxValues ) { + my $current_values = $self->ValuesForObject($obj); my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues; # (The +1 is for the new value we're adding) @@ -1039,19 +1165,25 @@ sub AddValueForObject { while ($extra_values) { my $extra_item = $current_values->Next; - unless ( $extra_item->id ) { - $RT::Logger->crit( -"We were just asked to delete a custom fieldvalue that doesn't exist!" - ); + $RT::Logger->crit( "We were just asked to delete " + ."a custom field value that doesn't exist!" ); $RT::Handle->Rollback(); return (undef); } $extra_item->Delete; $extra_values--; - } } + # For date, we need to store Content as ISO date + if ($self->Type eq 'Date') { + my $DateObj = new RT::Date( $self->CurrentUser ); + $DateObj->Set( + Format => 'unknown', + Value => $args{'Content'}, + ); + $args{'Content'} = $DateObj->ISO; + } my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser ); my $val = $newval->Create( ObjectType => ref($obj), @@ -1064,7 +1196,7 @@ sub AddValueForObject { unless ($val) { $RT::Handle->Rollback(); - return ($val); + return ($val, $self->loc("Couldn't create record")); } $RT::Handle->Commit(); @@ -1085,10 +1217,9 @@ and returns a boolean; returns true if the Pattern is empty. sub MatchPattern { my $self = shift; - my $regex = $self->Pattern; + my $regex = $self->Pattern or return 1; - return 1 if !length($regex); - return ($_[0] =~ $regex); + return (( defined $_[0] ? $_[0] : '') =~ $regex); } @@ -1107,8 +1238,8 @@ sub FriendlyPattern { my $self = shift; my $regex = $self->Pattern; - return '' if !length($regex); - if ($regex =~ /\(\?#([^)]*)\)/) { + return '' unless length $regex; + if ( $regex =~ /\(\?#([^)]*)\)/ ) { return '[' . $self->loc($1) . ']'; } else { @@ -1134,7 +1265,7 @@ sub DeleteValueForObject { my %args = ( Object => undef, Content => undef, Id => undef, - @_ ); + @_ ); unless ($self->CurrentUserHasRight('ModifyCustomField')) { @@ -1144,14 +1275,14 @@ sub DeleteValueForObject { my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser); if (my $id = $args{'Id'}) { - $oldval->Load($id); + $oldval->Load($id); } unless ($oldval->id) { - $oldval->LoadByObjectContentAndCustomField( - Object => $args{'Object'}, - Content => $args{'Content'}, - CustomField => $self->Id, - ); + $oldval->LoadByObjectContentAndCustomField( + Object => $args{'Object'}, + Content => $args{'Content'}, + CustomField => $self->Id, + ); } @@ -1177,26 +1308,26 @@ sub DeleteValueForObject { =head2 ValuesForObject OBJECT -Return an RT::ObjectCustomFieldValues object containing all of this custom field's values for OBJECT +Return an L object containing all of this custom field's values for OBJECT =cut sub ValuesForObject { - my $self = shift; + my $self = shift; my $object = shift; - my $values = new RT::ObjectCustomFieldValues($self->CurrentUser); - unless ($self->CurrentUserHasRight('SeeCustomField')) { + my $values = new RT::ObjectCustomFieldValues($self->CurrentUser); + unless ($self->CurrentUserHasRight('SeeCustomField')) { # Return an empty object if they have no rights to see return ($values); } - - - $values->LimitToCustomField($self->Id); - $values->LimitToEnabled(); + + + $values->LimitToCustomField($self->Id); + $values->LimitToEnabled(); $values->LimitToObject($object); - return ($values); + return ($values); } @@ -1206,10 +1337,10 @@ Tell RT that a certain object accepts custom fields Examples: - 'RT::Queue-RT::Ticket' => "Tickets", # loc - 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc - 'RT::User' => "Users", # loc - 'RT::Group' => "Groups", # loc + 'RT::Queue-RT::Ticket' => "Tickets", # loc + 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc + 'RT::User' => "Users", # loc + 'RT::Group' => "Groups", # loc This is a class method. @@ -1272,8 +1403,6 @@ With two arguments, attemptes to set the relevant template value. =cut - - sub _URLTemplate { my $self = shift; my $template_name = shift; @@ -1297,4 +1426,51 @@ sub _URLTemplate { } } + +sub SetBasedOn { + my $self = shift; + my $value = shift; + + return $self->DeleteAttribute( "BasedOn" ) + unless defined $value and length $value; + + my $cf = RT::CustomField->new( $self->CurrentUser ); + $cf->Load( ref $value ? $value->Id : $value ); + + return (0, "Permission denied") + unless $cf->Id && $cf->CurrentUserHasRight('SeeCustomField'); + + return $self->AddAttribute( + Name => "BasedOn", + Description => "Custom field whose CF we depend on", + Content => $cf->Id, + ); +} + +sub BasedOnObj { + my $self = shift; + my $obj = RT::CustomField->new( $self->CurrentUser ); + + my $attribute = $self->FirstAttribute("BasedOn"); + $obj->Load($attribute->Content) if defined $attribute; + return $obj; +} + +sub UILocation { + my $self = shift; + my $tag = $self->FirstAttribute( 'UILocation' ); + return $tag ? $tag->Content : ''; +} + +sub SetUILocation { + my $self = shift; + my $tag = shift; + if ( $tag ) { + return $self->SetAttribute( Name => 'UILocation', Content => $tag ); + } + else { + return $self->DeleteAttribute('UILocation'); + } +} + 1;