[Rt-commit] rt branch, 4.4/dump-and-merge-initialdata, created. rt-4.4.3-140-ge63b7e438
? sunnavy
sunnavy at bestpractical.com
Mon Aug 6 15:54:20 EDT 2018
The branch, 4.4/dump-and-merge-initialdata has been created
at e63b7e43856a1cdf58ce7819ed42c59dddf46539 (commit)
- Log -----------------------------------------------------------------
commit 819a7b31e108b7ab6dd636a3452420a12c32e694
Author: sunnavy <sunnavy at bestpractical.com>
Date: Fri Aug 3 02:06:23 2018 +0800
Fix typo to make @Assets really work
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index eaf3d76e3..cba68a240 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1141,7 +1141,7 @@ sub InsertData {
if ( @Assets ) {
$RT::Logger->debug("Creating Assets...");
- for my $item (@Catalogs) {
+ for my $item (@Assets) {
my $new_entry = RT::Asset->new(RT->SystemUser);
my ( $return, $msg ) = $new_entry->Create(%$item);
unless ( $return ) {
commit 88e2980396c7372c8682db616c263961be18da7c
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 16:11:03 2017 +0000
Add support for asset and catalog attributes for initialdata
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index cba68a240..009a98161 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1126,6 +1126,7 @@ sub InsertData {
$RT::Logger->debug("Creating Catalogs...");
for my $item (@Catalogs) {
+ my $attributes = delete $item->{ Attributes };
my $new_entry = RT::Catalog->new(RT->SystemUser);
my ( $return, $msg ) = $new_entry->Create(%$item);
unless ( $return ) {
@@ -1134,6 +1135,9 @@ sub InsertData {
else {
$RT::Logger->debug( $return ."." );
}
+
+ $_->{Object} = $new_entry for @{$attributes || []};
+ push @Attributes, @{$attributes || []};
}
$RT::Logger->debug("done.");
@@ -1142,6 +1146,7 @@ sub InsertData {
$RT::Logger->debug("Creating Assets...");
for my $item (@Assets) {
+ my $attributes = delete $item->{ Attributes };
my $new_entry = RT::Asset->new(RT->SystemUser);
my ( $return, $msg ) = $new_entry->Create(%$item);
unless ( $return ) {
@@ -1150,6 +1155,9 @@ sub InsertData {
else {
$RT::Logger->debug( $return ."." );
}
+
+ $_->{Object} = $new_entry for @{$attributes || []};
+ push @Attributes, @{$attributes || []};
}
$RT::Logger->debug("done.");
commit 8e0dd77bebe2fdaf9f937dfed9f96badd8f4d02e
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 16:21:42 2017 +0000
Add support for OCFVs in initialdata
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 009a98161..fb43fc577 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -853,10 +853,10 @@ sub InsertData {
# Slurp in stuff to insert from the datafile. Possible things to go in here:-
our (@Groups, @Users, @Members, @ACL, @Queues, @Classes, @ScripActions, @ScripConditions,
@Templates, @CustomFields, @CustomRoles, @Scrips, @Attributes, @Initial, @Final,
- @Catalogs, @Assets);
+ @Catalogs, @Assets, @OCFVs);
local (@Groups, @Users, @Members, @ACL, @Queues, @Classes, @ScripActions, @ScripConditions,
@Templates, @CustomFields, @CustomRoles, @Scrips, @Attributes, @Initial, @Final,
- @Catalogs, @Assets);
+ @Catalogs, @Assets, @OCFVs);
local $@;
@@ -907,6 +907,7 @@ sub InsertData {
Final => \@Final,
Catalogs => \@Catalogs,
Assets => \@Assets,
+ OCFVs => \@OCFVs,
},
) or return (0, "Couldn't load data from '$datafile' for import:\n\nERROR:" . $@);
}
@@ -930,6 +931,8 @@ sub InsertData {
$RT::Logger->debug("Creating groups...");
foreach my $item (@Groups) {
my $attributes = delete $item->{ Attributes };
+ my $ocfvs = delete $item->{ CustomFields };
+
my $new_entry = RT::Group->new( RT->SystemUser );
$item->{'Domain'} ||= 'UserDefined';
my $member_of = delete $item->{'MemberOf'};
@@ -942,6 +945,8 @@ sub InsertData {
$RT::Logger->debug($return .".");
$_->{Object} = $new_entry for @{$attributes || []};
push @Attributes, @{$attributes || []};
+ $_->{Object} = $new_entry for @{$ocfvs || []};
+ push @OCFVs, @{$ocfvs || []};
}
if ( $member_of ) {
$member_of = [ $member_of ] unless ref $member_of eq 'ARRAY';
@@ -991,6 +996,7 @@ sub InsertData {
$item->{'Password'} = $root_password;
}
my $attributes = delete $item->{ Attributes };
+ my $ocfvs = delete $item->{ CustomFields };
no warnings 'redefine';
local *RT::User::CanonicalizeUserInfo = sub { 1 }
@@ -1004,6 +1010,8 @@ sub InsertData {
$RT::Logger->debug( $return ."." );
$_->{Object} = $new_entry for @{$attributes || []};
push @Attributes, @{$attributes || []};
+ $_->{Object} = $new_entry for @{$ocfvs || []};
+ push @OCFVs, @{$ocfvs || []};
}
if ( $member_of ) {
$member_of = [ $member_of ] unless ref $member_of eq 'ARRAY';
@@ -1070,6 +1078,8 @@ sub InsertData {
$RT::Logger->debug("Creating queues...");
for my $item (@Queues) {
my $attributes = delete $item->{ Attributes };
+ my $ocfvs = delete $item->{ CustomFields };
+
my $new_entry = RT::Queue->new(RT->SystemUser);
my ( $return, $msg ) = $new_entry->Create(%$item);
unless ( $return ) {
@@ -1078,6 +1088,8 @@ sub InsertData {
$RT::Logger->debug( $return ."." );
$_->{Object} = $new_entry for @{$attributes || []};
push @Attributes, @{$attributes || []};
+ $_->{Object} = $new_entry for @{$ocfvs || []};
+ push @OCFVs, @{$ocfvs || []};
}
}
$RT::Logger->debug("done.");
@@ -1147,6 +1159,8 @@ sub InsertData {
for my $item (@Assets) {
my $attributes = delete $item->{ Attributes };
+ my $ocfvs = delete $item->{ CustomFields };
+
my $new_entry = RT::Asset->new(RT->SystemUser);
my ( $return, $msg ) = $new_entry->Create(%$item);
unless ( $return ) {
@@ -1158,6 +1172,8 @@ sub InsertData {
$_->{Object} = $new_entry for @{$attributes || []};
push @Attributes, @{$attributes || []};
+ $_->{Object} = $new_entry for @{$ocfvs || []};
+ push @OCFVs, @{$ocfvs || []};
}
$RT::Logger->debug("done.");
@@ -1446,6 +1462,31 @@ sub InsertData {
}
$RT::Logger->debug("done.");
}
+
+ if ( @OCFVs ) {
+ $RT::Logger->debug("Creating ObjectCustomFieldValues...");
+
+ for my $item (@OCFVs) {
+ my $obj = delete $item->{Object};
+
+ if ( ref $obj eq 'CODE' ) {
+ $obj = $obj->();
+ }
+
+ $item->{Field} = delete $item->{CustomField} if $item->{CustomField};
+ $item->{Value} = delete $item->{Content} if $item->{Content};
+
+ my ( $return, $msg ) = $obj->AddCustomFieldValue (%$item);
+ unless ( $return ) {
+ $RT::Logger->error( $msg );
+ }
+ else {
+ $RT::Logger->debug( $return ."." );
+ }
+ }
+ $RT::Logger->debug("done.");
+ }
+
if ( @Attributes ) {
$RT::Logger->debug("Creating attributes...");
my $sys = RT::System->new(RT->SystemUser);
commit 6dbe329359f8f80389b509061d6ade6ab91b3d73
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:26:50 2017 +0000
Insert @Members later, after queues, assets, custom roles etc have been created
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index fb43fc577..bd18b9b52 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1047,33 +1047,6 @@ sub InsertData {
}
$RT::Logger->debug("done.");
}
- if ( @Members ) {
- $RT::Logger->debug("Adding users and groups to groups...");
- for my $item (@Members) {
- my $group = RT::Group->new(RT->SystemUser);
- $group->LoadUserDefinedGroup( delete $item->{Group} );
- unless ($group->Id) {
- RT->Logger->error("Unable to find group '$group' to add members to");
- next;
- }
-
- my $class = delete $item->{Class} || 'RT::User';
- my $member = $class->new( RT->SystemUser );
- $item->{Domain} = 'UserDefined' if $member->isa("RT::Group");
- $member->LoadByCols( %$item );
- unless ($member->Id) {
- RT->Logger->error("Unable to find $class '".($item->{id} || $item->{Name})."' to add to ".$group->Name);
- next;
- }
-
- my ( $return, $msg) = $group->AddMember( $member->PrincipalObj->Id );
- unless ( $return ) {
- $RT::Logger->error( $msg );
- } else {
- $RT::Logger->debug( $return ."." );
- }
- }
- }
if ( @Queues ) {
$RT::Logger->debug("Creating queues...");
for my $item (@Queues) {
@@ -1296,6 +1269,34 @@ sub InsertData {
$RT::Logger->debug("done.");
}
+ if ( @Members ) {
+ $RT::Logger->debug("Adding users and groups to groups...");
+ for my $item (@Members) {
+ my $group = RT::Group->new(RT->SystemUser);
+ $group->LoadUserDefinedGroup( delete $item->{Group} );
+ unless ($group->Id) {
+ RT->Logger->error("Unable to find group '$group' to add members to");
+ next;
+ }
+
+ my $class = delete $item->{Class} || 'RT::User';
+ my $member = $class->new( RT->SystemUser );
+ $item->{Domain} = 'UserDefined' if $member->isa("RT::Group");
+ $member->LoadByCols( %$item );
+ unless ($member->Id) {
+ RT->Logger->error("Unable to find $class '".($item->{id} || $item->{Name})."' to add to ".$group->Name);
+ next;
+ }
+
+ my ( $return, $msg) = $group->AddMember( $member->PrincipalObj->Id );
+ unless ( $return ) {
+ $RT::Logger->error( $msg );
+ } else {
+ $RT::Logger->debug( $return ."." );
+ }
+ }
+ }
+
if ( @ACL ) {
$RT::Logger->debug("Creating ACL...");
for my $item (@ACL) {
commit b913d8ec6f140ad6dd248528f56ad50915c71ac3
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 20:18:32 2017 +0000
Support all the role group domains for @ACL
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index bd18b9b52..53cb36ea1 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1348,9 +1348,7 @@ sub InsertData {
$princ->LoadSystemInternalGroup( $item->{'GroupType'} );
} elsif ( $item->{'GroupDomain'} eq 'RT::System-Role' ) {
$princ->LoadRoleGroup( Object => RT->System, Name => $item->{'GroupType'} );
- } elsif ( $item->{'GroupDomain'} eq 'RT::Queue-Role' &&
- $item->{'Queue'} )
- {
+ } elsif ( $item->{'GroupDomain'} =~ /-Role$/ ) {
$princ->LoadRoleGroup( Object => $object, Name => $item->{'GroupType'} );
} else {
$princ->Load( $item->{'GroupId'} );
commit cd9a04b073b78ccfbd1cdcc230a7a4eaa3220aba
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 21:05:59 2017 +0000
initialdata support for @Articles
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 53cb36ea1..907475135 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -853,10 +853,10 @@ sub InsertData {
# Slurp in stuff to insert from the datafile. Possible things to go in here:-
our (@Groups, @Users, @Members, @ACL, @Queues, @Classes, @ScripActions, @ScripConditions,
@Templates, @CustomFields, @CustomRoles, @Scrips, @Attributes, @Initial, @Final,
- @Catalogs, @Assets, @OCFVs);
+ @Catalogs, @Assets, @Articles, @OCFVs);
local (@Groups, @Users, @Members, @ACL, @Queues, @Classes, @ScripActions, @ScripConditions,
@Templates, @CustomFields, @CustomRoles, @Scrips, @Attributes, @Initial, @Final,
- @Catalogs, @Assets, @OCFVs);
+ @Catalogs, @Assets, @Articles, @OCFVs);
local $@;
@@ -907,6 +907,7 @@ sub InsertData {
Final => \@Final,
Catalogs => \@Catalogs,
Assets => \@Assets,
+ Articles => \@Articles,
OCFVs => \@OCFVs,
},
) or return (0, "Couldn't load data from '$datafile' for import:\n\nERROR:" . $@);
@@ -1152,6 +1153,31 @@ sub InsertData {
$RT::Logger->debug("done.");
}
+ if ( @Articles ) {
+ $RT::Logger->debug("Creating Articles...");
+
+ for my $item (@Articles) {
+ my $attributes = delete $item->{ Attributes };
+ my $ocfvs = delete $item->{ CustomFields };
+
+ my $new_entry = RT::Article->new(RT->SystemUser);
+ my ( $return, $msg ) = $new_entry->Create(%$item);
+ unless ( $return ) {
+ $RT::Logger->error( $msg );
+ }
+ else {
+ $RT::Logger->debug( $return ."." );
+ }
+
+ $_->{Object} = $new_entry for @{$attributes || []};
+ push @Attributes, @{$attributes || []};
+ $_->{Object} = $new_entry for @{$ocfvs || []};
+ push @OCFVs, @{$ocfvs || []};
+ }
+
+ $RT::Logger->debug("done.");
+ }
+
if ( @CustomFields ) {
$RT::Logger->debug("Creating custom fields...");
commit 6a5e96997f384baeebe72c2e09954fe7e28b9a06
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 22:17:10 2017 +0000
Defer setting BasedOn for CFs later in the @CustomFields list
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 907475135..eb60569c0 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1181,6 +1181,8 @@ sub InsertData {
if ( @CustomFields ) {
$RT::Logger->debug("Creating custom fields...");
+ my @deferred_BasedOn;
+
for my $item ( @CustomFields ) {
my $attributes = delete $item->{ Attributes };
my $new_entry = RT::CustomField->new( RT->SystemUser );
@@ -1208,8 +1210,7 @@ sub InsertData {
if ($ok) {
$item->{'BasedOn'} = $basedon->Id;
} else {
- $RT::Logger->error("Unable to load $item->{BasedOn} as a $item->{LookupType} CF. Skipping BasedOn: $msg");
- delete $item->{'BasedOn'};
+ push @deferred_BasedOn, [$new_entry, delete $item->{'BasedOn'}];
}
} else {
$RT::Logger->error("Unable to load CF $item->{BasedOn} because no LookupType was specified. Skipping BasedOn");
@@ -1263,6 +1264,23 @@ sub InsertData {
push @Attributes, @{$attributes || []};
}
+ for ( @deferred_BasedOn ) {
+ my ($cf, $name) = @$_;
+ my $basedon = RT::CustomField->new($RT::SystemUser);
+ my ($ok, $msg ) = $basedon->LoadByCols(
+ Name => $name,
+ LookupType => $cf->LookupType,
+ Disabled => 0,
+ );
+ if ($ok) {
+ ($ok, $msg) = $cf->SetBasedOn($basedon->Id);
+ $RT::Logger->error("Unable to set $name as a " . $cf->LookupType . " BasedOn CF: $msg") if !$ok;
+ }
+ else {
+ $RT::Logger->error("Unable to load $name as a " . $cf->LookupType . " CF. Skipping BasedOn: $msg");
+ }
+ }
+
$RT::Logger->debug("done.");
}
commit 1de0a488e8a9316e00dc64a806a529038dbfe98b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 21:33:54 2017 +0000
Handle ObjectScrip Stage in initialdata
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index eb60569c0..36a6da923 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1495,11 +1495,14 @@ sub InsertData {
$RT::Logger->debug( $return ."." );
}
foreach my $q ( @queues ) {
- my ($return, $msg) = $new_entry->AddToObject(
- ObjectId => $q,
- Stage => $item->{'Stage'},
+ my %args = (
+ Stage => $item->{'Stage'},
+ (ref($q) ? %$q : (ObjectId => $q)),
);
- $RT::Logger->error( "Couldn't apply scrip to $q: $msg" )
+
+ my ($return, $msg) = $new_entry->AddToObject(%args);
+
+ $RT::Logger->error( "Couldn't apply scrip to $args{ObjectId}: $msg" )
unless $return;
}
}
commit ee902438a4aa6699011bb0af5c6deb573b4f1d3b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:53:01 2017 +0000
Handle passing a hash with Stage as first param to Queue
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 36a6da923..ca207ef73 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1486,7 +1486,18 @@ sub InsertData {
my @queues = ref $item->{'Queue'} eq 'ARRAY'? @{ $item->{'Queue'} }: $item->{'Queue'} || 0;
push @queues, 0 unless @queues; # add global queue at least
- my ( $return, $msg ) = $new_entry->Create( %$item, Queue => shift @queues );
+ my %args = %$item;
+ $args{Queue} = shift @queues;
+ if (ref($args{Queue})) {
+ # transform ScripObject->Create API into Scrip->Create API
+ $args{Queue}{Queue} = delete $args{Queue}{ObjectId};
+ %args = (
+ %args,
+ %{ $args{Queue} },
+ );
+ }
+
+ my ( $return, $msg ) = $new_entry->Create(%args);
unless ( $return ) {
$RT::Logger->error( $msg );
next;
commit c21aab9bdbc7e60fb74382812404fa1f21bd0c7c
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 00:31:27 2017 +0000
Fixing up ObjectScrip sort order
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index ca207ef73..207a3fece 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1491,6 +1491,7 @@ sub InsertData {
if (ref($args{Queue})) {
# transform ScripObject->Create API into Scrip->Create API
$args{Queue}{Queue} = delete $args{Queue}{ObjectId};
+ $args{Queue}{ObjectSortOrder} = delete $args{Queue}{SortOrder};
%args = (
%args,
%{ $args{Queue} },
diff --git a/lib/RT/Scrip.pm b/lib/RT/Scrip.pm
index 10f0605c2..e015bb39a 100644
--- a/lib/RT/Scrip.pm
+++ b/lib/RT/Scrip.pm
@@ -200,9 +200,10 @@ sub Create {
return ( $id, $msg ) unless $id;
(my $status, $msg) = RT::ObjectScrip->new( $self->CurrentUser )->Add(
- Scrip => $self,
- Stage => $args{'Stage'},
- ObjectId => $args{'Queue'},
+ Scrip => $self,
+ Stage => $args{'Stage'},
+ ObjectId => $args{'Queue'},
+ SortOrder => $args{'ObjectSortOrder'},
);
$RT::Logger->error( "Couldn't add scrip: $msg" ) unless $status;
commit 33edad4e6b591cc9219b34676b73f702d30d8c06
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:36:12 2017 +0000
Support GroupDomain for item in @Members
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 207a3fece..1bb840750 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1316,10 +1316,16 @@ sub InsertData {
if ( @Members ) {
$RT::Logger->debug("Adding users and groups to groups...");
for my $item (@Members) {
+ my $name = delete $item->{Group};
+ my $domain = delete $item->{GroupDomain} || 'UserDefined';
+
my $group = RT::Group->new(RT->SystemUser);
- $group->LoadUserDefinedGroup( delete $item->{Group} );
+ $group->LoadByCols(
+ Name => $name,
+ Domain => $domain,
+ );
unless ($group->Id) {
- RT->Logger->error("Unable to find group '$group' to add members to");
+ RT->Logger->error("Unable to find $domain group '$name' to add members to");
next;
}
commit b9eab03870a4faaaef5a3407f55794bd01d057f0
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 01:09:44 2017 +0000
NoAutoGlobal option for @Scrips
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 1bb840750..9595da012 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1489,8 +1489,16 @@ sub InsertData {
for my $item (@Scrips) {
my $new_entry = RT::Scrip->new(RT->SystemUser);
- my @queues = ref $item->{'Queue'} eq 'ARRAY'? @{ $item->{'Queue'} }: $item->{'Queue'} || 0;
- push @queues, 0 unless @queues; # add global queue at least
+ my @queues = ref $item->{'Queue'} eq 'ARRAY'
+ ? @{ $item->{'Queue'} }
+ : ($item->{'Queue'})
+ if $item->{'Queue'};
+
+ if (!@queues) {
+ push @queues, 0 unless $item->{'NoAutoGlobal'};
+ }
+
+ my $remove_global = (delete $item->{'NoAutoGlobal'}) && !@queues;
my %args = %$item;
$args{Queue} = shift @queues;
@@ -1512,6 +1520,13 @@ sub InsertData {
else {
$RT::Logger->debug( $return ."." );
}
+
+ if ($remove_global) {
+ my ($return, $msg) = $new_entry->RemoveFromObject(ObjectId => 0);
+ $RT::Logger->error( "Couldn't unapply scrip globally: $msg" )
+ unless $return;
+ }
+
foreach my $q ( @queues ) {
my %args = (
Stage => $item->{'Stage'},
commit 984db79697971c74c6a558e7564df6d653182c5e
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 20:00:29 2017 +0000
Support import of queue watcher groups
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 9595da012..9c4542d70 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1318,11 +1318,24 @@ sub InsertData {
for my $item (@Members) {
my $name = delete $item->{Group};
my $domain = delete $item->{GroupDomain} || 'UserDefined';
+ my $instance = delete $item->{GroupInstance};
+
+ if ($domain =~ /^(.+)-Role$/) {
+ my $class = $1;
+ if (!$class->DOES("RT::Record::Role::Roles")) {
+ RT->Logger->error("Invalid group domain '$domain' for group $name; skipping adding membership");
+ next;
+ }
+ my $object = $class->new(RT->SystemUser);
+ $object->Load($instance);
+ $instance = $object->Id;
+ }
my $group = RT::Group->new(RT->SystemUser);
$group->LoadByCols(
Name => $name,
Domain => $domain,
+ (defined $instance ? (Instance => $instance) : ()),
);
unless ($group->Id) {
RT->Logger->error("Unable to find $domain group '$name' to add members to");
commit 05d21ca3a33f8cb9f29b8bbec1a0d521b32dc053
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:04:54 2017 +0000
Handle explicit ApplyTo => 0 to make a CF which is global
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 9c4542d70..7ec75feab 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1232,22 +1232,25 @@ sub InsertData {
my $class = $new_entry->RecordClassFromLookupType;
if ($class) {
- if ($new_entry->IsOnlyGlobal and $apply_to) {
- $RT::Logger->warn("ApplyTo provided for global custom field ".$new_entry->Name );
- undef $apply_to;
- }
- if ( !$apply_to ) {
- # Apply to all by default
+ $apply_to = [ $apply_to ] unless ref $apply_to;
+ for my $name ( @{ $apply_to } ) {
my $ocf = RT::ObjectCustomField->new(RT->SystemUser);
- ( $return, $msg) = $ocf->Create( CustomField => $new_entry->Id );
- $RT::Logger->error( $msg ) unless $return and $ocf->Id;
- } else {
- $apply_to = [ $apply_to ] unless ref $apply_to;
- for my $name ( @{ $apply_to } ) {
+
+ # global CF
+ if (!$name) {
+ ( $return, $msg ) = $ocf->Create(
+ CustomField => $new_entry->Id,
+ );
+ $RT::Logger->error( $msg ) unless $return and $ocf->Id;
+ }
+ else {
+ if ($new_entry->IsOnlyGlobal) {
+ $RT::Logger->warn("ApplyTo '$name' provided for global custom field ".$new_entry->Name );
+ }
+
my $obj = $class->new(RT->SystemUser);
$obj->Load($name);
if ( $obj->Id ) {
- my $ocf = RT::ObjectCustomField->new(RT->SystemUser);
( $return, $msg ) = $ocf->Create(
CustomField => $new_entry->Id,
ObjectId => $obj->Id,
commit c92316ec9f5995d90510f50e788653afce8922c8
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:33:16 2017 +0000
Handle RightName in initialdata
This is what RT::ACE calls it
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 7ec75feab..d9f3d5419 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1432,6 +1432,8 @@ sub InsertData {
}
}
+ $item->{Right} = delete $item->{RightName} if $item->{RightName};
+
# Grant it
my @rights = ref($item->{'Right'}) eq 'ARRAY' ? @{$item->{'Right'}} : $item->{'Right'};
foreach my $right ( @rights ) {
commit c84eb72e7adbcf121fb0bd3aa54b7ab7a678cf78
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 01:12:36 2017 +0000
Handle global Class records more consistently in initialdata
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index d9f3d5419..585102593 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1077,19 +1077,21 @@ sub InsertData {
$item->{'ApplyTo'} = delete $item->{'Queue'};
}
- my $apply_to = delete $item->{'ApplyTo'};
+ my $apply_to = (delete $item->{'ApplyTo'}) || 0;
+ $apply_to = [ $apply_to ] unless ref $apply_to;
+
my $new_entry = RT::Class->new(RT->SystemUser);
my ( $return, $msg ) = $new_entry->Create(%$item);
unless ( $return ) {
$RT::Logger->error( $msg );
} else {
$RT::Logger->debug( $return ."." );
- if ( !$apply_to ) {
- ( $return, $msg) = $new_entry->AddToObject( RT::Queue->new(RT->SystemUser) );
- $RT::Logger->error( $msg ) unless $return;
- } else {
- $apply_to = [ $apply_to ] unless ref $apply_to;
- for my $name ( @{ $apply_to } ) {
+
+ for my $name ( @{ $apply_to } ) {
+ if ( !$name ) {
+ ( $return, $msg) = $new_entry->AddToObject( RT::Queue->new(RT->SystemUser) );
+ $RT::Logger->error( $msg ) unless $return;
+ } else {
my $queue = RT::Queue->new( RT->SystemUser );
$queue->Load( $name );
if ( $queue->id ) {
commit a713a7b5a9d5b3b6c79c5f987665359450984510
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 20:12:21 2017 +0000
Correctly load ACLs granted on user-defined groups
diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm
index 585102593..3d2dcd14b 100644
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@ -1390,6 +1390,14 @@ sub InsertData {
RT->Logger->error("Unable to load queue ".$item->{Queue}.": $msg");
next;
}
+ } elsif ( $item->{'Group'} || ($item->{ObjectType}||'') eq 'RT::Group') {
+ my $name = $item->{'Group'} || $item->{ObjectId};
+ $object = RT::Group->new(RT->SystemUser);
+ my ($ok, $msg) = $object->LoadUserDefinedGroup($name);
+ unless ( $ok ) {
+ RT->Logger->error("Unable to load user-defined group $name: $msg");
+ next;
+ }
} elsif ( $item->{ObjectType} and $item->{ObjectId}) {
$object = $item->{ObjectType}->new(RT->SystemUser);
my ($ok, $msg) = $object->Load( $item->{ObjectId} );
commit db9ee856c65b64a98cd78b59ed5337099f53a908
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 19:26:43 2017 +0000
Avoid creating duplicate SystemInternal and RT::Role-System groups
We've seen copies of the SystemInternal groups in the wild, which causes
symptoms such as privileged users being forced into self-service
initialdata is one avenue by which this could happen, as there was no
validation previously
diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm
index 25823ab47..ed7137907 100644
--- a/lib/RT/Group.pm
+++ b/lib/RT/Group.pm
@@ -305,10 +305,18 @@ sub _Create {
@_
);
- # Enforce uniqueness on user defined group names
- if ($args{'Domain'} and $args{'Domain'} eq 'UserDefined') {
- my ($ok, $msg) = $self->_ValidateUserDefinedName($args{'Name'});
- return ($ok, $msg) if not $ok;
+ if ($args{'Domain'}) {
+ # Enforce uniqueness on user defined group names
+ if ($args{'Domain'} eq 'UserDefined') {
+ my ($ok, $msg) = $self->_ValidateUserDefinedName($args{'Name'});
+ return ($ok, $msg) if not $ok;
+ }
+
+ # Enforce uniqueness on SystemInternal and system role groups
+ if ($args{'Domain'} eq 'SystemInternal' || $args{'Domain'} eq 'RT::System-Role') {
+ my ($ok, $msg) = $self->_ValidateNameForDomain($args{'Name'}, $args{'Domain'});
+ return ($ok, $msg) if not $ok;
+ }
}
$RT::Handle->BeginTransaction() unless ($args{'InsideTransaction'});
@@ -398,25 +406,42 @@ sub ValidateName {
return $self->SUPER::ValidateName($value);
}
-=head2 _ValidateUserDefinedName VALUE
+=head2 _ValidateNameForDomain VALUE DOMAIN
-Returns true if the user defined group name isn't in use, false otherwise.
+Returns true if the group name isn't in use in the same domain, false otherwise.
=cut
-sub _ValidateUserDefinedName {
- my ($self, $value) = @_;
+sub _ValidateNameForDomain {
+ my ($self, $value, $domain) = @_;
return (0, 'Name is required') unless length $value;
my $dupcheck = RT::Group->new(RT->SystemUser);
- $dupcheck->LoadUserDefinedGroup($value);
+ if ($domain eq 'UserDefined') {
+ $dupcheck->LoadUserDefinedGroup($value);
+ }
+ else {
+ $dupcheck->LoadByCols(Domain => $domain, Name => $value);
+ }
if ( $dupcheck->id && ( !$self->id || $self->id != $dupcheck->id ) ) {
return ( 0, $self->loc( "Group name '[_1]' is already in use", $value ) );
}
return 1;
}
+=head2 _ValidateUserDefinedName VALUE
+
+Returns true if the user defined group name isn't in use, false otherwise.
+
+=cut
+
+sub _ValidateUserDefinedName {
+ my ($self, $value) = @_;
+
+ return $self->_ValidateNameForDomain($value, 'UserDefined');
+}
+
=head2 _CreateACLEquivalenceGroup { Principal }
A helper subroutine which creates a group containing only
commit 41d321072967de442fb5ebe21b173f939823ea89
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 21:52:41 2017 +0000
Allow passing SortOrder to CustomField->Create
diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 603021423..9c148ebfb 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -265,6 +265,7 @@ sub Create {
Pattern => '',
Description => '',
Disabled => 0,
+ SortOrder => 0,
LookupType => '',
LinkValueTo => '',
IncludeContentForValue => '',
@@ -361,6 +362,7 @@ sub Create {
ValuesClass => $args{'ValuesClass'},
Description => $args{'Description'},
Disabled => $args{'Disabled'},
+ SortOrder => $args{'SortOrder'},
LookupType => $args{'LookupType'},
UniqueValues => $args{'UniqueValues'},
CanonicalizeClass => $args{'CanonicalizeClass'},
commit 0ae4036e4ae91027accfb7b38e245886cd8b6a47
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 17:49:25 2017 +0000
Avoid undef warnings when checking a new queue for name uniqueness
This adopts the same syntactical pattern as RT::Group
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index b9eaec302..0c8a7cdba 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -280,7 +280,7 @@ sub _ValidateName {
$tempqueue->Load($name);
#If this queue exists, return undef
- if ( $tempqueue->Name() && $tempqueue->id != $self->id) {
+ if ( $tempqueue->Name() && ( !$self->id || $tempqueue->id != $self->id ) ) {
return (undef, $self->loc("Queue already exists") );
}
commit 5896c8acc0c2e81a04e29d94e63fd0624d5c5eb1
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 20:09:56 2017 +0000
When serializing an ACE, make sure its user is too
Without this we may serialize an ACE and a user's ACLEquivalence group,
but not the user itself
diff --git a/lib/RT/ACE.pm b/lib/RT/ACE.pm
index 04a6c5003..0fcae1f4a 100644
--- a/lib/RT/ACE.pm
+++ b/lib/RT/ACE.pm
@@ -801,6 +801,11 @@ sub FindDependencies {
$self->SUPER::FindDependencies($walker, $deps);
$deps->Add( out => $self->PrincipalObj->Object );
+
+ if ($self->PrincipalObj->Object->Domain eq 'ACLEquivalence') {
+ $deps->Add( out => $self->PrincipalObj->Object->InstanceObj );
+ }
+
$deps->Add( out => $self->Object );
}
commit 8308c70e16db24da90671a9eb89d875145c25946
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 20:37:31 2017 +0000
Make sure queue-specific templates get serialized
diff --git a/lib/RT/ObjectScrip.pm b/lib/RT/ObjectScrip.pm
index bfb769a25..ef3a89635 100644
--- a/lib/RT/ObjectScrip.pm
+++ b/lib/RT/ObjectScrip.pm
@@ -269,6 +269,8 @@ sub FindDependencies {
my $obj = RT::Queue->new( $self->CurrentUser );
$obj->Load( $self->ObjectId );
$deps->Add( out => $obj );
+
+ $deps->Add( out => $self->ScripObj->TemplateObj($obj->Id) );
}
}
commit dbb9fd8519cfbc05e71650a8bb3cf9225171fa74
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 20:58:07 2017 +0000
Skip tickets ASAP as an optimization
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index 0c8a7cdba..6d74f842a 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -1074,11 +1074,13 @@ sub FindDependencies {
VALUE => 'RT::Queue-' );
$deps->Add( in => $objs );
- # Tickets
- $objs = RT::Tickets->new( $self->CurrentUser );
- $objs->Limit( FIELD => "Queue", VALUE => $self->Id );
- $objs->{allow_deleted_search} = 1;
- $deps->Add( in => $objs );
+ # Tickets (skipped early as an optimization)
+ if ($walker->{FollowTickets} || !defined($walker->{FollowTickets})) {
+ $objs = RT::Tickets->new( $self->CurrentUser );
+ $objs->Limit( FIELD => "Queue", VALUE => $self->Id );
+ $objs->{allow_deleted_search} = 1;
+ $deps->Add( in => $objs );
+ }
# Object Custom Roles
$objs = RT::ObjectCustomRoles->new( $self->CurrentUser );
commit 01e4fdd195707ea3ccc037d000f837c331f400de
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 19:15:08 2017 +0000
Migrate Storable-related concerns from Serializer to File
diff --git a/lib/RT/Migrate/Serializer.pm b/lib/RT/Migrate/Serializer.pm
index fa30c63c2..50a27b94e 100644
--- a/lib/RT/Migrate/Serializer.pm
+++ b/lib/RT/Migrate/Serializer.pm
@@ -53,7 +53,6 @@ use warnings;
use base 'RT::DependencyWalker';
-use Storable qw//;
sub cmp_version($$) { RT::Handle::cmp_version($_[0],$_[1]) };
use RT::Migrate::Incremental;
use RT::Migrate::Serializer::IncrementalRecord;
@@ -327,11 +326,8 @@ sub PushBasics {
sub InitStream {
my $self = shift;
- # Write the initial metadata
my $meta = $self->Metadata;
- $! = 0;
- Storable::nstore_fd( $meta, $self->{Filehandle} );
- die "Failed to write metadata: $!" if $!;
+ $self->WriteMetadata($meta);
return unless cmp_version($meta->{VersionFrom}, $meta->{Version}) < 0;
@@ -501,9 +497,7 @@ sub Visit {
if ($self->{Transform}{"+$class"}) {
my @extra = $self->{Transform}{"+$class"}->(\%data,\$class);
for my $e (@extra) {
- $! = 0;
- Storable::nstore_fd($e, $self->{Filehandle});
- die "Failed to write: $!" if $!;
+ $self->WriteRecord($e);
$self->{ObjectCount}{$e->[0]}++;
}
}
@@ -532,11 +526,7 @@ sub Visit {
);
}
- # Write it out; nstore_fd doesn't trap failures to write, so we have
- # to; by clearing $! and checking it afterwards.
- $! = 0;
- Storable::nstore_fd(\@store, $self->{Filehandle});
- die "Failed to write: $!" if $!;
+ $self->WriteRecord(\@store);
$self->{ObjectCount}{$store[0]}++;
}
diff --git a/lib/RT/Migrate/Serializer/File.pm b/lib/RT/Migrate/Serializer/File.pm
index 1afc6c6d9..53715eded 100644
--- a/lib/RT/Migrate/Serializer/File.pm
+++ b/lib/RT/Migrate/Serializer/File.pm
@@ -51,6 +51,8 @@ package RT::Migrate::Serializer::File;
use strict;
use warnings;
+use Storable qw//;
+
use base 'RT::Migrate::Serializer';
sub Init {
@@ -168,4 +170,23 @@ sub RotateFile {
$self->OpenFile;
}
+sub WriteMetadata {
+ my $self = shift;
+ my $meta = shift;
+ $! = 0;
+ Storable::nstore_fd( $meta, $self->{Filehandle} );
+ die "Failed to write metadata: $!" if $!;
+}
+
+sub WriteRecord {
+ my $self = shift;
+ my $record = shift;
+
+ # Write it out; nstore_fd doesn't trap failures to write, so we have
+ # to; by clearing $! and checking it afterwards.
+ $! = 0;
+ Storable::nstore_fd($record, $self->{Filehandle});
+ die "Failed to write: $!" if $!;
+}
+
1;
commit 522fcee95f92f88f237d5fb6bb2b88aec8a5ab72
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 19:33:38 2017 +0000
First pass at initialdata.json serializer export format
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
new file mode 100644
index 000000000..71964968b
--- /dev/null
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -0,0 +1,176 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# 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.
+#
+# 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.
+#
+# 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
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (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.)
+#
+# 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.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Migrate::Serializer::JSON;
+
+use strict;
+use warnings;
+use JSON qw//;
+
+use base 'RT::Migrate::Serializer';
+
+sub Init {
+ my $self = shift;
+
+ my %args = (
+ Directory => undef,
+ Force => undef,
+
+ @_,
+ );
+
+ # Set up the output directory we'll be writing to
+ my ($y,$m,$d) = (localtime)[5,4,3];
+ $args{Directory} = $RT::Organization .
+ sprintf(":%d-%02d-%02d",$y+1900,$m+1,$d)
+ unless defined $args{Directory};
+ system("rm", "-rf", $args{Directory}) if $args{Force};
+ die "Output directory $args{Directory} already exists"
+ if -d $args{Directory};
+ mkdir $args{Directory}
+ or die "Can't create output directory $args{Directory}: $!\n";
+ $self->{Directory} = delete $args{Directory};
+
+ $self->{Records} = {};
+
+ $self->SUPER::Init(@_);
+}
+
+sub Export {
+ my $self = shift;
+
+ # Write the initial metadata
+ $self->InitStream;
+
+ # Walk the objects
+ $self->Walk( @_ );
+
+ # Set up our output file
+ $self->OpenFile;
+
+ # Write out the initialdata
+ $self->WriteFile;
+
+ # Close everything back up
+ $self->CloseFile;
+
+ return $self->ObjectCount;
+}
+
+sub Files {
+ my $self = shift;
+ return ($self->Filename);
+}
+
+sub Filename {
+ my $self = shift;
+ return sprintf(
+ "%s/initialdata.json",
+ $self->{Directory},
+ );
+}
+
+sub Directory {
+ my $self = shift;
+ return $self->{Directory};
+}
+
+sub JSON {
+ my $self = shift;
+ return $self->{JSON} ||= JSON->new->pretty;
+}
+
+sub OpenFile {
+ my $self = shift;
+ open($self->{Filehandle}, ">", $self->Filename)
+ or die "Can't write to file @{[$self->Filename]}: $!";
+}
+
+sub CloseFile {
+ my $self = shift;
+ close($self->{Filehandle})
+ or die "Can't close @{[$self->Filename]}: $!";
+}
+
+sub WriteMetadata {
+ my $self = shift;
+ my $meta = shift;
+
+ # no need to write metadata
+ return;
+}
+
+sub WriteRecord {
+ my $self = shift;
+ my $record = shift;
+
+ my $type = $record->[0];
+ $type =~ s/^RT:://;
+
+ push @{ $self->{Records}{ $type } }, $record->[2];
+}
+
+sub WriteFile {
+ my $self = shift;
+ my %output;
+
+ for my $type (keys %{ $self->{Records} }) {
+ for my $record (@{ $self->{Records}{$type} }) {
+ for my $key (keys %$record) {
+ if (ref($record->{$key}) eq 'SCALAR') {
+ delete $record->{$key};
+ }
+ }
+ push @{ $output{$type} }, $record;
+ }
+ }
+
+ print { $self->{Filehandle} } $self->JSON->encode(\%output);
+}
+
+1;
commit 66df5d7d897faba40635ab12a204b73fcafb6f21
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 19:58:42 2017 +0000
Sort JSON keys so its output can be versioned
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 71964968b..ae5cb9228 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -122,7 +122,7 @@ sub Directory {
sub JSON {
my $self = shift;
- return $self->{JSON} ||= JSON->new->pretty;
+ return $self->{JSON} ||= JSON->new->pretty->canonical;
}
sub OpenFile {
commit d6a0f6f8a983c830d2ab3333d949841db75f3826
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 20:00:21 2017 +0000
Store records by id
This way we can more easily patch them up later (e.g. canonicalizing
ObjectScrips to Scrips.Queue)
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index ae5cb9228..f5e711bd5 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -152,7 +152,7 @@ sub WriteRecord {
my $type = $record->[0];
$type =~ s/^RT:://;
- push @{ $self->{Records}{ $type } }, $record->[2];
+ $self->{Records}{ $type }{$record->[1]} = $record->[2];
}
sub WriteFile {
@@ -160,7 +160,8 @@ sub WriteFile {
my %output;
for my $type (keys %{ $self->{Records} }) {
- for my $record (@{ $self->{Records}{$type} }) {
+ for my $id (keys %{ $self->{Records}{$type} }) {
+ my $record = $self->{Records}{$type}{$id};
for my $key (keys %$record) {
if (ref($record->{$key}) eq 'SCALAR') {
delete $record->{$key};
commit 6f7163d21bcf62ecae1f7abcd8f803b7f51ee75a
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 20:01:11 2017 +0000
For now, rather than deleting references, dereference them
It's not perfect, but it's better baseline behavior
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index f5e711bd5..6f5d595e7 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -164,7 +164,7 @@ sub WriteFile {
my $record = $self->{Records}{$type}{$id};
for my $key (keys %$record) {
if (ref($record->{$key}) eq 'SCALAR') {
- delete $record->{$key};
+ $record->{$key} = ${ $record->{$key} };
}
}
push @{ $output{$type} }, $record;
commit 5a009e56eefd4f0098eedd85216748e41921a92b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 20:01:50 2017 +0000
Use initialdata's type names rather than RT class names
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 6f5d595e7..4612fa329 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -155,19 +155,27 @@ sub WriteRecord {
$self->{Records}{ $type }{$record->[1]} = $record->[2];
}
+my %initialdataType = (
+ ACE => 'ACL',
+ Class => 'Classes',
+ GroupMember => 'Members',
+);
+
sub WriteFile {
my $self = shift;
my %output;
- for my $type (keys %{ $self->{Records} }) {
- for my $id (keys %{ $self->{Records}{$type} }) {
- my $record = $self->{Records}{$type}{$id};
+ for my $intype (keys %{ $self->{Records} }) {
+ my $outtype = $initialdataType{$intype} || ($intype . 's');
+
+ for my $id (keys %{ $self->{Records}{$intype} }) {
+ my $record = $self->{Records}{$intype}{$id};
for my $key (keys %$record) {
if (ref($record->{$key}) eq 'SCALAR') {
$record->{$key} = ${ $record->{$key} };
}
}
- push @{ $output{$type} }, $record;
+ push @{ $output{$outtype} }, $record;
}
}
commit 56f3d4b97c1b5d1cc88b93f654bcab4ca2f6fc11
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 20:15:43 2017 +0000
Defer canonicalizing away "RT::" from class names
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 4612fa329..64b0d3d20 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -149,10 +149,7 @@ sub WriteRecord {
my $self = shift;
my $record = shift;
- my $type = $record->[0];
- $type =~ s/^RT:://;
-
- $self->{Records}{ $type }{$record->[1]} = $record->[2];
+ $self->{Records}{ $record->[0] }{ $record->[1] } = $record->[2];
}
my %initialdataType = (
@@ -166,7 +163,9 @@ sub WriteFile {
my %output;
for my $intype (keys %{ $self->{Records} }) {
- my $outtype = $initialdataType{$intype} || ($intype . 's');
+ my $outtype = $intype;
+ $outtype =~ s/^RT:://;
+ $outtype = $initialdataType{$outtype} || ($outtype . 's');
for my $id (keys %{ $self->{Records}{$intype} }) {
my $record = $self->{Records}{$intype}{$id};
commit 68a8ac9ca78ee5a0f9bbef493d9c3b18b9c6e1a2
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 20:15:57 2017 +0000
Canonicalize references by name
e.g. "Queue: General" instead of "Queue: RT::Queue-example.com-1"
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 64b0d3d20..836fb2b28 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -158,6 +158,17 @@ my %initialdataType = (
GroupMember => 'Members',
);
+sub CanonicalizeReference {
+ my $self = shift;
+ my $ref = ${ shift(@_) };
+ my $context = shift;
+
+ my ($class, $id) = $ref =~ /^(.*?)-(.*)/
+ or return $ref;
+ my $record = $self->{Records}{$class}{$ref};
+ return $record->{Name} || $ref;
+}
+
sub WriteFile {
my $self = shift;
my %output;
@@ -171,7 +182,7 @@ sub WriteFile {
my $record = $self->{Records}{$intype}{$id};
for my $key (keys %$record) {
if (ref($record->{$key}) eq 'SCALAR') {
- $record->{$key} = ${ $record->{$key} };
+ $record->{$key} = $self->CanonicalizeReference($record->{$key}, $record);
}
}
push @{ $output{$outtype} }, $record;
commit 66db10a455312094bf8d9dafc97d36326eba2844
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 20:49:34 2017 +0000
Canonicalize OCFs as CF.ApplyTo
We need to take some steps to preserve SortOrder, but initialdata
ApplyTo doesn't support SortOrder directly
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 836fb2b28..d92622fd1 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -159,9 +159,10 @@ my %initialdataType = (
);
sub CanonicalizeReference {
- my $self = shift;
- my $ref = ${ shift(@_) };
+ my $self = shift;
+ my $ref = ${ shift(@_) };
my $context = shift;
+ my $for_key = shift;
my ($class, $id) = $ref =~ /^(.*?)-(.*)/
or return $ref;
@@ -169,10 +170,27 @@ sub CanonicalizeReference {
return $record->{Name} || $ref;
}
+sub CanonicalizeObjects {
+ my $self = shift;
+
+ if (my $OCFs = delete $self->{Records}{'RT::ObjectCustomField'}) {
+ for my $OCF (values %$OCFs) {
+ my $CF = $self->{Records}{'RT::CustomField'}{ ${ $OCF->{CustomField} } };
+ push @{ $CF->{ApplyTo} }, $OCF;
+ }
+
+ for my $CF (values %{ $self->{Records}{'RT::CustomField'} }) {
+ @{ $CF->{ApplyTo} } = map { $_->{ObjectId} } sort { $a->{SortOrder} <=> $b->{SortOrder} } @{ $CF->{ApplyTo} || [] };
+ }
+ }
+}
+
sub WriteFile {
my $self = shift;
my %output;
+ $self->CanonicalizeObjects;
+
for my $intype (keys %{ $self->{Records} }) {
my $outtype = $intype;
$outtype =~ s/^RT:://;
@@ -182,7 +200,7 @@ sub WriteFile {
my $record = $self->{Records}{$intype}{$id};
for my $key (keys %$record) {
if (ref($record->{$key}) eq 'SCALAR') {
- $record->{$key} = $self->CanonicalizeReference($record->{$key}, $record);
+ $record->{$key} = $self->CanonicalizeReference($record->{$key}, $record, $key);
}
}
push @{ $output{$outtype} }, $record;
commit f89c2196a856899e75c479367f6bc5f2996ac3fb
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 20:58:37 2017 +0000
Canonicalize ObjectClass into Class.ApplyTo
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index d92622fd1..963c6ed4e 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -170,21 +170,32 @@ sub CanonicalizeReference {
return $record->{Name} || $ref;
}
-sub CanonicalizeObjects {
+sub _CanonicalizeObjectType {
my $self = shift;
-
- if (my $OCFs = delete $self->{Records}{'RT::ObjectCustomField'}) {
- for my $OCF (values %$OCFs) {
- my $CF = $self->{Records}{'RT::CustomField'}{ ${ $OCF->{CustomField} } };
- push @{ $CF->{ApplyTo} }, $OCF;
+ my $object_class = shift;
+ my $object_primary_ref = shift;
+ my $primary_class = shift;
+
+ if (my $objects = delete $self->{Records}{$object_class}) {
+ for my $object (values %$objects) {
+ my $primary = $self->{Records}{$primary_class}{ ${ $object->{$object_primary_ref} } };
+ push @{ $primary->{ApplyTo} }, $object;
}
- for my $CF (values %{ $self->{Records}{'RT::CustomField'} }) {
- @{ $CF->{ApplyTo} } = map { $_->{ObjectId} } sort { $a->{SortOrder} <=> $b->{SortOrder} } @{ $CF->{ApplyTo} || [] };
+ for my $primary (values %{ $self->{Records}{$primary_class} }) {
+ @{ $primary->{ApplyTo} } = map { $_->{ObjectId} }
+ sort { $a->{SortOrder} <=> $b->{SortOrder} }
+ @{ $primary->{ApplyTo} || [] };
}
}
}
+sub CanonicalizeObjects {
+ my $self = shift;
+ $self->_CanonicalizeObjectType('RT::ObjectCustomField' => CustomField => 'RT::CustomField');
+ $self->_CanonicalizeObjectType('RT::ObjectClass' => Class => 'RT::Class');
+}
+
sub WriteFile {
my $self = shift;
my %output;
commit ab6de43c9b2ae5598e6c0a530a343cf86a80121b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 21:21:53 2017 +0000
Factor out _GetRecordByRef
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 963c6ed4e..f436e5a1c 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -158,15 +158,24 @@ my %initialdataType = (
GroupMember => 'Members',
);
+sub _GetRecordByRef {
+ my $self = shift;
+ my $ref = shift;
+
+ my ($class) = $ref =~ /^([\w:]+)-/
+ or return undef;
+ return $self->{Records}{$class}{$ref};
+}
+
sub CanonicalizeReference {
my $self = shift;
my $ref = ${ shift(@_) };
my $context = shift;
my $for_key = shift;
- my ($class, $id) = $ref =~ /^(.*?)-(.*)/
+ my $record = $self->_GetRecordByRef($ref)
or return $ref;
- my $record = $self->{Records}{$class}{$ref};
+
return $record->{Name} || $ref;
}
commit 6024bcc9b548ffb00b790ca5965e58c2a6d28681
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 21:22:28 2017 +0000
Canonicalize ObjectScrips as Scrips.Queue
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index f436e5a1c..89cfa7eda 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -179,30 +179,64 @@ sub CanonicalizeReference {
return $record->{Name} || $ref;
}
-sub _CanonicalizeObjectType {
+sub _CanonicalizeManyToMany {
my $self = shift;
- my $object_class = shift;
- my $object_primary_ref = shift;
- my $primary_class = shift;
+ my %args = (
+ object_class => '',
+ object_primary_ref => '',
+ primary_class => '',
+ primary_key => 'ApplyTo',
+ canonicalize_object => sub { $_->{ObjectId} },
+ @_,
+ );
+
+ my $object_class = $args{object_class};
+ my $object_primary_ref = $args{object_primary_ref};
+ my $primary_class = $args{primary_class};
+ my $primary_key = $args{primary_key};
+ my $canonicalize_object = $args{canonicalize_object};
if (my $objects = delete $self->{Records}{$object_class}) {
for my $object (values %$objects) {
my $primary = $self->{Records}{$primary_class}{ ${ $object->{$object_primary_ref} } };
- push @{ $primary->{ApplyTo} }, $object;
+ push @{ $primary->{$primary_key} }, $object;
}
for my $primary (values %{ $self->{Records}{$primary_class} }) {
- @{ $primary->{ApplyTo} } = map { $_->{ObjectId} }
- sort { $a->{SortOrder} <=> $b->{SortOrder} }
- @{ $primary->{ApplyTo} || [] };
+ @{ $primary->{$primary_key} }
+ = map &$canonicalize_object,
+ sort { $a->{SortOrder} <=> $b->{SortOrder} }
+ @{ $primary->{$primary_key} || [] };
}
}
}
sub CanonicalizeObjects {
my $self = shift;
- $self->_CanonicalizeObjectType('RT::ObjectCustomField' => CustomField => 'RT::CustomField');
- $self->_CanonicalizeObjectType('RT::ObjectClass' => Class => 'RT::Class');
+
+ $self->_CanonicalizeManyToMany(
+ object_class => 'RT::ObjectCustomField',
+ object_primary_ref => 'CustomField',
+ primary_class => 'RT::CustomField',
+ );
+
+ $self->_CanonicalizeManyToMany(
+ object_class => 'RT::ObjectClass',
+ object_primary_ref => 'Class',
+ primary_class => 'RT::Class'
+ );
+
+ $self->_CanonicalizeManyToMany(
+ object_class => 'RT::ObjectScrip',
+ object_primary_ref => 'Scrip',
+ primary_class => 'RT::Scrip',
+ primary_key => 'Queue',
+ canonicalize_object => sub {
+ ref($_->{ObjectId})
+ ? $self->_GetRecordByRef(${ $_->{ObjectId} })->{Name}
+ : $_->{ObjectId}
+ },
+ );
}
sub WriteFile {
commit fe0e99f43e93c003dc1d4ffbca46397ff91a8b1b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 21:33:54 2017 +0000
Handle ObjectScrip Stage in serializer importer
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 89cfa7eda..9f8343369 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -232,9 +232,10 @@ sub CanonicalizeObjects {
primary_class => 'RT::Scrip',
primary_key => 'Queue',
canonicalize_object => sub {
- ref($_->{ObjectId})
+ my $object = ref($_->{ObjectId})
? $self->_GetRecordByRef(${ $_->{ObjectId} })->{Name}
- : $_->{ObjectId}
+ : $_->{ObjectId};
+ return { ObjectId => $object, Stage => $_->{Stage} };
},
);
}
commit e350e59b7c19b17dde50226cea57c9bf37205954
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 21:54:07 2017 +0000
Canonicalize ObjectCustomRole as CustomRole.ApplyTo
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 9f8343369..d5e207cd7 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -223,7 +223,18 @@ sub CanonicalizeObjects {
$self->_CanonicalizeManyToMany(
object_class => 'RT::ObjectClass',
object_primary_ref => 'Class',
- primary_class => 'RT::Class'
+ primary_class => 'RT::Class',
+ );
+
+ $self->_CanonicalizeManyToMany(
+ object_class => 'RT::ObjectCustomRole',
+ object_primary_ref => 'CustomRole',
+ primary_class => 'RT::CustomRole',
+ canonicalize_object => sub {
+ ref($_->{ObjectId})
+ ? $self->_GetRecordByRef(${ $_->{ObjectId} })->{Name}
+ : $_->{ObjectId};
+ },
);
$self->_CanonicalizeManyToMany(
commit 8f17953f01558ae34739e89632f7609a54d9a4b8
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 17:57:34 2017 +0000
Avoid serializing attributes (for now)
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index d5e207cd7..105f1d044 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -257,6 +257,8 @@ sub WriteFile {
$self->CanonicalizeObjects;
+ delete $self->{Records}{'RT::Attribute'};
+
for my $intype (keys %{ $self->{Records} }) {
my $outtype = $intype;
$outtype =~ s/^RT:://;
commit d33b6ad9e198381142d39154f0a439916393e716
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 17:37:13 2017 +0000
Only serialize user-defined groups for initialdata
ACLEquivalence, role groups, etc will be created automatically
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 105f1d044..1dcf01faa 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -120,6 +120,23 @@ sub Directory {
return $self->{Directory};
}
+sub Observe {
+ my $self = shift;
+ my %args = @_;
+
+ my $obj = $args{object};
+
+ # avoid serializing ACLEquivalence, etc
+ if ($obj->isa("RT::Group")) {
+ return 0 unless $obj->Domain eq 'UserDefined';
+ }
+ if ($obj->isa("RT::GroupMember")) {
+ return 0 unless $obj->GroupObj->Object->Domain eq 'UserDefined';
+ }
+
+ return $self->SUPER::Observe(%args);
+}
+
sub JSON {
my $self = shift;
return $self->{JSON} ||= JSON->new->pretty->canonical;
commit 7cb1f345943d4bcfb675bcb4c59b7f90000a0fa2
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:23:49 2017 +0000
Have _GetRecordByRef dereference if needed
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 1dcf01faa..e417f5a8a 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -179,6 +179,7 @@ sub _GetRecordByRef {
my $self = shift;
my $ref = shift;
+ $ref = $$ref if ref($ref) eq 'SCALAR';
my ($class) = $ref =~ /^([\w:]+)-/
or return undef;
return $self->{Records}{$class}{$ref};
@@ -249,7 +250,7 @@ sub CanonicalizeObjects {
primary_class => 'RT::CustomRole',
canonicalize_object => sub {
ref($_->{ObjectId})
- ? $self->_GetRecordByRef(${ $_->{ObjectId} })->{Name}
+ ? $self->_GetRecordByRef($_->{ObjectId})->{Name}
: $_->{ObjectId};
},
);
@@ -261,7 +262,7 @@ sub CanonicalizeObjects {
primary_key => 'Queue',
canonicalize_object => sub {
my $object = ref($_->{ObjectId})
- ? $self->_GetRecordByRef(${ $_->{ObjectId} })->{Name}
+ ? $self->_GetRecordByRef($_->{ObjectId})->{Name}
: $_->{ObjectId};
return { ObjectId => $object, Stage => $_->{Stage} };
},
commit c40f76ca17d0e8ca5cb777ab9ce195163dbbf06b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:26:10 2017 +0000
Try harder to load references
Even if it's not in the serialized output (perhaps due to being
excluded) we can still inspect such objects because we have their class
and id, and the database
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index e417f5a8a..7fc4d4f55 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -180,9 +180,17 @@ sub _GetRecordByRef {
my $ref = shift;
$ref = $$ref if ref($ref) eq 'SCALAR';
- my ($class) = $ref =~ /^([\w:]+)-/
+
+ return RT->System if $ref eq 'RT::System';
+
+ my ($class, $id) = $ref =~ /^([\w:]+)-.*-(\d+)$/
or return undef;
- return $self->{Records}{$class}{$ref};
+
+ return $self->{Records}{$class}{$ref} || do {
+ my $obj = $class->new(RT->SystemUser);
+ $obj->Load($id);
+ $obj;
+ };
}
sub CanonicalizeReference {
commit 45243d74afb3085e98969cbd4b99c24025d2b6c7
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:33:32 2017 +0000
Canonicalize ACLs the way initialdata needs them
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 7fc4d4f55..ce46e3894 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -237,6 +237,37 @@ sub _CanonicalizeManyToMany {
}
}
+sub CanonicalizeACLs {
+ my $self = shift;
+
+ for my $ace (values %{ $self->{Records}{'RT::ACE'} }) {
+ my $principal = $self->_GetRecordByRef(delete $ace->{PrincipalId});
+ my $object = $self->_GetRecordByRef(delete $ace->{Object});
+
+ if ($principal->IsGroup) {
+ my $domain = $principal->Object->Domain;
+ if ($domain eq 'ACLEquivalence') {
+ $ace->{UserId} = $principal->Object->InstanceObj->Name;
+ }
+ else {
+ $ace->{GroupDomain} = $domain;
+ if ($domain eq 'SystemInternal') {
+ $ace->{GroupType} = $principal->Object->Name;
+ }
+ elsif ($domain eq 'RT::Queue-Role') {
+ $ace->{Queue} = $principal->Object->Instance;
+ }
+ }
+ }
+ else {
+ $ace->{UserId} = $principal->Object->Name;
+ }
+
+ $ace->{ObjectType} = ref($object);
+ $ace->{ObjectId} = $object->Id;
+ }
+}
+
sub CanonicalizeObjects {
my $self = shift;
@@ -282,6 +313,7 @@ sub WriteFile {
my %output;
$self->CanonicalizeObjects;
+ $self->CanonicalizeACLs;
delete $self->{Records}{'RT::Attribute'};
commit 3db856210d67e9013fe48322250aa08fe8ac696b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:42:07 2017 +0000
Disambiguate _GetSerializedByRef from _GetObjectByRef
The consuming API is different (serialized hashref vs live object)
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index ce46e3894..faa2892a1 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -175,7 +175,7 @@ my %initialdataType = (
GroupMember => 'Members',
);
-sub _GetRecordByRef {
+sub _GetObjectByRef {
my $self = shift;
my $ref = shift;
@@ -186,11 +186,21 @@ sub _GetRecordByRef {
my ($class, $id) = $ref =~ /^([\w:]+)-.*-(\d+)$/
or return undef;
- return $self->{Records}{$class}{$ref} || do {
- my $obj = $class->new(RT->SystemUser);
- $obj->Load($id);
- $obj;
- };
+ my $obj = $class->new(RT->SystemUser);
+ $obj->Load($id);
+ return $obj;
+}
+
+sub _GetSerializedByRef {
+ my $self = shift;
+ my $ref = shift;
+
+ $ref = $$ref if ref($ref) eq 'SCALAR';
+
+ my ($class) = $ref =~ /^([\w:]+)-/
+ or return undef;
+
+ return $self->{Records}{$class}{$ref};
}
sub CanonicalizeReference {
@@ -199,7 +209,7 @@ sub CanonicalizeReference {
my $context = shift;
my $for_key = shift;
- my $record = $self->_GetRecordByRef($ref)
+ my $record = $self->_GetSerializedByRef($ref)
or return $ref;
return $record->{Name} || $ref;
@@ -241,8 +251,8 @@ sub CanonicalizeACLs {
my $self = shift;
for my $ace (values %{ $self->{Records}{'RT::ACE'} }) {
- my $principal = $self->_GetRecordByRef(delete $ace->{PrincipalId});
- my $object = $self->_GetRecordByRef(delete $ace->{Object});
+ my $principal = $self->_GetObjectByRef(delete $ace->{PrincipalId});
+ my $object = $self->_GetObjectByRef(delete $ace->{Object});
if ($principal->IsGroup) {
my $domain = $principal->Object->Domain;
@@ -289,7 +299,7 @@ sub CanonicalizeObjects {
primary_class => 'RT::CustomRole',
canonicalize_object => sub {
ref($_->{ObjectId})
- ? $self->_GetRecordByRef($_->{ObjectId})->{Name}
+ ? $self->_GetSerializedByRef($_->{ObjectId})->{Name}
: $_->{ObjectId};
},
);
@@ -301,7 +311,7 @@ sub CanonicalizeObjects {
primary_key => 'Queue',
canonicalize_object => sub {
my $object = ref($_->{ObjectId})
- ? $self->_GetRecordByRef($_->{ObjectId})->{Name}
+ ? $self->_GetSerializedByRef($_->{ObjectId})->{Name}
: $_->{ObjectId};
return { ObjectId => $object, Stage => $_->{Stage} };
},
commit 30f78bcbfcf903e95465d1a3a74920bd5fd9b507
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:47:28 2017 +0000
Handle systemuser
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index faa2892a1..e1c3bb992 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -182,9 +182,10 @@ sub _GetObjectByRef {
$ref = $$ref if ref($ref) eq 'SCALAR';
return RT->System if $ref eq 'RT::System';
+ return RT->SystemUser if $ref eq 'RT::User-RT_System';
my ($class, $id) = $ref =~ /^([\w:]+)-.*-(\d+)$/
- or return undef;
+ or do { warn "Unable to canonicalize ref '$ref'"; return undef };
my $obj = $class->new(RT->SystemUser);
$obj->Load($id);
commit 6ebadd606e7d1ac9d6ef68ee7bed245b92e2bf2c
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:47:34 2017 +0000
Creator and LastUpdatedBy are always user ids
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index e1c3bb992..c33c2e3fd 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -210,6 +210,10 @@ sub CanonicalizeReference {
my $context = shift;
my $for_key = shift;
+ if ($for_key eq 'Creator' || $for_key eq 'LastUpdatedBy') {
+ return $self->_GetObjectByRef($ref)->Id;
+ }
+
my $record = $self->_GetSerializedByRef($ref)
or return $ref;
commit 2b8d4b9cc288458cbf2344119b78b4f371de61f1
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 18:47:53 2017 +0000
Avoid uniqueness violations (for now) by skipping id from initialdata output
This will likely be added in to handle updating existing records
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index c33c2e3fd..0d3a3555b 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -344,6 +344,7 @@ sub WriteFile {
$record->{$key} = $self->CanonicalizeReference($record->{$key}, $record, $key);
}
}
+ delete $record->{id};
push @{ $output{$outtype} }, $record;
}
}
commit 67663b9500c120e6c4fdba1c0fb9e870869fc41e
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 19:01:41 2017 +0000
CanonicalizeUsers
Skip principal fields, add Privileged
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 0d3a3555b..b93478735 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -283,6 +283,20 @@ sub CanonicalizeACLs {
}
}
+sub CanonicalizeUsers {
+ my $self = shift;
+
+ for my $user (values %{ $self->{Records}{'RT::User'} }) {
+ delete $user->{Principal};
+ delete $user->{PrincipalId};
+
+ my $object = RT::User->new(RT->SystemUser);
+ $object->Load($user->{id});
+
+ $user->{Privileged} = $object->Privileged ? JSON::true : JSON::false;
+ }
+}
+
sub CanonicalizeObjects {
my $self = shift;
@@ -329,6 +343,7 @@ sub WriteFile {
$self->CanonicalizeObjects;
$self->CanonicalizeACLs;
+ $self->CanonicalizeUsers;
delete $self->{Records}{'RT::Attribute'};
commit 6f166613e974a80737cfc68a0f14ddeeb3c7926a
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 21:19:01 2017 +0000
Fix serialized OCFs for queues
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index b93478735..a9dee2133 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -301,9 +301,14 @@ sub CanonicalizeObjects {
my $self = shift;
$self->_CanonicalizeManyToMany(
- object_class => 'RT::ObjectCustomField',
- object_primary_ref => 'CustomField',
- primary_class => 'RT::CustomField',
+ object_class => 'RT::ObjectCustomField',
+ object_primary_ref => 'CustomField',
+ primary_class => 'RT::CustomField',
+ canonicalize_object => sub {
+ ref($_->{ObjectId})
+ ? $self->_GetSerializedByRef($_->{ObjectId})->{Name}
+ : $_->{ObjectId};
+ },
);
$self->_CanonicalizeManyToMany(
commit 6bb58b4f926ceca864d5edc4f7471d8ffb66720c
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 21:30:44 2017 +0000
Roundtrip CFVs
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index a9dee2133..5d5bcdd5b 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -311,6 +311,18 @@ sub CanonicalizeObjects {
},
);
+ $self->_CanonicalizeManyToMany(
+ object_class => 'RT::CustomFieldValue',
+ object_primary_ref => 'CustomField',
+ primary_class => 'RT::CustomField',
+ primary_key => 'Values',
+ canonicalize_object => sub {
+ my %object = %$_;
+ delete @object{qw/id CustomField Created LastUpdated Creator LastUpdatedBy/};
+ return \%object;
+ },
+ );
+
$self->_CanonicalizeManyToMany(
object_class => 'RT::ObjectClass',
object_primary_ref => 'Class',
commit ab3d464965a4e620679c0a28a9b443ff9ca05d56
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 22:07:02 2017 +0000
Roundtrip group members
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 5d5bcdd5b..213d76fd8 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -297,6 +297,21 @@ sub CanonicalizeUsers {
}
}
+sub CanonicalizeGroupMembers {
+ my $self = shift;
+
+ for my $record (values %{ $self->{Records}{'RT::GroupMember'} }) {
+ my $group = $self->_GetObjectByRef(delete $record->{GroupId});
+ $record->{Group} = $group->Object->Name;
+
+ my $member = $self->_GetObjectByRef(delete $record->{MemberId});
+ $record->{Class} = ref($member->Object);
+ $record->{Name} = $member->Object->Name;
+
+ delete @$record{qw/Creator Created LastUpdated LastUpdatedBy/};
+ }
+}
+
sub CanonicalizeObjects {
my $self = shift;
@@ -361,6 +376,7 @@ sub WriteFile {
$self->CanonicalizeObjects;
$self->CanonicalizeACLs;
$self->CanonicalizeUsers;
+ $self->CanonicalizeGroupMembers;
delete $self->{Records}{'RT::Attribute'};
commit 7e21d67a7dd814693fa5a76e7b717a75911f4de1
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 00:31:27 2017 +0000
Fixing up ObjectScrip sort order
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 213d76fd8..928dd97b1 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -361,10 +361,11 @@ sub CanonicalizeObjects {
primary_class => 'RT::Scrip',
primary_key => 'Queue',
canonicalize_object => sub {
- my $object = ref($_->{ObjectId})
- ? $self->_GetSerializedByRef($_->{ObjectId})->{Name}
- : $_->{ObjectId};
- return { ObjectId => $object, Stage => $_->{Stage} };
+ my %object = %$_;
+ delete @object{qw/id Scrip Created LastUpdated Creator LastUpdatedBy/};
+ $object{ObjectId} = $self->_GetSerializedByRef($object{ObjectId})->{Name}
+ if $object{ObjectId}; # 0 meaning Global can stay 0
+ return \%object;
},
);
}
commit 98b5f0bc117054af62b9aaa984c840fcf2b0e7e3
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 01:09:44 2017 +0000
Round-trip unapplied scrips using a NoAutoGlobal key
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 928dd97b1..1bfc38d14 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -137,6 +137,15 @@ sub Observe {
return $self->SUPER::Observe(%args);
}
+sub PushBasics {
+ my $self = shift;
+ $self->SUPER::PushBasics(@_);
+
+ # we want to include all CFs, scrips, etc, not just the reachable ones
+ $self->PushCollections(qw(CustomFields CustomRoles));
+ $self->PushCollections(qw(Scrips)) if $self->{FollowScrips};
+}
+
sub JSON {
my $self = shift;
return $self->{JSON} ||= JSON->new->pretty->canonical;
@@ -227,6 +236,7 @@ sub _CanonicalizeManyToMany {
object_primary_ref => '',
primary_class => '',
primary_key => 'ApplyTo',
+ add_to_primary => undef,
canonicalize_object => sub { $_->{ObjectId} },
@_,
);
@@ -235,6 +245,7 @@ sub _CanonicalizeManyToMany {
my $object_primary_ref = $args{object_primary_ref};
my $primary_class = $args{primary_class};
my $primary_key = $args{primary_key};
+ my %add_to_primary = %{ $args{add_to_primary} || {} };
my $canonicalize_object = $args{canonicalize_object};
if (my $objects = delete $self->{Records}{$object_class}) {
@@ -248,6 +259,8 @@ sub _CanonicalizeManyToMany {
= map &$canonicalize_object,
sort { $a->{SortOrder} <=> $b->{SortOrder} }
@{ $primary->{$primary_key} || [] };
+
+ %$primary = (%$primary, %add_to_primary);
}
}
}
@@ -356,10 +369,11 @@ sub CanonicalizeObjects {
);
$self->_CanonicalizeManyToMany(
- object_class => 'RT::ObjectScrip',
- object_primary_ref => 'Scrip',
- primary_class => 'RT::Scrip',
- primary_key => 'Queue',
+ object_class => 'RT::ObjectScrip',
+ object_primary_ref => 'Scrip',
+ primary_class => 'RT::Scrip',
+ primary_key => 'Queue',
+ add_to_primary => { NoAutoGlobal => 1 },
canonicalize_object => sub {
my %object = %$_;
delete @object{qw/id Scrip Created LastUpdated Creator LastUpdatedBy/};
commit ce4bb242ff62d702dc365e72cb1db3f0c274dc6d
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 16:21:51 2017 +0000
Export OCFVs as CustomFields key on object for initialdata
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 1bfc38d14..48bf3fbd3 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -325,6 +325,26 @@ sub CanonicalizeGroupMembers {
}
}
+sub CanonicalizeObjectCustomFieldValues {
+ my $self = shift;
+
+ for my $record (values %{ $self->{Records}{'RT::ObjectCustomFieldValue'} }) {
+ my $object = $self->_GetSerializedByRef(delete $record->{Object});
+
+ my $cf = $self->_GetSerializedByRef(delete $record->{CustomField});
+ $record->{CustomField} = $cf->{Name};
+
+ delete @$record{qw/id Created Creator LastUpdated LastUpdatedBy/};
+
+ $record->{Content} = $record->{LargeContent} if $record->{LargeContent};
+ delete $record->{LargeContent};
+
+ push @{ $object->{CustomFields} }, $record;
+ }
+
+ delete $self->{Records}{'RT::ObjectCustomFieldValue'};
+}
+
sub CanonicalizeObjects {
my $self = shift;
@@ -389,6 +409,7 @@ sub WriteFile {
my %output;
$self->CanonicalizeObjects;
+ $self->CanonicalizeObjectCustomFieldValues;
$self->CanonicalizeACLs;
$self->CanonicalizeUsers;
$self->CanonicalizeGroupMembers;
commit a7dbcdf24bdc983c94d134071be473c6f8e95eab
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 16:35:35 2017 +0000
No need to rewrite LargeContent as Content
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 48bf3fbd3..455c6e551 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -336,9 +336,6 @@ sub CanonicalizeObjectCustomFieldValues {
delete @$record{qw/id Created Creator LastUpdated LastUpdatedBy/};
- $record->{Content} = $record->{LargeContent} if $record->{LargeContent};
- delete $record->{LargeContent};
-
push @{ $object->{CustomFields} }, $record;
}
commit 2081fc158c9937fc83a050ef2e817f5b7e5c2152
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 16:42:35 2017 +0000
Remove create/update metadata on initialdata export
RT doesn't handle it well on import, and we also don't export/import
transactions through initialdata anyway.
Better to let create/update reflect when the records in this RT instance
were actually created/updated and by whom, rather than whichever RT
instance happened to have exported the initialdata (which may have a
completely different set of users anyway!)
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 455c6e551..947d0a720 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -219,10 +219,6 @@ sub CanonicalizeReference {
my $context = shift;
my $for_key = shift;
- if ($for_key eq 'Creator' || $for_key eq 'LastUpdatedBy') {
- return $self->_GetObjectByRef($ref)->Id;
- }
-
my $record = $self->_GetSerializedByRef($ref)
or return $ref;
@@ -320,8 +316,6 @@ sub CanonicalizeGroupMembers {
my $member = $self->_GetObjectByRef(delete $record->{MemberId});
$record->{Class} = ref($member->Object);
$record->{Name} = $member->Object->Name;
-
- delete @$record{qw/Creator Created LastUpdated LastUpdatedBy/};
}
}
@@ -334,7 +328,7 @@ sub CanonicalizeObjectCustomFieldValues {
my $cf = $self->_GetSerializedByRef(delete $record->{CustomField});
$record->{CustomField} = $cf->{Name};
- delete @$record{qw/id Created Creator LastUpdated LastUpdatedBy/};
+ delete @$record{qw/id/};
push @{ $object->{CustomFields} }, $record;
}
@@ -363,7 +357,7 @@ sub CanonicalizeObjects {
primary_key => 'Values',
canonicalize_object => sub {
my %object = %$_;
- delete @object{qw/id CustomField Created LastUpdated Creator LastUpdatedBy/};
+ delete @object{qw/id CustomField/};
return \%object;
},
);
@@ -393,7 +387,7 @@ sub CanonicalizeObjects {
add_to_primary => { NoAutoGlobal => 1 },
canonicalize_object => sub {
my %object = %$_;
- delete @object{qw/id Scrip Created LastUpdated Creator LastUpdatedBy/};
+ delete @object{qw/id Scrip/};
$object{ObjectId} = $self->_GetSerializedByRef($object{ObjectId})->{Name}
if $object{ObjectId}; # 0 meaning Global can stay 0
return \%object;
@@ -405,6 +399,10 @@ sub WriteFile {
my $self = shift;
my %output;
+ for my $record (map { values %$_ } values %{ $self->{Records} }) {
+ delete @$record{qw/Creator Created LastUpdated LastUpdatedBy/};
+ }
+
$self->CanonicalizeObjects;
$self->CanonicalizeObjectCustomFieldValues;
$self->CanonicalizeACLs;
commit a0bbc30b34281e05d4b8988f9b6571d90a5d0319
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 16:46:25 2017 +0000
Remove spurious Principal and PrincipalId fields on Groups
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 947d0a720..3396cc4cc 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -306,6 +306,15 @@ sub CanonicalizeUsers {
}
}
+sub CanonicalizeGroups {
+ my $self = shift;
+
+ for my $group (values %{ $self->{Records}{'RT::Group'} }) {
+ delete $group->{Principal};
+ delete $group->{PrincipalId};
+ }
+}
+
sub CanonicalizeGroupMembers {
my $self = shift;
@@ -407,6 +416,7 @@ sub WriteFile {
$self->CanonicalizeObjectCustomFieldValues;
$self->CanonicalizeACLs;
$self->CanonicalizeUsers;
+ $self->CanonicalizeGroups;
$self->CanonicalizeGroupMembers;
delete $self->{Records}{'RT::Attribute'};
commit 64113d4162214ff77a62f9c4852bf9f7422e9077
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:00:56 2017 +0000
Remove spurious NoAutoGlobal
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 3396cc4cc..d756ff475 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -241,7 +241,7 @@ sub _CanonicalizeManyToMany {
my $object_primary_ref = $args{object_primary_ref};
my $primary_class = $args{primary_class};
my $primary_key = $args{primary_key};
- my %add_to_primary = %{ $args{add_to_primary} || {} };
+ my $add_to_primary = $args{add_to_primary};
my $canonicalize_object = $args{canonicalize_object};
if (my $objects = delete $self->{Records}{$object_class}) {
@@ -256,7 +256,9 @@ sub _CanonicalizeManyToMany {
sort { $a->{SortOrder} <=> $b->{SortOrder} }
@{ $primary->{$primary_key} || [] };
- %$primary = (%$primary, %add_to_primary);
+ if (ref($add_to_primary) eq 'CODE') {
+ $add_to_primary->($primary);
+ }
}
}
}
@@ -393,7 +395,10 @@ sub CanonicalizeObjects {
object_primary_ref => 'Scrip',
primary_class => 'RT::Scrip',
primary_key => 'Queue',
- add_to_primary => { NoAutoGlobal => 1 },
+ add_to_primary => sub {
+ my $primary = shift;
+ $primary->{NoAutoGlobal} = 1 if @{ $primary->{Queue} || [] } == 0;
+ },
canonicalize_object => sub {
my %object = %$_;
delete @object{qw/id Scrip/};
commit 80f8a8b008f5dc933570e1fc07e61b6d2c33085a
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:01:19 2017 +0000
ACLs without object type are for RT::System
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index d756ff475..8f55ae051 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -289,8 +289,10 @@ sub CanonicalizeACLs {
$ace->{UserId} = $principal->Object->Name;
}
- $ace->{ObjectType} = ref($object);
- $ace->{ObjectId} = $object->Id;
+ unless ($object->isa('RT::System')) {
+ $ace->{ObjectType} = ref($object);
+ $ace->{ObjectId} = $object->Id;
+ }
}
}
commit 7273202005d70d5f8487f89932bd09dd1ce47ed3
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:02:40 2017 +0000
ACL PrincipalType is implicit
Furthermore we were exporting UserId => '...', PrincipalType => 'Group'
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 8f55ae051..d4d80af99 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -267,6 +267,7 @@ sub CanonicalizeACLs {
my $self = shift;
for my $ace (values %{ $self->{Records}{'RT::ACE'} }) {
+ delete $ace->{PrincipalType};
my $principal = $self->_GetObjectByRef(delete $ace->{PrincipalId});
my $object = $self->_GetObjectByRef(delete $ace->{Object});
commit cb83c6b79eec302fa83f1d7a7a3d7d144731e77a
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:17:39 2017 +0000
Instance doesn't make sense for UserDefined or SystemInternal groups
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index d4d80af99..1dce59311 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -317,6 +317,9 @@ sub CanonicalizeGroups {
for my $group (values %{ $self->{Records}{'RT::Group'} }) {
delete $group->{Principal};
delete $group->{PrincipalId};
+
+ delete $group->{Instance} if $group->{Domain} eq 'UserDefined'
+ || $group->{Domain} eq 'SystemInternal';
}
}
commit 2e1a00b3c60a9f2462950ef5d8d87ca93d00a1b2
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:17:52 2017 +0000
Avoid serializing some empty keys for CFs
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 1dce59311..7f5ba4d48 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -336,6 +336,15 @@ sub CanonicalizeGroupMembers {
}
}
+sub CanonicalizeCustomFields {
+ my $self = shift;
+
+ for my $record (values %{ $self->{Records}{'RT::CustomField'} }) {
+ delete $record->{Pattern} if $record->{Pattern} eq "";
+ delete $record->{UniqueValues} if !$record->{UniqueValues};
+ }
+}
+
sub CanonicalizeObjectCustomFieldValues {
my $self = shift;
@@ -429,6 +438,7 @@ sub WriteFile {
$self->CanonicalizeUsers;
$self->CanonicalizeGroups;
$self->CanonicalizeGroupMembers;
+ $self->CanonicalizeCustomFields;
delete $self->{Records}{'RT::Attribute'};
commit 9c9d6e06b3f0527e81b7f247aec244fd1b277208
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:18:18 2017 +0000
Avoid serializing Disabled:0 for all objects
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 7f5ba4d48..4de4eea4e 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -455,6 +455,8 @@ sub WriteFile {
}
}
delete $record->{id};
+ delete $record->{Disabled} if !$record->{Disabled};
+
push @{ $output{$outtype} }, $record;
}
}
commit 2b0a32ebd6847cbc547e8b17539cdc8cc656e2dd
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:18:34 2017 +0000
Avoid exporting crucial system objects like RT_System user
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 4de4eea4e..a248fe589 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -424,6 +424,29 @@ sub CanonicalizeObjects {
);
}
+# Exclude critical system objects that should already be present in the importing side
+sub ShouldExcludeObject {
+ my $self = shift;
+ my $class = shift;
+ my $id = shift;
+ my $record = shift;
+
+ return 1 if $class eq 'RT::User'
+ && ($record->{Name} eq 'RT_System' || $record->{Name} eq 'Nobody');
+
+ return 1 if $class eq 'RT::ACE'
+ && ((($record->{UserId}||'') eq 'Nobody' && $record->{RightName} eq 'OwnTicket')
+ || (($record->{UserId}||'') eq 'RT_System' && $record->{RightName} eq 'SuperUser'));
+
+ return 1 if $class eq 'RT::Group'
+ && ($record->{Domain} eq 'RT::System-Role' || $record->{Domain} eq 'SystemInternal');
+
+ return 1 if $class eq 'RT::Queue'
+ && $record->{Name} eq '___Approvals';
+
+ return 0;
+}
+
sub WriteFile {
my $self = shift;
my %output;
@@ -449,6 +472,9 @@ sub WriteFile {
for my $id (keys %{ $self->{Records}{$intype} }) {
my $record = $self->{Records}{$intype}{$id};
+
+ next if $self->ShouldExcludeObject($intype, $id, $record);
+
for my $key (keys %$record) {
if (ref($record->{$key}) eq 'SCALAR') {
$record->{$key} = $self->CanonicalizeReference($record->{$key}, $record, $key);
commit 779787d31c45034c08adebe8902d4f8490807907
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:36:12 2017 +0000
Improve export of group membership
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index a248fe589..1320e5985 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -126,12 +126,14 @@ sub Observe {
my $obj = $args{object};
- # avoid serializing ACLEquivalence, etc
if ($obj->isa("RT::Group")) {
- return 0 unless $obj->Domain eq 'UserDefined';
+ return 0 if $obj->Domain eq 'ACLEquivalence'
+ || $obj->Domain =~ /^RT::(Queue|Catalog)-Role$/;
}
if ($obj->isa("RT::GroupMember")) {
- return 0 unless $obj->GroupObj->Object->Domain eq 'UserDefined';
+ my $domain = $obj->GroupObj->Object->Domain;
+ return 0 if $domain eq 'ACLEquivalence'
+ || $domain eq 'SystemInternal';
}
return $self->SUPER::Observe(%args);
@@ -329,6 +331,8 @@ sub CanonicalizeGroupMembers {
for my $record (values %{ $self->{Records}{'RT::GroupMember'} }) {
my $group = $self->_GetObjectByRef(delete $record->{GroupId});
$record->{Group} = $group->Object->Name;
+ $record->{GroupDomain} = $group->Object->Domain
+ unless $group->Object->Domain eq 'UserDefined';
my $member = $self->_GetObjectByRef(delete $record->{MemberId});
$record->{Class} = ref($member->Object);
@@ -444,6 +448,10 @@ sub ShouldExcludeObject {
return 1 if $class eq 'RT::Queue'
&& $record->{Name} eq '___Approvals';
+ return 1 if $class eq 'RT::GroupMember'
+ && $record->{Group} eq 'Owner' && $record->{GroupDomain} eq 'RT::System-Role'
+ && $record->{Class} eq 'RT::User' && $record->{Name} eq 'Nobody';
+
return 0;
}
commit c54abcd5c2a9432e6ab18b6a7edd761b5abae52a
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:39:46 2017 +0000
Refactor ShouldExcludeObject for clarity
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 1320e5985..9d02f472e 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -435,22 +435,27 @@ sub ShouldExcludeObject {
my $id = shift;
my $record = shift;
- return 1 if $class eq 'RT::User'
- && ($record->{Name} eq 'RT_System' || $record->{Name} eq 'Nobody');
-
- return 1 if $class eq 'RT::ACE'
- && ((($record->{UserId}||'') eq 'Nobody' && $record->{RightName} eq 'OwnTicket')
- || (($record->{UserId}||'') eq 'RT_System' && $record->{RightName} eq 'SuperUser'));
-
- return 1 if $class eq 'RT::Group'
- && ($record->{Domain} eq 'RT::System-Role' || $record->{Domain} eq 'SystemInternal');
-
- return 1 if $class eq 'RT::Queue'
- && $record->{Name} eq '___Approvals';
-
- return 1 if $class eq 'RT::GroupMember'
- && $record->{Group} eq 'Owner' && $record->{GroupDomain} eq 'RT::System-Role'
- && $record->{Class} eq 'RT::User' && $record->{Name} eq 'Nobody';
+ if ($class eq 'RT::User') {
+ return 1 if $record->{Name} eq 'RT_System'
+ || $record->{Name} eq 'Nobody';
+ }
+ elsif ($class eq 'RT::ACE') {
+ return 1 if ($record->{UserId}||'') eq 'Nobody' && $record->{RightName} eq 'OwnTicket';
+ return 1 if ($record->{UserId}||'') eq 'RT_System' && $record->{RightName} eq 'SuperUser';
+ }
+ elsif ($class eq 'RT::Group') {
+ return 1 if $record->{Domain} eq 'RT::System-Role'
+ || $record->{Domain} eq 'SystemInternal';
+ }
+ elsif ($class eq 'RT::Queue') {
+ return 1 if $record->{Name} eq '___Approvals';
+ }
+ elsif ($class eq 'RT::GroupMember') {
+ return 1 if $record->{Group} eq 'Owner'
+ && $record->{GroupDomain} eq 'RT::System-Role'
+ && $record->{Class} eq 'RT::User'
+ && $record->{Name} eq 'Nobody';
+ }
return 0;
}
commit 751e44ab931082f05265a6e76db79d7b66633341
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 19:53:40 2017 +0000
Serialize role members but not role groups
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 9d02f472e..2eb5532f3 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -127,8 +127,7 @@ sub Observe {
my $obj = $args{object};
if ($obj->isa("RT::Group")) {
- return 0 if $obj->Domain eq 'ACLEquivalence'
- || $obj->Domain =~ /^RT::(Queue|Catalog)-Role$/;
+ return 0 if $obj->Domain eq 'ACLEquivalence';
}
if ($obj->isa("RT::GroupMember")) {
my $domain = $obj->GroupObj->Object->Domain;
@@ -316,7 +315,17 @@ sub CanonicalizeUsers {
sub CanonicalizeGroups {
my $self = shift;
- for my $group (values %{ $self->{Records}{'RT::Group'} }) {
+ for my $id (keys %{ $self->{Records}{'RT::Group'} }) {
+ my $group = $self->{Records}{'RT::Group'}{$id};
+
+ # no need to serialize this because role groups are automatically
+ # created; but we can't exclude this in ->Observe because then we
+ # lose out on the group members
+ if ($group->{Domain} =~ /-Role$/) {
+ delete $self->{Records}{'RT::Group'}{$id};
+ next;
+ }
+
delete $group->{Principal};
delete $group->{PrincipalId};
@@ -452,7 +461,7 @@ sub ShouldExcludeObject {
}
elsif ($class eq 'RT::GroupMember') {
return 1 if $record->{Group} eq 'Owner'
- && $record->{GroupDomain} eq 'RT::System-Role'
+ && $record->{GroupDomain} =~ /-Role$/
&& $record->{Class} eq 'RT::User'
&& $record->{Name} eq 'Nobody';
}
commit 0798a710c87b232837128fad75d2c7957fc7bd82
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 20:00:29 2017 +0000
Support export of queue watcher groups
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 2eb5532f3..04a7b0f2d 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -338,14 +338,18 @@ sub CanonicalizeGroupMembers {
my $self = shift;
for my $record (values %{ $self->{Records}{'RT::GroupMember'} }) {
- my $group = $self->_GetObjectByRef(delete $record->{GroupId});
- $record->{Group} = $group->Object->Name;
- $record->{GroupDomain} = $group->Object->Domain
- unless $group->Object->Domain eq 'UserDefined';
-
- my $member = $self->_GetObjectByRef(delete $record->{MemberId});
- $record->{Class} = ref($member->Object);
- $record->{Name} = $member->Object->Name;
+ my $group = $self->_GetObjectByRef(delete $record->{GroupId})->Object;
+ my $domain = $group->Domain;
+
+ $record->{Group} = $group->Name;
+ $record->{GroupDomain} = $domain
+ unless $domain eq 'UserDefined';
+ $record->{GroupInstance} = \($group->InstanceObj->UID)
+ if $domain =~ /-Role$/;
+
+ my $member = $self->_GetObjectByRef(delete $record->{MemberId})->Object;
+ $record->{Class} = ref($member);
+ $record->{Name} = $member->Name;
}
}
commit 3b1efa0e0323762223625e0eb5540f90964ee141
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 20:18:32 2017 +0000
Handle group, role, and user ACLs
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 04a7b0f2d..e37632488 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -273,17 +273,18 @@ sub CanonicalizeACLs {
my $object = $self->_GetObjectByRef(delete $ace->{Object});
if ($principal->IsGroup) {
- my $domain = $principal->Object->Domain;
+ my $group = $principal->Object;
+ my $domain = $group->Domain;
if ($domain eq 'ACLEquivalence') {
- $ace->{UserId} = $principal->Object->InstanceObj->Name;
+ $ace->{UserId} = $group->InstanceObj->Name;
}
else {
$ace->{GroupDomain} = $domain;
- if ($domain eq 'SystemInternal') {
- $ace->{GroupType} = $principal->Object->Name;
+ if ($domain eq 'UserDefined') {
+ $ace->{GroupId} = $group->Name;
}
- elsif ($domain eq 'RT::Queue-Role') {
- $ace->{Queue} = $principal->Object->Instance;
+ if ($domain eq 'SystemInternal' || $domain =~ /-Role$/) {
+ $ace->{GroupType} = $group->Name;
}
}
}
@@ -293,7 +294,7 @@ sub CanonicalizeACLs {
unless ($object->isa('RT::System')) {
$ace->{ObjectType} = ref($object);
- $ace->{ObjectId} = $object->Id;
+ $ace->{ObjectId} = \($object->UID);
}
}
}
commit 18efb3178d5d5be1d126ae660b32a8c10f776f33
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 21 21:40:34 2017 +0000
Round-trip articles
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index e37632488..d15e9558d 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -380,6 +380,14 @@ sub CanonicalizeObjectCustomFieldValues {
delete $self->{Records}{'RT::ObjectCustomFieldValue'};
}
+sub CanonicalizeArticles {
+ my $self = shift;
+
+ for my $record (values %{ $self->{Records}{'RT::Article'} }) {
+ delete $record->{URI};
+ }
+}
+
sub CanonicalizeObjects {
my $self = shift;
@@ -489,6 +497,7 @@ sub WriteFile {
$self->CanonicalizeGroups;
$self->CanonicalizeGroupMembers;
$self->CanonicalizeCustomFields;
+ $self->CanonicalizeArticles;
delete $self->{Records}{'RT::Attribute'};
commit e4b810ecced0d1c2b984231481794c41dd5b34ee
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 18:15:46 2017 +0000
Avoid throwing errors on disabled many-to-many relationships
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index d15e9558d..a393e6f7c 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -253,7 +253,8 @@ sub _CanonicalizeManyToMany {
for my $primary (values %{ $self->{Records}{$primary_class} }) {
@{ $primary->{$primary_key} }
- = map &$canonicalize_object,
+ = grep defined,
+ map &$canonicalize_object,
sort { $a->{SortOrder} <=> $b->{SortOrder} }
@{ $primary->{$primary_key} || [] };
@@ -396,9 +397,10 @@ sub CanonicalizeObjects {
object_primary_ref => 'CustomField',
primary_class => 'RT::CustomField',
canonicalize_object => sub {
- ref($_->{ObjectId})
- ? $self->_GetSerializedByRef($_->{ObjectId})->{Name}
- : $_->{ObjectId};
+ my $id = $_->{ObjectId};
+ return $id if !ref($id);
+ my $serialized = $self->_GetSerializedByRef($id);
+ return $serialized ? $serialized->{Name} : undef;
},
);
@@ -418,6 +420,12 @@ sub CanonicalizeObjects {
object_class => 'RT::ObjectClass',
object_primary_ref => 'Class',
primary_class => 'RT::Class',
+ canonicalize_object => sub {
+ my $id = $_->{ObjectId};
+ return $id if !ref($id);
+ my $serialized = $self->_GetSerializedByRef($id);
+ return $serialized ? $serialized->{Name} : undef;
+ },
);
$self->_CanonicalizeManyToMany(
@@ -425,9 +433,10 @@ sub CanonicalizeObjects {
object_primary_ref => 'CustomRole',
primary_class => 'RT::CustomRole',
canonicalize_object => sub {
- ref($_->{ObjectId})
- ? $self->_GetSerializedByRef($_->{ObjectId})->{Name}
- : $_->{ObjectId};
+ my $id = $_->{ObjectId};
+ return $id if !ref($id);
+ my $serialized = $self->_GetSerializedByRef($id);
+ return $serialized ? $serialized->{Name} : undef;
},
);
@@ -443,8 +452,13 @@ sub CanonicalizeObjects {
canonicalize_object => sub {
my %object = %$_;
delete @object{qw/id Scrip/};
- $object{ObjectId} = $self->_GetSerializedByRef($object{ObjectId})->{Name}
- if $object{ObjectId}; # 0 meaning Global can stay 0
+
+ if (ref($_->{ObjectId})) {
+ my $serialized = $self->_GetSerializedByRef($_->{ObjectId});
+ return undef if !$serialized;
+ $object{ObjectId} = $serialized->{Name};
+ }
+
return \%object;
},
);
commit e4672182df5c0d7b0c204e31d7c690d2a2cb7fdf
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 19:42:21 2017 +0000
Avoid undef warnings on CFs without a pattern
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index a393e6f7c..fa262e653 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -359,7 +359,7 @@ sub CanonicalizeCustomFields {
my $self = shift;
for my $record (values %{ $self->{Records}{'RT::CustomField'} }) {
- delete $record->{Pattern} if $record->{Pattern} eq "";
+ delete $record->{Pattern} if ($record->{Pattern}||'') eq "";
delete $record->{UniqueValues} if !$record->{UniqueValues};
}
}
commit 4ca88f8d4f8243b37568bc4dd9adafd575308361
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 19:58:59 2017 +0000
Skip serializing OCFVs on live objects for disabled CFs
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index fa262e653..8a4415a4c 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -371,6 +371,7 @@ sub CanonicalizeObjectCustomFieldValues {
my $object = $self->_GetSerializedByRef(delete $record->{Object});
my $cf = $self->_GetSerializedByRef(delete $record->{CustomField});
+ next unless $cf && $cf->{Name}; # disabled CF on live object
$record->{CustomField} = $cf->{Name};
delete @$record{qw/id/};
commit b1dff1024adafe816f252df8b094505242742f78
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 21:26:04 2017 +0000
For stability, sort records consistently
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 8a4415a4c..f44f6456e 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -51,6 +51,7 @@ package RT::Migrate::Serializer::JSON;
use strict;
use warnings;
use JSON qw//;
+use List::MoreUtils 'uniq';
use base 'RT::Migrate::Serializer';
@@ -231,18 +232,22 @@ sub _CanonicalizeManyToMany {
my %args = (
object_class => '',
object_primary_ref => '',
+ object_sorter => '',
primary_class => '',
primary_key => 'ApplyTo',
add_to_primary => undef,
+ sort_uniq => 0,
canonicalize_object => sub { $_->{ObjectId} },
@_,
);
my $object_class = $args{object_class};
my $object_primary_ref = $args{object_primary_ref};
+ my $object_sorter = $args{object_sorter};
my $primary_class = $args{primary_class};
my $primary_key = $args{primary_key};
my $add_to_primary = $args{add_to_primary};
+ my $sort_uniq = $args{sort_uniq};
my $canonicalize_object = $args{canonicalize_object};
if (my $objects = delete $self->{Records}{$object_class}) {
@@ -255,9 +260,16 @@ sub _CanonicalizeManyToMany {
@{ $primary->{$primary_key} }
= grep defined,
map &$canonicalize_object,
- sort { $a->{SortOrder} <=> $b->{SortOrder} }
+ sort { $a->{SortOrder} <=> $b->{SortOrder}
+ || ($object_sorter ? $a->{$object_sorter} cmp $b->{$object_sorter} : 0) }
@{ $primary->{$primary_key} || [] };
+ if ($sort_uniq) {
+ @{ $primary->{$primary_key} }
+ = uniq sort
+ @{ $primary->{$primary_key} };
+ }
+
if (ref($add_to_primary) eq 'CODE') {
$add_to_primary->($primary);
}
@@ -397,6 +409,7 @@ sub CanonicalizeObjects {
object_class => 'RT::ObjectCustomField',
object_primary_ref => 'CustomField',
primary_class => 'RT::CustomField',
+ sort_uniq => 1,
canonicalize_object => sub {
my $id = $_->{ObjectId};
return $id if !ref($id);
@@ -408,6 +421,7 @@ sub CanonicalizeObjects {
$self->_CanonicalizeManyToMany(
object_class => 'RT::CustomFieldValue',
object_primary_ref => 'CustomField',
+ object_sorter => 'Name',
primary_class => 'RT::CustomField',
primary_key => 'Values',
canonicalize_object => sub {
@@ -421,6 +435,7 @@ sub CanonicalizeObjects {
object_class => 'RT::ObjectClass',
object_primary_ref => 'Class',
primary_class => 'RT::Class',
+ sort_uniq => 1,
canonicalize_object => sub {
my $id = $_->{ObjectId};
return $id if !ref($id);
@@ -433,6 +448,7 @@ sub CanonicalizeObjects {
object_class => 'RT::ObjectCustomRole',
object_primary_ref => 'CustomRole',
primary_class => 'RT::CustomRole',
+ sort_uniq => 1,
canonicalize_object => sub {
my $id = $_->{ObjectId};
return $id if !ref($id);
@@ -514,15 +530,23 @@ sub WriteFile {
$self->CanonicalizeCustomFields;
$self->CanonicalizeArticles;
- delete $self->{Records}{'RT::Attribute'};
+ my $all_records = $self->{Records};
- for my $intype (keys %{ $self->{Records} }) {
+ delete $all_records->{'RT::Attribute'};
+
+ for my $intype (keys %$all_records) {
my $outtype = $intype;
$outtype =~ s/^RT:://;
$outtype = $initialdataType{$outtype} || ($outtype . 's');
- for my $id (keys %{ $self->{Records}{$intype} }) {
- my $record = $self->{Records}{$intype}{$id};
+ my $records = $all_records->{$intype};
+
+ # sort by database id then serializer id for stability
+ for my $id (sort {
+ ($records->{$a}{id} || 0) <=> ($records->{$b}{id} || 0)
+ || $a cmp $b
+ } keys %$records) {
+ my $record = $records->{$id};
next if $self->ShouldExcludeObject($intype, $id, $record);
commit 9b8a088c1ce323699ca031055effa1c04b5827ee
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 21:39:36 2017 +0000
Clear user passwords and authtokens
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index f44f6456e..e8ab7e337 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -319,6 +319,9 @@ sub CanonicalizeUsers {
delete $user->{Principal};
delete $user->{PrincipalId};
+ delete $user->{Password};
+ delete $user->{AuthToken};
+
my $object = RT::User->new(RT->SystemUser);
$object->Load($user->{id});
commit 60590ae9c64c3be32f97188e2c7c5917678bffce
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 21:45:33 2017 +0000
Remove more empty fields
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index e8ab7e337..4eeec006c 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -430,6 +430,8 @@ sub CanonicalizeObjects {
canonicalize_object => sub {
my %object = %$_;
delete @object{qw/id CustomField/};
+ delete $object{Category} if !length($object{Category});
+ delete $object{Description} if !length($object{Description});
return \%object;
},
);
commit 1f624c94f818196e8b6bb609b68b1c9a5be74a76
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 21:54:37 2017 +0000
Handle disabled OCFVs by skipping them
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 4eeec006c..f967baef7 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -429,6 +429,8 @@ sub CanonicalizeObjects {
primary_key => 'Values',
canonicalize_object => sub {
my %object = %$_;
+ return if $object{Disabled} && !$self->{FollowDisabled};
+
delete @object{qw/id CustomField/};
delete $object{Category} if !length($object{Category});
delete $object{Description} if !length($object{Description});
commit d9f8bf98f51492a562f2a3248952b995d9974cb6
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 21:57:11 2017 +0000
Remove empty user keys
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index f967baef7..00ac3c94f 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -325,6 +325,11 @@ sub CanonicalizeUsers {
my $object = RT::User->new(RT->SystemUser);
$object->Load($user->{id});
+ for my $key (keys %$user) {
+ my $value = $user->{$key};
+ delete $user->{$key} if !defined($value) || !length($value);
+ }
+
$user->{Privileged} = $object->Privileged ? JSON::true : JSON::false;
}
}
commit 759ad60dababe4a65c57e684315a2fdbc99b9003
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 23:05:41 2017 +0000
Avoid undef warnings with sorting by sortorder
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 00ac3c94f..31e85823b 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -260,7 +260,7 @@ sub _CanonicalizeManyToMany {
@{ $primary->{$primary_key} }
= grep defined,
map &$canonicalize_object,
- sort { $a->{SortOrder} <=> $b->{SortOrder}
+ sort { ($a->{SortOrder}||0) <=> ($b->{SortOrder}||0)
|| ($object_sorter ? $a->{$object_sorter} cmp $b->{$object_sorter} : 0) }
@{ $primary->{$primary_key} || [] };
commit f6ad69b91c556028eed688b09016375c39c11bf0
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 23:32:16 2017 +0000
Sort ObjectScrips by queue name for stability
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 31e85823b..6156e1a73 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -237,6 +237,7 @@ sub _CanonicalizeManyToMany {
primary_key => 'ApplyTo',
add_to_primary => undef,
sort_uniq => 0,
+ finalize => undef,
canonicalize_object => sub { $_->{ObjectId} },
@_,
);
@@ -248,6 +249,7 @@ sub _CanonicalizeManyToMany {
my $primary_key = $args{primary_key};
my $add_to_primary = $args{add_to_primary};
my $sort_uniq = $args{sort_uniq};
+ my $finalize = $args{finalize};
my $canonicalize_object = $args{canonicalize_object};
if (my $objects = delete $self->{Records}{$object_class}) {
@@ -270,6 +272,10 @@ sub _CanonicalizeManyToMany {
@{ $primary->{$primary_key} };
}
+ if ($finalize) {
+ $finalize->($primary);
+ }
+
if (ref($add_to_primary) eq 'CODE') {
$add_to_primary->($primary);
}
@@ -490,6 +496,10 @@ sub CanonicalizeObjects {
return \%object;
},
+ finalize => sub {
+ my $scrip = shift;
+ @{ $scrip->{Queue} } = sort { $a->{ObjectId} cmp $b->{ObjectId} } @{ $scrip->{Queue} };
+ },
);
}
commit 7048bee5b3203c154f5d8eea750e5f485335ee55
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 16:27:16 2017 +0000
Don't serialized disabled OCFVs
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 6156e1a73..2ab8bf007 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -393,7 +393,14 @@ sub CanonicalizeCustomFields {
sub CanonicalizeObjectCustomFieldValues {
my $self = shift;
- for my $record (values %{ $self->{Records}{'RT::ObjectCustomFieldValue'} }) {
+ for my $id (keys %{ $self->{Records}{'RT::ObjectCustomFieldValue'} }) {
+ my $record = $self->{Records}{'RT::ObjectCustomFieldValue'}{$id};
+
+ if ($record->{Disabled} && !$self->{FollowDisabled}) {
+ delete $self->{Records}{'RT::ObjectCustomFieldValue'}{$id};
+ next;
+ }
+
my $object = $self->_GetSerializedByRef(delete $record->{Object});
my $cf = $self->_GetSerializedByRef(delete $record->{CustomField});
commit cd50567a3b9fe24296a800f1b022567e06f22ffa
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 16:31:34 2017 +0000
Factor out repeated deep hash lookups
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 2ab8bf007..66dd90237 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -252,13 +252,14 @@ sub _CanonicalizeManyToMany {
my $finalize = $args{finalize};
my $canonicalize_object = $args{canonicalize_object};
+ my $primary_records = $self->{Records}{$primary_class};
if (my $objects = delete $self->{Records}{$object_class}) {
for my $object (values %$objects) {
- my $primary = $self->{Records}{$primary_class}{ ${ $object->{$object_primary_ref} } };
+ my $primary = $primary_records->{ ${ $object->{$object_primary_ref} } };
push @{ $primary->{$primary_key} }, $object;
}
- for my $primary (values %{ $self->{Records}{$primary_class} }) {
+ for my $primary (values %$primary_records) {
@{ $primary->{$primary_key} }
= grep defined,
map &$canonicalize_object,
@@ -343,14 +344,15 @@ sub CanonicalizeUsers {
sub CanonicalizeGroups {
my $self = shift;
- for my $id (keys %{ $self->{Records}{'RT::Group'} }) {
- my $group = $self->{Records}{'RT::Group'}{$id};
+ my $records = $self->{Records}{'RT::Group'};
+ for my $id (keys %$records) {
+ my $group = $records->{$id};
# no need to serialize this because role groups are automatically
# created; but we can't exclude this in ->Observe because then we
# lose out on the group members
if ($group->{Domain} =~ /-Role$/) {
- delete $self->{Records}{'RT::Group'}{$id};
+ delete $records->{$id};
next;
}
@@ -393,11 +395,12 @@ sub CanonicalizeCustomFields {
sub CanonicalizeObjectCustomFieldValues {
my $self = shift;
- for my $id (keys %{ $self->{Records}{'RT::ObjectCustomFieldValue'} }) {
- my $record = $self->{Records}{'RT::ObjectCustomFieldValue'}{$id};
+ my $records = delete $self->{Records}{'RT::ObjectCustomFieldValue'};
+ for my $id (keys %$records) {
+ my $record = $records->{$id};
if ($record->{Disabled} && !$self->{FollowDisabled}) {
- delete $self->{Records}{'RT::ObjectCustomFieldValue'}{$id};
+ delete $records->{$id};
next;
}
@@ -411,8 +414,6 @@ sub CanonicalizeObjectCustomFieldValues {
push @{ $object->{CustomFields} }, $record;
}
-
- delete $self->{Records}{'RT::ObjectCustomFieldValue'};
}
sub CanonicalizeArticles {
commit 99b8357c47fefa8f5a55ddfbc664724f5d4248a8
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 16:54:07 2017 +0000
No need to include nobody member for single-member groups
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 66dd90237..919798232 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -367,17 +367,26 @@ sub CanonicalizeGroups {
sub CanonicalizeGroupMembers {
my $self = shift;
- for my $record (values %{ $self->{Records}{'RT::GroupMember'} }) {
+ my $records = $self->{Records}{'RT::GroupMember'};
+ for my $id (keys %$records) {
+ my $record = $records->{$id};
my $group = $self->_GetObjectByRef(delete $record->{GroupId})->Object;
+ my $member = $self->_GetObjectByRef(delete $record->{MemberId})->Object;
my $domain = $group->Domain;
+ # no need to explicitly include a Nobody member
+ if ($member->isa('RT::User') && $member->Name eq 'Nobody' && $group->SingleMemberRoleGroup) {
+ delete $records->{$id};
+ next;
+ }
+
+
$record->{Group} = $group->Name;
$record->{GroupDomain} = $domain
unless $domain eq 'UserDefined';
$record->{GroupInstance} = \($group->InstanceObj->UID)
if $domain =~ /-Role$/;
- my $member = $self->_GetObjectByRef(delete $record->{MemberId})->Object;
$record->{Class} = ref($member);
$record->{Name} = $member->Name;
}
commit a6910b6d0a0de130ba0026f04e77b64050e1fde4
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 17:58:14 2017 +0000
Add Sync option for JSON serializer
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 919798232..5245d1a66 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -79,6 +79,8 @@ sub Init {
$self->{Records} = {};
+ $self->{Sync} = $args{Sync};
+
$self->SUPER::Init(@_);
}
@@ -419,7 +421,7 @@ sub CanonicalizeObjectCustomFieldValues {
next unless $cf && $cf->{Name}; # disabled CF on live object
$record->{CustomField} = $cf->{Name};
- delete @$record{qw/id/};
+ delete $record->{id} unless $self->{Sync};
push @{ $object->{CustomFields} }, $record;
}
@@ -459,7 +461,8 @@ sub CanonicalizeObjects {
my %object = %$_;
return if $object{Disabled} && !$self->{FollowDisabled};
- delete @object{qw/id CustomField/};
+ delete $object{CustomField};
+ delete $object{id} unless $self->{Sync};
delete $object{Category} if !length($object{Category});
delete $object{Description} if !length($object{Description});
return \%object;
@@ -503,7 +506,8 @@ sub CanonicalizeObjects {
},
canonicalize_object => sub {
my %object = %$_;
- delete @object{qw/id Scrip/};
+ delete $object{Scrip};
+ delete $object{id} unless $self->{Sync};
if (ref($_->{ObjectId})) {
my $serialized = $self->_GetSerializedByRef($_->{ObjectId});
@@ -570,6 +574,7 @@ sub WriteFile {
$self->CanonicalizeArticles;
my $all_records = $self->{Records};
+ my $sync = $self->{Sync};
delete $all_records->{'RT::Attribute'};
@@ -594,7 +599,7 @@ sub WriteFile {
$record->{$key} = $self->CanonicalizeReference($record->{$key}, $record, $key);
}
}
- delete $record->{id};
+ delete $record->{id} unless $sync;
delete $record->{Disabled} if !$record->{Disabled};
push @{ $output{$outtype} }, $record;
commit 4acf32f761cbee9d92e4a69b24a664c3650d0695
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 18:05:11 2017 +0000
Delete empty Values => [] from custom fields
Especially important for CF types that don't use values
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 5245d1a66..8dc86664a 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -239,6 +239,7 @@ sub _CanonicalizeManyToMany {
primary_key => 'ApplyTo',
add_to_primary => undef,
sort_uniq => 0,
+ delete_empty => 0,
finalize => undef,
canonicalize_object => sub { $_->{ObjectId} },
@_,
@@ -251,6 +252,7 @@ sub _CanonicalizeManyToMany {
my $primary_key = $args{primary_key};
my $add_to_primary = $args{add_to_primary};
my $sort_uniq = $args{sort_uniq};
+ my $delete_empty = $args{delete_empty};
my $finalize = $args{finalize};
my $canonicalize_object = $args{canonicalize_object};
@@ -275,6 +277,10 @@ sub _CanonicalizeManyToMany {
@{ $primary->{$primary_key} };
}
+ if ($delete_empty) {
+ delete $primary->{$primary_key} if !@{ $primary->{$primary_key} };
+ }
+
if ($finalize) {
$finalize->($primary);
}
@@ -457,6 +463,7 @@ sub CanonicalizeObjects {
object_sorter => 'Name',
primary_class => 'RT::CustomField',
primary_key => 'Values',
+ delete_empty => 1,
canonicalize_object => sub {
my %object = %$_;
return if $object{Disabled} && !$self->{FollowDisabled};
commit db38a18d78642326d0e54e330559b565fb66eb51
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 22:07:38 2017 +0000
Default FollowTickets and FollowTransactions to 0 for initialdata
These don't round-trip anyway
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 8dc86664a..1ffa5ea93 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -59,8 +59,10 @@ sub Init {
my $self = shift;
my %args = (
- Directory => undef,
- Force => undef,
+ Directory => undef,
+ Force => undef,
+ FollowTickets => 0,
+ FollowTransactions => 0,
@_,
);
@@ -81,7 +83,7 @@ sub Init {
$self->{Sync} = $args{Sync};
- $self->SUPER::Init(@_);
+ $self->SUPER::Init(%args);
}
sub Export {
commit f2abed891fd204f757a1b75437ba0828cdcfab50
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 17:16:20 2017 +0000
Default to exporting scrips and ACLs
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 1ffa5ea93..7e228ffec 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -63,6 +63,8 @@ sub Init {
Force => undef,
FollowTickets => 0,
FollowTransactions => 0,
+ FollowScrips => 1,
+ FollowACL => 1,
@_,
);
commit 8fbdadfb10348f0e9b5e852e04a00e1523421414
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 19:17:29 2017 +0000
Initial rt-merge-initialdata
diff --git a/.gitignore b/.gitignore
index 1159ee89c..e6da7df18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,6 +42,7 @@
/sbin/rt-validator
/sbin/rt-validate-aliases
/sbin/rt-serializer
+/sbin/rt-merge-initialdata
/sbin/rt-importer
/sbin/rt-ldapimport
/sbin/rt-passwd
diff --git a/Makefile.in b/Makefile.in
index cc418241f..f20edc946 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -149,6 +149,7 @@ SYSTEM_BINARIES = rt-attributes-viewer \
rt-importer \
rt-ldapimport \
rt-passwd \
+ rt-merge-initialdata \
rt-preferences-viewer \
rt-serializer \
rt-server \
diff --git a/configure.ac b/configure.ac
index d7685d80d..6eae1aac6 100755
--- a/configure.ac
+++ b/configure.ac
@@ -484,6 +484,7 @@ AC_CONFIG_FILES([
sbin/rt-setup-fulltext-index
sbin/rt-fulltext-indexer
sbin/rt-serializer
+ sbin/rt-merge-initialdata
sbin/rt-importer
sbin/rt-passwd
bin/rt-crontool
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
new file mode 100644
index 000000000..5550bad5f
--- /dev/null
+++ b/sbin/rt-merge-initialdata.in
@@ -0,0 +1,171 @@
+#!@PERL@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# 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.
+#
+# 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.
+#
+# 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
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (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.)
+#
+# 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.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+# fix lib paths, some may be relative
+BEGIN {
+ require File::Spec;
+ my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
+ my $bin_path;
+
+ for my $lib (@libs) {
+ unless ( File::Spec->file_name_is_absolute($lib) ) {
+ unless ($bin_path) {
+ if ( File::Spec->file_name_is_absolute(__FILE__) ) {
+ $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
+ }
+ else {
+ require FindBin;
+ no warnings "once";
+ $bin_path = $FindBin::Bin;
+ }
+ }
+ $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
+ }
+ unshift @INC, $lib;
+ }
+
+}
+
+use RT;
+RT::LoadConfig();
+RT::Init();
+
+use JSON ();
+my $JSON = JSON->new->canonical;
+
+die "usage: $0 base edited\n" unless @ARGV == 2;
+my ($base_file, $edited_file) = @ARGV;
+
+my $base_records = slurp_json($base_file);
+my $edited_records = slurp_json($edited_file);
+
+my $export_options = delete $edited_records->{ExportOptions}
+ or die "Required metadata ExportOptions not present in $edited_file. Did you pass --sync to rt-serializer?";
+delete $base_records->{ExportOptions};
+
+my @record_types = qw/Groups Users Members ACL Queues Classes
+ ScripActions ScripConditions Templates
+ CustomFields CustomRoles Scrips
+ Catalogs Assets Articles/;
+
+for my $type (@record_types) {
+ my ($new_records, $updated_records, $deleted_records) = find_differences(
+ $base_records->{$type},
+ $edited_records->{$type},
+ $type,
+ );
+
+ my $collection_class = "RT::$type";
+ my $record_class = $collection_class->RecordClass;
+
+ for my $new (@$new_records) {
+ my $record = $record_class->new(RT->SystemUser);
+ }
+
+ for my $deleted (@$deleted_records) {
+ my $record = $record_class->new(RT->SystemUser);
+ $record->Load($deleted->{id});
+ }
+
+ for (@$updated_records) {
+ my ($base, $edited) = @_;
+ my $record = $record_class->new(RT->SystemUser);
+ $record->Load($base->{id});
+ }
+}
+
+sub find_differences {
+ my $base_records = shift;
+ my $edited_records = shift;
+ my $type = shift;
+
+ my (@new, @deleted, @updated);
+ my (%base_by_id, %edited_by_id);
+
+ for my $base_record (@$base_records) {
+ my $id = $base_record->{id};
+
+ if (!$id) {
+ die "Missing id for this $type record in $base_file: " . encode_json($base_record);
+ }
+ $base_by_id{$id} = $base_record;
+ }
+
+ for my $edited_record (@$edited_records) {
+ my $id = $edited_record->{id};
+
+ if (!$id) {
+ push @new, $edited_record;
+ }
+ elsif (!$base_by_id{$id}) {
+ die "$type record in $edited_file has id ($id) that doesn't correspond with a record in $base_file: " . $JSON->encode($edited_record);
+ }
+ else {
+ my $base_record = delete $base_by_id{$id};
+ next if $JSON->encode($base_record) eq $JSON->encode($edited_record);
+ push @updated, [ $base_record => $edited_record ];
+ }
+ }
+
+ for my $base_record (values %base_by_id) {
+ push @deleted, $base_record;
+ }
+
+ return (\@new, \@updated, \@deleted);
+}
+
+sub slurp_json {
+ my $file = shift;
+ local $/;
+ open (my $f, '<', $file)
+ or die "Cannot open initialdata file '$file' for read: $@";
+ return $JSON->decode(scalar <$f>);
+}
commit 0348a357cc7226b911e19d4c71a0fdefd33a1ea4
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 19:21:27 2017 +0000
Implement record disabling/deleting
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 5550bad5f..b28902158 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -111,8 +111,27 @@ for my $type (@record_types) {
}
for my $deleted (@$deleted_records) {
+ my $id = $deleted->{id};
my $record = $record_class->new(RT->SystemUser);
- $record->Load($deleted->{id});
+ $record->Load($id);
+
+ my ($ok, $msg);
+ if ($record->can('SetDisabled') || $record->_Accessible('Disabled', 'write')) {
+ ($ok, $msg) = $record->SetDisabled(1);
+ }
+ elsif ($record->can('Delete')) {
+ ($ok, $msg) = $record->Delete;
+ }
+ else {
+ die "No method to delete $record_class #$id";
+ }
+
+ if ($ok) {
+ RT->Logger->debug("Deleted $record_class $id: $msg");
+ }
+ else {
+ RT->Logger->error("Unable to delete $record_class $id: $msg");
+ }
}
for (@$updated_records) {
commit 04e90301f5ea6807cd302da8164558c36afa558c
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 21:19:33 2017 +0000
Handle simple scalar column updates
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index b28902158..6c7894e35 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -46,6 +46,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+use 5.010;
use strict;
use warnings;
@@ -78,6 +79,7 @@ use RT;
RT::LoadConfig();
RT::Init();
+use List::MoreUtils 'uniq';
use JSON ();
my $JSON = JSON->new->canonical;
@@ -96,6 +98,10 @@ my @record_types = qw/Groups Users Members ACL Queues Classes
CustomFields CustomRoles Scrips
Catalogs Assets Articles/;
+my %class_type = (
+ Members => 'GroupMembers',
+);
+
for my $type (@record_types) {
my ($new_records, $updated_records, $deleted_records) = find_differences(
$base_records->{$type},
@@ -103,9 +109,50 @@ for my $type (@record_types) {
$type,
);
- my $collection_class = "RT::$type";
+ my $collection_class = "RT::" . ($class_type{$type} || $type);
my $record_class = $collection_class->RecordClass;
+ for (@$updated_records) {
+ my ($base, $edited) = @$_;
+ my $id = $base->{id};
+ my $record = $record_class->new(RT->SystemUser);
+ $record->Load($id);
+ if (!$record->Id) {
+ RT->Logger->error("Unable to load $record_class $id for updating; skipping");
+ next;
+ }
+
+ for my $column (uniq sort keys(%$base), keys(%$edited)) {
+ my $base_value = $base->{$column} // "";
+ my $edited_value = $edited->{$column} // "";
+
+ for ($base_value, $edited_value) {
+ $_ = !!$_ if JSON::is_bool($_)
+ }
+
+ if (!ref($base_value) && !ref($edited_value)) {
+ next if $base_value eq $edited_value;
+
+ my $method = "Set$column";
+ if ($record->can($method) || $record->_Accessible($column, 'write')) {
+ my ($ok, $msg) = $record->$method($edited_value);
+ if ($ok) {
+ RT->Logger->debug("Updated $record_class #$id $column: $msg");
+ }
+ else {
+ RT->Logger->error("Unable to update $record_class #$id $column: $msg");
+ }
+ }
+ else {
+ die "Unable to handle updating $record_class $column (no method)";
+ }
+ }
+ else {
+ die "Unable to handle updating $record_class $column (composite value)";
+ }
+ }
+ }
+
for my $new (@$new_records) {
my $record = $record_class->new(RT->SystemUser);
}
@@ -127,18 +174,12 @@ for my $type (@record_types) {
}
if ($ok) {
- RT->Logger->debug("Deleted $record_class $id: $msg");
+ RT->Logger->debug("Deleted $record_class #$id: $msg");
}
else {
- RT->Logger->error("Unable to delete $record_class $id: $msg");
+ RT->Logger->error("Unable to delete $record_class #$id: $msg");
}
}
-
- for (@$updated_records) {
- my ($base, $edited) = @_;
- my $record = $record_class->new(RT->SystemUser);
- $record->Load($base->{id});
- }
}
sub find_differences {
commit c642683f448e6f37c202e0e4c3e9f316164b07f4
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 21:21:00 2017 +0000
Factor out an is_deeply_equal and use it for updates instead of eq
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 6c7894e35..d78ca2602 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -130,9 +130,9 @@ for my $type (@record_types) {
$_ = !!$_ if JSON::is_bool($_)
}
- if (!ref($base_value) && !ref($edited_value)) {
- next if $base_value eq $edited_value;
+ next if is_deeply_equal($base_value, $edited_value);
+ if (!ref($base_value) && !ref($edited_value)) {
my $method = "Set$column";
if ($record->can($method) || $record->_Accessible($column, 'write')) {
my ($ok, $msg) = $record->$method($edited_value);
@@ -210,7 +210,7 @@ sub find_differences {
}
else {
my $base_record = delete $base_by_id{$id};
- next if $JSON->encode($base_record) eq $JSON->encode($edited_record);
+ next if is_deeply_equal($base_record, $edited_record);
push @updated, [ $base_record => $edited_record ];
}
}
@@ -222,6 +222,11 @@ sub find_differences {
return (\@new, \@updated, \@deleted);
}
+sub is_deeply_equal {
+ my ($left, $right) = @_;
+ return $JSON->encode($left) eq $JSON->encode($right);
+}
+
sub slurp_json {
my $file = shift;
local $/;
commit 0d6b6c7dba954114b38e57462833aea23e7d3b6b
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 21:38:42 2017 +0000
Avoid blowing up on comparing scalars
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index d78ca2602..203677a6a 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -224,7 +224,8 @@ sub find_differences {
sub is_deeply_equal {
my ($left, $right) = @_;
- return $JSON->encode($left) eq $JSON->encode($right);
+ # use [] to avoid nonref issues without changing $JSON itself
+ return $JSON->encode([$left]) eq $JSON->encode([$right]);
}
sub slurp_json {
commit cbe2e787c6b487e0b1dc63bc9ab5a4cb068ac490
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 21:38:59 2017 +0000
Avoid spurious warnings when base!=edited but edited=current
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 203677a6a..348d6983b 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -135,6 +135,9 @@ for my $type (@record_types) {
if (!ref($base_value) && !ref($edited_value)) {
my $method = "Set$column";
if ($record->can($method) || $record->_Accessible($column, 'write')) {
+ # skip if it was already updated outside initialdata
+ next if ($record->$method//"") eq $edited_value;
+
my ($ok, $msg) = $record->$method($edited_value);
if ($ok) {
RT->Logger->debug("Updated $record_class #$id $column: $msg");
commit 5d76602f5abff480177712a18a8148793212d56f
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 21:39:17 2017 +0000
Reuse InsertData
This is bananas. It relies on a quirk of Perl's "open" function
which treats the filename as its content if you pass it as a reference.
In other words:
my $json = JSON->encode({ ... });
open my $handle, '<', \$json;
Will have $handle vend the $json content.
This lets us reuse RT::Handle::InsertData without a large refactor
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 348d6983b..b88c5b323 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -102,6 +102,8 @@ my %class_type = (
Members => 'GroupMembers',
);
+my %new_for_insertdata;
+
for my $type (@record_types) {
my ($new_records, $updated_records, $deleted_records) = find_differences(
$base_records->{$type},
@@ -112,6 +114,8 @@ for my $type (@record_types) {
my $collection_class = "RT::" . ($class_type{$type} || $type);
my $record_class = $collection_class->RecordClass;
+ $new_for_insertdata{$type} = $new_records;
+
for (@$updated_records) {
my ($base, $edited) = @$_;
my $id = $base->{id};
@@ -156,10 +160,6 @@ for my $type (@record_types) {
}
}
- for my $new (@$new_records) {
- my $record = $record_class->new(RT->SystemUser);
- }
-
for my $deleted (@$deleted_records) {
my $id = $deleted->{id};
my $record = $record_class->new(RT->SystemUser);
@@ -185,6 +185,9 @@ for my $type (@record_types) {
}
}
+my $new_json = $JSON->encode(\%new_for_insertdata);
+$RT::Handle->InsertData(\$new_json, undef, disconnect_after => 0);
+
sub find_differences {
my $base_records = shift;
my $edited_records = shift;
commit 6f1a539d4733828cdd7640e8832190023a56cc87
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Thu Mar 23 22:05:46 2017 +0000
Prompt user for conflict resolution
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index b88c5b323..38f81f790 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -139,10 +139,35 @@ for my $type (@record_types) {
if (!ref($base_value) && !ref($edited_value)) {
my $method = "Set$column";
if ($record->can($method) || $record->_Accessible($column, 'write')) {
+ my $current_value = $record->$column // "";
+
# skip if it was already updated outside initialdata
- next if ($record->$method//"") eq $edited_value;
+ next if $current_value eq $edited_value;
+
+ my $new_value = $edited_value;
+
+ if ($base_value ne $current_value) {
+ my $decision = decide_merge(
+ base_record => $base,
+ edited_record => $edited,
+ current_record => $record,
+ column => $column,
+ base_value => $base_value,
+ edited_value => $edited_value,
+ current_value => $current_value,
+ );
+ if ($decision eq 'base') {
+ $new_value = $base_value;
+ }
+ elsif ($decision eq 'edited') {
+ # continue
+ }
+ elsif ($decision eq 'current') {
+ next;
+ }
+ }
- my ($ok, $msg) = $record->$method($edited_value);
+ my ($ok, $msg) = $record->$method($new_value);
if ($ok) {
RT->Logger->debug("Updated $record_class #$id $column: $msg");
}
@@ -241,3 +266,53 @@ sub slurp_json {
or die "Cannot open initialdata file '$file' for read: $@";
return $JSON->decode(scalar <$f>);
}
+
+sub decide_merge {
+ my %args = (
+ base_record => undef,
+ edited_record => undef,
+ current_record => undef,
+ column => undef,
+ base_value => undef,
+ edited_value => undef,
+ current_value => undef,
+ @_,
+ );
+
+ my $record = $args{current_record};
+ my $id = $record->id;
+ my $type = ref($record);
+ $type =~ s/^RT:://;
+
+ local $| = 1;
+ while (1) {
+ print "\n";
+ print "Conflict resolution required for $type #$id $args{column}:\n";
+ print " Base value: $args{base_value}\n";
+ print " Edited value: $args{edited_value}\n";
+ print " Database value: $args{current_value}\n";
+ print "Would you like to (q)uit, (s)ee more context, or set value to (b)ase, (e)dited, or (d)atabase? ";
+
+ my $answer = (scalar <STDIN>) // "";
+ print "\n";
+
+ exit 1 if $answer =~ /^q/i;
+ return "base" if $answer =~ /^b/i;
+ return "edited" if $answer =~ /^e/i;
+ return "current" if $answer =~ /^d/i;
+
+ if ($answer =~ /^s/i) {
+ my %values = %{ $record->{values} };
+ delete $values{$_} for grep { ($values{$_}//"") eq ""} keys %values;
+ for ('base', 'edited', ['current', \%values]) {
+ my ($key, $record) = ref($_)
+ ? @$_
+ : ($_, $args{$_ . '_record'});
+ print "\u$key:\n";
+ print " $_\n"
+ for split /\n/, JSON->new->pretty->canonical->encode($record);
+ }
+ }
+ }
+}
+
commit 01de263883dd5f7369b9b8167a81372976360437
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 17:46:47 2017 +0000
Add CLI opt parser and docs for rt-merge-initialdata
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 38f81f790..1a2ebeda3 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -79,12 +79,27 @@ use RT;
RT::LoadConfig();
RT::Init();
+use Getopt::Long;
+use Pod::Usage qw//;
use List::MoreUtils 'uniq';
use JSON ();
my $JSON = JSON->new->canonical;
-die "usage: $0 base edited\n" unless @ARGV == 2;
-my ($base_file, $edited_file) = @ARGV;
+my %OPT;
+GetOptions(
+ \%OPT,
+ "help|h",
+
+ "base=s",
+ "edited=s",
+);
+
+exit Pod::Usage::pod2usage(-verbose => 1) if $OPT{help};
+
+my $base_file = $OPT{base};
+my $edited_file = $OPT{edited};
+
+exit Pod::Usage::pod2usage unless $base_file && $edited_file;
my $base_records = slurp_json($base_file);
my $edited_records = slurp_json($edited_file);
@@ -316,3 +331,51 @@ sub decide_merge {
}
}
+=head1 NAME
+
+rt-merge-initialdata - Merge changes from an edited initialdata
+
+=head1 SYNOPSIS
+
+ rt-merge-initialdata --base initialdata.json --edited updated.json
+
+
+This tool allows you to edit a JSON-based initialdata file and merge
+those changes into your RT instance. The intended workflow is you first
+create an initialdata from your running RT config with:
+
+ sbin/rt-serializer --sync --format JSON
+
+Then, copy the resulting C<initialdata.json> into a new file. Edit that
+C<updated.json> to create, update, and delete records (see below for
+considerations).
+
+Then run F<rt-merge-initialdata> like so:
+
+ rt-merge-initialdata --base initialdata.json --edited updated.json
+
+F<rt-merge-initialdata> will update any records that you edited, create
+any records you added, and disable (or delete) any records you removed.
+Note that the C<id> field is used to track identity across the base,
+edited, and database versions of the same record, so take care when
+dealing with it. (Any record without an C<id> is treated as a new one to
+be created, and any record in the base which does not have a
+corresponding C<id> in the edited file will be disabled/deleted).
+
+=head2 OPTIONS
+
+=over
+
+=item B<--base> I<filename>
+
+The filename of the "base" initialdata. This should be a pristine initialdata
+file exported by F<sbin/rt-serializer>.
+
+=item B<--edited> I<filename>
+
+The filename of the "edited" initialdata. This should be a copy of the
+filename provided to C<--base> with your edits made to it.
+
+=back
+
+=cut
commit d2fade7ae4bb481f4de10349dc16070d31acc2de
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 17:47:34 2017 +0000
Add --merge-strategy flag
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 1a2ebeda3..0d83ff0be 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -92,6 +92,7 @@ GetOptions(
"base=s",
"edited=s",
+ "merge-strategy=s",
);
exit Pod::Usage::pod2usage(-verbose => 1) if $OPT{help};
@@ -101,6 +102,13 @@ my $edited_file = $OPT{edited};
exit Pod::Usage::pod2usage unless $base_file && $edited_file;
+my $merge_strategy = $OPT{'merge-strategy'};
+
+die "Invalid value for --merge-strategy. Expected 'base', 'current', 'edited', or 'quit'."
+ unless !$merge_strategy || $merge_strategy =~ /^(base|current|edited|quit)$/;
+
+RT->Config->Set( LogToSTDERR => $OPT{log} ) if $OPT{log};
+
my $base_records = slurp_json($base_file);
my $edited_records = slurp_json($edited_file);
@@ -306,6 +314,13 @@ sub decide_merge {
print " Base value: $args{base_value}\n";
print " Edited value: $args{edited_value}\n";
print " Database value: $args{current_value}\n";
+
+ if ($merge_strategy) {
+ print "Using specified merge strategy '$merge_strategy'.\n" unless $OPT{quiet};
+ exit if $merge_strategy eq 'quit';
+ return $merge_strategy;
+ }
+
print "Would you like to (q)uit, (s)ee more context, or set value to (b)ase, (e)dited, or (d)atabase? ";
my $answer = (scalar <STDIN>) // "";
@@ -362,6 +377,13 @@ dealing with it. (Any record without an C<id> is treated as a new one to
be created, and any record in the base which does not have a
corresponding C<id> in the edited file will be disabled/deleted).
+Any changes made in the database after the base was exported will be
+untouched by F<rt-merge-initialdata>. If it turns out that a field was
+changed in both the edited initialdata I<and> the database, then a
+conflict will be presented to you with a prompt asking which version to
+keep. The C<--merge-strategy> flag lets you decide in advance to handle
+all such conflicts using a consistent strategy, to avoid prompting.
+
=head2 OPTIONS
=over
@@ -376,6 +398,35 @@ file exported by F<sbin/rt-serializer>.
The filename of the "edited" initialdata. This should be a copy of the
filename provided to C<--base> with your edits made to it.
+=item B<--merge-strategy> I<strategy>
+
+Automatically resolve all conflicts without prompting the user. The
+following merge strategies are available:
+
+=over 4
+
+=item C<edited>
+
+Take the edits as specified in the edited initialdata, overwriting the
+change made in the database after the base was exported.
+
+=item C<current>
+
+Keep the database's current value, ignoring the change made in the
+edited initialdata.
+
+=item C<base>
+
+Revert to the base initialdata's value, rolling back the change made in
+the database and ignoring the change made in the edited initialdata.
+
+=item C<quit>
+
+Abort processing at the first conflict. Note that this will likely
+result in a subset of the edits being applied to your database.
+
+=back
+
=back
=cut
commit e748fd600221a23cc092fa0bbbee9577865c5ac4
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 17:47:50 2017 +0000
Add --quiet and --log options
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 0d83ff0be..04fb1900e 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -89,6 +89,8 @@ my %OPT;
GetOptions(
\%OPT,
"help|h",
+ "quiet|q!",
+ "log=s",
"base=s",
"edited=s",
@@ -133,6 +135,7 @@ for my $type (@record_types) {
$edited_records->{$type},
$type,
);
+ RT->Logger->info("Merging changes from $type: " . scalar(@$new_records) . " new records, " . scalar(@$updated_records) . " updated, " . scalar(@$deleted_records) . " deleted");
my $collection_class = "RT::" . ($class_type{$type} || $type);
my $record_class = $collection_class->RecordClass;
@@ -309,11 +312,13 @@ sub decide_merge {
local $| = 1;
while (1) {
- print "\n";
- print "Conflict resolution required for $type #$id $args{column}:\n";
- print " Base value: $args{base_value}\n";
- print " Edited value: $args{edited_value}\n";
- print " Database value: $args{current_value}\n";
+ unless ($OPT{quiet} && $merge_strategy) {
+ print "\n";
+ print "Conflict resolution required for $type #$id $args{column}:\n";
+ print " Base value: $args{base_value}\n";
+ print " Edited value: $args{edited_value}\n";
+ print " Database value: $args{current_value}\n";
+ }
if ($merge_strategy) {
print "Using specified merge strategy '$merge_strategy'.\n" unless $OPT{quiet};
@@ -427,6 +432,14 @@ result in a subset of the edits being applied to your database.
=back
+=item B<--quiet>
+
+Avoid nonessential output.
+
+=item B<--log> I<level>
+
+Adjust LogToSTDERR config option.
+
=back
=cut
commit fdc8bb32e8cca3e09cdbbd502ef42a070c5c152d
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 17:58:12 2017 +0000
Add --dryrun flag
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 04fb1900e..f76c9ec7a 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -77,7 +77,6 @@ BEGIN {
use RT;
RT::LoadConfig();
-RT::Init();
use Getopt::Long;
use Pod::Usage qw//;
@@ -91,6 +90,7 @@ GetOptions(
"help|h",
"quiet|q!",
"log=s",
+ "dryrun",
"base=s",
"edited=s",
@@ -111,6 +111,11 @@ die "Invalid value for --merge-strategy. Expected 'base', 'current', 'edited', o
RT->Config->Set( LogToSTDERR => $OPT{log} ) if $OPT{log};
+RT::Init();
+
+my $dryrun = $OPT{'dryrun'};
+RT->Logger->debug("dryrun enabled") if $dryrun;
+
my $base_records = slurp_json($base_file);
my $edited_records = slurp_json($edited_file);
@@ -140,7 +145,12 @@ for my $type (@record_types) {
my $collection_class = "RT::" . ($class_type{$type} || $type);
my $record_class = $collection_class->RecordClass;
- $new_for_insertdata{$type} = $new_records;
+ if ($dryrun) {
+ RT->Logger->debug("Skipping create of " . scalar(@$new_records) . "x $type because of dry run.") if @$new_records;
+ }
+ else {
+ $new_for_insertdata{$type} = $new_records;
+ }
for (@$updated_records) {
my ($base, $edited) = @$_;
@@ -193,6 +203,11 @@ for my $type (@record_types) {
}
}
+ if ($dryrun) {
+ RT->Logger->debug("Skipping update $record_class #$id $column from '$current_value' to '$new_value' because of dry run.");
+ next;
+ }
+
my ($ok, $msg) = $record->$method($new_value);
if ($ok) {
RT->Logger->debug("Updated $record_class #$id $column: $msg");
@@ -218,9 +233,17 @@ for my $type (@record_types) {
my ($ok, $msg);
if ($record->can('SetDisabled') || $record->_Accessible('Disabled', 'write')) {
+ if ($dryrun) {
+ RT->Logger->debug("Skipping disabling of $record_class #$id because of dry run.");
+ next;
+ }
($ok, $msg) = $record->SetDisabled(1);
}
elsif ($record->can('Delete')) {
+ if ($dryrun) {
+ RT->Logger->debug("Skipping delete of $record_class #$id because of dry run.");
+ next;
+ }
($ok, $msg) = $record->Delete;
}
else {
@@ -236,8 +259,10 @@ for my $type (@record_types) {
}
}
-my $new_json = $JSON->encode(\%new_for_insertdata);
-$RT::Handle->InsertData(\$new_json, undef, disconnect_after => 0);
+if (grep { scalar(@$_) } values %new_for_insertdata) {
+ my $new_json = $JSON->encode(\%new_for_insertdata);
+ $RT::Handle->InsertData(\$new_json, undef, disconnect_after => 0);
+}
sub find_differences {
my $base_records = shift;
@@ -440,6 +465,12 @@ Avoid nonessential output.
Adjust LogToSTDERR config option.
+=item B<--dryrun>
+
+Attempt to process changes but skip making any actual changes to the
+database. Changes that would have been made are logged at level C<debug>
+so you may wish to pass C<--log=debug> to see them.
+
=back
=cut
commit a3016f3d79fa0a0d845a65e1e6db0347bdcc526f
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 20:43:07 2017 +0000
Avoid spurious 0 0 0 changes log entries
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index f76c9ec7a..6f03b84dc 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -140,6 +140,11 @@ for my $type (@record_types) {
$edited_records->{$type},
$type,
);
+
+ next if !@$new_records
+ && !@$updated_records
+ && !@$deleted_records;
+
RT->Logger->info("Merging changes from $type: " . scalar(@$new_records) . " new records, " . scalar(@$updated_records) . " updated, " . scalar(@$deleted_records) . " deleted");
my $collection_class = "RT::" . ($class_type{$type} || $type);
commit 0e930b614c6e11641aad034cd76efeabaceab9a8
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 20:43:40 2017 +0000
Factor out updating records (or not due to dryrun) and reporting results
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 6f03b84dc..ab43a178a 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -132,6 +132,23 @@ my %class_type = (
Members => 'GroupMembers',
);
+sub update_record {
+ my ($record, $description, $cb) = @_;
+
+ if ($dryrun) {
+ RT->Logger->debug("Skipping $description because of dry run.");
+ return;
+ }
+
+ my ($ok, $msg) = $cb->();
+ if ($ok) {
+ RT->Logger->debug("\u$description: $msg");
+ }
+ else {
+ RT->Logger->error("Unable to $description: $msg");
+ }
+}
+
my %new_for_insertdata;
for my $type (@record_types) {
@@ -208,18 +225,9 @@ for my $type (@record_types) {
}
}
- if ($dryrun) {
- RT->Logger->debug("Skipping update $record_class #$id $column from '$current_value' to '$new_value' because of dry run.");
- next;
- }
-
- my ($ok, $msg) = $record->$method($new_value);
- if ($ok) {
- RT->Logger->debug("Updated $record_class #$id $column: $msg");
- }
- else {
- RT->Logger->error("Unable to update $record_class #$id $column: $msg");
- }
+ update_record $record, "update $record_class #$id $column from '$current_value' to '$new_value'" => sub {
+ $record->$method($new_value);
+ };
}
else {
die "Unable to handle updating $record_class $column (no method)";
@@ -236,31 +244,19 @@ for my $type (@record_types) {
my $record = $record_class->new(RT->SystemUser);
$record->Load($id);
- my ($ok, $msg);
if ($record->can('SetDisabled') || $record->_Accessible('Disabled', 'write')) {
- if ($dryrun) {
- RT->Logger->debug("Skipping disabling of $record_class #$id because of dry run.");
- next;
- }
- ($ok, $msg) = $record->SetDisabled(1);
+ update_record $record, "disable $record_class #$id" => sub {
+ $record->SetDisabled(1);
+ };
}
elsif ($record->can('Delete')) {
- if ($dryrun) {
- RT->Logger->debug("Skipping delete of $record_class #$id because of dry run.");
- next;
- }
- ($ok, $msg) = $record->Delete;
+ update_record $record, "delete $record_class #$id" => sub {
+ $record->Delete;
+ };
}
else {
die "No method to delete $record_class #$id";
}
-
- if ($ok) {
- RT->Logger->debug("Deleted $record_class #$id: $msg");
- }
- else {
- RT->Logger->error("Unable to delete $record_class #$id: $msg");
- }
}
}
commit 8c9141541148a896b59c06e572836d8eff740f57
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 20:45:16 2017 +0000
Merge changes to CustomField.ApplyTo
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index ab43a178a..33f9c2f17 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -149,6 +149,90 @@ sub update_record {
}
}
+my %special_merger = (
+ CustomFields => {
+ ApplyTo => sub {
+ my %args = @_;
+ my $cf = $args{current_record};
+ my $name = $cf->Name;
+
+ my @base_queues = ref $args{base_value}
+ ? @{ $args{base_value} }
+ : $args{base_value};
+ my @edited_queues = ref $args{edited_value}
+ ? @{ $args{edited_value} }
+ : $args{edited_value};
+
+ # canonicalize to IDs
+ for (@base_queues, @edited_queues) {
+ next if $_ eq '0';
+
+ my $queue = RT::Queue->new(RT->SystemUser);
+ my ($ok, $msg) = $queue->Load($_);
+ die "Unable to load queue $_: $msg" if !$ok;
+ $_ = $queue->Id;
+ }
+
+ @base_queues = sort { $a <=> $b } @base_queues;
+ @edited_queues = sort { $a <=> $b } @edited_queues;
+
+ my (@add, @remove);
+
+ while (@base_queues || @edited_queues) {
+ my $base_queue = $base_queues[0];
+ my $edited_queue = $edited_queues[0];
+
+ # same queue, continue with no change
+ if (($base_queue//-1) == ($edited_queue//-1)) {
+ shift @base_queues;
+ shift @edited_queues;
+ }
+ # ran out of base queues, add edited queue
+ elsif (!defined($base_queue)) {
+ shift @edited_queues;
+ push @add, $edited_queue;
+ }
+ # ran out of edited queues, remove base queue
+ elsif (!defined($edited_queue)) {
+ shift @base_queues;
+ push @remove, $base_queue;
+ }
+ # there's a base queue not in edited, so remove
+ elsif ($base_queue < $edited_queue) {
+ shift @base_queues;
+ push @remove, $base_queue;
+ }
+ # there's a edited queue not in base, so add
+ elsif ($edited_queue < $base_queue) {
+ shift @edited_queues;
+ push @add, $edited_queue;
+ }
+ else {
+ die "Should never happen";
+ }
+ }
+
+ for my $id (@add) {
+ my $queue = RT::Queue->new(RT->SystemUser);
+ $queue->Load($id) if $id;
+ my $queue_name = $id ? "to queue " . $queue->Name : "globally";
+ update_record $cf, "add custom field $name $queue_name" => sub {
+ $cf->AddToObject($queue);
+ };
+ }
+
+ for my $id (@remove) {
+ my $queue = RT::Queue->new(RT->SystemUser);
+ $queue->Load($id) if $id;
+ my $queue_name = $id ? "from queue " . $queue->Name : "globally";
+ update_record $cf, "remove custom field $name $queue_name" => sub {
+ $cf->RemoveFromObject($queue);
+ };
+ }
+ }
+ },
+);
+
my %new_for_insertdata;
for my $type (@record_types) {
@@ -194,7 +278,17 @@ for my $type (@record_types) {
next if is_deeply_equal($base_value, $edited_value);
- if (!ref($base_value) && !ref($edited_value)) {
+ if (my $merger = $special_merger{$type}{$column}) {
+ $merger->(
+ base_record => $base,
+ edited_record => $edited,
+ current_record => $record,
+ column => $column,
+ base_value => $base_value,
+ edited_value => $edited_value,
+ );
+ }
+ elsif (!ref($base_value) && !ref($edited_value)) {
my $method = "Set$column";
if ($record->can($method) || $record->_Accessible($column, 'write')) {
my $current_value = $record->$column // "";
commit 97ea644373748348dada39deda9a91cfb1f571b1
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 20:56:10 2017 +0000
Refactor CustomField ApplyTo merger to reuse for Class ApplyTo
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index 33f9c2f17..d56af0243 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -149,87 +149,104 @@ sub update_record {
}
}
+sub _object_merger {
+ my %args = @_;
+ my $primary = $args{current_record};
+ my $primary_class = ref($primary);
+ my $secondary_class = $args{secondary_class};
+ my $name = $primary->Name;
+
+ my @base_objects = ref $args{base_value}
+ ? @{ $args{base_value} }
+ : $args{base_value};
+ my @edited_objects = ref $args{edited_value}
+ ? @{ $args{edited_value} }
+ : $args{edited_value};
+
+ # canonicalize to IDs
+ for (@base_objects, @edited_objects) {
+ next if $_ eq '0';
+
+ my $secondary = $secondary_class->new(RT->SystemUser);
+ my ($ok, $msg) = $secondary->Load($_);
+ die "Unable to load $secondary_class $_: $msg" if !$ok;
+ $_ = $secondary->Id;
+ }
+
+ @base_objects = sort { $a <=> $b } @base_objects;
+ @edited_objects = sort { $a <=> $b } @edited_objects;
+
+ my (@add, @remove);
+
+ while (@base_objects || @edited_objects) {
+ my $base_object = $base_objects[0];
+ my $edited_object = $edited_objects[0];
+
+ # same queue, continue with no change
+ if (($base_object//-1) == ($edited_object//-1)) {
+ shift @base_objects;
+ shift @edited_objects;
+ }
+ # ran out of base queues, add edited queue
+ elsif (!defined($base_object)) {
+ shift @edited_objects;
+ push @add, $edited_object;
+ }
+ # ran out of edited queues, remove base queue
+ elsif (!defined($edited_object)) {
+ shift @base_objects;
+ push @remove, $base_object;
+ }
+ # there's a base queue not in edited, so remove
+ elsif ($base_object < $edited_object) {
+ shift @base_objects;
+ push @remove, $base_object;
+ }
+ # there's a edited queue not in base, so add
+ elsif ($edited_object < $base_object) {
+ shift @edited_objects;
+ push @add, $edited_object;
+ }
+ else {
+ die "Should never happen";
+ }
+ }
+
+ for my $id (@add) {
+ my $secondary = $secondary_class->new(RT->SystemUser);
+ $secondary->Load($id) if $id;
+ my $secondary_name = $id ? "to $secondary_class " . $secondary->Name : "globally";
+ update_record $primary, "add $primary_class $name $secondary_name" => sub {
+ $primary->AddToObject($secondary);
+ };
+ }
+
+ for my $id (@remove) {
+ my $secondary = $secondary_class->new(RT->SystemUser);
+ $secondary->Load($id) if $id;
+ my $secondary_name = $id ? "from $secondary_class " . $secondary->Name : "globally";
+ update_record $primary, "remove $primary_class $name $secondary_name" => sub {
+ $primary->RemoveFromObject($secondary);
+ };
+ }
+}
+
my %special_merger = (
CustomFields => {
ApplyTo => sub {
- my %args = @_;
- my $cf = $args{current_record};
- my $name = $cf->Name;
-
- my @base_queues = ref $args{base_value}
- ? @{ $args{base_value} }
- : $args{base_value};
- my @edited_queues = ref $args{edited_value}
- ? @{ $args{edited_value} }
- : $args{edited_value};
-
- # canonicalize to IDs
- for (@base_queues, @edited_queues) {
- next if $_ eq '0';
-
- my $queue = RT::Queue->new(RT->SystemUser);
- my ($ok, $msg) = $queue->Load($_);
- die "Unable to load queue $_: $msg" if !$ok;
- $_ = $queue->Id;
- }
-
- @base_queues = sort { $a <=> $b } @base_queues;
- @edited_queues = sort { $a <=> $b } @edited_queues;
-
- my (@add, @remove);
-
- while (@base_queues || @edited_queues) {
- my $base_queue = $base_queues[0];
- my $edited_queue = $edited_queues[0];
-
- # same queue, continue with no change
- if (($base_queue//-1) == ($edited_queue//-1)) {
- shift @base_queues;
- shift @edited_queues;
- }
- # ran out of base queues, add edited queue
- elsif (!defined($base_queue)) {
- shift @edited_queues;
- push @add, $edited_queue;
- }
- # ran out of edited queues, remove base queue
- elsif (!defined($edited_queue)) {
- shift @base_queues;
- push @remove, $base_queue;
- }
- # there's a base queue not in edited, so remove
- elsif ($base_queue < $edited_queue) {
- shift @base_queues;
- push @remove, $base_queue;
- }
- # there's a edited queue not in base, so add
- elsif ($edited_queue < $base_queue) {
- shift @edited_queues;
- push @add, $edited_queue;
- }
- else {
- die "Should never happen";
- }
- }
-
- for my $id (@add) {
- my $queue = RT::Queue->new(RT->SystemUser);
- $queue->Load($id) if $id;
- my $queue_name = $id ? "to queue " . $queue->Name : "globally";
- update_record $cf, "add custom field $name $queue_name" => sub {
- $cf->AddToObject($queue);
- };
- }
-
- for my $id (@remove) {
- my $queue = RT::Queue->new(RT->SystemUser);
- $queue->Load($id) if $id;
- my $queue_name = $id ? "from queue " . $queue->Name : "globally";
- update_record $cf, "remove custom field $name $queue_name" => sub {
- $cf->RemoveFromObject($queue);
- };
- }
- }
+ _object_merger(
+ secondary_class => 'RT::Queue',
+ @_,
+ );
+ },
+ },
+ Classes => {
+ ApplyTo => sub {
+ _object_merger(
+ secondary_class => 'RT::Queue',
+ @_,
+ );
+ },
},
);
commit e699b0ba35723c9c32d8573be5b6e1fc28b17fc5
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Fri Mar 24 20:58:37 2017 +0000
uniq ApplyTo objects, just in case
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index d56af0243..c7403d895 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -173,8 +173,8 @@ sub _object_merger {
$_ = $secondary->Id;
}
- @base_objects = sort { $a <=> $b } @base_objects;
- @edited_objects = sort { $a <=> $b } @edited_objects;
+ @base_objects = uniq sort { $a <=> $b } @base_objects;
+ @edited_objects = uniq sort { $a <=> $b } @edited_objects;
my (@add, @remove);
commit 33c3a4a355980ca6f23cd07c5f4407add0052cea
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 16:55:40 2017 +0000
Add FollowDisabled option to serializer
diff --git a/lib/RT/Migrate/Serializer.pm b/lib/RT/Migrate/Serializer.pm
index 50a27b94e..dccaa84cf 100644
--- a/lib/RT/Migrate/Serializer.pm
+++ b/lib/RT/Migrate/Serializer.pm
@@ -66,6 +66,7 @@ sub Init {
AllUsers => 1,
AllGroups => 1,
FollowDeleted => 1,
+ FollowDisabled => 1,
FollowScrips => 0,
FollowTickets => 1,
@@ -86,6 +87,7 @@ sub Init {
AllUsers
AllGroups
FollowDeleted
+ FollowDisabled
FollowScrips
FollowTickets
FollowAssets
@@ -197,7 +199,7 @@ sub PushCollections {
$class->require or next;
my $collection = $class->new( RT->SystemUser );
- $collection->FindAllRows; # be explicit
+ $collection->FindAllRows if $self->{FollowDisabled};
$collection->CleanSlate; # some collections (like groups and users) join in _Init
$collection->UnLimit;
$collection->OrderBy( FIELD => 'id' );
@@ -398,6 +400,15 @@ sub Process {
return $self->Visit(%args);
}
+ if (!$self->{FollowDisabled}) {
+ return if ($obj->can('Disabled') || $obj->_Accessible('Disabled', 'read'))
+ && $obj->Disabled
+
+ # Disabled for OCFV means "old value" which we want to keep
+ # in the history
+ && !$obj->isa('RT::ObjectCustomFieldValue');
+ }
+
return $self->SUPER::Process( @_ );
}
commit 232b6dbad5b77cacc0c2a0f65a41169f10adea78
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 18:42:53 2017 +0000
Avoid serializing ACLs and GroupMembers if either parent is disabled
diff --git a/lib/RT/Migrate/Serializer.pm b/lib/RT/Migrate/Serializer.pm
index dccaa84cf..920d42a18 100644
--- a/lib/RT/Migrate/Serializer.pm
+++ b/lib/RT/Migrate/Serializer.pm
@@ -407,6 +407,18 @@ sub Process {
# Disabled for OCFV means "old value" which we want to keep
# in the history
&& !$obj->isa('RT::ObjectCustomFieldValue');
+
+ if ($obj->isa('RT::ACE')) {
+ my $principal = $obj->PrincipalObj;
+ return if $principal->Disabled;
+
+ # [issues.bestpractical.com #32662]
+ return if $principal->Object->Domain eq 'ACLEquivalence'
+ && $principal->Object->InstanceObj->Disabled;
+
+ return if !$obj->Object->isa('RT::System')
+ && $obj->Object->Disabled;
+ }
}
return $self->SUPER::Process( @_ );
@@ -458,6 +470,17 @@ sub Observe {
return 0 if $obj->Status eq "deleted" and not $self->{FollowDeleted};
return $self->{FollowAssets};
} elsif ($obj->isa("RT::ACE")) {
+ if (!$self->{FollowDisabled}) {
+ my $principal = $obj->PrincipalObj;
+ return 0 if $principal->Disabled;
+
+ # [issues.bestpractical.com #32662]
+ return 0 if $principal->Object->Domain eq 'ACLEquivalence'
+ && $principal->Object->InstanceObj->Disabled;
+
+ return 0 if !$obj->Object->isa('RT::System')
+ && $obj->Object->Disabled;
+ }
return $self->{FollowACL};
} elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) {
return $self->{FollowScrips};
@@ -468,6 +491,10 @@ sub Observe {
} elsif ($grp->Domain eq "SystemInternal") {
return 0 if $grp->UID eq $from;
}
+ if (!$self->{FollowDisabled}) {
+ return 0 if $grp->Disabled
+ || $obj->MemberObj->Disabled;
+ }
}
return 1;
commit 91891f4b49890d5deca6d05d999dfe0795693c5e
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 19:28:34 2017 +0000
Avoid error on broken ACLEquivalence principals
If you've deleted users directly from the database but haven't deleted
their ACLEquivalence principal or group, then this was exploding trying
to load a principal for an empty RT::User object
diff --git a/lib/RT/Migrate/Serializer.pm b/lib/RT/Migrate/Serializer.pm
index 920d42a18..7d933f304 100644
--- a/lib/RT/Migrate/Serializer.pm
+++ b/lib/RT/Migrate/Serializer.pm
@@ -414,7 +414,8 @@ sub Process {
# [issues.bestpractical.com #32662]
return if $principal->Object->Domain eq 'ACLEquivalence'
- && $principal->Object->InstanceObj->Disabled;
+ && (!$principal->Object->InstanceObj->Id
+ || $principal->Object->InstanceObj->Disabled);
return if !$obj->Object->isa('RT::System')
&& $obj->Object->Disabled;
@@ -475,8 +476,9 @@ sub Observe {
return 0 if $principal->Disabled;
# [issues.bestpractical.com #32662]
- return 0 if $principal->Object->Domain eq 'ACLEquivalence'
- && $principal->Object->InstanceObj->Disabled;
+ return if $principal->Object->Domain eq 'ACLEquivalence'
+ && (!$principal->Object->InstanceObj->Id
+ || $principal->Object->InstanceObj->Disabled);
return 0 if !$obj->Object->isa('RT::System')
&& $obj->Object->Disabled;
commit 7ae4502e1b0e8215c909983e10b9d446543accf1
Author: sunnavy <sunnavy at bestpractical.com>
Date: Sat Aug 4 17:08:18 2018 +0800
Sort OCFV values to keep the original id order
diff --git a/lib/RT/Migrate/Serializer/JSON.pm b/lib/RT/Migrate/Serializer/JSON.pm
index 7e228ffec..80d17a292 100644
--- a/lib/RT/Migrate/Serializer/JSON.pm
+++ b/lib/RT/Migrate/Serializer/JSON.pm
@@ -417,7 +417,7 @@ sub CanonicalizeObjectCustomFieldValues {
my $self = shift;
my $records = delete $self->{Records}{'RT::ObjectCustomFieldValue'};
- for my $id (keys %$records) {
+ for my $id ( sort { $records->{$a}{id} <=> $records->{$b}{id} } keys %$records ) {
my $record = $records->{$id};
if ($record->{Disabled} && !$self->{FollowDisabled}) {
commit f5e17ed632d1150919f6383e6a3e34716e36f3b0
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Wed Mar 22 16:55:40 2017 +0000
Add a --no-disabled flag to serializer
diff --git a/sbin/rt-serializer.in b/sbin/rt-serializer.in
index 79fea41c3..67749d0ed 100644
--- a/sbin/rt-serializer.in
+++ b/sbin/rt-serializer.in
@@ -99,6 +99,7 @@ GetOptions(
"users!",
"groups!",
+ "disabled!",
"deleted!",
"scrips!",
@@ -123,9 +124,10 @@ $args{Directory} = $OPT{directory};
$args{Force} = $OPT{force};
$args{MaxFileSize} = $OPT{size} if $OPT{size};
-$args{AllUsers} = $OPT{users} if defined $OPT{users};
-$args{AllGroups} = $OPT{groups} if defined $OPT{groups};
-$args{FollowDeleted} = $OPT{deleted} if defined $OPT{deleted};
+$args{AllUsers} = $OPT{users} if defined $OPT{users};
+$args{AllGroups} = $OPT{groups} if defined $OPT{groups};
+$args{FollowDeleted} = $OPT{deleted} if defined $OPT{deleted};
+$args{FollowDisabled} = $OPT{disabled} if defined $OPT{disabled};
$args{FollowScrips} = $OPT{scrips} if defined $OPT{scrips};
$args{FollowTickets} = $OPT{tickets} if defined $OPT{tickets};
@@ -181,7 +183,7 @@ if ($OPT{'limit-cfs'}) {
}
if (($OPT{clone} or $OPT{incremental})
- and grep { /^(users|groups|deleted|scrips|tickets|acls|assets)$/ } keys %OPT) {
+ and grep { /^(users|groups|deleted|disabled|scrips|tickets|acls|assets)$/ } keys %OPT) {
die "You cannot specify object types when cloning.\n\nPlease see $0 --help.\n";
}
@@ -356,6 +358,12 @@ consistency.
By default, all assets are serialized; passing C<--no-assets> skips
assets during serialization.
+=item B<--no-disabled>
+
+By default, all queues, custom fields, etc, including disabled ones, are
+serialized; passing C<--no-disabled> skips such disabled records during
+serialization.
+
=item B<--no-deleted>
By default, all tickets and assets, including deleted ones, are
commit 87d4019049f227f6de6bac158935bf40a4fb0abc
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Tue Mar 14 19:43:33 2017 +0000
Add --no-transactions flag to rt-serializer
diff --git a/lib/RT/Migrate/Serializer.pm b/lib/RT/Migrate/Serializer.pm
index 7d933f304..03b38ad46 100644
--- a/lib/RT/Migrate/Serializer.pm
+++ b/lib/RT/Migrate/Serializer.pm
@@ -70,6 +70,7 @@ sub Init {
FollowScrips => 0,
FollowTickets => 1,
+ FollowTransactions => 1,
FollowACL => 0,
FollowAssets => 1,
@@ -90,6 +91,7 @@ sub Init {
FollowDisabled
FollowScrips
FollowTickets
+ FollowTransactions
FollowAssets
FollowACL
Queues
@@ -484,6 +486,8 @@ sub Observe {
&& $obj->Object->Disabled;
}
return $self->{FollowACL};
+ } elsif ($obj->isa("RT::Transaction")) {
+ return $self->{FollowTransactions};
} elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) {
return $self->{FollowScrips};
} elsif ($obj->isa("RT::GroupMember")) {
diff --git a/sbin/rt-serializer.in b/sbin/rt-serializer.in
index 67749d0ed..4fdd6092d 100644
--- a/sbin/rt-serializer.in
+++ b/sbin/rt-serializer.in
@@ -104,6 +104,7 @@ GetOptions(
"scrips!",
"tickets!",
+ "transactions!",
"acls!",
"limit-queues=s@",
"limit-cfs=s@",
@@ -129,9 +130,10 @@ $args{AllGroups} = $OPT{groups} if defined $OPT{groups};
$args{FollowDeleted} = $OPT{deleted} if defined $OPT{deleted};
$args{FollowDisabled} = $OPT{disabled} if defined $OPT{disabled};
-$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{FollowScrips} = $OPT{scrips} if defined $OPT{scrips};
+$args{FollowTickets} = $OPT{tickets} if defined $OPT{tickets};
+$args{FollowTransactions} = $OPT{transactions} if defined $OPT{transactions};
+$args{FollowACL} = $OPT{acls} if defined $OPT{acls};
$args{HyperlinkUnmigrated} = $OPT{'hyperlink-unmigrated'} if defined $OPT{'hyperlink-unmigrated'};
@@ -183,7 +185,7 @@ if ($OPT{'limit-cfs'}) {
}
if (($OPT{clone} or $OPT{incremental})
- and grep { /^(users|groups|deleted|disabled|scrips|tickets|acls|assets)$/ } keys %OPT) {
+ and grep { /^(users|groups|deleted|disabled|scrips|tickets|transactions|acls|assets)$/ } keys %OPT) {
die "You cannot specify object types when cloning.\n\nPlease see $0 --help.\n";
}
@@ -402,6 +404,10 @@ 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<--no-transactions>
+
+Skip serialization of all transactions on any records (not just tickets).
+
=item B<--clone>
Serializes your entire database, creating a clone. This option should
commit 819e1d369cbe2de640362c1d1c1c6f026d01aa01
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Aug 6 21:17:18 2018 +0800
Initial rt-dump-initialdata
diff --git a/.gitignore b/.gitignore
index e6da7df18..4009d80a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@
/sbin/rt-attributes-viewer
/sbin/rt-clean-sessions
/sbin/rt-dump-database
+/sbin/rt-dump-initialdata
/sbin/rt-dump-metadata
/sbin/rt-email-dashboards
/sbin/rt-email-digest
diff --git a/Makefile.in b/Makefile.in
index f20edc946..cd615762e 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -140,6 +140,7 @@ BINARIES = $(RT_MAILGATE_BIN) \
SYSTEM_BINARIES = rt-attributes-viewer \
rt-clean-sessions \
+ rt-dump-initialdata \
rt-dump-metadata \
rt-email-dashboards \
rt-email-digest \
diff --git a/configure.ac b/configure.ac
index 6eae1aac6..ae647a39b 100755
--- a/configure.ac
+++ b/configure.ac
@@ -467,6 +467,7 @@ AC_CONFIG_FILES([
sbin/rt-attributes-viewer
sbin/rt-preferences-viewer
sbin/rt-session-viewer
+ sbin/rt-dump-initialdata
sbin/rt-dump-metadata
sbin/rt-setup-database
sbin/rt-test-dependencies
diff --git a/sbin/rt-dump-initialdata.in b/sbin/rt-dump-initialdata.in
new file mode 100644
index 000000000..af282797a
--- /dev/null
+++ b/sbin/rt-dump-initialdata.in
@@ -0,0 +1,356 @@
+#!@PERL@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# 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.
+#
+# 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.
+#
+# 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
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (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.)
+#
+# 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.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+# fix lib paths, some may be relative
+BEGIN {
+ require File::Spec;
+ my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
+ my $bin_path;
+
+ for my $lib (@libs) {
+ unless ( File::Spec->file_name_is_absolute($lib) ) {
+ unless ($bin_path) {
+ if ( File::Spec->file_name_is_absolute(__FILE__) ) {
+ $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
+ }
+ else {
+ require FindBin;
+ no warnings "once";
+ $bin_path = $FindBin::Bin;
+ }
+ }
+ $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
+ }
+ unshift @INC, $lib;
+ }
+
+}
+
+use RT;
+RT::LoadConfig();
+RT::Init();
+
+ at RT::Record::ISA = qw( DBIx::SearchBuilder::Record RT::Base );
+
+use RT::Migrate;
+use RT::Migrate::Serializer::JSON;
+use Getopt::Long;
+use Pod::Usage qw//;
+use Time::HiRes qw//;
+
+my %OPT;
+GetOptions(
+ \%OPT,
+ "help|?",
+ "verbose|v!",
+ "quiet|q!",
+
+ "directory|d=s",
+ "force|f!",
+ "size|s=i",
+
+ "users!",
+ "groups!",
+ "deleted!",
+
+ "scrips!",
+ "acls!",
+ "assets!",
+
+ "sync",
+
+ "gc=i",
+ "page=i",
+) or Pod::Usage::pod2usage();
+
+Pod::Usage::pod2usage(-verbose => 1) if $OPT{help};
+
+my %args;
+$args{Directory} = $OPT{directory};
+$args{Force} = $OPT{force};
+$args{MaxFileSize} = $OPT{size} if $OPT{size};
+
+$args{AllUsers} = $OPT{users} if defined $OPT{users};
+$args{AllGroups} = $OPT{groups} if defined $OPT{groups};
+$args{FollowDeleted} = $OPT{deleted} if defined $OPT{deleted};
+$args{FollowDisabled} = $OPT{disabled} if defined $OPT{disabled};
+
+$args{FollowScrips} = $OPT{scrips} if defined $OPT{scrips};
+$args{FollowACL} = $OPT{acls} if defined $OPT{acls};
+
+$args{FollowAssets} = $OPT{assets} if defined $OPT{assets};
+
+$args{Sync} = $OPT{sync} if $OPT{sync};
+
+$args{GC} = defined $OPT{gc} ? $OPT{gc} : 5000;
+$args{Page} = defined $OPT{page} ? $OPT{page} : 100;
+
+my $walker;
+
+my $gnuplot = `which gnuplot`;
+my $msg = "";
+if (-t STDOUT and not $OPT{verbose} and not $OPT{quiet}) {
+ $args{Progress} = RT::Migrate::progress(
+ top => \&gnuplot,
+ bottom => sub { print "\n$msg"; $msg = ""; },
+ counts => sub { $walker->ObjectCount },
+ bars => [
+ qw/Queue User Group GroupMember Attribute CustomField CustomFieldValue
+ ObjectCustomField ObjectCustomFieldValue Catalog Asset ACE CustomRole
+ Class Article ScripAction ScripCondition Template Scrip/
+ ],
+ max => { estimate() },
+ );
+ $args{MessageHandler} = sub {
+ print "\r", " "x60, "\r", $_[-1]; $msg = $_[-1];
+ };
+ $args{Verbose} = 0;
+}
+$args{Verbose} = 0 if $OPT{quiet};
+
+
+$walker = RT::Migrate::Serializer::JSON->new( FollowTickets => 0, FollowTransactions => 0, %args );
+
+my $log = RT::Migrate::setup_logging( $walker->{Directory} => 'initialdata.log' );
+print "Logging warnings and errors to $log\n" if $log;
+
+print "Beginning dumping initialdata...";
+my %counts = $walker->Export;
+
+my @files = $walker->Files;
+print "Wrote @{[scalar @files]} files:\n";
+print " $_\n" for @files;
+print "\n";
+
+print "Total object counts:\n";
+for (sort {$counts{$b} <=> $counts{$a}} keys %counts) {
+ printf "%8d %s\n", $counts{$_}, $_;
+}
+
+if ($log and -s $log) {
+ print STDERR "\n! Some warnings or errors occurred during initialdata dumping."
+ ."\n! Please see $log for details.\n\n";
+} else {
+ unlink $log;
+}
+
+sub estimate {
+ $| = 1;
+ my %e;
+
+ # Expected types we'll serialize
+ my @types = map { "RT::$_" } qw/
+ Queue User Group GroupMember Attribute CustomField CustomFieldValue
+ ObjectCustomField ObjectCustomFieldValue Catalog Asset ACE CustomRole
+ Class Article ScripAction ScripCondition Template Scrip/;
+
+ for my $class (@types) {
+ print "Estimating $class count...";
+ my $collection;
+ if ( $class eq 'RT::ACE' ) {
+ $collection = 'RT::ACL';
+ }
+ else {
+ $collection = $class . ( UNIVERSAL::can( $class . 'es', 'new' ) ? 'es' : 's' );
+ }
+
+ if ($collection->require) {
+ my $objs = $collection->new( RT->SystemUser );
+ $objs->FindAllRows;
+ $objs->UnLimit;
+ $objs->{allow_deleted_search} = 1 if $class eq "RT::Asset";
+ $e{$class} = $objs->DBIx::SearchBuilder::Count;
+ }
+ print "\r", " "x60, "\r";
+ }
+
+ return %e;
+}
+
+
+sub gnuplot {
+ my ($elapsed, $rows, $cols) = @_;
+ my $length = $walker->StackSize;
+ my $file = $walker->Directory . "/progress.plot";
+ open(my $dat, ">>", $file);
+ printf $dat "%10.3f\t%8d\n", $elapsed, $length;
+ close $dat;
+
+ if ($rows <= 24 or not $gnuplot) {
+ print "\n\n";
+ } elsif ($elapsed) {
+ my $gnuplot = qx|
+ gnuplot -e '
+ set term dumb $cols @{[$rows - 12]};
+ set xlabel "Seconds";
+ unset key;
+ set xrange [0:*];
+ set yrange [0:*];
+ set title "Queue length";
+ plot "$file" using 1:2 with lines
+ '
+ |;
+ if ($? == 0 and $gnuplot) {
+ $gnuplot =~ s/^(\s*\n)//;
+ print $gnuplot;
+ unlink $file;
+ } else {
+ warn "Couldn't run gnuplot (\$? == $?): $!\n";
+ }
+ } else {
+ print "\n" for 1..($rows - 13);
+ }
+}
+
+=head1 NAME
+
+rt-dump-initialdata - Serialize an RT database to disk
+
+=head1 SYNOPSIS
+
+ rt-validator --check && rt-dump-initialdata
+
+This script is used to write out the objects initialdata supports from
+RT database to disk, for later import into a different RT instance. It
+requires that the data in the database be self-consistent, in order to
+do so; please make sure that the database being exported passes
+validation by L<rt-validator> before attempting to use
+C<rt-dump-initialdata>.
+
+While running, it will attempt to estimate the number of remaining
+objects to be dumped; these estimates are pessimistic, and will be
+incorrect if C<--no-users> or C<--no-groups> is used.
+
+If the controlling terminal is large enough (more than 25 columns high)
+and the C<gnuplot> program is installed, it will also show a textual
+graph of the queue size over time.
+
+=head2 OPTIONS
+
+=over
+
+=item B<--directory> I<name>
+
+The name of the output directory to write data files to, which should
+not exist yet; it is a fatal error if it does. Defaults to
+C<< ./I<$Organization>:I<Date>/ >>, where I<$Organization> is as set in
+F<RT_SiteConfig.pm>, and I<Date> is today's date.
+
+=item B<--force>
+
+Remove the output directory before starting.
+
+=item B<--no-users>
+
+By default, all privileged users are dumped; passing C<--no-users>
+limits it to only those users which are referenced by dumped tickets
+and history, and are thus necessary for internal consistency.
+
+=item B<--no-groups>
+
+By default, all groups are dumped; passing C<--no-groups> limits it
+to only system-internal groups, which are needed for internal
+consistency.
+
+=item B<--no-assets>
+
+By default, all assets are dumped; passing C<--no-assets> skips
+assets during serialization.
+
+=item B<--no-disabled>
+
+By default, all queues, custom fields, etc, including disabled ones, are
+dumped; passing C<--no-disabled> skips such disabled records during
+serialization.
+
+=item B<--no-deleted>
+
+By default, all assets, including deleted ones, are dumped; passing
+C<--no-deleted> skips deleted assets.
+
+=item B<--no-scrips>
+
+By default, all scrips and templates are dumped; passing C<--no-scrips>
+skips them.
+
+=item B<--no-acls>
+
+By default, all ACLs are dumped; passing C<--no-acls> skips them.
+
+=item B<--sync>
+
+By default, record ids are ordinarily excluded. Pass C<--sync> to
+include record ids if you intend to use this for sync rather than
+creating a generic initialdata.
+
+=item B<--gc> I<n>
+
+Adjust how often the garbage collection sweep is done; lower numbers are
+more frequent. It shares the same code with C<rt-serializer>, See
+L<rt-serializer/GARBAGE COLLECTION>.
+
+=item B<--page> I<n>
+
+Adjust how many rows are pulled from the database in a single query. Disable
+paging by setting this to 0. Defaults to 100.
+
+=item B<--quiet>
+
+Do not show graphical progress UI.
+
+=item B<--verbose>
+
+Do not show graphical progress UI, but rather log was each row is
+written out.
+
+=back
+
+=cut
commit 45725576938d9dc2f0412100bea3aff4c3db56ab
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Aug 6 21:39:20 2018 +0800
Make label column a bit wider considering we have ObjectCustomFieldValues now
diff --git a/lib/RT/Migrate.pm b/lib/RT/Migrate.pm
index 50251b023..3746b7238 100644
--- a/lib/RT/Migrate.pm
+++ b/lib/RT/Migrate.pm
@@ -80,10 +80,10 @@ sub progress_bar {
my $fraction = $args{max} ? $args{now} / $args{max} : 0;
- my $max_width = $args{cols} - 30;
+ my $max_width = $args{cols} - 35;
my $bar_width = int($max_width * $fraction);
- return sprintf "%20s |%-" . $max_width . "s| %3d%%\n",
+ return sprintf "%25s |%-" . $max_width . "s| %3d%%\n",
$args{label}, $args{char} x $bar_width, $fraction*100;
}
@@ -163,9 +163,9 @@ sub progress {
}
}
print "\n";
- printf "%20s %s\n", "Elapsed time:",
+ printf "%25s %s\n", "Elapsed time:",
format_time($elapsed);
- printf "%20s %s\n", "Estimated left:",
+ printf "%25s %s\n", "Estimated left:",
(defined $left) ? format_time($left) : "-";
$args{bottom}->($elapsed, $rows, $cols);
commit eefd72603fbe4b9db751ea7e1b541a4d7c29000e
Author: sunnavy <sunnavy at bestpractical.com>
Date: Tue Aug 7 00:05:48 2018 +0800
Fix plural of words like "Class" in the progress bar of migration
diff --git a/lib/RT/Migrate.pm b/lib/RT/Migrate.pm
index 3746b7238..84d83a910 100644
--- a/lib/RT/Migrate.pm
+++ b/lib/RT/Migrate.pm
@@ -129,8 +129,9 @@ sub progress {
my %counts = $args{counts}->();
for my $class (map {"RT::$_"} @{$args{bars}}) {
+ my $suffix = UNIVERSAL::can( $class . 'es', 'new' ) ? 'es' : 's';
my $display = $class;
- $display =~ s/^RT::(.*)/@{[$1]}s:/;
+ $display =~ s/^RT::(.*)/@{[$1]}$suffix:/;
print progress_bar(
label => $display,
now => $counts{$class},
commit 1540433d485df593bac16e2dea84814efc7a751c
Author: sunnavy <sunnavy at bestpractical.com>
Date: Tue Aug 7 00:22:39 2018 +0800
Fix plural of "ACE" in the progress bar of migration
diff --git a/lib/RT/Migrate.pm b/lib/RT/Migrate.pm
index 84d83a910..71e37e9e2 100644
--- a/lib/RT/Migrate.pm
+++ b/lib/RT/Migrate.pm
@@ -129,9 +129,16 @@ sub progress {
my %counts = $args{counts}->();
for my $class (map {"RT::$_"} @{$args{bars}}) {
- my $suffix = UNIVERSAL::can( $class . 'es', 'new' ) ? 'es' : 's';
- my $display = $class;
- $display =~ s/^RT::(.*)/@{[$1]}$suffix:/;
+ my $display;
+ if ( $class eq 'RT::ACE' ) {
+ $display = 'ACL:';
+ }
+ else {
+ $display = $class;
+ my $suffix = UNIVERSAL::can( $class . 'es', 'new' ) ? 'es' : 's';
+ $display =~ s/^RT::(.*)/@{[$1]}$suffix:/;
+ }
+
print progress_bar(
label => $display,
now => $counts{$class},
commit 2f0127eb5f931aefbd35d32200bb76255cd18fcf
Author: sunnavy <sunnavy at bestpractical.com>
Date: Tue Aug 7 02:27:03 2018 +0800
Fix UTF-8 decoding issue in rt-merge-initialdata
JSON files are UTF-8 encoded, so we need to decode it like what we
decode initialdata in RT::Handle.
diff --git a/sbin/rt-merge-initialdata.in b/sbin/rt-merge-initialdata.in
index c7403d895..cbc5c887d 100644
--- a/sbin/rt-merge-initialdata.in
+++ b/sbin/rt-merge-initialdata.in
@@ -425,7 +425,7 @@ sub is_deeply_equal {
sub slurp_json {
my $file = shift;
local $/;
- open (my $f, '<', $file)
+ open (my $f, '<encoding(UTF-8)', $file)
or die "Cannot open initialdata file '$file' for read: $@";
return $JSON->decode(scalar <$f>);
}
commit 974e1e24f9cf6b8214877f81b9dadbcddea98631
Author: Shawn M Moore <shawn at bestpractical.com>
Date: Mon Mar 20 20:56:10 2017 +0000
First initialdata roundtrip tests
diff --git a/t/api/initialdata-roundtrip.t b/t/api/initialdata-roundtrip.t
new file mode 100644
index 000000000..8098284af
--- /dev/null
+++ b/t/api/initialdata-roundtrip.t
@@ -0,0 +1,1121 @@
+use utf8;
+use strict;
+use warnings;
+use JSON;
+
+use RT::Test tests => undef, config => << 'CONFIG';
+Plugin('RT::Extension::Initialdata::JSON');
+Set($InitialdataFormatHandlers, [ 'perl', 'RT::Extension::Initialdata::JSON' ]);
+CONFIG
+
+my $general = RT::Queue->new(RT->SystemUser);
+$general->Load('General');
+
+my @tests = (
+ {
+ name => 'Simple user-defined group',
+ create => sub {
+ my $group = RT::Group->new(RT->SystemUser);
+ my ($ok, $msg) = $group->CreateUserDefinedGroup(Name => 'Staff');
+ ok($ok, $msg);
+ },
+ absent => sub {
+ my $group = RT::Group->new(RT->SystemUser);
+ $group->LoadUserDefinedGroup('Staff');
+ ok(!$group->Id, 'No such group');
+ },
+ present => sub {
+ my $group = RT::Group->new(RT->SystemUser);
+ $group->LoadUserDefinedGroup('Staff');
+ ok($group->Id, 'Loaded group');
+ is($group->Name, 'Staff', 'Group name');
+ is($group->Domain, 'UserDefined', 'Domain');
+ },
+ },
+ {
+ name => 'Group membership and ACLs',
+ create => sub {
+ my $outer = RT::Group->new(RT->SystemUser);
+ my ($ok, $msg) = $outer->CreateUserDefinedGroup(Name => 'Outer');
+ ok($ok, $msg);
+
+ my $inner = RT::Group->new(RT->SystemUser);
+ ($ok, $msg) = $inner->CreateUserDefinedGroup(Name => 'Inner');
+ ok($ok, $msg);
+
+ my $unrelated = RT::Group->new(RT->SystemUser);
+ ($ok, $msg) = $unrelated->CreateUserDefinedGroup(Name => 'Unrelated');
+ ok($ok, $msg);
+
+ my $user = RT::User->new(RT->SystemUser);
+ ($ok, $msg) = $user->Create(Name => 'User');
+ ok($ok, $msg);
+
+ my $unprivileged = RT::User->new(RT->SystemUser);
+ ($ok, $msg) = $unprivileged->Create(Name => 'Unprivileged');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $outer->AddMember($inner->PrincipalId);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $inner->AddMember($user->PrincipalId);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $general->AddWatcher(Type => 'AdminCc', PrincipalId => $outer->PrincipalId);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $general->AdminCc->PrincipalObj->GrantRight(Object => $general, Right => 'ShowTicket');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $inner->PrincipalObj->GrantRight(Object => $general, Right => 'ModifyTicket');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $user->PrincipalObj->GrantRight(Object => $general, Right => 'OwnTicket');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $unprivileged->PrincipalObj->GrantRight(Object => RT->System, Right => 'ModifyTicket');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $inner->PrincipalObj->GrantRight(Object => $inner, Right => 'SeeGroup');
+ ok($ok, $msg);
+
+ },
+ present => sub {
+ my $outer = RT::Group->new(RT->SystemUser);
+ $outer->LoadUserDefinedGroup('Outer');
+ ok($outer->Id, 'Loaded group');
+ is($outer->Name, 'Outer', 'Group name');
+
+ my $inner = RT::Group->new(RT->SystemUser);
+ $inner->LoadUserDefinedGroup('Inner');
+ ok($inner->Id, 'Loaded group');
+ is($inner->Name, 'Inner', 'Group name');
+
+ my $unrelated = RT::Group->new(RT->SystemUser);
+ $unrelated->LoadUserDefinedGroup('Unrelated');
+ ok($unrelated->Id, 'Loaded group');
+ is($unrelated->Name, 'Unrelated', 'Group name');
+
+ my $user = RT::User->new(RT->SystemUser);
+ $user->Load('User');
+ ok($user->Id, 'Loaded user');
+ is($user->Name, 'User', 'User name');
+
+ my $unprivileged = RT::User->new(RT->SystemUser);
+ $unprivileged->Load('Unprivileged');
+ ok($unprivileged->Id, 'Loaded Unprivileged');
+ is($unprivileged->Name, 'Unprivileged', 'Unprivileged name');
+
+ ok($outer->HasMember($inner->PrincipalId), 'outer hasmember inner');
+ ok($inner->HasMember($user->PrincipalId), 'inner hasmember user');
+ ok($outer->HasMemberRecursively($user->PrincipalId), 'outer hasmember user recursively');
+ ok(!$outer->HasMember($user->PrincipalId), 'outer does not have member user directly');
+ ok(!$inner->HasMember($outer->PrincipalId), 'inner does not have member outer');
+
+ ok($general->AdminCc->HasMember($outer->PrincipalId), 'queue AdminCc');
+ ok($general->AdminCc->HasMemberRecursively($inner->PrincipalId), 'queue AdminCc');
+ ok($general->AdminCc->HasMemberRecursively($user->PrincipalId), 'queue AdminCc');
+
+ ok(!$outer->HasMemberRecursively($unrelated->PrincipalId), 'unrelated group membership');
+ ok(!$inner->HasMemberRecursively($unrelated->PrincipalId), 'unrelated group membership');
+ ok(!$general->AdminCc->HasMemberRecursively($unrelated->PrincipalId), 'unrelated group membership');
+
+ ok($general->AdminCc->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'AdminCc ShowTicket right');
+ ok($outer->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'outer ShowTicket right');
+ ok($inner->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'inner ShowTicket right');
+ ok($user->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'user ShowTicket right');
+ ok(!$unrelated->PrincipalObj->HasRight(Object => $general, Right => 'ShowTicket'), 'unrelated ShowTicket right');
+
+ ok(!$general->AdminCc->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'AdminCc ModifyTicket right');
+ ok(!$outer->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'outer ModifyTicket right');
+ ok($inner->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'inner ModifyTicket right');
+ ok($user->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'user ModifyTicket right');
+ ok(!$unrelated->PrincipalObj->HasRight(Object => $general, Right => 'ModifyTicket'), 'unrelated ModifyTicket right');
+
+ ok(!$general->AdminCc->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'AdminCc OwnTicket right');
+ ok(!$outer->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'outer OwnTicket right');
+ ok(!$inner->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'inner OwnTicket right');
+ ok($user->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'inner OwnTicket right');
+ ok(!$unrelated->PrincipalObj->HasRight(Object => $general, Right => 'OwnTicket'), 'unrelated OwnTicket right');
+
+ ok($unprivileged->PrincipalObj->HasRight(Object => RT->System, Right => 'ModifyTicket'), 'unprivileged ModifyTicket right');
+
+ ok(!$general->AdminCc->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'AdminCc SeeGroup right');
+ ok(!$outer->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'outer SeeGroup right');
+ ok($inner->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'inner SeeGroup right');
+ ok($user->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'user SeeGroup right');
+ ok(!$unrelated->PrincipalObj->HasRight(Object => $inner, Right => 'SeeGroup'), 'unrelated SeeGroup right');
+ },
+ },
+
+ {
+ name => 'Custom field on two queues',
+ create => sub {
+ my $bugs = RT::Queue->new(RT->SystemUser);
+ my ($ok, $msg) = $bugs->Create(Name => 'Bugs');
+ ok($ok, $msg);
+
+ my $features = RT::Queue->new(RT->SystemUser);
+ ($ok, $msg) = $features->Create(Name => 'Features');
+ ok($ok, $msg);
+
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ ($ok, $msg) = $cf->Create(
+ Name => 'Fixed In',
+ Type => 'SelectSingle',
+ LookupType => RT::Ticket->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+
+ ($ok, $msg) = $cf->AddToObject($bugs);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $cf->AddToObject($features);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $cf->AddValue(Name => '0.1', Description => 'Prototype', SortOrder => '1');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $cf->AddValue(Name => '1.0', Description => 'Gold', SortOrder => '10');
+ ok($ok, $msg);
+
+ # these next two are intentionally added in an order different from their SortOrder
+ ($ok, $msg) = $cf->AddValue(Name => '2.0', Description => 'Remaster', SortOrder => '20');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $cf->AddValue(Name => '1.1', Description => 'Gold Bugfix', SortOrder => '11');
+ ok($ok, $msg);
+
+ },
+ present => sub {
+ my $bugs = RT::Queue->new(RT->SystemUser);
+ $bugs->Load('Bugs');
+ ok($bugs->Id, 'Bugs queue loaded');
+ is($bugs->Name, 'Bugs');
+
+ my $features = RT::Queue->new(RT->SystemUser);
+ $features->Load('Features');
+ ok($features->Id, 'Features queue loaded');
+ is($features->Name, 'Features');
+
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ $cf->Load('Fixed In');
+ ok($cf->Id, 'Fixed In CF loaded');
+ is($cf->Name, 'Fixed In');
+ is($cf->Type, 'Select', 'Type');
+ is($cf->MaxValues, 1, 'MaxValues');
+ is($cf->LookupType, RT::Ticket->CustomFieldLookupType, 'LookupType');
+
+ ok($cf->IsAdded($bugs->Id), 'CF is on Bugs queue');
+ ok($cf->IsAdded($features->Id), 'CF is on Features queue');
+ ok(!$cf->IsAdded(0), 'CF is not global');
+ ok(!$cf->IsAdded($general->Id), 'CF is not on General queue');
+
+ my @values = map { {
+ Name => $_->Name,
+ Description => $_->Description,
+ SortOrder => $_->SortOrder,
+ } } @{ $cf->Values->ItemsArrayRef };
+
+ is_deeply(\@values, [
+ { Name => '0.1', Description => 'Prototype', SortOrder => '1' },
+ { Name => '1.0', Description => 'Gold', SortOrder => '10' },
+ { Name => '1.1', Description => 'Gold Bugfix', SortOrder => '11' },
+ { Name => '2.0', Description => 'Remaster', SortOrder => '20' },
+ ], 'CF values');
+ },
+ },
+
+ {
+ name => 'Custom field lookup types',
+ create => sub {
+ my %extra = (
+ Group => { method => 'CreateUserDefinedGroup' },
+ Asset => undef,
+ Article => { Class => 'General' },
+ Ticket => undef,
+ Transaction => undef,
+ User => undef,
+ );
+
+ for my $type (qw/Asset Article Group Queue Ticket Transaction User/) {
+ my $class = "RT::$type";
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ my ($ok, $msg) = $cf->Create(
+ Name => "$type CF",
+ Type => "FreeformSingle",
+ LookupType => $class->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+
+ # apply globally
+ ($ok, $msg) = $cf->AddToObject($cf->RecordClassFromLookupType->new(RT->SystemUser));
+ ok($ok, $msg);
+
+ next if exists($extra{$type}) && !defined($extra{$type});
+
+ my $obj = $class->new(RT->SystemUser);
+ my $method = delete($extra{$type}{method}) || 'Create';
+ ($ok, $msg) = $obj->$method(
+ Name => $type,
+ %{ $extra{$type} || {} },
+ );
+ ok($ok, "created $type: $msg");
+ ok($obj->Id, "loaded $type");
+
+ ($ok, $msg) = $obj->AddCustomFieldValue(
+ Field => $cf->Id,
+ Value => "$type Value",
+ );
+ ok($ok, $msg);
+ }
+ },
+ present => sub {
+ my %load = (
+ Transaction => undef,
+ Ticket => undef,
+ User => undef,
+ Asset => undef,
+ );
+
+ for my $type (qw/Asset Article Group Queue Ticket Transaction User/) {
+ my $class = "RT::$type";
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ $cf->Load("$type CF");
+ ok($cf->Id, "loaded $type CF");
+ is($cf->Name, "$type CF", 'Name');
+ is($cf->Type, 'Freeform', 'Type');
+ is($cf->MaxValues, 1, 'MaxValues');
+ is($cf->LookupType, $class->CustomFieldLookupType, 'LookupType');
+
+ next if exists($load{$type}) && !defined($load{$type});
+
+ my $obj = $class->new(RT->SystemUser);
+ $obj->LoadByCols(
+ %{ $load{$type} || { Name => $type } },
+ );
+ ok($obj->Id, "loaded $type");
+
+ is($obj->FirstCustomFieldValue($cf->Id), "$type Value", "CF value for $type");
+ }
+ },
+ },
+
+ {
+ name => 'Custom field LargeContent',
+ create => sub {
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ my ($ok, $msg) = $cf->Create(
+ Name => "Group CF",
+ Type => "FreeformSingle",
+ LookupType => RT::Group->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+
+ ($ok, $msg) = $cf->AddToObject(RT::Group->new(RT->SystemUser));
+ ok($ok, $msg);
+
+ my $group = RT::Group->new(RT->SystemUser);
+ ($ok, $msg) = $group->CreateUserDefinedGroup(Name => 'Group');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $group->AddCustomFieldValue(
+ Field => $cf->Id,
+ Value => scalar("abc" x 256),
+ );
+ ok($ok, $msg);
+ },
+ present => sub {
+ my $group = RT::Group->new(RT->SystemUser);
+ $group->LoadUserDefinedGroup('Group');
+ ok($group->Id, 'loaded Group');
+ is($group->FirstCustomFieldValue('Group CF'), scalar("abc" x 256), "CF LargeContent");
+ },
+ # the following test peers into the initialdata only to make sure that
+ # we are roundtripping LargeContent as expected; if this starts
+ # failing it's not necessarily a problem, but it's worthy of
+ # investigating whether the "present" tests are still testing
+ # what they were meant to test
+ raw => sub {
+ my $json = shift;
+ my ($group) = grep { $_->{Name} eq 'Group' } @{ $json->{Groups} };
+ ok($group, 'found the group');
+ my ($ocfv) = @{ $group->{CustomFields} };
+ ok($ocfv, 'found the OCFV');
+
+ is($ocfv->{CustomField}, 'Group CF', 'CustomField');
+ is($ocfv->{Content}, undef, 'no Content');
+ is($ocfv->{LargeContent}, scalar("abc" x 256), 'LargeContent');
+ is($ocfv->{ContentType}, "text/plain", 'ContentType');
+ }
+ },
+
+ {
+ name => 'Scrips including Disabled',
+ export_args => { FollowDisabled => 1 },
+ create => sub {
+ my $bugs = RT::Queue->new(RT->SystemUser);
+ my ($ok, $msg) = $bugs->Create(Name => 'Bugs');
+ ok($ok, $msg);
+
+ my $features = RT::Queue->new(RT->SystemUser);
+ ($ok, $msg) = $features->Create(Name => 'Features');
+ ok($ok, $msg);
+
+ my $disabled = RT::Scrip->new(RT->SystemUser);
+ ($ok, $msg) = $disabled->Create(
+ Queue => 0,
+ Description => 'Disabled Scrip',
+ Template => 'Blank',
+ ScripCondition => 'User Defined',
+ ScripAction => 'User Defined',
+ CustomIsApplicableCode => 'return "condition"',
+ CustomPrepareCode => 'return "prepare"',
+ CustomCommitCode => 'return "commit"',
+ );
+ ok($ok, $msg);
+ ($ok, $msg) = $disabled->SetDisabled(1);
+ ok($ok, $msg);
+
+ my $stages = RT::Scrip->new(RT->SystemUser);
+ ($ok, $msg) = $stages->Create(
+ Description => 'Staged Scrip',
+ Template => 'Transaction',
+ ScripCondition => 'On Create',
+ ScripAction => 'Notify Owner',
+ );
+ ok($ok, $msg);
+
+ ($ok, $msg) = $stages->RemoveFromObject(0);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $stages->AddToObject(
+ ObjectId => $bugs->Id,
+ Stage => 'TransactionBatch',
+ SortOrder => 42,
+ );
+ ok($ok, $msg);
+
+ ($ok, $msg) = $stages->AddToObject(
+ ObjectId => $features->Id,
+ Stage => 'TransactionCreate',
+ SortOrder => 99,
+ );
+ ok($ok, $msg);
+ },
+ present => sub {
+ my $bugs = RT::Queue->new(RT->SystemUser);
+ $bugs->Load('Bugs');
+ ok($bugs->Id, 'Bugs queue loaded');
+ is($bugs->Name, 'Bugs');
+
+ my $features = RT::Queue->new(RT->SystemUser);
+ $features->Load('Features');
+ ok($features->Id, 'Features queue loaded');
+ is($features->Name, 'Features');
+
+ my $disabled = RT::Scrip->new(RT->SystemUser);
+ $disabled->LoadByCols(Description => 'Disabled Scrip');
+ ok($disabled->Id, 'Disabled scrip loaded');
+ is($disabled->Description, 'Disabled Scrip', 'Description');
+ is($disabled->Template, 'Blank', 'Template');
+ is($disabled->ConditionObj->Name, 'User Defined', 'Condition');
+ is($disabled->ActionObj->Name, 'User Defined', 'Action');
+ is($disabled->CustomIsApplicableCode, 'return "condition"', 'Condition code');
+ is($disabled->CustomPrepareCode, 'return "prepare"', 'Prepare code');
+ is($disabled->CustomCommitCode, 'return "commit"', 'Commit code');
+ ok($disabled->Disabled, 'Disabled');
+ ok($disabled->IsGlobal, 'IsGlobal');
+
+ my $stages = RT::Scrip->new(RT->SystemUser);
+ $stages->LoadByCols(Description => 'Staged Scrip');
+ ok($stages->Id, 'Staged scrip loaded');
+ is($stages->Description, 'Staged Scrip');
+ ok(!$stages->Disabled, 'not Disabled');
+ ok(!$stages->IsGlobal, 'not Global');
+
+ my $bug_objectscrip = $stages->IsAdded($bugs->Id);
+ ok($bug_objectscrip, 'added to Bugs');
+ is($bug_objectscrip->Stage, 'TransactionBatch', 'Stage');
+ is($bug_objectscrip->SortOrder, 42, 'SortOrder');
+
+ my $features_objectscrip = $stages->IsAdded($features->Id);
+ ok($features_objectscrip, 'added to Features');
+ is($features_objectscrip->Stage, 'TransactionCreate', 'Stage');
+ is($features_objectscrip->SortOrder, 99, 'SortOrder');
+
+ ok(!$stages->IsAdded($general->Id), 'not added to General');
+ },
+ },
+
+ {
+ name => 'No disabled scrips',
+ create => sub {
+ my $disabled = RT::Scrip->new(RT->SystemUser);
+ my ($ok, $msg) = $disabled->Create(
+ Description => 'Disabled Scrip',
+ Template => 'Transaction',
+ ScripCondition => 'On Create',
+ ScripAction => 'Notify Owner',
+ );
+ ok($ok, $msg);
+ ($ok, $msg) = $disabled->SetDisabled(1);
+ ok($ok, $msg);
+
+ my $enabled = RT::Scrip->new(RT->SystemUser);
+ ($ok, $msg) = $enabled->Create(
+ Description => 'Enabled Scrip',
+ Template => 'Transaction',
+ ScripCondition => 'On Create',
+ ScripAction => 'Notify Owner',
+ );
+ ok($ok, $msg);
+ },
+ present => sub {
+ my $from_initialdata = shift;
+
+ my $disabled = RT::Scrip->new(RT->SystemUser);
+ $disabled->LoadByCols(Description => 'Disabled Scrip');
+
+ if ($from_initialdata) {
+ ok(!$disabled->Id, 'Disabled scrip absent in initialdata');
+ }
+ else {
+ ok($disabled->Id, 'Disabled scrip present because of the original creation');
+ ok($disabled->Disabled, 'Disabled scrip disabled');
+ }
+
+ my $enabled = RT::Scrip->new(RT->SystemUser);
+ $enabled->LoadByCols(Description => 'Enabled Scrip');
+ ok($enabled->Id, 'Enabled scrip present');
+ },
+ },
+
+ {
+ name => 'Disabled many-to-many relationships',
+ create => sub {
+ my $enabled_queue = RT::Queue->new(RT->SystemUser);
+ my ($ok, $msg) = $enabled_queue->Create(
+ Name => 'Enabled Queue',
+ );
+ ok($ok, $msg);
+
+ my $disabled_queue = RT::Queue->new(RT->SystemUser);
+ ($ok, $msg) = $disabled_queue->Create(
+ Name => 'Disabled Queue',
+ );
+ ok($ok, $msg);
+
+ my $enabled_cf = RT::CustomField->new(RT->SystemUser);
+ ($ok, $msg) = $enabled_cf->Create(
+ Name => 'Enabled CF',
+ Type => 'FreeformSingle',
+ LookupType => RT::Queue->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+
+ my $disabled_cf = RT::CustomField->new(RT->SystemUser);
+ ($ok, $msg) = $disabled_cf->Create(
+ Name => 'Disabled CF',
+ Type => 'FreeformSingle',
+ LookupType => RT::Queue->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+
+ my $enabled_scrip = RT::Scrip->new(RT->SystemUser);
+ ($ok, $msg) = $enabled_scrip->Create(
+ Queue => 0,
+ Description => 'Enabled Scrip',
+ Template => 'Blank',
+ ScripCondition => 'On Create',
+ ScripAction => 'Notify Owner',
+ );
+ ok($ok, $msg);
+ $enabled_scrip->RemoveFromObject(0);
+
+ my $disabled_scrip = RT::Scrip->new(RT->SystemUser);
+ ($ok, $msg) = $disabled_scrip->Create(
+ Queue => 0,
+ Description => 'Disabled Scrip',
+ Template => 'Blank',
+ ScripCondition => 'On Create',
+ ScripAction => 'Notify Owner',
+ );
+ ok($ok, $msg);
+ $disabled_scrip->RemoveFromObject(0);
+
+ my $enabled_class = RT::Class->new(RT->SystemUser);
+ ($ok, $msg) = $enabled_class->Create(
+ Name => 'Enabled Class',
+ );
+ ok($ok, $msg);
+
+ my $disabled_class = RT::Class->new(RT->SystemUser);
+ ($ok, $msg) = $disabled_class->Create(
+ Name => 'Disabled Class',
+ );
+ ok($ok, $msg);
+
+ my $enabled_role = RT::CustomRole->new(RT->SystemUser);
+ ($ok, $msg) = $enabled_role->Create(
+ Name => 'Enabled Role',
+ );
+ ok($ok, $msg);
+
+ my $disabled_role = RT::CustomRole->new(RT->SystemUser);
+ ($ok, $msg) = $disabled_role->Create(
+ Name => 'Disabled Role',
+ );
+ ok($ok, $msg);
+
+ my $enabled_group = RT::Group->new(RT->SystemUser);
+ ($ok, $msg) = $enabled_group->CreateUserDefinedGroup(
+ Name => 'Enabled Group',
+ );
+ ok($ok, $msg);
+
+ my $disabled_group = RT::Group->new(RT->SystemUser);
+ ($ok, $msg) = $disabled_group->CreateUserDefinedGroup(
+ Name => 'Disabled Group',
+ );
+ ok($ok, $msg);
+
+ my $enabled_user = RT::User->new(RT->SystemUser);
+ ($ok, $msg) = $enabled_user->Create(
+ Name => 'Enabled User',
+ );
+ ok($ok, $msg);
+
+ my $disabled_user = RT::User->new(RT->SystemUser);
+ ($ok, $msg) = $disabled_user->Create(
+ Name => 'Disabled User',
+ );
+ ok($ok, $msg);
+
+ for my $object ($enabled_cf, $disabled_cf,
+ $enabled_scrip, $disabled_scrip,
+ $enabled_class, $disabled_class,
+ $enabled_role, $disabled_role) {
+
+ # slightly inconsistent API
+ my ($queue_a, $queue_b) = ($disabled_queue, $enabled_queue);
+ ($queue_a, $queue_b) = ($queue_a->Id, $queue_b->Id)
+ if $object->isa('RT::Scrip')
+ || $object->isa('RT::CustomRole');
+
+ ($ok, $msg) = $object->AddToObject($queue_a);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $object->AddToObject($queue_b);
+ ok($ok, $msg);
+ }
+
+ for my $principal ($enabled_group, $disabled_group,
+ $enabled_user, $disabled_user) {
+ ($ok, $msg) = $principal->PrincipalObj->GrantRight(Object => RT->System, Right => 'SeeQueue');
+ ok($ok, $msg);
+
+ for my $queue ($enabled_queue, $disabled_queue) {
+ ($ok, $msg) = $principal->PrincipalObj->GrantRight(Object => $queue, Right => 'ShowTicket');
+ ok($ok, $msg);
+
+ ($ok, $msg) = $queue->AddWatcher(Type => 'AdminCc', PrincipalId => $principal->PrincipalId);
+ ok($ok, $msg);
+ }
+ }
+
+ for my $cf ($enabled_cf, $disabled_cf) {
+ for my $queue ($enabled_queue, $disabled_queue) {
+ ($ok, $msg) = $queue->AddCustomFieldValue(Field => $cf->Id, Value => $cf->Name);
+ ok($ok, $msg);
+ }
+ }
+
+ for my $object ($disabled_queue, $disabled_cf,
+ $disabled_scrip, $disabled_class,
+ $disabled_role, $disabled_group,
+ $disabled_user) {
+ ($ok, $msg) = $object->SetDisabled(1);
+ ok($ok, $msg);
+ }
+ },
+ present => sub {
+ my $from_initialdata = shift;
+
+ my $enabled_queue = RT::Queue->new(RT->SystemUser);
+ $enabled_queue->Load('Enabled Queue');
+ ok($enabled_queue->Id, 'loaded Enabled queue');
+ is($enabled_queue->Name, 'Enabled Queue', 'Enabled Queue Name');
+
+ my $disabled_queue = RT::Queue->new(RT->SystemUser);
+ $disabled_queue->Load('Disabled Queue');
+
+ my $enabled_cf = RT::CustomField->new(RT->SystemUser);
+ $enabled_cf->Load('Enabled CF');
+ ok($enabled_cf->Id, 'loaded Enabled CF');
+ is($enabled_cf->Name, 'Enabled CF', 'Enabled CF Name');
+ ok($enabled_cf->IsAdded($enabled_queue->Id), 'Enabled CF added to General');
+
+ is($enabled_queue->FirstCustomFieldValue('Enabled CF'), 'Enabled CF', 'OCFV');
+
+ my $disabled_cf = RT::CustomField->new(RT->SystemUser);
+ $disabled_cf->Load('Disabled CF');
+
+ my $enabled_scrip = RT::Scrip->new(RT->SystemUser);
+ $enabled_scrip->LoadByCols(Description => 'Enabled Scrip');
+ ok($enabled_scrip->Id, 'loaded Enabled Scrip');
+ is($enabled_scrip->Description, 'Enabled Scrip', 'Enabled Scrip Name');
+ ok($enabled_scrip->IsAdded($enabled_queue->Id), 'Enabled Scrip added to General');
+ my $disabled_scrip = RT::Scrip->new(RT->SystemUser);
+ $disabled_scrip->LoadByCols(Description => 'Disabled Scrip');
+
+ my $enabled_class = RT::Class->new(RT->SystemUser);
+ $enabled_class->Load('Enabled Class');
+ ok($enabled_class->Id, 'loaded Enabled Class');
+ is($enabled_class->Name, 'Enabled Class', 'Enabled Class Name');
+ ok($enabled_class->IsApplied($enabled_queue->Id), 'Enabled Class added to General');
+
+ my $disabled_class = RT::Class->new(RT->SystemUser);
+ $disabled_class->Load('Disabled Class');
+
+ my $enabled_role = RT::CustomRole->new(RT->SystemUser);
+ $enabled_role->Load('Enabled Role');
+ ok($enabled_role->Id, 'loaded Enabled Role');
+ is($enabled_role->Name, 'Enabled Role', 'Enabled Role Name');
+ ok($enabled_role->IsAdded($enabled_queue->Id), 'Enabled Role added to General');
+
+ my $disabled_role = RT::CustomRole->new(RT->SystemUser);
+ $disabled_role->Load('Disabled Role');
+
+ my $enabled_group = RT::Group->new(RT->SystemUser);
+ $enabled_group->LoadUserDefinedGroup('Enabled Group');
+ ok($enabled_group->Id, 'loaded Enabled Group');
+ is($enabled_group->Name, 'Enabled Group', 'Enabled Group Name');
+ ok($enabled_group->PrincipalObj->HasRight(Object => $enabled_queue, Right => 'ShowTicket'), 'Enabled Group has queue right');
+ ok($enabled_group->PrincipalObj->HasRight(Object => RT->System, Right => 'SeeQueue'), 'Enabled Group has global right');
+ ok($enabled_queue->AdminCc->HasMember($enabled_group->PrincipalObj), 'Enabled Group still queue watcher');
+
+ my $disabled_group = RT::Group->new(RT->SystemUser);
+ $disabled_group->LoadUserDefinedGroup('Disabled Group');
+
+ my $enabled_user = RT::User->new(RT->SystemUser);
+ $enabled_user->Load('Enabled User');
+ ok($enabled_user->Id, 'loaded Enabled User');
+ is($enabled_user->Name, 'Enabled User', 'Enabled User Name');
+ ok($enabled_user->PrincipalObj->HasRight(Object => $enabled_queue, Right => 'ShowTicket'), 'Enabled User has queue right');
+ ok($enabled_user->PrincipalObj->HasRight(Object => RT->System, Right => 'SeeQueue'), 'Enabled User has global right');
+ ok($enabled_queue->AdminCc->HasMember($enabled_user->PrincipalObj), 'Enabled User still queue watcher');
+
+ my $disabled_user = RT::User->new(RT->SystemUser);
+ $disabled_user->Load('Disabled User');
+
+ for my $object ($disabled_queue, $disabled_cf,
+ $disabled_scrip, $disabled_class,
+ $disabled_role, $disabled_group,
+ $disabled_user) {
+ if ($from_initialdata) {
+ ok(!$object->Id, "disabled " . ref($object) . " excluded");
+ }
+ else {
+ ok($object->Disabled, "disabled " . ref($object));
+ }
+ }
+ },
+ },
+
+ {
+ name => 'Unapplied Objects',
+ create => sub {
+ my $scrip = RT::Scrip->new(RT->SystemUser);
+ my ($ok, $msg) = $scrip->Create(
+ Queue => 0,
+ Description => 'Unapplied Scrip',
+ Template => 'Blank',
+ ScripCondition => 'On Create',
+ ScripAction => 'Notify Owner',
+ );
+ ok($ok, $msg);
+ ($ok, $msg) = $scrip->RemoveFromObject(0);
+ ok($ok, $msg);
+
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ ($ok, $msg) = $cf->Create(
+ Name => 'Unapplied CF',
+ Type => 'FreeformSingle',
+ LookupType => RT::Ticket->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+
+ my $class = RT::Class->new(RT->SystemUser);
+ ($ok, $msg) = $class->Create(
+ Name => 'Unapplied Class',
+ );
+ ok($ok, $msg);
+
+ my $role = RT::CustomRole->new(RT->SystemUser);
+ ($ok, $msg) = $role->Create(
+ Name => 'Unapplied Custom Role',
+ );
+ ok($ok, $msg);
+ },
+ present => sub {
+ my $scrip = RT::Scrip->new(RT->SystemUser);
+ $scrip->LoadByCols(Description => 'Unapplied Scrip');
+ ok($scrip->Id, 'Unapplied scrip loaded');
+ is($scrip->Description, 'Unapplied Scrip');
+ ok(!$scrip->Disabled, 'not Disabled');
+ ok(!$scrip->IsGlobal, 'not Global');
+ ok(!$scrip->IsAdded($general->Id), 'not applied to General queue');
+
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ $cf->Load('Unapplied CF');
+ ok($cf->Id, 'Unapplied CF loaded');
+ is($cf->Name, 'Unapplied CF');
+ ok(!$cf->Disabled, 'not Disabled');
+ ok(!$cf->IsGlobal, 'not Global');
+ ok(!$cf->IsAdded($general->Id), 'not applied to General queue');
+
+ my $class = RT::Class->new(RT->SystemUser);
+ $class->Load('Unapplied Class');
+ ok($class->Id, 'Unapplied Class loaded');
+ is($class->Name, 'Unapplied Class');
+ ok(!$class->Disabled, 'not Disabled');
+ ok(!$class->IsApplied(0), 'not Global');
+ ok(!$class->IsApplied($general->Id), 'not applied to General queue');
+
+ my $role = RT::CustomRole->new(RT->SystemUser);
+ $role->Load('Unapplied Custom Role');
+ ok($role->Id, 'Unapplied Custom Role loaded');
+ is($role->Name, 'Unapplied Custom Role');
+ ok(!$role->Disabled, 'not Disabled');
+ ok(!$role->IsAdded(0), 'not Global');
+ ok(!$role->IsAdded($general->Id), 'not applied to General queue');
+ },
+ },
+
+ {
+ name => 'Global Objects',
+ create => sub {
+ my $scrip = RT::Scrip->new(RT->SystemUser);
+ my ($ok, $msg) = $scrip->Create(
+ Queue => 0,
+ Description => 'Global Scrip',
+ Template => 'Blank',
+ ScripCondition => 'On Create',
+ ScripAction => 'Notify Owner',
+ );
+ ok($ok, $msg);
+
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ ($ok, $msg) = $cf->Create(
+ Name => 'Global CF',
+ Type => 'FreeformSingle',
+ LookupType => RT::Ticket->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+ ($ok, $msg) = $cf->AddToObject(RT::Queue->new(RT->SystemUser));
+ ok($ok, $msg);
+
+ my $class = RT::Class->new(RT->SystemUser);
+ ($ok, $msg) = $class->Create(
+ Name => 'Global Class',
+ );
+ ok($ok, $msg);
+ ($ok, $msg) = $class->AddToObject(RT::Queue->new(RT->SystemUser));
+ ok($ok, $msg);
+ },
+ present => sub {
+ my $scrip = RT::Scrip->new(RT->SystemUser);
+ $scrip->LoadByCols(Description => 'Global Scrip');
+ ok($scrip->Id, 'Global scrip loaded');
+ is($scrip->Description, 'Global Scrip');
+ ok(!$scrip->Disabled, 'not Disabled');
+ ok($scrip->IsGlobal, 'Global');
+ ok(!$scrip->IsAdded($general->Id), 'not applied to General queue');
+
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ $cf->Load('Global CF');
+ ok($cf->Id, 'Global CF loaded');
+ is($cf->Name, 'Global CF');
+ ok(!$cf->Disabled, 'not Disabled');
+ ok($cf->IsGlobal, 'Global');
+ ok(!$cf->IsAdded($general->Id), 'not applied to General queue');
+
+ my $class = RT::Class->new(RT->SystemUser);
+ $class->Load('Global Class');
+ ok($class->Id, 'Global Class loaded');
+ is($class->Name, 'Global Class');
+ ok(!$class->Disabled, 'not Disabled');
+ ok($class->IsApplied(0), 'Global');
+ ok(!$class->IsApplied($general->Id), 'not applied to General queue');
+ },
+ },
+ {
+ name => 'Templates',
+ create => sub {
+ my $global = RT::Template->new(RT->SystemUser);
+ my ($ok, $msg) = $global->Create(
+ Name => 'Initialdata test',
+ Queue => 0,
+ Description => 'foo',
+ Content => "Hello こんにちは",
+ Type => "Simple",
+ );
+ ok($ok, $msg);
+
+ my $queue = RT::Template->new(RT->SystemUser);
+ ($ok, $msg) = $queue->Create(
+ Name => 'Initialdata test',
+ Queue => $general->Id,
+ Description => 'override for Swedes',
+ Content => "Hello Hallå",
+ Type => "Simple",
+ );
+ ok($ok, $msg);
+
+ my $standalone = RT::Template->new(RT->SystemUser);
+ ($ok, $msg) = $standalone->Create(
+ Name => 'Standalone test',
+ Queue => $general->Id,
+ Description => 'no global version',
+ Content => "this was broken!",
+ Type => "Perl",
+ );
+ ok($ok, $msg);
+ },
+ present => sub {
+ my $global = RT::Template->new(RT->SystemUser);
+ $global->LoadGlobalTemplate('Initialdata test');
+ ok($global->Id, 'loaded template');
+ is($global->Name, 'Initialdata test', 'Name');
+ is($global->Queue, 0, 'Queue');
+ is($global->Description, 'foo', 'Description');
+ is($global->Content, 'Hello こんにちは', 'Content');
+ is($global->Type, 'Simple', 'Type');
+
+ my $queue = RT::Template->new(RT->SystemUser);
+ $queue->LoadQueueTemplate(Name => 'Initialdata test', Queue => $general->Id);
+ ok($queue->Id, 'loaded template');
+ is($queue->Name, 'Initialdata test', 'Name');
+ is($queue->Queue, $general->Id, 'Queue');
+ is($queue->Description, 'override for Swedes', 'Description');
+ is($queue->Content, 'Hello Hallå', 'Content');
+ is($queue->Type, 'Simple', 'Type');
+
+ my $standalone = RT::Template->new(RT->SystemUser);
+ $standalone->LoadQueueTemplate(Name => 'Standalone test', Queue => $general->Id);
+ ok($standalone->Id, 'loaded template');
+ is($standalone->Name, 'Standalone test', 'Name');
+ is($standalone->Queue, $general->Id, 'Queue');
+ is($standalone->Description, 'no global version', 'Description');
+ is($standalone->Content, 'this was broken!', 'Content');
+ is($standalone->Type, 'Perl', 'Type');
+ },
+ },
+ {
+ name => 'Articles',
+ create => sub {
+ my $class = RT::Class->new(RT->SystemUser);
+ my ($ok, $msg) = $class->Create(
+ Name => 'Test',
+ );
+ ok($ok, $msg);
+
+ my $content = RT::CustomField->new(RT->SystemUser);
+ $content->LoadByCols(
+ Name => "Content",
+ Type => "Text",
+ LookupType => RT::Article->CustomFieldLookupType,
+ );
+ ok($content->Id, "loaded builtin Content CF");
+
+ my $tags = RT::CustomField->new(RT->SystemUser);
+ ($ok, $msg) = $tags->Create(
+ Name => "Tags",
+ Type => "FreeformMultiple",
+ LookupType => RT::Article->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+ ($ok, $msg) = $tags->AddToObject($class);
+ ok($ok, $msg);
+
+ my $clearance = RT::CustomField->new(RT->SystemUser);
+ ($ok, $msg) = $clearance->Create(
+ Name => "Clearance",
+ Type => "SelectSingle",
+ LookupType => RT::Article->CustomFieldLookupType,
+ );
+ ok($ok, $msg);
+ ($ok, $msg) = $clearance->AddToObject($class);
+ ok($ok, $msg);
+
+ ($ok, $msg) = $clearance->AddValue(Name => 'Unclassified');
+ ok($ok, $msg);
+ ($ok, $msg) = $clearance->AddValue(Name => 'Classified');
+ ok($ok, $msg);
+ ($ok, $msg) = $clearance->AddValue(Name => 'Top Secret');
+ ok($ok, $msg);
+
+ my $coffee = RT::Article->new(RT->SystemUser);
+ ($ok, $msg) = $coffee->Create(
+ Class => 'Test',
+ Name => 'Coffee time',
+ "CustomField-" . $content->Id => 'Always',
+ "CustomField-" . $clearance->Id => 'Unclassified',
+ "CustomField-" . $tags->Id => ['drink', 'coffee', 'how the humans live'],
+ );
+ ok($ok, $msg);
+
+ my $twd = RT::Article->new(RT->SystemUser);
+ ($ok, $msg) = $twd->Create(
+ Class => 'Test',
+ Name => 'Total world domination plans',
+ "CustomField-" . $content->Id => 'REDACTED',
+ "CustomField-" . $clearance->Id => 'Top Secret',
+ "CustomField-" . $tags->Id => ['snakes', 'clowns'],
+ );
+ ok($ok, $msg);
+ },
+ present => sub {
+ my $class = RT::Class->new(RT->SystemUser);
+ $class->Load('Test');
+ ok($class->Id, 'loaded class');
+ is($class->Name, 'Test', 'Name');
+
+ my $coffee = RT::Article->new(RT->SystemUser);
+ $coffee->LoadByCols(Name => 'Coffee time');
+ ok($coffee->Id, 'loaded article');
+ is($coffee->Name, 'Coffee time', 'Name');
+ is($coffee->Class, $class->Id, 'Class');
+ is($coffee->FirstCustomFieldValue('Content'), 'Always', 'Content CF');
+ is($coffee->FirstCustomFieldValue('Clearance'), 'Unclassified', 'Clearance CF');
+ is($coffee->CustomFieldValuesAsString('Tags', Separator => '.'), 'drink.coffee.how the humans live', 'Tags CF');
+
+ my $twd = RT::Article->new(RT->SystemUser);
+ $twd->LoadByCols(Name => 'Total world domination plans');
+ ok($twd->Id, 'loaded article');
+ is($twd->Name, 'Total world domination plans', 'Name');
+ is($twd->Class, $class->Id, 'Class');
+ is($twd->FirstCustomFieldValue('Content'), 'REDACTED', 'Content CF');
+ is($twd->FirstCustomFieldValue('Clearance'), 'Top Secret', 'Clearance CF');
+ is($twd->CustomFieldValuesAsString('Tags', Separator => '.'), 'snakes.clowns', 'Tags CF');
+ },
+ },
+);
+
+my $id = 0;
+for my $test (@tests) {
+ $id++;
+ my $directory = File::Spec->catdir(RT::Test->temp_directory, "export-$id");
+
+ # we get a lot of warnings about already-existing objects; suppress them
+ # for now until we clean it up
+ my $warn = $SIG{__WARN__};
+ local $SIG{__WARN__} = sub {
+ return if $_[0] =~ join '|', (
+ qr/^Name in use$/,
+ qr/^A Template with that name already exists$/,
+ qr/^.* already has the right .* on .*$/,
+ qr/^Invalid value for Name$/,
+ qr/^Queue already exists$/,
+ qr/^Invalid Name \(names must be unique and may not be all digits\)$/,
+ );
+
+ # Avoid reporting this anonymous call frame as the source of the warning
+ goto &$warn;
+ };
+
+ my $name = delete $test->{name};
+ my $create = delete $test->{create};
+ my $absent = delete $test->{absent};
+ my $present = delete $test->{present};
+ my $raw = delete $test->{raw};
+ my $export_args = delete $test->{export_args};
+ fail("Unexpected keys for test #$id ($name): " . join(', ', sort keys %$test)) if keys %$test;
+
+ subtest "$name (ordinary creation)" => sub {
+ autorollback(sub {
+ $absent->(0) if $absent;
+ $create->();
+ $present->(0) if $present;
+ export_initialdata($directory, %{ $export_args || {} });
+ });
+ };
+
+ if ($raw) {
+ subtest "$name (testing initialdata)" => sub {
+ my $file = File::Spec->catfile($directory, "initialdata.json");
+ my $content = slurp($file);
+ my $json = JSON->new->decode($content);
+ $raw->($json, $content);
+ };
+ }
+
+ subtest "$name (from export-$id/initialdata.json)" => sub {
+ autorollback(sub {
+ $absent->(1) if $absent;
+ import_initialdata($directory);
+ $present->(1) if $present;
+ });
+ };
+}
+
+done_testing();
+
+sub autorollback {
+ my $code = shift;
+
+ $RT::Handle->BeginTransaction;
+ {
+ # avoid "Rollback and commit are mixed while escaping nested transaction" warnings
+ # due to (begin; (begin; commit); rollback)
+ no warnings 'redefine';
+ local *DBIx::SearchBuilder::Handle::BeginTransaction = sub {};
+ local *DBIx::SearchBuilder::Handle::Commit = sub {};
+ local *DBIx::SearchBuilder::Handle::Rollback = sub {};
+
+ $code->();
+ }
+ $RT::Handle->Rollback;
+}
+
+sub export_initialdata {
+ my $directory = shift;
+ my %args = @_;
+ local @RT::Record::ISA = qw( DBIx::SearchBuilder::Record RT::Base );
+
+ use RT::Migrate::Serializer::JSON;
+ my $migrator = RT::Migrate::Serializer::JSON->new(
+ Directory => $directory,
+ Verbose => 0,
+ AllUsers => 0,
+ FollowACL => 1,
+ FollowScrips => 1,
+ FollowTransactions => 0,
+ FollowTickets => 0,
+ FollowAssets => 0,
+ FollowDisabled => 0,
+ %args,
+ );
+
+ $migrator->Export;
+}
+
+sub import_initialdata {
+ my $directory = shift;
+ my $initialdata = File::Spec->catfile($directory, "initialdata.json");
+
+ ok(-e $initialdata, "File $initialdata exists");
+
+ my ($rv, $msg) = RT->DatabaseHandle->InsertData( $initialdata, undef, disconnect_after => 0 );
+ ok($rv, "Inserted test data from $initialdata")
+ or diag "Error: $msg";
+}
+
+sub slurp {
+ my $file = shift;
+ local $/;
+ open (my $f, '<:encoding(UTF-8)', $file)
+ or die "Cannot open initialdata file '$file' for read: $@";
+ return scalar <$f>;
+}
commit e63b7e43856a1cdf58ce7819ed42c59dddf46539
Author: sunnavy <sunnavy at bestpractical.com>
Date: Tue Aug 7 03:22:40 2018 +0800
Skip tests if required RT::Extension::Initialdata::JSON is not available
diff --git a/t/api/initialdata-roundtrip.t b/t/api/initialdata-roundtrip.t
index 8098284af..14f2995ff 100644
--- a/t/api/initialdata-roundtrip.t
+++ b/t/api/initialdata-roundtrip.t
@@ -3,10 +3,22 @@ use strict;
use warnings;
use JSON;
-use RT::Test tests => undef, config => << 'CONFIG';
+require RT::Test;
+use Test::More;
+
+eval {
+ RT::Test->import(
+ tests => undef,
+ config => << 'CONFIG',
Plugin('RT::Extension::Initialdata::JSON');
Set($InitialdataFormatHandlers, [ 'perl', 'RT::Extension::Initialdata::JSON' ]);
CONFIG
+ );
+};
+
+if ( $@ ) {
+ plan skip_all => 'Unable to test without RT::Extension::Initialdata::JSON';
+}
my $general = RT::Queue->new(RT->SystemUser);
$general->Load('General');
@@ -1060,7 +1072,7 @@ for my $test (@tests) {
};
}
-done_testing();
+RT::Test::done_testing();
sub autorollback {
my $code = shift;
-----------------------------------------------------------------------
More information about the rt-commit
mailing list