1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
46 package RT::CustomField;
49 no warnings qw(redefine);
51 use vars qw(%FieldTypes $RIGHTS %FRIENDLY_OBJECT_TYPES);
53 use RT::CustomFieldValues;
54 use RT::ObjectCustomFieldValues;
59 'Select multiple values', # loc
60 'Select one value', # loc
61 'Select up to [_1] values', # loc
64 'Enter multiple values', # loc
65 'Enter one value', # loc
66 'Enter up to [_1] values', # loc
69 'Fill in multiple text areas', # loc
70 'Fill in one text area', # loc
71 'Fill in up to [_1] text areas',# loc
74 'Fill in multiple wikitext areas', # loc
75 'Fill in one wikitext area', # loc
76 'Fill in up to [_1] wikitext areas',# loc
79 'Upload multiple images', # loc
80 'Upload one image', # loc
81 'Upload up to [_1] images', # loc
84 'Upload multiple files', # loc
85 'Upload one file', # loc
86 'Upload up to [_1] files', # loc
91 %FRIENDLY_OBJECT_TYPES = ();
93 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc
94 RT::CustomField->_ForObjectType(
95 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc
96 RT::CustomField->_ForObjectType( 'RT::User' => "Users", ); #loc
97 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", ); #loc
100 SeeCustomField => 'See custom fields', # loc_pair
101 AdminCustomField => 'Create, delete and modify custom fields', # loc_pair
102 ModifyCustomField => 'Add, delete and modify custom field values for objects' #loc_pair
106 # Tell RT::ACE that this sort of object can get acls granted
107 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
109 foreach my $right ( keys %{$RIGHTS} ) {
110 $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
113 sub AvailableRights {
120 RT::CustomField_Overlay
124 =head1 'CORE' METHODS
130 =head2 Create PARAMHASH
132 Create takes a hash of values and creates a row in the database:
137 varchar(255) 'Pattern'.
138 smallint(6) 'Repeated'.
139 varchar(255) 'Description'.
141 varchar(255) 'LookupType'.
142 smallint(6) 'Disabled'.
144 'LookupType' is generally the result of either
145 RT::Ticket->CustomFieldLookupType or RT::Transaction->CustomFieldLookupType
166 unless ($self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField')) {
167 return (0, $self->loc('Permission Denied'));
171 if ($args{TypeComposite}) {
172 @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
174 elsif ($args{Type} =~ s/(?:(Single)|Multiple)$//) {
175 # old style Type string
176 $args{'MaxValues'} = $1 ? 1 : 0;
179 if ( !exists $args{'Queue'}) {
180 # do nothing -- things below are strictly backward compat
182 elsif ( ! $args{'Queue'} ) {
183 unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
184 return ( 0, $self->loc('Permission Denied') );
186 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
189 my $queue = RT::Queue->new($self->CurrentUser);
190 $queue->Load($args{'Queue'});
191 unless ($queue->Id) {
192 return (0, $self->loc("Queue not found"));
194 unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
195 return ( 0, $self->loc('Permission Denied') );
197 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
199 my $rv = $self->SUPER::Create(
200 Name => $args{'Name'},
201 Type => $args{'Type'},
202 MaxValues => $args{'MaxValues'},
203 Pattern => $args{'Pattern'},
204 Description => $args{'Description'},
205 Disabled => $args{'Disabled'},
206 LookupType => $args{'LookupType'},
207 Repeated => $args{'Repeated'},
210 return $rv unless exists $args{'Queue'};
212 # Compat code -- create a new ObjectCustomField mapping
213 my $OCF = RT::ObjectCustomField->new($self->CurrentUser);
215 CustomField => $self->Id,
216 ObjectId => $args{'Queue'},
224 Load a custom field. If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
233 if ($id =~ /^\d+$/) {
234 return ($self->SUPER::Load($id));
236 return($self->LoadByName(Name => $id));
243 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
245 Loads the Custom field named NAME.
247 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
249 If the Queue parameter is '0', look for global ticket custom fields.
251 If no queue parameter is specified, look for any and all custom fields with this name.
253 BUG/TODO, this won't let you specify that you only want user or group CFs.
257 # Compatibility for API change after 3.0 beta 1
258 *LoadNameAndQueue = \&LoadByName;
259 # Change after 3.4 beta.
260 *LoadByNameAndQueue = \&LoadByName;
270 # if we're looking for a queue by name, make it a number
271 if (defined $args{'Queue'} && $args{'Queue'} !~ /^\d+$/) {
272 my $QueueObj = RT::Queue->new($self->CurrentUser);
273 $QueueObj->Load($args{'Queue'});
274 $args{'Queue'} = $QueueObj->Id;
277 # XXX - really naive implementation. Slow. - not really. still just one query
279 my $CFs = RT::CustomFields->new($self->CurrentUser);
281 $CFs->Limit( FIELD => 'Name', VALUE => $args{'Name'} );
282 # Don't limit to queue if queue is 0. Trying to do so breaks
283 # RT::Group type CFs.
284 if (defined $args{'Queue'}) {
285 $CFs->LimitToQueue( $args{'Queue'} );
288 # When loading by name, it's ok if they're disabled. That's not a big deal.
289 $CFs->{'find_disabled_rows'}=1;
291 # We only want one entry.
292 $CFs->RowsPerPage(1);
293 unless ($CFs->First) {
296 return($self->Load($CFs->First->id));
302 # {{{ Dealing with custom field values
306 use_ok(RT::CustomField);
307 ok(my $cf = RT::CustomField->new($RT::SystemUser));
308 ok(my ($id, $msg)= $cf->Create( Name => 'TestingCF',
311 Description => 'A Testing custom field',
312 Type=> 'SelectSingle'), 'Created a global CustomField');
313 ok($id != 0, 'Global custom field correctly created');
314 ok ($cf->SingleValue);
315 is($cf->Type, 'Select');
316 is($cf->MaxValues, 1);
318 my ($val, $msg) = $cf->SetMaxValues('0');
320 is($cf->Type, 'Select');
321 is($cf->MaxValues, 0);
322 ok(!$cf->SingleValue );
323 ok(my ($bogus_val, $bogus_msg) = $cf->SetType('BogusType') , "Trying to set a custom field's type to a bogus type");
324 ok($bogus_val == 0, "Unable to set a custom field's type to a bogus type");
326 ok(my $bad_cf = RT::CustomField->new($RT::SystemUser));
327 ok(my ($bad_id, $bad_msg)= $cf->Create( Name => 'TestingCF-bad',
330 Description => 'A Testing custom field with a bogus Type',
331 Type=> 'SelectSingleton'), 'Created a global CustomField with a bogus type');
332 ok($bad_id == 0, 'Global custom field correctly decided to not create a cf with a bogus type ');
342 Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder
346 ok(my $cf = RT::CustomField->new($RT::SystemUser));
349 ok(my ($val,$msg) = $cf->AddValue(Name => 'foo' , Description => 'TestCFValue', SortOrder => '6'));
351 ok (my ($delval, $delmsg) = $cf->DeleteValue($val));
352 ok ($delval,"Deleting a cf value: $delmsg");
360 my %args = ( Name => undef,
361 Description => undef,
365 unless ($self->CurrentUserHasRight('AdminCustomField')) {
366 return (0, $self->loc('Permission Denied'));
369 unless ($args{'Name'}) {
370 return(0, $self->loc("Can't add a custom field value without a name"));
372 my $newval = RT::CustomFieldValue->new($self->CurrentUser);
373 return($newval->Create(
374 CustomField => $self->Id,
375 Name =>$args{'Name'},
376 Description => ($args{'Description'} || ''),
377 SortOrder => ($args{'SortOrder'} || '0')
386 =head2 DeleteValue ID
388 Deletes a value from this custom field by id.
390 Does not remove this value for any article which has had it selected
397 unless ($self->CurrentUserHasRight('AdminCustomField')) {
398 return (0, $self->loc('Permission Denied'));
401 my $val_to_del = RT::CustomFieldValue->new($self->CurrentUser);
402 $val_to_del->Load($id);
403 unless ($val_to_del->Id) {
404 return (0, $self->loc("Couldn't find that value"));
406 unless ($val_to_del->CustomField == $self->Id) {
407 return (0, $self->loc("That is not a value for this custom field"));
410 my $retval = $val_to_del->Delete();
412 return ($retval, $self->loc("Custom field value deleted"));
414 return(0, $self->loc("Custom field value could not be deleted"));
424 Return a CustomFieldeValues object of all acceptable values for this Custom Field.
429 *ValuesObj = \&Values;
434 my $cf_values = RT::CustomFieldValues->new($self->CurrentUser);
435 # if the user has no rights, return an empty object
436 if ($self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
437 $cf_values->LimitToCustomField($self->Id);
446 # {{{ Ticket related routines
448 # {{{ ValuesForTicket
450 =head2 ValuesForTicket TICKET
452 Returns a RT::ObjectCustomFieldValues object of this Field's values for TICKET.
453 TICKET is a ticket id.
455 This is deprecated -- use ValuesForObject instead.
460 sub ValuesForTicket {
462 my $ticket_id = shift;
464 $RT::Logger->debug( ref($self) . " -> ValuesForTicket deprecated in favor of ValuesForObject");
465 my $ticket = RT::Ticket->new($self->CurrentUser);
466 $ticket->Load($ticket_id);
468 return $self->ValuesForObject($ticket);
473 # {{{ AddValueForTicket
475 =head2 AddValueForTicket HASH
477 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
479 This is deprecated -- use AddValueForObject instead.
483 sub AddValueForTicket {
485 my %args = ( Ticket => undef,
488 $RT::Logger->debug( ref($self) . " -> AddValueForTicket deprecated in favor of AddValueForObject");
491 my $ticket = RT::Ticket->new($self->CurrentUser);
492 $ticket->Load($args{'Ticket'});
493 return($self->AddValueForObject(Content => $args{'Content'}, Object => $ticket,@_));
500 # {{{ DeleteValueForTicket
502 =head2 DeleteValueForTicket HASH
504 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
506 This is deprecated -- use DeleteValueForObject instead.
510 sub DeleteValueForTicket {
512 my %args = ( Ticket => undef,
516 $RT::Logger->debug( ref($self) . " -> DeleteValueForTicket deprecated in favor of DeleteValueForObject");
519 my $ticket = RT::Ticket->new($self->CurrentUser);
520 $ticket->load($args{'Ticket'});
521 return ($self->DeleteValueForObject(Object => $ticket, Content => $args{'Content'}, @_));
529 =head2 ValidateQueue Queue
531 Make sure that the queue specified is a valid queue name
539 if ($id eq '0') { # 0 means "Global" null would _not_ be ok.
543 my $q = RT::Queue->new($RT::SystemUser);
558 Retuns an array of the types of CustomField that are supported
563 return (keys %FieldTypes);
569 =head2 FriendlyType [TYPE, MAX_VALUES]
571 Returns a localized human-readable version of the custom field type.
572 If a custom field type is specified as the parameter, the friendly type for that type will be returned
579 my $type = @_ ? shift : $self->Type;
580 my $max = @_ ? shift : $self->MaxValues;
582 if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
583 return ( $self->loc( $friendly_type, $max ) );
586 return ( $self->loc( $type ) );
590 sub FriendlyTypeComposite {
592 my $composite = shift || $self->TypeComposite;
593 return $self->FriendlyType(split(/-/, $composite, 2));
597 =head2 ValidateType TYPE
599 Takes a single string. returns true if that string is a value
604 ok(my $cf = RT::CustomField->new($RT::SystemUser));
605 ok($cf->ValidateType('SelectSingle'));
606 ok($cf->ValidateType('SelectMultiple'));
607 ok(!$cf->ValidateType('SelectFooMultiple'));
617 if ($type =~ s/(?:Single|Multiple)$//) {
618 $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead");
621 if( $FieldTypes{$type}) {
633 if ($type =~ s/(?:(Single)|Multiple)$//) {
634 warn "'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead";
635 $self->SetMaxValues($1 ? 1 : 0);
637 $self->SUPER::SetType($type);
644 Returns true if this CustomField only accepts a single value.
645 Returns false if it accepts multiple values
651 if ($self->MaxValues == 1) {
659 sub UnlimitedValues {
661 if ($self->MaxValues == 0) {
671 # {{{ sub CurrentUserHasRight
673 =head2 CurrentUserHasRight RIGHT
675 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
679 sub CurrentUserHasRight {
683 return $self->CurrentUser->HasRight(
696 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
697 return ( 0, $self->loc('Permission Denied') );
699 return ( $self->SUPER::_Set(@_) );
709 Takes the name of a table column.
710 Returns its value as a string, if the user passes an ACL check
719 # we need to do the rights check
720 unless ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
723 return ( $self->__Value($field) );
728 # {{{ sub SetDisabled
733 1 will cause this custom field to no longer be avaialble for tickets.
734 0 will re-enable this queue
741 $RT::Logger->debug( ref($_[0]) . " -> Queue deprecated");
747 $RT::Logger->debug( ref($_[0]) . " -> SetQueue deprecated");
753 $RT::Logger->debug( ref($_[0]) . " -> QueueObj deprecated");
758 =head2 SetTypeComposite
760 Set this custom field's type and maximum values as a composite value
765 sub SetTypeComposite {
767 my $composite = shift;
768 my ($type, $max_values) = split(/-/, $composite, 2);
769 $self->SetType($type);
770 $self->SetMaxValues($max_values);
775 Autrijus: care to doc how LookupTypes work?
782 if ($lookup ne $self->LookupType) {
783 # Okay... We need to invalidate our existing relationships
784 my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
785 $ObjectCustomFields->LimitToCustomField($self->Id);
786 $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
788 $self->SUPER::SetLookupType($lookup);
793 Returns a composite value composed of this object's type and maximum values
800 join('-', $self->Type, $self->MaxValues);
803 =head2 TypeComposites
805 Returns an array of all possible composite values for custom fields.
811 return grep !/Text-0/, map { ("$_-1", "$_-0") } $self->Types;
816 Returns an array of LookupTypes available
823 return keys %FRIENDLY_OBJECT_TYPES;
826 my @FriendlyObjectTypes = (
827 "[_1] objects", # loc
828 "[_1]'s [_2] objects", # loc
829 "[_1]'s [_2]'s [_3] objects", # loc
832 =head2 FriendlyTypeLookup
836 sub FriendlyLookupType {
838 my $lookup = shift || $self->LookupType;
840 return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
841 if (defined $FRIENDLY_OBJECT_TYPES{$lookup} );
843 my @types = map { s/^RT::// ? $self->loc($_) : $_ }
844 grep { defined and length }
845 split( /-/, $lookup )
847 return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
851 =head2 AddToObject OBJECT
853 Add this custom field as a custom field for a single object, such as a queue or group.
863 my $id = $object->Id || 0;
865 unless (index($self->LookupType, ref($object)) == 0) {
866 return ( 0, $self->loc('Lookup type mismatch') );
869 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
870 return ( 0, $self->loc('Permission Denied') );
873 my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
875 $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
876 if ( $ObjectCF->Id ) {
877 return ( 0, $self->loc("That is already the current value") );
880 $ObjectCF->Create( ObjectId => $id, CustomField => $self->Id );
882 return ( $id, $msg );
886 =head2 RemoveFromObject OBJECT
888 Remove this custom field for a single object, such as a queue or group.
895 sub RemoveFromObject {
898 my $id = $object->Id || 0;
900 unless (index($self->LookupType, ref($object)) == 0) {
901 return ( 0, $self->loc('Object type mismatch') );
904 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
905 return ( 0, $self->loc('Permission Denied') );
908 my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
910 $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
911 unless ( $ObjectCF->Id ) {
912 return ( 0, $self->loc("This custom field does not apply to that object") );
914 my ( $id, $msg ) = $ObjectCF->Delete;
916 return ( $id, $msg );
919 # {{{ AddValueForObject
921 =head2 AddValueForObject HASH
923 Adds a custom field value for a record object of some kind.
924 Takes a param hash of
938 sub AddValueForObject {
943 LargeContent => undef,
944 ContentType => undef,
947 my $obj = $args{'Object'} or return;
949 unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
950 return ( 0, $self->loc('Permission Denied') );
953 $RT::Handle->BeginTransaction;
955 my $current_values = $self->ValuesForObject($obj);
957 if ( $self->MaxValues ) {
958 my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
960 # (The +1 is for the new value we're adding)
962 # If we have a set of current values and we've gone over the maximum
963 # allowed number of values, we'll need to delete some to make room.
964 # which former values are blown away is not guaranteed
966 while ($extra_values) {
967 my $extra_item = $current_values->Next;
969 unless ( $extra_item->id ) {
971 "We were just asked to delete a custom fieldvalue that doesn't exist!"
973 $RT::Handle->Rollback();
981 my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
982 my $val = $newval->Create(
983 ObjectType => ref($obj),
984 ObjectId => $obj->Id,
985 Content => $args{'Content'},
986 LargeContent => $args{'LargeContent'},
987 ContentType => $args{'ContentType'},
988 CustomField => $self->Id
992 $RT::Handle->Rollback();
996 $RT::Handle->Commit();
1003 # {{{ DeleteValueForObject
1005 =head2 DeleteValueForObject HASH
1007 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1009 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1013 sub DeleteValueForObject {
1015 my %args = ( Object => undef,
1021 unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1022 return (0, $self->loc('Permission Denied'));
1025 my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1027 if (my $id = $args{'Id'}) {
1030 unless ($oldval->id) {
1031 $oldval->LoadByObjectContentAndCustomField(
1032 Object => $args{'Object'},
1033 Content => $args{'Content'},
1034 CustomField => $self->Id,
1039 # check ot make sure we found it
1040 unless ($oldval->Id) {
1041 return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1045 my $ret = $oldval->Delete();
1047 return(0, $self->loc("Custom field value could not be found"));
1049 return($oldval->Id, $self->loc("Custom field value deleted"));
1053 =head2 ValuesForObject OBJECT
1055 Return an RT::ObjectCustomFieldValues object containing all of this custom field's values for OBJECT
1059 sub ValuesForObject {
1063 my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
1064 unless ($self->CurrentUserHasRight('SeeCustomField')) {
1065 # Return an empty object if they have no rights to see
1070 $values->LimitToCustomField($self->Id);
1071 $values->LimitToEnabled();
1072 $values->LimitToObject($object);
1078 =head2 _ForObjectType PATH FRIENDLYNAME
1080 Tell RT that a certain object accepts custom fields
1084 'RT::Queue-RT::Ticket' => "Tickets", # loc
1085 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
1086 'RT::User' => "Users", # loc
1087 'RT::Group' => "Groups", # loc
1089 This is a class method.
1093 sub _ForObjectType {
1096 my $friendly_name = shift;
1098 $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;