use locale;
use File::Find;
use Storable;
+use Time::HiRes qw(time);
our $loaded;
our $filters;
die "no data dir $path" unless -d $path;
my @files;
- my $changes;
+ my $edits;
find( sub {
my $file = $File::Find::name;
if ( -f $file && $file =~ m/\.(js(on)?|txt)$/ ) {
$file =~ s/$path\/*//;
push @files, $file;
- } elsif ( -f $file && $file =~ m/([^\/]+)\.changes\/(\d+\.\d+.+)/ ) {
- push @{ $changes->{$1} }, $2
+ } elsif ( -f $file && $file =~ m/([^\/]+)\.edits\/(\d+\.\d+.+)/ ) {
+ push @{ $edits->{$1} }, $2
} else {
warn "IGNORE: $file\n";
}
loaded => $loaded,
filters => $filters,
dump_path => { map { $_ => $self->_dump_path($_) } @files },
- changes => $changes,
+ edits => $edits,
);
}
my ( $self, $path ) = @_;
my $dump_path = $self->_dump_path( $path );
+ my $first_load = ! -e $dump_path;
warn "save loaded to $dump_path";
my $info = $loaded->{$path};
store $info, $dump_path;
- # sync timestamp
- #my $mtime = $loaded->{$path}->{mtime};
- #utime $mtime, $mtime, $dump_path;
+ if ( $first_load ) {
+ my $mtime = $loaded->{$path}->{mtime};
+ utime $mtime, $mtime, $dump_path;
+ warn "sync time to $path at $mtime\n";
+ }
warn $dump_path, ' ', -s $dump_path, " bytes\n";
return $dump_path;
}
+
+sub __stats {
+
+ my $stats;
+
+ my $nr_items = $#{ $_[0] } + 1;
+
+ warn "__stats $nr_items\n";
+
+ foreach my $e ( @{ $_[0] } ) {
+ foreach my $n ( keys %$e ) {
+ $stats->{$n}->{count}++;
+ my @v;
+ if ( ref $e->{$n} eq 'ARRAY' ) {
+ $stats->{$n}->{array} += $#{ $e->{$n} } + 1;
+ @v = @{ $e->{$n} };
+ } else {
+ @v = ( $e->{$n} );
+ }
+
+ foreach my $x ( @v ) {
+ $stats->{$n}->{numeric}++
+ if $x =~ m/^[-+]?([0-9]*\.[0-9]+|[0-9]+)$/;
+ $stats->{$n}->{empty}++
+ if length $x == 0; # faster than $x =~ m/^\s*$/;
+ }
+
+ }
+ }
+
+ foreach my $n ( keys %$stats ) {
+ my $s = $stats->{$n};
+ next unless defined $s->{array};
+ if ( $s->{array} == $s->{count} ) {
+ delete $s->{array};
+ if ( $s->{count} == $nr_items ) {
+ warn "check $n for uniqeness\n";
+ my $unique;
+ foreach my $e ( @{ $_[0] } ) {
+ if ( ++$unique->{ $e->{$n}->[0] } == 2 ) {
+ $unique = 0;
+ last;
+ }
+ }
+ if ( $unique ) {
+ $stats->{$n}->{unique} = 1;
+ warn "# $n unique ",dump( $unique );
+ }
+ }
+ }
+ }
+
+ warn "# __stats ",dump($stats);
+
+ return $stats;
+}
+
+sub _param_or_session {
+ $_[0]->param( $_[1] ) || $_[0]->session( $_[1] )
+}
+
+sub stats {
+ my $self = shift;
+ my $path = $self->_param_or_session('path');
+ warn "stats $path\n";
+ delete $loaded->{$path}->{stats};
+ $self->redirect_to( '/data/columns' );
+}
+
+
sub _load_path {
my ( $self, $path ) = @_;
warn "file format unknown $path";
}
- my $stats;
-
- foreach my $e ( @{ $data->{items} } ) {
- foreach my $n ( keys %$e ) {
- $stats->{$n}->{count}++;
- my @v;
- if ( ref $e->{$n} eq 'ARRAY' ) {
- $stats->{$n}->{array} += $#{ $e->{$n} } + 1;
- @v = @{ $e->{$n} };
- } else {
- @v = ( $e->{$n} );
- }
-
- foreach my $x ( @v ) {
- $stats->{$n}->{numeric}++
- if $x =~ m/^[-+]?([0-9]*\.[0-9]+|[0-9]+)$/;
- $stats->{$n}->{empty}++
- if length $x == 0; # faster than $x =~ m/^\s*$/;
- }
-
- }
- }
-
- foreach my $n ( keys %$stats ) {
- next unless defined $stats->{$n}->{array};
- delete $stats->{$n}->{array}
- if $stats->{$n}->{array} == $stats->{$n}->{count};
- }
+ my $stats = __stats( $data->{items} );
if ( ! @header ) {
if ( defined $data->{header} ) {
grep { defined $stats->{$_}->{count} } keys %$stats
unless @header;
- warn dump($stats);
-
my $info = {
header => [ @header ],
stats => $stats,
$self->session('path' => $path);
$self->_load_path( $path );
+ my $redirect_to = '/data/items';
+
$self->session( 'header' => $loaded->{$path}->{header} );
if ( ! defined $loaded->{$path}->{columns} ) {
- $self->session( 'columns' => $loaded->{$path}->{header} );
- $self->session( 'order' => $loaded->{$path}->{header}->[0] );
- $self->redirect_to( '/data/columns' );
- } else {
- $self->session( 'columns' => $loaded->{$path}->{columns} );
- $self->session( 'order' => $loaded->{$path}->{columns}->[0] );
- $self->redirect_to( '/data/items' );
+ my $columns_path = $self->_permanent_path( 'columns' );
+ if ( -e $columns_path ) {
+ my @columns = map { s/[\r\n]+$//; $_ } read_file $columns_path;
+ $loaded->{$path}->{columns} = [ @columns ];
+ warn "# columns_path $columns_path ",dump(@columns);
+ } else {
+ $loaded->{$path}->{columns} = $loaded->{$path}->{header}
+ }
+
+ $redirect_to = '/data/columns';
}
+ $self->session( 'columns' => $loaded->{$path}->{columns} );
+ $self->session( 'order' => $loaded->{$path}->{columns}->[0] );
+ $self->redirect_to( $redirect_to );
}
sub _loaded {
my ( $self, $name ) = @_;
- my $path = $self->session('path');
+ my $path = $self->session('path') || $self->param('path');
$self->redirect_to('/data/index') unless $path;
if ( ! defined $loaded->{$path}->{$name} ) {
warn "$path $name isn't loaded\n";
$self->_load_path( $path );
$self->redirect_to('/data/index')
unless defined $loaded->{$path}->{$name};
+ if ( ! defined $loaded->{$path}->{stats} ) {
+ warn "rebuild stats for $path\n";
+ $loaded->{$path}->{stats} = __stats( $loaded->{$path}->{data}->{items} );
+ }
}
return $loaded->{$path}->{$name};
}
return $checked;
}
+sub _permanent_path {
+ my $self = shift;
+ my $path = $self->_param_or_session('path');
+ $self->app->home->rel_dir('data') . '/' . join('.', $path, @_);
+}
+
+sub _export_path {
+ my $self = shift;
+ my $path = $self->_param_or_session('path');
+ my $dir = $self->app->home->rel_dir('public') . '/export/';
+ mkdir $dir unless -e $dir;
+ $dir .= $path;
+ mkdir $dir unless -e $dir;
+ $dir . '/' . join('.', @_);
+}
sub columns {
my $self = shift;
if ( $self->param('columns') ) {
- $self->_perm_array('columns');
+ my @columns = $self->_param_array('columns');
+ write_file( $self->_permanent_path( 'columns' ), map { "$_\n" } @columns );
$self->redirect_to('/data/items');
}
- my $stats = $self->_loaded( 'stats' ); # || $self->redirect_to( '/data/index' );
+ my $stats = $self->_loaded( 'stats' );
my @columns;
@columns = grep { defined $stats->{$_}->{count} } @{ $self->session('columns') } if $self->session('columns');
message => 'Select columns to display',
stats => $stats,
columns => \@columns,
- checked => $self->_checked( $self->_perm_array('columns') ),
+ checked => $self->_checked( $self->_param_array('columns') ),
);
}
-sub _perm_array {
+sub _param_array {
my ($self,$name) = @_;
my @array = $self->param($name);
return @array;
}
-sub _perm_scalar {
+sub _param_scalar {
my ($self,$name,$default) = @_;
my $scalar = $self->param($name);
my @vals = $self->param('filter_vals');
$self->_remove_filter( $name );
- $self->_filter_on_data( $name, @vals ) if @vals;
+ if ( @vals ) {
+ $self->_filter_on_data( $name, @vals );
+ if ( my $permanent = $self->param('_permanent') ) {
+ my $permanent_path = $self->_export_path( 'filter', $name, $permanent );
+ write_file $permanent_path, map { "$_\n" } @vals;
+ warn "permanent filter $permanent_path ", -s $permanent_path;
+ }
+ }
$self->session( 'offset' => 0 );
my $path = $self->session('path');
$self->redirect_to('/data/index') unless defined $loaded->{ $path };
- my @columns = $self->_perm_array('columns');
+ my @columns = $self->_param_array('columns');
$self->redirect_to('/data/columns') unless @columns;
- my $order = $self->_perm_scalar('order', $columns[0]);
- my $sort = $self->_perm_scalar('sort', 'a');
- my $offset = $self->_perm_scalar('offset', 0);
- my $limit = $self->_perm_scalar('limit', 20);
- $self->_perm_scalar('show', 'table');
+ my $order = $self->_param_scalar('order', $columns[0]);
+ my $sort = $self->_param_scalar('sort', 'a');
+ my $offset = $self->_param_scalar('offset', 0);
+ my $limit = $self->_param_scalar('limit', 20);
+ $self->_param_scalar('show', 'table');
# fix offset when changing limit
$offset = int( $offset / $limit ) * $limit;
my $facet;
my $name = $self->param('name') || die "no name";
- my $all = $self->_perm_scalar('all', 1);
+ my $all = $self->_param_scalar('all', 1);
my $data = $self->_loaded('data');
my $filters = $self->_current_filters;
if ( $old ne $new
&& ! ( $old eq 'undef' && length($new_content) == 0 ) # new value empty, previous undef
) {
- warn "# update $path $i $old -> $new\n";
+ my $edit = {
+ path => $path,
+ column => $name,
+ pos => $i,
+ old => $loaded->{$path}->{data}->{items}->[$i]->{$name},
+ new => $v,
+ time => $self->param('time') || time(),
+ user => $self->param('user') || $ENV{'LOGNAME'},
+ unique => {
+ map { $_ => $loaded->{$path}->{data}->{items}->[$i]->{$_}->[0] }
+ grep { defined $loaded->{$path}->{stats}->{$_}->{unique} }
+ keys %{ $loaded->{$path}->{stats} }
+ },
+ };
+ my $edit_path = $self->_permanent_path( 'edits' );
+ mkdir $edit_path unless -d $edit_path;
+ $edit_path .= '/' . $edit->{time};
+ store $edit, $edit_path;
+ utime $edit->{time}, $edit->{time}, $edit_path;
+ warn "# $edit_path ", dump($edit);
+
+ warn "# edit $path $i $old -> $new\n";
$loaded->{$path}->{data}->{items}->[$i]->{$name} = $v;
if ( defined $loaded->{$path}->{sorted}->{$name} ) {
sub save {
my $self = shift;
- my $path = $self->param('path');
- $path ||= $self->session('path');
-
+ my $path = $self->_param_or_session('path');
my $dump_path = $self->_save( $path );
$self->session('save_path' => 0);
$self->redirect_to( '/data/items' );
}
+sub export {
+ my $self = shift;
+ $self->render( export => [
+ map { s{^.+/public/export/}{}; $_ }
+ glob( $self->_export_path . '*' )
+ ] );
+}
+
+sub __loaded_paths {
+ return
+ grep { defined $loaded->{$_}->{data} }
+ keys %$loaded;
+}
+
1;