[Rt-commit] rt branch, 4.4.1/serializer-importer-updates-4.4.1, created. rt-4.4.1-2-g1061f21
Jim Brandt
jbrandt at bestpractical.com
Wed Dec 14 10:13:08 EST 2016
The branch, 4.4.1/serializer-importer-updates-4.4.1 has been created
at 1061f217ffbdba540e3a0b8ad3d3456a72d59fe0 (commit)
- Log -----------------------------------------------------------------
commit 88c5d61ceb1ab03c69e43ed5c7e1374d90487295
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Tue Dec 13 11:50:21 2016 -0500
Apply fixes and improvements from 4.2 work
RT 4.2 had a series of updates applied via merge
564b9de7. Pull them into 4.4.
diff --git a/lib/RT/Attribute.pm b/lib/RT/Attribute.pm
index 9943b57..eddd37e 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -632,6 +632,65 @@ sub FindDependencies {
$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 {
@@ -640,11 +699,220 @@ sub PreInflate {
if ($data->{Object} and ref $data->{Object}) {
my $on_uid = ${ $data->{Object} };
- return if $importer->ShouldSkipTransaction($on_uid);
+
+ # 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;
diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index eb2c3a0..dddfa14 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -1636,13 +1636,12 @@ sub PreInflate {
return;
};
- # Go looking for the pre-existing version of the it
+ # Go looking for the pre-existing version of it
if ($data->{Domain} eq "ACLEquivalence") {
$obj->LoadACLEquivalenceGroup( $data->{Instance} );
return $duplicated->() if $obj->Id;
- # Update the name and description for the new ID
- $data->{Name} = 'User '. $data->{Instance};
+ # Update description for the new ID
$data->{Description} = 'ACL equiv. for user '.$data->{Instance};
} elsif ($data->{Domain} eq "UserDefined") {
$data->{Name} = $importer->Qualify($data->{Name});
diff --git a/lib/RT/Migrate/Importer.pm b/lib/RT/Migrate/Importer.pm
index 7897434..5c25d9a 100644
--- a/lib/RT/Migrate/Importer.pm
+++ b/lib/RT/Migrate/Importer.pm
@@ -65,17 +65,20 @@ sub new {
sub Init {
my $self = shift;
my %args = (
- OriginalId => undef,
- Progress => undef,
- Statefile => undef,
- DumpObjects => undef,
- HandleError => undef,
+ OriginalId => undef,
+ Progress => undef,
+ Statefile => undef,
+ DumpObjects => undef,
+ HandleError => undef,
+ ExcludeOrganization => undef,
@_,
);
# Should we attempt to preserve record IDs as they are created?
$self->{OriginalId} = $args{OriginalId};
+ $self->{ExcludeOrganization} = $args{ExcludeOrganization};
+
$self->{Progress} = $args{Progress};
$self->{HandleError} = sub { 0 };
@@ -179,6 +182,9 @@ sub Resolve {
Field => $ref->{uri},
Value => $self->LookupObj($uid)->URI,
) if defined $ref->{uri};
+ if (my $method = $ref->{method}) {
+ $obj->$method($self, $ref, $class, $id);
+ }
}
delete $self->{Pending}{$uid};
}
@@ -291,6 +297,7 @@ sub Qualify {
my ($string) = @_;
return $string if $self->{Clone};
return $string if not defined $self->{Organization};
+ return $string if $self->{ExcludeOrganization};
return $string if $self->{Organization} eq $RT::Organization;
return $self->{Organization}.": $string";
}
@@ -332,7 +339,7 @@ sub Create {
# Load it back to get real values into the columns
$obj = $class->new( RT->SystemUser );
$obj->Load( $id );
- $obj->PostInflate( $self );
+ $obj->PostInflate( $self, $uid );
return $obj;
}
@@ -399,9 +406,13 @@ sub ReadStream {
# If it's a ticket, we might need to create a
# TicketCustomField for the previous ID
if ($class eq "RT::Ticket" and $self->{OriginalId}) {
+ my $value = $self->{ExcludeOrganization}
+ ? $origid
+ : $self->Organization . ":$origid";
+
my ($id, $msg) = $obj->AddCustomFieldValue(
Field => $self->{OriginalId},
- Value => $self->Organization . ":$origid",
+ Value => $value,
RecordTransaction => 0,
);
warn "Failed to add custom field to $uid: $msg"
diff --git a/lib/RT/Migrate/Importer/File.pm b/lib/RT/Migrate/Importer/File.pm
index cfad9ae..3d36f3d 100644
--- a/lib/RT/Migrate/Importer/File.pm
+++ b/lib/RT/Migrate/Importer/File.pm
@@ -192,7 +192,7 @@ sub SaveState {
NewQueues NewCFs
SkipTransactions Pending Invalid
UIDs
- OriginalId Clone
+ OriginalId ExcludeOrganization Clone
/;
Storable::nstore(\%data, $self->{Statefile});
diff --git a/lib/RT/ObjectClass.pm b/lib/RT/ObjectClass.pm
index c51d9d4..f5a5da6 100644
--- a/lib/RT/ObjectClass.pm
+++ b/lib/RT/ObjectClass.pm
@@ -229,6 +229,18 @@ sub FindDependencies {
$deps->Add( out => $obj );
}
+sub Serialize {
+ my $self = shift;
+ my %args = (@_);
+ my %store = $self->SUPER::Serialize(@_);
+
+ if ($store{ObjectId}) {
+ my $obj = $self->ObjectType->new( RT->SystemUser );
+ $obj->Load( $store{ObjectId} );
+ $store{ObjectId} = \($obj->UID);
+ }
+ return %store;
+}
RT::Base->_ImportOverlays();
diff --git a/lib/RT/ObjectScrip.pm b/lib/RT/ObjectScrip.pm
index 8399398..b270d25 100644
--- a/lib/RT/ObjectScrip.pm
+++ b/lib/RT/ObjectScrip.pm
@@ -272,6 +272,19 @@ sub FindDependencies {
}
}
+sub Serialize {
+ my $self = shift;
+ my %args = (@_);
+ my %store = $self->SUPER::Serialize(@_);
+
+ if ($store{ObjectId}) {
+ my $obj = RT::Queue->new( RT->SystemUser );
+ $obj->Load( $store{ObjectId} );
+ $store{ObjectId} = \($obj->UID);
+ }
+ return %store;
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/ObjectTopic.pm b/lib/RT/ObjectTopic.pm
index c9429fd..dfe2bfd 100644
--- a/lib/RT/ObjectTopic.pm
+++ b/lib/RT/ObjectTopic.pm
@@ -212,6 +212,19 @@ sub FindDependencies {
$deps->Add( out => $obj );
}
+sub Serialize {
+ my $self = shift;
+ my %args = (@_);
+ my %store = $self->SUPER::Serialize(@_);
+
+ if ($store{ObjectId}) {
+ my $obj = $self->ObjectType->new( RT->SystemUser );
+ $obj->Load( $store{ObjectId} );
+ $store{ObjectId} = \($obj->UID);
+ }
+ return %store;
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index 9afdedf..d84197f 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -2059,6 +2059,9 @@ sub Serialize {
my $cf = RT::CustomField->new( RT->SystemUser );
$cf->Load( $store{Field} );
$store{Field} = \($cf->UID);
+
+ $store{OldReference} = \($self->OldReferenceObject->UID) if $self->OldReference;
+ $store{NewReference} = \($self->NewReferenceObject->UID) if $self->NewReference;
} elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
for my $field (qw/OldValue NewValue/) {
my $user = RT::User->new( RT->SystemUser );
diff --git a/sbin/rt-importer.in b/sbin/rt-importer.in
index 6d3cda5..387128e 100644
--- a/sbin/rt-importer.in
+++ b/sbin/rt-importer.in
@@ -95,6 +95,7 @@ GetOptions(
"resume!",
"originalid|i=s",
+ "exclude-organization",
"ask",
"ignore-errors",
@@ -142,11 +143,12 @@ elsif ($OPT{'ignore-errors'}) {
}
my $import = RT::Migrate::Importer::File->new(
- Directory => $dir,
- OriginalId => $OPT{originalid},
- DumpObjects => $OPT{dump},
- Resume => $OPT{resume},
- HandleError => $error_handler,
+ Directory => $dir,
+ OriginalId => $OPT{originalid},
+ ExcludeOrganization => $OPT{'exclude-organization'},
+ DumpObjects => $OPT{dump},
+ Resume => $OPT{resume},
+ HandleError => $error_handler,
);
if ($import->Metadata and -t STDOUT and not $OPT{quiet}) {
@@ -222,6 +224,14 @@ current database; this may include users, queues, and tickets.
It is possible to stop the import process with ^C; it can be later
resumed by re-running the importer.
+Certain records (notably queues and groups) will have their original
+Organization name prepended to them on import. This is primarily to avoid
+duplicate names (for example importing a General queue into an RT that
+already has one would otherwise cause a name collision error). If you are
+confident you won't have any name collisions in queues or groups, you may
+suppress this behavior by passing the B<--exclude-organization> flag to
+C<rt-importer>.
+
=head2 OPTIONS
=over
@@ -236,6 +246,12 @@ Places the original ticket organization and ID into a global custom
field with the given name. If no global ticket custom field with that
name is found in the current database, it will create one.
+=item B<--exclude-organization>
+
+Ordinarily certain records (groups, queues, the B<--originalid> custom field)
+include the organization name of the original RT instance. Use this option to
+suppress that behavior and use the original name directly.
+
=item B<--ask>
Prompt for action when an error occurs inserting a record into the
commit 1061f217ffbdba540e3a0b8ad3d3456a72d59fe0
Author: Jim Brandt <jbrandt at bestpractical.com>
Date: Tue Dec 13 14:26:16 2016 -0500
New feature to serialize selected queues
Changes pulled in from branch 4.2/migrator-additions.
diff --git a/lib/RT/Link.pm b/lib/RT/Link.pm
index 0dadc3b..afbad99 100644
--- a/lib/RT/Link.pm
+++ b/lib/RT/Link.pm
@@ -558,6 +558,27 @@ sub Serialize {
delete $store{LocalBase} if $store{Base};
delete $store{LocalTarget} if $store{Target};
+
+ for my $dir (qw/Base Target/) {
+ my $uri = $self->${\($dir.'URI')};
+ my $object = $self->${\($dir.'Obj')};
+
+ if ($uri->IsLocal) {
+ if ($args{serializer}->Observe(object => $object)) {
+ # no action needed; the object is being migrated
+ }
+ elsif ($args{serializer}{HyperlinkUnmigrated}) {
+ # object is not being migrated; hyperlinkify
+ $store{$dir} = $uri->AsHREF;
+ }
+ else {
+ # object is not being migrated and hyperlinks not desired,
+ # so drop this RT::Link altogether
+ return;
+ }
+ }
+ }
+
return %store;
}
diff --git a/lib/RT/Migrate/Serializer.pm b/lib/RT/Migrate/Serializer.pm
index 381728e..a8f234d 100644
--- a/lib/RT/Migrate/Serializer.pm
+++ b/lib/RT/Migrate/Serializer.pm
@@ -58,6 +58,7 @@ sub cmp_version($$) { RT::Handle::cmp_version($_[0],$_[1]) };
use RT::Migrate::Incremental;
use RT::Migrate::Serializer::IncrementalRecord;
use RT::Migrate::Serializer::IncrementalRecords;
+use List::MoreUtils 'none';
sub Init {
my $self = shift;
@@ -88,6 +89,9 @@ sub Init {
FollowScrips
FollowTickets
FollowACL
+ Queues
+ CustomFields
+ HyperlinkUnmigrated
Clone
Incremental
/;
@@ -251,6 +255,11 @@ sub PushBasics {
OPERATOR => 'IN',
VALUE => [ qw/RT::User RT::Group RT::Queue/ ],
);
+
+ if ($self->{CustomFields}) {
+ $cfs->Limit(FIELD => 'id', OPERATOR => 'IN', VALUE => $self->{CustomFields});
+ }
+
$self->PushObj( $cfs );
# Global attributes
@@ -293,7 +302,14 @@ sub PushBasics {
$self->PushCollections(qw(Topics Classes));
}
- $self->PushCollections(qw(Queues));
+ if ($self->{Queues}) {
+ my $queues = RT::Queues->new(RT->SystemUser);
+ $queues->Limit(FIELD => 'id', OPERATOR => 'IN', VALUE => $self->{Queues});
+ $self->PushObj($queues);
+ }
+ else {
+ $self->PushCollections(qw(Queues));
+ }
}
sub InitStream {
@@ -400,7 +416,25 @@ sub Observe {
my $from = $args{from};
if ($obj->isa("RT::Ticket")) {
return 0 if $obj->Status eq "deleted" and not $self->{FollowDeleted};
+ my $queue = $obj->Queue;
+ return 0 if $self->{Queues} && none { $queue == $_ } @{ $self->{Queues} };
return $self->{FollowTickets};
+ } elsif ($obj->isa("RT::Queue")) {
+ my $id = $obj->Id;
+ return 0 if $self->{Queues} && none { $id == $_ } @{ $self->{Queues} };
+ return 1;
+ } elsif ($obj->isa("RT::CustomField")) {
+ my $id = $obj->Id;
+ return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
+ return 1;
+ } elsif ($obj->isa("RT::ObjectCustomFieldValue")) {
+ my $id = $obj->CustomField;
+ return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
+ return 1;
+ } elsif ($obj->isa("RT::ObjectCustomField")) {
+ my $id = $obj->CustomField;
+ return 0 if $self->{CustomFields} && none { $id == $_ } @{ $self->{CustomFields} };
+ return 1;
} elsif ($obj->isa("RT::ACE")) {
return $self->{FollowACL};
} elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) {
@@ -473,10 +507,13 @@ sub Visit {
\%data,
);
} else {
+ my %serialized = $obj->Serialize(serializer => $self);
+ return unless %serialized;
+
@store = (
ref($obj),
$obj->UID,
- { $obj->Serialize },
+ \%serialized,
);
}
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index d84197f..3d7f12a 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -2080,19 +2080,45 @@ sub Serialize {
if ($store{OldValue}) {
my $base = RT::URI->new( $self->CurrentUser );
$base->FromURI( $store{OldValue} );
- $store{OldValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
+ if ($base->Resolver && (my $object = $base->Object)) {
+ if ($args{serializer}->Observe(object => $object)) {
+ $store{OldValue} = \($object->UID);
+ }
+ elsif ($args{serializer}{HyperlinkUnmigrated}) {
+ $store{OldValue} = $base->AsHREF;
+ }
+ else {
+ $store{OldValue} = "(not migrated)";
+ }
+ }
}
} elsif ($type eq "AddLink") {
if ($store{NewValue}) {
my $base = RT::URI->new( $self->CurrentUser );
$base->FromURI( $store{NewValue} );
- $store{NewValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
+ if ($base->Resolver && (my $object = $base->Object)) {
+ if ($args{serializer}->Observe(object => $object)) {
+ $store{NewValue} = \($object->UID);
+ }
+ elsif ($args{serializer}{HyperlinkUnmigrated}) {
+ $store{NewValue} = $base->AsHREF;
+ }
+ else {
+ $store{NewValue} = "(not migrated)";
+ }
+ }
}
} elsif ($type eq "Set" and $store{Field} eq "Queue") {
for my $field (qw/OldValue NewValue/) {
my $queue = RT::Queue->new( RT->SystemUser );
$queue->Load( $store{$field} );
- $store{$field} = \($queue->UID);
+ if ($args{serializer}->Observe(object => $queue)) {
+ $store{$field} = \($queue->UID);
+ }
+ else {
+ $store{$field} = "$RT::Organization: " . $queue->Name . " (not migrated)";
+
+ }
}
} elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
my $ticket = RT::Ticket->new( RT->SystemUser );
diff --git a/sbin/rt-serializer.in b/sbin/rt-serializer.in
index c008e78..2a9de13 100644
--- a/sbin/rt-serializer.in
+++ b/sbin/rt-serializer.in
@@ -104,6 +104,9 @@ GetOptions(
"scrips!",
"tickets!",
"acls!",
+ "limit-queues=s@",
+ "limit-cfs=s@",
+ "hyperlink-unmigrated!",
"clone",
"incremental",
@@ -127,12 +130,53 @@ $args{FollowScrips} = $OPT{scrips} if defined $OPT{scrips};
$args{FollowTickets} = $OPT{tickets} if defined $OPT{tickets};
$args{FollowACL} = $OPT{acls} if defined $OPT{acls};
+$args{HyperlinkUnmigrated} = $OPT{'hyperlink-unmigrated'} if defined $OPT{'hyperlink-unmigrated'};
+
$args{Clone} = $OPT{clone} if $OPT{clone};
$args{Incremental} = $OPT{incremental} if $OPT{incremental};
$args{GC} = defined $OPT{gc} ? $OPT{gc} : 5000;
$args{Page} = defined $OPT{page} ? $OPT{page} : 100;
+if ($OPT{'limit-queues'}) {
+ my @queue_ids;
+
+ for my $name (split ',', join ',', @{ $OPT{'limit-queues'} }) {
+ $name =~ s/^\s+//; $name =~ s/\s+$//;
+ my $queue = RT::Queue->new(RT->SystemUser);
+ $queue->Load($name);
+ if (!$queue->Id) {
+ die "Unable to load queue '$name'";
+ }
+ push @queue_ids, $queue->Id;
+ }
+
+ $args{Queues} = \@queue_ids;
+}
+
+if ($OPT{'limit-cfs'}) {
+ my @cf_ids;
+
+ for my $name (split ',', join ',', @{ $OPT{'limit-cfs'} }) {
+ $name =~ s/^\s+//; $name =~ s/\s+$//;
+
+ # numeric means id
+ if ($name =~ /^\d+$/) {
+ push @cf_ids, $name;
+ }
+ else {
+ my $cfs = RT::CustomFields->new(RT->SystemUser);
+ $cfs->Limit(FIELD => 'Name', VALUE => $name);
+ if (!$cfs->Count) {
+ die "Unable to load any custom field named '$name'";
+ }
+ push @cf_ids, map { $_->Id } @{ $cfs->ItemsArrayRef };
+ }
+ }
+
+ $args{CustomFields} = \@cf_ids;
+}
+
if (($OPT{clone} or $OPT{incremental})
and grep { /^(users|groups|deleted|scrips|tickets|acls)$/ } keys %OPT) {
die "You cannot specify object types when cloning.\n\nPlease see $0 --help.\n";
@@ -321,6 +365,24 @@ serialized.
Skip serialization of all ticket data.
+=item B<--limit-queues>
+
+Takes a list of queue IDs or names separated by commas. When provided, only
+that set of queues (and the tickets in them) will be serialized.
+
+=item B<--limit-cfs>
+
+Takes a list of custom field IDs or names separated by commas. When provided,
+only that set of custom fields will be serialized.
+
+=item B<--hyperlink-unmigrated>
+
+Replace links to local records which are not being migrated with hyperlinks.
+The hyperlinks will use the serializing RT's configured URL.
+
+Without this option, such links are instead dropped, and transactions which
+had updated such links will be replaced with an explanatory message.
+
=item B<--clone>
Serializes your entire database, creating a clone. This option should
-----------------------------------------------------------------------
More information about the rt-commit
mailing list