import rt 3.6.6
[freeside.git] / rt / lib / RT / CustomField_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
262
263 If the Queue parameter is '0', look for global ticket custom fields.
264
265 If no queue parameter is specified, look for any and all custom fields with this name.
266
267 BUG/TODO, this won't let you specify that you only want user or group CFs.
268
269 =cut
270
271 # Compatibility for API change after 3.0 beta 1
272 *LoadNameAndQueue = \&LoadByName;
273 # Change after 3.4 beta.
274 *LoadByNameAndQueue = \&LoadByName;
275
276 sub LoadByName {
277     my $self = shift;
278     my %args = (
279         Queue => undef,
280         Name  => undef,
281         @_,
282     );
283
284     # if we're looking for a queue by name, make it a number
285     if  (defined $args{'Queue'}  &&  $args{'Queue'} !~ /^\d+$/) {
286         my $QueueObj = RT::Queue->new($self->CurrentUser);
287         $QueueObj->Load($args{'Queue'});
288         $args{'Queue'} = $QueueObj->Id;
289     }
290
291     # XXX - really naive implementation.  Slow. - not really. still just one query
292
293     my $CFs = RT::CustomFields->new($self->CurrentUser);
294
295     $CFs->Limit( FIELD => 'Name', VALUE => $args{'Name'}, CASESENSITIVE => 0);
296     # Don't limit to queue if queue is 0.  Trying to do so breaks
297     # RT::Group type CFs.
298     if (defined $args{'Queue'}) {
299         $CFs->LimitToQueue( $args{'Queue'} );
300     }
301
302     # When loading by name, it's ok if they're disabled. That's not a big deal.
303     $CFs->{'find_disabled_rows'}=1;
304
305     # We only want one entry.
306     $CFs->RowsPerPage(1);
307     unless ($CFs->First) {
308         return(0);
309     }
310     return($self->Load($CFs->First->id));
311
312 }
313
314 # }}}
315
316 # {{{ Dealing with custom field values 
317
318 =begin testing
319
320 use_ok(RT::CustomField);
321 ok(my $cf = RT::CustomField->new($RT::SystemUser));
322 ok(my ($id, $msg)=  $cf->Create( Name => 'TestingCF',
323                                  Queue => '0',
324                                  SortOrder => '1',
325                                  Description => 'A Testing custom field',
326                                  Type=> 'SelectSingle'), 'Created a global CustomField');
327 ok($id != 0, 'Global custom field correctly created');
328 ok ($cf->SingleValue);
329 is($cf->Type, 'Select');
330 is($cf->MaxValues, 1);
331
332 my ($val, $msg) = $cf->SetMaxValues('0');
333 ok($val, $msg);
334 is($cf->Type, 'Select');
335 is($cf->MaxValues, 0);
336 ok(!$cf->SingleValue );
337 ok(my ($bogus_val, $bogus_msg) = $cf->SetType('BogusType') , "Trying to set a custom field's type to a bogus type");
338 ok($bogus_val == 0, "Unable to set a custom field's type to a bogus type");
339
340 ok(my $bad_cf = RT::CustomField->new($RT::SystemUser));
341 ok(my ($bad_id, $bad_msg)=  $cf->Create( Name => 'TestingCF-bad',
342                                  Queue => '0',
343                                  SortOrder => '1',
344                                  Description => 'A Testing custom field with a bogus Type',
345                                  Type=> 'SelectSingleton'), 'Created a global CustomField with a bogus type');
346 ok($bad_id == 0, 'Global custom field correctly decided to not create a cf with a bogus type ');
347
348 =end testing
349
350 =cut
351
352 # {{{ AddValue
353
354 =head2 AddValue HASH
355
356 Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
357
358 =begin testing
359
360 ok(my $cf = RT::CustomField->new($RT::SystemUser));
361 $cf->Load(1);
362 ok($cf->Id == 1);
363 ok(my ($val,$msg)  = $cf->AddValue(Name => 'foo' , Description => 'TestCFValue', SortOrder => '6'));
364 ok($val != 0);
365 ok (my ($delval, $delmsg) = $cf->DeleteValue($val));
366 ok ($delval,"Deleting a cf value: $delmsg");
367
368 =end testing
369
370 =cut
371
372 sub AddValue {
373     my $self = shift;
374     my %args = @_;
375
376     unless ($self->CurrentUserHasRight('AdminCustomField')) {
377         return (0, $self->loc('Permission Denied'));
378     }
379
380     # allow zero value
381     if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
382         return(0, $self->loc("Can't add a custom field value without a name"));
383     }
384
385     my $newval = RT::CustomFieldValue->new($self->CurrentUser);
386     return($newval->Create(%args, CustomField => $self->Id));
387 }
388
389
390 # }}}
391
392 # {{{ DeleteValue
393
394 =head2 DeleteValue ID
395
396 Deletes a value from this custom field by id. 
397
398 Does not remove this value for any article which has had it selected    
399
400 =cut
401
402 sub DeleteValue {
403         my $self = shift;
404     my $id = shift;
405     unless ($self->CurrentUserHasRight('AdminCustomField')) {
406         return (0, $self->loc('Permission Denied'));
407     }
408
409         my $val_to_del = RT::CustomFieldValue->new($self->CurrentUser);
410         $val_to_del->Load($id);
411         unless ($val_to_del->Id) {
412                 return (0, $self->loc("Couldn't find that value"));
413         }
414         unless ($val_to_del->CustomField == $self->Id) {
415                 return (0, $self->loc("That is not a value for this custom field"));
416         }
417
418         my $retval = $val_to_del->Delete();
419     if ($retval) {
420         return ($retval, $self->loc("Custom field value deleted"));
421     } else {
422         return(0, $self->loc("Custom field value could not be deleted"));
423     }
424 }
425
426 # }}}
427
428 # {{{ Values
429
430 =head2 Values FIELD
431
432 Return a CustomFieldeValues object of all acceptable values for this Custom Field.
433
434
435 =cut
436
437 *ValuesObj = \&Values;
438
439 sub Values {
440     my $self = shift;
441
442     my $cf_values = RT::CustomFieldValues->new($self->CurrentUser);
443     # if the user has no rights, return an empty object
444     if ($self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
445         $cf_values->LimitToCustomField($self->Id);
446     }
447     return ($cf_values);
448 }
449
450 # }}}
451
452 # }}}
453
454 # {{{ Ticket related routines
455
456 # {{{ ValuesForTicket
457
458 =head2 ValuesForTicket TICKET
459
460 Returns a RT::ObjectCustomFieldValues object of this Field's values for TICKET.
461 TICKET is a ticket id.
462
463 This is deprecated -- use ValuesForObject instead.
464
465
466 =cut
467
468 sub ValuesForTicket {
469         my $self = shift;
470     my $ticket_id = shift;
471     
472     $RT::Logger->debug( ref($self) . " -> ValuesForTicket deprecated in favor of ValuesForObject at (". join(":",caller).")"); 
473     my $ticket = RT::Ticket->new($self->CurrentUser);
474     $ticket->Load($ticket_id);
475
476     return $self->ValuesForObject($ticket);
477 }
478
479 # }}}
480
481 # {{{ AddValueForTicket
482
483 =head2 AddValueForTicket HASH
484
485 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
486
487 This is deprecated -- use AddValueForObject instead.
488
489 =cut
490
491 sub AddValueForTicket {
492         my $self = shift;
493         my %args = ( Ticket => undef,
494                  Content => undef,
495                      @_ );
496     $RT::Logger->debug( ref($self) . " -> AddValueForTicket deprecated in favor of AddValueForObject at (". join(":",caller).")");
497
498
499     my $ticket = RT::Ticket->new($self->CurrentUser);
500     $ticket->Load($args{'Ticket'});
501     return($self->AddValueForObject(Content => $args{'Content'}, Object => $ticket,@_));
502
503 }
504
505
506 # }}}
507
508 # {{{ DeleteValueForTicket
509
510 =head2 DeleteValueForTicket HASH
511
512 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
513
514 This is deprecated -- use DeleteValueForObject instead.
515
516 =cut
517
518 sub DeleteValueForTicket {
519         my $self = shift;
520         my %args = ( Ticket => undef,
521                  Content => undef,
522                      @_ );
523
524     $RT::Logger->debug( ref($self) . " -> DeleteValueForTicket deprecated in favor of DeleteValueForObject at (". join(":",caller).")"); 
525
526
527     my $ticket = RT::Ticket->new($self->CurrentUser);
528     $ticket->load($args{'Ticket'});
529     return ($self->DeleteValueForObject(Object => $ticket, Content => $args{'Content'}, @_));
530
531 }
532
533 # }}}
534 # }}}
535
536
537 =head2 ValidateQueue Queue
538
539 Make sure that the queue specified is a valid queue name
540
541 =cut
542
543 sub ValidateQueue {
544     my $self = shift;
545     my $id = shift;
546
547     if ($id eq '0') { # 0 means "Global" null would _not_ be ok.
548         return (1); 
549     }
550
551     my $q = RT::Queue->new($RT::SystemUser);
552     $q->Load($id);
553     unless ($q->id) {
554         return undef;
555     }
556     return (1);
557
558
559 }
560
561
562 # {{{ Types
563
564 =head2 Types 
565
566 Retuns an array of the types of CustomField that are supported
567
568 =cut
569
570 sub Types {
571         return (keys %FieldTypes);
572 }
573
574 # }}}
575
576 # {{{ IsSelectionType
577  
578 =head2 IsSelectionType 
579
580 Retuns a boolean value indicating whether the C<Values> method makes sense
581 to this Custom Field.
582
583 =cut
584
585 sub IsSelectionType {
586     my $self = shift;
587     $self->Type =~ /(?:Select|Combobox)/;
588 }
589
590 # }}}
591
592
593 =head2 FriendlyType [TYPE, MAX_VALUES]
594
595 Returns a localized human-readable version of the custom field type.
596 If a custom field type is specified as the parameter, the friendly type for that type will be returned
597
598 =cut
599
600 sub FriendlyType {
601     my $self = shift;
602
603     my $type = @_ ? shift : $self->Type;
604     my $max  = @_ ? shift : $self->MaxValues;
605
606     if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
607         return ( $self->loc( $friendly_type, $max ) );
608     }
609     else {
610         return ( $self->loc( $type ) );
611     }
612 }
613
614 sub FriendlyTypeComposite {
615     my $self = shift;
616     my $composite = shift || $self->TypeComposite;
617     return $self->FriendlyType(split(/-/, $composite, 2));
618 }
619
620
621 =head2 ValidateType TYPE
622
623 Takes a single string. returns true if that string is a value
624 type of custom field
625
626 =begin testing
627
628 ok(my $cf = RT::CustomField->new($RT::SystemUser));
629 ok($cf->ValidateType('SelectSingle'));
630 ok($cf->ValidateType('SelectMultiple'));
631 ok(!$cf->ValidateType('SelectFooMultiple'));
632
633 =end testing
634
635 =cut
636
637 sub ValidateType {
638     my $self = shift;
639     my $type = shift;
640
641     if ($type =~ s/(?:Single|Multiple)$//) {
642         $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
643     }
644
645     if( $FieldTypes{$type}) {
646         return(1);
647     }
648     else {
649         return undef;
650     }
651 }
652
653
654 sub SetType {
655     my $self = shift;
656     my $type = shift;
657     if ($type =~ s/(?:(Single)|Multiple)$//) {
658         $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
659         $self->SetMaxValues($1 ? 1 : 0);
660     }
661     $self->SUPER::SetType($type);
662 }
663
664 =head2 SetPattern STRING
665
666 Takes a single string representing a regular expression.  Performs basic
667 validation on that regex, and sets the C<Pattern> field for the CF if it
668 is valid.
669
670 =cut
671
672 sub SetPattern {
673     my $self = shift;
674     my $regex = shift;
675
676     my ($ok, $msg) = $self->_IsValidRegex($regex);
677     if ($ok) {
678         return $self->SUPER::SetPattern($regex);
679     }
680     else {
681         return (0, $self->loc("Invalid pattern: [_1]", $msg));
682     }
683 }
684
685 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
686
687 Tests if the string contains an invalid regex.
688
689 =cut
690
691 sub _IsValidRegex {
692     my $self  = shift;
693     my $regex = shift or return (1, 'valid');
694
695     local $^W; local $@;
696     $SIG{__DIE__} = sub { 1 };
697     $SIG{__WARN__} = sub { 1 };
698
699     if (eval { qr/$regex/; 1 }) {
700         return (1, 'valid');
701     }
702
703     my $err = $@;
704     $err =~ s{[,;].*}{};    # strip debug info from error
705     chomp $err;
706     return (0, $err);
707 }
708
709 # {{{ SingleValue
710
711 =head2 SingleValue
712
713 Returns true if this CustomField only accepts a single value. 
714 Returns false if it accepts multiple values
715
716 =cut
717
718 sub SingleValue {
719     my $self = shift;
720     if ($self->MaxValues == 1) {
721         return 1;
722     } 
723     else {
724         return undef;
725     }
726 }
727
728 sub UnlimitedValues {
729     my $self = shift;
730     if ($self->MaxValues == 0) {
731         return 1;
732     } 
733     else {
734         return undef;
735     }
736 }
737
738 # }}}
739
740 # {{{ sub CurrentUserHasRight
741
742 =head2 CurrentUserHasRight RIGHT
743
744 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
745
746 =cut
747
748 sub CurrentUserHasRight {
749     my $self  = shift;
750     my $right = shift;
751
752     return $self->CurrentUser->HasRight(
753         Object => $self,
754         Right  => $right,
755     );
756 }
757
758 # }}}
759
760 # {{{ sub _Set
761
762 sub _Set {
763     my $self = shift;
764
765     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
766         return ( 0, $self->loc('Permission Denied') );
767     }
768     return ( $self->SUPER::_Set(@_) );
769
770 }
771
772 # }}}
773
774 # {{{ sub _Value 
775
776 =head2 _Value
777
778 Takes the name of a table column.
779 Returns its value as a string, if the user passes an ACL check
780
781 =cut
782
783 sub _Value {
784
785     my $self  = shift;
786     my $field = shift;
787
788     # we need to do the rights check
789     unless ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
790             return (undef);
791     }
792     return ( $self->__Value($field) );
793
794 }
795
796 # }}}
797 # {{{ sub SetDisabled
798
799 =head2 SetDisabled
800
801 Takes a boolean.
802 1 will cause this custom field to no longer be avaialble for tickets.
803 0 will re-enable this queue
804
805 =cut
806
807 # }}}
808
809 sub Queue {
810     $RT::Logger->debug( ref($_[0]) . " -> Queue deprecated at (". join(":",caller).")");
811     
812     return 0;
813 }
814
815 sub SetQueue {
816     $RT::Logger->debug( ref($_[0]) . " -> SetQueue deprecated at (". join(":",caller).")");
817
818     return 0;
819 }
820
821 sub QueueObj {
822     $RT::Logger->debug( ref($_[0]) . " -> QueueObj deprecated at (". join(":",caller).")");
823
824     return undef;
825 }
826
827 =head2 SetTypeComposite
828
829 Set this custom field's type and maximum values as a composite value
830
831
832 =cut
833
834 sub SetTypeComposite {
835     my $self = shift;
836     my $composite = shift;
837     my ($type, $max_values) = split(/-/, $composite, 2);
838     $self->SetType($type);
839     $self->SetMaxValues($max_values);
840 }
841
842 =head2 SetLookupType
843
844 Autrijus: care to doc how LookupTypes work?
845
846 =cut
847
848 sub SetLookupType {
849     my $self = shift;
850     my $lookup = shift;
851     if ($lookup ne $self->LookupType) {
852         # Okay... We need to invalidate our existing relationships
853         my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
854         $ObjectCustomFields->LimitToCustomField($self->Id);
855         $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
856     }
857     $self->SUPER::SetLookupType($lookup);
858 }
859
860 =head2 TypeComposite
861
862 Returns a composite value composed of this object's type and maximum values
863
864 =cut
865
866
867 sub TypeComposite {
868     my $self = shift;
869     join('-', $self->Type, $self->MaxValues);
870 }
871
872 =head2 TypeComposites
873
874 Returns an array of all possible composite values for custom fields.
875
876 =cut
877
878 sub TypeComposites {
879     my $self = shift;
880     return grep !/(?:[Tt]ext|Combobox)-0/, map { ("$_-1", "$_-0") } $self->Types;
881 }
882
883 =head2 LookupTypes
884
885 Returns an array of LookupTypes available
886
887 =cut
888
889
890 sub LookupTypes {
891     my $self = shift;
892     return keys %FRIENDLY_OBJECT_TYPES;
893 }
894
895 my @FriendlyObjectTypes = (
896     "[_1] objects",                 # loc
897     "[_1]'s [_2] objects",          # loc
898     "[_1]'s [_2]'s [_3] objects",   # loc
899 );
900
901 =head2 FriendlyTypeLookup
902
903 =cut
904
905 sub FriendlyLookupType {
906     my $self = shift;
907     my $lookup = shift || $self->LookupType;
908    
909     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
910                    if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
911
912     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
913       grep { defined and length }
914       split( /-/, $lookup )
915       or return;
916     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
917 }
918
919
920 =head2 AddToObject OBJECT
921
922 Add this custom field as a custom field for a single object, such as a queue or group.
923
924 Takes an object 
925
926 =cut
927
928
929 sub AddToObject {
930     my $self  = shift;
931     my $object = shift;
932     my $id = $object->Id || 0;
933
934     unless (index($self->LookupType, ref($object)) == 0) {
935         return ( 0, $self->loc('Lookup type mismatch') );
936     }
937
938     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
939         return ( 0, $self->loc('Permission Denied') );
940     }
941
942     my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
943
944     $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
945     if ( $ObjectCF->Id ) {
946         return ( 0, $self->loc("That is already the current value") );
947     }
948     my ( $oid, $msg ) =
949       $ObjectCF->Create( ObjectId => $id, CustomField => $self->Id );
950
951     return ( $oid, $msg );
952 }
953
954
955 =head2 RemoveFromObject OBJECT
956
957 Remove this custom field  for a single object, such as a queue or group.
958
959 Takes an object 
960
961 =cut
962
963
964 sub RemoveFromObject {
965     my $self = shift;
966     my $object = shift;
967     my $id = $object->Id || 0;
968
969     unless (index($self->LookupType, ref($object)) == 0) {
970         return ( 0, $self->loc('Object type mismatch') );
971     }
972
973     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
974         return ( 0, $self->loc('Permission Denied') );
975     }
976
977     my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
978
979     $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
980     unless ( $ObjectCF->Id ) {
981         return ( 0, $self->loc("This custom field does not apply to that object") );
982     }
983     # XXX: Delete doesn't return anything
984     my ( $oid, $msg ) = $ObjectCF->Delete;
985
986     return ( $oid, $msg );
987 }
988
989 # {{{ AddValueForObject
990
991 =head2 AddValueForObject HASH
992
993 Adds a custom field value for a record object of some kind. 
994 Takes a param hash of 
995
996 Required:
997
998     Object
999     Content
1000
1001 Optional:
1002
1003     LargeContent
1004     ContentType
1005
1006 =cut
1007
1008 sub AddValueForObject {
1009     my $self = shift;
1010     my %args = (
1011         Object       => undef,
1012         Content      => undef,
1013         LargeContent => undef,
1014         ContentType  => undef,
1015         @_
1016     );
1017     my $obj = $args{'Object'} or return;
1018
1019     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1020         return ( 0, $self->loc('Permission Denied') );
1021     }
1022
1023     unless ( $self->MatchPattern($args{Content}) ) {
1024         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1025     }
1026
1027     $RT::Handle->BeginTransaction;
1028
1029     my $current_values = $self->ValuesForObject($obj);
1030
1031     if ( $self->MaxValues ) {
1032         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1033
1034         # (The +1 is for the new value we're adding)
1035
1036         # If we have a set of current values and we've gone over the maximum
1037         # allowed number of values, we'll need to delete some to make room.
1038         # which former values are blown away is not guaranteed
1039
1040         while ($extra_values) {
1041             my $extra_item = $current_values->Next;
1042
1043             unless ( $extra_item->id ) {
1044                 $RT::Logger->crit(
1045 "We were just asked to delete a custom fieldvalue that doesn't exist!"
1046                 );
1047                 $RT::Handle->Rollback();
1048                 return (undef);
1049             }
1050             $extra_item->Delete;
1051             $extra_values--;
1052
1053         }
1054     }
1055     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1056     my $val    = $newval->Create(
1057         ObjectType   => ref($obj),
1058         ObjectId     => $obj->Id,
1059         Content      => $args{'Content'},
1060         LargeContent => $args{'LargeContent'},
1061         ContentType  => $args{'ContentType'},
1062         CustomField  => $self->Id
1063     );
1064
1065     unless ($val) {
1066         $RT::Handle->Rollback();
1067         return ($val);
1068     }
1069
1070     $RT::Handle->Commit();
1071     return ($val);
1072
1073 }
1074
1075 # }}}
1076
1077 # {{{ MatchPattern
1078
1079 =head2 MatchPattern STRING
1080
1081 Tests the incoming string against the Pattern of this custom field object
1082 and returns a boolean; returns true if the Pattern is empty.
1083
1084 =cut
1085
1086 sub MatchPattern {
1087     my $self = shift;
1088     my $regex = $self->Pattern;
1089
1090     return 1 if !length($regex);
1091     return ($_[0] =~ $regex);
1092 }
1093
1094
1095 # }}}
1096
1097 # {{{ FriendlyPattern
1098
1099 =head2 FriendlyPattern
1100
1101 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1102 and localizing it.
1103
1104 =cut
1105
1106 sub FriendlyPattern {
1107     my $self = shift;
1108     my $regex = $self->Pattern;
1109
1110     return '' if !length($regex);
1111     if ($regex =~ /\(\?#([^)]*)\)/) {
1112         return '[' . $self->loc($1) . ']';
1113     }
1114     else {
1115         return $regex;
1116     }
1117 }
1118
1119
1120 # }}}
1121
1122 # {{{ DeleteValueForObject
1123
1124 =head2 DeleteValueForObject HASH
1125
1126 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1127
1128 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1129
1130 =cut
1131
1132 sub DeleteValueForObject {
1133     my $self = shift;
1134     my %args = ( Object => undef,
1135                  Content => undef,
1136                  Id => undef,
1137                      @_ );
1138
1139
1140     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1141         return (0, $self->loc('Permission Denied'));
1142     }
1143
1144     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1145
1146     if (my $id = $args{'Id'}) {
1147         $oldval->Load($id);
1148     }
1149     unless ($oldval->id) { 
1150         $oldval->LoadByObjectContentAndCustomField(
1151             Object => $args{'Object'}, 
1152             Content =>  $args{'Content'}, 
1153             CustomField => $self->Id,
1154         );
1155     }
1156
1157
1158     # check to make sure we found it
1159     unless ($oldval->Id) {
1160         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1161     }
1162
1163     # for single-value fields, we need to validate that empty string is a valid value for it
1164     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1165         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1166     }
1167
1168     # delete it
1169
1170     my $ret = $oldval->Delete();
1171     unless ($ret) {
1172         return(0, $self->loc("Custom field value could not be found"));
1173     }
1174     return($oldval->Id, $self->loc("Custom field value deleted"));
1175 }
1176
1177
1178 =head2 ValuesForObject OBJECT
1179
1180 Return an RT::ObjectCustomFieldValues object containing all of this custom field's values for OBJECT 
1181
1182 =cut
1183
1184 sub ValuesForObject {
1185         my $self = shift;
1186     my $object = shift;
1187
1188         my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
1189         unless ($self->CurrentUserHasRight('SeeCustomField')) {
1190         # Return an empty object if they have no rights to see
1191         return ($values);
1192     }
1193         
1194         
1195         $values->LimitToCustomField($self->Id);
1196         $values->LimitToEnabled();
1197     $values->LimitToObject($object);
1198
1199         return ($values);
1200 }
1201
1202
1203 =head2 _ForObjectType PATH FRIENDLYNAME
1204
1205 Tell RT that a certain object accepts custom fields
1206
1207 Examples:
1208
1209     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1210     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1211     'RT::User'                             => "Users",                  # loc
1212     'RT::Group'                            => "Groups",                 # loc
1213
1214 This is a class method. 
1215
1216 =cut
1217
1218 sub _ForObjectType {
1219     my $self = shift;
1220     my $path = shift;
1221     my $friendly_name = shift;
1222
1223     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1224
1225 }
1226
1227
1228 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1229
1230 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1231 uses this field to automatically include content into the user's browser
1232 as they display records with custom fields in RT.
1233
1234 =cut
1235
1236 sub SetIncludeContentForValue {
1237     shift->IncludeContentForValue(@_);
1238 }
1239 sub IncludeContentForValue{
1240     my $self = shift;
1241     $self->_URLTemplate('IncludeContentForValue', @_);
1242 }
1243
1244
1245
1246 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1247
1248 Gets or sets the  C<LinkValueTo> for this custom field. RT
1249 uses this field to make custom field values into hyperlinks in the user's
1250 browser as they display records with custom fields in RT.
1251
1252 =cut
1253
1254
1255 sub SetLinkValueTo {
1256     shift->LinkValueTo(@_);
1257 }
1258
1259 sub LinkValueTo {
1260     my $self = shift;
1261     $self->_URLTemplate('LinkValueTo', @_);
1262
1263 }
1264
1265
1266 =head2 _URLTemplate  NAME [VALUE]
1267
1268 With one argument, returns the _URLTemplate named C<NAME>, but only if
1269 the current user has the right to see this custom field.
1270
1271 With two arguments, attemptes to set the relevant template value.
1272
1273 =cut
1274
1275
1276
1277 sub _URLTemplate {
1278     my $self          = shift;
1279     my $template_name = shift;
1280     if (@_) {
1281
1282         my $value = shift;
1283         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1284             return ( 0, $self->loc('Permission Denied') );
1285         }
1286         $self->SetAttribute( Name => $template_name, Content => $value );
1287         return ( 1, $self->loc('Updated') );
1288     } else {
1289         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1290             return (undef);
1291         }
1292
1293         my @attr = $self->Attributes->Named($template_name);
1294         my $attr = shift @attr;
1295
1296         if ($attr) { return $attr->Content }
1297
1298     }
1299 }
1300 1;