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