This commit was manufactured by cvs2svn to create tag 'freeside_2_1_0'.
[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
49 package RT::CustomField;
50
51 use strict;
52 no warnings qw(redefine);
53
54 use RT::CustomFieldValues;
55 use RT::ObjectCustomFields;
56 use RT::ObjectCustomFieldValues;
57
58
59 our %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     Autocomplete => [
96         'Enter multiple values with autocompletion',    # loc
97         'Enter one value with autocompletion',            # loc
98         'Enter up to [_1] values with autocompletion',    # loc
99     ],
100 );
101
102
103 our %FRIENDLY_OBJECT_TYPES =  ();
104
105 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
106 RT::CustomField->_ForObjectType(
107     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", );    #loc
108 RT::CustomField->_ForObjectType( 'RT::User'  => "Users", );                           #loc
109 RT::CustomField->_ForObjectType( 'RT::Queue'  => "Queues", );                         #loc
110 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", );                          #loc
111
112 our $RIGHTS = {
113     SeeCustomField            => 'See custom fields',       # loc_pair
114     AdminCustomField          => 'Create, delete and modify custom fields',        # loc_pair
115     ModifyCustomField         => 'Add, delete and modify custom field values for objects' #loc_pair
116 };
117
118 # Tell RT::ACE that this sort of object can get acls granted
119 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
120
121 foreach my $right ( keys %{$RIGHTS} ) {
122     $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
123 }
124
125 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
126
127 Adds the given rights to the list of possible rights.  This method
128 should be called during server startup, not at runtime.
129
130 =cut
131
132 sub AddRights {
133     my $self = shift;
134     my %new = @_;
135     $RIGHTS = { %$RIGHTS, %new };
136     %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
137                                       map { lc($_) => $_ } keys %new);
138 }
139
140 sub AvailableRights {
141     my $self = shift;
142     return $RIGHTS;
143 }
144
145 =head1 NAME
146
147   RT::CustomField_Overlay - overlay for RT::CustomField
148
149 =head1 DESCRIPTION
150
151 =head1 'CORE' METHODS
152
153 =head2 Create PARAMHASH
154
155 Create takes a hash of values and creates a row in the database:
156
157   varchar(200) 'Name'.
158   varchar(200) 'Type'.
159   int(11) 'MaxValues'.
160   varchar(255) 'Pattern'.
161   smallint(6) 'Repeated'.
162   varchar(255) 'Description'.
163   int(11) 'SortOrder'.
164   varchar(255) 'LookupType'.
165   smallint(6) 'Disabled'.
166
167 C<LookupType> is generally the result of either
168 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
169
170 =cut
171
172 sub Create {
173     my $self = shift;
174     my %args = (
175         Name        => '',
176         Type        => '',
177         MaxValues   => 0,
178         Pattern     => '',
179         Description => '',
180         Disabled    => 0,
181         LookupType  => '',
182         Repeated    => 0,
183         LinkValueTo => '',
184         IncludeContentForValue => '',
185         @_,
186     );
187
188     unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
189         return (0, $self->loc('Permission Denied'));
190     }
191
192     if ( $args{TypeComposite} ) {
193         @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
194     }
195     elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
196         # old style Type string
197         $args{'MaxValues'} = $1 ? 1 : 0;
198     }
199     $args{'MaxValues'} = int $args{'MaxValues'};
200
201     if ( !exists $args{'Queue'}) {
202     # do nothing -- things below are strictly backward compat
203     }
204     elsif (  ! $args{'Queue'} ) {
205         unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
206             return ( 0, $self->loc('Permission Denied') );
207         }
208         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
209     }
210     else {
211         my $queue = RT::Queue->new($self->CurrentUser);
212         $queue->Load($args{'Queue'});
213         unless ($queue->Id) {
214             return (0, $self->loc("Queue not found"));
215         }
216         unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
217             return ( 0, $self->loc('Permission Denied') );
218         }
219         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
220         $args{'Queue'} = $queue->Id;
221     }
222
223     my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
224     return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
225
226     if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
227         $RT::Logger->warning("Support for 'multiple' Texts or Comboboxes is not implemented");
228         $args{'MaxValues'} = 1;
229     }
230
231     (my $rv, $msg) = $self->SUPER::Create(
232         Name        => $args{'Name'},
233         Type        => $args{'Type'},
234         MaxValues   => $args{'MaxValues'},
235         Pattern     => $args{'Pattern'},
236         Description => $args{'Description'},
237         Disabled    => $args{'Disabled'},
238         LookupType  => $args{'LookupType'},
239         Repeated    => $args{'Repeated'},
240     );
241
242     if ( exists $args{'LinkValueTo'}) {
243         $self->SetLinkValueTo($args{'LinkValueTo'});
244     }
245
246     if ( exists $args{'IncludeContentForValue'}) {
247         $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
248     }
249
250     if ( exists $args{'ValuesClass'} ) {
251         $self->SetValuesClass( $args{'ValuesClass'} );
252     }
253
254     if ( exists $args{'BasedOn'} ) {
255         $self->SetBasedOn( $args{'BasedOn'} );
256     }
257
258     return ($rv, $msg) unless exists $args{'Queue'};
259
260     # Compat code -- create a new ObjectCustomField mapping
261     my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
262     $OCF->Create(
263         CustomField => $self->Id,
264         ObjectId => $args{'Queue'},
265     );
266
267     return ($rv, $msg);
268 }
269
270 =head2 Load ID/NAME
271
272 Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
273
274 =cut
275
276 sub Load {
277     my $self = shift;
278     my $id = shift || '';
279
280     if ( $id =~ /^\d+$/ ) {
281         return $self->SUPER::Load( $id );
282     } else {
283         return $self->LoadByName( Name => $id );
284     }
285 }
286
287
288 # {{{ sub LoadByName
289
290 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
291
292 Loads the Custom field named NAME.
293
294 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
295 with the same Name.
296
297 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
298
299 If the Queue parameter is '0', look for global ticket custom fields.
300
301 If no queue parameter is specified, look for any and all custom fields with this name.
302
303 BUG/TODO, this won't let you specify that you only want user or group CFs.
304
305 =cut
306
307 # Compatibility for API change after 3.0 beta 1
308 *LoadNameAndQueue = \&LoadByName;
309 # Change after 3.4 beta.
310 *LoadByNameAndQueue = \&LoadByName;
311
312 sub LoadByName {
313     my $self = shift;
314     my %args = (
315         Queue => undef,
316         Name  => undef,
317         @_,
318     );
319
320     unless ( defined $args{'Name'} && length $args{'Name'} ) {
321         $RT::Logger->error("Couldn't load Custom Field without Name");
322         return wantarray ? (0, $self->loc("No name provided")) : 0;
323     }
324
325     # if we're looking for a queue by name, make it a number
326     if ( defined $args{'Queue'} && $args{'Queue'} =~ /\D/ ) {
327         my $QueueObj = RT::Queue->new( $self->CurrentUser );
328         $QueueObj->Load( $args{'Queue'} );
329         $args{'Queue'} = $QueueObj->Id;
330     }
331
332     # XXX - really naive implementation.  Slow. - not really. still just one query
333
334     my $CFs = RT::CustomFields->new( $self->CurrentUser );
335     $CFs->SetContextObject( $self->ContextObject );
336     my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
337     $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
338     # Don't limit to queue if queue is 0.  Trying to do so breaks
339     # RT::Group type CFs.
340     if ( defined $args{'Queue'} ) {
341         $CFs->LimitToQueue( $args{'Queue'} );
342     }
343
344     # When loading by name, we _can_ load disabled fields, but prefer
345     # non-disabled fields.
346     $CFs->FindAllRows;
347     $CFs->OrderByCols(
348         { FIELD => "Disabled", ORDER => 'ASC' },
349     );
350
351     # We only want one entry.
352     $CFs->RowsPerPage(1);
353
354     # version before 3.8 just returns 0, so we need to test if wantarray to be
355     # backward compatible.
356     return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
357
358     return $self->LoadById( $first->id );
359 }
360
361 # }}}
362
363 # {{{ Dealing with custom field values 
364
365
366 =head2 Custom field values
367
368 =head3 Values FIELD
369
370 Return a object (collection) of all acceptable values for this Custom Field.
371 Class of the object can vary and depends on the return value
372 of the C<ValuesClass> method.
373
374 =cut
375
376 *ValuesObj = \&Values;
377
378 sub Values {
379     my $self = shift;
380
381     my $class = $self->ValuesClass || 'RT::CustomFieldValues';
382     eval "require $class" or die "$@";
383     my $cf_values = $class->new( $self->CurrentUser );
384     # if the user has no rights, return an empty object
385     if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
386         $cf_values->LimitToCustomField( $self->Id );
387     }
388     return ($cf_values);
389 }
390
391 # {{{ AddValue
392
393 =head3 AddValue HASH
394
395 Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
396
397 =cut
398
399 sub AddValue {
400     my $self = shift;
401     my %args = @_;
402
403     unless ($self->CurrentUserHasRight('AdminCustomField')) {
404         return (0, $self->loc('Permission Denied'));
405     }
406
407     # allow zero value
408     if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
409         return (0, $self->loc("Can't add a custom field value without a name"));
410     }
411
412     my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
413     return $newval->Create( %args, CustomField => $self->Id );
414 }
415
416
417 # }}}
418
419 # {{{ DeleteValue
420
421 =head3 DeleteValue ID
422
423 Deletes a value from this custom field by id.
424
425 Does not remove this value for any article which has had it selected
426
427 =cut
428
429 sub DeleteValue {
430     my $self = shift;
431     my $id = shift;
432     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
433         return (0, $self->loc('Permission Denied'));
434     }
435
436     my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
437     $val_to_del->Load( $id );
438     unless ( $val_to_del->Id ) {
439         return (0, $self->loc("Couldn't find that value"));
440     }
441     unless ( $val_to_del->CustomField == $self->Id ) {
442         return (0, $self->loc("That is not a value for this custom field"));
443     }
444
445     my $retval = $val_to_del->Delete;
446     unless ( $retval ) {
447         return (0, $self->loc("Custom field value could not be deleted"));
448     }
449     return ($retval, $self->loc("Custom field value deleted"));
450 }
451
452 # }}}
453
454
455 =head2 ValidateQueue Queue
456
457 Make sure that the queue specified is a valid queue name
458
459 =cut
460
461 sub ValidateQueue {
462     my $self = shift;
463     my $id = shift;
464
465     return undef unless defined $id;
466     # 0 means "Global" null would _not_ be ok.
467     return 1 if $id eq '0';
468
469     my $q = RT::Queue->new( $RT::SystemUser );
470     $q->Load( $id );
471     return undef unless $q->id;
472     return 1;
473 }
474
475
476 # {{{ Types
477
478 =head2 Types 
479
480 Retuns an array of the types of CustomField that are supported
481
482 =cut
483
484 sub Types {
485     return (keys %FieldTypes);
486 }
487
488 # }}}
489
490 # {{{ IsSelectionType
491
492 =head2 IsSelectionType 
493
494 Retuns a boolean value indicating whether the C<Values> method makes sense
495 to this Custom Field.
496
497 =cut
498
499 sub IsSelectionType {
500     my $self = shift;
501     my $type = @_? shift : $self->Type;
502     return undef unless $type;
503
504     $type =~ /(?:Select|Combobox|Autocomplete)/;
505 }
506
507 # }}}
508
509
510 =head2 IsExternalValues
511
512 =cut
513
514 sub IsExternalValues {
515     my $self = shift;
516     my $selectable = $self->IsSelectionType( @_ );
517     return $selectable unless $selectable;
518
519     my $class = $self->ValuesClass;
520     return 0 if $class eq 'RT::CustomFieldValues';
521     return 1;
522 }
523
524 sub ValuesClass {
525     my $self = shift;
526     return '' unless $self->IsSelectionType;
527
528     my $class = $self->FirstAttribute( 'ValuesClass' );
529     $class = $class->Content if $class;
530     return $class || 'RT::CustomFieldValues';
531 }
532
533 sub SetValuesClass {
534     my $self = shift;
535     my $class = shift || 'RT::CustomFieldValues';
536
537     if( $class eq 'RT::CustomFieldValues' ) {
538         return $self->DeleteAttribute( 'ValuesClass' );
539     }
540     return $self->SetAttribute( Name => 'ValuesClass', Content => $class );
541 }
542
543
544 =head2 FriendlyType [TYPE, MAX_VALUES]
545
546 Returns a localized human-readable version of the custom field type.
547 If a custom field type is specified as the parameter, the friendly type for that type will be returned
548
549 =cut
550
551 sub FriendlyType {
552     my $self = shift;
553
554     my $type = @_ ? shift : $self->Type;
555     my $max  = @_ ? shift : $self->MaxValues;
556     $max = 0 unless $max;
557
558     if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
559         return ( $self->loc( $friendly_type, $max ) );
560     }
561     else {
562         return ( $self->loc( $type ) );
563     }
564 }
565
566 sub FriendlyTypeComposite {
567     my $self = shift;
568     my $composite = shift || $self->TypeComposite;
569     return $self->FriendlyType(split(/-/, $composite, 2));
570 }
571
572
573 =head2 ValidateType TYPE
574
575 Takes a single string. returns true if that string is a value
576 type of custom field
577
578
579 =cut
580
581 sub ValidateType {
582     my $self = shift;
583     my $type = shift;
584
585     if ( $type =~ s/(?:Single|Multiple)$// ) {
586         $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
587     }
588
589     if ( $FieldTypes{$type} ) {
590         return 1;
591     }
592     else {
593         return undef;
594     }
595 }
596
597
598 sub SetType {
599     my $self = shift;
600     my $type = shift;
601     if ($type =~ s/(?:(Single)|Multiple)$//) {
602         $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
603         $self->SetMaxValues($1 ? 1 : 0);
604     }
605     $self->SUPER::SetType($type);
606 }
607
608 =head2 SetPattern STRING
609
610 Takes a single string representing a regular expression.  Performs basic
611 validation on that regex, and sets the C<Pattern> field for the CF if it
612 is valid.
613
614 =cut
615
616 sub SetPattern {
617     my $self = shift;
618     my $regex = shift;
619
620     my ($ok, $msg) = $self->_IsValidRegex($regex);
621     if ($ok) {
622         return $self->SUPER::SetPattern($regex);
623     }
624     else {
625         return (0, $self->loc("Invalid pattern: [_1]", $msg));
626     }
627 }
628
629 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
630
631 Tests if the string contains an invalid regex.
632
633 =cut
634
635 sub _IsValidRegex {
636     my $self  = shift;
637     my $regex = shift or return (1, 'valid');
638
639     local $^W; local $@;
640     local $SIG{__DIE__} = sub { 1 };
641     local $SIG{__WARN__} = sub { 1 };
642
643     if (eval { qr/$regex/; 1 }) {
644         return (1, 'valid');
645     }
646
647     my $err = $@;
648     $err =~ s{[,;].*}{};    # strip debug info from error
649     chomp $err;
650     return (0, $err);
651 }
652
653 # {{{ SingleValue
654
655 =head2 SingleValue
656
657 Returns true if this CustomField only accepts a single value. 
658 Returns false if it accepts multiple values
659
660 =cut
661
662 sub SingleValue {
663     my $self = shift;
664     if (($self->MaxValues||0) == 1) {
665         return 1;
666     } 
667     else {
668         return undef;
669     }
670 }
671
672 sub UnlimitedValues {
673     my $self = shift;
674     if (($self->MaxValues||0) == 0) {
675         return 1;
676     } 
677     else {
678         return undef;
679     }
680 }
681
682 # }}}
683
684 =head2 CurrentUserHasRight RIGHT
685
686 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
687
688 =cut
689
690 sub CurrentUserHasRight {
691     my $self  = shift;
692     my $right = shift;
693
694     return $self->CurrentUser->HasRight(
695         Object => $self,
696         Right  => $right,
697     );
698 }
699
700 =head2 ACLEquivalenceObjects
701
702 Returns list of objects via which users can get rights on this custom field. For custom fields
703 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
704
705 =cut
706
707 sub ACLEquivalenceObjects {
708     my $self = shift;
709
710     my $ctx = $self->ContextObject
711         or return;
712     return ($ctx, $ctx->ACLEquivalenceObjects);
713 }
714
715 =head2 ContextObject and SetContextObject
716
717 Set or get a context for this object. It can be ticket, queue or another object
718 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
719 queue level to allow people to see all fields applied to the queue.
720
721 =cut
722
723 sub SetContextObject {
724     my $self = shift;
725     return $self->{'context_object'} = shift;
726 }
727   
728 sub ContextObject {
729     my $self = shift;
730     return $self->{'context_object'};
731 }
732   
733 # {{{ sub _Set
734
735 sub _Set {
736     my $self = shift;
737
738     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
739         return ( 0, $self->loc('Permission Denied') );
740     }
741     return $self->SUPER::_Set( @_ );
742
743 }
744
745 # }}}
746
747 # {{{ sub _Value 
748
749 =head2 _Value
750
751 Takes the name of a table column.
752 Returns its value as a string, if the user passes an ACL check
753
754 =cut
755
756 sub _Value {
757     my $self  = shift;
758     return undef unless $self->id;
759
760     # we need to do the rights check
761     unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
762         $RT::Logger->debug(
763             "Permission denied. User #". $self->CurrentUser->id
764             ." has no SeeCustomField right on CF #". $self->id
765         );
766         return (undef);
767     }
768     return $self->__Value( @_ );
769 }
770
771 # }}}
772 # {{{ sub SetDisabled
773
774 =head2 SetDisabled
775
776 Takes a boolean.
777 1 will cause this custom field to no longer be avaialble for objects.
778 0 will re-enable this field.
779
780 =cut
781
782 # }}}
783
784 =head2 SetTypeComposite
785
786 Set this custom field's type and maximum values as a composite value
787
788 =cut
789
790 sub SetTypeComposite {
791     my $self = shift;
792     my $composite = shift;
793
794     my $old = $self->TypeComposite;
795
796     my ($type, $max_values) = split(/-/, $composite, 2);
797     if ( $type ne $self->Type ) {
798         my ($status, $msg) = $self->SetType( $type );
799         return ($status, $msg) unless $status;
800     }
801     if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
802         my ($status, $msg) = $self->SetMaxValues( $max_values );
803         return ($status, $msg) unless $status;
804     }
805     return 1, $self->loc(
806         "Type changed from '[_1]' to '[_2]'",
807         $self->FriendlyTypeComposite( $old ),
808         $self->FriendlyTypeComposite( $composite ),
809     );
810 }
811
812 =head2 TypeComposite
813
814 Returns a composite value composed of this object's type and maximum values
815
816 =cut
817
818
819 sub TypeComposite {
820     my $self = shift;
821     return join '-', ($self->Type || ''), ($self->MaxValues || 0);
822 }
823
824 =head2 TypeComposites
825
826 Returns an array of all possible composite values for custom fields.
827
828 =cut
829
830 sub TypeComposites {
831     my $self = shift;
832     return grep !/(?:[Tt]ext|Combobox)-0/, map { ("$_-1", "$_-0") } $self->Types;
833 }
834
835 =head2 SetLookupType
836
837 Autrijus: care to doc how LookupTypes work?
838
839 =cut
840
841 sub SetLookupType {
842     my $self = shift;
843     my $lookup = shift;
844     if ( $lookup ne $self->LookupType ) {
845         # Okay... We need to invalidate our existing relationships
846         my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
847         $ObjectCustomFields->LimitToCustomField($self->Id);
848         $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
849     }
850     return $self->SUPER::SetLookupType($lookup);
851 }
852
853 =head2 LookupTypes
854
855 Returns an array of LookupTypes available
856
857 =cut
858
859
860 sub LookupTypes {
861     my $self = shift;
862     return keys %FRIENDLY_OBJECT_TYPES;
863 }
864
865 my @FriendlyObjectTypes = (
866     "[_1] objects",            # loc
867     "[_1]'s [_2] objects",        # loc
868     "[_1]'s [_2]'s [_3] objects",   # loc
869 );
870
871 =head2 FriendlyLookupType
872
873 Returns a localized description of the type of this custom field
874
875 =cut
876
877 sub FriendlyLookupType {
878     my $self = shift;
879     my $lookup = shift || $self->LookupType;
880    
881     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
882                      if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
883
884     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
885       grep { defined and length }
886       split( /-/, $lookup )
887       or return;
888     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
889 }
890
891 sub RecordClassFromLookupType {
892     my $self = shift;
893     my ($class) = ($self->LookupType =~ /^([^-]+)/);
894     unless ( $class ) {
895         $RT::Logger->error(
896             "Custom Field #". $self->id 
897             ." has incorrect LookupType '". $self->LookupType ."'"
898         );
899         return undef;
900     }
901     return $class;
902 }
903
904 sub CollectionClassFromLookupType {
905     my $self = shift;
906
907     my $record_class = $self->RecordClassFromLookupType;
908     return undef unless $record_class;
909
910     my $collection_class;
911     if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
912         $collection_class = $record_class.'Collection';
913     } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
914         $collection_class = $record_class.'es';
915     } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
916         $collection_class = $record_class.'s';
917     } else {
918         $RT::Logger->error("Can not find a collection class for record class '$record_class'");
919         return undef;
920     }
921     return $collection_class;
922 }
923
924 =head1 AppliedTo
925
926 Returns collection with objects this custom field is applied to.
927 Class of the collection depends on L</LookupType>.
928 See all L</NotAppliedTo> .
929
930 Doesn't takes into account if object is applied globally.
931
932 =cut
933
934 sub AppliedTo {
935     my $self = shift;
936
937     my ($res, $ocfs_alias) = $self->_AppliedTo;
938     return $res unless $res;
939
940     $res->Limit(
941         ALIAS     => $ocfs_alias,
942         FIELD     => 'id',
943         OPERATOR  => 'IS NOT',
944         VALUE     => 'NULL',
945     );
946
947     return $res;
948 }
949
950 =head1 NotAppliedTo
951
952 Returns collection with objects this custom field is not applied to.
953 Class of the collection depends on L</LookupType>.
954 See all L</AppliedTo> .
955
956 Doesn't takes into account if object is applied globally.
957
958 =cut
959
960 sub NotAppliedTo {
961     my $self = shift;
962
963     my ($res, $ocfs_alias) = $self->_AppliedTo;
964     return $res unless $res;
965
966     $res->Limit(
967         ALIAS     => $ocfs_alias,
968         FIELD     => 'id',
969         OPERATOR  => 'IS',
970         VALUE     => 'NULL',
971     );
972
973     return $res;
974 }
975
976 sub _AppliedTo {
977     my $self = shift;
978
979     my ($class) = $self->CollectionClassFromLookupType;
980     return undef unless $class;
981
982     my $res = $class->new( $self->CurrentUser );
983
984     # If CF is a Group CF, only display user-defined groups
985     if ( $class eq 'RT::Groups' ) {
986         $res->LimitToUserDefinedGroups;
987     }
988
989     $res->OrderBy( FIELD => 'Name' );
990     my $ocfs_alias = $res->Join(
991         TYPE   => 'LEFT',
992         ALIAS1 => 'main',
993         FIELD1 => 'id',
994         TABLE2 => 'ObjectCustomFields',
995         FIELD2 => 'ObjectId',
996     );
997     $res->Limit(
998         LEFTJOIN => $ocfs_alias,
999         ALIAS    => $ocfs_alias,
1000         FIELD    => 'CustomField',
1001         VALUE    => $self->id,
1002     );
1003     return ($res, $ocfs_alias);
1004 }
1005
1006 =head2 IsApplied
1007
1008 Takes object id and returns corresponding L<RT::ObjectCustomField>
1009 record if this custom field is applied to the object. Use 0 to check
1010 if custom field is applied globally.
1011
1012 =cut
1013
1014 sub IsApplied {
1015     my $self = shift;
1016     my $id = shift;
1017     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1018     $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1019     return undef unless $ocf->id;
1020     return $ocf;
1021 }
1022
1023 =head2 AddToObject OBJECT
1024
1025 Add this custom field as a custom field for a single object, such as a queue or group.
1026
1027 Takes an object 
1028
1029 =cut
1030
1031
1032 sub AddToObject {
1033     my $self  = shift;
1034     my $object = shift;
1035     my $id = $object->Id || 0;
1036
1037     unless (index($self->LookupType, ref($object)) == 0) {
1038         return ( 0, $self->loc('Lookup type mismatch') );
1039     }
1040
1041     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1042         return ( 0, $self->loc('Permission Denied') );
1043     }
1044
1045     if ( $self->IsApplied( $id ) ) {
1046         return ( 0, $self->loc("Custom field is already applied to the object") );
1047     }
1048
1049     if ( $id ) {
1050         # applying locally
1051         return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1052             if $self->IsApplied( 0 );
1053     }
1054     else {
1055         my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1056         $applied->LimitToCustomField( $self->id );
1057         while ( my $record = $applied->Next ) {
1058             $record->Delete;
1059         }
1060     }
1061
1062     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1063     my ( $oid, $msg ) = $ocf->Create(
1064         ObjectId => $id, CustomField => $self->id,
1065     );
1066     return ( $oid, $msg );
1067 }
1068
1069
1070 =head2 RemoveFromObject OBJECT
1071
1072 Remove this custom field  for a single object, such as a queue or group.
1073
1074 Takes an object 
1075
1076 =cut
1077
1078 sub RemoveFromObject {
1079     my $self = shift;
1080     my $object = shift;
1081     my $id = $object->Id || 0;
1082
1083     unless (index($self->LookupType, ref($object)) == 0) {
1084         return ( 0, $self->loc('Object type mismatch') );
1085     }
1086
1087     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1088         return ( 0, $self->loc('Permission Denied') );
1089     }
1090
1091     my $ocf = $self->IsApplied( $id );
1092     unless ( $ocf ) {
1093         return ( 0, $self->loc("This custom field does not apply to that object") );
1094     }
1095
1096     # XXX: Delete doesn't return anything
1097     my ( $oid, $msg ) = $ocf->Delete;
1098     return ( $oid, $msg );
1099 }
1100
1101 # {{{ AddValueForObject
1102
1103 =head2 AddValueForObject HASH
1104
1105 Adds a custom field value for a record object of some kind. 
1106 Takes a param hash of 
1107
1108 Required:
1109
1110     Object
1111     Content
1112
1113 Optional:
1114
1115     LargeContent
1116     ContentType
1117
1118 =cut
1119
1120 sub AddValueForObject {
1121     my $self = shift;
1122     my %args = (
1123         Object       => undef,
1124         Content      => undef,
1125         LargeContent => undef,
1126         ContentType  => undef,
1127         @_
1128     );
1129     my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1130
1131     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1132         return ( 0, $self->loc('Permission Denied') );
1133     }
1134
1135     unless ( $self->MatchPattern($args{'Content'}) ) {
1136         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1137     }
1138
1139     $RT::Handle->BeginTransaction;
1140
1141     if ( $self->MaxValues ) {
1142         my $current_values = $self->ValuesForObject($obj);
1143         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1144
1145         # (The +1 is for the new value we're adding)
1146
1147         # If we have a set of current values and we've gone over the maximum
1148         # allowed number of values, we'll need to delete some to make room.
1149         # which former values are blown away is not guaranteed
1150
1151         while ($extra_values) {
1152             my $extra_item = $current_values->Next;
1153             unless ( $extra_item->id ) {
1154                 $RT::Logger->crit( "We were just asked to delete "
1155                     ."a custom field value that doesn't exist!" );
1156                 $RT::Handle->Rollback();
1157                 return (undef);
1158             }
1159             $extra_item->Delete;
1160             $extra_values--;
1161         }
1162     }
1163     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1164     my $val    = $newval->Create(
1165         ObjectType   => ref($obj),
1166         ObjectId     => $obj->Id,
1167         Content      => $args{'Content'},
1168         LargeContent => $args{'LargeContent'},
1169         ContentType  => $args{'ContentType'},
1170         CustomField  => $self->Id
1171     );
1172
1173     unless ($val) {
1174         $RT::Handle->Rollback();
1175         return ($val, $self->loc("Couldn't create record"));
1176     }
1177
1178     $RT::Handle->Commit();
1179     return ($val);
1180
1181 }
1182
1183 # }}}
1184
1185 # {{{ MatchPattern
1186
1187 =head2 MatchPattern STRING
1188
1189 Tests the incoming string against the Pattern of this custom field object
1190 and returns a boolean; returns true if the Pattern is empty.
1191
1192 =cut
1193
1194 sub MatchPattern {
1195     my $self = shift;
1196     my $regex = $self->Pattern or return 1;
1197
1198     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1199 }
1200
1201
1202 # }}}
1203
1204 # {{{ FriendlyPattern
1205
1206 =head2 FriendlyPattern
1207
1208 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1209 and localizing it.
1210
1211 =cut
1212
1213 sub FriendlyPattern {
1214     my $self = shift;
1215     my $regex = $self->Pattern;
1216
1217     return '' unless length $regex;
1218     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1219         return '[' . $self->loc($1) . ']';
1220     }
1221     else {
1222         return $regex;
1223     }
1224 }
1225
1226
1227 # }}}
1228
1229 # {{{ DeleteValueForObject
1230
1231 =head2 DeleteValueForObject HASH
1232
1233 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1234
1235 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1236
1237 =cut
1238
1239 sub DeleteValueForObject {
1240     my $self = shift;
1241     my %args = ( Object => undef,
1242                  Content => undef,
1243                  Id => undef,
1244              @_ );
1245
1246
1247     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1248         return (0, $self->loc('Permission Denied'));
1249     }
1250
1251     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1252
1253     if (my $id = $args{'Id'}) {
1254         $oldval->Load($id);
1255     }
1256     unless ($oldval->id) { 
1257         $oldval->LoadByObjectContentAndCustomField(
1258             Object => $args{'Object'}, 
1259             Content =>  $args{'Content'}, 
1260             CustomField => $self->Id,
1261         );
1262     }
1263
1264
1265     # check to make sure we found it
1266     unless ($oldval->Id) {
1267         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1268     }
1269
1270     # for single-value fields, we need to validate that empty string is a valid value for it
1271     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1272         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1273     }
1274
1275     # delete it
1276
1277     my $ret = $oldval->Delete();
1278     unless ($ret) {
1279         return(0, $self->loc("Custom field value could not be found"));
1280     }
1281     return($oldval->Id, $self->loc("Custom field value deleted"));
1282 }
1283
1284
1285 =head2 ValuesForObject OBJECT
1286
1287 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1288
1289 =cut
1290
1291 sub ValuesForObject {
1292     my $self = shift;
1293     my $object = shift;
1294
1295     my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
1296     unless ($self->CurrentUserHasRight('SeeCustomField')) {
1297         # Return an empty object if they have no rights to see
1298         return ($values);
1299     }
1300     
1301     
1302     $values->LimitToCustomField($self->Id);
1303     $values->LimitToEnabled();
1304     $values->LimitToObject($object);
1305
1306     return ($values);
1307 }
1308
1309
1310 =head2 _ForObjectType PATH FRIENDLYNAME
1311
1312 Tell RT that a certain object accepts custom fields
1313
1314 Examples:
1315
1316     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1317     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1318     'RT::User'                             => "Users",                  # loc
1319     'RT::Group'                            => "Groups",                 # loc
1320
1321 This is a class method. 
1322
1323 =cut
1324
1325 sub _ForObjectType {
1326     my $self = shift;
1327     my $path = shift;
1328     my $friendly_name = shift;
1329
1330     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1331
1332 }
1333
1334
1335 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1336
1337 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1338 uses this field to automatically include content into the user's browser
1339 as they display records with custom fields in RT.
1340
1341 =cut
1342
1343 sub SetIncludeContentForValue {
1344     shift->IncludeContentForValue(@_);
1345 }
1346 sub IncludeContentForValue{
1347     my $self = shift;
1348     $self->_URLTemplate('IncludeContentForValue', @_);
1349 }
1350
1351
1352
1353 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1354
1355 Gets or sets the  C<LinkValueTo> for this custom field. RT
1356 uses this field to make custom field values into hyperlinks in the user's
1357 browser as they display records with custom fields in RT.
1358
1359 =cut
1360
1361
1362 sub SetLinkValueTo {
1363     shift->LinkValueTo(@_);
1364 }
1365
1366 sub LinkValueTo {
1367     my $self = shift;
1368     $self->_URLTemplate('LinkValueTo', @_);
1369
1370 }
1371
1372
1373 =head2 _URLTemplate  NAME [VALUE]
1374
1375 With one argument, returns the _URLTemplate named C<NAME>, but only if
1376 the current user has the right to see this custom field.
1377
1378 With two arguments, attemptes to set the relevant template value.
1379
1380 =cut
1381
1382 sub _URLTemplate {
1383     my $self          = shift;
1384     my $template_name = shift;
1385     if (@_) {
1386
1387         my $value = shift;
1388         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1389             return ( 0, $self->loc('Permission Denied') );
1390         }
1391         $self->SetAttribute( Name => $template_name, Content => $value );
1392         return ( 1, $self->loc('Updated') );
1393     } else {
1394         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1395             return (undef);
1396         }
1397
1398         my @attr = $self->Attributes->Named($template_name);
1399         my $attr = shift @attr;
1400
1401         if ($attr) { return $attr->Content }
1402
1403     }
1404 }
1405
1406 sub SetBasedOn {
1407     my $self = shift;
1408     my $value = shift;
1409
1410     return $self->DeleteAttribute( "BasedOn" )
1411         unless defined $value and length $value;
1412
1413     my $cf = RT::CustomField->new( $self->CurrentUser );
1414     $cf->Load( ref $value ? $value->Id : $value );
1415
1416     return (0, "Permission denied")
1417         unless $cf->Id && $cf->CurrentUserHasRight('SeeCustomField');
1418
1419     return $self->AddAttribute(
1420         Name => "BasedOn",
1421         Description => "Custom field whose CF we depend on",
1422         Content => $cf->Id,
1423     );
1424 }
1425
1426 sub BasedOnObj {
1427     my $self = shift;
1428     my $obj = RT::CustomField->new( $self->CurrentUser );
1429
1430     my $attribute = $self->FirstAttribute("BasedOn");
1431     $obj->Load($attribute->Content) if defined $attribute;
1432     return $obj;
1433 }
1434
1435 1;