#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
Content => '',
ContentType => '',
Object => undef,
- @_);
+ @_);
if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
- $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
- $args{ObjectId} = $args{Object}->Id;
+ $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
+ $args{ObjectId} = $args{Object}->Id;
} else {
return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
$args{'ContentType'} = 'storable';
}
-
$self->SUPER::Create(
Name => $args{'Name'},
Content => $args{'Content'},
);
return (
- $self->LoadByCols(
- Name => $args{'Name'},
- ObjectType => ref($args{'Object'}),
- ObjectId => $args{'Object'}->Id,
- )
+ $self->LoadByCols(
+ Name => $args{'Name'},
+ ObjectType => ref($args{'Object'}),
+ ObjectId => $args{'Object'}->Id,
+ )
);
}
return(0, "Content couldn't be frozen");
}
}
- return $self->_Set( Field => 'Content', Value => $content );
+ my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content );
+ return ($ok, $self->loc("Attribute updated")) if $ok;
+ return ($ok, $msg);
}
=head2 SubValue KEY
unless ($self->CurrentUserHasRight('delete')) {
return (0,$self->loc('Permission Denied'));
}
+
return($self->SUPER::Delete(@_));
}
{
id =>
- {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
+ {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
Name =>
- {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
+ {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
Description =>
- {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
+ {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
Content =>
- {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'blob', default => ''},
+ {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'blob', default => ''},
ContentType =>
- {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
+ {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
ObjectType =>
- {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
+ {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
ObjectId =>
- {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
+ {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
Creator =>
- {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
+ {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
Created =>
- {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
+ {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
LastUpdatedBy =>
- {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
+ {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
LastUpdated =>
- {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
+ {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
}
};
+sub FindDependencies {
+ my $self = shift;
+ my ($walker, $deps) = @_;
+
+ $self->SUPER::FindDependencies($walker, $deps);
+ $deps->Add( out => $self->Object );
+
+ # dashboards in menu attribute has dependencies on each of its dashboards
+ if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
+ my $content = $self->Content;
+ for my $pane (values %{ $content || {} }) {
+ for my $dash_id (@$pane) {
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($dash_id);
+ $deps->Add( out => $attr );
+ }
+ }
+ }
+ # homepage settings attribute has dependencies on each of the searches in it
+ elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
+ my $content = $self->Content;
+ for my $pane (values %{ $content || {} }) {
+ for my $component (@$pane) {
+ # this hairy code mirrors what's in the saved search loader
+ # in /Elements/ShowSearch
+ if ($component->{type} eq 'saved') {
+ if ($component->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($3);
+ $deps->Add( out => $attr );
+ }
+ }
+ elsif ($component->{type} eq 'system') {
+ my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $component->{name} );
+ unless ( $search && $search->Id ) {
+ my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
+ foreach my $custom (@custom_searches) {
+ if ($custom->Description eq $component->{name}) { $search = $custom; last }
+ }
+ }
+ $deps->Add( out => $search ) if $search;
+ }
+ }
+ }
+ }
+ # dashboards have dependencies on all the searches and dashboards they use
+ elsif ($self->Name eq 'Dashboard') {
+ my $content = $self->Content;
+ for my $pane (values %{ $content->{Panes} || {} }) {
+ for my $component (@$pane) {
+ if ($component->{portlet_type} eq 'search' || $component->{portlet_type} eq 'dashboard') {
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($component->{id});
+ $deps->Add( out => $attr );
+ }
+ }
+ }
+ }
+ # each subscription depends on its dashboard
+ elsif ($self->Name eq 'Subscription') {
+ my $content = $self->Content;
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($content->{DashboardId});
+ $deps->Add( out => $attr );
+ }
+}
+
+sub PreInflate {
+ my $class = shift;
+ my ($importer, $uid, $data) = @_;
+
+ if ($data->{Object} and ref $data->{Object}) {
+ my $on_uid = ${ $data->{Object} };
+
+ # skip attributes of objects we're not inflating
+ # exception: we don't inflate RT->System, but we want RT->System's searches
+ unless ($on_uid eq RT->System->UID && $data->{Name} =~ /Search/) {
+ return if $importer->ShouldSkipTransaction($on_uid);
+ }
+ }
+
+ return $class->SUPER::PreInflate( $importer, $uid, $data );
+}
+
+# this method will be called repeatedly to fix up this attribute's contents
+# (a list of searches, dashboards) during the import process, as the
+# ordinary dependency resolution system can't quite handle the subtlety
+# involved (e.g. a user simply declares out-dependencies on all of her
+# attributes, but those attributes (e.g. dashboards, saved searches,
+# dashboards in menu preferences) have dependencies amongst themselves).
+# if this attribute (e.g. a user's dashboard) fails to load an attribute
+# (e.g. a user's saved search) then it postpones and repeats the postinflate
+# process again when that user's saved search has been imported
+# this method updates Content each time through, each time getting closer and
+# closer to the fully inflated attribute
+sub PostInflateFixup {
+ my $self = shift;
+ my $importer = shift;
+ my $spec = shift;
+
+ # decode UIDs to be raw dashboard IDs
+ if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
+ my $content = $self->Content;
+
+ for my $pane (values %{ $content || {} }) {
+ for (@$pane) {
+ if (ref($_) eq 'SCALAR') {
+ my $attr = $importer->LookupObj($$_);
+ if ($attr) {
+ $_ = $attr->Id;
+ }
+ else {
+ $importer->Postpone(
+ for => $$_,
+ uid => $spec->{uid},
+ method => 'PostInflateFixup',
+ );
+ }
+ }
+ }
+ }
+ $self->SetContent($content);
+ }
+ # decode UIDs to be saved searches
+ elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
+ my $content = $self->Content;
+
+ for my $pane (values %{ $content || {} }) {
+ for (@$pane) {
+ if (ref($_->{uid}) eq 'SCALAR') {
+ my $uid = $_->{uid};
+ my $attr = $importer->LookupObj($$uid);
+
+ if ($attr) {
+ if ($_->{type} eq 'saved') {
+ $_->{name} = join '-', $attr->ObjectType, $attr->ObjectId, 'SavedSearch', $attr->id;
+ }
+ # if type is system, name doesn't need to change
+ # if type is anything else, pass it through as is
+ delete $_->{uid};
+ }
+ else {
+ $importer->Postpone(
+ for => $$uid,
+ uid => $spec->{uid},
+ method => 'PostInflateFixup',
+ );
+ }
+ }
+ }
+ }
+ $self->SetContent($content);
+ }
+ elsif ($self->Name eq 'Dashboard') {
+ my $content = $self->Content;
+
+ for my $pane (values %{ $content->{Panes} || {} }) {
+ for (@$pane) {
+ if (ref($_->{uid}) eq 'SCALAR') {
+ my $uid = $_->{uid};
+ my $attr = $importer->LookupObj($$uid);
+
+ if ($attr) {
+ # update with the new id numbers assigned to us
+ $_->{id} = $attr->Id;
+ $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId;
+ delete $_->{uid};
+ }
+ else {
+ $importer->Postpone(
+ for => $$uid,
+ uid => $spec->{uid},
+ method => 'PostInflateFixup',
+ );
+ }
+ }
+ }
+ }
+ $self->SetContent($content);
+ }
+ elsif ($self->Name eq 'Subscription') {
+ my $content = $self->Content;
+ if (ref($content->{DashboardId}) eq 'SCALAR') {
+ my $attr = $importer->LookupObj(${ $content->{DashboardId} });
+ if ($attr) {
+ $content->{DashboardId} = $attr->Id;
+ }
+ else {
+ $importer->Postpone(
+ for => ${ $content->{DashboardId} },
+ uid => $spec->{uid},
+ method => 'PostInflateFixup',
+ );
+ }
+ }
+ $self->SetContent($content);
+ }
+}
+
+sub PostInflate {
+ my $self = shift;
+ my ($importer, $uid) = @_;
+
+ $self->SUPER::PostInflate( $importer, $uid );
+
+ # this method is separate because it needs to be callable multple times,
+ # and we can't guarantee that SUPER::PostInflate can deal with that
+ $self->PostInflateFixup($importer, { uid => $uid });
+}
+
+sub Serialize {
+ my $self = shift;
+ my %args = (@_);
+ my %store = $self->SUPER::Serialize(@_);
+
+ # encode raw dashboard IDs to be UIDs
+ if ($store{Name} eq RT::User::_PrefName("DashboardsInMenu")) {
+ my $content = $self->_DeserializeContent($store{Content});
+ for my $pane (values %{ $content || {} }) {
+ for (@$pane) {
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($_);
+ $_ = \($attr->UID);
+ }
+ }
+ $store{Content} = $self->_SerializeContent($content);
+ }
+ # encode saved searches to be UIDs
+ elsif ($store{Name} eq RT::User::_PrefName("HomepageSettings")) {
+ my $content = $self->_DeserializeContent($store{Content});
+ for my $pane (values %{ $content || {} }) {
+ for (@$pane) {
+ # this hairy code mirrors what's in the saved search loader
+ # in /Elements/ShowSearch
+ if ($_->{type} eq 'saved') {
+ if ($_->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($3);
+ $_->{uid} = \($attr->UID);
+ }
+ # if we can't parse the name, just pass it through
+ }
+ elsif ($_->{type} eq 'system') {
+ my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $_->{name} );
+ unless ( $search && $search->Id ) {
+ my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
+ foreach my $custom (@custom_searches) {
+ if ($custom->Description eq $_->{name}) { $search = $custom; last }
+ }
+ }
+ # if we can't load the search, just pass it through
+ if ($search) {
+ $_->{uid} = \($search->UID);
+ }
+ }
+ # pass through everything else (e.g. component)
+ }
+ }
+ $store{Content} = $self->_SerializeContent($content);
+ }
+ # encode saved searches and dashboards to be UIDs
+ elsif ($store{Name} eq 'Dashboard') {
+ my $content = $self->_DeserializeContent($store{Content}) || {};
+ for my $pane (values %{ $content->{Panes} || {} }) {
+ for (@$pane) {
+ if ($_->{portlet_type} eq 'search' || $_->{portlet_type} eq 'dashboard') {
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($_->{id});
+ $_->{uid} = \($attr->UID);
+ }
+ # pass through everything else (e.g. component)
+ }
+ }
+ $store{Content} = $self->_SerializeContent($content);
+ }
+ # encode subscriptions to have dashboard UID
+ elsif ($store{Name} eq 'Subscription') {
+ my $content = $self->_DeserializeContent($store{Content});
+ my $attr = RT::Attribute->new($self->CurrentUser);
+ $attr->LoadById($content->{DashboardId});
+ $content->{DashboardId} = \($attr->UID);
+ $store{Content} = $self->_SerializeContent($content);
+ }
+
+ return %store;
+}
+
RT::Base->_ImportOverlays();
1;