Dashboard.pm 12.7 KB
Newer Older
1
# BEGIN BPS TAGGED BLOCK {{{
Jesse Vincent's avatar
Jesse Vincent committed
2
#
3
# COPYRIGHT:
Jesse Vincent's avatar
Jesse Vincent committed
4
#
5
# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
Kevin Falcone's avatar
Kevin Falcone committed
6
#                                          <sales@bestpractical.com>
Jesse Vincent's avatar
Jesse Vincent committed
7
#
8
# (Except where explicitly superseded by other copyright notices)
Jesse Vincent's avatar
Jesse Vincent committed
9
10
#
#
11
# LICENSE:
Jesse Vincent's avatar
Jesse Vincent committed
12
#
13
14
15
16
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
Jesse Vincent's avatar
Jesse Vincent committed
17
#
18
19
20
21
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
Jesse Vincent's avatar
Jesse Vincent committed
22
#
23
24
25
26
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
Jesse Vincent's avatar
Jesse Vincent committed
27
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
Jesse Vincent's avatar
Jesse Vincent committed
28
29
#
#
30
# CONTRIBUTION SUBMISSION POLICY:
Jesse Vincent's avatar
Jesse Vincent committed
31
#
32
33
34
35
36
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
Jesse Vincent's avatar
Jesse Vincent committed
37
#
38
39
40
41
42
43
44
45
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
Jesse Vincent's avatar
Jesse Vincent committed
46
#
47
# END BPS TAGGED BLOCK }}}
Jesse Vincent's avatar
Jesse Vincent committed
48

49
50
51
52
53
54
55
56
57
58
59
60
=head1 NAME

  RT::Dashboard - an API for saving and retrieving dashboards

=head1 SYNOPSIS

  use RT::Dashboard

=head1 DESCRIPTION

  Dashboard is an object that can belong to either an RT::User or an
  RT::Group.  It consists of an ID, a name, and a number of
Shawn Moore's avatar
Shawn Moore committed
61
  saved searches and portlets.
62
63
64
65
66
67
68
69
70
71

=head1 METHODS


=cut

package RT::Dashboard;

use strict;
use warnings;
72

Shawn Moore's avatar
Shawn Moore committed
73
use base qw/RT::SharedSetting/;
74

75
76
use RT::SavedSearch;

77
use RT::System;
78
RT::System::AddRights(
79
    SubscribeDashboard => 'Subscribe to dashboards', #loc_pair
80
81
82
83
84
85
86
87
88
89

    SeeDashboard       => 'View system dashboards', #loc_pair
    CreateDashboard    => 'Create system dashboards', #loc_pair
    ModifyDashboard    => 'Modify system dashboards', #loc_pair
    DeleteDashboard    => 'Delete system dashboards', #loc_pair

    SeeOwnDashboard    => 'View personal dashboards', #loc_pair
    CreateOwnDashboard => 'Create personal dashboards', #loc_pair
    ModifyOwnDashboard => 'Modify personal dashboards', #loc_pair
    DeleteOwnDashboard => 'Delete personal dashboards', #loc_pair
90
91
);

92
93
94
95
96
97
98
99
100
101
102
103
104
RT::System::AddRightCategories(
    SubscribeDashboard => 'Staff',

    SeeDashboard       => 'General',
    CreateDashboard    => 'Admin',
    ModifyDashboard    => 'Admin',
    DeleteDashboard    => 'Admin',

    SeeOwnDashboard    => 'Staff',
    CreateOwnDashboard => 'Staff',
    ModifyOwnDashboard => 'Staff',
    DeleteOwnDashboard => 'Staff',
);
105

106
=head2 ObjectName
107

108
An object of this class is called "dashboard"
109
110
111

=cut

112
sub ObjectName { "dashboard" }
113

114
115
116
117
sub SaveAttribute {
    my $self   = shift;
    my $object = shift;
    my $args   = shift;
118

119
    return $object->AddAttribute(
120
        'Name'        => 'Dashboard',
Shawn Moore's avatar
Shawn Moore committed
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
139
        ($status, $msg) = $self->{'Attribute'}->SetDescription($args->{'Name'})
            unless $self->Name eq $args->{'Name'};
140
141
    }

142
143
144
145
146
147
148
149
150
151
152
153
154
155
    if ($status && $args->{'Privacy'}) {
        my ($new_obj_type, $new_obj_id) = split /-/, $args->{'Privacy'};
        my ($obj_type, $obj_id) = split /-/, $self->Privacy;

        my $attr = $self->{'Attribute'};
        if ($new_obj_type ne $obj_type) {
            ($status, $msg) = $attr->SetObjectType($new_obj_type);
        }
        if ($status && $new_obj_id != $obj_id ) {
            ($status, $msg) = $attr->SetObjectId($new_obj_id);
        }
        $self->{'Privacy'} = $args->{'Privacy'} if $status;
    }

156
    return ($status, $msg);
157
158
}

159
160
161
162
163
164
165
166
167
=head2 PostLoadValidate

Ensure that the ID corresponds to an actual dashboard object, since it's all
attributes under the hood.

=cut

sub PostLoadValidate {
    my $self = shift;
168
    return (0, "Invalid object type") unless $self->{'Attribute'}->Name eq 'Dashboard';
169
170
171
    return 1;
}

172
173
174
175
176
177
178
179
180
181
182
183
=head2 Panes

Returns a hashref of pane name to portlets

=cut

sub Panes {
    my $self = shift;
    return unless ref($self->{'Attribute'}) eq 'RT::Attribute';
    return $self->{'Attribute'}->SubValue('Panes') || {};
}

184
185
186
187
188
189
190
191
192
=head2 Portlets

Returns the list of this dashboard's portlets, each a hashref with key
C<portlet_type> being C<search> or C<component>.

=cut

sub Portlets {
    my $self = shift;
193
    return map { @$_ } values %{ $self->Panes };
194
195
}

Shawn Moore's avatar
Shawn Moore committed
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
=head2 Dashboards

Returns a list of loaded sub-dashboards

=cut

sub Dashboards {
    my $self = shift;
    return map {
        my $search = RT::Dashboard->new($self->CurrentUser);
        $search->LoadById($_->{id});
        $search
    } grep { $_->{portlet_type} eq 'dashboard' } $self->Portlets;
}

211
212
213
214
215
216
217
218
219
=head2 Searches

Returns a list of loaded saved searches

=cut

sub Searches {
    my $self = shift;
    return map {
220
221
222
        my $search = RT::SavedSearch->new($self->CurrentUser);
        $search->Load($_->{privacy}, $_->{id});
        $search
223
    } grep { $_->{portlet_type} eq 'search' } $self->Portlets;
224
225
}

226
=head2 ShowSearchName Portlet
227
228
229
230
231
232

Returns an array for one saved search, suitable for passing to
/Elements/ShowSearch.

=cut

233
sub ShowSearchName {
234
    my $self = shift;
235
236
    my $portlet = shift;

Shawn Moore's avatar
Shawn Moore committed
237
238
    if ($portlet->{privacy} eq 'RT::System') {
        return Name => $portlet->{description};
239
240
    }

Shawn Moore's avatar
Shawn Moore committed
241
    return SavedSearch => join('-', $portlet->{privacy}, 'SavedSearch', $portlet->{id});
242
243
}

244
245
246
247
248
249
250
251
252
253
254
255
=head2 PossibleHiddenSearches

This will return a list of saved searches that are potentially not visible by
all users for whom the dashboard is visible. You may pass in a privacy to
use instead of the dashboard's privacy.

=cut

sub PossibleHiddenSearches {
    my $self = shift;
    my $privacy = shift || $self->Privacy;

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
261
# dashboards from. You probably want to use the wrapper methods like
# ObjectsForLoading, ObjectsForCreating, etc.
262
263
264
265

sub _PrivacyObjects {
    my $self = shift;

266
267
    my @objects;

268
269
    my $CurrentUser = $self->CurrentUser;
    push @objects, $CurrentUser->UserObj;
270
271
272
273
274

    my $groups = RT::Groups->new($CurrentUser);
    $groups->LimitToUserDefinedGroups;
    $groups->WithMember( PrincipalId => $CurrentUser->Id,
                         Recursively => 1 );
275
    push @objects, @{ $groups->ItemsArrayRef };
276

277
    push @objects, RT::System->new($CurrentUser);
278
279
280
281

    return @objects;
}

282
283
# ACLs

284
285
286
sub _CurrentUserCan {
    my $self    = shift;
    my $privacy = shift || $self->Privacy;
287
    my %args    = @_;
288

289
290
291
292
293
    if (!defined($privacy)) {
        $RT::Logger->debug("No privacy provided to $self->_CurrentUserCan");
        return 0;
    }

294
295
296
    my $object = $self->_GetObject($privacy);
    return 0 unless $object;

297
    my $level;
298

299
300
301
302
303
       if ($object->isa('RT::User'))   { $level = 'Own' }
    elsif ($object->isa('RT::Group'))  { $level = 'Group' }
    elsif ($object->isa('RT::System')) { $level = '' }
    else {
        $RT::Logger->error("Unknown object $object from privacy $privacy");
304
305
306
307
        return 0;
    }

    # users are mildly special-cased, since we actually have to check that
308
    # the user is operating on himself
309
310
311
312
    if ($object->isa('RT::User')) {
        return 0 unless $object->Id == $self->CurrentUser->Id;
    }

313
314
315
316
317
318
    my $right = $args{FullRight}
             || join('', $args{Right}, $level, 'Dashboard');

    # all rights, except group rights, are global
    $object = $RT::System unless $object->isa('RT::Group');

319
320
321
322
323
324
    return $self->CurrentUser->HasRight(
        Right  => $right,
        Object => $object,
    );
}

325
sub CurrentUserCanSee {
326
    my $self    = shift;
327
328
    my $privacy = shift;

329
    $self->_CurrentUserCan($privacy, Right => 'See');
330
331
}

332
333
sub CurrentUserCanCreate {
    my $self    = shift;
334
335
    my $privacy = shift;

336
    $self->_CurrentUserCan($privacy, Right => 'Create');
337
338
}

339
sub CurrentUserCanModify {
340
    my $self    = shift;
341
342
    my $privacy = shift;

343
    $self->_CurrentUserCan($privacy, Right => 'Modify');
344
345
346
}

sub CurrentUserCanDelete {
347
    my $self    = shift;
348
349
    my $privacy = shift;

350
351
352
353
354
355
356
357
    $self->_CurrentUserCan($privacy, Right => 'Delete');
}

sub CurrentUserCanSubscribe {
    my $self    = shift;
    my $privacy = shift;

    $self->_CurrentUserCan($privacy, FullRight => 'SubscribeDashboard');
358
359
}

360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
=head2 Subscription

Returns the L<RT::Attribute> representing the current user's subscription
to this dashboard if there is one; otherwise, returns C<undef>.

=cut

sub Subscription {
    my $self = shift;

    # no subscription to unloaded dashboards
    return unless $self->id;

    for my $sub ($self->CurrentUser->UserObj->Attributes->Named('Subscription')) {
        return $sub if $sub->SubValue('DashboardId') == $self->id;
    }

    return;
}

380
381
sub ObjectsForLoading {
    my $self = shift;
382
    my %args = (
383
        IncludeSuperuserGroups => 1,
384
385
        @_
    );
386
387
    my @objects;

388
389
390
    # If you've been granted the SeeOwnDashboard global right (which you
    # could have by way of global user right or global group right), you
    # get to see your own dashboards
391
392
    my $CurrentUser = $self->CurrentUser;
    push @objects, $CurrentUser->UserObj
Shawn M Moore's avatar
Shawn M Moore committed
393
        if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeOwnDashboard');
394

395
396
397
    # Find groups for which: (a) you are a member of the group, and (b)
    # you have been granted SeeGroupDashboard on (by any means), and (c)
    # have at least one dashboard
398
    my $groups = RT::Groups->new($CurrentUser);
399
    $groups->LimitToUserDefinedGroups;
400
401
    $groups->ForWhichCurrentUserHasRight(
        Right             => 'SeeGroupDashboard',
402
        IncludeSuperusers => $args{IncludeSuperuserGroups},
403
    );
404
405
406
407
    $groups->WithMember(
        Recursively => 1,
        PrincipalId => $CurrentUser->UserObj->PrincipalId
    );
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
    my $attrs = $groups->Join(
        ALIAS1 => 'main',
        FIELD1 => 'id',
        TABLE2 => 'Attributes',
        FIELD2 => 'ObjectId',
    );
    $groups->Limit(
        ALIAS => $attrs,
        FIELD => 'ObjectType',
        VALUE => 'RT::Group',
    );
    $groups->Limit(
        ALIAS => $attrs,
        FIELD => 'Name',
        VALUE => 'Dashboard',
    );
424
425
    push @objects, @{ $groups->ItemsArrayRef };

426
427
428
    # Finally, if you have been granted the SeeDashboard right (which
    # you could have by way of global user right or global group right),
    # you can see system dashboards.
429
    push @objects, RT::System->new($CurrentUser)
Shawn M Moore's avatar
Shawn M Moore committed
430
        if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeDashboard');
431
432
433

    return @objects;
}
434

435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
sub CurrentUserCanCreateAny {
    my $self = shift;
    my @objects;

    my $CurrentUser = $self->CurrentUser;
    return 1
        if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateOwnDashboard');

    my $groups = RT::Groups->new($CurrentUser);
    $groups->LimitToUserDefinedGroups;
    $groups->ForWhichCurrentUserHasRight(
        Right             => 'CreateGroupDashboard',
        IncludeSuperusers => 1,
    );
    return 1 if $groups->Count;

    return 1
        if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateDashboard');

    return 0;
}

457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
=head2 Delete

Deletes the dashboard and related subscriptions.
Returns a tuple of status and message, where status is true upon success.

=cut

sub Delete {
    my $self = shift;
    my $id = $self->id;
    my ( $status, $msg ) = $self->SUPER::Delete(@_);
    if ( $status ) {
        # delete all the subscriptions
        my $subscriptions = RT::Attributes->new( RT->SystemUser );
        $subscriptions->Limit(
            FIELD => 'Name',
            VALUE => 'Subscription',
        );
        $subscriptions->Limit(
            FIELD => 'Description',
            VALUE => "Subscription to dashboard $id",
        );
        while ( my $subscription = $subscriptions->Next ) {
            $subscription->Delete();
        }
    }

    return ( $status, $msg );
}

487
RT::Base->_ImportOverlays();
488
489

1;