]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | # BEGIN BPS TAGGED BLOCK {{{ |
2 | # | |
3 | # COPYRIGHT: | |
4 | # | |
5 | # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC | |
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 | =head1 NAME | |
50 | ||
51 | RT::Record - Base class for RT record objects | |
52 | ||
53 | =head1 SYNOPSIS | |
54 | ||
55 | ||
56 | =head1 DESCRIPTION | |
57 | ||
58 | ||
59 | ||
60 | =head1 METHODS | |
61 | ||
62 | =cut | |
63 | ||
64 | package RT::Record; | |
65 | ||
66 | use strict; | |
67 | use warnings; | |
68 | ||
69 | ||
70 | use RT::Date; | |
71 | use RT::User; | |
72 | use RT::Attributes; | |
73 | use Encode qw(); | |
74 | ||
75 | our $_TABLE_ATTR = { }; | |
76 | use base RT->Config->Get('RecordBaseClass'); | |
77 | use base 'RT::Base'; | |
78 | ||
79 | ||
80 | sub _Init { | |
81 | my $self = shift; | |
82 | $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)}); | |
83 | $self->CurrentUser(@_); | |
84 | } | |
85 | ||
86 | ||
87 | ||
88 | =head2 _PrimaryKeys | |
89 | ||
90 | The primary keys for RT classes is 'id' | |
91 | ||
92 | =cut | |
93 | ||
94 | sub _PrimaryKeys { return ['id'] } | |
95 | # short circuit many, many thousands of calls from searchbuilder | |
96 | sub _PrimaryKey { 'id' } | |
97 | ||
98 | =head2 Id | |
99 | ||
100 | Override L<DBIx::SearchBuilder/Id> to avoid a few lookups RT doesn't do | |
101 | on a very common codepath | |
102 | ||
103 | C<id> is an alias to C<Id> and is the preferred way to call this method. | |
104 | ||
105 | =cut | |
106 | ||
107 | sub Id { | |
108 | return shift->{'values'}->{id}; | |
109 | } | |
110 | ||
111 | *id = \&Id; | |
112 | ||
113 | =head2 Delete | |
114 | ||
115 | Delete this record object from the database. | |
116 | ||
117 | =cut | |
118 | ||
119 | sub Delete { | |
120 | my $self = shift; | |
121 | my ($rv) = $self->SUPER::Delete; | |
122 | if ($rv) { | |
123 | return ($rv, $self->loc("Object deleted")); | |
124 | } else { | |
125 | ||
126 | return(0, $self->loc("Object could not be deleted")) | |
127 | } | |
128 | } | |
129 | ||
130 | =head2 ObjectTypeStr | |
131 | ||
132 | Returns a string which is this object's type. The type is the class, | |
133 | without the "RT::" prefix. | |
134 | ||
135 | ||
136 | =cut | |
137 | ||
138 | sub ObjectTypeStr { | |
139 | my $self = shift; | |
140 | if (ref($self) =~ /^.*::(\w+)$/) { | |
141 | return $self->loc($1); | |
142 | } else { | |
143 | return $self->loc(ref($self)); | |
144 | } | |
145 | } | |
146 | ||
147 | =head2 Attributes | |
148 | ||
149 | Return this object's attributes as an RT::Attributes object | |
150 | ||
151 | =cut | |
152 | ||
153 | sub Attributes { | |
154 | my $self = shift; | |
155 | unless ($self->{'attributes'}) { | |
156 | $self->{'attributes'} = RT::Attributes->new($self->CurrentUser); | |
157 | $self->{'attributes'}->LimitToObject($self); | |
158 | $self->{'attributes'}->OrderByCols({FIELD => 'id'}); | |
159 | } | |
160 | return ($self->{'attributes'}); | |
161 | } | |
162 | ||
163 | ||
164 | =head2 AddAttribute { Name, Description, Content } | |
165 | ||
166 | Adds a new attribute for this object. | |
167 | ||
168 | =cut | |
169 | ||
170 | sub AddAttribute { | |
171 | my $self = shift; | |
172 | my %args = ( Name => undef, | |
173 | Description => undef, | |
174 | Content => undef, | |
175 | @_ ); | |
176 | ||
177 | my $attr = RT::Attribute->new( $self->CurrentUser ); | |
178 | my ( $id, $msg ) = $attr->Create( | |
179 | Object => $self, | |
180 | Name => $args{'Name'}, | |
181 | Description => $args{'Description'}, | |
182 | Content => $args{'Content'} ); | |
183 | ||
184 | ||
185 | # XXX TODO: Why won't RedoSearch work here? | |
186 | $self->Attributes->_DoSearch; | |
187 | ||
188 | return ($id, $msg); | |
189 | } | |
190 | ||
191 | ||
192 | =head2 SetAttribute { Name, Description, Content } | |
193 | ||
194 | Like AddAttribute, but replaces all existing attributes with the same Name. | |
195 | ||
196 | =cut | |
197 | ||
198 | sub SetAttribute { | |
199 | my $self = shift; | |
200 | my %args = ( Name => undef, | |
201 | Description => undef, | |
202 | Content => undef, | |
203 | @_ ); | |
204 | ||
205 | my @AttributeObjs = $self->Attributes->Named( $args{'Name'} ) | |
206 | or return $self->AddAttribute( %args ); | |
207 | ||
208 | my $AttributeObj = pop( @AttributeObjs ); | |
209 | $_->Delete foreach @AttributeObjs; | |
210 | ||
211 | $AttributeObj->SetDescription( $args{'Description'} ); | |
212 | $AttributeObj->SetContent( $args{'Content'} ); | |
213 | ||
214 | $self->Attributes->RedoSearch; | |
215 | return 1; | |
216 | } | |
217 | ||
218 | =head2 DeleteAttribute NAME | |
219 | ||
220 | Deletes all attributes with the matching name for this object. | |
221 | ||
222 | =cut | |
223 | ||
224 | sub DeleteAttribute { | |
225 | my $self = shift; | |
226 | my $name = shift; | |
227 | my ($val,$msg) = $self->Attributes->DeleteEntry( Name => $name ); | |
228 | $self->ClearAttributes; | |
229 | return ($val,$msg); | |
230 | } | |
231 | ||
232 | =head2 FirstAttribute NAME | |
233 | ||
234 | Returns the first attribute with the matching name for this object (as an | |
235 | L<RT::Attribute> object), or C<undef> if no such attributes exist. | |
236 | If there is more than one attribute with the matching name on the | |
237 | object, the first value that was set is returned. | |
238 | ||
239 | =cut | |
240 | ||
241 | sub FirstAttribute { | |
242 | my $self = shift; | |
243 | my $name = shift; | |
244 | return ($self->Attributes->Named( $name ))[0]; | |
245 | } | |
246 | ||
247 | ||
248 | sub ClearAttributes { | |
249 | my $self = shift; | |
250 | delete $self->{'attributes'}; | |
251 | ||
252 | } | |
253 | ||
254 | sub _Handle { return $RT::Handle } | |
255 | ||
256 | ||
257 | ||
258 | =head2 Create PARAMHASH | |
259 | ||
260 | Takes a PARAMHASH of Column -> Value pairs. | |
261 | If any Column has a Validate$PARAMNAME subroutine defined and the | |
262 | value provided doesn't pass validation, this routine returns | |
263 | an error. | |
264 | ||
265 | If this object's table has any of the following atetributes defined as | |
266 | 'Auto', this routine will automatically fill in their values. | |
267 | ||
268 | =over | |
269 | ||
270 | =item Created | |
271 | ||
272 | =item Creator | |
273 | ||
274 | =item LastUpdated | |
275 | ||
276 | =item LastUpdatedBy | |
277 | ||
278 | =back | |
279 | ||
280 | =cut | |
281 | ||
282 | sub Create { | |
283 | my $self = shift; | |
284 | my %attribs = (@_); | |
285 | foreach my $key ( keys %attribs ) { | |
286 | if (my $method = $self->can("Validate$key")) { | |
287 | if (! $method->( $self, $attribs{$key} ) ) { | |
288 | if (wantarray) { | |
289 | return ( 0, $self->loc('Invalid value for [_1]', $key) ); | |
290 | } | |
291 | else { | |
292 | return (0); | |
293 | } | |
294 | } | |
295 | } | |
296 | } | |
297 | ||
298 | ||
299 | ||
300 | my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) = gmtime(); | |
301 | ||
302 | my $now_iso = | |
303 | sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $mday, $hour, $min, $sec); | |
304 | ||
305 | $attribs{'Created'} = $now_iso if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'}); | |
306 | ||
307 | if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) { | |
308 | $attribs{'Creator'} = $self->CurrentUser->id || '0'; | |
309 | } | |
310 | $attribs{'LastUpdated'} = $now_iso | |
311 | if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'}); | |
312 | ||
313 | $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0' | |
314 | if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'}); | |
315 | ||
316 | my $id = $self->SUPER::Create(%attribs); | |
317 | if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) { | |
318 | if ( $id->errno ) { | |
319 | if (wantarray) { | |
320 | return ( 0, | |
321 | $self->loc( "Internal Error: [_1]", $id->{error_message} ) ); | |
322 | } | |
323 | else { | |
324 | return (0); | |
325 | } | |
326 | } | |
327 | } | |
328 | # If the object was created in the database, | |
329 | # load it up now, so we're sure we get what the database | |
330 | # has. Arguably, this should not be necessary, but there | |
331 | # isn't much we can do about it. | |
332 | ||
333 | unless ($id) { | |
334 | if (wantarray) { | |
335 | return ( $id, $self->loc('Object could not be created') ); | |
336 | } | |
337 | else { | |
338 | return ($id); | |
339 | } | |
340 | ||
341 | } | |
342 | ||
343 | if (UNIVERSAL::isa('errno',$id)) { | |
344 | return(undef); | |
345 | } | |
346 | ||
347 | $self->Load($id) if ($id); | |
348 | ||
349 | ||
350 | ||
351 | if (wantarray) { | |
352 | return ( $id, $self->loc('Object created') ); | |
353 | } | |
354 | else { | |
355 | return ($id); | |
356 | } | |
357 | ||
358 | } | |
359 | ||
360 | ||
361 | ||
362 | =head2 LoadByCols | |
363 | ||
364 | Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the | |
365 | DB is case sensitive | |
366 | ||
367 | =cut | |
368 | ||
369 | sub LoadByCols { | |
370 | my $self = shift; | |
371 | ||
372 | # We don't want to hang onto this | |
373 | $self->ClearAttributes; | |
374 | ||
375 | return $self->SUPER::LoadByCols( @_ ) unless $self->_Handle->CaseSensitive; | |
376 | ||
377 | # If this database is case sensitive we need to uncase objects for | |
378 | # explicit loading | |
379 | my %hash = (@_); | |
380 | foreach my $key ( keys %hash ) { | |
381 | ||
382 | # If we've been passed an empty value, we can't do the lookup. | |
383 | # We don't need to explicitly downcase integers or an id. | |
384 | if ( $key ne 'id' && defined $hash{ $key } && $hash{ $key } !~ /^\d+$/ ) { | |
385 | my ($op, $val, $func); | |
386 | ($key, $op, $val, $func) = | |
387 | $self->_Handle->_MakeClauseCaseInsensitive( $key, '=', delete $hash{ $key } ); | |
388 | $hash{$key}->{operator} = $op; | |
389 | $hash{$key}->{value} = $val; | |
390 | $hash{$key}->{function} = $func; | |
391 | } | |
392 | } | |
393 | return $self->SUPER::LoadByCols( %hash ); | |
394 | } | |
395 | ||
396 | ||
397 | ||
398 | # There is room for optimizations in most of those subs: | |
399 | ||
400 | ||
401 | sub LastUpdatedObj { | |
402 | my $self = shift; | |
403 | my $obj = RT::Date->new( $self->CurrentUser ); | |
404 | ||
405 | $obj->Set( Format => 'sql', Value => $self->LastUpdated ); | |
406 | return $obj; | |
407 | } | |
408 | ||
409 | ||
410 | ||
411 | sub CreatedObj { | |
412 | my $self = shift; | |
413 | my $obj = RT::Date->new( $self->CurrentUser ); | |
414 | ||
415 | $obj->Set( Format => 'sql', Value => $self->Created ); | |
416 | ||
417 | return $obj; | |
418 | } | |
419 | ||
420 | ||
421 | # | |
422 | # TODO: This should be deprecated | |
423 | # | |
424 | sub AgeAsString { | |
425 | my $self = shift; | |
426 | return ( $self->CreatedObj->AgeAsString() ); | |
427 | } | |
428 | ||
429 | ||
430 | ||
431 | # TODO this should be deprecated | |
432 | ||
433 | sub LastUpdatedAsString { | |
434 | my $self = shift; | |
435 | if ( $self->LastUpdated ) { | |
436 | return ( $self->LastUpdatedObj->AsString() ); | |
437 | ||
438 | } | |
439 | else { | |
440 | return "never"; | |
441 | } | |
442 | } | |
443 | ||
444 | ||
445 | # | |
446 | # TODO This should be deprecated | |
447 | # | |
448 | sub CreatedAsString { | |
449 | my $self = shift; | |
450 | return ( $self->CreatedObj->AsString() ); | |
451 | } | |
452 | ||
453 | ||
454 | # | |
455 | # TODO This should be deprecated | |
456 | # | |
457 | sub LongSinceUpdateAsString { | |
458 | my $self = shift; | |
459 | if ( $self->LastUpdated ) { | |
460 | ||
461 | return ( $self->LastUpdatedObj->AgeAsString() ); | |
462 | ||
463 | } | |
464 | else { | |
465 | return "never"; | |
466 | } | |
467 | } | |
468 | ||
469 | ||
470 | ||
471 | # | |
472 | sub _Set { | |
473 | my $self = shift; | |
474 | ||
475 | my %args = ( | |
476 | Field => undef, | |
477 | Value => undef, | |
478 | IsSQL => undef, | |
479 | @_ | |
480 | ); | |
481 | ||
482 | #if the user is trying to modify the record | |
483 | # TODO: document _why_ this code is here | |
484 | ||
485 | if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) { | |
486 | $args{'Value'} = 0; | |
487 | } | |
488 | ||
489 | my $old_val = $self->__Value($args{'Field'}); | |
490 | $self->_SetLastUpdated(); | |
491 | my $ret = $self->SUPER::_Set( | |
492 | Field => $args{'Field'}, | |
493 | Value => $args{'Value'}, | |
494 | IsSQL => $args{'IsSQL'} | |
495 | ); | |
496 | my ($status, $msg) = $ret->as_array(); | |
497 | ||
498 | # @values has two values, a status code and a message. | |
499 | ||
500 | # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool | |
501 | # we want to change the standard "success" message | |
502 | if ($status) { | |
503 | $msg = | |
504 | $self->loc( | |
505 | "[_1] changed from [_2] to [_3]", | |
506 | $self->loc( $args{'Field'} ), | |
507 | ( $old_val ? '"' . $old_val . '"' : $self->loc("(no value)") ), | |
508 | '"' . $self->__Value( $args{'Field'}) . '"' | |
509 | ); | |
510 | } else { | |
511 | ||
512 | $msg = $self->CurrentUser->loc_fuzzy($msg); | |
513 | } | |
514 | return wantarray ? ($status, $msg) : $ret; | |
515 | ||
516 | } | |
517 | ||
518 | ||
519 | ||
520 | =head2 _SetLastUpdated | |
521 | ||
522 | This routine updates the LastUpdated and LastUpdatedBy columns of the row in question | |
523 | It takes no options. Arguably, this is a bug | |
524 | ||
525 | =cut | |
526 | ||
527 | sub _SetLastUpdated { | |
528 | my $self = shift; | |
529 | use RT::Date; | |
530 | my $now = RT::Date->new( $self->CurrentUser ); | |
531 | $now->SetToNow(); | |
532 | ||
533 | if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) { | |
534 | my ( $msg, $val ) = $self->__Set( | |
535 | Field => 'LastUpdated', | |
536 | Value => $now->ISO | |
537 | ); | |
538 | } | |
539 | if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) { | |
540 | my ( $msg, $val ) = $self->__Set( | |
541 | Field => 'LastUpdatedBy', | |
542 | Value => $self->CurrentUser->id | |
543 | ); | |
544 | } | |
545 | } | |
546 | ||
547 | ||
548 | ||
549 | =head2 CreatorObj | |
550 | ||
551 | Returns an RT::User object with the RT account of the creator of this row | |
552 | ||
553 | =cut | |
554 | ||
555 | sub CreatorObj { | |
556 | my $self = shift; | |
557 | unless ( exists $self->{'CreatorObj'} ) { | |
558 | ||
559 | $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser ); | |
560 | $self->{'CreatorObj'}->Load( $self->Creator ); | |
561 | } | |
562 | return ( $self->{'CreatorObj'} ); | |
563 | } | |
564 | ||
565 | ||
566 | ||
567 | =head2 LastUpdatedByObj | |
568 | ||
569 | Returns an RT::User object of the last user to touch this object | |
570 | ||
571 | =cut | |
572 | ||
573 | sub LastUpdatedByObj { | |
574 | my $self = shift; | |
575 | unless ( exists $self->{LastUpdatedByObj} ) { | |
576 | $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser ); | |
577 | $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy ); | |
578 | } | |
579 | return $self->{'LastUpdatedByObj'}; | |
580 | } | |
581 | ||
582 | ||
583 | ||
584 | =head2 URI | |
585 | ||
586 | Returns this record's URI | |
587 | ||
588 | =cut | |
589 | ||
590 | sub URI { | |
591 | my $self = shift; | |
592 | my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser); | |
593 | return($uri->URIForObject($self)); | |
594 | } | |
595 | ||
596 | ||
597 | =head2 ValidateName NAME | |
598 | ||
599 | Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name | |
600 | ||
601 | =cut | |
602 | ||
603 | sub ValidateName { | |
604 | my $self = shift; | |
605 | my $value = shift; | |
606 | if (defined $value && $value=~ /^\d+$/) { | |
607 | return(0); | |
608 | } else { | |
609 | return(1); | |
610 | } | |
611 | } | |
612 | ||
613 | ||
614 | ||
615 | =head2 SQLType attribute | |
616 | ||
617 | return the SQL type for the attribute 'attribute' as stored in _ClassAccessible | |
618 | ||
619 | =cut | |
620 | ||
621 | sub SQLType { | |
622 | my $self = shift; | |
623 | my $field = shift; | |
624 | ||
625 | return ($self->_Accessible($field, 'type')); | |
626 | ||
627 | ||
628 | } | |
629 | ||
630 | sub __Value { | |
631 | my $self = shift; | |
632 | my $field = shift; | |
633 | my %args = ( decode_utf8 => 1, @_ ); | |
634 | ||
635 | unless ($field) { | |
636 | $RT::Logger->error("__Value called with undef field"); | |
637 | } | |
638 | ||
639 | my $value = $self->SUPER::__Value($field); | |
640 | ||
86404187 MKG |
641 | return undef if (!defined $value); |
642 | ||
84fb5b46 MKG |
643 | if ( $args{'decode_utf8'} ) { |
644 | if ( !utf8::is_utf8($value) ) { | |
645 | utf8::decode($value); | |
646 | } | |
647 | } | |
648 | else { | |
649 | if ( utf8::is_utf8($value) ) { | |
650 | utf8::encode($value); | |
651 | } | |
652 | } | |
653 | ||
654 | return $value; | |
655 | ||
656 | } | |
657 | ||
658 | # Set up defaults for DBIx::SearchBuilder::Record::Cachable | |
659 | ||
660 | sub _CacheConfig { | |
661 | { | |
662 | 'cache_p' => 1, | |
663 | 'cache_for_sec' => 30, | |
664 | } | |
665 | } | |
666 | ||
667 | ||
668 | ||
669 | sub _BuildTableAttributes { | |
670 | my $self = shift; | |
671 | my $class = ref($self) || $self; | |
672 | ||
673 | my $attributes; | |
674 | if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) { | |
675 | $attributes = $self->_CoreAccessible(); | |
676 | } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) { | |
677 | $attributes = $self->_ClassAccessible(); | |
678 | ||
679 | } | |
680 | ||
681 | foreach my $column (keys %$attributes) { | |
682 | foreach my $attr ( keys %{ $attributes->{$column} } ) { | |
683 | $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr}; | |
684 | } | |
685 | } | |
686 | foreach my $method ( qw(_OverlayAccessible _VendorAccessible _LocalAccessible) ) { | |
687 | next unless UNIVERSAL::can( $self, $method ); | |
688 | $attributes = $self->$method(); | |
689 | ||
690 | foreach my $column ( keys %$attributes ) { | |
691 | foreach my $attr ( keys %{ $attributes->{$column} } ) { | |
692 | $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr}; | |
693 | } | |
694 | } | |
695 | } | |
696 | } | |
697 | ||
698 | ||
699 | =head2 _ClassAccessible | |
700 | ||
701 | Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in | |
702 | DBIx::SearchBuilder::Record | |
703 | ||
704 | =cut | |
705 | ||
706 | sub _ClassAccessible { | |
707 | my $self = shift; | |
708 | return $_TABLE_ATTR->{ref($self) || $self}; | |
709 | } | |
710 | ||
711 | =head2 _Accessible COLUMN ATTRIBUTE | |
712 | ||
713 | returns the value of ATTRIBUTE for COLUMN | |
714 | ||
715 | ||
716 | =cut | |
717 | ||
718 | sub _Accessible { | |
719 | my $self = shift; | |
720 | my $column = shift; | |
721 | my $attribute = lc(shift); | |
722 | return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column}); | |
723 | return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0; | |
724 | ||
725 | } | |
726 | ||
727 | =head2 _EncodeLOB BODY MIME_TYPE | |
728 | ||
729 | Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database | |
730 | ||
731 | =cut | |
732 | ||
733 | sub _EncodeLOB { | |
734 | my $self = shift; | |
735 | my $Body = shift; | |
736 | my $MIMEType = shift || ''; | |
737 | my $Filename = shift; | |
738 | ||
739 | my $ContentEncoding = 'none'; | |
740 | ||
741 | #get the max attachment length from RT | |
742 | my $MaxSize = RT->Config->Get('MaxAttachmentSize'); | |
743 | ||
744 | #if the current attachment contains nulls and the | |
745 | #database doesn't support embedded nulls | |
746 | ||
747 | if ( ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) { | |
748 | ||
749 | # set a flag telling us to mimencode the attachment | |
750 | $ContentEncoding = 'base64'; | |
751 | ||
752 | #cut the max attchment size by 25% (for mime-encoding overhead. | |
753 | $RT::Logger->debug("Max size is $MaxSize"); | |
754 | $MaxSize = $MaxSize * 3 / 4; | |
755 | # Some databases (postgres) can't handle non-utf8 data | |
756 | } elsif ( !$RT::Handle->BinarySafeBLOBs | |
757 | && $MIMEType !~ /text\/plain/gi | |
758 | && !Encode::is_utf8( $Body, 1 ) ) { | |
759 | $ContentEncoding = 'quoted-printable'; | |
760 | } | |
761 | ||
762 | #if the attachment is larger than the maximum size | |
763 | if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) { | |
764 | ||
765 | # if we're supposed to truncate large attachments | |
766 | if (RT->Config->Get('TruncateLongAttachments')) { | |
767 | ||
768 | # truncate the attachment to that length. | |
769 | $Body = substr( $Body, 0, $MaxSize ); | |
770 | ||
771 | } | |
772 | ||
773 | # elsif we're supposed to drop large attachments on the floor, | |
774 | elsif (RT->Config->Get('DropLongAttachments')) { | |
775 | ||
776 | # drop the attachment on the floor | |
777 | $RT::Logger->info( "$self: Dropped an attachment of size " | |
778 | . length($Body)); | |
779 | $RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) ); | |
780 | $Filename .= ".txt" if $Filename; | |
781 | return ("none", "Large attachment dropped", "plain/text", $Filename ); | |
782 | } | |
783 | } | |
784 | ||
785 | # if we need to mimencode the attachment | |
786 | if ( $ContentEncoding eq 'base64' ) { | |
787 | ||
788 | # base64 encode the attachment | |
789 | Encode::_utf8_off($Body); | |
790 | $Body = MIME::Base64::encode_base64($Body); | |
791 | ||
792 | } elsif ($ContentEncoding eq 'quoted-printable') { | |
793 | Encode::_utf8_off($Body); | |
794 | $Body = MIME::QuotedPrint::encode($Body); | |
795 | } | |
796 | ||
797 | ||
798 | return ($ContentEncoding, $Body, $MIMEType, $Filename ); | |
799 | ||
800 | } | |
801 | ||
802 | sub _DecodeLOB { | |
803 | my $self = shift; | |
804 | my $ContentType = shift || ''; | |
805 | my $ContentEncoding = shift || 'none'; | |
806 | my $Content = shift; | |
807 | ||
808 | if ( $ContentEncoding eq 'base64' ) { | |
809 | $Content = MIME::Base64::decode_base64($Content); | |
810 | } | |
811 | elsif ( $ContentEncoding eq 'quoted-printable' ) { | |
812 | $Content = MIME::QuotedPrint::decode($Content); | |
813 | } | |
814 | elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) { | |
815 | return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) ); | |
816 | } | |
817 | if ( RT::I18N::IsTextualContentType($ContentType) ) { | |
818 | $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content); | |
819 | } | |
820 | return ($Content); | |
821 | } | |
822 | ||
823 | # A helper table for links mapping to make it easier | |
824 | # to build and parse links between tickets | |
825 | ||
826 | use vars '%LINKDIRMAP'; | |
827 | ||
828 | %LINKDIRMAP = ( | |
829 | MemberOf => { Base => 'MemberOf', | |
830 | Target => 'HasMember', }, | |
831 | RefersTo => { Base => 'RefersTo', | |
832 | Target => 'ReferredToBy', }, | |
833 | DependsOn => { Base => 'DependsOn', | |
834 | Target => 'DependedOnBy', }, | |
835 | MergedInto => { Base => 'MergedInto', | |
836 | Target => 'MergedInto', }, | |
837 | ||
838 | ); | |
839 | ||
840 | =head2 Update ARGSHASH | |
841 | ||
842 | Updates fields on an object for you using the proper Set methods, | |
843 | skipping unchanged values. | |
844 | ||
845 | ARGSRef => a hashref of attributes => value for the update | |
846 | AttributesRef => an arrayref of keys in ARGSRef that should be updated | |
847 | AttributePrefix => a prefix that should be added to the attributes in AttributesRef | |
848 | when looking up values in ARGSRef | |
849 | Bare attributes are tried before prefixed attributes | |
850 | ||
851 | Returns a list of localized results of the update | |
852 | ||
853 | =cut | |
854 | ||
855 | sub Update { | |
856 | my $self = shift; | |
857 | ||
858 | my %args = ( | |
859 | ARGSRef => undef, | |
860 | AttributesRef => undef, | |
861 | AttributePrefix => undef, | |
862 | @_ | |
863 | ); | |
864 | ||
865 | my $attributes = $args{'AttributesRef'}; | |
866 | my $ARGSRef = $args{'ARGSRef'}; | |
867 | my %new_values; | |
868 | ||
869 | # gather all new values | |
870 | foreach my $attribute (@$attributes) { | |
871 | my $value; | |
872 | if ( defined $ARGSRef->{$attribute} ) { | |
873 | $value = $ARGSRef->{$attribute}; | |
874 | } | |
875 | elsif ( | |
876 | defined( $args{'AttributePrefix'} ) | |
877 | && defined( | |
878 | $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute } | |
879 | ) | |
880 | ) { | |
881 | $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }; | |
882 | ||
883 | } | |
884 | else { | |
885 | next; | |
886 | } | |
887 | ||
888 | $value =~ s/\r\n/\n/gs; | |
889 | ||
890 | # If Queue is 'General', we want to resolve the queue name for | |
891 | # the object. | |
892 | ||
893 | # This is in an eval block because $object might not exist. | |
894 | # and might not have a Name method. But "can" won't find autoloaded | |
895 | # items. If it fails, we don't care | |
896 | do { | |
897 | no warnings "uninitialized"; | |
898 | local $@; | |
899 | eval { | |
900 | my $object = $attribute . "Obj"; | |
901 | my $name = $self->$object->Name; | |
902 | next if $name eq $value || $name eq ($value || 0); | |
903 | }; | |
904 | next if $value eq $self->$attribute(); | |
905 | next if ($value || 0) eq $self->$attribute(); | |
906 | }; | |
907 | ||
908 | $new_values{$attribute} = $value; | |
909 | } | |
910 | ||
911 | return $self->_UpdateAttributes( | |
912 | Attributes => $attributes, | |
913 | NewValues => \%new_values, | |
914 | ); | |
915 | } | |
916 | ||
917 | sub _UpdateAttributes { | |
918 | my $self = shift; | |
919 | my %args = ( | |
920 | Attributes => [], | |
921 | NewValues => {}, | |
922 | @_, | |
923 | ); | |
924 | ||
925 | my @results; | |
926 | ||
927 | foreach my $attribute (@{ $args{Attributes} }) { | |
928 | next if !exists($args{NewValues}{$attribute}); | |
929 | ||
930 | my $value = $args{NewValues}{$attribute}; | |
931 | my $method = "Set$attribute"; | |
932 | my ( $code, $msg ) = $self->$method($value); | |
933 | my ($prefix) = ref($self) =~ /RT(?:.*)::(\w+)/; | |
934 | ||
935 | # Default to $id, but use name if we can get it. | |
936 | my $label = $self->id; | |
937 | $label = $self->Name if (UNIVERSAL::can($self,'Name')); | |
938 | # this requires model names to be loc'ed. | |
939 | ||
940 | =for loc | |
941 | ||
942 | "Ticket" # loc | |
943 | "User" # loc | |
944 | "Group" # loc | |
945 | "Queue" # loc | |
946 | ||
947 | =cut | |
948 | ||
949 | push @results, $self->loc( $prefix ) . " $label: ". $msg; | |
950 | ||
951 | =for loc | |
952 | ||
953 | "[_1] could not be set to [_2].", # loc | |
954 | "That is already the current value", # loc | |
955 | "No value sent to _Set!", # loc | |
956 | "Illegal value for [_1]", # loc | |
957 | "The new value has been set.", # loc | |
958 | "No column specified", # loc | |
959 | "Immutable field", # loc | |
960 | "Nonexistant field?", # loc | |
961 | "Invalid data", # loc | |
962 | "Couldn't find row", # loc | |
963 | "Missing a primary key?: [_1]", # loc | |
964 | "Found Object", # loc | |
965 | ||
966 | =cut | |
967 | ||
968 | } | |
969 | ||
970 | return @results; | |
971 | } | |
972 | ||
973 | ||
974 | ||
975 | ||
976 | =head2 Members | |
977 | ||
978 | This returns an RT::Links object which references all the tickets | |
979 | which are 'MembersOf' this ticket | |
980 | ||
981 | =cut | |
982 | ||
983 | sub Members { | |
984 | my $self = shift; | |
985 | return ( $self->_Links( 'Target', 'MemberOf' ) ); | |
986 | } | |
987 | ||
988 | ||
989 | ||
990 | =head2 MemberOf | |
991 | ||
992 | This returns an RT::Links object which references all the tickets that this | |
993 | ticket is a 'MemberOf' | |
994 | ||
995 | =cut | |
996 | ||
997 | sub MemberOf { | |
998 | my $self = shift; | |
999 | return ( $self->_Links( 'Base', 'MemberOf' ) ); | |
1000 | } | |
1001 | ||
1002 | ||
1003 | ||
1004 | =head2 RefersTo | |
1005 | ||
1006 | This returns an RT::Links object which shows all references for which this ticket is a base | |
1007 | ||
1008 | =cut | |
1009 | ||
1010 | sub RefersTo { | |
1011 | my $self = shift; | |
1012 | return ( $self->_Links( 'Base', 'RefersTo' ) ); | |
1013 | } | |
1014 | ||
1015 | ||
1016 | ||
1017 | =head2 ReferredToBy | |
1018 | ||
1019 | This returns an L<RT::Links> object which shows all references for which this ticket is a target | |
1020 | ||
1021 | =cut | |
1022 | ||
1023 | sub ReferredToBy { | |
1024 | my $self = shift; | |
1025 | return ( $self->_Links( 'Target', 'RefersTo' ) ); | |
1026 | } | |
1027 | ||
1028 | ||
1029 | ||
1030 | =head2 DependedOnBy | |
1031 | ||
1032 | This returns an RT::Links object which references all the tickets that depend on this one | |
1033 | ||
1034 | =cut | |
1035 | ||
1036 | sub DependedOnBy { | |
1037 | my $self = shift; | |
1038 | return ( $self->_Links( 'Target', 'DependsOn' ) ); | |
1039 | } | |
1040 | ||
1041 | ||
1042 | ||
1043 | ||
1044 | =head2 HasUnresolvedDependencies | |
1045 | ||
1046 | Takes a paramhash of Type (default to '__any'). Returns the number of | |
1047 | unresolved dependencies, if $self->UnresolvedDependencies returns an | |
1048 | object with one or more members of that type. Returns false | |
1049 | otherwise. | |
1050 | ||
1051 | =cut | |
1052 | ||
1053 | sub HasUnresolvedDependencies { | |
1054 | my $self = shift; | |
1055 | my %args = ( | |
1056 | Type => undef, | |
1057 | @_ | |
1058 | ); | |
1059 | ||
1060 | my $deps = $self->UnresolvedDependencies; | |
1061 | ||
1062 | if ($args{Type}) { | |
1063 | $deps->Limit( FIELD => 'Type', | |
1064 | OPERATOR => '=', | |
1065 | VALUE => $args{Type}); | |
1066 | } | |
1067 | else { | |
1068 | $deps->IgnoreType; | |
1069 | } | |
1070 | ||
1071 | if ($deps->Count > 0) { | |
1072 | return $deps->Count; | |
1073 | } | |
1074 | else { | |
1075 | return (undef); | |
1076 | } | |
1077 | } | |
1078 | ||
1079 | ||
1080 | ||
1081 | =head2 UnresolvedDependencies | |
1082 | ||
1083 | Returns an RT::Tickets object of tickets which this ticket depends on | |
1084 | and which have a status of new, open or stalled. (That list comes from | |
1085 | RT::Queue->ActiveStatusArray | |
1086 | ||
1087 | =cut | |
1088 | ||
1089 | ||
1090 | sub UnresolvedDependencies { | |
1091 | my $self = shift; | |
1092 | my $deps = RT::Tickets->new($self->CurrentUser); | |
1093 | ||
1094 | my @live_statuses = RT::Queue->ActiveStatusArray(); | |
1095 | foreach my $status (@live_statuses) { | |
1096 | $deps->LimitStatus(VALUE => $status); | |
1097 | } | |
1098 | $deps->LimitDependedOnBy($self->Id); | |
1099 | ||
1100 | return($deps); | |
1101 | ||
1102 | } | |
1103 | ||
1104 | ||
1105 | ||
1106 | =head2 AllDependedOnBy | |
1107 | ||
1108 | Returns an array of RT::Ticket objects which (directly or indirectly) | |
1109 | depends on this ticket; takes an optional 'Type' argument in the param | |
1110 | hash, which will limit returned tickets to that type, as well as cause | |
1111 | tickets with that type to serve as 'leaf' nodes that stops the recursive | |
1112 | dependency search. | |
1113 | ||
1114 | =cut | |
1115 | ||
1116 | sub AllDependedOnBy { | |
1117 | my $self = shift; | |
1118 | return $self->_AllLinkedTickets( LinkType => 'DependsOn', | |
1119 | Direction => 'Target', @_ ); | |
1120 | } | |
1121 | ||
1122 | =head2 AllDependsOn | |
1123 | ||
1124 | Returns an array of RT::Ticket objects which this ticket (directly or | |
1125 | indirectly) depends on; takes an optional 'Type' argument in the param | |
1126 | hash, which will limit returned tickets to that type, as well as cause | |
1127 | tickets with that type to serve as 'leaf' nodes that stops the | |
1128 | recursive dependency search. | |
1129 | ||
1130 | =cut | |
1131 | ||
1132 | sub AllDependsOn { | |
1133 | my $self = shift; | |
1134 | return $self->_AllLinkedTickets( LinkType => 'DependsOn', | |
1135 | Direction => 'Base', @_ ); | |
1136 | } | |
1137 | ||
1138 | sub _AllLinkedTickets { | |
1139 | my $self = shift; | |
1140 | ||
1141 | my %args = ( | |
1142 | LinkType => undef, | |
1143 | Direction => undef, | |
1144 | Type => undef, | |
1145 | _found => {}, | |
1146 | _top => 1, | |
1147 | @_ | |
1148 | ); | |
1149 | ||
1150 | my $dep = $self->_Links( $args{Direction}, $args{LinkType}); | |
1151 | while (my $link = $dep->Next()) { | |
1152 | my $uri = $args{Direction} eq 'Target' ? $link->BaseURI : $link->TargetURI; | |
1153 | next unless ($uri->IsLocal()); | |
1154 | my $obj = $args{Direction} eq 'Target' ? $link->BaseObj : $link->TargetObj; | |
1155 | next if $args{_found}{$obj->Id}; | |
1156 | ||
1157 | if (!$args{Type}) { | |
1158 | $args{_found}{$obj->Id} = $obj; | |
1159 | $obj->_AllLinkedTickets( %args, _top => 0 ); | |
1160 | } | |
1161 | elsif ($obj->Type and $obj->Type eq $args{Type}) { | |
1162 | $args{_found}{$obj->Id} = $obj; | |
1163 | } | |
1164 | else { | |
1165 | $obj->_AllLinkedTickets( %args, _top => 0 ); | |
1166 | } | |
1167 | } | |
1168 | ||
1169 | if ($args{_top}) { | |
1170 | return map { $args{_found}{$_} } sort keys %{$args{_found}}; | |
1171 | } | |
1172 | else { | |
1173 | return 1; | |
1174 | } | |
1175 | } | |
1176 | ||
1177 | ||
1178 | ||
1179 | =head2 DependsOn | |
1180 | ||
1181 | This returns an RT::Links object which references all the tickets that this ticket depends on | |
1182 | ||
1183 | =cut | |
1184 | ||
1185 | sub DependsOn { | |
1186 | my $self = shift; | |
1187 | return ( $self->_Links( 'Base', 'DependsOn' ) ); | |
1188 | } | |
1189 | ||
1190 | ||
1191 | ||
1192 | ||
1193 | ||
1194 | ||
1195 | =head2 Links DIRECTION [TYPE] | |
1196 | ||
1197 | Return links (L<RT::Links>) to/from this object. | |
1198 | ||
1199 | DIRECTION is either 'Base' or 'Target'. | |
1200 | ||
1201 | TYPE is a type of links to return, it can be omitted to get | |
1202 | links of any type. | |
1203 | ||
1204 | =cut | |
1205 | ||
1206 | sub Links { shift->_Links(@_) } | |
1207 | ||
1208 | sub _Links { | |
1209 | my $self = shift; | |
1210 | ||
1211 | #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic --- | |
1212 | #tobias meant by $f | |
1213 | my $field = shift; | |
1214 | my $type = shift || ""; | |
1215 | ||
1216 | unless ( $self->{"$field$type"} ) { | |
1217 | $self->{"$field$type"} = RT::Links->new( $self->CurrentUser ); | |
1218 | # at least to myself | |
1219 | $self->{"$field$type"}->Limit( FIELD => $field, | |
1220 | VALUE => $self->URI, | |
1221 | ENTRYAGGREGATOR => 'OR' ); | |
1222 | $self->{"$field$type"}->Limit( FIELD => 'Type', | |
1223 | VALUE => $type ) | |
1224 | if ($type); | |
1225 | } | |
1226 | return ( $self->{"$field$type"} ); | |
1227 | } | |
1228 | ||
1229 | ||
1230 | ||
1231 | ||
1232 | =head2 FormatType | |
1233 | ||
1234 | Takes a Type and returns a string that is more human readable. | |
1235 | ||
1236 | =cut | |
1237 | ||
1238 | sub FormatType{ | |
1239 | my $self = shift; | |
1240 | my %args = ( Type => '', | |
1241 | @_ | |
1242 | ); | |
1243 | $args{Type} =~ s/([A-Z])/" " . lc $1/ge; | |
1244 | $args{Type} =~ s/^\s+//; | |
1245 | return $args{Type}; | |
1246 | } | |
1247 | ||
1248 | ||
1249 | ||
1250 | ||
1251 | =head2 FormatLink | |
1252 | ||
1253 | Takes either a Target or a Base and returns a string of human friendly text. | |
1254 | ||
1255 | =cut | |
1256 | ||
1257 | sub FormatLink { | |
1258 | my $self = shift; | |
1259 | my %args = ( Object => undef, | |
1260 | FallBack => '', | |
1261 | @_ | |
1262 | ); | |
1263 | my $text = "URI " . $args{FallBack}; | |
1264 | if ($args{Object} && $args{Object}->isa("RT::Ticket")) { | |
1265 | $text = "Ticket " . $args{Object}->id; | |
1266 | } | |
1267 | return $text; | |
1268 | } | |
1269 | ||
1270 | ||
1271 | ||
1272 | =head2 _AddLink | |
1273 | ||
1274 | Takes a paramhash of Type and one of Base or Target. Adds that link to this object. | |
1275 | ||
1276 | Returns C<link id>, C<message> and C<exist> flag. | |
1277 | ||
1278 | ||
1279 | =cut | |
1280 | ||
1281 | sub _AddLink { | |
1282 | my $self = shift; | |
1283 | my %args = ( Target => '', | |
1284 | Base => '', | |
1285 | Type => '', | |
1286 | Silent => undef, | |
1287 | @_ ); | |
1288 | ||
1289 | ||
1290 | # Remote_link is the URI of the object that is not this ticket | |
1291 | my $remote_link; | |
1292 | my $direction; | |
1293 | ||
1294 | if ( $args{'Base'} and $args{'Target'} ) { | |
1295 | $RT::Logger->debug( "$self tried to create a link. both base and target were specified" ); | |
1296 | return ( 0, $self->loc("Can't specifiy both base and target") ); | |
1297 | } | |
1298 | elsif ( $args{'Base'} ) { | |
1299 | $args{'Target'} = $self->URI(); | |
1300 | $remote_link = $args{'Base'}; | |
1301 | $direction = 'Target'; | |
1302 | } | |
1303 | elsif ( $args{'Target'} ) { | |
1304 | $args{'Base'} = $self->URI(); | |
1305 | $remote_link = $args{'Target'}; | |
1306 | $direction = 'Base'; | |
1307 | } | |
1308 | else { | |
1309 | return ( 0, $self->loc('Either base or target must be specified') ); | |
1310 | } | |
1311 | ||
1312 | # Check if the link already exists - we don't want duplicates | |
1313 | use RT::Link; | |
1314 | my $old_link = RT::Link->new( $self->CurrentUser ); | |
1315 | $old_link->LoadByParams( Base => $args{'Base'}, | |
1316 | Type => $args{'Type'}, | |
1317 | Target => $args{'Target'} ); | |
1318 | if ( $old_link->Id ) { | |
1319 | $RT::Logger->debug("$self Somebody tried to duplicate a link"); | |
1320 | return ( $old_link->id, $self->loc("Link already exists"), 1 ); | |
1321 | } | |
1322 | ||
1323 | # }}} | |
1324 | ||
1325 | ||
1326 | # Storing the link in the DB. | |
1327 | my $link = RT::Link->new( $self->CurrentUser ); | |
1328 | my ($linkid, $linkmsg) = $link->Create( Target => $args{Target}, | |
1329 | Base => $args{Base}, | |
1330 | Type => $args{Type} ); | |
1331 | ||
1332 | unless ($linkid) { | |
1333 | $RT::Logger->error("Link could not be created: ".$linkmsg); | |
1334 | return ( 0, $self->loc("Link could not be created") ); | |
1335 | } | |
1336 | ||
1337 | my $basetext = $self->FormatLink(Object => $link->BaseObj, | |
1338 | FallBack => $args{Base}); | |
1339 | my $targettext = $self->FormatLink(Object => $link->TargetObj, | |
1340 | FallBack => $args{Target}); | |
1341 | my $typetext = $self->FormatType(Type => $args{Type}); | |
1342 | my $TransString = | |
1343 | "$basetext $typetext $targettext."; | |
1344 | return ( $linkid, $TransString ) ; | |
1345 | } | |
1346 | ||
1347 | ||
1348 | ||
1349 | =head2 _DeleteLink | |
1350 | ||
1351 | Delete a link. takes a paramhash of Base, Target and Type. | |
1352 | Either Base or Target must be null. The null value will | |
1353 | be replaced with this ticket\'s id | |
1354 | ||
1355 | =cut | |
1356 | ||
1357 | sub _DeleteLink { | |
1358 | my $self = shift; | |
1359 | my %args = ( | |
1360 | Base => undef, | |
1361 | Target => undef, | |
1362 | Type => undef, | |
1363 | @_ | |
1364 | ); | |
1365 | ||
1366 | #we want one of base and target. we don't care which | |
1367 | #but we only want _one_ | |
1368 | ||
1369 | my $direction; | |
1370 | my $remote_link; | |
1371 | ||
1372 | if ( $args{'Base'} and $args{'Target'} ) { | |
1373 | $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target"); | |
1374 | return ( 0, $self->loc("Can't specifiy both base and target") ); | |
1375 | } | |
1376 | elsif ( $args{'Base'} ) { | |
1377 | $args{'Target'} = $self->URI(); | |
1378 | $remote_link = $args{'Base'}; | |
1379 | $direction = 'Target'; | |
1380 | } | |
1381 | elsif ( $args{'Target'} ) { | |
1382 | $args{'Base'} = $self->URI(); | |
1383 | $remote_link = $args{'Target'}; | |
1384 | $direction='Base'; | |
1385 | } | |
1386 | else { | |
1387 | $RT::Logger->error("Base or Target must be specified"); | |
1388 | return ( 0, $self->loc('Either base or target must be specified') ); | |
1389 | } | |
1390 | ||
1391 | my $link = RT::Link->new( $self->CurrentUser ); | |
1392 | $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} ); | |
1393 | ||
1394 | ||
1395 | $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} ); | |
1396 | #it's a real link. | |
1397 | ||
1398 | if ( $link->id ) { | |
1399 | my $basetext = $self->FormatLink(Object => $link->BaseObj, | |
1400 | FallBack => $args{Base}); | |
1401 | my $targettext = $self->FormatLink(Object => $link->TargetObj, | |
1402 | FallBack => $args{Target}); | |
1403 | my $typetext = $self->FormatType(Type => $args{Type}); | |
1404 | my $linkid = $link->id; | |
1405 | $link->Delete(); | |
1406 | my $TransString = "$basetext no longer $typetext $targettext."; | |
1407 | return ( 1, $TransString); | |
1408 | } | |
1409 | ||
1410 | #if it's not a link we can find | |
1411 | else { | |
1412 | $RT::Logger->debug("Couldn't find that link"); | |
1413 | return ( 0, $self->loc("Link not found") ); | |
1414 | } | |
1415 | } | |
1416 | ||
1417 | ||
86404187 MKG |
1418 | =head1 LockForUpdate |
1419 | ||
1420 | In a database transaction, gains an exclusive lock on the row, to | |
1421 | prevent race conditions. On SQLite, this is a "RESERVED" lock on the | |
1422 | entire database. | |
84fb5b46 | 1423 | |
86404187 | 1424 | =cut |
84fb5b46 | 1425 | |
86404187 MKG |
1426 | sub LockForUpdate { |
1427 | my $self = shift; | |
1428 | ||
1429 | my $pk = $self->_PrimaryKey; | |
1430 | my $id = @_ ? $_[0] : $self->$pk; | |
1431 | $self->_expire if $self->isa("DBIx::SearchBuilder::Record::Cachable"); | |
1432 | if (RT->Config->Get('DatabaseType') eq "SQLite") { | |
1433 | # SQLite does DB-level locking, upgrading the transaction to | |
1434 | # "RESERVED" on the first UPDATE/INSERT/DELETE. Do a no-op | |
1435 | # UPDATE to force the upgade. | |
1436 | return RT->DatabaseHandle->dbh->do( | |
1437 | "UPDATE " .$self->Table. | |
1438 | " SET $pk = $pk WHERE 1 = 0"); | |
1439 | } else { | |
1440 | return $self->_LoadFromSQL( | |
1441 | "SELECT * FROM ".$self->Table | |
1442 | ." WHERE $pk = ? FOR UPDATE", | |
1443 | $id, | |
1444 | ); | |
1445 | } | |
1446 | } | |
84fb5b46 MKG |
1447 | |
1448 | =head2 _NewTransaction PARAMHASH | |
1449 | ||
1450 | Private function to create a new RT::Transaction object for this ticket update | |
1451 | ||
1452 | =cut | |
1453 | ||
1454 | sub _NewTransaction { | |
1455 | my $self = shift; | |
1456 | my %args = ( | |
1457 | TimeTaken => undef, | |
1458 | Type => undef, | |
1459 | OldValue => undef, | |
1460 | NewValue => undef, | |
1461 | OldReference => undef, | |
1462 | NewReference => undef, | |
1463 | ReferenceType => undef, | |
1464 | Data => undef, | |
1465 | Field => undef, | |
1466 | MIMEObj => undef, | |
1467 | ActivateScrips => 1, | |
1468 | CommitScrips => 1, | |
1469 | SquelchMailTo => undef, | |
1470 | @_ | |
1471 | ); | |
1472 | ||
86404187 MKG |
1473 | my $in_txn = RT->DatabaseHandle->TransactionDepth; |
1474 | RT->DatabaseHandle->BeginTransaction unless $in_txn; | |
1475 | ||
1476 | $self->LockForUpdate; | |
1477 | ||
84fb5b46 MKG |
1478 | my $old_ref = $args{'OldReference'}; |
1479 | my $new_ref = $args{'NewReference'}; | |
1480 | my $ref_type = $args{'ReferenceType'}; | |
1481 | if ($old_ref or $new_ref) { | |
1482 | $ref_type ||= ref($old_ref) || ref($new_ref); | |
1483 | if (!$ref_type) { | |
1484 | $RT::Logger->error("Reference type not specified for transaction"); | |
1485 | return; | |
1486 | } | |
1487 | $old_ref = $old_ref->Id if ref($old_ref); | |
1488 | $new_ref = $new_ref->Id if ref($new_ref); | |
1489 | } | |
1490 | ||
1491 | require RT::Transaction; | |
1492 | my $trans = RT::Transaction->new( $self->CurrentUser ); | |
1493 | my ( $transaction, $msg ) = $trans->Create( | |
1494 | ObjectId => $self->Id, | |
1495 | ObjectType => ref($self), | |
1496 | TimeTaken => $args{'TimeTaken'}, | |
1497 | Type => $args{'Type'}, | |
1498 | Data => $args{'Data'}, | |
1499 | Field => $args{'Field'}, | |
1500 | NewValue => $args{'NewValue'}, | |
1501 | OldValue => $args{'OldValue'}, | |
1502 | NewReference => $new_ref, | |
1503 | OldReference => $old_ref, | |
1504 | ReferenceType => $ref_type, | |
1505 | MIMEObj => $args{'MIMEObj'}, | |
1506 | ActivateScrips => $args{'ActivateScrips'}, | |
1507 | CommitScrips => $args{'CommitScrips'}, | |
1508 | SquelchMailTo => $args{'SquelchMailTo'}, | |
1509 | ); | |
1510 | ||
1511 | # Rationalize the object since we may have done things to it during the caching. | |
1512 | $self->Load($self->Id); | |
1513 | ||
1514 | $RT::Logger->warning($msg) unless $transaction; | |
1515 | ||
1516 | $self->_SetLastUpdated; | |
1517 | ||
1518 | if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) { | |
1519 | $self->_UpdateTimeTaken( $args{'TimeTaken'} ); | |
1520 | } | |
1521 | if ( RT->Config->Get('UseTransactionBatch') and $transaction ) { | |
1522 | push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'}; | |
1523 | } | |
86404187 MKG |
1524 | |
1525 | RT->DatabaseHandle->Commit unless $in_txn; | |
1526 | ||
84fb5b46 MKG |
1527 | return ( $transaction, $msg, $trans ); |
1528 | } | |
1529 | ||
1530 | ||
1531 | ||
1532 | =head2 Transactions | |
1533 | ||
1534 | Returns an RT::Transactions object of all transactions on this record object | |
1535 | ||
1536 | =cut | |
1537 | ||
1538 | sub Transactions { | |
1539 | my $self = shift; | |
1540 | ||
1541 | use RT::Transactions; | |
1542 | my $transactions = RT::Transactions->new( $self->CurrentUser ); | |
1543 | ||
1544 | #If the user has no rights, return an empty object | |
1545 | $transactions->Limit( | |
1546 | FIELD => 'ObjectId', | |
1547 | VALUE => $self->id, | |
1548 | ); | |
1549 | $transactions->Limit( | |
1550 | FIELD => 'ObjectType', | |
1551 | VALUE => ref($self), | |
1552 | ); | |
1553 | ||
1554 | return ($transactions); | |
1555 | } | |
1556 | ||
1557 | # | |
1558 | ||
1559 | sub CustomFields { | |
1560 | my $self = shift; | |
1561 | my $cfs = RT::CustomFields->new( $self->CurrentUser ); | |
1562 | ||
1563 | $cfs->SetContextObject( $self ); | |
1564 | # XXX handle multiple types properly | |
1565 | $cfs->LimitToLookupType( $self->CustomFieldLookupType ); | |
1566 | $cfs->LimitToGlobalOrObjectId( | |
1567 | $self->_LookupId( $self->CustomFieldLookupType ) | |
1568 | ); | |
1569 | $cfs->ApplySortOrder; | |
1570 | ||
1571 | return $cfs; | |
1572 | } | |
1573 | ||
1574 | # TODO: This _only_ works for RT::Class classes. it doesn't work, for example, | |
1575 | # for RT::IR classes. | |
1576 | ||
1577 | sub _LookupId { | |
1578 | my $self = shift; | |
1579 | my $lookup = shift; | |
1580 | my @classes = ($lookup =~ /RT::(\w+)-/g); | |
1581 | ||
1582 | my $object = $self; | |
1583 | foreach my $class (reverse @classes) { | |
1584 | my $method = "${class}Obj"; | |
1585 | $object = $object->$method; | |
1586 | } | |
1587 | ||
1588 | return $object->Id; | |
1589 | } | |
1590 | ||
1591 | ||
1592 | =head2 CustomFieldLookupType | |
1593 | ||
1594 | Returns the path RT uses to figure out which custom fields apply to this object. | |
1595 | ||
1596 | =cut | |
1597 | ||
1598 | sub CustomFieldLookupType { | |
1599 | my $self = shift; | |
1600 | return ref($self); | |
1601 | } | |
1602 | ||
1603 | ||
1604 | =head2 AddCustomFieldValue { Field => FIELD, Value => VALUE } | |
1605 | ||
1606 | VALUE should be a string. FIELD can be any identifier of a CustomField | |
1607 | supported by L</LoadCustomFieldByIdentifier> method. | |
1608 | ||
1609 | Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field, | |
1610 | deletes the old value. | |
1611 | If VALUE is not a valid value for the custom field, returns | |
1612 | (0, 'Error message' ) otherwise, returns ($id, 'Success Message') where | |
1613 | $id is ID of created L<ObjectCustomFieldValue> object. | |
1614 | ||
1615 | =cut | |
1616 | ||
1617 | sub AddCustomFieldValue { | |
1618 | my $self = shift; | |
1619 | $self->_AddCustomFieldValue(@_); | |
1620 | } | |
1621 | ||
1622 | sub _AddCustomFieldValue { | |
1623 | my $self = shift; | |
1624 | my %args = ( | |
1625 | Field => undef, | |
1626 | Value => undef, | |
1627 | LargeContent => undef, | |
1628 | ContentType => undef, | |
1629 | RecordTransaction => 1, | |
1630 | @_ | |
1631 | ); | |
1632 | ||
1633 | my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'}); | |
1634 | unless ( $cf->Id ) { | |
1635 | return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) ); | |
1636 | } | |
1637 | ||
1638 | my $OCFs = $self->CustomFields; | |
1639 | $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id ); | |
1640 | unless ( $OCFs->Count ) { | |
1641 | return ( | |
1642 | 0, | |
1643 | $self->loc( | |
1644 | "Custom field [_1] does not apply to this object", | |
86404187 | 1645 | ref $args{'Field'} ? $args{'Field'}->id : $args{'Field'} |
84fb5b46 MKG |
1646 | ) |
1647 | ); | |
1648 | } | |
1649 | ||
1650 | # empty string is not correct value of any CF, so undef it | |
1651 | foreach ( qw(Value LargeContent) ) { | |
1652 | $args{ $_ } = undef if defined $args{ $_ } && !length $args{ $_ }; | |
1653 | } | |
1654 | ||
1655 | unless ( $cf->ValidateValue( $args{'Value'} ) ) { | |
1656 | return ( 0, $self->loc("Invalid value for custom field") ); | |
1657 | } | |
1658 | ||
1659 | # If the custom field only accepts a certain # of values, delete the existing | |
1660 | # value and record a "changed from foo to bar" transaction | |
1661 | unless ( $cf->UnlimitedValues ) { | |
1662 | ||
1663 | # Load up a ObjectCustomFieldValues object for this custom field and this ticket | |
1664 | my $values = $cf->ValuesForObject($self); | |
1665 | ||
1666 | # We need to whack any old values here. In most cases, the custom field should | |
1667 | # only have one value to delete. In the pathalogical case, this custom field | |
1668 | # used to be a multiple and we have many values to whack.... | |
1669 | my $cf_values = $values->Count; | |
1670 | ||
1671 | if ( $cf_values > $cf->MaxValues ) { | |
1672 | my $i = 0; #We want to delete all but the max we can currently have , so we can then | |
1673 | # execute the same code to "change" the value from old to new | |
1674 | while ( my $value = $values->Next ) { | |
1675 | $i++; | |
1676 | if ( $i < $cf_values ) { | |
1677 | my ( $val, $msg ) = $cf->DeleteValueForObject( | |
1678 | Object => $self, | |
1679 | Content => $value->Content | |
1680 | ); | |
1681 | unless ($val) { | |
1682 | return ( 0, $msg ); | |
1683 | } | |
1684 | my ( $TransactionId, $Msg, $TransactionObj ) = | |
1685 | $self->_NewTransaction( | |
1686 | Type => 'CustomField', | |
1687 | Field => $cf->Id, | |
1688 | OldReference => $value, | |
1689 | ); | |
1690 | } | |
1691 | } | |
1692 | $values->RedoSearch if $i; # redo search if have deleted at least one value | |
1693 | } | |
1694 | ||
1695 | my ( $old_value, $old_content ); | |
1696 | if ( $old_value = $values->First ) { | |
1697 | $old_content = $old_value->Content; | |
1698 | $old_content = undef if defined $old_content && !length $old_content; | |
1699 | ||
1700 | my $is_the_same = 1; | |
1701 | if ( defined $args{'Value'} ) { | |
1702 | $is_the_same = 0 unless defined $old_content | |
1703 | && lc $old_content eq lc $args{'Value'}; | |
1704 | } else { | |
1705 | $is_the_same = 0 if defined $old_content; | |
1706 | } | |
1707 | if ( $is_the_same ) { | |
1708 | my $old_content = $old_value->LargeContent; | |
1709 | if ( defined $args{'LargeContent'} ) { | |
1710 | $is_the_same = 0 unless defined $old_content | |
1711 | && $old_content eq $args{'LargeContent'}; | |
1712 | } else { | |
1713 | $is_the_same = 0 if defined $old_content; | |
1714 | } | |
1715 | } | |
1716 | ||
1717 | return $old_value->id if $is_the_same; | |
1718 | } | |
1719 | ||
1720 | my ( $new_value_id, $value_msg ) = $cf->AddValueForObject( | |
1721 | Object => $self, | |
1722 | Content => $args{'Value'}, | |
1723 | LargeContent => $args{'LargeContent'}, | |
1724 | ContentType => $args{'ContentType'}, | |
1725 | ); | |
1726 | ||
1727 | unless ( $new_value_id ) { | |
1728 | return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg ) ); | |
1729 | } | |
1730 | ||
1731 | my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser ); | |
1732 | $new_value->Load( $new_value_id ); | |
1733 | ||
1734 | # now that adding the new value was successful, delete the old one | |
1735 | if ( $old_value ) { | |
1736 | my ( $val, $msg ) = $old_value->Delete(); | |
1737 | return ( 0, $msg ) unless $val; | |
1738 | } | |
1739 | ||
1740 | if ( $args{'RecordTransaction'} ) { | |
1741 | my ( $TransactionId, $Msg, $TransactionObj ) = | |
1742 | $self->_NewTransaction( | |
1743 | Type => 'CustomField', | |
1744 | Field => $cf->Id, | |
1745 | OldReference => $old_value, | |
1746 | NewReference => $new_value, | |
1747 | ); | |
1748 | } | |
1749 | ||
1750 | my $new_content = $new_value->Content; | |
1751 | ||
1752 | # For datetime, we need to display them in "human" format in result message | |
1753 | #XXX TODO how about date without time? | |
1754 | if ($cf->Type eq 'DateTime') { | |
1755 | my $DateObj = RT::Date->new( $self->CurrentUser ); | |
1756 | $DateObj->Set( | |
1757 | Format => 'ISO', | |
1758 | Value => $new_content, | |
1759 | ); | |
1760 | $new_content = $DateObj->AsString; | |
1761 | ||
1762 | if ( defined $old_content && length $old_content ) { | |
1763 | $DateObj->Set( | |
1764 | Format => 'ISO', | |
1765 | Value => $old_content, | |
1766 | ); | |
1767 | $old_content = $DateObj->AsString; | |
1768 | } | |
1769 | } | |
1770 | ||
1771 | unless ( defined $old_content && length $old_content ) { | |
1772 | return ( $new_value_id, $self->loc( "[_1] [_2] added", $cf->Name, $new_content )); | |
1773 | } | |
1774 | elsif ( !defined $new_content || !length $new_content ) { | |
1775 | return ( $new_value_id, | |
1776 | $self->loc( "[_1] [_2] deleted", $cf->Name, $old_content ) ); | |
1777 | } | |
1778 | else { | |
1779 | return ( $new_value_id, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content, $new_content)); | |
1780 | } | |
1781 | ||
1782 | } | |
1783 | ||
1784 | # otherwise, just add a new value and record "new value added" | |
1785 | else { | |
1786 | my ($new_value_id, $msg) = $cf->AddValueForObject( | |
1787 | Object => $self, | |
1788 | Content => $args{'Value'}, | |
1789 | LargeContent => $args{'LargeContent'}, | |
1790 | ContentType => $args{'ContentType'}, | |
1791 | ); | |
1792 | ||
1793 | unless ( $new_value_id ) { | |
1794 | return ( 0, $self->loc( "Could not add new custom field value: [_1]", $msg ) ); | |
1795 | } | |
1796 | if ( $args{'RecordTransaction'} ) { | |
1797 | my ( $tid, $msg ) = $self->_NewTransaction( | |
1798 | Type => 'CustomField', | |
1799 | Field => $cf->Id, | |
1800 | NewReference => $new_value_id, | |
1801 | ReferenceType => 'RT::ObjectCustomFieldValue', | |
1802 | ); | |
1803 | unless ( $tid ) { | |
1804 | return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $msg ) ); | |
1805 | } | |
1806 | } | |
1807 | return ( $new_value_id, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name ) ); | |
1808 | } | |
1809 | } | |
1810 | ||
1811 | ||
1812 | ||
1813 | =head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE } | |
1814 | ||
1815 | Deletes VALUE as a value of CustomField FIELD. | |
1816 | ||
1817 | VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue. | |
1818 | ||
1819 | If VALUE is not a valid value for the custom field, returns | |
1820 | (0, 'Error message' ) otherwise, returns (1, 'Success Message') | |
1821 | ||
1822 | =cut | |
1823 | ||
1824 | sub DeleteCustomFieldValue { | |
1825 | my $self = shift; | |
1826 | my %args = ( | |
1827 | Field => undef, | |
1828 | Value => undef, | |
1829 | ValueId => undef, | |
1830 | @_ | |
1831 | ); | |
1832 | ||
1833 | my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'}); | |
1834 | unless ( $cf->Id ) { | |
1835 | return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) ); | |
1836 | } | |
1837 | ||
1838 | my ( $val, $msg ) = $cf->DeleteValueForObject( | |
1839 | Object => $self, | |
1840 | Id => $args{'ValueId'}, | |
1841 | Content => $args{'Value'}, | |
1842 | ); | |
1843 | unless ($val) { | |
1844 | return ( 0, $msg ); | |
1845 | } | |
1846 | ||
1847 | my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction( | |
1848 | Type => 'CustomField', | |
1849 | Field => $cf->Id, | |
1850 | OldReference => $val, | |
1851 | ReferenceType => 'RT::ObjectCustomFieldValue', | |
1852 | ); | |
1853 | unless ($TransactionId) { | |
1854 | return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) ); | |
1855 | } | |
1856 | ||
1857 | my $old_value = $TransactionObj->OldValue; | |
1858 | # For datetime, we need to display them in "human" format in result message | |
1859 | if ( $cf->Type eq 'DateTime' ) { | |
1860 | my $DateObj = RT::Date->new( $self->CurrentUser ); | |
1861 | $DateObj->Set( | |
1862 | Format => 'ISO', | |
1863 | Value => $old_value, | |
1864 | ); | |
1865 | $old_value = $DateObj->AsString; | |
1866 | } | |
1867 | return ( | |
1868 | $TransactionId, | |
1869 | $self->loc( | |
1870 | "[_1] is no longer a value for custom field [_2]", | |
1871 | $old_value, $cf->Name | |
1872 | ) | |
1873 | ); | |
1874 | } | |
1875 | ||
1876 | ||
1877 | ||
1878 | =head2 FirstCustomFieldValue FIELD | |
1879 | ||
1880 | Return the content of the first value of CustomField FIELD for this ticket | |
1881 | Takes a field id or name | |
1882 | ||
1883 | =cut | |
1884 | ||
1885 | sub FirstCustomFieldValue { | |
1886 | my $self = shift; | |
1887 | my $field = shift; | |
1888 | ||
1889 | my $values = $self->CustomFieldValues( $field ); | |
1890 | return undef unless my $first = $values->First; | |
1891 | return $first->Content; | |
1892 | } | |
1893 | ||
1894 | =head2 CustomFieldValuesAsString FIELD | |
1895 | ||
1896 | Return the content of the CustomField FIELD for this ticket. | |
1897 | If this is a multi-value custom field, values will be joined with newlines. | |
1898 | ||
1899 | Takes a field id or name as the first argument | |
1900 | ||
1901 | Takes an optional Separator => "," second and third argument | |
1902 | if you want to join the values using something other than a newline | |
1903 | ||
1904 | =cut | |
1905 | ||
1906 | sub CustomFieldValuesAsString { | |
1907 | my $self = shift; | |
1908 | my $field = shift; | |
1909 | my %args = @_; | |
1910 | my $separator = $args{Separator} || "\n"; | |
1911 | ||
1912 | my $values = $self->CustomFieldValues( $field ); | |
1913 | return join ($separator, grep { defined $_ } | |
1914 | map { $_->Content } @{$values->ItemsArrayRef}); | |
1915 | } | |
1916 | ||
1917 | ||
1918 | ||
1919 | =head2 CustomFieldValues FIELD | |
1920 | ||
1921 | Return a ObjectCustomFieldValues object of all values of the CustomField whose | |
1922 | id or Name is FIELD for this record. | |
1923 | ||
1924 | Returns an RT::ObjectCustomFieldValues object | |
1925 | ||
1926 | =cut | |
1927 | ||
1928 | sub CustomFieldValues { | |
1929 | my $self = shift; | |
1930 | my $field = shift; | |
1931 | ||
1932 | if ( $field ) { | |
1933 | my $cf = $self->LoadCustomFieldByIdentifier( $field ); | |
1934 | ||
1935 | # we were asked to search on a custom field we couldn't find | |
1936 | unless ( $cf->id ) { | |
1937 | $RT::Logger->warning("Couldn't load custom field by '$field' identifier"); | |
1938 | return RT::ObjectCustomFieldValues->new( $self->CurrentUser ); | |
1939 | } | |
1940 | return ( $cf->ValuesForObject($self) ); | |
1941 | } | |
1942 | ||
1943 | # we're not limiting to a specific custom field; | |
1944 | my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser ); | |
1945 | $ocfs->LimitToObject( $self ); | |
1946 | return $ocfs; | |
1947 | } | |
1948 | ||
1949 | =head2 LoadCustomFieldByIdentifier IDENTIFER | |
1950 | ||
1951 | Find the custom field has id or name IDENTIFIER for this object. | |
1952 | ||
1953 | If no valid field is found, returns an empty RT::CustomField object. | |
1954 | ||
1955 | =cut | |
1956 | ||
1957 | sub LoadCustomFieldByIdentifier { | |
1958 | my $self = shift; | |
1959 | my $field = shift; | |
1960 | ||
1961 | my $cf; | |
1962 | if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) { | |
1963 | $cf = RT::CustomField->new($self->CurrentUser); | |
1964 | $cf->SetContextObject( $self ); | |
1965 | $cf->LoadById( $field->id ); | |
1966 | } | |
1967 | elsif ($field =~ /^\d+$/) { | |
1968 | $cf = RT::CustomField->new($self->CurrentUser); | |
1969 | $cf->SetContextObject( $self ); | |
1970 | $cf->LoadById($field); | |
1971 | } else { | |
1972 | ||
1973 | my $cfs = $self->CustomFields($self->CurrentUser); | |
1974 | $cfs->SetContextObject( $self ); | |
1975 | $cfs->Limit(FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0); | |
1976 | $cf = $cfs->First || RT::CustomField->new($self->CurrentUser); | |
1977 | } | |
1978 | return $cf; | |
1979 | } | |
1980 | ||
1981 | sub ACLEquivalenceObjects { } | |
1982 | ||
1983 | sub BasicColumns { } | |
1984 | ||
1985 | sub WikiBase { | |
1986 | return RT->Config->Get('WebPath'). "/index.html?q="; | |
1987 | } | |
1988 | ||
1989 | RT::Base->_ImportOverlays(); | |
1990 | ||
1991 | 1; |