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'));
370 if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
371 return(0, $self->loc("Can't add a custom field value without a name"));
373 my $newval = RT::CustomFieldValue->new($self->CurrentUser);
374 return($newval->Create(
375 CustomField => $self->Id,
376 Name =>$args{'Name'},
377 Description => ($args{'Description'} || ''),
378 SortOrder => ($args{'SortOrder'} || '0')
387 =head2 DeleteValue ID
389 Deletes a value from this custom field by id.
391 Does not remove this value for any article which has had it selected
398 unless ($self->CurrentUserHasRight('AdminCustomField')) {
399 return (0, $self->loc('Permission Denied'));
402 my $val_to_del = RT::CustomFieldValue->new($self->CurrentUser);
403 $val_to_del->Load($id);
404 unless ($val_to_del->Id) {
405 return (0, $self->loc("Couldn't find that value"));
407 unless ($val_to_del->CustomField == $self->Id) {
408 return (0, $self->loc("That is not a value for this custom field"));
411 my $retval = $val_to_del->Delete();
413 return ($retval, $self->loc("Custom field value deleted"));
415 return(0, $self->loc("Custom field value could not be deleted"));
425 Return a CustomFieldeValues object of all acceptable values for this Custom Field.
430 *ValuesObj = \&Values;
435 my $cf_values = RT::CustomFieldValues->new($self->CurrentUser);
436 # if the user has no rights, return an empty object
437 if ($self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
438 $cf_values->LimitToCustomField($self->Id);
447 # {{{ Ticket related routines
449 # {{{ ValuesForTicket
451 =head2 ValuesForTicket TICKET
453 Returns a RT::ObjectCustomFieldValues object of this Field's values for TICKET.
454 TICKET is a ticket id.
456 This is deprecated -- use ValuesForObject instead.
461 sub ValuesForTicket {
463 my $ticket_id = shift;
465 $RT::Logger->debug( ref($self) . " -> ValuesForTicket deprecated in favor of ValuesForObject at (". join(":",caller).")");
466 my $ticket = RT::Ticket->new($self->CurrentUser);
467 $ticket->Load($ticket_id);
469 return $self->ValuesForObject($ticket);
474 # {{{ AddValueForTicket
476 =head2 AddValueForTicket HASH
478 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
480 This is deprecated -- use AddValueForObject instead.
484 sub AddValueForTicket {
486 my %args = ( Ticket => undef,
489 $RT::Logger->debug( ref($self) . " -> AddValueForTicket deprecated in favor of AddValueForObject at (". join(":",caller).")");
492 my $ticket = RT::Ticket->new($self->CurrentUser);
493 $ticket->Load($args{'Ticket'});
494 return($self->AddValueForObject(Content => $args{'Content'}, Object => $ticket,@_));
501 # {{{ DeleteValueForTicket
503 =head2 DeleteValueForTicket HASH
505 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
507 This is deprecated -- use DeleteValueForObject instead.
511 sub DeleteValueForTicket {
513 my %args = ( Ticket => undef,
517 $RT::Logger->debug( ref($self) . " -> DeleteValueForTicket deprecated in favor of DeleteValueForObject at (". join(":",caller).")");
520 my $ticket = RT::Ticket->new($self->CurrentUser);
521 $ticket->load($args{'Ticket'});
522 return ($self->DeleteValueForObject(Object => $ticket, Content => $args{'Content'}, @_));
530 =head2 ValidateQueue Queue
532 Make sure that the queue specified is a valid queue name
540 if ($id eq '0') { # 0 means "Global" null would _not_ be ok.
544 my $q = RT::Queue->new($RT::SystemUser);
559 Retuns an array of the types of CustomField that are supported
564 return (keys %FieldTypes);
570 =head2 FriendlyType [TYPE, MAX_VALUES]
572 Returns a localized human-readable version of the custom field type.
573 If a custom field type is specified as the parameter, the friendly type for that type will be returned
580 my $type = @_ ? shift : $self->Type;
581 my $max = @_ ? shift : $self->MaxValues;
583 if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
584 return ( $self->loc( $friendly_type, $max ) );
587 return ( $self->loc( $type ) );
591 sub FriendlyTypeComposite {
593 my $composite = shift || $self->TypeComposite;
594 return $self->FriendlyType(split(/-/, $composite, 2));
598 =head2 ValidateType TYPE
600 Takes a single string. returns true if that string is a value
605 ok(my $cf = RT::CustomField->new($RT::SystemUser));
606 ok($cf->ValidateType('SelectSingle'));
607 ok($cf->ValidateType('SelectMultiple'));
608 ok(!$cf->ValidateType('SelectFooMultiple'));
618 if ($type =~ s/(?:Single|Multiple)$//) {
619 $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
622 if( $FieldTypes{$type}) {
634 if ($type =~ s/(?:(Single)|Multiple)$//) {
635 $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
636 $self->SetMaxValues($1 ? 1 : 0);
638 $self->SUPER::SetType($type);
645 Returns true if this CustomField only accepts a single value.
646 Returns false if it accepts multiple values
652 if ($self->MaxValues == 1) {
660 sub UnlimitedValues {
662 if ($self->MaxValues == 0) {
672 # {{{ sub CurrentUserHasRight
674 =head2 CurrentUserHasRight RIGHT
676 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
680 sub CurrentUserHasRight {
684 return $self->CurrentUser->HasRight(
697 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
698 return ( 0, $self->loc('Permission Denied') );
700 return ( $self->SUPER::_Set(@_) );
710 Takes the name of a table column.
711 Returns its value as a string, if the user passes an ACL check
720 # we need to do the rights check
721 unless ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
724 return ( $self->__Value($field) );
729 # {{{ sub SetDisabled
734 1 will cause this custom field to no longer be avaialble for tickets.
735 0 will re-enable this queue
742 $RT::Logger->debug( ref($_[0]) . " -> Queue deprecated at (". join(":",caller).")");
748 $RT::Logger->debug( ref($_[0]) . " -> SetQueue deprecated at (". join(":",caller).")");
754 $RT::Logger->debug( ref($_[0]) . " -> QueueObj deprecated at (". join(":",caller).")");
759 =head2 SetTypeComposite
761 Set this custom field's type and maximum values as a composite value
766 sub SetTypeComposite {
768 my $composite = shift;
769 my ($type, $max_values) = split(/-/, $composite, 2);
770 $self->SetType($type);
771 $self->SetMaxValues($max_values);
776 Autrijus: care to doc how LookupTypes work?
783 if ($lookup ne $self->LookupType) {
784 # Okay... We need to invalidate our existing relationships
785 my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
786 $ObjectCustomFields->LimitToCustomField($self->Id);
787 $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
789 $self->SUPER::SetLookupType($lookup);
794 Returns a composite value composed of this object's type and maximum values
801 join('-', $self->Type, $self->MaxValues);
804 =head2 TypeComposites
806 Returns an array of all possible composite values for custom fields.
812 return grep !/Text-0/, map { ("$_-1", "$_-0") } $self->Types;
817 Returns an array of LookupTypes available
824 return keys %FRIENDLY_OBJECT_TYPES;
827 my @FriendlyObjectTypes = (
828 "[_1] objects", # loc
829 "[_1]'s [_2] objects", # loc
830 "[_1]'s [_2]'s [_3] objects", # loc
833 =head2 FriendlyTypeLookup
837 sub FriendlyLookupType {
839 my $lookup = shift || $self->LookupType;
841 return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
842 if (defined $FRIENDLY_OBJECT_TYPES{$lookup} );
844 my @types = map { s/^RT::// ? $self->loc($_) : $_ }
845 grep { defined and length }
846 split( /-/, $lookup )
848 return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
852 =head2 AddToObject OBJECT
854 Add this custom field as a custom field for a single object, such as a queue or group.
864 my $id = $object->Id || 0;
866 unless (index($self->LookupType, ref($object)) == 0) {
867 return ( 0, $self->loc('Lookup type mismatch') );
870 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
871 return ( 0, $self->loc('Permission Denied') );
874 my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
876 $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
877 if ( $ObjectCF->Id ) {
878 return ( 0, $self->loc("That is already the current value") );
881 $ObjectCF->Create( ObjectId => $id, CustomField => $self->Id );
883 return ( $oid, $msg );
887 =head2 RemoveFromObject OBJECT
889 Remove this custom field for a single object, such as a queue or group.
896 sub RemoveFromObject {
899 my $id = $object->Id || 0;
901 unless (index($self->LookupType, ref($object)) == 0) {
902 return ( 0, $self->loc('Object type mismatch') );
905 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
906 return ( 0, $self->loc('Permission Denied') );
909 my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
911 $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
912 unless ( $ObjectCF->Id ) {
913 return ( 0, $self->loc("This custom field does not apply to that object") );
915 # XXX: Delete doesn't return anything
916 my ( $oid, $msg ) = $ObjectCF->Delete;
918 return ( $oid, $msg );
921 # {{{ AddValueForObject
923 =head2 AddValueForObject HASH
925 Adds a custom field value for a record object of some kind.
926 Takes a param hash of
940 sub AddValueForObject {
945 LargeContent => undef,
946 ContentType => undef,
949 my $obj = $args{'Object'} or return;
951 unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
952 return ( 0, $self->loc('Permission Denied') );
955 $RT::Handle->BeginTransaction;
957 my $current_values = $self->ValuesForObject($obj);
959 if ( $self->MaxValues ) {
960 my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
962 # (The +1 is for the new value we're adding)
964 # If we have a set of current values and we've gone over the maximum
965 # allowed number of values, we'll need to delete some to make room.
966 # which former values are blown away is not guaranteed
968 while ($extra_values) {
969 my $extra_item = $current_values->Next;
971 unless ( $extra_item->id ) {
973 "We were just asked to delete a custom fieldvalue that doesn't exist!"
975 $RT::Handle->Rollback();
983 my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
984 my $val = $newval->Create(
985 ObjectType => ref($obj),
986 ObjectId => $obj->Id,
987 Content => $args{'Content'},
988 LargeContent => $args{'LargeContent'},
989 ContentType => $args{'ContentType'},
990 CustomField => $self->Id
994 $RT::Handle->Rollback();
998 $RT::Handle->Commit();
1005 # {{{ DeleteValueForObject
1007 =head2 DeleteValueForObject HASH
1009 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1011 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1015 sub DeleteValueForObject {
1017 my %args = ( Object => undef,
1023 unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1024 return (0, $self->loc('Permission Denied'));
1027 my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1029 if (my $id = $args{'Id'}) {
1032 unless ($oldval->id) {
1033 $oldval->LoadByObjectContentAndCustomField(
1034 Object => $args{'Object'},
1035 Content => $args{'Content'},
1036 CustomField => $self->Id,
1041 # check ot make sure we found it
1042 unless ($oldval->Id) {
1043 return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1047 my $ret = $oldval->Delete();
1049 return(0, $self->loc("Custom field value could not be found"));
1051 return($oldval->Id, $self->loc("Custom field value deleted"));
1055 =head2 ValuesForObject OBJECT
1057 Return an RT::ObjectCustomFieldValues object containing all of this custom field's values for OBJECT
1061 sub ValuesForObject {
1065 my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
1066 unless ($self->CurrentUserHasRight('SeeCustomField')) {
1067 # Return an empty object if they have no rights to see
1072 $values->LimitToCustomField($self->Id);
1073 $values->LimitToEnabled();
1074 $values->LimitToObject($object);
1080 =head2 _ForObjectType PATH FRIENDLYNAME
1082 Tell RT that a certain object accepts custom fields
1086 'RT::Queue-RT::Ticket' => "Tickets", # loc
1087 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
1088 'RT::User' => "Users", # loc
1089 'RT::Group' => "Groups", # loc
1091 This is a class method.
1095 sub _ForObjectType {
1098 my $friendly_name = shift;
1100 $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;