import rt 3.6.10
[freeside.git] / rt / lib / RT / CustomField_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC 
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
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
16 # from www.gnu.org.
17
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.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46
47 # END BPS TAGGED BLOCK }}}
48 package RT::CustomField;
49
50 use strict;
51 no warnings qw(redefine);
52
53 use vars qw(%FieldTypes $RIGHTS %FRIENDLY_OBJECT_TYPES);
54
55 use RT::CustomFieldValues;
56 use RT::ObjectCustomFieldValues;
57
58
59 %FieldTypes = (
60     Select => [
61         'Select multiple values',       # loc
62         'Select one value',             # loc
63         'Select up to [_1] values',     # loc
64     ],
65     Freeform => [
66         'Enter multiple values',        # loc
67         'Enter one value',              # loc
68         'Enter up to [_1] values',      # loc
69     ],
70     Text => [
71         'Fill in multiple text areas',  # loc
72         'Fill in one text area',        # loc
73         'Fill in up to [_1] text areas',# loc
74     ],
75     Wikitext => [
76         'Fill in multiple wikitext areas',      # loc
77         'Fill in one wikitext area',    # loc
78         'Fill in up to [_1] wikitext areas',# loc
79     ],
80     Image => [
81         'Upload multiple images',       # loc
82         'Upload one image',             # loc
83         'Upload up to [_1] images',     # loc
84     ],
85     Binary => [
86         'Upload multiple files',        # loc
87         'Upload one file',              # loc
88         'Upload up to [_1] files',      # loc
89     ],
90     Combobox => [
91         'Combobox: Select or enter multiple values',    # loc
92         'Combobox: Select or enter one value',          # loc
93         'Combobox: Select or enter up to [_1] values',  # loc
94     ],
95 );
96
97
98 %FRIENDLY_OBJECT_TYPES =  ();
99
100 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
101 RT::CustomField->_ForObjectType(
102     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", );    #loc
103 RT::CustomField->_ForObjectType( 'RT::User'  => "Users", );                           #loc
104 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", );                          #loc
105
106 $RIGHTS = {
107     SeeCustomField            => 'See custom fields',       # loc_pair
108     AdminCustomField          => 'Create, delete and modify custom fields',        # loc_pair
109     ModifyCustomField         => 'Add, delete and modify custom field values for objects' #loc_pair
110
111 };
112
113 # Tell RT::ACE that this sort of object can get acls granted
114 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
115
116 foreach my $right ( keys %{$RIGHTS} ) {
117     $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
118 }
119
120 sub AvailableRights {
121     my $self = shift;
122     return($RIGHTS);
123 }
124
125 =head1 NAME
126
127   RT::CustomField_Overlay 
128
129 =head1 DESCRIPTION
130
131 =head1 'CORE' METHODS
132
133 =cut
134
135
136
137 =head2 Create PARAMHASH
138
139 Create takes a hash of values and creates a row in the database:
140
141   varchar(200) 'Name'.
142   varchar(200) 'Type'.
143   int(11) 'MaxValues'.
144   varchar(255) 'Pattern'.
145   smallint(6) 'Repeated'.
146   varchar(255) 'Description'.
147   int(11) 'SortOrder'.
148   varchar(255) 'LookupType'.
149   smallint(6) 'Disabled'.
150
151   'LookupType' is generally the result of either 
152   RT::Ticket->CustomFieldLookupType or RT::Transaction->CustomFieldLookupType
153
154 =cut
155
156
157
158
159 sub Create {
160     my $self = shift;
161     my %args = ( 
162                 Name => '',
163                 Type => '',
164                 MaxValues => '0',
165                 Pattern  => '',
166                 Description => '',
167                 Disabled => '0',
168                 LookupType  => '',
169                 Repeated  => '0',
170
171                   @_);
172
173     unless ($self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField')) {
174         return (0, $self->loc('Permission Denied'));
175     }
176
177
178     if ($args{TypeComposite}) {
179         @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
180     }
181     elsif ($args{Type} =~ s/(?:(Single)|Multiple)$//) {
182         # old style Type string
183         $args{'MaxValues'} = $1 ? 1 : 0;
184     }
185     
186     if ( !exists $args{'Queue'}) {
187         # do nothing -- things below are strictly backward compat
188     }
189     elsif (  ! $args{'Queue'} ) {
190         unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
191             return ( 0, $self->loc('Permission Denied') );
192         }
193         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
194     }
195     else {
196         my $queue = RT::Queue->new($self->CurrentUser);
197         $queue->Load($args{'Queue'});
198         unless ($queue->Id) {
199             return (0, $self->loc("Queue not found"));
200         }
201         unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
202             return ( 0, $self->loc('Permission Denied') );
203         }
204         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
205         $args{'Queue'} = $queue->Id;
206     }
207
208     my ($ok, $msg) = $self->_IsValidRegex($args{'Pattern'});
209     if (!$ok) {
210         return (0, $self->loc("Invalid pattern: [_1]", $msg));
211     }
212
213     my $rv = $self->SUPER::Create(
214                          Name => $args{'Name'},
215                          Type => $args{'Type'},
216                          MaxValues => $args{'MaxValues'},
217                          Pattern  => $args{'Pattern'},
218                          Description => $args{'Description'},
219                          Disabled => $args{'Disabled'},
220                          LookupType => $args{'LookupType'},
221                          Repeated => $args{'Repeated'},
222 );
223
224     return $rv unless exists $args{'Queue'};
225
226     # Compat code -- create a new ObjectCustomField mapping
227     my $OCF = RT::ObjectCustomField->new($self->CurrentUser);
228     $OCF->Create(
229         CustomField => $self->Id,
230         ObjectId => $args{'Queue'},
231     );
232
233     return $rv;
234 }
235
236 =head2 Load ID/NAME
237
238 Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
239
240 =cut
241
242
243 sub Load {
244     my $self = shift;
245     my $id = shift;
246
247     if ($id =~ /^\d+$/) {
248         return ($self->SUPER::Load($id));
249     } else {
250         return($self->LoadByName(Name => $id));
251     }
252 }
253
254
255 # {{{ sub LoadByName
256
257 =head2  LoadByName (Queue => QUEUEID, Name => NAME)
258
259 Loads the Custom field named NAME.
260
261 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
262 with the same Name.
263
264 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
265
266 If the Queue parameter is '0', look for global ticket custom fields.
267
268 If no queue parameter is specified, look for any and all custom fields with this name.
269
270 BUG/TODO, this won't let you specify that you only want user or group CFs.
271
272 =cut
273
274 # Compatibility for API change after 3.0 beta 1
275 *LoadNameAndQueue = \&LoadByName;
276 # Change after 3.4 beta.
277 *LoadByNameAndQueue = \&LoadByName;
278
279 sub LoadByName {
280     my $self = shift;
281     my %args = (
282         Queue => undef,
283         Name  => undef,
284         @_,
285     );
286
287     # if we're looking for a queue by name, make it a number
288     if  (defined $args{'Queue'}  &&  $args{'Queue'} !~ /^\d+$/) {
289         my $QueueObj = RT::Queue->new($self->CurrentUser);
290         $QueueObj->Load($args{'Queue'});
291         $args{'Queue'} = $QueueObj->Id;
292     }
293
294     # XXX - really naive implementation.  Slow. - not really. still just one query
295
296     my $CFs = RT::CustomFields->new($self->CurrentUser);
297
298     $CFs->Limit( FIELD => 'Name', VALUE => $args{'Name'}, CASESENSITIVE => 0);
299     # Don't limit to queue if queue is 0.  Trying to do so breaks
300     # RT::Group type CFs.
301     if (defined $args{'Queue'}) {
302         $CFs->LimitToQueue( $args{'Queue'} );
303     }
304
305     # When loading by name, it's ok if they're disabled. That's not a big deal.
306     $CFs->{'find_disabled_rows'}=1;
307
308     # We only want one entry.
309     $CFs->RowsPerPage(1);
310     unless ($CFs->First) {
311         return(0);
312     }
313     return($self->Load($CFs->First->id));
314
315 }
316
317 # }}}
318
319 # {{{ Dealing with custom field values 
320
321 =begin testing
322
323 use_ok(RT::CustomField);
324 ok(my $cf = RT::CustomField->new($RT::SystemUser));
325 ok(my ($id, $msg)=  $cf->Create( Name => 'TestingCF',
326                                  Queue => '0',
327                                  SortOrder => '1',
328                                  Description => 'A Testing custom field',
329                                  Type=> 'SelectSingle'), 'Created a global CustomField');
330 ok($id != 0, 'Global custom field correctly created');
331 ok ($cf->SingleValue);
332 is($cf->Type, 'Select');
333 is($cf->MaxValues, 1);
334
335 my ($val, $msg) = $cf->SetMaxValues('0');
336 ok($val, $msg);
337 is($cf->Type, 'Select');
338 is($cf->MaxValues, 0);
339 ok(!$cf->SingleValue );
340 ok(my ($bogus_val, $bogus_msg) = $cf->SetType('BogusType') , "Trying to set a custom field's type to a bogus type");
341 ok($bogus_val == 0, "Unable to set a custom field's type to a bogus type");
342
343 ok(my $bad_cf = RT::CustomField->new($RT::SystemUser));
344 ok(my ($bad_id, $bad_msg)=  $cf->Create( Name => 'TestingCF-bad',
345                                  Queue => '0',
346                                  SortOrder => '1',
347                                  Description => 'A Testing custom field with a bogus Type',
348                                  Type=> 'SelectSingleton'), 'Created a global CustomField with a bogus type');
349 ok($bad_id == 0, 'Global custom field correctly decided to not create a cf with a bogus type ');
350
351 =end testing
352
353 =cut
354
355 # {{{ AddValue
356
357 =head2 AddValue HASH
358
359 Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
360
361 =begin testing
362
363 ok(my $cf = RT::CustomField->new($RT::SystemUser));
364 $cf->Load(1);
365 ok($cf->Id == 1);
366 ok(my ($val,$msg)  = $cf->AddValue(Name => 'foo' , Description => 'TestCFValue', SortOrder => '6'));
367 ok($val != 0);
368 ok (my ($delval, $delmsg) = $cf->DeleteValue($val));
369 ok ($delval,"Deleting a cf value: $delmsg");
370
371 =end testing
372
373 =cut
374
375 sub AddValue {
376     my $self = shift;
377     my %args = @_;
378
379     unless ($self->CurrentUserHasRight('AdminCustomField')) {
380         return (0, $self->loc('Permission Denied'));
381     }
382
383     # allow zero value
384     if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
385         return(0, $self->loc("Can't add a custom field value without a name"));
386     }
387
388     my $newval = RT::CustomFieldValue->new($self->CurrentUser);
389     return($newval->Create(%args, CustomField => $self->Id));
390 }
391
392
393 # }}}
394
395 # {{{ DeleteValue
396
397 =head2 DeleteValue ID
398
399 Deletes a value from this custom field by id. 
400
401 Does not remove this value for any article which has had it selected    
402
403 =cut
404
405 sub DeleteValue {
406         my $self = shift;
407     my $id = shift;
408     unless ($self->CurrentUserHasRight('AdminCustomField')) {
409         return (0, $self->loc('Permission Denied'));
410     }
411
412         my $val_to_del = RT::CustomFieldValue->new($self->CurrentUser);
413         $val_to_del->Load($id);
414         unless ($val_to_del->Id) {
415                 return (0, $self->loc("Couldn't find that value"));
416         }
417         unless ($val_to_del->CustomField == $self->Id) {
418                 return (0, $self->loc("That is not a value for this custom field"));
419         }
420
421         my $retval = $val_to_del->Delete();
422     if ($retval) {
423         return ($retval, $self->loc("Custom field value deleted"));
424     } else {
425         return(0, $self->loc("Custom field value could not be deleted"));
426     }
427 }
428
429 # }}}
430
431 # {{{ Values
432
433 =head2 Values FIELD
434
435 Return a CustomFieldeValues object of all acceptable values for this Custom Field.
436
437
438 =cut
439
440 *ValuesObj = \&Values;
441
442 sub Values {
443     my $self = shift;
444
445     my $cf_values = RT::CustomFieldValues->new($self->CurrentUser);
446     # if the user has no rights, return an empty object
447     if ($self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
448         $cf_values->LimitToCustomField($self->Id);
449     }
450     return ($cf_values);
451 }
452
453 # }}}
454
455 # }}}
456
457 # {{{ Ticket related routines
458
459 # {{{ ValuesForTicket
460
461 =head2 ValuesForTicket TICKET
462
463 Returns a RT::ObjectCustomFieldValues object of this Field's values for TICKET.
464 TICKET is a ticket id.
465
466 This is deprecated -- use ValuesForObject instead.
467
468
469 =cut
470
471 sub ValuesForTicket {
472         my $self = shift;
473     my $ticket_id = shift;
474     
475     $RT::Logger->debug( ref($self) . " -> ValuesForTicket deprecated in favor of ValuesForObject at (". join(":",caller).")"); 
476     my $ticket = RT::Ticket->new($self->CurrentUser);
477     $ticket->Load($ticket_id);
478
479     return $self->ValuesForObject($ticket);
480 }
481
482 # }}}
483
484 # {{{ AddValueForTicket
485
486 =head2 AddValueForTicket HASH
487
488 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
489
490 This is deprecated -- use AddValueForObject instead.
491
492 =cut
493
494 sub AddValueForTicket {
495         my $self = shift;
496         my %args = ( Ticket => undef,
497                  Content => undef,
498                      @_ );
499     $RT::Logger->debug( ref($self) . " -> AddValueForTicket deprecated in favor of AddValueForObject at (". join(":",caller).")");
500
501
502     my $ticket = RT::Ticket->new($self->CurrentUser);
503     $ticket->Load($args{'Ticket'});
504     return($self->AddValueForObject(Content => $args{'Content'}, Object => $ticket,@_));
505
506 }
507
508
509 # }}}
510
511 # {{{ DeleteValueForTicket
512
513 =head2 DeleteValueForTicket HASH
514
515 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
516
517 This is deprecated -- use DeleteValueForObject instead.
518
519 =cut
520
521 sub DeleteValueForTicket {
522         my $self = shift;
523         my %args = ( Ticket => undef,
524                  Content => undef,
525                      @_ );
526
527     $RT::Logger->debug( ref($self) . " -> DeleteValueForTicket deprecated in favor of DeleteValueForObject at (". join(":",caller).")"); 
528
529
530     my $ticket = RT::Ticket->new($self->CurrentUser);
531     $ticket->load($args{'Ticket'});
532     return ($self->DeleteValueForObject(Object => $ticket, Content => $args{'Content'}, @_));
533
534 }
535
536 # }}}
537 # }}}
538
539
540 =head2 ValidateQueue Queue
541
542 Make sure that the queue specified is a valid queue name
543
544 =cut
545
546 sub ValidateQueue {
547     my $self = shift;
548     my $id = shift;
549
550     if ($id eq '0') { # 0 means "Global" null would _not_ be ok.
551         return (1); 
552     }
553
554     my $q = RT::Queue->new($RT::SystemUser);
555     $q->Load($id);
556     unless ($q->id) {
557         return undef;
558     }
559     return (1);
560
561
562 }
563
564
565 # {{{ Types
566
567 =head2 Types 
568
569 Retuns an array of the types of CustomField that are supported
570
571 =cut
572
573 sub Types {
574         return (keys %FieldTypes);
575 }
576
577 # }}}
578
579 # {{{ IsSelectionType
580  
581 =head2 IsSelectionType 
582
583 Retuns a boolean value indicating whether the C<Values> method makes sense
584 to this Custom Field.
585
586 =cut
587
588 sub IsSelectionType {
589     my $self = shift;
590     $self->Type =~ /(?:Select|Combobox)/;
591 }
592
593 # }}}
594
595
596 =head2 FriendlyType [TYPE, MAX_VALUES]
597
598 Returns a localized human-readable version of the custom field type.
599 If a custom field type is specified as the parameter, the friendly type for that type will be returned
600
601 =cut
602
603 sub FriendlyType {
604     my $self = shift;
605
606     my $type = @_ ? shift : $self->Type;
607     my $max  = @_ ? shift : $self->MaxValues;
608
609     if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
610         return ( $self->loc( $friendly_type, $max ) );
611     }
612     else {
613         return ( $self->loc( $type ) );
614     }
615 }
616
617 sub FriendlyTypeComposite {
618     my $self = shift;
619     my $composite = shift || $self->TypeComposite;
620     return $self->FriendlyType(split(/-/, $composite, 2));
621 }
622
623
624 =head2 ValidateType TYPE
625
626 Takes a single string. returns true if that string is a value
627 type of custom field
628
629 =begin testing
630
631 ok(my $cf = RT::CustomField->new($RT::SystemUser));
632 ok($cf->ValidateType('SelectSingle'));
633 ok($cf->ValidateType('SelectMultiple'));
634 ok(!$cf->ValidateType('SelectFooMultiple'));
635
636 =end testing
637
638 =cut
639
640 sub ValidateType {
641     my $self = shift;
642     my $type = shift;
643
644     if ($type =~ s/(?:Single|Multiple)$//) {
645         $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
646     }
647
648     if( $FieldTypes{$type}) {
649         return(1);
650     }
651     else {
652         return undef;
653     }
654 }
655
656
657 sub SetType {
658     my $self = shift;
659     my $type = shift;
660     if ($type =~ s/(?:(Single)|Multiple)$//) {
661         $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
662         $self->SetMaxValues($1 ? 1 : 0);
663     }
664     $self->SUPER::SetType($type);
665 }
666
667 =head2 SetPattern STRING
668
669 Takes a single string representing a regular expression.  Performs basic
670 validation on that regex, and sets the C<Pattern> field for the CF if it
671 is valid.
672
673 =cut
674
675 sub SetPattern {
676     my $self = shift;
677     my $regex = shift;
678
679     my ($ok, $msg) = $self->_IsValidRegex($regex);
680     if ($ok) {
681         return $self->SUPER::SetPattern($regex);
682     }
683     else {
684         return (0, $self->loc("Invalid pattern: [_1]", $msg));
685     }
686 }
687
688 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
689
690 Tests if the string contains an invalid regex.
691
692 =cut
693
694 sub _IsValidRegex {
695     my $self  = shift;
696     my $regex = shift or return (1, 'valid');
697
698     local $^W; local $@;
699     $SIG{__DIE__} = sub { 1 };
700     $SIG{__WARN__} = sub { 1 };
701
702     if (eval { qr/$regex/; 1 }) {
703         return (1, 'valid');
704     }
705
706     my $err = $@;
707     $err =~ s{[,;].*}{};    # strip debug info from error
708     chomp $err;
709     return (0, $err);
710 }
711
712 # {{{ SingleValue
713
714 =head2 SingleValue
715
716 Returns true if this CustomField only accepts a single value. 
717 Returns false if it accepts multiple values
718
719 =cut
720
721 sub SingleValue {
722     my $self = shift;
723     if ($self->MaxValues == 1) {
724         return 1;
725     } 
726     else {
727         return undef;
728     }
729 }
730
731 sub UnlimitedValues {
732     my $self = shift;
733     if ($self->MaxValues == 0) {
734         return 1;
735     } 
736     else {
737         return undef;
738     }
739 }
740
741 # }}}
742
743 # {{{ sub CurrentUserHasRight
744
745 =head2 CurrentUserHasRight RIGHT
746
747 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
748
749 =cut
750
751 sub CurrentUserHasRight {
752     my $self  = shift;
753     my $right = shift;
754
755     return $self->CurrentUser->HasRight(
756         Object => $self,
757         Right  => $right,
758     );
759 }
760
761 # }}}
762
763 # {{{ sub _Set
764
765 sub _Set {
766     my $self = shift;
767
768     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
769         return ( 0, $self->loc('Permission Denied') );
770     }
771     return ( $self->SUPER::_Set(@_) );
772
773 }
774
775 # }}}
776
777 # {{{ sub _Value 
778
779 =head2 _Value
780
781 Takes the name of a table column.
782 Returns its value as a string, if the user passes an ACL check
783
784 =cut
785
786 sub _Value {
787
788     my $self  = shift;
789     my $field = shift;
790
791     # we need to do the rights check
792     unless ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
793             return (undef);
794     }
795     return ( $self->__Value($field) );
796
797 }
798
799 # }}}
800 # {{{ sub SetDisabled
801
802 =head2 SetDisabled
803
804 Takes a boolean.
805 1 will cause this custom field to no longer be avaialble for tickets.
806 0 will re-enable this queue
807
808 =cut
809
810 # }}}
811
812 sub Queue {
813     $RT::Logger->debug( ref($_[0]) . " -> Queue deprecated at (". join(":",caller).")");
814     
815     return 0;
816 }
817
818 sub SetQueue {
819     $RT::Logger->debug( ref($_[0]) . " -> SetQueue deprecated at (". join(":",caller).")");
820
821     return 0;
822 }
823
824 sub QueueObj {
825     $RT::Logger->debug( ref($_[0]) . " -> QueueObj deprecated at (". join(":",caller).")");
826
827     return undef;
828 }
829
830 =head2 SetTypeComposite
831
832 Set this custom field's type and maximum values as a composite value
833
834
835 =cut
836
837 sub SetTypeComposite {
838     my $self = shift;
839     my $composite = shift;
840     my ($type, $max_values) = split(/-/, $composite, 2);
841     $self->SetType($type);
842     $self->SetMaxValues($max_values);
843 }
844
845 =head2 SetLookupType
846
847 Autrijus: care to doc how LookupTypes work?
848
849 =cut
850
851 sub SetLookupType {
852     my $self = shift;
853     my $lookup = shift;
854     if ($lookup ne $self->LookupType) {
855         # Okay... We need to invalidate our existing relationships
856         my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
857         $ObjectCustomFields->LimitToCustomField($self->Id);
858         $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
859     }
860     $self->SUPER::SetLookupType($lookup);
861 }
862
863 =head2 TypeComposite
864
865 Returns a composite value composed of this object's type and maximum values
866
867 =cut
868
869
870 sub TypeComposite {
871     my $self = shift;
872     join('-', $self->Type, $self->MaxValues);
873 }
874
875 =head2 TypeComposites
876
877 Returns an array of all possible composite values for custom fields.
878
879 =cut
880
881 sub TypeComposites {
882     my $self = shift;
883     return grep !/(?:[Tt]ext|Combobox)-0/, map { ("$_-1", "$_-0") } $self->Types;
884 }
885
886 =head2 LookupTypes
887
888 Returns an array of LookupTypes available
889
890 =cut
891
892
893 sub LookupTypes {
894     my $self = shift;
895     return keys %FRIENDLY_OBJECT_TYPES;
896 }
897
898 my @FriendlyObjectTypes = (
899     "[_1] objects",                 # loc
900     "[_1]'s [_2] objects",          # loc
901     "[_1]'s [_2]'s [_3] objects",   # loc
902 );
903
904 =head2 FriendlyTypeLookup
905
906 =cut
907
908 sub FriendlyLookupType {
909     my $self = shift;
910     my $lookup = shift || $self->LookupType;
911    
912     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
913                    if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
914
915     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
916       grep { defined and length }
917       split( /-/, $lookup )
918       or return;
919     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
920 }
921
922
923 =head2 AddToObject OBJECT
924
925 Add this custom field as a custom field for a single object, such as a queue or group.
926
927 Takes an object 
928
929 =cut
930
931
932 sub AddToObject {
933     my $self  = shift;
934     my $object = shift;
935     my $id = $object->Id || 0;
936
937     unless (index($self->LookupType, ref($object)) == 0) {
938         return ( 0, $self->loc('Lookup type mismatch') );
939     }
940
941     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
942         return ( 0, $self->loc('Permission Denied') );
943     }
944
945     my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
946
947     $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
948     if ( $ObjectCF->Id ) {
949         return ( 0, $self->loc("That is already the current value") );
950     }
951     my ( $oid, $msg ) =
952       $ObjectCF->Create( ObjectId => $id, CustomField => $self->Id );
953
954     return ( $oid, $msg );
955 }
956
957
958 =head2 RemoveFromObject OBJECT
959
960 Remove this custom field  for a single object, such as a queue or group.
961
962 Takes an object 
963
964 =cut
965
966
967 sub RemoveFromObject {
968     my $self = shift;
969     my $object = shift;
970     my $id = $object->Id || 0;
971
972     unless (index($self->LookupType, ref($object)) == 0) {
973         return ( 0, $self->loc('Object type mismatch') );
974     }
975
976     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
977         return ( 0, $self->loc('Permission Denied') );
978     }
979
980     my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
981
982     $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
983     unless ( $ObjectCF->Id ) {
984         return ( 0, $self->loc("This custom field does not apply to that object") );
985     }
986     # XXX: Delete doesn't return anything
987     my ( $oid, $msg ) = $ObjectCF->Delete;
988
989     return ( $oid, $msg );
990 }
991
992 # {{{ AddValueForObject
993
994 =head2 AddValueForObject HASH
995
996 Adds a custom field value for a record object of some kind. 
997 Takes a param hash of 
998
999 Required:
1000
1001     Object
1002     Content
1003
1004 Optional:
1005
1006     LargeContent
1007     ContentType
1008
1009 =cut
1010
1011 sub AddValueForObject {
1012     my $self = shift;
1013     my %args = (
1014         Object       => undef,
1015         Content      => undef,
1016         LargeContent => undef,
1017         ContentType  => undef,
1018         @_
1019     );
1020     my $obj = $args{'Object'} or return;
1021
1022     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1023         return ( 0, $self->loc('Permission Denied') );
1024     }
1025
1026     unless ( $self->MatchPattern($args{Content}) ) {
1027         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1028     }
1029
1030     $RT::Handle->BeginTransaction;
1031
1032     my $current_values = $self->ValuesForObject($obj);
1033
1034     if ( $self->MaxValues ) {
1035         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1036
1037         # (The +1 is for the new value we're adding)
1038
1039         # If we have a set of current values and we've gone over the maximum
1040         # allowed number of values, we'll need to delete some to make room.
1041         # which former values are blown away is not guaranteed
1042
1043         while ($extra_values) {
1044             my $extra_item = $current_values->Next;
1045
1046             unless ( $extra_item->id ) {
1047                 $RT::Logger->crit(
1048 "We were just asked to delete a custom fieldvalue that doesn't exist!"
1049                 );
1050                 $RT::Handle->Rollback();
1051                 return (undef);
1052             }
1053             $extra_item->Delete;
1054             $extra_values--;
1055
1056         }
1057     }
1058     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1059     my $val    = $newval->Create(
1060         ObjectType   => ref($obj),
1061         ObjectId     => $obj->Id,
1062         Content      => $args{'Content'},
1063         LargeContent => $args{'LargeContent'},
1064         ContentType  => $args{'ContentType'},
1065         CustomField  => $self->Id
1066     );
1067
1068     unless ($val) {
1069         $RT::Handle->Rollback();
1070         return ($val);
1071     }
1072
1073     $RT::Handle->Commit();
1074     return ($val);
1075
1076 }
1077
1078 # }}}
1079
1080 # {{{ MatchPattern
1081
1082 =head2 MatchPattern STRING
1083
1084 Tests the incoming string against the Pattern of this custom field object
1085 and returns a boolean; returns true if the Pattern is empty.
1086
1087 =cut
1088
1089 sub MatchPattern {
1090     my $self = shift;
1091     my $regex = $self->Pattern;
1092
1093     return 1 if !length($regex);
1094     return ($_[0] =~ $regex);
1095 }
1096
1097
1098 # }}}
1099
1100 # {{{ FriendlyPattern
1101
1102 =head2 FriendlyPattern
1103
1104 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1105 and localizing it.
1106
1107 =cut
1108
1109 sub FriendlyPattern {
1110     my $self = shift;
1111     my $regex = $self->Pattern;
1112
1113     return '' if !length($regex);
1114     if ($regex =~ /\(\?#([^)]*)\)/) {
1115         return '[' . $self->loc($1) . ']';
1116     }
1117     else {
1118         return $regex;
1119     }
1120 }
1121
1122
1123 # }}}
1124
1125 # {{{ DeleteValueForObject
1126
1127 =head2 DeleteValueForObject HASH
1128
1129 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1130
1131 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1132
1133 =cut
1134
1135 sub DeleteValueForObject {
1136     my $self = shift;
1137     my %args = ( Object => undef,
1138                  Content => undef,
1139                  Id => undef,
1140                      @_ );
1141
1142
1143     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1144         return (0, $self->loc('Permission Denied'));
1145     }
1146
1147     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1148
1149     if (my $id = $args{'Id'}) {
1150         $oldval->Load($id);
1151     }
1152     unless ($oldval->id) { 
1153         $oldval->LoadByObjectContentAndCustomField(
1154             Object => $args{'Object'}, 
1155             Content =>  $args{'Content'}, 
1156             CustomField => $self->Id,
1157         );
1158     }
1159
1160
1161     # check to make sure we found it
1162     unless ($oldval->Id) {
1163         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1164     }
1165
1166     # for single-value fields, we need to validate that empty string is a valid value for it
1167     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1168         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1169     }
1170
1171     # delete it
1172
1173     my $ret = $oldval->Delete();
1174     unless ($ret) {
1175         return(0, $self->loc("Custom field value could not be found"));
1176     }
1177     return($oldval->Id, $self->loc("Custom field value deleted"));
1178 }
1179
1180
1181 =head2 ValuesForObject OBJECT
1182
1183 Return an RT::ObjectCustomFieldValues object containing all of this custom field's values for OBJECT 
1184
1185 =cut
1186
1187 sub ValuesForObject {
1188         my $self = shift;
1189     my $object = shift;
1190
1191         my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
1192         unless ($self->CurrentUserHasRight('SeeCustomField')) {
1193         # Return an empty object if they have no rights to see
1194         return ($values);
1195     }
1196         
1197         
1198         $values->LimitToCustomField($self->Id);
1199         $values->LimitToEnabled();
1200     $values->LimitToObject($object);
1201
1202         return ($values);
1203 }
1204
1205
1206 =head2 _ForObjectType PATH FRIENDLYNAME
1207
1208 Tell RT that a certain object accepts custom fields
1209
1210 Examples:
1211
1212     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1213     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1214     'RT::User'                             => "Users",                  # loc
1215     'RT::Group'                            => "Groups",                 # loc
1216
1217 This is a class method. 
1218
1219 =cut
1220
1221 sub _ForObjectType {
1222     my $self = shift;
1223     my $path = shift;
1224     my $friendly_name = shift;
1225
1226     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1227
1228 }
1229
1230
1231 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1232
1233 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1234 uses this field to automatically include content into the user's browser
1235 as they display records with custom fields in RT.
1236
1237 =cut
1238
1239 sub SetIncludeContentForValue {
1240     shift->IncludeContentForValue(@_);
1241 }
1242 sub IncludeContentForValue{
1243     my $self = shift;
1244     $self->_URLTemplate('IncludeContentForValue', @_);
1245 }
1246
1247
1248
1249 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1250
1251 Gets or sets the  C<LinkValueTo> for this custom field. RT
1252 uses this field to make custom field values into hyperlinks in the user's
1253 browser as they display records with custom fields in RT.
1254
1255 =cut
1256
1257
1258 sub SetLinkValueTo {
1259     shift->LinkValueTo(@_);
1260 }
1261
1262 sub LinkValueTo {
1263     my $self = shift;
1264     $self->_URLTemplate('LinkValueTo', @_);
1265
1266 }
1267
1268
1269 =head2 _URLTemplate  NAME [VALUE]
1270
1271 With one argument, returns the _URLTemplate named C<NAME>, but only if
1272 the current user has the right to see this custom field.
1273
1274 With two arguments, attemptes to set the relevant template value.
1275
1276 =cut
1277
1278
1279
1280 sub _URLTemplate {
1281     my $self          = shift;
1282     my $template_name = shift;
1283     if (@_) {
1284
1285         my $value = shift;
1286         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1287             return ( 0, $self->loc('Permission Denied') );
1288         }
1289         $self->SetAttribute( Name => $template_name, Content => $value );
1290         return ( 1, $self->loc('Updated') );
1291     } else {
1292         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1293             return (undef);
1294         }
1295
1296         my @attr = $self->Attributes->Named($template_name);
1297         my $attr = shift @attr;
1298
1299         if ($attr) { return $attr->Content }
1300
1301     }
1302 }
1303 1;