Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / Class.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2015 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 package RT::Class;
50
51 use strict;
52 use warnings;
53 use base 'RT::Record';
54
55
56 use RT::System;
57 use RT::CustomFields;
58 use RT::ACL;
59 use RT::Articles;
60 use RT::ObjectClass;
61 use RT::ObjectClasses;
62
63 use Role::Basic 'with';
64 with "RT::Record::Role::Rights";
65
66 sub Table {'Classes'}
67
68 =head2 Load IDENTIFIER
69
70 Loads a class, either by name or by id
71
72 =cut
73
74 sub Load {
75     my $self = shift;
76     my $id   = shift ;
77
78     return unless $id;
79     if ( $id =~ /^\d+$/ ) {
80         $self->SUPER::Load($id);
81     }
82     else {
83         $self->LoadByCols( Name => $id );
84     }
85 }
86
87 __PACKAGE__->AddRight( Staff   => SeeClass            => 'See that this class exists'); # loc
88 __PACKAGE__->AddRight( Staff   => CreateArticle       => 'Create articles in this class'); # loc
89 __PACKAGE__->AddRight( General => ShowArticle         => 'See articles in this class'); # loc
90 __PACKAGE__->AddRight( Staff   => ShowArticleHistory  => 'See changes to articles in this class'); # loc
91 __PACKAGE__->AddRight( General => SeeCustomField      => 'View custom field values' ); # loc
92 __PACKAGE__->AddRight( Staff   => ModifyArticle       => 'Modify or delete articles in this class'); # loc
93 __PACKAGE__->AddRight( Staff   => ModifyArticleTopics => 'Modify topics for articles in this class'); # loc
94 __PACKAGE__->AddRight( Staff   => ModifyCustomField   => 'Modify custom field values' ); # loc
95 __PACKAGE__->AddRight( Admin   => AdminClass          => 'Modify metadata and custom fields for this class'); # loc
96 __PACKAGE__->AddRight( Admin   => AdminTopics         => 'Modify topic hierarchy associated with this class'); # loc
97 __PACKAGE__->AddRight( Admin   => ShowACL             => 'Display Access Control List'); # loc
98 __PACKAGE__->AddRight( Admin   => ModifyACL           => 'Create, modify and delete Access Control List entries'); # loc
99 __PACKAGE__->AddRight( Staff   => DeleteArticle       => 'Delete articles in this class'); # loc
100
101 # {{{ Create
102
103 =head2 Create PARAMHASH
104
105 Create takes a hash of values and creates a row in the database:
106
107   varchar(255) 'Name'.
108   varchar(255) 'Description'.
109   int(11) 'SortOrder'.
110
111 =cut
112
113 sub Create {
114     my $self = shift;
115     my %args = (
116         Name        => '',
117         Description => '',
118         SortOrder   => '0',
119         HotList     => 0,
120         @_
121     );
122
123     unless (
124         $self->CurrentUser->HasRight(
125             Right  => 'AdminClass',
126             Object => $RT::System
127         )
128       )
129     {
130         return ( 0, $self->loc('Permission Denied') );
131     }
132
133     $self->SUPER::Create(
134         Name        => $args{'Name'},
135         Description => $args{'Description'},
136         SortOrder   => $args{'SortOrder'},
137         HotList     => $args{'HotList'},
138     );
139
140 }
141
142 sub ValidateName {
143     my $self   = shift;
144     my $newval = shift;
145
146     return undef unless ($newval);
147     my $obj = RT::Class->new($RT::SystemUser);
148     $obj->Load($newval);
149     return undef if $obj->id && ( !$self->id || $self->id != $obj->id );
150     return $self->SUPER::ValidateName($newval);
151
152 }
153
154 # }}}
155
156 # }}}
157
158 # {{{ ACCESS CONTROL
159
160 # {{{ sub _Set
161 sub _Set {
162     my $self = shift;
163
164     unless ( $self->CurrentUserHasRight('AdminClass') ) {
165         return ( 0, $self->loc('Permission Denied') );
166     }
167     return ( $self->SUPER::_Set(@_) );
168 }
169
170 # }}}
171
172 # {{{ sub _Value
173
174 sub _Value {
175     my $self = shift;
176
177     unless ( $self->CurrentUserHasRight('SeeClass') ) {
178         return (undef);
179     }
180
181     return ( $self->__Value(@_) );
182 }
183
184 # }}}
185
186 sub ArticleCustomFields {
187     my $self = shift;
188
189
190     my $cfs = RT::CustomFields->new( $self->CurrentUser );
191     if ( $self->CurrentUserHasRight('SeeClass') ) {
192         $cfs->SetContextObject( $self );
193         $cfs->LimitToGlobalOrObjectId( $self->Id );
194         $cfs->LimitToLookupType( RT::Article->CustomFieldLookupType );
195         $cfs->ApplySortOrder;
196     }
197     return ($cfs);
198 }
199
200
201 =head1 AppliedTo
202
203 Returns collection of Queues this Class is applied to.
204 Doesn't takes into account if object is applied globally.
205
206 =cut
207
208 sub AppliedTo {
209     my $self = shift;
210
211     my ($res, $ocfs_alias) = $self->_AppliedTo;
212     return $res unless $res;
213
214     $res->Limit(
215         ALIAS     => $ocfs_alias,
216         FIELD     => 'id',
217         OPERATOR  => 'IS NOT',
218         VALUE     => 'NULL',
219     );
220
221     return $res;
222 }
223
224 =head1 NotAppliedTo
225
226 Returns collection of Queues this Class is not applied to.
227
228 Doesn't takes into account if object is applied globally.
229
230 =cut
231
232 sub NotAppliedTo {
233     my $self = shift;
234
235     my ($res, $ocfs_alias) = $self->_AppliedTo;
236     return $res unless $res;
237
238     $res->Limit(
239         ALIAS     => $ocfs_alias,
240         FIELD     => 'id',
241         OPERATOR  => 'IS',
242         VALUE     => 'NULL',
243     );
244
245     return $res;
246 }
247
248 sub _AppliedTo {
249     my $self = shift;
250
251     my $res = RT::Queues->new( $self->CurrentUser );
252
253     $res->OrderBy( FIELD => 'Name' );
254     my $ocfs_alias = $res->Join(
255         TYPE   => 'LEFT',
256         ALIAS1 => 'main',
257         FIELD1 => 'id',
258         TABLE2 => 'ObjectClasses',
259         FIELD2 => 'ObjectId',
260     );
261     $res->Limit(
262         LEFTJOIN => $ocfs_alias,
263         ALIAS    => $ocfs_alias,
264         FIELD    => 'Class',
265         VALUE    => $self->id,
266     );
267     return ($res, $ocfs_alias);
268 }
269
270 =head2 IsApplied
271
272 Takes object id and returns corresponding L<RT::ObjectClass>
273 record if this Class is applied to the object. Use 0 to check
274 if Class is applied globally.
275
276 =cut
277
278 sub IsApplied {
279     my $self = shift;
280     my $id = shift;
281     return unless defined $id;
282     my $oc = RT::ObjectClass->new( $self->CurrentUser );
283     $oc->LoadByCols( Class=> $self->id, ObjectId => $id,
284                      ObjectType => ( $id ? 'RT::Queue' : 'RT::System' ));
285     return undef unless $oc->id;
286     return $oc;
287 }
288
289 =head2 AddToObject OBJECT
290
291 Apply this Class to a single object, to start with we support Queues
292
293 Takes an object
294
295 =cut
296
297
298 sub AddToObject {
299     my $self  = shift;
300     my $object = shift;
301     my $id = $object->Id || 0;
302
303     unless ( $object->CurrentUserHasRight('AdminClass') ) {
304         return ( 0, $self->loc('Permission Denied') );
305     }
306
307     my $queue = RT::Queue->new( $self->CurrentUser );
308     if ( $id ) {
309         my ($ok, $msg) = $queue->Load( $id );
310         unless ($ok) {
311             return ( 0, $self->loc('Invalid Queue, unable to apply Class: [_1]',$msg ) );
312         }
313
314     }
315
316     if ( $self->IsApplied( $id ) ) {
317         return ( 0, $self->loc("Class is already applied to [_1]",$queue->Name) );
318     }
319
320     if ( $id ) {
321         # applying locally
322         return (0, $self->loc("Class is already applied Globally") )
323             if $self->IsApplied( 0 );
324     }
325     else {
326         my $applied = RT::ObjectClasses->new( $self->CurrentUser );
327         $applied->LimitToClass( $self->id );
328         while ( my $record = $applied->Next ) {
329             $record->Delete;
330         }
331     }
332
333     my $oc = RT::ObjectClass->new( $self->CurrentUser );
334     my ( $oid, $msg ) = $oc->Create(
335         ObjectId => $id, Class => $self->id,
336         ObjectType => ( $id ? 'RT::Queue' : 'RT::System' ),
337     );
338     return ( $oid, $msg );
339 }
340
341
342 =head2 RemoveFromObject OBJECT
343
344 Remove this class from a single queue object
345
346 =cut
347
348 sub RemoveFromObject {
349     my $self = shift;
350     my $object = shift;
351     my $id = $object->Id || 0;
352
353     unless ( $object->CurrentUserHasRight('AdminClass') ) {
354         return ( 0, $self->loc('Permission Denied') );
355     }
356
357     my $ocf = $self->IsApplied( $id );
358     unless ( $ocf ) {
359         return ( 0, $self->loc("This class does not apply to that object") );
360     }
361
362     # XXX: Delete doesn't return anything
363     my ( $oid, $msg ) = $ocf->Delete;
364     return ( $oid, $msg );
365 }
366
367 sub SubjectOverride {
368     my $self = shift;
369     my $override = $self->FirstAttribute('SubjectOverride');
370     return $override ? $override->Content : 0;
371 }
372
373 sub SetSubjectOverride {
374     my $self = shift;
375     my $override = shift;
376
377     if ( $override == $self->SubjectOverride ) {
378         return (0, "SubjectOverride is already set to that");
379     }
380
381     my $cf = RT::CustomField->new($self->CurrentUser);
382     $cf->Load($override);
383
384     if ( $override ) {
385         my ($ok, $msg) = $self->SetAttribute( Name => 'SubjectOverride', Content => $override );
386         return ($ok, $ok ? $self->loc('Added Subject Override: [_1]', $cf->Name) :
387                            $self->loc('Unable to add Subject Override: [_1] [_2]', $cf->Name, $msg));
388     } else {
389         my ($ok, $msg) = $self->DeleteAttribute('SubjectOverride');
390         return ($ok, $ok ? $self->loc('Removed Subject Override') :
391                            $self->loc('Unable to add Subject Override: [_1] [_2]', $cf->Name, $msg));
392     }
393 }
394
395 =head2 id
396
397 Returns the current value of id. 
398 (In the database, id is stored as int(11).)
399
400
401 =cut
402
403
404 =head2 Name
405
406 Returns the current value of Name. 
407 (In the database, Name is stored as varchar(255).)
408
409
410
411 =head2 SetName VALUE
412
413
414 Set Name to VALUE. 
415 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
416 (In the database, Name will be stored as a varchar(255).)
417
418
419 =cut
420
421
422 =head2 Description
423
424 Returns the current value of Description. 
425 (In the database, Description is stored as varchar(255).)
426
427
428
429 =head2 SetDescription VALUE
430
431
432 Set Description to VALUE. 
433 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
434 (In the database, Description will be stored as a varchar(255).)
435
436
437 =cut
438
439
440 =head2 SortOrder
441
442 Returns the current value of SortOrder. 
443 (In the database, SortOrder is stored as int(11).)
444
445
446
447 =head2 SetSortOrder VALUE
448
449
450 Set SortOrder to VALUE. 
451 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
452 (In the database, SortOrder will be stored as a int(11).)
453
454
455 =cut
456
457
458 =head2 Disabled
459
460 Returns the current value of Disabled. 
461 (In the database, Disabled is stored as int(2).)
462
463
464
465 =head2 SetDisabled VALUE
466
467
468 Set Disabled to VALUE. 
469 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
470 (In the database, Disabled will be stored as a int(2).)
471
472
473 =cut
474
475
476 =head2 HotList
477
478 Returns the current value of HotList. 
479 (In the database, HotList is stored as int(2).)
480
481
482
483 =head2 SetHotList VALUE
484
485
486 Set HotList to VALUE. 
487 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
488 (In the database, HotList will be stored as a int(2).)
489
490
491 =cut
492
493
494 =head2 Creator
495
496 Returns the current value of Creator. 
497 (In the database, Creator is stored as int(11).)
498
499
500 =cut
501
502
503 =head2 Created
504
505 Returns the current value of Created. 
506 (In the database, Created is stored as datetime.)
507
508
509 =cut
510
511
512 =head2 LastUpdatedBy
513
514 Returns the current value of LastUpdatedBy. 
515 (In the database, LastUpdatedBy is stored as int(11).)
516
517
518 =cut
519
520
521 =head2 LastUpdated
522
523 Returns the current value of LastUpdated. 
524 (In the database, LastUpdated is stored as datetime.)
525
526
527 =cut
528
529
530
531 sub _CoreAccessible {
532     {
533      
534         id =>
535                 {read => 1, type => 'int(11)', default => ''},
536         Name => 
537                 {read => 1, write => 1, type => 'varchar(255)', default => ''},
538         Description => 
539                 {read => 1, write => 1, type => 'varchar(255)', default => ''},
540         SortOrder => 
541                 {read => 1, write => 1, type => 'int(11)', default => '0'},
542         Disabled => 
543                 {read => 1, write => 1, type => 'int(2)', default => '0'},
544         HotList => 
545                 {read => 1, write => 1, type => 'int(2)', default => '0'},
546         Creator => 
547                 {read => 1, auto => 1, type => 'int(11)', default => '0'},
548         Created => 
549                 {read => 1, auto => 1, type => 'datetime', default => ''},
550         LastUpdatedBy => 
551                 {read => 1, auto => 1, type => 'int(11)', default => '0'},
552         LastUpdated => 
553                 {read => 1, auto => 1, type => 'datetime', default => ''},
554
555  }
556 };
557
558 sub FindDependencies {
559     my $self = shift;
560     my ($walker, $deps) = @_;
561
562     $self->SUPER::FindDependencies($walker, $deps);
563
564     my $articles = RT::Articles->new( $self->CurrentUser );
565     $articles->Limit( FIELD => "Class", VALUE => $self->Id );
566     $deps->Add( in => $articles );
567
568     my $topics = RT::Topics->new( $self->CurrentUser );
569     $topics->LimitToObject( $self );
570     $deps->Add( in => $topics );
571
572     my $objectclasses = RT::ObjectClasses->new( $self->CurrentUser );
573     $objectclasses->LimitToClass( $self->Id );
574     $deps->Add( in => $objectclasses );
575
576     # Custom Fields on things _in_ this class (CFs on the class itself
577     # have already been dealt with)
578     my $ocfs = RT::ObjectCustomFields->new( $self->CurrentUser );
579     $ocfs->Limit( FIELD           => 'ObjectId',
580                   OPERATOR        => '=',
581                   VALUE           => $self->id,
582                   ENTRYAGGREGATOR => 'OR' );
583     $ocfs->Limit( FIELD           => 'ObjectId',
584                   OPERATOR        => '=',
585                   VALUE           => 0,
586                   ENTRYAGGREGATOR => 'OR' );
587     my $cfs = $ocfs->Join(
588         ALIAS1 => 'main',
589         FIELD1 => 'CustomField',
590         TABLE2 => 'CustomFields',
591         FIELD2 => 'id',
592     );
593     $ocfs->Limit( ALIAS    => $cfs,
594                   FIELD    => 'LookupType',
595                   OPERATOR => 'STARTSWITH',
596                   VALUE    => 'RT::Class-' );
597     $deps->Add( in => $ocfs );
598 }
599
600 sub PreInflate {
601     my $class = shift;
602     my ($importer, $uid, $data) = @_;
603
604     $class->SUPER::PreInflate( $importer, $uid, $data );
605
606     return if $importer->MergeBy( "Name", $class, $uid, $data );
607
608     return 1;
609 }
610
611 RT::Base->_ImportOverlays();
612
613 1;
614