Upgrade to 4.2.2
[usit-rt.git] / lib / RT / Record / AddAndSort.pm
CommitLineData
af59614d
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
320f0092 5# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
af59614d
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
49use strict;
50use warnings;
51
52package RT::Record::AddAndSort;
53use base 'RT::Record';
54
55=head1 NAME
56
57RT::Record::AddAndSort - base class for records that can be added and sorted
58
59=head1 DESCRIPTION
60
61Base class for L<RT::ObjectCustomField> and L<RT::ObjectScrip> that unifies
62application of L<RT::CustomField>s and L<RT::Scrip>s to various objects. Also,
63deals with order of the records.
64
65=head1 METHODS
66
67=head2 Meta information
68
69=head3 CollectionClass
70
71Returns class representing collection for this record class. Basicly adds 's'
72at the end. Should be overriden if default doesn't work.
73
74For example returns L<RT::ObjectCustomFields> when called on L<RT::ObjectCustomField>.
75
76=cut
77
78sub CollectionClass {
79 return (ref($_[0]) || $_[0]).'s';
80}
81
82=head3 TargetField
83
84Returns name of the field in the table where id of object we add is stored.
85By default deletes everything up to '::Object' from class name.
86This method allows to use friendlier argument names and methods.
87
88For example returns 'Scrip' for L<RT::ObjectScrip>.
89
90=cut
91
92sub TargetField {
93 my $class = ref($_[0]) || $_[0];
94 $class =~ s/.*::Object// or return undef;
95 return $class;
96}
97
98=head3 ObjectCollectionClass
99
100Takes an object under L</TargetField> name and should return class
101name representing collection the object can be added to.
102
103Must be overriden by sub classes.
104
105
106See L<RT::ObjectScrip/ObjectCollectionClass> and L<RT::ObjectCustomField/CollectionClass>.
107
108=cut
109
110sub ObjectCollectionClass { die "should be subclassed" }
111
112=head2 Manipulation
113
114=head3 Create
115
116Takes 'ObjectId' with id of an object we can be added to, object we can
117add to under L</TargetField> name, Disabled and SortOrder.
118
119This method doesn't create duplicates. If record already exists then it's not created, but
120loaded instead. Note that nothing is updated if record exist.
121
122If SortOrder is not defined then it's calculated to place new record last. If it's
123provided then it's caller's duty to make sure it is correct value.
124
125Example:
126
127 my $ocf = RT::ObjectCustomField->new( RT->SystemUser );
128 my ($id, $msg) = $ocf->Create( CustomField => 1, ObjectId => 0 );
129
130See L</Add> which has more error checks. Also, L<RT::Scrip> and L<RT::CustomField>
131have more appropriate methods that B<should be> prefered over calling this directly.
132
133=cut
134
135sub Create {
136 my $self = shift;
137 my %args = (
138 ObjectId => 0,
139 SortOrder => undef,
140 @_
141 );
142
143 my $tfield = $self->TargetField;
144
145 my $target = $self->TargetObj( $args{ $tfield } );
146 unless ( $target->id ) {
147 $RT::Logger->error("Couldn't load ". ref($target) ." '$args{$tfield}'");
148 return 0;
149 }
150
151 my $exist = $self->new($self->CurrentUser);
152 $exist->LoadByCols( ObjectId => $args{'ObjectId'}, $tfield => $target->id );
153 if ( $exist->id ) {
154 $self->Load( $exist->id );
155 return $self->id;
156 }
157
158 unless ( defined $args{'SortOrder'} ) {
159 $args{'SortOrder'} = $self->NextSortOrder(
160 %args,
161 $tfield => $target,
162 );
163 }
164
165 return $self->SUPER::Create(
166 %args,
167 $tfield => $target->id,
168 );
169}
170
171=head3 Add
172
173Helper method that wraps L</Create> and does more checks to make sure
174result is consistent. Doesn't allow adding a record to an object if the
175record is already global. Removes record from particular objects when
176asked to add the record globally.
177
178=cut
179
180sub Add {
181 my $self = shift;
182 my %args = (@_);
183
184 my $field = $self->TargetField;
185
186 my $tid = $args{ $field };
187 $tid = $tid->id if ref $tid;
188 $tid ||= $self->TargetObj->id;
189
190 my $oid = $args{'ObjectId'};
191 $oid = $oid->id if ref $oid;
192 $oid ||= 0;
193
194 if ( $self->IsAdded( $tid => $oid ) ) {
195 return ( 0, $self->loc("Is already added to the object") );
196 }
197
198 if ( $oid ) {
199 # adding locally
200 return (0, $self->loc("Couldn't add as it's global already") )
201 if $self->IsAdded( $tid => 0 );
202 }
203 else {
204 $self->DeleteAll( $field => $tid );
205 }
206
207 return $self->Create(
208 %args, $field => $tid, ObjectId => $oid,
209 );
210}
211
212sub IsAdded {
213 my $self = shift;
214 my ($tid, $oid) = @_;
215 my $record = $self->new( $self->CurrentUser );
216 $record->LoadByCols( $self->TargetField => $tid, ObjectId => $oid );
217 return $record->id;
218}
219
220=head3 AddedTo
221
222Returns collection with objects target of this record is added to.
223Class of the collection depends on L</ObjectCollectionClass>.
224See all L</NotAddedTo>.
225
226For example returns L<RT::Queues> collection if the target is L<RT::Scrip>.
227
228Returns empty collection if target is added globally.
229
230=cut
231
232sub AddedTo {
233 my $self = shift;
234
235 my ($res, $alias) = $self->_AddedTo( @_ );
236 return $res unless $res;
237
238 $res->Limit(
239 ALIAS => $alias,
240 FIELD => 'id',
241 OPERATOR => 'IS NOT',
242 VALUE => 'NULL',
243 );
244
245 return $res;
246}
247
248=head3 NotAddedTo
249
250Returns collection with objects target of this record is not added to.
251Class of the collection depends on L</ObjectCollectionClass>.
252See all L</AddedTo>.
253
254Returns empty collection if target is added globally.
255
256=cut
257
258sub NotAddedTo {
259 my $self = shift;
260
261 my ($res, $alias) = $self->_AddedTo( @_ );
262 return $res unless $res;
263
264 $res->Limit(
265 ALIAS => $alias,
266 FIELD => 'id',
267 OPERATOR => 'IS',
268 VALUE => 'NULL',
269 );
270
271 return $res;
272}
273
274sub _AddedTo {
275 my $self = shift;
276 my %args = (@_);
277
278 my $field = $self->TargetField;
279 my $target = $args{ $field } || $self->TargetObj;
280
281 my ($class) = $self->ObjectCollectionClass( $field => $target );
282 return undef unless $class;
283
284 my $res = $class->new( $self->CurrentUser );
285
286 # If target added to a Group, only display user-defined groups
287 $res->LimitToUserDefinedGroups if $class eq 'RT::Groups';
288
289 $res->OrderBy( FIELD => 'Name' );
290 my $alias = $res->Join(
291 TYPE => 'LEFT',
292 ALIAS1 => 'main',
293 FIELD1 => 'id',
294 TABLE2 => $self->Table,
295 FIELD2 => 'ObjectId',
296 );
297 $res->Limit(
298 LEFTJOIN => $alias,
299 ALIAS => $alias,
300 FIELD => $field,
301 VALUE => $target->id,
302 );
303 return ($res, $alias);
304}
305
306=head3 Delete
307
308Deletes this record.
309
310=cut
311
312sub Delete {
313 my $self = shift;
314
315 return $self->SUPER::Delete if $self->IsSortOrderShared;
316
317 # Move everything below us up
318 my $siblings = $self->Neighbors;
319 $siblings->Limit( FIELD => 'SortOrder', OPERATOR => '>=', VALUE => $self->SortOrder );
320 $siblings->OrderBy( FIELD => 'SortOrder', ORDER => 'ASC' );
321 foreach my $record ( @{ $siblings->ItemsArrayRef } ) {
322 $record->SetSortOrder($record->SortOrder - 1);
323 }
324
325 return $self->SUPER::Delete;
326}
327
328=head3 DeleteAll
329
330Helper method to delete all applications for one target (Scrip, CustomField, ...).
331Target can be provided in arguments. If it's not then L</TargetObj> is used.
332
333 $object_scrip->DeleteAll;
334
335 $object_scrip->DeleteAll( Scrip => $scrip );
336
337=cut
338
339sub DeleteAll {
340 my $self = shift;
341 my %args = (@_);
342
343 my $field = $self->TargetField;
344
345 my $id = $args{ $field };
346 $id = $id->id if ref $id;
347 $id ||= $self->TargetObj->id;
348
349 my $list = $self->CollectionClass->new( $self->CurrentUser );
350 $list->Limit( FIELD => $field, VALUE => $id );
351 $_->Delete foreach @{ $list->ItemsArrayRef };
352}
353
354=head3 MoveUp
355
356Moves record up.
357
358=cut
359
360sub MoveUp { return shift->Move( Up => @_ ) }
361
362=head3 MoveDown
363
364Moves record down.
365
366=cut
367
368sub MoveDown { return shift->Move( Down => @_ ) }
369
370=head3 Move
371
372Takes 'up' or 'down'. One method that implements L</MoveUp> and L</MoveDown>.
373
374=cut
375
376sub Move {
377 my $self = shift;
378 my $dir = lc(shift || 'up');
379
380 my %meta;
381 if ( $dir eq 'down' ) {
382 %meta = qw(
383 next_op >
384 next_order ASC
385 prev_op <=
386 diff +1
387 );
388 } else {
389 %meta = qw(
390 next_op <
391 next_order DESC
392 prev_op >=
393 diff -1
394 );
395 }
396
397 my $siblings = $self->Siblings;
398 $siblings->Limit( FIELD => 'SortOrder', OPERATOR => $meta{'next_op'}, VALUE => $self->SortOrder );
399 $siblings->OrderBy( FIELD => 'SortOrder', ORDER => $meta{'next_order'} );
400
401 my @next = ($siblings->Next, $siblings->Next);
402 unless ($next[0]) {
403 return $dir eq 'down'
404 ? (0, "Can not move down. It's already at the bottom")
405 : (0, "Can not move up. It's already at the top")
406 ;
407 }
408
409 my ($new_sort_order, $move);
410
411 unless ( $self->ObjectId ) {
412 # moving global, it can not share sort order, so just move it
413 # on place of next global and move everything in between one number
414
415 $new_sort_order = $next[0]->SortOrder;
416 $move = $self->Neighbors;
417 $move->Limit(
418 FIELD => 'SortOrder', OPERATOR => $meta{'next_op'}, VALUE => $self->SortOrder,
419 );
420 $move->Limit(
421 FIELD => 'SortOrder', OPERATOR => $meta{'prev_op'}, VALUE => $next[0]->SortOrder,
422 ENTRYAGGREGATOR => 'AND',
423 );
424 }
425 elsif ( $next[0]->ObjectId == $self->ObjectId ) {
426 # moving two locals, just swap them, they should follow 'so = so+/-1' rule
427 $new_sort_order = $next[0]->SortOrder;
428 $move = $next[0];
429 }
430 else {
431 # moving local behind global
432 unless ( $self->IsSortOrderShared ) {
433 # not shared SO allows us to swap
434 $new_sort_order = $next[0]->SortOrder;
435 $move = $next[0];
436 }
437 elsif ( $next[1] ) {
438 # more records there and shared SO, we have to move everything
439 $new_sort_order = $next[0]->SortOrder;
440 $move = $self->Neighbors;
441 $move->Limit(
442 FIELD => 'SortOrder', OPERATOR => $meta{prev_op}, VALUE => $next[0]->SortOrder,
443 );
444 }
445 else {
446 # shared SO and place after is free, so just jump
447 $new_sort_order = $next[0]->SortOrder + $meta{'diff'};
448 }
449 }
450
451 if ( $move ) {
452 foreach my $record ( $move->isa('RT::Record')? ($move) : @{ $move->ItemsArrayRef } ) {
453 my ($status, $msg) = $record->SetSortOrder(
454 $record->SortOrder - $meta{'diff'}
455 );
456 return (0, "Couldn't move: $msg") unless $status;
457 }
458 }
459
460 my ($status, $msg) = $self->SetSortOrder( $new_sort_order );
461 unless ( $status ) {
462 return (0, "Couldn't move: $msg");
463 }
464
465 return (1,"Moved");
466}
467
468=head2 Accessors, instrospection and traversing.
469
470=head3 TargetObj
471
472Returns target object of this record. Returns L<RT::Scrip> object for
473L<RT::ObjectScrip>.
474
475=cut
476
477sub TargetObj {
478 my $self = shift;
479 my $id = shift;
480
481 my $method = $self->TargetField .'Obj';
482 return $self->$method( $id );
483}
484
485=head3 NextSortOrder
486
487Returns next available SortOrder value in the L<neighborhood|/Neighbors>.
488Pass arguments to L</Neighbors> and can take optional ObjectId argument,
489calls ObjectId if it's not provided.
490
491=cut
492
493sub NextSortOrder {
494 my $self = shift;
495 my %args = (@_);
496
497 my $oid = $args{'ObjectId'};
498 $oid = $self->ObjectId unless defined $oid;
499 $oid ||= 0;
500
501 my $neighbors = $self->Neighbors( %args );
502 if ( $oid ) {
503 $neighbors->LimitToObjectId( $oid );
504 $neighbors->LimitToObjectId( 0 );
505 } elsif ( !$neighbors->_isLimited ) {
506 $neighbors->UnLimit;
507 }
508 $neighbors->OrderBy( FIELD => 'SortOrder', ORDER => 'DESC' );
509 return 0 unless my $first = $neighbors->First;
510 return $first->SortOrder + 1;
511}
512
513=head3 IsSortOrderShared
514
515Returns true if this record shares SortOrder value with a L<neighbor|/Neighbors>.
516
517=cut
518
519sub IsSortOrderShared {
520 my $self = shift;
521 return 0 unless $self->ObjectId;
522
523 my $neighbors = $self->Neighbors;
524 $neighbors->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->id );
525 $neighbors->Limit( FIELD => 'SortOrder', VALUE => $self->SortOrder );
526 return $neighbors->Count;
527}
528
529=head2 Neighbors and Siblings
530
531These two methods should only be understood by developers who wants
532to implement new classes of records that can be added to other records
533and sorted.
534
535Main purpose is to maintain SortOrder values.
536
537Let's take a look at custom fields. A custom field can be created for tickets,
538queues, transactions, users... Custom fields created for tickets can
539be added globally or to particular set of queues. Custom fields for
540tickets are neighbors. Neighbor custom fields added to the same objects
541are siblings. Custom fields added globally are sibling to all neighbors.
542
543For scrips Stage defines neighborhood.
544
545Let's look at the three scrips in create stage S1, S2 and S3, queues Q1 and Q2 and
546G for global.
547
548 S1@Q1, S3@Q2 0
549 S2@G 1
550 S1@Q2 2
551
552Above table says that S2 is added globally, S1 is added to Q1 and executed
553before S2 in this queue, also S1 is added to Q1, but exectued after S2 in this
554queue, S3 is only added to Q2 and executed before S2 and S1.
555
556Siblings are scrips added to an object including globally added or only
557globally added. In our example there are three different collection
558of siblings: (S2) - global, (S1, S2) for Q1, (S3, S2, S1) for Q2.
559
560Sort order can be shared between neighbors, but can not be shared between siblings.
561
562Here is what happens with sort order if we move S1@Q2 one position up:
563
564 S3@Q2 0
565 S1@Q1, S1@Q2 1
566 S2@G 2
567
568One position more:
569
570 S1@Q2 0
571 S1@Q1, S3@Q2 1
572 S2@G 2
573
574Hopefuly it's enough to understand how it works.
575
576Targets from different neighborhood can not be sorted against each other.
577
578=head3 Neighbors
579
580Returns collection of records of this class with all
581neighbors. By default all possible targets are neighbors.
582
583Takes the same arguments as L</Create> method. If arguments are not passed
584then uses the current record.
585
586See L</Neighbors and Siblings> for detailed description.
587
588See L<RT::ObjectCustomField/Neighbors> for example.
589
590=cut
591
592sub Neighbors {
593 my $self = shift;
594 return $self->CollectionClass->new( $self->CurrentUser );
595}
596
597=head3 Siblings
598
599Returns collection of records of this class with siblings.
600
601Takes the same arguments as L</Neighbors>. Siblings is subset of L</Neighbors>.
602
603=cut
604
605sub Siblings {
606 my $self = shift;
607 my %args = @_;
608
609 my $oid = $args{'ObjectId'};
610 $oid = $self->ObjectId unless defined $oid;
611 $oid ||= 0;
612
613 my $res = $self->Neighbors( %args );
614 $res->LimitToObjectId( $oid );
615 $res->LimitToObjectId( 0 ) if $oid;
616 return $res;
617}
618
619RT::Base->_ImportOverlays();
620
6211;