view release on metacpan or search on metacpan
lib/App/SD/CLI/Command.pm view on Meta::CPAN
package App::SD::CLI::Command;
use Any::Moose 'Role';
use Params::Validate qw(validate);
=head2 get_content %args
This is a helper routine for use in SD commands to enable getting records
in different ways such as from a file, on the commandline, or from an
editor. Returns the record content.
Valid keys in %args are type => str, default_edit => bool, and
prefill_props => $props_hash_ref, props_order => $order_array_ref,
footer => str, header => str.
lib/App/SD/CLI/Command/Publish.pm view on Meta::CPAN
package App::SD::Server::Static;
use Any::Moose;
extends 'App::SD::Server';
use Params::Validate;
use JSON;
sub log_request { }
sub send_content {
my $self = shift;
my %args = validate( @_, { content => 1, content_type => 0, encode_as => 0, static => 0 } );
if ( $args{'encode_as'} && $args{'encode_as'} eq 'json' ) {
$args{'content'} = to_json( $args{'content'} );
}
return $args{'content'};
}
sub _send_redirect {
my $self = shift;
my %args = validate( @_, { to => 1 } );
die "REDIRECT " . $args{to} . "\n";
}
sub _send_404 {}
__PACKAGE__->meta->make_immutable;
no Any::Moose;
1;
lib/App/SD/CLI/Command/Ticket/Create.pm view on Meta::CPAN
package App::SD::CLI::Command::Ticket::Create;
use Any::Moose;
use Params::Validate qw/validate/;
extends 'Prophet::CLI::Command::Create';
with 'App::SD::CLI::Model::Ticket';
with 'App::SD::CLI::Command';
with 'Prophet::CLI::TextEditorCommand';
sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), e => 'edit' };
# use actual valid ticket props in the help message, and make note of the
# interactive editing mode
override usage_msg => sub {
lib/App/SD/CLI/Command/Ticket/Create.pm view on Meta::CPAN
my $done = 0;
while (!$done) {
$done = $self->try_to_edit( template => \$template_to_edit, record => $record);
}
};
sub process_template {
my $self = shift;
my %args = validate( @_, { template => 1, edited => 1, record => 1 } );
my $record = $args{record};
my $updated = $args{edited};
( my $props_ref, my $comment ) = $self->parse_record_template($updated);
for my $prop ( keys %$props_ref ) {
$self->context->set_prop( $prop => $props_ref->{$prop} );
}
my $error;
lib/App/SD/CLI/Command/Ticket/Update.pm view on Meta::CPAN
package App::SD::CLI::Command::Ticket::Update;
use Any::Moose;
use Params::Validate qw/validate/;
extends 'Prophet::CLI::Command::Update';
with 'App::SD::CLI::Model::Ticket';
with 'App::SD::CLI::Command';
with 'Prophet::CLI::TextEditorCommand';
sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), a => 'all-props' };
sub usage_msg {
my $self = shift;
lib/App/SD/CLI/Command/Ticket/Update.pm view on Meta::CPAN
my $done = 0;
while (!$done) {
$done = $self->try_to_edit( template => \$template_to_edit, record => $record);
}
};
sub process_template {
my $self = shift;
my %args = validate( @_, { template => 1, edited => 1, record => 1 } );
my $record = $args{record};
my $updated = $args{edited};
my ( $props_ref, $comment ) = $self->parse_record_template($updated);
no warnings 'uninitialized';
# if a formerly existing prop was removed from the output, delete it
# (deleting is currently the equivalent of setting to '', and
# we want to do this all in one changeset)
lib/App/SD/CLI/Model/Ticket.pm view on Meta::CPAN
=head2 add_comment content => str, uuid => str
A convenience method that takes a content string and a ticket uuid and creates
a new comment record, for use in other commands (such as ticket create
and ticket update).
=cut
sub add_comment {
my $self = shift;
validate(@_, { content => 1, uuid => 1 } );
my %args = @_;
require App::SD::CLI::Command::Ticket::Comment::Create;
$self->context->mutate_attributes( args => \%args );
my $command = App::SD::CLI::Command::Ticket::Comment::Create->new(
uuid => $args{uuid},
cli => $self->cli,
context => $self->context,
type => 'comment',
lib/App/SD/CLI/Model/Ticket.pm view on Meta::CPAN
$self->build_template_section(
header => comment_separator,
data => ''
)
);
}
sub _build_kv_pairs {
my $self = shift;
my %args = validate (@_, { order => 1, data => 1,
verbose => 1, record => 1 });
my $string = '';
for my $prop ( @{$args{order}}) {
# if called with --verbose, we print descriptions and valid values for
# props (if they exist)
if ( $args{verbose} ) {
if ( my $desc = $self->app_handle->setting(
label => 'prop_descriptions' )->get()->[0]->{$prop} ) {
$string .= '# '.$desc."\n";
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
key => $replica_token_key,
value => $password,
} ],
);
}
}
sub integrate_changeset {
my $self = shift;
my %args = validate(
@_,
{ changeset => { isa => 'Prophet::ChangeSet' },
resolver => { optional => 1 },
resolver_class => { optional => 1 },
resdb => { optional => 1 },
conflict_callback => { optional => 1 },
reporting_callback => { optional => 1 }
}
);
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
To avoid publishing prophet-private data, It skips any change with a record type
that record_type starts with '__'.
This is probably a bug.
=cut
sub integrate_change {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' },
);
# don't push internal records
return if $change->record_type =~ /^__/;
Prophet::App->require( $self->push_encoder());
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
=item changeset
=item start_time
=back
=cut
sub record_pushed_transactions {
my $self = shift;
my %args = validate( @_,
{ ticket => 1,
changeset => { isa => 'Prophet::ChangeSet' }, start_time => 1} );
my $earliest_valid_txn_date;
# walk through every transaction on the ticket, starting with the latest
for my $txn ( $self->get_txn_list_by_date($args{ticket}) ) {
# walk backwards through all transactions on the ticket we just updated
# Skip any transaction where the remote user isn't me, this might
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
=head2 record_pushed_transaction $foreign_transaction_id, $changeset
Record that this replica was the original source of $foreign_transaction_id
(with changeset $changeset)
=cut
sub record_pushed_transaction {
my $self = shift;
my %args = validate( @_,
{ transaction => 1, changeset => { isa => 'Prophet::ChangeSet' },
record => 1 } );
my $key = join('-', "foreign-txn-from" , $self->uuid ,
'record' , $args{record} , 'txn' , $args{transaction} );
my $value = join(':', $args{changeset}->original_source_uuid,
$args{changeset}->original_sequence_no );
$self->store_local_metadata($key => $value);
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
all records of this type for the remote replica (they'll be
obsolete)
We use this cache to avoid integrating changesets we've pushed to the
remote replica when doing a subsequent pull
=cut
sub foreign_transaction_originated_locally {
my $self = shift;
my ( $id, $record ) = validate_pos( @_, 1, 1 );
return $self->fetch_local_metadata(
"foreign-txn-from-" . $self->uuid . '-record-' . $record . '-txn-' . $id );
}
sub traverse_changesets {
my $self = shift;
my %args = validate( @_,
{ after => 1,
callback => 1,
before_load_changeset_callback => { type => CODEREF, optional => 1},
reporting_callback => { type => CODEREF, optional => 1 },
}
);
Prophet::App->require( $self->pull_encoder());
my $recoder = $self->pull_encoder->new( { sync_source => $self } );
my ( $changesets ) = $recoder->run( after => $args{'after'} );
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
sub uuid_for_remote_id {
my ( $self, $id ) = @_;
return $self->_lookup_uuid_for_remote_id($id)
||$self->_url_based_uuid_for_remote_ticket_id( $id);
}
sub _lookup_uuid_for_remote_id {
my $self = shift;
my ($id) = validate_pos( @_, 1 );
return $self->fetch_local_metadata(
'local_uuid_for_'. $self->_url_based_uuid_for_remote_ticket_id($id));
}
sub _set_uuid_for_remote_id {
my $self = shift;
my %args = validate( @_, { uuid => 1, remote_id => 1 } );
return $self->store_local_metadata('local_uuid_for_'.
$self->_url_based_uuid_for_remote_ticket_id( $args{'remote_id'} ),
$args{uuid}
);
}
sub _url_based_uuid_for_remote_ticket_id {
my $self = shift;
my $id = shift;
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
};
my $prop = $self->uuid . '-id';
my $id = $ticket->prop( $prop )
or warn "ticket #$uuid_or_luid has no property '$prop'";
return $id;
}
sub _set_remote_id_for_uuid {
my $self = shift;
my %args = validate(
@_,
{ uuid => 1,
remote_id => 1
}
);
require App::SD::Model::Ticket;
my $ticket = App::SD::Model::Ticket->new(
app_handle => $self->app_handle,
type => 'ticket'
lib/App/SD/ForeignReplica.pm view on Meta::CPAN
=head2 record_remote_id_for_pushed_record
When pushing a record created within the prophet cloud to a foreign replica, we
need to do bookkeeping to record the prophet uuid to remote id mapping.
=cut
sub record_remote_id_for_pushed_record {
my $self = shift;
my %args = validate(
@_,
{ uuid => 1,
remote_id => 1
}
);
$self->_set_uuid_for_remote_id(%args);
$self->_set_remote_id_for_uuid(%args);
}
sub record_upstream_last_modified_date {
lib/App/SD/ForeignReplica/PullEncoder.pm view on Meta::CPAN
package App::SD::ForeignReplica::PullEncoder;
use Any::Moose;
use App::SD::Util;
use Params::Validate qw/validate/;
with 'Prophet::CLI::ProgressBar';
sub run {
my $self = shift;
my %args = validate( @_, { after => 1});
$self->sync_source->log('Finding matching tickets');
my $tickets = $self->find_matching_tickets( query => $self->sync_source->query );
if ( @$tickets == 0 ) {
$self->sync_source->log("No tickets found.");
return;
}
my $counter = 0;
lib/App/SD/ForeignReplica/PushEncoder.pm view on Meta::CPAN
package App::SD::ForeignReplica::PushEncoder;
use Any::Moose;
use Params::Validate;
sub integrate_change {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my ($id, $record);
# if the original_sequence_no of this changeset is <=
# the last changeset our sync source for the original_sequence_no, we can skip it.
# XXX TODO - this logic should be at the changeset level, not the cahnge level, as it applies to all
# changes in the changeset
lib/App/SD/Model/Attachment.pm view on Meta::CPAN
package App::SD::Model::Attachment;
use Any::Moose;
extends 'App::SD::Record';
use Params::Validate qw/validate/;
use constant collection_class => 'App::SD::Collection::Attachment';
has '+type' => ( default => 'attachment');
sub _default_summary_format { '%s,$luid | %s,name | %s,content_type'}
__PACKAGE__->register_reference( ticket => 'App::SD::Model::Ticket');
sub create {
my $self = shift;
my %args = validate( @_, {props => 1});
return (0,"You can't create an attachment without specifying a 'ticket' uuid") unless ($args{'props'}->{'ticket'});
$args{'props'}->{'content_type'} ||= 'text/plain'; # XXX TODO use real mime typing;
$self->SUPER::create(%args);
}
lib/App/SD/Model/Ticket.pm view on Meta::CPAN
=head2 _default_summary_format
The default ticket summary format (used for displaying tickets in a
list, generally).
=cut
sub _default_summary_format { '%s,$luid | %s,summary | %s,status' }
=head2 validate_prop_status { props = $hashref, errors = $hashref }
Determines whether the status prop value given in C<$args{props}{status}>
is valid.
Returns true if the status is valid. If the status is invalid, sets
C<$args{errors}{status}> to an error message and returns false.
=cut
sub validate_prop_status {
my ( $self, %args ) = @_;
return $self->validate_prop_from_recommended_values( 'status', \%args );
}
sub validate_prop_component {
my ( $self, %args ) = @_;
return $self->validate_prop_from_recommended_values( 'component', \%args );
}
sub validate_prop_milestone {
my ( $self, %args ) = @_;
return $self->validate_prop_from_recommended_values( 'milestone', \%args );
}
sub _recommended_values_for_prop_milestone {
return @{ shift->app_handle->setting( label => 'milestones' )->get() };
}
sub _recommended_values_for_prop_status {
return @{ shift->app_handle->setting( label => 'statuses' )->get() };
}
lib/App/SD/Record.pm view on Meta::CPAN
package App::SD::Record;
use Any::Moose;
use Params::Validate;
extends 'Prophet::Record';
override declared_props => sub { 'created' };
sub canonicalize_prop_created {
my $self = shift;
my %args = validate(@_, { props => 1, errors => 1});
# has the record been created yet? if not, we don't want to try to
# get its properties
my $props = $self->uuid ? $self->get_props : {};
my $created = $args{props}->{created}
|| $args{props}->{date}
|| $props->{created}
|| $props->{date};
lib/App/SD/Replica/debbugs/PullEncoder.pm view on Meta::CPAN
has sync_source => (
isa => 'App::SD::Replica::debbugs',
is => 'rw',
);
our $DEBUG = $Prophet::Handle::DEBUG;
sub run {
my $self = shift;
my %args = validate( @_, {
# mandatory args go here
example => 1,
}
);
# TODO: code goes here
}
our %PROP_MAP = (
remote_prop => 'sd_prop',
lib/App/SD/Replica/debbugs/PullEncoder.pm view on Meta::CPAN
}
memoize 'resolve_user_id_to_email';
=head2 find_matching_tickets QUERY
=cut
sub find_matching_tickets {
my $self = shift;
my ($query) = validate_pos(@_, 1);
return $self->sync_source->rt->search( type => 'ticket', query => $query );
}
=head2 find_matching_transactions TASK, START
=cut
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { task => 1, starting_transaction => 1 } );
# ...
return \@matched;
}
__PACKAGE__->meta->make_immutable;
no Any::Moose;
1;
lib/App/SD/Replica/debbugs/PushEncoder.pm view on Meta::CPAN
is => 'rw');
=head2 integrate_change L<Prophet::Change>, L<Prophet::ChangeSet>
Should be able to leave as-is, theoretically.
=cut
sub integrate_change {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my $id;
eval {
if ( $change->record_type eq 'ticket'
and $change->change_type eq 'add_file'
)
{
lib/App/SD/Replica/debbugs/PushEncoder.pm view on Meta::CPAN
warn $@ if $@;
return $id;
}
=head2 integrate_ticket_create L<Prophet::Change>, L<Prophet::ChangeSet>
=cut
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# ...
# returns the id of the new ticket
# XXX is this uuid or what?
}
=head2 integrate_comment L<Prophet::Change>, L<Prophet::ChangeSet>
=cut
sub integrate_comment {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# ...
# returns the remote id of the ticket for this change
}
=head2 integrate_attachment L<Prophet::Change>, L<Prophet::ChangeSet>
=cut
sub integrate_attachment {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# ...
# returns the remote id of the ticket for this change
}
=head2 integrate_ticket_update L<Prophet::Change>, L<Prophet::ChangeSet>
=cut
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
}
__PACKAGE__->meta->make_immutable;
no Any::Moose;
lib/App/SD/Replica/gcode/PullEncoder.pm view on Meta::CPAN
}
=head2 find_matching_transactions { ticket => $id, starting_transaction => $num }
Returns a reference to an array of all transactions (as hashes) on ticket $id after transaction $num.
=cut
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my @raw_txns = @{ $args{ticket}->comments };
my @txns;
for my $txn ( sort { $a->sequence <=> $b->sequence } @raw_txns ) {
my $txn_date = $txn->date->epoch;
# Skip things we know we've already pulled
next if $txn_date < ( $args{'starting_transaction'} || 0 );
# Skip things we've pushed
lib/App/SD/Replica/gcode/PullEncoder.pm view on Meta::CPAN
txn => $txn,
changeset => $changeset,
attachment => $att,
);
}
}
sub _recode_attachment_create {
my $self = shift;
my %args =
validate( @_,
{ ticket_uuid => 1, txn => 1, changeset => 1, attachment => 1 } );
my $change = Prophet::Change->new(
{
record_type => 'attachment',
record_uuid => $self->sync_source->uuid_for_url(
$self->sync_source->remote_url
. "/attachment/"
. $args{'attachment'}->id,
),
change_type => 'add_file',
lib/App/SD/Replica/gcode/PushEncoder.pm view on Meta::CPAN
use Net::Google::Code;
use Try::Tiny;
has sync_source => (
isa => 'App::SD::Replica::gcode',
is => 'rw',
);
sub integrate_change {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my ( $id, $record );
# if the original_sequence_no of this changeset is <=
# the last changeset our sync source for the original_sequence_no, we can skip it.
# XXX TODO - this logic should be at the changeset level, not the cahnge level, as it applies to all
# changes in the changeset
lib/App/SD/Replica/gcode/PushEncoder.pm view on Meta::CPAN
);
} catch {
$self->sync_source->log( "Push error: " . $_ );
};
return $id;
}
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my $remote_ticket_id =
$self->sync_source->remote_id_for_uuid( $change->record_uuid );
my $ticket = $self->sync_source->gcode->issue();
$ticket->load($remote_ticket_id);
$ticket->update( %{ $self->_recode_props_for_integrate($change) }, );
return $remote_ticket_id;
}
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Build up a ticket object out of all the record's attributes
my $ticket = $self->sync_source->gcode->issue;
my $id =
$ticket->create( %{ $self->_recode_props_for_integrate($change) } );
return $id;
}
sub integrate_comment {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
my $ticket = $self->sync_source->gcode->issue( id => $ticket_id );
my %content = ( comment => $props{'content'}, );
$ticket->update(%content);
return $ticket_id;
}
sub integrate_attachment {
my ( $self, $change, $changeset ) = validate_pos(
@_,
{ isa => 'App::SD::Replica::gcode::PushEncoder' },
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
my $ticket = $self->sync_source->gcode->issue( id => $ticket_id, );
lib/App/SD/Replica/gcode/PushEncoder.pm view on Meta::CPAN
open my $fh, '>', $file or die $!;
print $fh $props{content};
close $fh;
my %content = ( comment => '(See attachments)', files => ["$file"] );
$ticket->update(%content);
return $ticket_id;
}
sub _recode_props_for_integrate {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my %attr;
for my $key ( keys %props ) {
if ( $key =~ /^(summary|owner|cc|blocked_on)/ ) {
$attr{$key} = $props{$key};
}
elsif ( $key eq 'status' ) {
$attr{$key} = ucfirst $props{$key};
lib/App/SD/Replica/github/PullEncoder.pm view on Meta::CPAN
Returns a reference to an array of all transactions (as hashes) on ticket $id
after transaction $num.
For GitHub, we can't get change history for tickets; we can only get comments.
=cut
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my @raw_txns =
@{ $self->sync_source->github->issue->comments( $args{ticket}->{number} ) };
for my $comment (@raw_txns) {
$comment->{updated_at} =
App::SD::Util::string_to_datetime( $comment->{updated_at} );
$comment->{created_at} =
App::SD::Util::string_to_datetime( $comment->{created_at} );
}
lib/App/SD/Replica/github/PushEncoder.pm view on Meta::CPAN
use Params::Validate;
use Path::Class;
has sync_source => (
isa => 'App::SD::Replica::github',
is => 'rw',
);
sub integrate_change {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my ( $id, $record );
# if the original_sequence_no of this changeset is <=
# the last changeset our sync source for the original_sequence_no, we can skip it.
# XXX TODO - this logic should be at the changeset level, not the cahnge level, as it applies to all
# changes in the changeset
lib/App/SD/Replica/github/PushEncoder.pm view on Meta::CPAN
if ( my $err = $@ ) {
$self->sync_source->log( "Push error: " . $err );
}
return $id;
}
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my $remote_ticket_id =
$self->sync_source->remote_id_for_uuid( $change->record_uuid );
my $ticket = $self->sync_source->github->issue();
my $attr = $self->_recode_props_for_integrate($change);
$ticket->edit( $remote_ticket_id, $attr->{title}, $attr->{body} );
if ( $attr->{status} ) {
$ticket->reopen( $remote_ticket_id ) if $attr->{status} eq 'open';
$ticket->close( $remote_ticket_id ) if $attr->{status} eq 'closed';
}
return $remote_ticket_id;
}
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Build up a ticket object out of all the record's attributes
my $ticket = $self->sync_source->github->issue;
my $attr = $self->_recode_props_for_integrate($change);
my $new =
$ticket->open( $attr->{title}, $attr->{body} );
# TODO: better error handler?
if ( $new->{error} ) {
die "\n\n$new->{error}";
}
return $new->{number};
}
sub integrate_comment {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
my $ticket = $self->sync_source->github->issue();
$ticket->comment($ticket_id, $props{'content'});
return $ticket_id;
}
sub _recode_props_for_integrate {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my %attr;
for my $key ( keys %props ) {
if ( $key eq 'summary' ) {
$attr{title} = $props{$key};
}
elsif ( $key eq 'body' ) {
$attr{$key} = $props{$key};
lib/App/SD/Replica/hm/PullEncoder.pm view on Meta::CPAN
}
return $status->{content}{tasks};
}
# hiveminder transaction ~= prophet changeset
# hiveminder taskhistory ~= prophet change
# hiveminder taskemail ~= prophet change
#
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my $txns = $self->sync_source->hm->search( 'TaskTransaction', task_id => $args{ticket}->{id} )
|| [];
my @matched;
for my $txn (@$txns) {
# Skip things we know we don't want
next if $txn->{'id'} < $args{'starting_transaction'};
# Skip things we've pushed
lib/App/SD/Replica/hm/PullEncoder.pm view on Meta::CPAN
serial => $txn->{id},
object => $txn
};
}
return \@matched;
}
sub add_prop_change {
my $self = shift;
my %args = validate( @_, { history_entry => 1, previous_state => 1, change => 1 } );
no warnings 'uninitialized';
my $field = qq{$args{'history_entry'}{'field'}} ||'';
my $old = qq{$args{'history_entry'}{'old_value'}} ||'';
my $new = qq{$args{'history_entry'}{'new_value'}} ||'';
if ( $args{'previous_state'}->{$field} eq $new ) {
$args{'previous_state'}->{$field} = $old;
} else {
$args{'previous_state'}->{$field} = $old;
warn "$field: ". $args{'previous_state'}->{$field} . " != " . $new . "\n\n";
}
$args{change}->add_prop_change( name => $field, old => $old, new => $new );
}
sub recode_create {
my $self = shift;
my %args = validate( @_, { task => 1, transaction => 1 } );
my $source = $self->sync_source;
my $res = Prophet::Change->new(
{ record_type => 'ticket',
record_uuid => $source->uuid_for_remote_id( $args{'task'}->{'id'} ),
change_type => 'add_file'
}
);
$args{'task'}{ $source->uuid . '-' . $_ } = delete $args{'task'}->{$_}
lib/App/SD/Replica/hm/PullEncoder.pm view on Meta::CPAN
while ( my ( $k, $v ) = each %{ $args{'task'} } ) {
$res->add_prop_change( name => $k, old => undef, new => $v );
}
return $res;
}
sub recode_update {
my $self = shift;
my %args = validate( @_, { task => 1, transaction => 1 } );
# In Hiveminder, a changeset has only one change
my $res = Prophet::Change->new(
{ record_type => 'ticket',
record_uuid => $self->sync_source->uuid_for_remote_id( $args{'task'}->{'id'} ),
change_type => 'update_file'
}
);
for my $entry ( @{ $args{'transaction'}->{'history_entries'} } ) {
lib/App/SD/Replica/hm/PullEncoder.pm view on Meta::CPAN
history_entry => $entry,
previous_state => $args{'task'},
);
}
return $res;
}
# This is a comment, basically.
sub recode_email {
my $self = shift;
my %args = validate( @_, { task => 1, transaction => 1 } );
# I *think* we should only ever have one email entry at a time, but let's
# check to make sure
if ( scalar @{$args{'transaction'}->{'email_entries'}} > 1 ) {
use Data::Dumper;
die "more than one entry in email_entries:\n"
. Dumper($args{'transaction'}->{'email_entries'});
}
my $ticket_uuid = $self->sync_source->uuid_for_remote_id( $args{'transaction'}->{'task_id'} );
lib/App/SD/Replica/hm/PushEncoder.pm view on Meta::CPAN
use Data::Dumper;
use Path::Class;
has sync_source => (
isa => 'App::SD::Replica::hm',
is => 'rw'
);
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset )
= validate_pos( @_, { isa => 'Prophet::Change' }, { isa => 'Prophet::ChangeSet' } );
# Build up a ticket object out of all the record's attributes
my %args = (
owner => 'me',
group => 0,
complete => 0,
will_complete => 1,
repeat_stacking => 0,
%{ $self->_recode_props_for_create($change) }
);
lib/App/SD/Replica/hm/PushEncoder.pm view on Meta::CPAN
changeset => $changeset,
record => $tid
);
return $tid;
}
sub integrate_comment {
my $self = shift;
my ( $change, $changeset )
= validate_pos( @_, { isa => 'Prophet::Change' }, { isa => 'Prophet::ChangeSet' } );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} )
or die "Couldn't get remote id of SD ticket";
my $email = $self->comment_as_email( \%props );
my $status = $self->sync_source->hm->act(
'CreateTaskEmail',
task_id => $ticket_id,
message => $email->as_string,
);
return $status->{'id'} unless $self->sync_source->request_failed($status);
die "Couldn't integrate comment: " . $self->sync_source->decode_error($status);
}
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset )
= validate_pos( @_, { isa => 'Prophet::Change' }, { isa => 'Prophet::ChangeSet' } );
my %args = $self->translate_props($change);
return unless keys %args;
my $tid = $self->sync_source->remote_id_for_uuid( $change->record_uuid )
or die "Couldn't get remote id of SD ticket";
my ( $seen_current, $dropped_all, @new_requestors ) = ( 0, 0 );
if ( exists $args{'requestor_id'}
&& defined $args{'requestor_id'}
lib/App/SD/Replica/hm/PushEncoder.pm view on Meta::CPAN
message => $email->as_string,
);
warn "Couldn't add a comment on the recently created HM task"
if $self->sync_source->request_failed($status);
return $status->{'id'};
}
sub integrate_attachment {
my $self = shift;
my ( $change, $changeset )
= validate_pos( @_, { isa => 'Prophet::Change' }, { isa => 'Prophet::ChangeSet' } );
unless ( $self->sync_source->user_info->{'pro_account'} ) {
warn "Pro account is required to push attachments";
return;
}
my %props = $self->translate_props($change);
$props{'content'} = {
content => $props{'content'},
filename => delete $props{'name'},
lib/App/SD/Replica/hm/PushEncoder.pm view on Meta::CPAN
From => $props->{'creator'},
Date => $props->{'created'},
],
body => $props->{'content'},
);
return $res;
}
sub _recode_props_for_integrate {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %props = $self->translate_props($change);
my %attr;
for my $key ( keys %props ) {
$attr{$key} = $props{$key};
}
return \%attr;
}
sub translate_props {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %PROP_MAP = $self->sync_source->property_map('push');
my %props = map { $_->name => $_->new_value } $change->prop_changes;
delete $props{$_} for @{ delete $PROP_MAP{'_delete'} };
while ( my ( $k, $v ) = each %PROP_MAP ) {
next unless exists $props{$k};
$props{$v} = delete $props{$k};
}
return %props;
lib/App/SD/Replica/lighthouse/PullEncoder.pm view on Meta::CPAN
}
=head2 find_matching_transactions { ticket => $id, starting_transaction => $num }
Returns a reference to an array of all transactions (as hashes) on ticket $id after transaction $num.
=cut
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my $sequence = 0;
# hack, let's add sequence for comments
my @raw_versions =
map { $_->{sequence} = $sequence++; $_ } $args{ticket}->versions;
my @raw_attachments = $args{ticket}->attachments;
my @raw_txns = ( @raw_versions, @raw_attachments );
my @txns;
for my $txn ( @raw_txns ) {
my $txn_date = $txn->created_at->epoch;
lib/App/SD/Replica/lighthouse/PullEncoder.pm view on Meta::CPAN
);
$comment->add_prop_change( name => 'ticket', new => $ticket_uuid, );
$changeset->add_change( { change => $comment } );
}
}
}
sub _recode_attachment_create {
my $self = shift;
my %args =
validate( @_,
{ ticket_uuid => 1, changeset => 1, attachment => 1 } );
my $change = Prophet::Change->new(
{
record_type => 'attachment',
record_uuid => $self->sync_source->uuid_for_url(
$self->sync_source->remote_url
. "/attachment/"
. $args{'attachment'}->id,
),
change_type => 'add_file',
lib/App/SD/Replica/lighthouse/PushEncoder.pm view on Meta::CPAN
use Params::Validate;
use Path::Class;
has sync_source => (
isa => 'App::SD::Replica::lighthouse',
is => 'rw',
);
sub integrate_change {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my ( $id, $record );
return
if $self->sync_source->app_handle->handle->last_changeset_from_source(
$changeset->original_source_uuid ) >= $changeset->original_sequence_no;
my $before_integration = time();
lib/App/SD/Replica/lighthouse/PushEncoder.pm view on Meta::CPAN
if ( my $err = $@ ) {
$self->sync_source->log( "Push error: " . $err );
}
return $id;
}
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my $remote_ticket_id =
$self->sync_source->remote_id_for_uuid( $change->record_uuid );
my $ticket = $self->sync_source->lighthouse->ticket;
$ticket->load( $remote_ticket_id );
lib/App/SD/Replica/lighthouse/PushEncoder.pm view on Meta::CPAN
$ticket->update(
map { $_ => $attr->{$_} }
grep { exists $attr->{$_} }
qw/title body state assigned_user_id milestone_id tag/
);
return $remote_ticket_id;
}
sub integrate_comment {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
my $ticket = $self->sync_source->lighthouse->ticket;
$ticket->load( $ticket_id );
my %content = ( body => $props{'content'} || '' );
$ticket->update(%content);
return $ticket_id;
}
sub integrate_ticket_attachment {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
$self->sync_source->log(
'Warn: Net::Lighthouse does *not* support attachment yet');
return $ticket_id;
}
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Build up a ticket object out of all the record's attributes
my $ticket = $self->sync_source->lighthouse->ticket;
my $attr = $self->_recode_props_for_integrate($change);
$ticket->create(
map { $_ => $attr->{$_} }
grep { exists $attr->{$_} }
qw/title body state assigned_user_id milestone_id tag/
);
return $ticket->number;
}
sub _recode_props_for_integrate {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my %attr;
for my $key ( keys %props ) {
if ( $key eq 'summary' ) {
$attr{title} = $props{$key};
}
elsif ( $key eq 'status' ) {
$attr{state} = $props{$key};
lib/App/SD/Replica/redmine/PullEncoder.pm view on Meta::CPAN
my $redmine = $self->sync_source->redmine;
my $search = $redmine->search_ticket( $query{query} );
my @results = $search->results;
return \@results;
}
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my @txns;
my $raw_txn = $args{ticket}->histories;
for my $txn (@$raw_txn) {
push @txns, {
timestamp => $txn->date->epoch,
serial => $txn->id,
object => $txn
}
lib/App/SD/Replica/redmine/PushEncoder.pm view on Meta::CPAN
use Params::Validate;
has sync_source => (
isa => 'App::SD::Replica::redmine',
is => 'rw',
required => 1
);
sub integrate_change {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my ( $id, $record );
return
if $self->sync_source->app_handle->handle->last_changeset_from_source(
$changeset->original_source_uuid ) >= $changeset->original_sequence_no;
lib/App/SD/Replica/redmine/PushEncoder.pm view on Meta::CPAN
if ( my $err = $@ ) {
$self->sync_source->log( "Push error: " . $err );
}
return $id;
}
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my $remote_ticket_id
= $self->sync_source->remote_id_for_uuid( $change->record_uuid );
my $attr = $self->_recode_props_for_integrate($change);
my $ticket = Net::Redmine::Ticket->load(
connection => $self->sync_source->redmine->connection,
lib/App/SD/Replica/redmine/PushEncoder.pm view on Meta::CPAN
if ( $attr->{state} ) {
$ticket->status("Open") if $attr->{state} eq 'open';
$ticket->status("Closed") if $attr->{state} eq 'closed';
}
$ticket->save;
return $remote_ticket_id;
}
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my $attr = $self->_recode_props_for_integrate($change);
my $ticket = $self->sync_source->redmine->create(ticket => $attr);
# TODO error
return $ticket->{id};
}
sub integrate_comment {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id
= $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
my $ticket = Net::Redmine::Ticket->load(
connection => $self->sync_source->redmine->connection,
id => $ticket_id
);
$ticket->description( $props{'content'} );
$ticket->save;
return $ticket_id;
}
sub _recode_props_for_integrate {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my %attr;
for my $key ( keys %props ) {
if ( $key eq 'summary' ) {
$attr{subject} = $props{$key};
}
elsif ( $key eq 'body' ) {
$attr{description} = $props{$key};
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
}
=head2 find_matching_tickets query => QUERY
Returns an RT::Client ticket collection for all tickets found matching your QUERY string.
=cut
sub find_matching_tickets {
my $self = shift;
my %args = validate(@_,{query => 1});
my $query = $args{query};
# If we've ever synced, we can limit our search to only newer things
if ( my $before = $self->_only_pull_tickets_modified_after ) {
$query = "($query) AND LastUpdated >= '" . $before->ymd('-') . " " . $before->hms(':') . "'";
$self->sync_source->log( "Skipping all tickets not updated since " . $before->iso8601 );
}
return [map {
Prophet::CLI->end_pager();
# squelch chatty RT::Client::REST "Unknown key" warnings unless debugging turned on
local $SIG{__WARN__} = sub { $self->sync_source->log_debug(@_) };
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
=head2 find_matching_transactions { ticket => $id, starting_transaction => $num }
Returns a reference to an array of all transactions (as hashes) on ticket $id after transaction $num.
=cut
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my @txns;
my $rt_handle = $self->sync_source->rt;
my $ticket_id = $self->ticket_id( $args{ticket} );
my $latest = $self->sync_source->app_handle->handle->last_changeset_from_source(
$self->sync_source->uuid_for_remote_id($ticket_id) ) || 0;
for my $txn ( sort $rt_handle->get_transaction_ids( parent_id => $ticket_id ) ) {
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
}
return $changeset;
}
{ # Recoding RT transactions
sub _recode_attachment_create {
my $self = shift;
my %args = validate( @_, { ticket => 1, txn => 1, changeset => 1, attachment => 1 } );
my $change = Prophet::Change->new(
{ record_type => 'attachment',
record_uuid => $self->sync_source->uuid_for_url( $self->sync_source->remote_url . "/attachment/" . $args{'attachment'}->{'id'} ),
change_type => 'add_file'
}
);
$change->add_prop_change( name => 'content_type', old => undef, new => $args{'attachment'}->{'ContentType'});
$change->add_prop_change( name => 'created', old => undef, new => $args{'txn'}->{'Created'} );
$change->add_prop_change( name => 'creator', old => undef, new => $self->resolve_user_id_to( email_address => $args{'attachment'}->{'Creator'}));
$change->add_prop_change( name => 'content', old => undef, new => $args{'attachment'}->{'Content'});
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
sub _recode_txn_Keyword {} # RT 2 - unused
sub _recode_txn_CommentEmailRecord { return; }
sub _recode_txn_EmailRecord { return; }
sub _recode_txn_AddReminder { return; }
sub _recode_txn_ResolveReminder { return; }
sub _recode_txn_DeleteLink { }
sub _recode_txn_Status {
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
$args{txn}->{'Type'} = 'Set';
return $self->_recode_txn_Set(%args);
}
sub _recode_txn_Told {
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
$args{txn}->{'Type'} = 'Set';
return $self->_recode_txn_Set(%args);
}
sub _recode_txn_Set {
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
my $change = Prophet::Change->new(
{ record_type => 'ticket',
record_uuid => $self->sync_source->uuid_for_remote_id( $args{'ticket'}->{$self->sync_source->uuid . '-id'} ),
change_type => 'update_file'
}
);
my ($field, $old, $new) = @{ $args{txn} }{qw(Field OldValue NewValue)};
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
$change->add_prop_change( name => $field, old => $old, new => $new );
}
*_recode_txn_Steal = \&_recode_txn_Set;
*_recode_txn_Take = \&_recode_txn_Set;
*_recode_txn_Give = \&_recode_txn_Set;
sub _recode_txn_Create {
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
my $change = Prophet::Change->new( {
record_type => 'ticket',
record_uuid => $self->sync_source->uuid_for_remote_id(
$args{'ticket'}->{$self->sync_source->uuid . '-id'}
),
change_type => 'add_file'
} );
$args{'changeset'}->add_change( { change => $change } );
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
$self->_recode_content_update(%args); # add the create content txn as a seperate change in this changeset
}
*_recode_txn_Link = \&_recode_txn_AddLink;
sub _recode_txn_AddLink {
# XXX, TODO: syncing links doesn't work
return;
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
my $new_state = $args{'ticket'}->{ $args{'txn'}->{'Field'} };
$args{'ticket'}->{ $args{'txn'}->{'Field'} } = $self->warp_list_to_old_value(
$args{'ticket'}->{ $args{'txn'}->{'Field'} },
$args{'txn'}->{'NewValue'},
$args{'txn'}->{'OldValue'}
);
my $change = Prophet::Change->new( {
record_type => 'ticket',
record_uuid => $self->sync_source->uuid_for_remote_id(
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
$change->add_prop_change(
name => $args{'txn'}->{'Field'},
old => $args{'ticket'}->{ $args{'txn'}->{'Field'} },
new => $new_state
);
$args{'changeset'}->add_change( { change => $change } );
}
sub _recode_content_update {
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
my $url = $self->sync_source->remote_url . "/transaction/" . $args{'txn'}->{'id'};
my $change = Prophet::Change->new( {
record_type => 'comment',
record_uuid => $self->sync_source->uuid_for_url( $url ),
change_type => 'add_file',
} );
$change->add_prop_change( name => 'created', new => $args{'txn'}->{'Created'});
$change->add_prop_change( name => 'type', new => $args{'txn'}->{'Type'});
$change->add_prop_change( name => 'creator', new => $self->resolve_user_id_to(
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
$args{'ticket'}->{ $self->sync_source->uuid . '-id'}
) );
$args{'changeset'}->add_change( { change => $change } );
}
*_recode_txn_Comment = \&_recode_content_update;
*_recode_txn_Correspond = \&_recode_content_update;
sub _recode_txn_AddWatcher {
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
my $type = $args{'txn'}->{'Field'};
my $new_state = $args{'ticket'}->{ $type .'s' };
$args{'ticket'}->{ $type .'s' } = $self->warp_list_to_old_value(
$new_state,
$self->resolve_user_id_to( email_address => $args{'txn'}->{'NewValue'} ),
$self->resolve_user_id_to( email_address => $args{'txn'}->{'OldValue'} )
);
lib/App/SD/Replica/rt/PullEncoder.pm view on Meta::CPAN
old => $args{'ticket'}->{ $args{'txn'}->{'Field'} .'s' },
new => $new_state
);
$args{'changeset'}->add_change( { change => $change } );
}
*_recode_txn_DelWatcher = \&_recode_txn_AddWatcher;
sub _recode_txn_CustomField {
my $self = shift;
my %args = validate( @_, { txn => 1, ticket => 1, changeset => 1 } );
my $new = $args{'txn'}->{'NewValue'};
my $old = $args{'txn'}->{'OldValue'};
my $name;
if ( $args{'txn'}->{'Description'} =~ /^(.*) $new added by/ ) {
$name = $1;
}
elsif ( $args{'txn'}->{'Description'} =~ /^(.*) changed to $new by/ ) {
$name = $1;
lib/App/SD/Replica/rt/PushEncoder.pm view on Meta::CPAN
extends 'App::SD::ForeignReplica::PushEncoder';
use Params::Validate;
has sync_source =>
( isa => 'App::SD::Replica::rt',
is => 'rw');
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my $remote_ticket_id = $self->sync_source->remote_id_for_uuid( $change->record_uuid );
my $ticket = RT::Client::REST::Ticket->new(
rt => $self->sync_source->rt,
id => $remote_ticket_id,
%{ $self->_recode_props_for_integrate($change) }
)->store();
return $remote_ticket_id;
}
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Build up a ticket object out of all the record's attributes
my $ticket = RT::Client::REST::Ticket->new(
rt => $self->sync_source->rt,
queue => $self->sync_source->rt_queue(),
%{ $self->_recode_props_for_integrate($change) }
)->store( text => "Not yet pulling in ticket creation comment" );
return $ticket->id;
}
sub integrate_comment {
my $self = shift;
my ($change, $changeset) = validate_pos( @_, { isa => 'Prophet::Change' }, {isa => 'Prophet::ChangeSet'} );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
# Figure out the remote site's ticket ID for this change's record
my $ticket = RT::Client::REST::Ticket->new( rt => $self->sync_source->rt, id => $ticket_id);
my %content = ( message => $props{'content'},
);
if ( ($props{'type'} ||'') eq 'comment' ) {
$ticket->comment( %content);
} else {
$ticket->correspond(%content);
}
return $ticket_id;
}
sub integrate_attachment {
my ($self, $change, $changeset ) = validate_pos( @_, { isa => 'App::SD::Replica::rt::PushEncoder'}, { isa => 'Prophet::Change' }, { isa => 'Prophet::ChangeSet' });
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'});
my $ticket = RT::Client::REST::Ticket->new( rt => $self->sync_source->rt, id => $ticket_id );
my $tempdir = File::Temp::tempdir( CLEANUP => 1 );
my $file = File::Spec->catfile( $tempdir, ( $props{'name'} || 'unnamed' ) );
open my $fh, '>', $file or die $!;
print $fh $props{content};
close $fh;
my %content = ( message => '(See attachments)', attachments => ["$file"]);
$ticket->correspond(%content);
return $ticket_id;
}
sub _recode_props_for_integrate {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my %attr;
for my $key ( keys %props ) {
next unless ( $key =~ /^(summary|queue|status|owner|custom)/ );
if ( $key =~ /^custom-(.*)/ ) {
$attr{cf}->{$1} = $props{$key};
} elsif ( $key eq 'summary' ) {
$attr{'subject'} = $props{summary};
lib/App/SD/Replica/trac/PullEncoder.pm view on Meta::CPAN
=head2 find_matching_transactions { ticket => $id, starting_transaction => $num }
Returns a reference to an array of all transactions (as hashes) on ticket $id
after transaction $num.
=cut
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my @raw_txns = @{$args{ticket}->history->entries};
my @txns;
# XXX TODO make this one loop.
for my $txn ( sort { $a->date cmp $b->date} @raw_txns) {
my $txn_date = $txn->date->epoch;
# Skip things we know we've already pulled
next if $txn_date < ( $args{'starting_transaction'} ||0 );
# Skip things we've pushed
next if ($self->sync_source->foreign_transaction_originated_locally($txn_date, $args{'ticket'}->id) );
lib/App/SD/Replica/trac/PullEncoder.pm view on Meta::CPAN
}
return unless $changeset->has_changes;
return $changeset;
}
sub _recode_attachment_create {
my $self = shift;
my %args =
validate( @_,
{ ticket => 1, txn => 1, changeset => 1, attachment => 1 } );
my $change = Prophet::Change->new(
{
record_type => 'attachment',
record_uuid => $self->sync_source->uuid_for_url(
$self->sync_source->remote_url
. "/attachment/"
. $args{'attachment'}->date->epoch
),
change_type => 'add_file',
lib/App/SD/Replica/trac/PushEncoder.pm view on Meta::CPAN
is => 'rw');
extends 'App::SD::ForeignReplica::PushEncoder';
sub after_integrate_change {
usleep(1100); # trac only accepts one ticket update per second. Yes.
}
sub integrate_ticket_update {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Figure out the remote site's ticket ID for this change's record
my $remote_ticket_id =
$self->sync_source->remote_id_for_uuid( $change->record_uuid );
my $ticket = Net::Trac::Ticket->new( connection => $self->sync_source->trac);
$ticket->load($remote_ticket_id) or
die "couldn't load remote track ticket $remote_ticket_id\n";
$ticket->update( %{ $self->_recode_props_for_integrate($change) } ) or
die "couldn't update remote track ticket $remote_ticket_id\n";
return $remote_ticket_id;
}
sub integrate_ticket_create {
my $self = shift;
my ( $change, $changeset ) = validate_pos(
@_,
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' }
);
# Build up a ticket object out of all the record's attributes
my $ticket = Net::Trac::Ticket->new(
connection => $self->sync_source->trac);
my $id = $ticket->create( %{ $self->_recode_props_for_integrate($change) });
return $id
}
sub integrate_comment {
my $self = shift;
my ($change, $changeset) = validate_pos( @_,
{ isa => 'Prophet::Change' }, {isa => 'Prophet::ChangeSet'} );
# Figure out the remote site's ticket ID for this change's record
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
my $ticket = Net::Trac::Ticket->new( connection => $self->sync_source->trac);
$ticket->load($ticket_id);
$ticket->comment( $props{content});
return $ticket_id;
}
sub integrate_attachment {
my ($self, $change, $changeset ) = validate_pos( @_,
{ isa => 'App::SD::Replica::trac::PushEncoder'},
{ isa => 'Prophet::Change' },
{ isa => 'Prophet::ChangeSet' });
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my $ticket_id = $self->sync_source->remote_id_for_uuid( $props{'ticket'} );
my $ticket = Net::Trac::Ticket->new( connection => $self->sync_source->trac);
$ticket->load($ticket_id);
lib/App/SD/Replica/trac/PushEncoder.pm view on Meta::CPAN
my $file = File::Spec->catfile( $tempdir, ( $props{'name'} || 'unnamed' ) );
open my $fh, '>', $file or die $!;
print $fh $props{content};
close $fh;
$ticket->attach( file => $file) || die "Could not attach file for ticket $ticket_id";
return $ticket_id;
}
sub _recode_props_for_integrate {
my $self = shift;
my ($change) = validate_pos( @_, { isa => 'Prophet::Change' } );
my %props = map { $_->name => $_->new_value } $change->prop_changes;
my %attr;
for my $key ( keys %props ) {
next unless ( $key =~ /^(summary|status|owner)/ );
if ( $key eq 'status' ) {
my $active_statuses =
$self->sync_source->database_settings->{active_statuses};
if ( grep { $props{$key} eq $_ } @$active_statuses, 'closed' ) {
lib/App/SD/Util.pm view on Meta::CPAN
package App::SD::Util;
use Any::Moose; # for warnings and strict at the least
use DateTime;
use Params::Validate qw/:all/;
my %MONTHS = ( jan => 1, feb => 2, mar => 3, apr => 4, may => 5, jun => 6, jul => 7, aug => 8, sep => 9, oct => 10, nov => 11, dec => 12);
sub string_to_datetime {
my ($date)= validate_pos(@_, { type => SCALAR | UNDEF} );
return unless defined($date);
if ($date =~ /^(\d{4})-(\d{2})-(\d{2})[T\s](\d{1,2}):(\d{2}):(\d{2})Z?$/ ){
my ($year,$month,$day, $hour,$min,$sec) = ($1,$2,$3,$4,$5,$6);
my $dt = DateTime->new( year => $year,
month => $month,
day => $day,
hour => $hour,
minute => $min,