]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | # BEGIN BPS TAGGED BLOCK {{{ |
2 | # | |
3 | # COPYRIGHT: | |
4 | # | |
320f0092 | 5 | # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC |
84fb5b46 MKG |
6 | # <sales@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 | use warnings; | |
af59614d | 53 | use 5.010; |
84fb5b46 | 54 | |
01e3b242 | 55 | use Scalar::Util 'blessed'; |
84fb5b46 MKG |
56 | |
57 | use base 'RT::Record'; | |
58 | ||
af59614d MKG |
59 | use Role::Basic 'with'; |
60 | with "RT::Record::Role::Rights"; | |
84fb5b46 | 61 | |
af59614d | 62 | sub Table {'CustomFields'} |
84fb5b46 | 63 | |
af59614d | 64 | use Scalar::Util qw(blessed); |
84fb5b46 MKG |
65 | use RT::CustomFieldValues; |
66 | use RT::ObjectCustomFields; | |
67 | use RT::ObjectCustomFieldValues; | |
68 | ||
69 | our %FieldTypes = ( | |
70 | Select => { | |
71 | sort_order => 10, | |
72 | selection_type => 1, | |
73 | ||
c33a4027 MKG |
74 | labels => [ 'Select multiple values', # loc |
75 | 'Select one value', # loc | |
76 | 'Select up to [quant,_1,value,values]', # loc | |
84fb5b46 MKG |
77 | ], |
78 | ||
79 | render_types => { | |
80 | multiple => [ | |
81 | ||
82 | # Default is the first one | |
83 | 'Select box', # loc | |
84 | 'List', # loc | |
85 | ], | |
86 | single => [ 'Select box', # loc | |
87 | 'Dropdown', # loc | |
88 | 'List', # loc | |
89 | ] | |
90 | }, | |
91 | ||
92 | }, | |
93 | Freeform => { | |
94 | sort_order => 20, | |
95 | selection_type => 0, | |
96 | ||
c33a4027 MKG |
97 | labels => [ 'Enter multiple values', # loc |
98 | 'Enter one value', # loc | |
99 | 'Enter up to [quant,_1,value,values]', # loc | |
84fb5b46 MKG |
100 | ] |
101 | }, | |
102 | Text => { | |
103 | sort_order => 30, | |
104 | selection_type => 0, | |
105 | labels => [ | |
c33a4027 MKG |
106 | 'Fill in multiple text areas', # loc |
107 | 'Fill in one text area', # loc | |
108 | 'Fill in up to [quant,_1,text area,text areas]', # loc | |
84fb5b46 MKG |
109 | ] |
110 | }, | |
111 | Wikitext => { | |
112 | sort_order => 40, | |
113 | selection_type => 0, | |
114 | labels => [ | |
c33a4027 MKG |
115 | 'Fill in multiple wikitext areas', # loc |
116 | 'Fill in one wikitext area', # loc | |
117 | 'Fill in up to [quant,_1,wikitext area,wikitext areas]', # loc | |
84fb5b46 MKG |
118 | ] |
119 | }, | |
120 | ||
121 | Image => { | |
122 | sort_order => 50, | |
123 | selection_type => 0, | |
124 | labels => [ | |
125 | 'Upload multiple images', # loc | |
126 | 'Upload one image', # loc | |
c33a4027 | 127 | 'Upload up to [quant,_1,image,images]', # loc |
84fb5b46 MKG |
128 | ] |
129 | }, | |
130 | Binary => { | |
131 | sort_order => 60, | |
132 | selection_type => 0, | |
133 | labels => [ | |
c33a4027 MKG |
134 | 'Upload multiple files', # loc |
135 | 'Upload one file', # loc | |
136 | 'Upload up to [quant,_1,file,files]', # loc | |
84fb5b46 MKG |
137 | ] |
138 | }, | |
139 | ||
140 | Combobox => { | |
141 | sort_order => 70, | |
142 | selection_type => 1, | |
143 | labels => [ | |
c33a4027 MKG |
144 | 'Combobox: Select or enter multiple values', # loc |
145 | 'Combobox: Select or enter one value', # loc | |
146 | 'Combobox: Select or enter up to [quant,_1,value,values]', # loc | |
84fb5b46 MKG |
147 | ] |
148 | }, | |
149 | Autocomplete => { | |
150 | sort_order => 80, | |
151 | selection_type => 1, | |
152 | labels => [ | |
c33a4027 MKG |
153 | 'Enter multiple values with autocompletion', # loc |
154 | 'Enter one value with autocompletion', # loc | |
155 | 'Enter up to [quant,_1,value,values] with autocompletion', # loc | |
84fb5b46 MKG |
156 | ] |
157 | }, | |
158 | ||
159 | Date => { | |
160 | sort_order => 90, | |
161 | selection_type => 0, | |
162 | labels => [ | |
c33a4027 MKG |
163 | 'Select multiple dates', # loc |
164 | 'Select date', # loc | |
165 | 'Select up to [quant,_1,date,dates]', # loc | |
84fb5b46 MKG |
166 | ] |
167 | }, | |
168 | DateTime => { | |
169 | sort_order => 100, | |
170 | selection_type => 0, | |
171 | labels => [ | |
c33a4027 MKG |
172 | 'Select multiple datetimes', # loc |
173 | 'Select datetime', # loc | |
174 | 'Select up to [quant,_1,datetime,datetimes]', # loc | |
84fb5b46 MKG |
175 | ] |
176 | }, | |
177 | ||
178 | IPAddress => { | |
179 | sort_order => 110, | |
180 | selection_type => 0, | |
181 | ||
c33a4027 MKG |
182 | labels => [ 'Enter multiple IP addresses', # loc |
183 | 'Enter one IP address', # loc | |
184 | 'Enter up to [quant,_1,IP address,IP addresses]', # loc | |
84fb5b46 MKG |
185 | ] |
186 | }, | |
187 | IPAddressRange => { | |
188 | sort_order => 120, | |
189 | selection_type => 0, | |
190 | ||
c33a4027 MKG |
191 | labels => [ 'Enter multiple IP address ranges', # loc |
192 | 'Enter one IP address range', # loc | |
193 | 'Enter up to [quant,_1,IP address range,IP address ranges]', # loc | |
84fb5b46 MKG |
194 | ] |
195 | }, | |
196 | ); | |
197 | ||
198 | ||
af59614d MKG |
199 | my %BUILTIN_GROUPINGS; |
200 | my %FRIENDLY_LOOKUP_TYPES = (); | |
84fb5b46 | 201 | |
af59614d MKG |
202 | __PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc |
203 | __PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc | |
204 | __PACKAGE__->RegisterLookupType( 'RT::User' => "Users", ); #loc | |
205 | __PACKAGE__->RegisterLookupType( 'RT::Queue' => "Queues", ); #loc | |
206 | __PACKAGE__->RegisterLookupType( 'RT::Group' => "Groups", ); #loc | |
84fb5b46 | 207 | |
af59614d MKG |
208 | __PACKAGE__->RegisterBuiltInGroupings( |
209 | 'RT::Ticket' => [ qw(Basics Dates Links People) ], | |
210 | 'RT::User' => [ 'Identity', 'Access control', 'Location', 'Phones' ], | |
211 | ); | |
84fb5b46 | 212 | |
320f0092 MKG |
213 | __PACKAGE__->AddRight( General => SeeCustomField => 'View custom fields'); # loc |
214 | __PACKAGE__->AddRight( Admin => AdminCustomField => 'Create, modify and delete custom fields'); # loc | |
215 | __PACKAGE__->AddRight( Admin => AdminCustomFieldValues => 'Create, modify and delete custom fields values'); # loc | |
216 | __PACKAGE__->AddRight( Staff => ModifyCustomField => 'Add, modify and delete custom field values for objects'); # loc | |
84fb5b46 MKG |
217 | |
218 | =head1 NAME | |
219 | ||
220 | RT::CustomField_Overlay - overlay for RT::CustomField | |
221 | ||
222 | =head1 DESCRIPTION | |
223 | ||
224 | =head1 'CORE' METHODS | |
225 | ||
226 | =head2 Create PARAMHASH | |
227 | ||
228 | Create takes a hash of values and creates a row in the database: | |
229 | ||
230 | varchar(200) 'Name'. | |
231 | varchar(200) 'Type'. | |
232 | int(11) 'MaxValues'. | |
233 | varchar(255) 'Pattern'. | |
84fb5b46 MKG |
234 | varchar(255) 'Description'. |
235 | int(11) 'SortOrder'. | |
236 | varchar(255) 'LookupType'. | |
237 | smallint(6) 'Disabled'. | |
238 | ||
239 | C<LookupType> is generally the result of either | |
240 | C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>. | |
241 | ||
242 | =cut | |
243 | ||
244 | sub Create { | |
245 | my $self = shift; | |
246 | my %args = ( | |
247 | Name => '', | |
248 | Type => '', | |
249 | MaxValues => 0, | |
250 | Pattern => '', | |
251 | Description => '', | |
252 | Disabled => 0, | |
253 | LookupType => '', | |
84fb5b46 MKG |
254 | LinkValueTo => '', |
255 | IncludeContentForValue => '', | |
256 | @_, | |
257 | ); | |
258 | ||
259 | unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) { | |
260 | return (0, $self->loc('Permission Denied')); | |
261 | } | |
262 | ||
263 | if ( $args{TypeComposite} ) { | |
264 | @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2); | |
265 | } | |
266 | elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) { | |
267 | # old style Type string | |
268 | $args{'MaxValues'} = $1 ? 1 : 0; | |
269 | } | |
270 | $args{'MaxValues'} = int $args{'MaxValues'}; | |
271 | ||
272 | if ( !exists $args{'Queue'}) { | |
273 | # do nothing -- things below are strictly backward compat | |
274 | } | |
275 | elsif ( ! $args{'Queue'} ) { | |
276 | unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) { | |
277 | return ( 0, $self->loc('Permission Denied') ); | |
278 | } | |
279 | $args{'LookupType'} = 'RT::Queue-RT::Ticket'; | |
280 | } | |
281 | else { | |
282 | my $queue = RT::Queue->new($self->CurrentUser); | |
283 | $queue->Load($args{'Queue'}); | |
284 | unless ($queue->Id) { | |
285 | return (0, $self->loc("Queue not found")); | |
286 | } | |
287 | unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) { | |
288 | return ( 0, $self->loc('Permission Denied') ); | |
289 | } | |
290 | $args{'LookupType'} = 'RT::Queue-RT::Ticket'; | |
291 | $args{'Queue'} = $queue->Id; | |
292 | } | |
293 | ||
294 | my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} ); | |
295 | return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok; | |
296 | ||
297 | if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) { | |
298 | $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented"); | |
299 | $args{'MaxValues'} = 1; | |
300 | } | |
301 | ||
302 | if ( $args{'RenderType'} ||= undef ) { | |
303 | my $composite = join '-', @args{'Type', 'MaxValues'}; | |
304 | return (0, $self->loc("This custom field has no Render Types")) | |
305 | unless $self->HasRenderTypes( $composite ); | |
306 | ||
307 | if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) { | |
308 | $args{'RenderType'} = undef; | |
309 | } else { | |
310 | return (0, $self->loc("Invalid Render Type") ) | |
311 | unless grep $_ eq $args{'RenderType'}, $self->RenderTypes( $composite ); | |
312 | } | |
313 | } | |
314 | ||
315 | $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues'; | |
316 | if ( $args{'ValuesClass'} ||= undef ) { | |
317 | return (0, $self->loc("This Custom Field can not have list of values")) | |
318 | unless $self->IsSelectionType( $args{'Type'} ); | |
319 | ||
320 | unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) { | |
321 | return (0, $self->loc("Invalid Custom Field values source")); | |
322 | } | |
323 | } | |
324 | ||
af59614d MKG |
325 | $args{'Disabled'} ||= 0; |
326 | ||
84fb5b46 MKG |
327 | (my $rv, $msg) = $self->SUPER::Create( |
328 | Name => $args{'Name'}, | |
329 | Type => $args{'Type'}, | |
330 | RenderType => $args{'RenderType'}, | |
331 | MaxValues => $args{'MaxValues'}, | |
332 | Pattern => $args{'Pattern'}, | |
333 | BasedOn => $args{'BasedOn'}, | |
334 | ValuesClass => $args{'ValuesClass'}, | |
335 | Description => $args{'Description'}, | |
336 | Disabled => $args{'Disabled'}, | |
337 | LookupType => $args{'LookupType'}, | |
84fb5b46 MKG |
338 | ); |
339 | ||
340 | if ($rv) { | |
341 | if ( exists $args{'LinkValueTo'}) { | |
342 | $self->SetLinkValueTo($args{'LinkValueTo'}); | |
343 | } | |
344 | ||
345 | if ( exists $args{'IncludeContentForValue'}) { | |
346 | $self->SetIncludeContentForValue($args{'IncludeContentForValue'}); | |
347 | } | |
348 | ||
349 | return ($rv, $msg) unless exists $args{'Queue'}; | |
350 | ||
351 | # Compat code -- create a new ObjectCustomField mapping | |
352 | my $OCF = RT::ObjectCustomField->new( $self->CurrentUser ); | |
353 | $OCF->Create( | |
354 | CustomField => $self->Id, | |
355 | ObjectId => $args{'Queue'}, | |
356 | ); | |
357 | } | |
358 | ||
359 | return ($rv, $msg); | |
360 | } | |
361 | ||
362 | =head2 Load ID/NAME | |
363 | ||
364 | Load a custom field. If the value handed in is an integer, load by custom field ID. Otherwise, Load by name. | |
365 | ||
366 | =cut | |
367 | ||
368 | sub Load { | |
369 | my $self = shift; | |
370 | my $id = shift || ''; | |
371 | ||
372 | if ( $id =~ /^\d+$/ ) { | |
373 | return $self->SUPER::Load( $id ); | |
374 | } else { | |
375 | return $self->LoadByName( Name => $id ); | |
376 | } | |
377 | } | |
378 | ||
379 | ||
380 | ||
c33a4027 | 381 | =head2 LoadByName Name => C<NAME>, [...] |
84fb5b46 | 382 | |
c33a4027 | 383 | Loads the Custom field named NAME. As other optional parameters, takes: |
84fb5b46 | 384 | |
c33a4027 | 385 | =over |
84fb5b46 | 386 | |
c33a4027 | 387 | =item LookupType => C<LOOKUPTYPE> |
84fb5b46 | 388 | |
c33a4027 MKG |
389 | The type of Custom Field to look for; while this parameter is not |
390 | required, it is highly suggested, or you may not find the Custom Field | |
391 | you are expecting. It should be passed a C<LookupType> such as | |
392 | L<RT::Ticket/CustomFieldLookupType> or | |
393 | L<RT::User/CustomFieldLookupType>. | |
84fb5b46 | 394 | |
c33a4027 | 395 | =item ObjectType => C<CLASS> |
84fb5b46 | 396 | |
c33a4027 MKG |
397 | The class of object that the custom field is applied to. This can be |
398 | intuited from the provided C<LookupType>. | |
399 | ||
400 | =item ObjectId => C<ID> | |
401 | ||
402 | limits the custom field search to one applied to the relevant id. For | |
403 | example, if a C<LookupType> of C<< RT::Ticket->CustomFieldLookupType >> | |
404 | is used, this is which Queue the CF must be applied to. Pass 0 to only | |
405 | search custom fields that are applied globally. | |
406 | ||
407 | =item IncludeDisabled => C<BOOLEAN> | |
408 | ||
409 | Whether it should return Disabled custom fields if they match; defaults | |
410 | to on, though non-Disabled custom fields are returned preferentially. | |
411 | ||
412 | =item IncludeGlobal => C<BOOLEAN> | |
413 | ||
414 | Whether to also search global custom fields, even if a value is provided | |
415 | for C<ObjectId>; defaults to off. Non-global custom fields are returned | |
416 | preferentially. | |
417 | ||
418 | =back | |
419 | ||
420 | For backwards compatibility, a value passed for C<Queue> is equivalent | |
421 | to specifying a C<LookupType> of L<RT::Ticket/CustomFieldLookupType>, | |
422 | and a C<ObjectId> of the value passed as C<Queue>. | |
423 | ||
424 | If multiple custom fields match the above constraints, the first | |
425 | according to C<SortOrder> will be returned; ties are broken by C<id>, | |
426 | lowest-first. | |
427 | ||
428 | =head2 LoadNameAndQueue | |
429 | ||
430 | =head2 LoadByNameAndQueue | |
431 | ||
432 | Deprecated alternate names for L</LoadByName>. | |
84fb5b46 MKG |
433 | |
434 | =cut | |
435 | ||
436 | # Compatibility for API change after 3.0 beta 1 | |
437 | *LoadNameAndQueue = \&LoadByName; | |
438 | # Change after 3.4 beta. | |
439 | *LoadByNameAndQueue = \&LoadByName; | |
440 | ||
441 | sub LoadByName { | |
442 | my $self = shift; | |
443 | my %args = ( | |
c33a4027 | 444 | Name => undef, |
af59614d | 445 | LookupType => undef, |
c33a4027 MKG |
446 | ObjectType => undef, |
447 | ObjectId => undef, | |
448 | ||
449 | IncludeDisabled => 1, | |
450 | IncludeGlobal => 0, | |
451 | ||
452 | # Back-compat | |
453 | Queue => undef, | |
454 | ||
84fb5b46 MKG |
455 | @_, |
456 | ); | |
457 | ||
458 | unless ( defined $args{'Name'} && length $args{'Name'} ) { | |
459 | $RT::Logger->error("Couldn't load Custom Field without Name"); | |
460 | return wantarray ? (0, $self->loc("No name provided")) : 0; | |
461 | } | |
462 | ||
c33a4027 MKG |
463 | if ( defined $args{'Queue'} ) { |
464 | # Set a LookupType for backcompat, otherwise we'll calculate | |
465 | # one of RT::Queue from your ContextObj. Older code was relying | |
466 | # on us defaulting to RT::Queue-RT::Ticket in old LimitToQueue call. | |
467 | $args{LookupType} ||= 'RT::Queue-RT::Ticket'; | |
468 | $args{ObjectId} //= delete $args{Queue}; | |
469 | } | |
470 | ||
471 | # Default the ObjectType to the top category of the LookupType; it's | |
472 | # what the CFs are assigned on. | |
473 | $args{ObjectType} ||= $1 if $args{LookupType} and $args{LookupType} =~ /^([^-]+)/; | |
474 | ||
475 | # Resolve the ObjectId/ObjectType; this is necessary to properly | |
476 | # limit ObjectId, and also possibly useful to set a ContextObj if we | |
477 | # are currently lacking one. It is not strictly necessary if we | |
478 | # have a context object and were passed a numeric ObjectId, but it | |
479 | # cannot hurt to verify its sanity. Skip if we have a false | |
480 | # ObjectId, which means "global", or if we lack an ObjectType | |
481 | if ($args{ObjectId} and $args{ObjectType}) { | |
482 | my ($obj, $ok, $msg); | |
483 | eval { | |
484 | $obj = $args{ObjectType}->new( $self->CurrentUser ); | |
485 | ($ok, $msg) = $obj->Load( $args{ObjectId} ); | |
486 | }; | |
84fb5b46 | 487 | |
c33a4027 MKG |
488 | if ($ok) { |
489 | $args{ObjectId} = $obj->id; | |
490 | $self->SetContextObject( $obj ) | |
491 | unless $self->ContextObject; | |
492 | } else { | |
493 | $RT::Logger->warning("Failed to load $args{ObjectType} '$args{ObjectId}'"); | |
494 | if ($args{IncludeGlobal}) { | |
495 | # Fall back to acting like we were only asked about the | |
496 | # global case | |
497 | $args{ObjectId} = 0; | |
498 | } else { | |
499 | # If they didn't also want global results, there's no | |
500 | # point in searching; abort | |
501 | return wantarray ? (0, $self->loc("Not found")) : 0; | |
502 | } | |
503 | } | |
504 | } elsif (not $args{ObjectType} and $args{ObjectId}) { | |
505 | # If we skipped out on the above due to lack of ObjectType, make | |
506 | # sure we clear out ObjectId of anything lingering | |
507 | $RT::Logger->warning("No LookupType or ObjectType passed; ignoring ObjectId"); | |
508 | delete $args{ObjectId}; | |
509 | } | |
84fb5b46 MKG |
510 | |
511 | my $CFs = RT::CustomFields->new( $self->CurrentUser ); | |
512 | $CFs->SetContextObject( $self->ContextObject ); | |
513 | my $field = $args{'Name'} =~ /\D/? 'Name' : 'id'; | |
514 | $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0); | |
af59614d MKG |
515 | |
516 | # The context object may be a ticket, for example, as context for a | |
517 | # queue CF. The valid lookup types are thus the entire set of | |
518 | # ACLEquivalenceObjects for the context object. | |
519 | $args{LookupType} ||= [ | |
520 | map {$_->CustomFieldLookupType} | |
521 | ($self->ContextObject, $self->ContextObject->ACLEquivalenceObjects) ] | |
522 | if $self->ContextObject; | |
523 | ||
c33a4027 | 524 | # Apply LookupType limits |
af59614d MKG |
525 | $args{LookupType} = [ $args{LookupType} ] |
526 | if $args{LookupType} and not ref($args{LookupType}); | |
527 | $CFs->Limit( FIELD => "LookupType", OPERATOR => "IN", VALUE => $args{LookupType} ) | |
528 | if $args{LookupType}; | |
529 | ||
c33a4027 MKG |
530 | # Default to by SortOrder and id; this mirrors the standard ordering |
531 | # of RT::CustomFields (minus the Name, which is guaranteed to be | |
532 | # fixed) | |
533 | my @order = ( | |
534 | { FIELD => 'SortOrder', | |
535 | ORDER => 'ASC' }, | |
536 | { FIELD => 'id', | |
537 | ORDER => 'ASC' }, | |
538 | ); | |
539 | ||
540 | if (defined $args{ObjectId}) { | |
541 | # The join to OCFs is distinct -- either we have a global | |
542 | # application or an objectid match, but never both. Even if | |
543 | # this were not the case, we care only for the first row. | |
544 | my $ocfs = $CFs->_OCFAlias( Distinct => 1); | |
545 | if ($args{IncludeGlobal}) { | |
546 | $CFs->Limit( | |
547 | ALIAS => $ocfs, | |
548 | FIELD => 'ObjectId', | |
549 | OPERATOR => 'IN', | |
550 | VALUE => [ $args{ObjectId}, 0 ], | |
551 | ); | |
552 | # Find the queue-specific first | |
553 | unshift @order, { ALIAS => $ocfs, FIELD => "ObjectId", ORDER => "DESC" }; | |
554 | } else { | |
555 | $CFs->Limit( | |
556 | ALIAS => $ocfs, | |
557 | FIELD => 'ObjectId', | |
558 | VALUE => $args{ObjectId}, | |
559 | ); | |
560 | } | |
84fb5b46 MKG |
561 | } |
562 | ||
c33a4027 MKG |
563 | if ($args{IncludeDisabled}) { |
564 | # Load disabled fields, but return them only as a last resort. | |
565 | # This goes at the front of @order, as we prefer the | |
566 | # non-disabled global CF to the disabled Queue-specific CF. | |
567 | $CFs->FindAllRows; | |
568 | unshift @order, { FIELD => "Disabled", ORDER => 'ASC' }; | |
569 | } | |
570 | ||
571 | # Apply the above orderings | |
572 | $CFs->OrderByCols( @order ); | |
84fb5b46 MKG |
573 | |
574 | # We only want one entry. | |
575 | $CFs->RowsPerPage(1); | |
576 | ||
577 | # version before 3.8 just returns 0, so we need to test if wantarray to be | |
578 | # backward compatible. | |
579 | return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First; | |
580 | ||
581 | return $self->LoadById( $first->id ); | |
582 | } | |
583 | ||
584 | ||
585 | ||
586 | ||
587 | =head2 Custom field values | |
588 | ||
589 | =head3 Values FIELD | |
590 | ||
591 | Return a object (collection) of all acceptable values for this Custom Field. | |
592 | Class of the object can vary and depends on the return value | |
593 | of the C<ValuesClass> method. | |
594 | ||
595 | =cut | |
596 | ||
597 | *ValuesObj = \&Values; | |
598 | ||
599 | sub Values { | |
600 | my $self = shift; | |
601 | ||
602 | my $class = $self->ValuesClass; | |
603 | if ( $class ne 'RT::CustomFieldValues') { | |
c33a4027 | 604 | $class->require or die "Can't load $class: $@"; |
84fb5b46 MKG |
605 | } |
606 | my $cf_values = $class->new( $self->CurrentUser ); | |
607 | # if the user has no rights, return an empty object | |
608 | if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) { | |
609 | $cf_values->LimitToCustomField( $self->Id ); | |
610 | } else { | |
611 | $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' ); | |
612 | } | |
613 | return ($cf_values); | |
614 | } | |
615 | ||
616 | ||
617 | =head3 AddValue HASH | |
618 | ||
619 | Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder | |
620 | ||
621 | =cut | |
622 | ||
623 | sub AddValue { | |
624 | my $self = shift; | |
625 | my %args = @_; | |
626 | ||
627 | unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) { | |
628 | return (0, $self->loc('Permission Denied')); | |
629 | } | |
630 | ||
631 | # allow zero value | |
632 | if ( !defined $args{'Name'} || $args{'Name'} eq '' ) { | |
633 | return (0, $self->loc("Can't add a custom field value without a name")); | |
634 | } | |
635 | ||
636 | my $newval = RT::CustomFieldValue->new( $self->CurrentUser ); | |
637 | return $newval->Create( %args, CustomField => $self->Id ); | |
638 | } | |
639 | ||
640 | ||
641 | ||
642 | ||
643 | =head3 DeleteValue ID | |
644 | ||
645 | Deletes a value from this custom field by id. | |
646 | ||
647 | Does not remove this value for any article which has had it selected | |
648 | ||
649 | =cut | |
650 | ||
651 | sub DeleteValue { | |
652 | my $self = shift; | |
653 | my $id = shift; | |
654 | unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) { | |
655 | return (0, $self->loc('Permission Denied')); | |
656 | } | |
657 | ||
658 | my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser ); | |
659 | $val_to_del->Load( $id ); | |
660 | unless ( $val_to_del->Id ) { | |
661 | return (0, $self->loc("Couldn't find that value")); | |
662 | } | |
663 | unless ( $val_to_del->CustomField == $self->Id ) { | |
664 | return (0, $self->loc("That is not a value for this custom field")); | |
665 | } | |
666 | ||
667 | my $retval = $val_to_del->Delete; | |
668 | unless ( $retval ) { | |
669 | return (0, $self->loc("Custom field value could not be deleted")); | |
670 | } | |
671 | return ($retval, $self->loc("Custom field value deleted")); | |
672 | } | |
673 | ||
674 | ||
675 | =head2 ValidateQueue Queue | |
676 | ||
677 | Make sure that the name specified is valid | |
678 | ||
679 | =cut | |
680 | ||
681 | sub ValidateName { | |
682 | my $self = shift; | |
683 | my $value = shift; | |
684 | ||
685 | return 0 unless length $value; | |
686 | ||
687 | return $self->SUPER::ValidateName($value); | |
688 | } | |
689 | ||
690 | =head2 ValidateQueue Queue | |
691 | ||
692 | Make sure that the queue specified is a valid queue name | |
693 | ||
694 | =cut | |
695 | ||
696 | sub ValidateQueue { | |
697 | my $self = shift; | |
698 | my $id = shift; | |
699 | ||
700 | return undef unless defined $id; | |
701 | # 0 means "Global" null would _not_ be ok. | |
702 | return 1 if $id eq '0'; | |
703 | ||
704 | my $q = RT::Queue->new( RT->SystemUser ); | |
705 | $q->Load( $id ); | |
706 | return undef unless $q->id; | |
707 | return 1; | |
708 | } | |
709 | ||
710 | ||
711 | ||
712 | =head2 Types | |
713 | ||
714 | Retuns an array of the types of CustomField that are supported | |
715 | ||
716 | =cut | |
717 | ||
718 | sub Types { | |
719 | return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes); | |
720 | } | |
721 | ||
722 | ||
723 | =head2 IsSelectionType | |
724 | ||
725 | Retuns a boolean value indicating whether the C<Values> method makes sense | |
726 | to this Custom Field. | |
727 | ||
728 | =cut | |
729 | ||
730 | sub IsSelectionType { | |
731 | my $self = shift; | |
732 | my $type = @_? shift : $self->Type; | |
733 | return undef unless $type; | |
734 | return $FieldTypes{$type}->{selection_type}; | |
735 | } | |
736 | ||
737 | ||
738 | ||
739 | =head2 IsExternalValues | |
740 | ||
741 | =cut | |
742 | ||
743 | sub IsExternalValues { | |
744 | my $self = shift; | |
745 | return 0 unless $self->IsSelectionType( @_ ); | |
746 | return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1; | |
747 | } | |
748 | ||
749 | sub ValuesClass { | |
750 | my $self = shift; | |
751 | return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues'; | |
752 | } | |
753 | ||
754 | sub SetValuesClass { | |
755 | my $self = shift; | |
756 | my $class = shift || 'RT::CustomFieldValues'; | |
757 | ||
758 | if ( $class eq 'RT::CustomFieldValues' ) { | |
759 | return $self->_Set( Field => 'ValuesClass', Value => undef, @_ ); | |
760 | } | |
761 | ||
762 | return (0, $self->loc("This Custom Field can not have list of values")) | |
763 | unless $self->IsSelectionType; | |
764 | ||
765 | unless ( $self->ValidateValuesClass( $class ) ) { | |
766 | return (0, $self->loc("Invalid Custom Field values source")); | |
767 | } | |
768 | return $self->_Set( Field => 'ValuesClass', Value => $class, @_ ); | |
769 | } | |
770 | ||
771 | sub ValidateValuesClass { | |
772 | my $self = shift; | |
773 | my $class = shift; | |
774 | ||
c36a7e1d | 775 | return 1 if !$class || $class eq 'RT::CustomFieldValues'; |
84fb5b46 MKG |
776 | return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources'); |
777 | return undef; | |
778 | } | |
779 | ||
780 | ||
781 | =head2 FriendlyType [TYPE, MAX_VALUES] | |
782 | ||
783 | Returns a localized human-readable version of the custom field type. | |
784 | If a custom field type is specified as the parameter, the friendly type for that type will be returned | |
785 | ||
786 | =cut | |
787 | ||
788 | sub FriendlyType { | |
789 | my $self = shift; | |
790 | ||
791 | my $type = @_ ? shift : $self->Type; | |
792 | my $max = @_ ? shift : $self->MaxValues; | |
793 | $max = 0 unless $max; | |
794 | ||
795 | if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) { | |
796 | return ( $self->loc( $friendly_type, $max ) ); | |
797 | } | |
798 | else { | |
799 | return ( $self->loc( $type ) ); | |
800 | } | |
801 | } | |
802 | ||
803 | sub FriendlyTypeComposite { | |
804 | my $self = shift; | |
805 | my $composite = shift || $self->TypeComposite; | |
806 | return $self->FriendlyType(split(/-/, $composite, 2)); | |
807 | } | |
808 | ||
809 | ||
810 | =head2 ValidateType TYPE | |
811 | ||
812 | Takes a single string. returns true if that string is a value | |
813 | type of custom field | |
814 | ||
815 | ||
816 | =cut | |
817 | ||
818 | sub ValidateType { | |
819 | my $self = shift; | |
820 | my $type = shift; | |
821 | ||
822 | if ( $type =~ s/(?:Single|Multiple)$// ) { | |
af59614d MKG |
823 | RT->Deprecated( |
824 | Arguments => "suffix 'Single' or 'Multiple'", | |
825 | Instead => "MaxValues", | |
826 | Remove => "4.4", | |
827 | ); | |
84fb5b46 MKG |
828 | } |
829 | ||
830 | if ( $FieldTypes{$type} ) { | |
831 | return 1; | |
832 | } | |
833 | else { | |
834 | return undef; | |
835 | } | |
836 | } | |
837 | ||
838 | ||
839 | sub SetType { | |
840 | my $self = shift; | |
841 | my $type = shift; | |
842 | if ($type =~ s/(?:(Single)|Multiple)$//) { | |
af59614d MKG |
843 | RT->Deprecated( |
844 | Arguments => "suffix 'Single' or 'Multiple'", | |
845 | Instead => "MaxValues", | |
846 | Remove => "4.4", | |
847 | ); | |
84fb5b46 MKG |
848 | $self->SetMaxValues($1 ? 1 : 0); |
849 | } | |
850 | $self->_Set(Field => 'Type', Value =>$type); | |
851 | } | |
852 | ||
853 | =head2 SetPattern STRING | |
854 | ||
855 | Takes a single string representing a regular expression. Performs basic | |
856 | validation on that regex, and sets the C<Pattern> field for the CF if it | |
857 | is valid. | |
858 | ||
859 | =cut | |
860 | ||
861 | sub SetPattern { | |
862 | my $self = shift; | |
863 | my $regex = shift; | |
864 | ||
865 | my ($ok, $msg) = $self->_IsValidRegex($regex); | |
866 | if ($ok) { | |
867 | return $self->_Set(Field => 'Pattern', Value => $regex); | |
868 | } | |
869 | else { | |
870 | return (0, $self->loc("Invalid pattern: [_1]", $msg)); | |
871 | } | |
872 | } | |
873 | ||
874 | =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg) | |
875 | ||
876 | Tests if the string contains an invalid regex. | |
877 | ||
878 | =cut | |
879 | ||
880 | sub _IsValidRegex { | |
881 | my $self = shift; | |
882 | my $regex = shift or return (1, 'valid'); | |
883 | ||
884 | local $^W; local $@; | |
885 | local $SIG{__DIE__} = sub { 1 }; | |
886 | local $SIG{__WARN__} = sub { 1 }; | |
887 | ||
888 | if (eval { qr/$regex/; 1 }) { | |
889 | return (1, 'valid'); | |
890 | } | |
891 | ||
892 | my $err = $@; | |
893 | $err =~ s{[,;].*}{}; # strip debug info from error | |
894 | chomp $err; | |
895 | return (0, $err); | |
896 | } | |
897 | ||
898 | ||
899 | =head2 SingleValue | |
900 | ||
901 | Returns true if this CustomField only accepts a single value. | |
902 | Returns false if it accepts multiple values | |
903 | ||
904 | =cut | |
905 | ||
906 | sub SingleValue { | |
907 | my $self = shift; | |
908 | if (($self->MaxValues||0) == 1) { | |
909 | return 1; | |
910 | } | |
911 | else { | |
912 | return undef; | |
913 | } | |
914 | } | |
915 | ||
916 | sub UnlimitedValues { | |
917 | my $self = shift; | |
918 | if (($self->MaxValues||0) == 0) { | |
919 | return 1; | |
920 | } | |
921 | else { | |
922 | return undef; | |
923 | } | |
924 | } | |
925 | ||
926 | ||
84fb5b46 MKG |
927 | =head2 ACLEquivalenceObjects |
928 | ||
929 | Returns list of objects via which users can get rights on this custom field. For custom fields | |
930 | these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">. | |
931 | ||
932 | =cut | |
933 | ||
934 | sub ACLEquivalenceObjects { | |
935 | my $self = shift; | |
936 | ||
937 | my $ctx = $self->ContextObject | |
938 | or return; | |
939 | return ($ctx, $ctx->ACLEquivalenceObjects); | |
940 | } | |
941 | ||
942 | =head2 ContextObject and SetContextObject | |
943 | ||
af59614d MKG |
944 | Set or get a context for this object. It can be ticket, queue or another |
945 | object this CF added to. Used for ACL control, for example | |
946 | SeeCustomField can be granted on queue level to allow people to see all | |
947 | fields added to the queue. | |
84fb5b46 MKG |
948 | |
949 | =cut | |
950 | ||
951 | sub SetContextObject { | |
952 | my $self = shift; | |
953 | return $self->{'context_object'} = shift; | |
954 | } | |
955 | ||
956 | sub ContextObject { | |
957 | my $self = shift; | |
958 | return $self->{'context_object'}; | |
959 | } | |
960 | ||
961 | sub ValidContextType { | |
962 | my $self = shift; | |
963 | my $class = shift; | |
964 | ||
965 | my %valid; | |
966 | $valid{$_}++ for split '-', $self->LookupType; | |
967 | delete $valid{'RT::Transaction'}; | |
968 | ||
969 | return $valid{$class}; | |
970 | } | |
971 | ||
972 | =head2 LoadContextObject | |
973 | ||
974 | Takes an Id for a Context Object and loads the right kind of RT::Object | |
975 | for this particular Custom Field (based on the LookupType) and returns it. | |
976 | This is a good way to ensure you don't try to use a Queue as a Context | |
977 | Object on a User Custom Field. | |
978 | ||
979 | =cut | |
980 | ||
981 | sub LoadContextObject { | |
982 | my $self = shift; | |
983 | my $type = shift; | |
984 | my $contextid = shift; | |
985 | ||
986 | unless ( $self->ValidContextType($type) ) { | |
987 | RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id); | |
988 | return; | |
989 | } | |
990 | ||
991 | my $context_object = $type->new( $self->CurrentUser ); | |
992 | my ($id, $msg) = $context_object->LoadById( $contextid ); | |
993 | unless ( $id ) { | |
994 | RT->Logger->debug("Invalid ContextObject id: $msg"); | |
995 | return; | |
996 | } | |
997 | return $context_object; | |
998 | } | |
999 | ||
1000 | =head2 ValidateContextObject | |
1001 | ||
af59614d MKG |
1002 | Ensure that a given ContextObject applies to this Custom Field. For |
1003 | custom fields that are assigned to Queues or to Classes, this checks | |
1004 | that the Custom Field is actually added to that object. For Global | |
1005 | Custom Fields, it returns true as long as the Object is of the right | |
1006 | type, because you may be using your permissions on a given Queue of | |
1007 | Class to see a Global CF. For CFs that are only added globally, you | |
1008 | don't need a ContextObject. | |
84fb5b46 MKG |
1009 | |
1010 | =cut | |
1011 | ||
1012 | sub ValidateContextObject { | |
1013 | my $self = shift; | |
1014 | my $object = shift; | |
1015 | ||
af59614d | 1016 | return 1 if $self->IsGlobal; |
84fb5b46 MKG |
1017 | |
1018 | # global only custom fields don't have objects | |
1019 | # that should be used as context objects. | |
af59614d | 1020 | return if $self->IsOnlyGlobal; |
84fb5b46 MKG |
1021 | |
1022 | # Otherwise, make sure we weren't passed a user object that we're | |
1023 | # supposed to treat as a queue. | |
1024 | return unless $self->ValidContextType(ref $object); | |
1025 | ||
af59614d MKG |
1026 | # Check that it is added correctly |
1027 | my ($added_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects); | |
1028 | return unless $added_to; | |
1029 | return $self->IsAdded($added_to->id); | |
84fb5b46 MKG |
1030 | } |
1031 | ||
1032 | ||
1033 | sub _Set { | |
1034 | my $self = shift; | |
1035 | ||
1036 | unless ( $self->CurrentUserHasRight('AdminCustomField') ) { | |
1037 | return ( 0, $self->loc('Permission Denied') ); | |
1038 | } | |
1039 | return $self->SUPER::_Set( @_ ); | |
1040 | ||
1041 | } | |
1042 | ||
1043 | ||
1044 | ||
1045 | =head2 _Value | |
1046 | ||
1047 | Takes the name of a table column. | |
1048 | Returns its value as a string, if the user passes an ACL check | |
1049 | ||
1050 | =cut | |
1051 | ||
1052 | sub _Value { | |
1053 | my $self = shift; | |
1054 | return undef unless $self->id; | |
1055 | ||
1056 | # we need to do the rights check | |
1057 | unless ( $self->CurrentUserHasRight('SeeCustomField') ) { | |
1058 | $RT::Logger->debug( | |
1059 | "Permission denied. User #". $self->CurrentUser->id | |
1060 | ." has no SeeCustomField right on CF #". $self->id | |
1061 | ); | |
1062 | return (undef); | |
1063 | } | |
1064 | return $self->__Value( @_ ); | |
1065 | } | |
1066 | ||
1067 | ||
1068 | =head2 SetDisabled | |
1069 | ||
1070 | Takes a boolean. | |
1071 | 1 will cause this custom field to no longer be avaialble for objects. | |
1072 | 0 will re-enable this field. | |
1073 | ||
1074 | =cut | |
1075 | ||
1076 | ||
1077 | =head2 SetTypeComposite | |
1078 | ||
1079 | Set this custom field's type and maximum values as a composite value | |
1080 | ||
1081 | =cut | |
1082 | ||
1083 | sub SetTypeComposite { | |
1084 | my $self = shift; | |
1085 | my $composite = shift; | |
1086 | ||
1087 | my $old = $self->TypeComposite; | |
1088 | ||
1089 | my ($type, $max_values) = split(/-/, $composite, 2); | |
1090 | if ( $type ne $self->Type ) { | |
1091 | my ($status, $msg) = $self->SetType( $type ); | |
1092 | return ($status, $msg) unless $status; | |
1093 | } | |
1094 | if ( ($max_values || 0) != ($self->MaxValues || 0) ) { | |
1095 | my ($status, $msg) = $self->SetMaxValues( $max_values ); | |
1096 | return ($status, $msg) unless $status; | |
1097 | } | |
1098 | my $render = $self->RenderType; | |
1099 | if ( $render and not grep { $_ eq $render } $self->RenderTypes ) { | |
1100 | # We switched types and our render type is no longer valid, so unset it | |
1101 | # and use the default | |
1102 | $self->SetRenderType( undef ); | |
1103 | } | |
1104 | return 1, $self->loc( | |
1105 | "Type changed from '[_1]' to '[_2]'", | |
1106 | $self->FriendlyTypeComposite( $old ), | |
1107 | $self->FriendlyTypeComposite( $composite ), | |
1108 | ); | |
1109 | } | |
1110 | ||
1111 | =head2 TypeComposite | |
1112 | ||
1113 | Returns a composite value composed of this object's type and maximum values | |
1114 | ||
1115 | =cut | |
1116 | ||
1117 | ||
1118 | sub TypeComposite { | |
1119 | my $self = shift; | |
1120 | return join '-', ($self->Type || ''), ($self->MaxValues || 0); | |
1121 | } | |
1122 | ||
1123 | =head2 TypeComposites | |
1124 | ||
1125 | Returns an array of all possible composite values for custom fields. | |
1126 | ||
1127 | =cut | |
1128 | ||
1129 | sub TypeComposites { | |
1130 | my $self = shift; | |
1131 | return grep !/(?:[Tt]ext|Combobox|Date|DateTime)-0/, map { ("$_-1", "$_-0") } $self->Types; | |
1132 | } | |
1133 | ||
1134 | =head2 RenderType | |
1135 | ||
1136 | Returns the type of form widget to render for this custom field. Currently | |
1137 | this only affects fields which return true for L</HasRenderTypes>. | |
1138 | ||
1139 | =cut | |
1140 | ||
1141 | sub RenderType { | |
1142 | my $self = shift; | |
1143 | return '' unless $self->HasRenderTypes; | |
1144 | ||
1145 | return $self->_Value( 'RenderType', @_ ) | |
1146 | || $self->DefaultRenderType; | |
1147 | } | |
1148 | ||
1149 | =head2 SetRenderType TYPE | |
1150 | ||
1151 | Sets this custom field's render type. | |
1152 | ||
1153 | =cut | |
1154 | ||
1155 | sub SetRenderType { | |
1156 | my $self = shift; | |
1157 | my $type = shift; | |
1158 | return (0, $self->loc("This custom field has no Render Types")) | |
1159 | unless $self->HasRenderTypes; | |
1160 | ||
1161 | if ( !$type || $type eq $self->DefaultRenderType ) { | |
1162 | return $self->_Set( Field => 'RenderType', Value => undef, @_ ); | |
1163 | } | |
1164 | ||
1165 | if ( not grep { $_ eq $type } $self->RenderTypes ) { | |
1166 | return (0, $self->loc("Invalid Render Type for custom field of type [_1]", | |
1167 | $self->FriendlyType)); | |
1168 | } | |
1169 | ||
84fb5b46 MKG |
1170 | return $self->_Set( Field => 'RenderType', Value => $type, @_ ); |
1171 | } | |
1172 | ||
1173 | =head2 DefaultRenderType [TYPE COMPOSITE] | |
1174 | ||
1175 | Returns the default render type for this custom field's type or the TYPE | |
1176 | COMPOSITE specified as an argument. | |
1177 | ||
1178 | =cut | |
1179 | ||
1180 | sub DefaultRenderType { | |
1181 | my $self = shift; | |
1182 | my $composite = @_ ? shift : $self->TypeComposite; | |
1183 | my ($type, $max) = split /-/, $composite, 2; | |
1184 | return unless $type and $self->HasRenderTypes($composite); | |
1185 | return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0]; | |
1186 | } | |
1187 | ||
1188 | =head2 HasRenderTypes [TYPE_COMPOSITE] | |
1189 | ||
1190 | Returns a boolean value indicating whether the L</RenderTypes> and | |
1191 | L</RenderType> methods make sense for this custom field. | |
1192 | ||
1193 | Currently true only for type C<Select>. | |
1194 | ||
1195 | =cut | |
1196 | ||
1197 | sub HasRenderTypes { | |
1198 | my $self = shift; | |
1199 | my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2; | |
1200 | return undef unless $type; | |
1201 | return defined $FieldTypes{$type}->{render_types} | |
1202 | ->{ $max == 1 ? 'single' : 'multiple' }; | |
1203 | } | |
1204 | ||
1205 | =head2 RenderTypes [TYPE COMPOSITE] | |
1206 | ||
1207 | Returns the valid render types for this custom field's type or the TYPE | |
1208 | COMPOSITE specified as an argument. | |
1209 | ||
1210 | =cut | |
1211 | ||
1212 | sub RenderTypes { | |
1213 | my $self = shift; | |
1214 | my $composite = @_ ? shift : $self->TypeComposite; | |
1215 | my ($type, $max) = split /-/, $composite, 2; | |
1216 | return unless $type and $self->HasRenderTypes($composite); | |
1217 | return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }}; | |
1218 | } | |
1219 | ||
1220 | =head2 SetLookupType | |
1221 | ||
1222 | Autrijus: care to doc how LookupTypes work? | |
1223 | ||
1224 | =cut | |
1225 | ||
1226 | sub SetLookupType { | |
1227 | my $self = shift; | |
1228 | my $lookup = shift; | |
1229 | if ( $lookup ne $self->LookupType ) { | |
1230 | # Okay... We need to invalidate our existing relationships | |
af59614d | 1231 | RT::ObjectCustomField->new($self->CurrentUser)->DeleteAll( CustomField => $self ); |
84fb5b46 MKG |
1232 | } |
1233 | return $self->_Set(Field => 'LookupType', Value =>$lookup); | |
1234 | } | |
1235 | ||
1236 | =head2 LookupTypes | |
1237 | ||
1238 | Returns an array of LookupTypes available | |
1239 | ||
1240 | =cut | |
1241 | ||
1242 | ||
1243 | sub LookupTypes { | |
1244 | my $self = shift; | |
af59614d | 1245 | return sort keys %FRIENDLY_LOOKUP_TYPES; |
84fb5b46 MKG |
1246 | } |
1247 | ||
84fb5b46 MKG |
1248 | =head2 FriendlyLookupType |
1249 | ||
1250 | Returns a localized description of the type of this custom field | |
1251 | ||
1252 | =cut | |
1253 | ||
1254 | sub FriendlyLookupType { | |
1255 | my $self = shift; | |
1256 | my $lookup = shift || $self->LookupType; | |
af59614d MKG |
1257 | |
1258 | return ($self->loc( $FRIENDLY_LOOKUP_TYPES{$lookup} )) | |
1259 | if defined $FRIENDLY_LOOKUP_TYPES{$lookup}; | |
84fb5b46 MKG |
1260 | |
1261 | my @types = map { s/^RT::// ? $self->loc($_) : $_ } | |
1262 | grep { defined and length } | |
1263 | split( /-/, $lookup ) | |
1264 | or return; | |
af59614d MKG |
1265 | |
1266 | state $LocStrings = [ | |
1267 | "[_1] objects", # loc | |
1268 | "[_1]'s [_2] objects", # loc | |
1269 | "[_1]'s [_2]'s [_3] objects", # loc | |
1270 | ]; | |
1271 | return ( $self->loc( $LocStrings->[$#types], @types ) ); | |
84fb5b46 MKG |
1272 | } |
1273 | ||
01e3b242 MKG |
1274 | =head1 RecordClassFromLookupType |
1275 | ||
1276 | Returns the type of Object referred to by ObjectCustomFields' ObjectId column | |
1277 | ||
1278 | Optionally takes a LookupType to use instead of using the value on the loaded | |
1279 | record. In this case, the method may be called on the class instead of an | |
1280 | object. | |
1281 | ||
1282 | =cut | |
1283 | ||
84fb5b46 MKG |
1284 | sub RecordClassFromLookupType { |
1285 | my $self = shift; | |
01e3b242 MKG |
1286 | my $type = shift || $self->LookupType; |
1287 | my ($class) = ($type =~ /^([^-]+)/); | |
84fb5b46 | 1288 | unless ( $class ) { |
01e3b242 MKG |
1289 | if (blessed($self) and $self->LookupType eq $type) { |
1290 | $RT::Logger->error( | |
1291 | "Custom Field #". $self->id | |
1292 | ." has incorrect LookupType '$type'" | |
1293 | ); | |
1294 | } else { | |
1295 | RT->Logger->error("Invalid LookupType passed as argument: $type"); | |
1296 | } | |
1297 | return undef; | |
1298 | } | |
1299 | return $class; | |
1300 | } | |
1301 | ||
1302 | =head1 ObjectTypeFromLookupType | |
1303 | ||
1304 | Returns the ObjectType used in ObjectCustomFieldValues rows for this CF | |
1305 | ||
1306 | Optionally takes a LookupType to use instead of using the value on the loaded | |
1307 | record. In this case, the method may be called on the class instead of an | |
1308 | object. | |
1309 | ||
1310 | =cut | |
1311 | ||
1312 | sub ObjectTypeFromLookupType { | |
1313 | my $self = shift; | |
1314 | my $type = shift || $self->LookupType; | |
1315 | my ($class) = ($type =~ /([^-]+)$/); | |
1316 | unless ( $class ) { | |
1317 | if (blessed($self) and $self->LookupType eq $type) { | |
1318 | $RT::Logger->error( | |
1319 | "Custom Field #". $self->id | |
1320 | ." has incorrect LookupType '$type'" | |
1321 | ); | |
1322 | } else { | |
1323 | RT->Logger->error("Invalid LookupType passed as argument: $type"); | |
1324 | } | |
84fb5b46 MKG |
1325 | return undef; |
1326 | } | |
1327 | return $class; | |
1328 | } | |
1329 | ||
1330 | sub CollectionClassFromLookupType { | |
1331 | my $self = shift; | |
1332 | ||
1333 | my $record_class = $self->RecordClassFromLookupType; | |
1334 | return undef unless $record_class; | |
1335 | ||
1336 | my $collection_class; | |
1337 | if ( UNIVERSAL::can($record_class.'Collection', 'new') ) { | |
1338 | $collection_class = $record_class.'Collection'; | |
1339 | } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) { | |
1340 | $collection_class = $record_class.'es'; | |
1341 | } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) { | |
1342 | $collection_class = $record_class.'s'; | |
1343 | } else { | |
1344 | $RT::Logger->error("Can not find a collection class for record class '$record_class'"); | |
1345 | return undef; | |
1346 | } | |
1347 | return $collection_class; | |
1348 | } | |
1349 | ||
af59614d MKG |
1350 | =head2 Groupings |
1351 | ||
1352 | Returns a (sorted and lowercased) list of the groupings in which this custom | |
1353 | field appears. | |
1354 | ||
1355 | If called on a loaded object, the returned list is limited to groupings which | |
1356 | apply to the record class this CF applies to (L</RecordClassFromLookupType>). | |
84fb5b46 | 1357 | |
af59614d MKG |
1358 | If passed a loaded object or a class name, the returned list is limited to |
1359 | groupings which apply to the class of the object or the specified class. | |
1360 | ||
1361 | If called on an unloaded object, all potential groupings are returned. | |
84fb5b46 MKG |
1362 | |
1363 | =cut | |
1364 | ||
af59614d | 1365 | sub Groupings { |
84fb5b46 | 1366 | my $self = shift; |
af59614d MKG |
1367 | my $record_class = $self->_GroupingClass(shift); |
1368 | ||
1369 | my $config = RT->Config->Get('CustomFieldGroupings'); | |
1370 | $config = {} unless ref($config) eq 'HASH'; | |
1371 | ||
1372 | my @groups; | |
1373 | if ( $record_class ) { | |
1374 | push @groups, sort {lc($a) cmp lc($b)} keys %{ $BUILTIN_GROUPINGS{$record_class} || {} }; | |
1375 | if ( ref($config->{$record_class} ||= []) eq "ARRAY") { | |
1376 | my @order = @{ $config->{$record_class} }; | |
1377 | while (@order) { | |
1378 | push @groups, shift(@order); | |
1379 | shift(@order); | |
1380 | } | |
1381 | } else { | |
1382 | @groups = sort {lc($a) cmp lc($b)} keys %{ $config->{$record_class} }; | |
1383 | } | |
1384 | } else { | |
1385 | my %all = (%$config, %BUILTIN_GROUPINGS); | |
1386 | @groups = sort {lc($a) cmp lc($b)} map {$self->Groupings($_)} grep {$_} keys(%all); | |
1387 | } | |
84fb5b46 | 1388 | |
af59614d MKG |
1389 | my %seen; |
1390 | return | |
1391 | grep defined && length && !$seen{lc $_}++, | |
1392 | @groups; | |
84fb5b46 MKG |
1393 | } |
1394 | ||
af59614d | 1395 | =head2 CustomGroupings |
84fb5b46 | 1396 | |
af59614d MKG |
1397 | Identical to L</Groupings> but filters out built-in groupings from the the |
1398 | returned list. | |
84fb5b46 MKG |
1399 | |
1400 | =cut | |
1401 | ||
af59614d | 1402 | sub CustomGroupings { |
84fb5b46 | 1403 | my $self = shift; |
af59614d MKG |
1404 | my $record_class = $self->_GroupingClass(shift); |
1405 | return grep !$BUILTIN_GROUPINGS{$record_class}{$_}, $self->Groupings( $record_class ); | |
1406 | } | |
84fb5b46 | 1407 | |
af59614d MKG |
1408 | sub _GroupingClass { |
1409 | my $self = shift; | |
1410 | my $record = shift; | |
84fb5b46 | 1411 | |
af59614d MKG |
1412 | my $record_class = ref($record) || $record || ''; |
1413 | $record_class = $self->RecordClassFromLookupType | |
1414 | if !$record_class and blessed($self) and $self->id; | |
84fb5b46 | 1415 | |
af59614d | 1416 | return $record_class; |
84fb5b46 MKG |
1417 | } |
1418 | ||
af59614d | 1419 | =head2 RegisterBuiltInGroupings |
84fb5b46 | 1420 | |
af59614d MKG |
1421 | Registers groupings to be considered a fundamental part of RT, either via use |
1422 | in core RT or via an extension. These groupings must be rendered explicitly in | |
1423 | Mason by specific calls to F</Elements/ShowCustomFields> and | |
1424 | F</Elements/EditCustomFields>. They will not show up automatically on normal | |
1425 | display pages like configured custom groupings. | |
1426 | ||
1427 | Takes a set of key-value pairs of class names (valid L<RT::Record> subclasses) | |
1428 | and array refs of grouping names to consider built-in. | |
84fb5b46 | 1429 | |
af59614d MKG |
1430 | If a class already contains built-in groupings (such as L<RT::Ticket> and |
1431 | L<RT::User>), new groupings are appended. | |
84fb5b46 MKG |
1432 | |
1433 | =cut | |
1434 | ||
af59614d | 1435 | sub RegisterBuiltInGroupings { |
84fb5b46 | 1436 | my $self = shift; |
af59614d | 1437 | my %new = @_; |
84fb5b46 | 1438 | |
af59614d MKG |
1439 | while (my ($k,$v) = each %new) { |
1440 | $v = [$v] unless ref($v) eq 'ARRAY'; | |
1441 | $BUILTIN_GROUPINGS{$k} = { | |
1442 | %{$BUILTIN_GROUPINGS{$k} || {}}, | |
1443 | map { $_ => 1 } @$v | |
1444 | }; | |
1445 | } | |
1446 | $BUILTIN_GROUPINGS{''} = { map { %$_ } values %BUILTIN_GROUPINGS }; | |
1447 | } | |
84fb5b46 | 1448 | |
af59614d | 1449 | =head1 IsOnlyGlobal |
84fb5b46 | 1450 | |
af59614d MKG |
1451 | Certain custom fields (users, groups) should only be added globally; |
1452 | codify that set here for reference. | |
1453 | ||
1454 | =cut | |
84fb5b46 | 1455 | |
af59614d | 1456 | sub IsOnlyGlobal { |
84fb5b46 MKG |
1457 | my $self = shift; |
1458 | ||
af59614d MKG |
1459 | return ($self->LookupType =~ /^RT::(?:Group|User)/io); |
1460 | ||
1461 | } | |
1462 | sub ApplyGlobally { | |
1463 | RT->Deprecated( | |
1464 | Instead => "IsOnlyGlobal", | |
1465 | Remove => "4.4", | |
1466 | ); | |
1467 | return shift->IsOnlyGlobal(@_); | |
1468 | } | |
84fb5b46 | 1469 | |
af59614d | 1470 | =head1 AddedTo |
84fb5b46 | 1471 | |
af59614d MKG |
1472 | Returns collection with objects this custom field is added to. |
1473 | Class of the collection depends on L</LookupType>. | |
1474 | See all L</NotAddedTo> . | |
84fb5b46 | 1475 | |
af59614d MKG |
1476 | Doesn't takes into account if object is added globally. |
1477 | ||
1478 | =cut | |
1479 | ||
1480 | sub AddedTo { | |
1481 | my $self = shift; | |
1482 | return RT::ObjectCustomField->new( $self->CurrentUser ) | |
1483 | ->AddedTo( CustomField => $self ); | |
1484 | } | |
1485 | sub AppliedTo { | |
1486 | RT->Deprecated( | |
1487 | Instead => "AddedTo", | |
1488 | Remove => "4.4", | |
84fb5b46 | 1489 | ); |
af59614d MKG |
1490 | shift->AddedTo(@_); |
1491 | }; | |
1492 | ||
1493 | =head1 NotAddedTo | |
1494 | ||
1495 | Returns collection with objects this custom field is not added to. | |
1496 | Class of the collection depends on L</LookupType>. | |
1497 | See all L</AddedTo> . | |
1498 | ||
1499 | Doesn't take into account if the object is added globally. | |
1500 | ||
1501 | =cut | |
1502 | ||
1503 | sub NotAddedTo { | |
1504 | my $self = shift; | |
1505 | return RT::ObjectCustomField->new( $self->CurrentUser ) | |
1506 | ->NotAddedTo( CustomField => $self ); | |
84fb5b46 | 1507 | } |
af59614d MKG |
1508 | sub NotAppliedTo { |
1509 | RT->Deprecated( | |
1510 | Instead => "NotAddedTo", | |
1511 | Remove => "4.4", | |
1512 | ); | |
1513 | shift->NotAddedTo(@_) | |
1514 | }; | |
84fb5b46 | 1515 | |
af59614d | 1516 | =head2 IsAdded |
84fb5b46 MKG |
1517 | |
1518 | Takes object id and returns corresponding L<RT::ObjectCustomField> | |
af59614d MKG |
1519 | record if this custom field is added to the object. Use 0 to check |
1520 | if custom field is added globally. | |
84fb5b46 MKG |
1521 | |
1522 | =cut | |
1523 | ||
af59614d | 1524 | sub IsAdded { |
84fb5b46 MKG |
1525 | my $self = shift; |
1526 | my $id = shift; | |
1527 | my $ocf = RT::ObjectCustomField->new( $self->CurrentUser ); | |
1528 | $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 ); | |
1529 | return undef unless $ocf->id; | |
1530 | return $ocf; | |
1531 | } | |
af59614d MKG |
1532 | sub IsApplied { |
1533 | RT->Deprecated( | |
1534 | Instead => "IsAdded", | |
1535 | Remove => "4.4", | |
1536 | ); | |
1537 | shift->IsAdded(@_); | |
1538 | }; | |
1539 | ||
1540 | sub IsGlobal { return shift->IsAdded(0) } | |
1541 | ||
1542 | =head2 IsAddedToAny | |
1543 | ||
1544 | Returns true if custom field is applied to any object. | |
1545 | ||
1546 | =cut | |
1547 | ||
1548 | sub IsAddedToAny { | |
1549 | my $self = shift; | |
1550 | my $id = shift; | |
1551 | my $ocf = RT::ObjectCustomField->new( $self->CurrentUser ); | |
1552 | $ocf->LoadByCols( CustomField => $self->id ); | |
1553 | return $ocf->id ? 1 : 0; | |
1554 | } | |
84fb5b46 MKG |
1555 | |
1556 | =head2 AddToObject OBJECT | |
1557 | ||
1558 | Add this custom field as a custom field for a single object, such as a queue or group. | |
1559 | ||
1560 | Takes an object | |
1561 | ||
1562 | =cut | |
1563 | ||
84fb5b46 MKG |
1564 | sub AddToObject { |
1565 | my $self = shift; | |
1566 | my $object = shift; | |
1567 | my $id = $object->Id || 0; | |
1568 | ||
1569 | unless (index($self->LookupType, ref($object)) == 0) { | |
1570 | return ( 0, $self->loc('Lookup type mismatch') ); | |
1571 | } | |
1572 | ||
1573 | unless ( $object->CurrentUserHasRight('AssignCustomFields') ) { | |
1574 | return ( 0, $self->loc('Permission Denied') ); | |
1575 | } | |
1576 | ||
84fb5b46 | 1577 | my $ocf = RT::ObjectCustomField->new( $self->CurrentUser ); |
af59614d MKG |
1578 | my ( $oid, $msg ) = $ocf->Add( |
1579 | CustomField => $self->id, ObjectId => $id, | |
84fb5b46 MKG |
1580 | ); |
1581 | return ( $oid, $msg ); | |
1582 | } | |
1583 | ||
1584 | ||
1585 | =head2 RemoveFromObject OBJECT | |
1586 | ||
1587 | Remove this custom field for a single object, such as a queue or group. | |
1588 | ||
1589 | Takes an object | |
1590 | ||
1591 | =cut | |
1592 | ||
1593 | sub RemoveFromObject { | |
1594 | my $self = shift; | |
1595 | my $object = shift; | |
1596 | my $id = $object->Id || 0; | |
1597 | ||
1598 | unless (index($self->LookupType, ref($object)) == 0) { | |
1599 | return ( 0, $self->loc('Object type mismatch') ); | |
1600 | } | |
1601 | ||
1602 | unless ( $object->CurrentUserHasRight('AssignCustomFields') ) { | |
1603 | return ( 0, $self->loc('Permission Denied') ); | |
1604 | } | |
1605 | ||
af59614d | 1606 | my $ocf = $self->IsAdded( $id ); |
84fb5b46 | 1607 | unless ( $ocf ) { |
af59614d | 1608 | return ( 0, $self->loc("This custom field cannot be added to that object") ); |
84fb5b46 MKG |
1609 | } |
1610 | ||
1611 | # XXX: Delete doesn't return anything | |
1612 | my ( $oid, $msg ) = $ocf->Delete; | |
1613 | return ( $oid, $msg ); | |
1614 | } | |
1615 | ||
1616 | ||
1617 | =head2 AddValueForObject HASH | |
1618 | ||
1619 | Adds a custom field value for a record object of some kind. | |
1620 | Takes a param hash of | |
1621 | ||
1622 | Required: | |
1623 | ||
1624 | Object | |
1625 | Content | |
1626 | ||
1627 | Optional: | |
1628 | ||
1629 | LargeContent | |
1630 | ContentType | |
1631 | ||
1632 | =cut | |
1633 | ||
1634 | sub AddValueForObject { | |
1635 | my $self = shift; | |
1636 | my %args = ( | |
1637 | Object => undef, | |
1638 | Content => undef, | |
1639 | LargeContent => undef, | |
1640 | ContentType => undef, | |
1641 | @_ | |
1642 | ); | |
1643 | my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') ); | |
1644 | ||
1645 | unless ( $self->CurrentUserHasRight('ModifyCustomField') ) { | |
1646 | return ( 0, $self->loc('Permission Denied') ); | |
1647 | } | |
1648 | ||
1649 | unless ( $self->MatchPattern($args{'Content'}) ) { | |
1650 | return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) ); | |
1651 | } | |
1652 | ||
1653 | $RT::Handle->BeginTransaction; | |
1654 | ||
1655 | if ( $self->MaxValues ) { | |
1656 | my $current_values = $self->ValuesForObject($obj); | |
1657 | my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues; | |
1658 | ||
1659 | # (The +1 is for the new value we're adding) | |
1660 | ||
1661 | # If we have a set of current values and we've gone over the maximum | |
1662 | # allowed number of values, we'll need to delete some to make room. | |
1663 | # which former values are blown away is not guaranteed | |
1664 | ||
1665 | while ($extra_values) { | |
1666 | my $extra_item = $current_values->Next; | |
1667 | unless ( $extra_item->id ) { | |
1668 | $RT::Logger->crit( "We were just asked to delete " | |
1669 | ."a custom field value that doesn't exist!" ); | |
1670 | $RT::Handle->Rollback(); | |
1671 | return (undef); | |
1672 | } | |
1673 | $extra_item->Delete; | |
1674 | $extra_values--; | |
1675 | } | |
1676 | } | |
1677 | ||
84fb5b46 MKG |
1678 | my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser ); |
1679 | my ($val, $msg) = $newval->Create( | |
1680 | ObjectType => ref($obj), | |
1681 | ObjectId => $obj->Id, | |
1682 | Content => $args{'Content'}, | |
1683 | LargeContent => $args{'LargeContent'}, | |
1684 | ContentType => $args{'ContentType'}, | |
1685 | CustomField => $self->Id | |
1686 | ); | |
1687 | ||
1688 | unless ($val) { | |
1689 | $RT::Handle->Rollback(); | |
1690 | return ($val, $self->loc("Couldn't create record: [_1]", $msg)); | |
1691 | } | |
1692 | ||
1693 | $RT::Handle->Commit(); | |
1694 | return ($val); | |
1695 | ||
1696 | } | |
1697 | ||
1698 | ||
c33a4027 MKG |
1699 | sub _CanonicalizeValue { |
1700 | my $self = shift; | |
1701 | my $args = shift; | |
1702 | ||
1703 | my $type = $self->_Value('Type'); | |
1704 | return 1 unless $type; | |
1705 | ||
1706 | my $method = '_CanonicalizeValue'. $type; | |
1707 | return 1 unless $self->can($method); | |
1708 | $self->$method($args); | |
1709 | } | |
84fb5b46 MKG |
1710 | |
1711 | sub _CanonicalizeValueDateTime { | |
1712 | my $self = shift; | |
1713 | my $args = shift; | |
1714 | my $DateObj = RT::Date->new( $self->CurrentUser ); | |
1715 | $DateObj->Set( Format => 'unknown', | |
1716 | Value => $args->{'Content'} ); | |
1717 | $args->{'Content'} = $DateObj->ISO; | |
c33a4027 | 1718 | return 1; |
84fb5b46 MKG |
1719 | } |
1720 | ||
1721 | # For date, we need to store Content as ISO date | |
1722 | sub _CanonicalizeValueDate { | |
1723 | my $self = shift; | |
1724 | my $args = shift; | |
1725 | ||
1726 | # in case user input date with time, let's omit it by setting timezone | |
1727 | # to utc so "hour" won't affect "day" | |
1728 | my $DateObj = RT::Date->new( $self->CurrentUser ); | |
1729 | $DateObj->Set( Format => 'unknown', | |
1730 | Value => $args->{'Content'}, | |
84fb5b46 | 1731 | ); |
c36a7e1d | 1732 | $args->{'Content'} = $DateObj->Date( Timezone => 'user' ); |
c33a4027 MKG |
1733 | return 1; |
1734 | } | |
1735 | ||
1736 | sub _CanonicalizeValueIPAddress { | |
1737 | my $self = shift; | |
1738 | my $args = shift; | |
1739 | ||
1740 | $args->{Content} = RT::ObjectCustomFieldValue->ParseIP( $args->{Content} ); | |
1741 | return (0, $self->loc("Content is not a valid IP address")) | |
1742 | unless $args->{Content}; | |
1743 | return 1; | |
1744 | } | |
1745 | ||
1746 | sub _CanonicalizeValueIPAddressRange { | |
1747 | my $self = shift; | |
1748 | my $args = shift; | |
1749 | ||
1750 | my $content = $args->{Content}; | |
1751 | $content .= "-".$args->{LargeContent} if $args->{LargeContent}; | |
1752 | ||
1753 | ($args->{Content}, $args->{LargeContent}) | |
1754 | = RT::ObjectCustomFieldValue->ParseIPRange( $content ); | |
1755 | ||
1756 | $args->{ContentType} = 'text/plain'; | |
1757 | return (0, $self->loc("Content is not a valid IP address range")) | |
1758 | unless $args->{Content}; | |
1759 | return 1; | |
84fb5b46 MKG |
1760 | } |
1761 | ||
1762 | =head2 MatchPattern STRING | |
1763 | ||
1764 | Tests the incoming string against the Pattern of this custom field object | |
1765 | and returns a boolean; returns true if the Pattern is empty. | |
1766 | ||
1767 | =cut | |
1768 | ||
1769 | sub MatchPattern { | |
1770 | my $self = shift; | |
1771 | my $regex = $self->Pattern or return 1; | |
1772 | ||
1773 | return (( defined $_[0] ? $_[0] : '') =~ $regex); | |
1774 | } | |
1775 | ||
1776 | ||
1777 | ||
1778 | ||
1779 | =head2 FriendlyPattern | |
1780 | ||
1781 | Prettify the pattern of this custom field, by taking the text in C<(?#text)> | |
1782 | and localizing it. | |
1783 | ||
1784 | =cut | |
1785 | ||
1786 | sub FriendlyPattern { | |
1787 | my $self = shift; | |
1788 | my $regex = $self->Pattern; | |
1789 | ||
1790 | return '' unless length $regex; | |
1791 | if ( $regex =~ /\(\?#([^)]*)\)/ ) { | |
1792 | return '[' . $self->loc($1) . ']'; | |
1793 | } | |
1794 | else { | |
1795 | return $regex; | |
1796 | } | |
1797 | } | |
1798 | ||
1799 | ||
1800 | ||
1801 | ||
1802 | =head2 DeleteValueForObject HASH | |
1803 | ||
1804 | Deletes a custom field value for a ticket. Takes a param hash of Object and Content | |
1805 | ||
1806 | Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false | |
1807 | ||
1808 | =cut | |
1809 | ||
1810 | sub DeleteValueForObject { | |
1811 | my $self = shift; | |
1812 | my %args = ( Object => undef, | |
1813 | Content => undef, | |
1814 | Id => undef, | |
1815 | @_ ); | |
1816 | ||
1817 | ||
1818 | unless ($self->CurrentUserHasRight('ModifyCustomField')) { | |
1819 | return (0, $self->loc('Permission Denied')); | |
1820 | } | |
1821 | ||
1822 | my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser); | |
1823 | ||
1824 | if (my $id = $args{'Id'}) { | |
1825 | $oldval->Load($id); | |
1826 | } | |
1827 | unless ($oldval->id) { | |
1828 | $oldval->LoadByObjectContentAndCustomField( | |
1829 | Object => $args{'Object'}, | |
1830 | Content => $args{'Content'}, | |
1831 | CustomField => $self->Id, | |
1832 | ); | |
1833 | } | |
1834 | ||
1835 | ||
1836 | # check to make sure we found it | |
1837 | unless ($oldval->Id) { | |
1838 | return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name)); | |
1839 | } | |
1840 | ||
1841 | # for single-value fields, we need to validate that empty string is a valid value for it | |
1842 | if ( $self->SingleValue and not $self->MatchPattern( '' ) ) { | |
1843 | return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) ); | |
1844 | } | |
1845 | ||
1846 | # delete it | |
1847 | ||
1848 | my $ret = $oldval->Delete(); | |
1849 | unless ($ret) { | |
1850 | return(0, $self->loc("Custom field value could not be found")); | |
1851 | } | |
1852 | return($oldval->Id, $self->loc("Custom field value deleted")); | |
1853 | } | |
1854 | ||
1855 | ||
1856 | =head2 ValuesForObject OBJECT | |
1857 | ||
1858 | Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT | |
1859 | ||
1860 | =cut | |
1861 | ||
1862 | sub ValuesForObject { | |
1863 | my $self = shift; | |
1864 | my $object = shift; | |
1865 | ||
1866 | my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser); | |
403d7b0b | 1867 | unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) { |
84fb5b46 | 1868 | # Return an empty object if they have no rights to see |
403d7b0b | 1869 | $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" ); |
84fb5b46 MKG |
1870 | return ($values); |
1871 | } | |
403d7b0b | 1872 | |
84fb5b46 | 1873 | $values->LimitToCustomField($self->Id); |
84fb5b46 MKG |
1874 | $values->LimitToObject($object); |
1875 | ||
1876 | return ($values); | |
1877 | } | |
1878 | ||
1879 | ||
af59614d | 1880 | =head2 RegisterLookupType LOOKUPTYPE FRIENDLYNAME |
84fb5b46 | 1881 | |
af59614d MKG |
1882 | Tell RT that a certain object accepts custom fields via a lookup type and |
1883 | provide a friendly name for such CFs. | |
84fb5b46 MKG |
1884 | |
1885 | Examples: | |
1886 | ||
1887 | 'RT::Queue-RT::Ticket' => "Tickets", # loc | |
1888 | 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc | |
1889 | 'RT::User' => "Users", # loc | |
1890 | 'RT::Group' => "Groups", # loc | |
403d7b0b | 1891 | 'RT::Queue' => "Queues", # loc |
84fb5b46 MKG |
1892 | |
1893 | This is a class method. | |
1894 | ||
1895 | =cut | |
1896 | ||
af59614d | 1897 | sub RegisterLookupType { |
84fb5b46 MKG |
1898 | my $self = shift; |
1899 | my $path = shift; | |
1900 | my $friendly_name = shift; | |
1901 | ||
af59614d MKG |
1902 | $FRIENDLY_LOOKUP_TYPES{$path} = $friendly_name; |
1903 | } | |
84fb5b46 | 1904 | |
af59614d MKG |
1905 | sub _ForObjectType { |
1906 | RT->Deprecated( | |
1907 | Instead => 'RegisterLookupType', | |
1908 | Remove => '4.4', | |
1909 | ); | |
1910 | my $self = shift; | |
1911 | $self->RegisterLookupType(@_); | |
84fb5b46 MKG |
1912 | } |
1913 | ||
1914 | ||
1915 | =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue) | |
1916 | ||
1917 | Gets or sets the C<IncludeContentForValue> for this custom field. RT | |
1918 | uses this field to automatically include content into the user's browser | |
1919 | as they display records with custom fields in RT. | |
1920 | ||
1921 | =cut | |
1922 | ||
1923 | sub SetIncludeContentForValue { | |
1924 | shift->IncludeContentForValue(@_); | |
1925 | } | |
1926 | sub IncludeContentForValue{ | |
1927 | my $self = shift; | |
1928 | $self->_URLTemplate('IncludeContentForValue', @_); | |
1929 | } | |
1930 | ||
1931 | ||
1932 | ||
1933 | =head2 LinkValueTo [VALUE] (and SetLinkValueTo) | |
1934 | ||
1935 | Gets or sets the C<LinkValueTo> for this custom field. RT | |
1936 | uses this field to make custom field values into hyperlinks in the user's | |
1937 | browser as they display records with custom fields in RT. | |
1938 | ||
1939 | =cut | |
1940 | ||
1941 | ||
1942 | sub SetLinkValueTo { | |
1943 | shift->LinkValueTo(@_); | |
1944 | } | |
1945 | ||
1946 | sub LinkValueTo { | |
1947 | my $self = shift; | |
1948 | $self->_URLTemplate('LinkValueTo', @_); | |
1949 | ||
1950 | } | |
1951 | ||
1952 | ||
1953 | =head2 _URLTemplate NAME [VALUE] | |
1954 | ||
1955 | With one argument, returns the _URLTemplate named C<NAME>, but only if | |
1956 | the current user has the right to see this custom field. | |
1957 | ||
1958 | With two arguments, attemptes to set the relevant template value. | |
1959 | ||
1960 | =cut | |
1961 | ||
1962 | sub _URLTemplate { | |
1963 | my $self = shift; | |
1964 | my $template_name = shift; | |
1965 | if (@_) { | |
1966 | ||
1967 | my $value = shift; | |
1968 | unless ( $self->CurrentUserHasRight('AdminCustomField') ) { | |
1969 | return ( 0, $self->loc('Permission Denied') ); | |
1970 | } | |
c33a4027 MKG |
1971 | if (length $value and defined $value) { |
1972 | $self->SetAttribute( Name => $template_name, Content => $value ); | |
1973 | } else { | |
1974 | $self->DeleteAttribute( $template_name ); | |
1975 | } | |
84fb5b46 MKG |
1976 | return ( 1, $self->loc('Updated') ); |
1977 | } else { | |
1978 | unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) { | |
1979 | return (undef); | |
1980 | } | |
1981 | ||
c33a4027 MKG |
1982 | my ($attr) = $self->Attributes->Named($template_name); |
1983 | return undef unless $attr; | |
1984 | return $attr->Content; | |
84fb5b46 MKG |
1985 | } |
1986 | } | |
1987 | ||
1988 | sub SetBasedOn { | |
1989 | my $self = shift; | |
1990 | my $value = shift; | |
1991 | ||
1992 | return $self->_Set( Field => 'BasedOn', Value => $value, @_ ) | |
1993 | unless defined $value and length $value; | |
1994 | ||
1995 | my $cf = RT::CustomField->new( $self->CurrentUser ); | |
1996 | $cf->SetContextObject( $self->ContextObject ); | |
1997 | $cf->Load( ref $value ? $value->id : $value ); | |
1998 | ||
af59614d | 1999 | return (0, "Permission Denied") |
84fb5b46 MKG |
2000 | unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField'); |
2001 | ||
2002 | # XXX: Remove this restriction once we support lists and cascaded selects | |
2003 | if ( $self->RenderType =~ /List/ ) { | |
2004 | return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type.")); | |
2005 | } | |
2006 | ||
2007 | return $self->_Set( Field => 'BasedOn', Value => $value, @_ ) | |
2008 | } | |
2009 | ||
2010 | sub BasedOnObj { | |
2011 | my $self = shift; | |
2012 | ||
2013 | my $obj = RT::CustomField->new( $self->CurrentUser ); | |
2014 | $obj->SetContextObject( $self->ContextObject ); | |
2015 | if ( $self->BasedOn ) { | |
2016 | $obj->Load( $self->BasedOn ); | |
2017 | } | |
2018 | return $obj; | |
2019 | } | |
2020 | ||
2021 | ||
2022 | ||
2023 | ||
2024 | ||
2025 | ||
2026 | =head2 id | |
2027 | ||
2028 | Returns the current value of id. | |
2029 | (In the database, id is stored as int(11).) | |
2030 | ||
2031 | ||
2032 | =cut | |
2033 | ||
2034 | ||
2035 | =head2 Name | |
2036 | ||
2037 | Returns the current value of Name. | |
2038 | (In the database, Name is stored as varchar(200).) | |
2039 | ||
2040 | ||
2041 | ||
2042 | =head2 SetName VALUE | |
2043 | ||
2044 | ||
2045 | Set Name to VALUE. | |
2046 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2047 | (In the database, Name will be stored as a varchar(200).) | |
2048 | ||
2049 | ||
2050 | =cut | |
2051 | ||
2052 | ||
2053 | =head2 Type | |
2054 | ||
2055 | Returns the current value of Type. | |
2056 | (In the database, Type is stored as varchar(200).) | |
2057 | ||
2058 | ||
2059 | ||
2060 | =head2 SetType VALUE | |
2061 | ||
2062 | ||
2063 | Set Type to VALUE. | |
2064 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2065 | (In the database, Type will be stored as a varchar(200).) | |
2066 | ||
2067 | ||
2068 | =cut | |
2069 | ||
2070 | ||
2071 | =head2 RenderType | |
2072 | ||
2073 | Returns the current value of RenderType. | |
2074 | (In the database, RenderType is stored as varchar(64).) | |
2075 | ||
2076 | ||
2077 | ||
2078 | =head2 SetRenderType VALUE | |
2079 | ||
2080 | ||
2081 | Set RenderType to VALUE. | |
2082 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2083 | (In the database, RenderType will be stored as a varchar(64).) | |
2084 | ||
2085 | ||
2086 | =cut | |
2087 | ||
2088 | ||
2089 | =head2 MaxValues | |
2090 | ||
2091 | Returns the current value of MaxValues. | |
2092 | (In the database, MaxValues is stored as int(11).) | |
2093 | ||
2094 | ||
2095 | ||
2096 | =head2 SetMaxValues VALUE | |
2097 | ||
2098 | ||
2099 | Set MaxValues to VALUE. | |
2100 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2101 | (In the database, MaxValues will be stored as a int(11).) | |
2102 | ||
2103 | ||
2104 | =cut | |
2105 | ||
2106 | ||
2107 | =head2 Pattern | |
2108 | ||
2109 | Returns the current value of Pattern. | |
2110 | (In the database, Pattern is stored as text.) | |
2111 | ||
2112 | ||
2113 | ||
2114 | =head2 SetPattern VALUE | |
2115 | ||
2116 | ||
2117 | Set Pattern to VALUE. | |
2118 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2119 | (In the database, Pattern will be stored as a text.) | |
2120 | ||
2121 | ||
2122 | =cut | |
2123 | ||
2124 | ||
84fb5b46 MKG |
2125 | =head2 BasedOn |
2126 | ||
2127 | Returns the current value of BasedOn. | |
2128 | (In the database, BasedOn is stored as int(11).) | |
2129 | ||
2130 | ||
2131 | ||
2132 | =head2 SetBasedOn VALUE | |
2133 | ||
2134 | ||
2135 | Set BasedOn to VALUE. | |
2136 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2137 | (In the database, BasedOn will be stored as a int(11).) | |
2138 | ||
2139 | ||
2140 | =cut | |
2141 | ||
2142 | ||
2143 | =head2 Description | |
2144 | ||
2145 | Returns the current value of Description. | |
2146 | (In the database, Description is stored as varchar(255).) | |
2147 | ||
2148 | ||
2149 | ||
2150 | =head2 SetDescription VALUE | |
2151 | ||
2152 | ||
2153 | Set Description to VALUE. | |
2154 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2155 | (In the database, Description will be stored as a varchar(255).) | |
2156 | ||
2157 | ||
2158 | =cut | |
2159 | ||
2160 | ||
2161 | =head2 SortOrder | |
2162 | ||
2163 | Returns the current value of SortOrder. | |
2164 | (In the database, SortOrder is stored as int(11).) | |
2165 | ||
2166 | ||
2167 | ||
2168 | =head2 SetSortOrder VALUE | |
2169 | ||
2170 | ||
2171 | Set SortOrder to VALUE. | |
2172 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2173 | (In the database, SortOrder will be stored as a int(11).) | |
2174 | ||
2175 | ||
2176 | =cut | |
2177 | ||
2178 | ||
2179 | =head2 LookupType | |
2180 | ||
2181 | Returns the current value of LookupType. | |
2182 | (In the database, LookupType is stored as varchar(255).) | |
2183 | ||
2184 | ||
2185 | ||
2186 | =head2 SetLookupType VALUE | |
2187 | ||
2188 | ||
2189 | Set LookupType to VALUE. | |
2190 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2191 | (In the database, LookupType will be stored as a varchar(255).) | |
2192 | ||
2193 | ||
2194 | =cut | |
2195 | ||
2196 | ||
2197 | =head2 Creator | |
2198 | ||
2199 | Returns the current value of Creator. | |
2200 | (In the database, Creator is stored as int(11).) | |
2201 | ||
2202 | ||
2203 | =cut | |
2204 | ||
2205 | ||
2206 | =head2 Created | |
2207 | ||
2208 | Returns the current value of Created. | |
2209 | (In the database, Created is stored as datetime.) | |
2210 | ||
2211 | ||
2212 | =cut | |
2213 | ||
2214 | ||
2215 | =head2 LastUpdatedBy | |
2216 | ||
2217 | Returns the current value of LastUpdatedBy. | |
2218 | (In the database, LastUpdatedBy is stored as int(11).) | |
2219 | ||
2220 | ||
2221 | =cut | |
2222 | ||
2223 | ||
2224 | =head2 LastUpdated | |
2225 | ||
2226 | Returns the current value of LastUpdated. | |
2227 | (In the database, LastUpdated is stored as datetime.) | |
2228 | ||
2229 | ||
2230 | =cut | |
2231 | ||
2232 | ||
2233 | =head2 Disabled | |
2234 | ||
2235 | Returns the current value of Disabled. | |
2236 | (In the database, Disabled is stored as smallint(6).) | |
2237 | ||
2238 | ||
2239 | ||
2240 | =head2 SetDisabled VALUE | |
2241 | ||
2242 | ||
2243 | Set Disabled to VALUE. | |
2244 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
2245 | (In the database, Disabled will be stored as a smallint(6).) | |
2246 | ||
2247 | ||
2248 | =cut | |
2249 | ||
2250 | ||
2251 | ||
2252 | sub _CoreAccessible { | |
2253 | { | |
2254 | ||
2255 | id => | |
2256 | {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, | |
2257 | Name => | |
2258 | {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''}, | |
2259 | Type => | |
2260 | {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''}, | |
2261 | RenderType => | |
2262 | {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, | |
2263 | MaxValues => | |
2264 | {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, | |
2265 | Pattern => | |
2266 | {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, | |
c36a7e1d MKG |
2267 | ValuesClass => |
2268 | {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, | |
84fb5b46 MKG |
2269 | BasedOn => |
2270 | {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, | |
2271 | Description => | |
2272 | {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, | |
2273 | SortOrder => | |
2274 | {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, | |
2275 | LookupType => | |
2276 | {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, | |
2277 | Creator => | |
2278 | {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, | |
2279 | Created => | |
2280 | {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, | |
2281 | LastUpdatedBy => | |
2282 | {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, | |
2283 | LastUpdated => | |
2284 | {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, | |
2285 | Disabled => | |
2286 | {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'}, | |
2287 | ||
2288 | } | |
2289 | }; | |
2290 | ||
af59614d MKG |
2291 | sub FindDependencies { |
2292 | my $self = shift; | |
2293 | my ($walker, $deps) = @_; | |
2294 | ||
2295 | $self->SUPER::FindDependencies($walker, $deps); | |
2296 | ||
2297 | $deps->Add( out => $self->BasedOnObj ) | |
2298 | if $self->BasedOnObj->id; | |
2299 | ||
2300 | my $applied = RT::ObjectCustomFields->new( $self->CurrentUser ); | |
2301 | $applied->LimitToCustomField( $self->id ); | |
2302 | $deps->Add( in => $applied ); | |
2303 | ||
2304 | $deps->Add( in => $self->Values ) if $self->ValuesClass eq "RT::CustomFieldValues"; | |
2305 | } | |
2306 | ||
84fb5b46 MKG |
2307 | |
2308 | RT::Base->_ImportOverlays(); | |
2309 | ||
2310 | 1; |