6d4c515623006c01fe77e36f3322ace28a691739
[freeside.git] / rt / lib / RT / Dashboard.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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::Dashboard - an API for saving and retrieving dashboards
52
53 =head1 SYNOPSIS
54
55   use RT::Dashboard
56
57 =head1 DESCRIPTION
58
59   Dashboard is an object that can belong to either an RT::User or an
60   RT::Group.  It consists of an ID, a name, and a number of
61   saved searches and portlets.
62
63 =head1 METHODS
64
65
66 =cut
67
68 package RT::Dashboard;
69
70 use RT::SavedSearch;
71
72 use strict;
73 use warnings;
74
75 use base qw/RT::SharedSetting/;
76
77 use RT::System;
78 RT::System::AddRights(
79     SubscribeDashboard => 'Subscribe to dashboards', #loc_pair
80
81     SeeDashboard       => 'View system dashboards', #loc_pair
82     CreateDashboard    => 'Create system dashboards', #loc_pair
83     ModifyDashboard    => 'Modify system dashboards', #loc_pair
84     DeleteDashboard    => 'Delete system dashboards', #loc_pair
85
86     SeeOwnDashboard    => 'View personal dashboards', #loc_pair
87     CreateOwnDashboard => 'Create personal dashboards', #loc_pair
88     ModifyOwnDashboard => 'Modify personal dashboards', #loc_pair
89     DeleteOwnDashboard => 'Delete personal dashboards', #loc_pair
90 );
91
92 RT::System::AddRightCategories(
93     SubscribeDashboard => 'Staff',
94
95     SeeDashboard       => 'General',
96     CreateDashboard    => 'Admin',
97     ModifyDashboard    => 'Admin',
98     DeleteDashboard    => 'Admin',
99
100     SeeOwnDashboard    => 'Staff',
101     CreateOwnDashboard => 'Staff',
102     ModifyOwnDashboard => 'Staff',
103     DeleteOwnDashboard => 'Staff',
104 );
105
106 =head2 ObjectName
107
108 An object of this class is called "dashboard"
109
110 =cut
111
112 sub ObjectName { "dashboard" } # loc
113
114 sub SaveAttribute {
115     my $self   = shift;
116     my $object = shift;
117     my $args   = shift;
118
119     return $object->AddAttribute(
120         'Name'        => 'Dashboard',
121         'Description' => $args->{'Name'},
122         'Content'     => {Panes => $args->{'Panes'}},
123     );
124 }
125
126 sub UpdateAttribute {
127     my $self = shift;
128     my $args = shift;
129
130     my ($status, $msg) = (1, undef);
131     if (defined $args->{'Panes'}) {
132         ($status, $msg) = $self->{'Attribute'}->SetSubValues(
133             Panes => $args->{'Panes'},
134         );
135     }
136
137     if ($status && $args->{'Name'}) {
138         ($status, $msg) = $self->{'Attribute'}->SetDescription($args->{'Name'})
139             unless $self->Name eq $args->{'Name'};
140     }
141
142     if ($status && $args->{'Privacy'}) {
143         my ($new_obj_type, $new_obj_id) = split /-/, $args->{'Privacy'};
144         my ($obj_type, $obj_id) = split /-/, $self->Privacy;
145
146         my $attr = $self->{'Attribute'};
147         if ($new_obj_type ne $obj_type) {
148             ($status, $msg) = $attr->SetObjectType($new_obj_type);
149         }
150         if ($status && $new_obj_id != $obj_id ) {
151             ($status, $msg) = $attr->SetObjectId($new_obj_id);
152         }
153         $self->{'Privacy'} = $args->{'Privacy'} if $status;
154     }
155
156     return ($status, $msg);
157 }
158
159 =head2 PostLoadValidate
160
161 Ensure that the ID corresponds to an actual dashboard object, since it's all
162 attributes under the hood.
163
164 =cut
165
166 sub PostLoadValidate {
167     my $self = shift;
168     return (0, "Invalid object type") unless $self->{'Attribute'}->Name eq 'Dashboard';
169     return 1;
170 }
171
172 =head2 Panes
173
174 Returns a hashref of pane name to portlets
175
176 =cut
177
178 sub Panes {
179     my $self = shift;
180     return unless ref($self->{'Attribute'}) eq 'RT::Attribute';
181     return $self->{'Attribute'}->SubValue('Panes') || {};
182 }
183
184 =head2 Portlets
185
186 Returns the list of this dashboard's portlets, each a hashref with key
187 C<portlet_type> being C<search> or C<component>.
188
189 =cut
190
191 sub Portlets {
192     my $self = shift;
193     return map { @$_ } values %{ $self->Panes };
194 }
195
196 =head2 Dashboards
197
198 Returns a list of loaded sub-dashboards
199
200 =cut
201
202 sub Dashboards {
203     my $self = shift;
204     return map {
205         my $search = RT::Dashboard->new($self->CurrentUser);
206         $search->LoadById($_->{id});
207         $search
208     } grep { $_->{portlet_type} eq 'dashboard' } $self->Portlets;
209 }
210
211 =head2 Searches
212
213 Returns a list of loaded saved searches
214
215 =cut
216
217 sub Searches {
218     my $self = shift;
219     return map {
220         my $search = RT::SavedSearch->new($self->CurrentUser);
221         $search->Load($_->{privacy}, $_->{id});
222         $search
223     } grep { $_->{portlet_type} eq 'search' } $self->Portlets;
224 }
225
226 =head2 ShowSearchName Portlet
227
228 Returns an array for one saved search, suitable for passing to
229 /Elements/ShowSearch.
230
231 =cut
232
233 sub ShowSearchName {
234     my $self = shift;
235     my $portlet = shift;
236
237     if ($portlet->{privacy} eq 'RT::System') {
238         return Name => $portlet->{description};
239     }
240
241     return SavedSearch => join('-', $portlet->{privacy}, 'SavedSearch', $portlet->{id});
242 }
243
244 =head2 PossibleHiddenSearches
245
246 This will return a list of saved searches that are potentially not visible by
247 all users for whom the dashboard is visible. You may pass in a privacy to
248 use instead of the dashboard's privacy.
249
250 =cut
251
252 sub PossibleHiddenSearches {
253     my $self = shift;
254     my $privacy = shift || $self->Privacy;
255
256     return grep { !$_->IsVisibleTo($privacy) } $self->Searches, $self->Dashboards;
257 }
258
259 # _PrivacyObjects: returns a list of objects that can be used to load
260 # dashboards from. You probably want to use the wrapper methods like
261 # ObjectsForLoading, ObjectsForCreating, etc.
262
263 sub _PrivacyObjects {
264     my $self = shift;
265
266     my @objects;
267
268     my $CurrentUser = $self->CurrentUser;
269     push @objects, $CurrentUser->UserObj;
270
271     my $groups = RT::Groups->new($CurrentUser);
272     $groups->LimitToUserDefinedGroups;
273     $groups->WithMember( PrincipalId => $CurrentUser->Id,
274                          Recursively => 1 );
275     push @objects, @{ $groups->ItemsArrayRef };
276
277     push @objects, RT::System->new($CurrentUser);
278
279     return @objects;
280 }
281
282 # ACLs
283
284 sub _CurrentUserCan {
285     my $self    = shift;
286     my $privacy = shift || $self->Privacy;
287     my %args    = @_;
288
289     if (!defined($privacy)) {
290         $RT::Logger->debug("No privacy provided to $self->_CurrentUserCan");
291         return 0;
292     }
293
294     my $object = $self->_GetObject($privacy);
295     return 0 unless $object;
296
297     my $level;
298
299        if ($object->isa('RT::User'))   { $level = 'Own' }
300     elsif ($object->isa('RT::Group'))  { $level = 'Group' }
301     elsif ($object->isa('RT::System')) { $level = '' }
302     else {
303         $RT::Logger->error("Unknown object $object from privacy $privacy");
304         return 0;
305     }
306
307     # users are mildly special-cased, since we actually have to check that
308     # the user is operating on himself
309     if ($object->isa('RT::User')) {
310         return 0 unless $object->Id == $self->CurrentUser->Id;
311     }
312
313     my $right = $args{FullRight}
314              || join('', $args{Right}, $level, 'Dashboard');
315
316     # all rights, except group rights, are global
317     $object = $RT::System unless $object->isa('RT::Group');
318
319     return $self->CurrentUser->HasRight(
320         Right  => $right,
321         Object => $object,
322     );
323 }
324
325 sub CurrentUserCanSee {
326     my $self    = shift;
327     my $privacy = shift;
328
329     $self->_CurrentUserCan($privacy, Right => 'See');
330 }
331
332 sub CurrentUserCanCreate {
333     my $self    = shift;
334     my $privacy = shift;
335
336     $self->_CurrentUserCan($privacy, Right => 'Create');
337 }
338
339 sub CurrentUserCanModify {
340     my $self    = shift;
341     my $privacy = shift;
342
343     $self->_CurrentUserCan($privacy, Right => 'Modify');
344 }
345
346 sub CurrentUserCanDelete {
347     my $self    = shift;
348     my $privacy = shift;
349
350     $self->_CurrentUserCan($privacy, Right => 'Delete');
351 }
352
353 sub CurrentUserCanSubscribe {
354     my $self    = shift;
355     my $privacy = shift;
356
357     $self->_CurrentUserCan($privacy, FullRight => 'SubscribeDashboard');
358 }
359
360 =head2 Subscription
361
362 Returns the L<RT::Attribute> representing the current user's subscription
363 to this dashboard if there is one; otherwise, returns C<undef>.
364
365 =cut
366
367 sub Subscription {
368     my $self = shift;
369
370     # no subscription to unloaded dashboards
371     return unless $self->id;
372
373     for my $sub ($self->CurrentUser->UserObj->Attributes->Named('Subscription')) {
374         return $sub if $sub->SubValue('DashboardId') == $self->id;
375     }
376
377     return;
378 }
379
380 sub ObjectsForLoading {
381     my $self = shift;
382     my %args = (
383         IncludeSuperuserGroups => 1,
384         @_
385     );
386     my @objects;
387
388     # If you've been granted the SeeOwnDashboard global right (which you
389     # could have by way of global user right or global group right), you
390     # get to see your own dashboards
391     my $CurrentUser = $self->CurrentUser;
392     push @objects, $CurrentUser->UserObj
393         if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeOwnDashboard');
394
395     # Find groups for which: (a) you are a member of the group, and (b)
396     # you have been granted SeeGroupDashboard on (by any means), and (c)
397     # have at least one dashboard
398     my $groups = RT::Groups->new($CurrentUser);
399     $groups->LimitToUserDefinedGroups;
400     $groups->ForWhichCurrentUserHasRight(
401         Right             => 'SeeGroupDashboard',
402         IncludeSuperusers => $args{IncludeSuperuserGroups},
403     );
404     $groups->WithMember(
405         Recursively => 1,
406         PrincipalId => $CurrentUser->UserObj->PrincipalId
407     );
408     my $attrs = $groups->Join(
409         ALIAS1 => 'main',
410         FIELD1 => 'id',
411         TABLE2 => 'Attributes',
412         FIELD2 => 'ObjectId',
413     );
414     $groups->Limit(
415         ALIAS => $attrs,
416         FIELD => 'ObjectType',
417         VALUE => 'RT::Group',
418     );
419     $groups->Limit(
420         ALIAS => $attrs,
421         FIELD => 'Name',
422         VALUE => 'Dashboard',
423     );
424     push @objects, @{ $groups->ItemsArrayRef };
425
426     # Finally, if you have been granted the SeeDashboard right (which
427     # you could have by way of global user right or global group right),
428     # you can see system dashboards.
429     push @objects, RT::System->new($CurrentUser)
430         if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeDashboard');
431
432     return @objects;
433 }
434
435 sub CurrentUserCanCreateAny {
436     my $self = shift;
437     my @objects;
438
439     my $CurrentUser = $self->CurrentUser;
440     return 1
441         if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateOwnDashboard');
442
443     my $groups = RT::Groups->new($CurrentUser);
444     $groups->LimitToUserDefinedGroups;
445     $groups->ForWhichCurrentUserHasRight(
446         Right             => 'CreateGroupDashboard',
447         IncludeSuperusers => 1,
448     );
449     return 1 if $groups->Count;
450
451     return 1
452         if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateDashboard');
453
454     return 0;
455 }
456
457 =head2 Delete
458
459 Deletes the dashboard and related subscriptions.
460 Returns a tuple of status and message, where status is true upon success.
461
462 =cut
463
464 sub Delete {
465     my $self = shift;
466     my $id = $self->id;
467     my ( $status, $msg ) = $self->SUPER::Delete(@_);
468     if ( $status ) {
469         # delete all the subscriptions
470         my $subscriptions = RT::Attributes->new( RT->SystemUser );
471         $subscriptions->Limit(
472             FIELD => 'Name',
473             VALUE => 'Subscription',
474         );
475         $subscriptions->Limit(
476             FIELD => 'Description',
477             VALUE => "Subscription to dashboard $id",
478         );
479         while ( my $subscription = $subscriptions->Next ) {
480             $subscription->Delete();
481         }
482     }
483
484     return ( $status, $msg );
485 }
486
487 RT::Base->_ImportOverlays();
488
489 1;