[Bps-public-commit] r9551 - in RT-Client-Console/trunk: . bin lib/RT lib/RT/Client lib/RT/Client/Console lib/RT/Client/Console/Session lib/RT/Client/Console/Session/Ticket t
dams at bestpractical.com
dams at bestpractical.com
Mon Nov 5 09:11:10 EST 2007
Author: dams
Date: Mon Nov 5 09:11:08 2007
New Revision: 9551
Added:
RT-Client-Console/trunk/Makefile.PL
RT-Client-Console/trunk/README
RT-Client-Console/trunk/bin/
RT-Client-Console/trunk/bin/rtconsole (contents, props changed)
RT-Client-Console/trunk/lib/
RT-Client-Console/trunk/lib/RT/
RT-Client-Console/trunk/lib/RT/Client/
RT-Client-Console/trunk/lib/RT/Client/Console/
RT-Client-Console/trunk/lib/RT/Client/Console.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Cnx.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/
RT-Client-Console/trunk/lib/RT/Client/Console/Session.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/KeyHandler.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Progress.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Root.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Status.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Attachments.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/CustFields.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Header.pm
RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Links.pm
RT-Client-Console/trunk/t/
Log:
initial import
Added: RT-Client-Console/trunk/Makefile.PL
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/Makefile.PL Mon Nov 5 09:11:08 2007
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+ NAME => 'RT::Client::Console',
+ AUTHOR => 'Damien "dams" Krotkine <dams at zarb.org>',
+ VERSION_FROM => 'lib/RT/Client/Console.pm',
+ ABSTRACT_FROM => 'lib/RT/Client/Console.pm',
+ PL_FILES => {},
+ PREREQ_PM => {
+ 'Test::More' => 0,
+ 'version' => 0,
+ },
+ EXE_FILES => [ 'bin/rtconsole' ],
+ dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
+ clean => { FILES => 'RT-Client-Console-*' },
+);
Added: RT-Client-Console/trunk/README
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/README Mon Nov 5 09:11:08 2007
@@ -0,0 +1,47 @@
+RT-Client-Console version 0.0.1
+
+[ REPLACE THIS...
+
+ The README is used to introduce the module and provide instructions on
+ how to install the module, any machine dependencies it may have (for
+ example C compilers and installed libraries) and any other information
+ that should be understood before the module is installed.
+
+ A README file is required for CPAN modules since CPAN extracts the
+ README file from a module distribution so that people browsing the
+ archive can use it get an idea of the modules uses. It is usually a
+ good idea to provide version information here so that people can
+ decide whether fixes for the module are worth downloading.
+]
+
+
+INSTALLATION
+
+To install this module, run the following commands:
+
+ perl Makefile.PL
+ make
+ make test
+ make install
+
+
+Alternatively, to install with Module::Build, you can use the following commands:
+
+ perl Build.PL
+ ./Build
+ ./Build test
+ ./Build install
+
+
+
+DEPENDENCIES
+
+None.
+
+
+COPYRIGHT AND LICENCE
+
+Copyright (C) 2007, Damien "dams" Krotkine
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself.
Added: RT-Client-Console/trunk/bin/rtconsole
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/bin/rtconsole Mon Nov 5 09:11:08 2007
@@ -0,0 +1,181 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Getopt::Long;
+use Pod::Usage;
+
+use Config::Tiny;
+
+my %options;
+GetOptions(\%options,
+ 'server=s',
+ 'user=s',
+ 'pass=s',
+ 'queues=s',
+ 'config-file=s',
+ 'generate-config',
+ 'help',
+ ) or usage();
+$options{help} and usage();
+
+sub usage {
+ pod2usage( {
+ -verbose => 1,
+ }
+ );
+}
+
+my $config = {};
+exists $options{$_} and $config->{connection}{$_} = $options{$_} foreach (qw(server user pass));
+exists $options{$_} and $config->{server}{$_} = $options{$_} foreach (qw(queues));
+
+my $config_filename = $ENV{HOME} . '/.rtconcolerc';
+exists $options{'config-file'} && length $options{'config-file'} and $config_filename = $options{'config-file'};
+
+my $config_ini;
+if ($options{'generate-config'}) {
+ if (! -e $config_filename) {
+ $config_ini = Config::Tiny->new() or
+ die "Couldn't open $config_filename to save the configuration. The error was : $Config::Tiny::errstr \n";
+
+ } else {
+ $config_ini = Config::Tiny->read($config_filename) or
+ die "Couldn't open $config_filename to save the configuration. The error was : $Config::Tiny::errstr \n";
+ }
+ $config_ini->{$_} = $config->{$_} foreach keys %$config;
+ $config_ini->write($config_filename) or
+ die "Couldn't open $config_filename to save the configuration. The error was : $Config::Tiny::errstr \n";
+ print "wrote configuration to file $config_filename\n";
+ exit;
+}
+
+if (-e $config_filename) {
+ $config_ini = Config::Tiny->read($config_filename) or
+ die "Couldn't open $config_filename to save the configuration. The error was : $Config::Tiny::errstr \n";
+ $config->{$_} = $config_ini->{$_} foreach keys %$config_ini;
+}
+
+
+use Curses;
+my $curses_handler = new Curses;
+noecho();
+nodelay(1);
+$curses_handler->keypad(1);
+$curses_handler->syncok(1);
+curs_set(0);
+leaveok(1);
+
+# Erase the main window
+
+$curses_handler->erase();
+
+my $queues = $config->{server}{queues};
+my @queues = split(/\s+/, $queues);
+
+use RT::Client::Console;
+RT::Client::Console->run(curses_handler => $curses_handler,
+ rt_servername => $config->{connection}{server},
+ rt_username => $config->{connection}{user},
+ rt_password => $config->{connection}{pass},
+ queue_ids => \@queues,
+ );
+
+
+
+exit(0);
+
+__END__
+
+=head1 NAME
+
+rtconsole - RT text client console
+
+=head1 VERSION
+
+version 0.1
+
+=head1 USAGE
+
+ rtconsole [options]
+
+=head1 OPTIONS
+
+Options can be set --like=this, --like this, or -l this
+
+=over
+
+=item --server server_name
+
+Specify the RT server
+
+=item --user user_name
+
+Specify the user to connect to the server
+
+=item --pass password
+
+Specify the password to connect to the server
+
+=item --queues='1 12 54 23 84'
+
+Specify the ids of the queues on the server. That's because for now we can't
+get the list of queues automatically.
+
+=item --config-file filename
+
+Specify the config file to read. Default $HOME/.rtconsolerc
+
+=item --generate-config
+
+Generates the config file
+
+=item --version
+
+=item --usage
+
+=item --help
+
+=back
+
+=head1 DESCRIPTION
+
+rtconsole is a text client to RT using ncurses.
+
+=head1 FILES
+
+The config file $HOME/.rtconsolerc (see the --config-file option) can be use to
+set options. The format is .ini file style. Here is an example with all
+possible keys/values for now :
+
+ [connection]
+ server=rt.cpan.org
+ user=dams
+ pass=my_password
+
+ [server]
+ queues=1 2 42 12 3
+
+For now there is only one section cannled Connection. More will be added in
+the next versions of this software.
+
+You can generate the config file by using --generate-config. It'll be saved at
+the location sp--config-file, or ad the default location
+
+=head1 AUTHOR
+
+Damien "dams" Krotkine (DAMS at CPAN.org)
+
+=head1 BUGS
+
+There are undoubtedly serious bugs lurking somewhere in this code.
+Bug reports and other feedback are most welcome.
+
+=head1 COPYRIGHT
+
+Copyright (c) 2007, Damien Krotkine. All Rights Reserved.
+This module is free software. It may be used, redistributed
+and/or modified under the terms of Perl itself
+
+=cut
Added: RT-Client-Console/trunk/lib/RT/Client/Console.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,381 @@
+package RT::Client::Console;
+
+use warnings;
+use strict;
+use Carp;
+use version;
+our $VERSION = '0.0.1';
+
+use Params::Validate qw(:all);
+
+# global heap to keep an application-level state
+my %GLOBAL_HEAP = ( curses => {
+ handler => undef,
+ },
+ rt => { cnx => {
+ handler => undef,
+ servername => undef,
+ username => undef,
+ password => undef,
+ },
+ tickets => {
+ current => undef,
+ attachments => {
+ current => undef,
+ total => 0,
+ },
+ total => 0,
+ },
+ },
+ server => {
+ id_to_queue => {},
+ name_to_queue => {},
+ },
+ ui => {
+ tab => {
+ current => undef,
+ total => 0,
+ },
+ modal_sessions => [],
+ },
+# current_atta => undef,
+# total_tab => 0,
+# current_tab => undef,
+ sessions => [],
+ );
+sub GLOBAL_HEAP { \%GLOBAL_HEAP }
+
+sub run {
+ my ($class, @args) = @_;
+ my %params = validate( @args, { curses_handler => { isa => 'Curses' },
+ rt_servername => 0,
+ rt_username => 0,
+ rt_password => 0,
+ queue_ids => 0,
+ }
+ );
+
+
+ $class->GLOBAL_HEAP->{curses}{handler} = delete $params{curses_handler};
+
+ use RT::Client::Console::Session::Root;
+ RT::Client::Console::Session::Root->create();
+
+ use RT::Client::Console::Session::KeyHandler;
+ RT::Client::Console::Session::KeyHandler->create();
+
+ if ( exists $params{rt_servername}) {
+ use RT::Client::Console::Cnx;
+ RT::Client::Console::Cnx->connect(%params);
+ }
+
+ use RT::Client::Console::Session;
+ RT::Client::Console::Session->run();
+
+}
+
+
+
+use Curses::Forms::Dialog;
+sub error {
+ my ($class, $message) = @_;
+ dialog('Error', BTN_OK, $message,
+ qw(white red yellow));
+ return;
+}
+
+use Curses::Forms::Dialog::Input;
+sub input_ok_cancel {
+ my ($class, $title, $message, $length) = @_;
+ my ($rv, $value) = input($title, BTN_OK | BTN_CANCEL, $message, $length || 256,
+ qw(white blue yellow));
+ # XXX bug, if one doesn't enter anything, or an empty string or 0 or '0'...
+ if ( $rv == 0) {
+ return $value
+ }
+}
+
+use Curses::Widgets::ListBox;
+sub input_list {
+ my ($class, %args) = @_;
+ my @items = @{$args{items}};
+
+ my $list_style = 1; #simple
+ ref $items[0] eq 'HASH' and $list_style = 0; #complex
+
+ my @display_items = $list_style ? @items : map { $_->{text} } @items;
+ my @value_items = $list_style ? @items : map { $_->{value} } @items;
+
+ my $i;
+ my %index_of = map { $_ => $i++ } @value_items;
+ my %name_of = reverse %index_of;
+ my $value_idx = $index_of{$args{value}};
+ my $title = $args{title};
+
+ my ($screen_w, $screen_h);
+ my $curses_handler = $class->GLOBAL_HEAP->{curses}{handler};
+ $curses_handler->getmaxyx($screen_h, $screen_w);
+
+ use List::Util qw(min max);
+ my $height = min(@display_items + 2, $screen_h - 20);
+ my $width = min( max( map { length } (@display_items, $title) ) + 2, $screen_w - 20 );
+
+ my $list_box = Curses::Widgets::ListBox->new({
+ LINES => $height,
+ COLUMNS => $width,
+ Y => $screen_h/2-($height+2)/2,
+ X => $screen_w/2-($width+2)/2,,
+ LISTITEMS => \@display_items,
+ MULTISEL => 0,
+ VALUE => $value_idx,
+ FOCUSSWITCH => "\n",
+ SELECTEDCOL => 'red',
+ CAPTION => $title,
+ CAPTIONCOL => 'yellow',
+ CURSORPOS => $value_idx,
+ });
+ $class->my_execute($list_box, $curses_handler);
+ my $new_value = $name_of{$list_box->getField('VALUE')};
+ return $new_value;
+}
+
+
+{
+
+my %label_widgets;
+my $widget_name_index = 0;
+
+sub struct_to_widgets {
+ my ($class, $header_labels, $max_lines, $max_columns) = @_;
+ my @header_labels = @$header_labels;
+
+ my $x = 0;
+ my %label_widgets;
+ foreach my $group (@header_labels) {
+ my $y = 0;
+ use List::Util qw(max);
+ my $key_width = max( map { length } map { $_->[0] } @$group );
+ $x + $key_width > $max_columns
+ and $key_width = $max_columns - $x;
+ $key_width > 0 or last;
+
+ my $value_width = max( map { length } map { $_->[1] || '' } @$group );
+ $x + $key_width + 1 + $value_width > $max_columns
+ and $value_width = $max_columns - ($x + $key_width + 1);
+
+ foreach my $element (@$group) {
+
+ $y > $max_lines and last;
+
+ $label_widgets{"label$widget_name_index"} =
+ {
+ TYPE => 'Label',
+ X => $x,
+ Y => $y,
+ COLUMNS => $key_width,
+ LINES => 1,
+ FOREGROUND => 'yellow',
+ BACKGROUND => 'blue',
+ VALUE => $element->[0],
+ ALIGNMENT => 'R',
+ };
+ $widget_name_index++;
+
+ if ($value_width) {
+
+ $label_widgets{"label$widget_name_index"} =
+ {
+ TYPE => 'Label',
+ X => $x + $key_width + 1,
+ Y => $y,
+ COLUMNS => $value_width,
+ LINES => 1,
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ VALUE => $element->[1],
+ ALIGNMENT => 'L',
+ };
+ $widget_name_index++;
+ }
+ $y++;
+ }
+ $x += $key_width + 1 + $value_width +2;
+ }
+ return %label_widgets;
+}
+
+}
+
+sub my_execute {
+ my $class = shift;
+ my $self = shift;
+ my $mwh = shift;
+ my $conf = $self->{CONF};
+ my $func = $$conf{'INPUTFUNC'} || \&Curses::Widgets::scankey;
+ my $regex = $$conf{'FOCUSSWITCH'};
+ my $key;
+
+ $self->draw($mwh, 1);
+
+ while (1) {
+ $key = &$func($mwh);
+ if (defined $key) {
+ $self->input_key($key);
+ if (defined $regex) {
+ return $key if ($key =~ /^[$regex]/);
+ }
+ }
+ $self->draw($mwh, 1);
+ }
+}
+
+1; # Magic true value required at end of module
+__END__
+
+=head1 NAME
+
+RT::Client::Console - [One line description of module's purpose here]
+
+
+=head1 VERSION
+
+This document describes RT::Client::Console version 0.0.1
+
+
+=head1 SYNOPSIS
+
+ use RT::Client::Console;
+
+=for author to fill in:
+ Brief code example(s) here showing commonest usage(s).
+ This section will be as far as many users bother reading
+ so make it as educational and exeplary as possible.
+
+
+=head1 DESCRIPTION
+
+=for author to fill in:
+ Write a full description of the module and its features here.
+ Use subsections (=head2, =head3) as appropriate.
+
+
+=head1 INTERFACE
+
+=for author to fill in:
+ Write a separate section listing the public components of the modules
+ interface. These normally consist of either subroutines that may be
+ exported, or methods that may be called on objects belonging to the
+ classes provided by the module.
+
+
+=head1 DIAGNOSTICS
+
+=for author to fill in:
+ List every single error and warning message that the module can
+ generate (even the ones that will "never happen"), with a full
+ explanation of each problem, one or more likely causes, and any
+ suggested remedies.
+
+=over
+
+=item C<< Error message here, perhaps with %s placeholders >>
+
+[Description of error here]
+
+=item C<< Another error message here >>
+
+[Description of error here]
+
+[Et cetera, et cetera]
+
+=back
+
+
+=head1 CONFIGURATION AND ENVIRONMENT
+
+=for author to fill in:
+ A full explanation of any configuration system(s) used by the
+ module, including the names and locations of any configuration
+ files, and the meaning of any environment variables or properties
+ that can be set. These descriptions must also include details of any
+ configuration language used.
+
+RT::Client::Console requires no configuration files or environment variables.
+
+
+=head1 DEPENDENCIES
+
+=for author to fill in:
+ A list of all the other modules that this module relies upon,
+ including any restrictions on versions, and an indication whether
+ the module is part of the standard Perl distribution, part of the
+ module's distribution, or must be installed separately. ]
+
+None.
+
+
+=head1 INCOMPATIBILITIES
+
+=for author to fill in:
+ A list of any modules that this module cannot be used in conjunction
+ with. This may be due to name conflicts in the interface, or
+ competition for system or program resources, or due to internal
+ limitations of Perl (for example, many modules that use source code
+ filters are mutually incompatible).
+
+None reported.
+
+
+=head1 BUGS AND LIMITATIONS
+
+=for author to fill in:
+ A list of known problems with the module, together with some
+ indication Whether they are likely to be fixed in an upcoming
+ release. Also a list of restrictions on the features the module
+ does provide: data types that cannot be handled, performance issues
+ and the circumstances in which they may arise, practical
+ limitations on the size of data sets, special cases that are not
+ (yet) handled, etc.
+
+No bugs have been reported.
+
+Please report any bugs or feature requests to
+C<bug-rt-client-console at rt.cpan.org>, or through the web interface at
+L<http://rt.cpan.org>.
+
+
+=head1 AUTHOR
+
+Damien "dams" Krotkine C<< <dams at zarb.org> >>
+
+
+=head1 LICENCE AND COPYRIGHT
+
+Copyright (c) 2007, Damien "dams" Krotkine C<< <dams at zarb.org> >>. All rights reserved.
+
+This module is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself. See L<perlartistic>.
+
+
+=head1 DISCLAIMER OF WARRANTY
+
+BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
+YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+NECESSARY SERVICING, REPAIR, OR CORRECTION.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
+LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
+OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
+THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Cnx.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Cnx.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,87 @@
+package RT::Client::Console::Cnx;
+
+use base qw(RT::Client::Console);
+
+use Params::Validate qw(:all);
+
+sub connect {
+ my ($class, @args) = @_;
+ my %params = validate( @args, { rt_servername => 0,
+ rt_username => 0,
+ rt_password => 0,
+ queue_ids => 0,
+ }
+ );
+
+ $params{queue_ids} ||= [];
+
+ if (!$params{rt_servername}) {
+ $params{rt_servername} = $class->input_ok_cancel('Connexion', 'RT server name');
+ }
+ $params{rt_servername} or return;
+
+ use RT::Client::REST;
+ use Error qw(:try);
+ try {
+ my $rt_handler = RT::Client::REST->new(
+ server => $params{rt_servername},
+ );
+ if (!(defined $params{rt_username} && defined $params{rt_password})) {
+ use Curses::Forms::Dialog::Logon;
+ use Curses::Forms::Dialog::Input;
+ (my $rv, $params{rt_username}, $params{rt_password}) = logon('connect to RT server', BTN_OK | BTN_CANCEL, 50, qw(white red yellow) );
+ }
+ $rt_handler->login(username => $params{rt_username}, password => $params{rt_password});
+ $class->GLOBAL_HEAP->{rt}{cnx}{handler} = $rt_handler;
+ $class->GLOBAL_HEAP->{rt}{cnx}{servername} = $params{rt_servername};
+ $class->GLOBAL_HEAP->{rt}{cnx}{username} = $params{rt_username};
+ $class->GLOBAL_HEAP->{rt}{cnx}{password} = $params{rt_password};
+
+ if (@{$params{queue_ids}}) {
+
+
+ my $idx = 0;
+ my $rt_handler = $class->GLOBAL_HEAP->{rt}{cnx}{handler};
+
+ use RT::Client::Console::Session::Progress;
+ RT::Client::Console::Session::Progress->add_progress(
+ steps_nb => sub { scalar(@{$params{queue_ids}}) },
+ caption => sub { 'generating queues' },
+ initially => sub { },
+ code => sub {
+ my $id = $params{queue_ids}->[$idx++];
+ defined $id or return;
+ use RT::Client::REST::Queue;
+ my $queue;
+ try {
+ $queue = RT::Client::REST::Queue->new( rt => $rt_handler,
+ id => $id, )->retrieve();
+ } catch Exception::Class::Base with { my $dummy = 0; };
+ if (defined $queue) {
+ $class->GLOBAL_HEAP->{server}{id_to_queue}{$id} = $queue;
+ $class->GLOBAL_HEAP->{server}{name_to_queue}{$queue->name()} = $queue;
+ }
+ return 1;
+ },
+ finally => sub { },
+ );
+ }
+ } catch Exception::Class::Base with {
+ $class->error("problem logging in: $@" . shift->message());
+# print STDERR Dumper(shift); use Data::Dumper;
+# print STDERR $@ . "\n";
+ };
+ return;
+}
+
+sub disconnect {
+ my ($class) = @_;
+ undef $class->GLOBAL_HEAP->{rt}{cnx}{handler};
+ undef $class->GLOBAL_HEAP->{rt}{cnx}{servername};
+ undef $class->GLOBAL_HEAP->{rt}{cnx}{username};
+ undef $class->GLOBAL_HEAP->{rt}{cnx}{password};
+ return;
+}
+
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,99 @@
+package RT::Client::Console::Session;
+
+use base qw(RT::Client::Console);
+
+use POE;
+
+sub create {
+ my ($class, $name, %args) = @_;
+ $args{inline_states}{_start} = sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+ $kernel->alias_set($name);
+ $kernel->call($name, 'init');
+ };
+ POE::Session->create(%args);
+ push @{$class->GLOBAL_HEAP->{sessions}}, $name;
+}
+
+sub run {
+ my ($class) = @_;
+ $poe_kernel->run();
+}
+
+
+
+
+
+{
+
+my $modal_index = 0;
+
+sub create_modal {
+ my ($class, %args) = @_;
+
+ my $text = $args{text} . "\n";
+ $args{keys}{c} ||= { text => 'cancel',
+ code => sub { return 1 },
+ };
+
+ while (my ($k, $v) = each %{$args{keys}} ) {
+ $text .= "$k : " . $v->{text} . "\n";
+ }
+ my $height = scalar( () = $text =~ /(\n)/g) + 1;
+ use List::Util qw(max);
+ my $width = max (map { length } (split(/\n/, $text), $args{title}));
+
+ my ($screen_w, $screen_h);
+ my $curses_handler = $class->GLOBAL_HEAP->{curses}{handler};
+ $curses_handler->getmaxyx($screen_h, $screen_w);
+
+ use Curses::Widgets::Label;
+ my $label = Curses::Widgets::Label->new({
+ CAPTION => $args{title},
+ CAPTIONCOL => 'yellow',
+ BORDER => 1,
+ LINES => $height,
+ COLUMNS => $width,
+ Y => $screen_h/2-($height+2)/2,
+ X => $screen_w/2-($width+2)/2,,
+ VALUE => $text,
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ BORDERCOL => 'white',
+ });
+
+
+ my $modal_session_name = 'modal_' . ++$modal_index;
+ POE::Session->create(
+ inline_states => {
+ _start => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ $heap->{label} = $label;
+ $kernel->alias_set($modal_session_name);
+ },
+ key_handler => sub {
+ my ( $kernel, $heap, $keystroke ) = @_[ KERNEL, HEAP, ARG0 ];
+ exists $args{keys}->{$keystroke} or return;
+ if ($args{keys}{$keystroke}{code}->()) {
+ # stop modal mode
+ pop @{$class->GLOBAL_HEAP->{modal_sessions}};
+ $kernel->post('key_handler', 'draw_all');
+ } else {
+ $kernel->yield('draw');
+ }
+ },
+ draw => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ my $curses_handler = $class->GLOBAL_HEAP->{curses}{handler};
+ $heap->{label}->draw($curses_handler);
+ },
+ },
+ );
+ push @{$class->GLOBAL_HEAP->{modal_sessions}}, $modal_session_name;
+
+}
+
+}
+
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/KeyHandler.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/KeyHandler.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,358 @@
+package RT::Client::Console::Session::KeyHandler;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+
+use POE;
+
+use Curses;
+
+# class method
+sub create {
+ my ($class) = @_;
+
+ $class->SUPER::create(
+ 'key_handler',
+ inline_states => {
+ init => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP];
+ print STDERR "key_handler : init\n";
+ $kernel->yield('compute_keys');
+ $kernel->yield('draw_all');
+
+ # Generate events from console input. Sets up Curses, too.
+ # use POE::Wheel::MyCurses;
+ $heap->{console} = POE::Wheel::MyCurses->new(
+ InputEvent => 'handler',
+ );
+ },
+ handler => sub {
+ my ($kernel, $heap, $keystroke) = @_[ KERNEL, HEAP, ARG0];
+ if ($keystroke ne -1) {
+ if ($keystroke lt ' ') {
+ $keystroke = '<' . uc(unctrl($keystroke)) . '>';
+ } elsif ($keystroke =~ /^\d{2,}$/) {
+ $keystroke = '<' . uc(keyname($keystroke)) . '>';
+ }
+ print STDERR "handler got $keystroke\n";
+ if (@{$class->GLOBAL_HEAP->{modal_sessions}}) {
+ print STDERR "modal handler : " . $class->GLOBAL_HEAP->{modal_sessions}->[-1] . "\n";
+ $kernel->call($class->GLOBAL_HEAP->{modal_sessions}->[-1], 'key_handler', $keystroke);
+# $kernel->yield('compute_keys');
+ $kernel->yield('draw_all');
+ } elsif (exists $heap->{key_to_action}->{$keystroke}) {
+ my $action = $heap->{key_to_action}->{$keystroke};
+ print STDERR "action : $action, event : $action->{event}\n";
+ $kernel->call($action->{session}, $action->{event});
+ $kernel->call('key_handler', 'compute_keys');
+ $kernel->call('key_handler', 'draw_all');
+ }
+ }
+ },
+ compute_keys => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ my $status_message = '';
+ $heap->{key_to_action} = {};
+ foreach my $session (@{$class->GLOBAL_HEAP->{sessions}}) {
+ my @list = $kernel->call($session, 'available_keys');
+ foreach (@list) {
+ defined && ref or next;
+ my ($key, $message, $event) = @$_;
+ defined $key or next;
+ $status_message .= " | $key: $message";
+ $heap->{key_to_action}->{$key} = { session => $session, event => $event };
+ }
+ }
+ $kernel->call('status', 'set_message', $status_message);
+ return;
+ },
+ draw_all => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+ noutrefresh();
+ foreach my $session (@{$class->GLOBAL_HEAP->{sessions}}) {
+ $kernel->call($session, 'draw');
+ }
+ foreach my $modal_session (@{$class->GLOBAL_HEAP->{modal_sessions}}) {
+ $kernel->call($modal_session, 'draw');
+ }
+ doupdate();
+ }
+ },
+ heap => {
+ console => undef,
+ },
+ );
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+package POE::Wheel::MyCurses;
+
+use strict;
+
+use vars qw($VERSION);
+$VERSION = do {my($r)=(q$Revision: 2102 $=~/(\d+)/);sprintf"1.%04d",$r};
+
+use Carp qw(croak);
+#use Curses qw(
+# initscr start_color cbreak raw noecho nonl nodelay timeout keypad
+# intrflush meta typeahead mousemask ALL_MOUSE_EVENTS clear refresh
+# endwin COLS
+#);
+
+use Curses;
+
+use POSIX qw(:fcntl_h);
+use POE qw( Wheel );
+
+
+sub SELF_STATE_READ () { 0 }
+sub SELF_STATE_WRITE () { 1 }
+sub SELF_EVENT_INPUT () { 2 }
+sub SELF_ID () { 3 }
+
+sub new {
+ my $type = shift;
+ my %params = @_;
+
+ croak "$type needs a working Kernel" unless defined $poe_kernel;
+
+ my $input_event = delete $params{InputEvent};
+ croak "$type requires an InputEvent parameter" unless defined $input_event;
+
+ if (scalar keys %params) {
+ carp( "unknown parameters in $type constructor call: ",
+ join(', ', keys %params)
+ );
+ }
+
+ # Create the object.
+ my $self = bless
+ [ undef, # SELF_STATE_READ
+ undef, # SELF_STATE_WRITE
+ $input_event, # SELF_EVENT_INPUT
+ &POE::Wheel::allocate_wheel_id(), # SELF_ID
+ ];
+
+ # Set up the screen, and enable color, mangle the terminal and
+ # keyboard.
+
+ #initscr();
+ #start_color();
+
+ #cbreak();
+ #raw();
+ #noecho();
+ #nonl();
+
+ # Both of these achieve nonblocking input.
+ #nodelay(1);
+ #timeout(0);
+
+ keypad(1);
+ intrflush(0);
+ meta(1);
+ typeahead(-1);
+
+ my $old_mouse_events = 0;
+ mousemask(ALL_MOUSE_EVENTS, $old_mouse_events);
+
+# clear();
+# refresh();
+
+ # Define the input event.
+ $self->_define_input_state();
+
+ # Oop! Return ourself. I forgot to do this.
+ $self;
+}
+
+sub _define_input_state {
+ my $self = shift;
+
+ # Register the select-read handler.
+ if (defined $self->[SELF_EVENT_INPUT]) {
+ # Stupid closure tricks.
+ my $event_input = \$self->[SELF_EVENT_INPUT];
+ my $unique_id = $self->[SELF_ID];
+
+ $poe_kernel->state
+ ( $self->[SELF_STATE_READ] = ref($self) . "($unique_id) -> select read",
+ sub {
+
+ # Prevents SEGV in older Perls.
+ 0 && CRIMSON_SCOPE_HACK('<');
+
+ my ($k, $me) = @_[KERNEL, SESSION];
+
+ # Curses' getch() normally blocks, but we've already
+ # determined that STDIN has something for us. Be explicit
+ # about which getch() to use.
+ while ((my $keystroke = Curses::getch) ne '-1') {
+ $k->call( $me, $$event_input, $keystroke, $unique_id );
+ }
+ }
+ );
+
+ # Now start reading from it.
+ $poe_kernel->select_read( \*STDIN, $self->[SELF_STATE_READ] );
+
+ # Turn blocking back on for STDIN. Some Curses implementations
+ # don't deal well with non-blocking STDIN.
+ my $flags = fcntl(STDIN, F_GETFL, 0) or die $!;
+ fcntl(STDIN, F_SETFL, $flags & ~O_NONBLOCK) or die $!;
+ }
+ else {
+ $poe_kernel->select_read( \*STDIN );
+ }
+}
+
+sub DESTROY {
+ my $self = shift;
+
+ # Turn off the select.
+ $poe_kernel->select( \*STDIN );
+
+ # Remove states.
+ if ($self->[SELF_STATE_READ]) {
+ $poe_kernel->state($self->[SELF_STATE_READ]);
+ $self->[SELF_STATE_READ] = undef;
+ }
+
+ # Restore the terminal.
+ endwin if COLS;
+
+ &POE::Wheel::free_wheel_id($self->[SELF_ID]);
+}
+
+###############################################################################
+1;
+
+__END__
+
+=head1 NAME
+
+POE::Wheel::Curses - non-blocking Curses.pm input for full-screen console apps
+
+=head1 SYNOPSIS
+
+ use POE;
+ use Curses; # for unctrl, etc
+ use POE::Wheel::Curses;
+
+ # Generate events from console input. Sets up Curses, too.
+ $heap->{console} = POE::Wheel::Curses->new(
+ InputEvent => 'got_keystroke',
+ );
+
+ # A keystroke handler. This is the body of the program's main input
+ # loop.
+ sub keystroke_handler {
+ my ($keystroke, $wheel_id) = @_[ARG0, ARG1];
+
+ # Control characters. Change them into something printable via
+ # Curses' unctrl function.
+
+ if ($keystroke lt ' ') {
+ $keystroke = '<' . uc(unctrl($keystroke)) . '>';
+ }
+
+ # Extended keys get translated into their names via Curses'
+ # keyname function.
+
+ elsif ($keystroke =~ /^\d{2,}$/) {
+ $keystroke = '<' . uc(keyname($keystroke)) . '>';
+ }
+
+ # Just display it.
+ addstr( $heap->{some_window}, $keystroke );
+ noutrefresh( $heap->{some_window} );
+ doupdate;
+ }
+
+=head1 DESCRIPTION
+
+Many console programs work best with full-screen input: top, systat,
+nethack, and various text editors. POE::Wheel::Curses provides a
+simple way to add full-screen interfaces to POE programs.
+
+Whenever something occurs on a recognized input device-- usually just
+the keyboard, but also sometimes the mouse, as in the case of
+ncurses-- the Curses wheel will emit a predetermined event to tell the
+program about it. This lets the program do other non-blocking things
+in between keystrokes, like interact on sockets or watch log files or
+move monsters or highlight text or something.
+
+=head1 PUBLIC METHODS
+
+=over 2
+
+=item new NOT_SO_MANY_THINGS
+
+new() creates a new Curses wheel. Note, though, that there can be
+only one Curses wheel in any given program, since they glom onto
+*STDIN real hard. Maybe this will change.
+
+new() always returns a Curses wheel reference, even if there is a
+problem glomming onto *STDIN or otherwise initializing curses.
+
+new() accepts only one parameter so far: InputEvent. InputEvent
+contains the name of the event that the Curses wheel will emit
+whenever there is input on the console or terminal.
+
+=back
+
+=head1 EVENTS AND PARAMETERS
+
+=over 2
+
+=item InputEvent
+
+InputEvent defines the event that will be emitted when the Curses
+wheel detects and receives input.
+
+InputEvent is accompanied by two parameters:
+
+C<ARG0> contains the raw keystroke as received by Curses' getch()
+function. It may be passed to Curses' unctrl() and keyname()
+functions for further processing.
+
+C<ARG1> contains the ID of the Curses wheel.
+
+=back
+
+=head1 SEE ALSO
+
+curses, Curses, POE::Wheel.
+
+The SEE ALSO section in L<POE> contains a table of contents covering
+the entire POE distribution.
+
+=head1 BUGS
+
+Curses implementations vary widely, and Wheel::Curses was written on a
+system sporting ncurses. The functions used may not be the same as
+those used on systems with other curses implementations, and Bad
+Things might happen. Please send patches.
+
+=head1 AUTHORS & COPYRIGHTS
+
+Please see L<POE> for more information about authors and contributors.
+
+=cut
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Progress.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Progress.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,106 @@
+package RT::Client::Console::Session::Progress;
+
+use base qw(RT::Client::Console::Session);
+
+use POE;
+
+my @progress_texts = ();
+
+# class method
+sub create {
+ my ($class) = @_;
+
+ $class->SUPER::create(
+ 'progress_draw',
+ inline_states => {
+ draw => sub {
+ my ( $kernel, $heap) = @_[ KERNEL, HEAP ];
+ my $draw_x = 0;
+ my @toremove = ();
+ my ($screen_w, $screen_h);
+ my $curses_handler = RT::Client::Console->GLOBAL_HEAP->{curses}{handler};
+ $curses_handler->getmaxyx($screen_h, $screen_w);
+
+ my $label = Curses::Widgets::Label->new({
+ BORDER => 0,
+ X => 0,
+ Y => $screen_h - 1,
+ COLUMNS => $screen_w,
+ LINES => 1,
+ VALUE => '',
+ FOREGROUND => 'black',
+ BACKGROUND => 'black',
+ });
+ $label->draw($curses_handler);
+
+ foreach my $pos (0.. at progress_texts-1) {
+ my ($text, $erase) = @{$progress_texts[$pos]};
+ length $text or next;
+
+ if ($erase) {
+ push @toremove, $pos;
+ } else {
+ $label = Curses::Widgets::Label->new({
+ BORDER => 0,
+ LINES => 1,
+ COLUMNS => length($text),
+ Y => $screen_h - 1,
+ X => $draw_x,
+ VALUE => $text,
+ FOREGROUND => 'white',
+ BACKGROUND => 'red',
+ });
+ $label->draw($curses_handler);
+ $draw_x += length($text) + 1;
+ }
+ }
+ foreach(@toremove) {
+ splice(@progress_texts, $_, 1);
+ }
+ },
+ },
+ );
+}
+
+sub add_progress {
+ my ($class, %args) = @_;
+
+ my $progress_text = ['', 0];
+ push @progress_texts, $progress_text;
+
+ my $progress_session = POE::Session->create(
+ inline_states => {
+ _start => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ $heap->{value} = 0;
+ $args{initially}->();
+ $kernel->yield('draw');
+ $kernel->yield('code');
+ },
+ code => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ if ($args{code}->()) {
+ $heap->{value}++;
+ $kernel->yield('draw');
+ $kernel->yield('code');
+ } else {
+ $args{finally}->();
+ $kernel->yield('draw', 1);
+ }
+ },
+ draw => sub {
+ my ( $kernel, $heap, $erase ) = @_[ KERNEL, HEAP, ARG0 ];
+
+ my $value = $args{caption}->() . ':' . int($heap->{value}*100/($args{steps_nb}->()||1)) . '%';
+ $erase and $value = ' ' x length $value;
+
+ $progress_text->[0] = $value;
+ $progress_text->[1] = $erase;
+
+ $kernel->post('key_handler', 'draw_all');
+ },
+ },
+ );
+}
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Root.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Root.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,62 @@
+package RT::Client::Console::Session::Root;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+
+use POE;
+
+# class method
+sub create {
+ my ($class) = @_;
+
+ $class->SUPER::create(
+ 'root',
+ inline_states => {
+ init => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+ print STDERR "root : init\n";
+ $class->GLOBAL_HEAP->{curses}{handler}->clear();
+ $kernel->yield('create_status_session');
+ $kernel->yield('create_progress_session');
+ },
+ available_keys => sub {
+ my @available_list = ();
+ if (!$class->GLOBAL_HEAP->{rt}{cnx}{handler}) {
+ push @available_list, ['s', 'connect to RT server', 'connect_server'];
+ }
+ if ($class->GLOBAL_HEAP->{rt}{cnx}{handler}) {
+ push @available_list, ['d', 'disconnect from RT server', 'disconnect_server'];
+ if ($class->GLOBAL_HEAP->{rt}{tickets}{current}) {
+ push @available_list, ['c', 'close current ticket', 'close_ticket'];
+ } else {
+ push @available_list, ['o', 'open a ticket', 'open_ticket'];
+ }
+ }
+ return @available_list;
+ },
+ create_status_session => sub {
+ use RT::Client::Console::Session::Status;
+ RT::Client::Console::Session::Status->create();
+ },
+ create_progress_session => sub {
+ use RT::Client::Console::Session::Progress;
+ RT::Client::Console::Session::Progress->create();
+ },
+ connect_server => sub {
+ use RT::Client::Console::Cnx;
+ RT::Client::Console::Cnx->connect();
+ },
+ disconnect_server => sub {
+ use RT::Client::Console::Cnx;
+ RT::Client::Console::Cnx->disconnect();
+ },
+ open_ticket => sub {
+ use RT::Client::Console::Session::Ticket;
+ RT::Client::Console::Session::Ticket->create();
+ },
+ }
+ );
+}
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Status.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Status.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,61 @@
+package RT::Client::Console::Session::Status;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+
+use POE;
+# class method
+sub create {
+ my ($class) = @_;
+
+ $class->SUPER::create(
+ 'status',
+ inline_states => {
+ init => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ # Get the main screen max y & X
+ my ($screen_w, $screen_h);
+ $class->GLOBAL_HEAP->{curses}{handler}->getmaxyx($screen_h, $screen_w);
+
+ $heap->{'pos_x'} = 0;
+ $heap->{'pos_y'} = $screen_h - 4;
+ $heap->{width} = $screen_w-2;
+ $heap->{height} = 1;
+ },
+ set_message => sub {
+ my ($kernel, $heap, $message) = @_[ KERNEL, HEAP, ARG0 ];
+ $heap->{message} = $message;
+ },
+ draw => sub {
+ my ($kernel,$heap) = @_[ KERNEL, HEAP ];
+ my $label;
+
+ # Render the comment box
+ use Curses::Widgets::Label;
+ $label = Curses::Widgets::Label->new({
+ CAPTION => ' Keys ',
+ BORDER => 1,
+ LINES => $heap->{height},
+ COLUMNS => $heap->{width},
+ Y => $heap->{'pos_y'},
+ X => $heap->{'pos_x'},
+ VALUE => $heap->{message},
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ BORDERCOL => 'black',
+ });
+ #refresh;
+ $label->draw($class->GLOBAL_HEAP->{curses}{handler});
+ },
+ },
+ heap => { 'pos_x' => 0,
+ 'pos_y' => 0,
+ 'width' => 0,
+ 'height' => 0,
+ 'message' => 'default',
+ },
+ );
+}
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,55 @@
+package RT::Client::Console::Session::Ticket;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+
+use RT::Client::REST;
+
+use POE;
+
+sub load_ticket {
+ my ($class, $rt_handler, $id) = @_;
+
+ use RT::Client::REST::Ticket;
+ my $t = RT::Client::REST::Ticket->new(
+ rt => $rt_handler,
+ id => $id,
+ );
+ $t->retrieve();
+ return $t;
+}
+
+# class method
+sub create {
+ my ($class) = @_;
+
+ if (my $id = $class->input_ok_cancel('Open a ticket', 'Ticket number')) {
+
+ use Error qw(:try);
+ try {
+
+ my $ticket = $class->load_ticket($class->GLOBAL_HEAP->{rt}{cnx}{handler}, $id);
+ $class->GLOBAL_HEAP->{rt}{tickets}{current} = $ticket;
+
+ use RT::Client::Console::Session::Ticket::Header;
+ RT::Client::Console::Session::Ticket::Header->create();
+
+ use RT::Client::Console::Session::Ticket::CustFields;
+ RT::Client::Console::Session::Ticket::CustFields->create();
+
+ use RT::Client::Console::Session::Ticket::Links;
+ RT::Client::Console::Session::Ticket::Links->create();
+
+ use RT::Client::Console::Session::Ticket::Attachments;
+ RT::Client::Console::Session::Ticket::Attachments->create();
+
+ } catch Exception::Class::Base with {
+ $class->error("problem opening/retrieving rt $rt_num : " . shift->message());
+ return;
+ };
+
+ }
+}
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Attachments.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Attachments.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,193 @@
+package RT::Client::Console::Session::Ticket::Attachments;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+use Error qw(:try);
+
+use POE;
+
+# class method
+sub create {
+ my ($class) = @_;
+ $class->SUPER::create(
+ 'ticket_attachments',
+ inline_states => {
+ init => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+
+
+ my ($screen_w, $screen_h);
+ $class->GLOBAL_HEAP->{curses}{handler}->getmaxyx($screen_h, $screen_w);
+
+ $heap->{'pos_x'} = 0;
+ $heap->{'pos_y'} = 1+5+5;
+ $heap->{width} = $screen_w * 2 / 3 - 2;
+ $heap->{height} = $screen_h - 5 - 7 - 5;
+
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+
+ },
+ available_keys => sub {
+ return (['<KEY_NPAGE>', 'next attachment', 'next_attachment'],
+ ['<KEY_PPAGE>', 'previous attachment', 'prev_attachment']
+ );
+ },
+ next_attachment => sub {
+ my ( $kernel, $heap) = @_[ KERNEL, HEAP ];
+ $class->GLOBAL_HEAP->{rt}{attachments}{current}++;
+ $class->GLOBAL_HEAP->{rt}{attachments}{current} > $class->GLOBAL_HEAP->{rt}{attachments}{total} - 1
+ and $class->GLOBAL_HEAP->{rt}{attachments}{current} = $class->GLOBAL_HEAP->{rt}{attachments}{total} - 1;
+ $kernel->call('key_handler', 'draw_all');
+ },
+ prev_attachment => sub {
+ my ( $kernel, $heap) = @_[ KERNEL, HEAP ];
+ $class->GLOBAL_HEAP->{rt}{attachments}{current}--;
+ $class->GLOBAL_HEAP->{rt}{attachments}{current} < 0
+ and $class->GLOBAL_HEAP->{rt}{attachments}{current} = 0;
+ $kernel->call('key_handler', 'draw_all');
+ },
+ draw => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+ my $label;
+
+ if (!defined($heap->{attachments})) {
+ $class->_generate_job($kernel, $heap);
+ }
+ defined($heap->{attachments}) or return;
+ my $total = $class->GLOBAL_HEAP->{rt}{attachments}{total};
+ $total > 0 or return;
+ $class->GLOBAL_HEAP->{rt}{attachments}{current} ||= 0;
+
+ my $idx = $class->GLOBAL_HEAP->{rt}{attachments}{current};
+
+ my $attachment = $heap->{attachments}->[$idx];
+
+ my $text = '...loading...';
+ my $user_details = '';
+
+ if (defined $attachment) {
+ try {
+ my $user_id = $attachment->creator_id();
+ use RT::Client::REST::User;
+
+ my ($user, $user_name, $user_email, $user_real_name, $user_gecos, $user_comments)
+ = _get_user_details( rt => $class->GLOBAL_HEAP->{rt}{cnx}{handler},
+ id => $user_id,
+ );
+ $user_details = "By : $user_real_name ($user_name) <$user_email>";
+
+ } catch Exception::Class::Base with {
+ my $e = shift;
+ warn ref($e), ": ", $e->message || $e->description, "\n";
+ };
+
+ $text = $class->as_text($attachment);
+ }
+ my $title = 'Attachment ' . ($idx + 1) . " / $total - $user_details";
+
+ use Curses::Widgets::ListBox;
+#my $widget = Curses::Widgets::ListBox->new({
+# Y => 2,
+# X => 38,
+# COLUMNS => 20,
+# LISTITEMS => ['Ham', 'Eggs', 'Cheese', 'Hash Browns', 'Toast'],
+# MULTISEL => 1,
+# VALUE => [0, 2],
+# SELECTEDCOL => 'green',
+# CAPTION => 'List Box',
+# CAPTIONCOL => 'yellow',
+# });
+
+ use Curses::Widgets::TextMemo;
+ my $widget = Curses::Widgets::TextMemo->new(
+ {
+ X => $heap->{'pos_x'},
+ Y => $heap->{'pos_y'},
+ COLUMNS => $heap->{width},
+ LINES => $heap->{height},
+ MAXLENGTH => undef,
+ FOREGROUND => 'white',
+ BACKGROUND => 'black',
+ VALUE => $text,
+ BORDERCOL => 'blue',
+ BORDER => 1,
+ CAPTION => $title,
+ CAPTIONCOL => 'yellow',
+ READONLY => 1,
+ }
+ );
+# $widget->execute($class->GLOBAL_HEAP->{curses}{handler});
+ $widget->draw($class->GLOBAL_HEAP->{curses}{handler});
+ },
+ },
+ heap => { 'pos_x' => 0,
+ 'pos_y' => 0,
+ 'width' => 0,
+ 'height' => 0,
+ },
+ );
+}
+
+
+use Memoize;
+memoize('_get_user_details');
+
+sub _get_user_details {
+ my (%args) = @_;
+ my $user = RT::Client::REST::User->new( %args )->retrieve;
+ my $user_name = $user->name();
+ my $user_email = $user->email_address();
+ my $user_real_name = $user->real_name();
+ my $user_gecos = $user->gecos();
+ my $user_comments = $user->comments();
+ return ($user, $user_name, $user_email, $user_real_name, $user_gecos, $user_comments);
+}
+
+sub as_text {
+ my ($class, $attachment) = @_;
+ my $s = 'content :(' . $attachment->content_type() . ')' . "\n"
+ . 'subject :{' . $attachment->subject() . '}' . "\n"
+ . 'filename:{' . $attachment->file_name() . '}';
+ if ($attachment->content_type eq 'text/plain') {
+ return $s . "\n\n" . $attachment->content();
+ } elsif ($attachment->content_type eq 'multipart/mixed') {
+ return $s . "\n\n[" . $attachment->content() . "]\n";
+ }
+ else {
+ return $s;
+ }
+}
+
+sub _generate_job {
+ my ($class, $kernel, $heap) = @_;
+ $heap->{attachments} = [];
+
+ my @ids;
+ my $idx = 0;
+ my $rt_handler = $class->GLOBAL_HEAP->{rt}{cnx}{handler};
+ my $iterator;
+ use RT::Client::Console::Session::Progress;
+ RT::Client::Console::Session::Progress->add_progress(
+ steps_nb => sub { $class->GLOBAL_HEAP->{rt}{attachments}{total} },
+ caption => sub { 'attachments' },
+ initially => sub {
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+ my $attachments_obj = $ticket->attachments();
+ my $count = $attachments_obj->count();
+ $class->GLOBAL_HEAP->{rt}{attachments}{total} = $count;
+ $iterator = $attachments_obj->get_iterator();
+ },
+ code => sub {
+ my $attachment = $iterator->();
+ defined $attachment or return;
+ push @{$heap->{attachments}}, $attachment;
+ $idx++ or $kernel->call('key_handler', 'draw_all');
+ return 1;
+ },
+ finally => sub { },
+ );
+}
+
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/CustFields.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/CustFields.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,134 @@
+package RT::Client::Console::Session::Ticket::CustFields;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+
+use POE;
+
+# class method
+sub create {
+ my ($class) = @_;
+ $class->SUPER::create(
+ 'ticket_custfields',
+ inline_states => {
+ init => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+ print STDERR "ticket_custfields : init\n";
+
+ my ($screen_w, $screen_h);
+ $class->GLOBAL_HEAP->{curses}{handler}->getmaxyx($screen_h, $screen_w);
+
+ $heap->{'pos_x'} = 0;
+ $heap->{'pos_y'} = 8;
+ $heap->{width} = $screen_w * 2 / 3;
+ $heap->{height} = 3;
+
+ },
+ available_keys => sub {
+ return (['u', 'change custom fields', 'change_custfields']);
+ },
+ change_custfields => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ $heap->{change_custfields_mode} = 1;
+ my $text = qq(
+
+ c : cancel
+);
+ my $height = scalar( () = $text =~ /(\n)/g) + 1;
+ use List::Util qw(max);
+ my $title = ' Change ticket custom fields ';
+ my $width = max (map { length } (split(/\n/, $text), $title));
+ my ($screen_w, $screen_h);
+ my $curses_handler = $class->GLOBAL_HEAP->{curses}{handler};
+ $curses_handler->getmaxyx($screen_h, $screen_w);
+
+ use Curses::Widgets::Label;
+ my $label = Curses::Widgets::Label->new({
+ CAPTION => $title,
+ CAPTIONCOL => 'yellow',
+ BORDER => 1,
+ LINES => $height,
+ COLUMNS => $width,
+ Y => $screen_h/2-($height+2)/2,
+ X => $screen_w/2-($width+2)/2,,
+ VALUE => $text,
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ BORDERCOL => 'white',
+ });
+ $label->draw($curses_handler);
+ $class->GLOBAL_HEAP->{modal_session} = 'ticket_custfields';
+ },
+ modal_handler => sub {
+ my ( $kernel, $heap, $keystroke) = @_[ KERNEL, HEAP, ARG0 ];
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+ if ($keystroke eq 'c' || $keystroke eq '<^[>') {
+ delete $class->GLOBAL_HEAP->{modal_session};
+ } else {
+ $kernel->yield('change_custfields');
+ }
+ return;
+ },
+ draw => sub {
+ my ( $kernel, $heap) = @_[ KERNEL, HEAP ];
+ my $label;
+
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+ my @custom_fields = sort $ticket->cf();
+ use POSIX qw(floor);
+ my $per_col = POSIX::floor(@custom_fields/3);
+ my @custom_fields_labels;
+
+ # first 2 column
+ foreach (1..2) {
+ push @custom_fields_labels,
+ [
+ map {
+ [ "$_:", (defined $ticket->cf($_) ? $ticket->cf($_) : '') ],
+ } splice @custom_fields, 0, $per_col
+ ];
+
+ }
+ # third column
+ push @custom_fields_labels,
+ [
+ map {
+ [ "$_:", $ticket->cf($_) ],
+ } @custom_fields
+ ];
+
+ my %custom_fields_widgets = $class->struct_to_widgets(\@custom_fields_labels, $heap->{height}, $heap->{width});
+
+ use Curses::Forms;
+ my $form = Curses::Forms->new({
+ X => $heap->{'pos_x'},
+ Y => $heap->{'pos_y'},
+ COLUMNS => $heap->{width},
+ LINES => $heap->{height},
+
+ BORDER => 0,
+ BORDERCOL => 'blue',
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ DERIVED => 1,
+ # AUTOCENTER => 1,
+ TABORDER => [],
+ FOCUSED => 'label1',
+ WIDGETS => \%custom_fields_widgets,
+ },
+ );
+ $form->draw($class->GLOBAL_HEAP->{curses}{handler});
+ # refresh($mwh);
+ },
+ },
+ heap => { 'pos_x' => 0,
+ 'pos_y' => 0,
+ 'width' => 0,
+ 'height' => 0,
+ change_custfields_mode => 0,
+ },
+ );
+}
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Header.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Header.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,159 @@
+package RT::Client::Console::Session::Ticket::Header;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+
+use POE;
+
+# class method
+sub create {
+ my ($class) = @_;
+
+ $class->SUPER::create(
+ 'ticket_header',
+ inline_states => {
+ init => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+
+ my ($screen_w, $screen_h);
+ $class->GLOBAL_HEAP->{curses}{handler}->getmaxyx($screen_h, $screen_w);
+
+ $heap->{'pos_x'} = 0;
+ $heap->{'pos_y'} = 1;
+ $heap->{width} = $screen_w * 2 / 3 - 2;
+ $heap->{height} = 5;
+
+ },
+ available_keys => sub {
+ return (['h', 'change ticket header', 'change_header']);
+ },
+ change_header => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+ $class->create_modal( title => ' Change ticket headers ',
+ text => '',
+ keys => {
+ s => { text => 'change subject',
+ code => sub {
+ if (my $new_subject = $class->input_ok_cancel(' Change subject ', $ticket->subject(), 500)) {
+ $ticket->subject($new_subject);
+ return 1; # stop modal mode
+ }
+ }
+ },
+ t => { text => 'change status',
+ code => sub {
+ if (my $new_status = $class->input_list(title => ' Change status ',
+ items => [ qw(new open resolved stalled rejected deleted) ],
+ value => $ticket->status(),
+ )) {
+ $ticket->status($new_status);
+ return 1; # stop modal mode
+ }
+ }
+ },
+ q => { text => 'change queue',
+ code => sub {
+
+ my $queues = $class->GLOBAL_HEAP->{server}{id_to_queue};
+
+ my @queues_list_items;
+ while (my ($id, $queue) = each %$queues) {
+ push @queues_list_items, { text => $queue->name() . ' - ' . $queue->description(),
+ value => $id,
+ };
+ };
+ @queues_list_items = sort { $a->{text} cmp $b->{text} } @queues_list_items;
+
+ if (my $new_queue_id = $class->input_list(title => ' Change queue ',
+ items => [ @queues_list_items ],
+ value => $class->GLOBAL_HEAP->{server}{name_to_queue}{$ticket->queue()}->id(),
+ )) {
+
+ my $new_queue_name = $class->GLOBAL_HEAP->{server}{id_to_queue}{$new_queue_id}->name();
+ $ticket->queue($new_queue_name);
+ return 1; # stop modal mode
+ }
+ }
+ },
+ p => { text => 'change priority',
+ code => sub {
+ if (my $new_priority = $class->input_ok_cancel(' Change priority ', $ticket->priority(), 20)) {
+ $ticket->priority($new_priority);
+ return 1; # stop modal mode
+ }
+ }
+ },
+ o => { text => 'change owner',
+ code => sub {},
+ },
+ },
+ );
+ },
+ draw => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+ my $label;
+
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+ my @requestors = $ticket->requestors();
+ my @requestor_text_list = map {
+ [ 'Requestor ' . $_ . ':' => $requestors[$_-1] ]
+ } (1.. at requestors);
+
+ my @header_labels = (
+ # first column
+ [ [ 'Id:' => $ticket->id() ],
+ [ 'Status:' => $ticket->status() ],
+ [ 'Queue:' => $ticket->queue() ],
+ [ 'Priority:' => $ticket->priority() ],
+ ],
+
+ # second column
+ [ [ 'Owner:' => $ticket->owner() ],
+ @requestor_text_list,
+ [ 'Cc:' => $ticket->cc() ],
+ ],
+
+ # third column
+ [ [ 'Created:' => $ticket->created() ],
+ [ 'Updated:' => $ticket->last_updated() ],
+ ],
+
+ );
+
+ my %label_widgets = $class->struct_to_widgets(\@header_labels, $heap->{height}-2, $heap->{width}-2);
+
+ use Curses::Forms;
+ my $form = Curses::Forms->new({
+ X => $heap->{'pos_x'},
+ Y => $heap->{'pos_y'},
+ COLUMNS => $heap->{width},
+ LINES => $heap->{height},
+
+ BORDER => 1,
+ BORDERCOL => 'yellow',
+ CAPTION => $ticket->subject(),
+ CAPTIONCOL => 'yellow',
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ DERIVED => 1,
+ # AUTOCENTER => 1,
+ TABORDER => [],
+ FOCUSED => 'label1',
+ WIDGETS => \%label_widgets,
+ },
+ );
+ $form->draw($class->GLOBAL_HEAP->{curses}{handler});
+ # refresh($mwh);
+ },
+ },
+ heap => { 'pos_x' => 0,
+ 'pos_y' => 0,
+ 'width' => 0,
+ 'height' => 0,
+ },
+ );
+}
+
+1;
Added: RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Links.pm
==============================================================================
--- (empty file)
+++ RT-Client-Console/trunk/lib/RT/Client/Console/Session/Ticket/Links.pm Mon Nov 5 09:11:08 2007
@@ -0,0 +1,211 @@
+package RT::Client::Console::Session::Ticket::Links;
+
+use base qw(RT::Client::Console::Session);
+
+use Params::Validate qw(:all);
+
+use POE;
+
+# class method
+sub create {
+ my ($class) = @_;
+ $class->SUPER::create(
+ 'ticket_links',
+ inline_states => {
+ init => sub {
+ my ($kernel, $heap) = @_[ KERNEL, HEAP ];
+
+ my ($screen_w, $screen_h);
+ $class->GLOBAL_HEAP->{curses}{handler}->getmaxyx($screen_h, $screen_w);
+
+ $heap->{'pos_x'} = $screen_w * 2 / 3 + 1;
+ $heap->{'pos_y'} = 1;
+ $heap->{width} = $screen_w - ($screen_w * 2 / 3);
+ $heap->{height} = $screen_h - 5 - 1;
+ },
+ available_keys => sub {
+ return (['l', 'change links', 'change_links']);
+ },
+ change_links => sub {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+ $heap->{change_links_mode} = 1;
+ my $text = qq(
+
+ c : cancel
+);
+ my $height = scalar( () = $text =~ /(\n)/g) + 1;
+ use List::Util qw(max);
+ my $title = ' Change ticket links ';
+ my $width = max (map { length } (split(/\n/, $text), $title));
+ my ($screen_w, $screen_h);
+ my $curses_handler = $class->GLOBAL_HEAP->{curses}{handler};
+ $curses_handler->getmaxyx($screen_h, $screen_w);
+
+ use Curses::Widgets::Label;
+ my $label = Curses::Widgets::Label->new({
+ CAPTION => $title,
+ CAPTIONCOL => 'yellow',
+ BORDER => 1,
+ LINES => $height,
+ COLUMNS => $width,
+ Y => $screen_h/2-($height+2)/2,
+ X => $screen_w/2-($width+2)/2,,
+ VALUE => $text,
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ BORDERCOL => 'white',
+ });
+ $label->draw($curses_handler);
+ $class->GLOBAL_HEAP->{modal_session} = 'ticket_links';
+ },
+ modal_handler => sub {
+ my ( $kernel, $heap, $keystroke) = @_[ KERNEL, HEAP, ARG0 ];
+
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+ if ($keystroke eq 'c' || $keystroke eq '<^[>') {
+ delete $class->GLOBAL_HEAP->{modal_session};
+ } else {
+ $kernel->yield('change_links');
+ }
+ return;
+ },
+ draw => sub {
+ my ( $kernel, $heap) = @_[ KERNEL, HEAP ];
+ my $label;
+
+ my $ticket = $class->GLOBAL_HEAP->{rt}{tickets}{current};
+
+ if (!defined($heap->{parents})) {
+ $class->_generate_job($kernel, $heap, 'parents', q(HasMember=') . $ticket->id() . q('))
+ }
+ if (!defined($heap->{children})) {
+ $class->_generate_job($kernel, $heap, 'children', q(MemberOf=') . $ticket->id() . q('))
+ }
+ if (!defined($heap->{depends})) {
+ $class->_generate_job($kernel, $heap, 'depends', q(DependedOnBy=') . $ticket->id() . q('))
+ }
+ if (!defined($heap->{depended})) {
+ $class->_generate_job($kernel, $heap, 'depended', q(DependsOn=') . $ticket->id() . q('))
+ }
+ if (!defined($heap->{refers})) {
+ $class->_generate_job($kernel, $heap, 'refers', q(ReferredToBy=') . $ticket->id() . q('))
+ }
+ if (!defined($heap->{refered})) {
+ $class->_generate_job($kernel, $heap, 'refered', q(RefersTo=') . $ticket->id() . q('))
+ }
+
+
+ my $_ticket_to_label = sub {
+ my ($t) = @_;
+ defined $t && ref($t) or return '';
+ return $t->id() . ' ' . $t->subject()
+ };
+
+
+ my @d = @{$heap->{depends}};
+ my @depends_on = (
+ [ 'Depends on:' => $_ticket_to_label->($d[0]) ],
+ map { [ '' => $_ticket_to_label->($_) ] } @d[1..$#d]
+ );
+ my @d2 = @{$heap->{depended}};
+ my @depended_on_by = (
+ [ 'Depended on by:' => $_ticket_to_label->($d2[0]) ],
+ map { [ '' => $_ticket_to_label->($_) ] } @d2[1..$#d2]
+ );
+ my @p = @{$heap->{parents}};
+ my @parents = (
+ [ 'Parents:' => $_ticket_to_label->($p[0]) ],
+ map { [ '' => $_ticket_to_label->($_) ] } @p[1..$#p]
+ );
+ my @c = @{$heap->{children}};
+ my @children = (
+ [ 'Children:' => $_ticket_to_label->($c[0]) ],
+ map { [ '' => $_ticket_to_label->($_) ] } @c[1..$#c]
+ );
+ my @r = @{$heap->{refers}};
+ my @refers_to = (
+ [ 'Refers to:' => $_ticket_to_label->($r[0]) ],
+ map { [ '' => $_ticket_to_label->($_) ] } @r[1..$#r]
+ );
+ my @r2 = @{$heap->{refered}};
+ my @referred_to_by = (
+ [ 'Refered to by:' => $_ticket_to_label->($r2[0]) ],
+ map { [ '' => $_ticket_to_label->($_) ] } @r2[1..$#r2]
+ );
+
+
+ my @links_labels = (
+ # first column
+ [ @depends_on,
+ @depended_on_by,
+ @parents,
+ @children,
+ @refers_to,
+ @referred_to_by,
+ ],
+ );
+
+
+ my %links_widgets = $class->struct_to_widgets(\@links_labels, $heap->{height}-2, $heap->{width}-2);
+
+ use Curses::Forms;
+ my $form = Curses::Forms->new({
+ X => $heap->{'pos_x'},
+ Y => $heap->{'pos_y'},
+ COLUMNS => $heap->{width},
+ LINES => $heap->{height},
+
+ BORDER => 0,
+ BORDERCOL => 'blue',
+ FOREGROUND => 'white',
+ BACKGROUND => 'blue',
+ DERIVED => 1,
+ # AUTOCENTER => 1,
+ TABORDER => [],
+ FOCUSED => 'label1',
+ WIDGETS => \%links_widgets,
+ },
+ );
+ use Data::Dumper;
+ $form->draw($class->GLOBAL_HEAP->{curses}{handler});
+ # refresh($mwh);
+ },
+ },
+ heap => { 'pos_x' => 0,
+ 'pos_y' => 0,
+ 'width' => 0,
+ 'height' => 0,
+ change_custfields_mode => 0,
+ },
+ );
+}
+
+sub _generate_job {
+ my ($class, $kernel, $heap, $element, $query) = @_;
+ $heap->{$element} = [];
+ my @ids;
+ my $idx = 0;
+ my $rt_handler = $class->GLOBAL_HEAP->{rt}{cnx}{handler};
+ use RT::Client::Console::Session::Progress;
+ RT::Client::Console::Session::Progress->add_progress(
+ steps_nb => sub { scalar(@ids) },
+ caption => sub { $element },
+ initially => sub {
+ @ids = $rt_handler->search( type => 'ticket',
+ query => $query,
+ );
+ },
+ code => sub { my $id = $ids[$idx++];
+ defined $id or return;
+ use RT::Client::Console::Session::Ticket;
+ push @{$heap->{$element}},
+ RT::Client::Console::Session::Ticket->load_ticket($rt_handler, $id);
+ # $kernel->post('ticket_links', 'draw');
+ return 1;
+ },
+ finally => sub {
+ $kernel->post('ticket_links', 'draw'),
+ },
+ );
+}
+1;
More information about the Bps-public-commit
mailing list