App-SD

 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,



( run in 0.722 second using v1.01-cache-2.11-cpan-a5abf4f5562 )