Further work on import functions
This commit is contained in:
parent
593efcedfa
commit
9096bef00d
9 changed files with 382 additions and 0 deletions
2
cpanfile
2
cpanfile
|
@ -22,6 +22,8 @@ requires 'Module::Runtime';
|
||||||
requires 'DBIx::Class::DeploymentHandler';
|
requires 'DBIx::Class::DeploymentHandler';
|
||||||
requires 'DBIx::Class::Fixtures';
|
requires 'DBIx::Class::Fixtures';
|
||||||
requires 'GIS::Distance';
|
requires 'GIS::Distance';
|
||||||
|
requires 'Text::CSV';
|
||||||
|
requires 'Try::Tiny';
|
||||||
|
|
||||||
feature 'schema-graph', 'Draw diagrams of Schema' => sub {
|
feature 'schema-graph', 'Draw diagrams of Schema' => sub {
|
||||||
requires 'GraphViz';
|
requires 'GraphViz';
|
||||||
|
|
|
@ -200,6 +200,12 @@ sub startup {
|
||||||
|
|
||||||
$admin_routes->get('/reports/transactions')->to('admin-reports#transaction_data');
|
$admin_routes->get('/reports/transactions')->to('admin-reports#transaction_data');
|
||||||
|
|
||||||
|
$admin_routes->get('/import')->to('admin-import#index');
|
||||||
|
$admin_routes->get('/import/add')->to('admin-import#get_add');
|
||||||
|
$admin_routes->post('/import/add')->to('admin-import#post_add');
|
||||||
|
$admin_routes->get('/import/:set_id')->to('admin-import#list');
|
||||||
|
$admin_routes->get('/import/:set_id/:value_id')->to('admin-import#get_value');
|
||||||
|
$admin_routes->post('/import/:set_id/:value_id')->to('admin-import#post_value');
|
||||||
# my $user_routes = $r->under('/')->to('root#under');
|
# my $user_routes = $r->under('/')->to('root#under');
|
||||||
|
|
||||||
# $user_routes->get('/home')->to('root#home');
|
# $user_routes->get('/home')->to('root#home');
|
||||||
|
|
|
@ -1,9 +1,147 @@
|
||||||
package Pear::LocalLoop::Controller::Admin::Import;
|
package Pear::LocalLoop::Controller::Admin::Import;
|
||||||
use Mojo::Base 'Mojolicious::Controller';
|
use Mojo::Base 'Mojolicious::Controller';
|
||||||
|
|
||||||
|
use Text::CSV;
|
||||||
|
use Try::Tiny;
|
||||||
|
|
||||||
|
has result_set => sub {
|
||||||
|
my $c = shift;
|
||||||
|
return $c->schema->resultset('ImportSet');
|
||||||
|
};
|
||||||
|
|
||||||
sub index {
|
sub index {
|
||||||
my $c = shift;
|
my $c = shift;
|
||||||
|
|
||||||
|
my $import_rs = $c->result_set->search(
|
||||||
|
undef,
|
||||||
|
{
|
||||||
|
page => $c->param('page') || 1,
|
||||||
|
rows => 10,
|
||||||
|
order_by => { -desc => 'date' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$c->stash( import_rs => $import_rs );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub list {
|
||||||
|
my $c = shift;
|
||||||
|
my $set_id = $c->param('set_id');
|
||||||
|
|
||||||
|
my $import_set = $c->result_set->find($set_id);
|
||||||
|
my $import_value_rs = $c->result_set->get_values($set_id);
|
||||||
|
my $import_users_rs = $c->result_set->get_users($set_id);
|
||||||
|
my $import_org_rs = $c->result_set->get_orgs($set_id);
|
||||||
|
|
||||||
|
$c->stash(
|
||||||
|
import_set => $import_set,
|
||||||
|
import_value_rs => $import_value_rs,
|
||||||
|
import_users_rs => $import_users_rs,
|
||||||
|
import_org_rs => $import_org_rs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_add {
|
||||||
|
my $c = shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub post_add {
|
||||||
|
my $c = shift;
|
||||||
|
|
||||||
|
my $csv_data = $c->param('csv');
|
||||||
|
my $date_format = $c->param('date_format');
|
||||||
|
|
||||||
|
my $csv = Text::CSV->new({
|
||||||
|
binary => 1,
|
||||||
|
allow_whitespace => 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
open my $fh, '<', \$csv_data;
|
||||||
|
|
||||||
|
# List context returns the actual headers
|
||||||
|
my @csv_headers;
|
||||||
|
my $error;
|
||||||
|
try {
|
||||||
|
@csv_headers = $csv->header( $fh );
|
||||||
|
} catch {
|
||||||
|
$error = $_;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( defined $error ) {
|
||||||
|
$c->flash( error => $error, csv_data => $csv_data, date_format => $date_format );
|
||||||
|
$c->redirect_to( '/admin/import/add' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Text::CSV Already errors on duplicate columns, so this is fine
|
||||||
|
my @required = grep {/^user$|^value$|^date$|^organisation$/} @csv_headers;
|
||||||
|
|
||||||
|
unless ( scalar( @required ) == 4 ) {
|
||||||
|
$c->flash( error => 'Required columns not available', csv_data => $csv_data, date_format => $date_format );
|
||||||
|
$c->redirect_to( '/admin/import/add' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $csv_output = $csv->getline_hr_all( $fh );
|
||||||
|
|
||||||
|
unless ( scalar( @$csv_output ) ) {
|
||||||
|
$c->flash( error => "No data found", csv_data => $csv_data, date_format => $date_format );
|
||||||
|
$c->redirect_to( '/admin/import/add' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for my $data ( @$csv_output ) {
|
||||||
|
Dwarn $data;
|
||||||
|
for my $key ( qw/ user value organisation / ) {
|
||||||
|
unless ( defined $data->{$key} ) {
|
||||||
|
$c->flash( error => "Undefined [$key] data found", csv_data => $csv_data, date_format => $date_format );
|
||||||
|
$c->redirect_to( '/admin/import/add' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( defined $data->{date} ) {
|
||||||
|
my $dtp = DateTime::Format::Strptime->new( pattern => $date_format );
|
||||||
|
my $dt_obj = $dtp->parse_datetime($data->{date});
|
||||||
|
unless ( defined $dt_obj ) {
|
||||||
|
$c->flash( error => "Undefined or incorrect format for [date] data found", csv_data => $csv_data, date_format => $date_format );
|
||||||
|
$c->redirect_to( '/admin/import/add' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$data->{date} = $dt_obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $value_set;
|
||||||
|
$c->schema->txn_do(
|
||||||
|
sub {
|
||||||
|
$value_set = $c->result_set->create({});
|
||||||
|
|
||||||
|
$value_set->values->populate(
|
||||||
|
[
|
||||||
|
[ qw/ user_name purchase_value purchase_date org_name / ],
|
||||||
|
( map { [ @{$_}{qw/ user value date organisation /} ] } @$csv_output ),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
unless ( defined $value_set ) {
|
||||||
|
$c->flash( error => 'Error creating new Value Set', csv_data => $csv_data, date_format => $date_format );
|
||||||
|
$c->redirect_to( '/admin/import/add' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$c->flash( success => 'Created Value Set' );
|
||||||
|
$c->redirect_to( '/admin/import/' . $value_set->id );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_value {
|
||||||
|
my $c = shift;
|
||||||
|
my $set_id = $c->param('set_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
sub post_value {
|
||||||
|
my $c = shift;
|
||||||
|
my $set_id = $c->param('set_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
43
lib/Pear/LocalLoop/Schema/ResultSet/ImportSet.pm
Normal file
43
lib/Pear/LocalLoop/Schema/ResultSet/ImportSet.pm
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package Pear::LocalLoop::Schema::ResultSet::ImportSet;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use base 'DBIx::Class::ResultSet';
|
||||||
|
|
||||||
|
sub get_values {
|
||||||
|
my $self = shift;
|
||||||
|
my $id = shift;
|
||||||
|
|
||||||
|
return $self->find($id)->search_related(
|
||||||
|
'values',
|
||||||
|
undef,
|
||||||
|
{
|
||||||
|
order_by => { -asc => 'id' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_users {
|
||||||
|
my $self = shift;
|
||||||
|
my $id = shift;
|
||||||
|
|
||||||
|
return $self->get_values($id)->search({},
|
||||||
|
{
|
||||||
|
group_by => 'user_name',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_orgs {
|
||||||
|
my $self = shift;
|
||||||
|
my $id = shift;
|
||||||
|
|
||||||
|
return $self->get_values($id)->search({},
|
||||||
|
{
|
||||||
|
group_by => 'org_name',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
43
templates/admin/import/get_add.html.ep
Normal file
43
templates/admin/import/get_add.html.ep
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
% layout 'admin';
|
||||||
|
% title 'Import';
|
||||||
|
% content_for javascript => begin
|
||||||
|
% end
|
||||||
|
% if ( my $error = flash 'error' ) {
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<strong>Error!</strong> <%= $error %>
|
||||||
|
</div>
|
||||||
|
% } elsif ( my $success = flash 'success' ) {
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
<strong>Success!</strong> <%= $success %>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h3>Add CSV Import</h3>
|
||||||
|
<p>
|
||||||
|
Copy and paste a CSV in to import it. The first line should contain the
|
||||||
|
headers The following headers are recognised:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>user</li>
|
||||||
|
<li>value</li>
|
||||||
|
<li>date</li>
|
||||||
|
<li>organisation</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Any columns which are not in the list above will be ignored and can be
|
||||||
|
called anything you want, as long as they are not in the list above. They
|
||||||
|
must also be unique.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For date format, consult <a href="https://metacpan.org/pod/DateTime::Format::Strptime#STRPTIME-PATTERN-TOKENS">here</a> for the patterns
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<form action="<%= url_for %>" method="post">
|
||||||
|
<input type="text" class="form-control mb-3" name="date_format" required value="<%=flash 'date_format' %>"/>
|
||||||
|
<textarea class="form-control mb-3" rows="15" name="csv"><%= flash 'csv_data' %></textarea>
|
||||||
|
<button class="btn btn-success btn-block" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
0
templates/admin/import/get_value.html.ep
Normal file
0
templates/admin/import/get_value.html.ep
Normal file
45
templates/admin/import/index.html.ep
Normal file
45
templates/admin/import/index.html.ep
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
% layout 'admin';
|
||||||
|
% title 'Import';
|
||||||
|
% content_for javascript => begin
|
||||||
|
% end
|
||||||
|
% if ( my $error = flash 'error' ) {
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<strong>Error!</strong> <%= $error %>
|
||||||
|
</div>
|
||||||
|
% } elsif ( my $success = flash 'success' ) {
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
<strong>Success!</strong> <%= $success %>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">
|
||||||
|
CSV Import
|
||||||
|
<a href="<%= url_for . '/add' %>" class="btn btn-success" style="float: right">Import Data</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% for my $import ( $import_rs->all ) {
|
||||||
|
<div class="col col-md-4 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="font-bold"><%= $import->id %></span>
|
||||||
|
%= format_human_datetime $import->date;
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-right">
|
||||||
|
<a href="<%= url_for . '/' . $import->id %>" class="card-link">
|
||||||
|
Edit Import
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
%= bootstrap_pagination( $c->param('page') || 1, $import_rs->pager->last_page, { class => 'justify-content-center' } );
|
||||||
|
</div>
|
||||||
|
</div>
|
102
templates/admin/import/list.html.ep
Normal file
102
templates/admin/import/list.html.ep
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
% layout 'admin';
|
||||||
|
% title 'Import';
|
||||||
|
% content_for javascript => begin
|
||||||
|
% end
|
||||||
|
% if ( my $error = flash 'error' ) {
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<strong>Error!</strong> <%= $error %>
|
||||||
|
</div>
|
||||||
|
% } elsif ( my $success = flash 'success' ) {
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
<strong>Success!</strong> <%= $success %>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">
|
||||||
|
User Assignments
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
Unique users in this Import, and their assigned entity
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
% for my $user ( $import_users_rs->all ) {
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
%= $user->user_name
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<span class="text-muted font-italic">Unassigned</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<a class="btn btn-primary">Select</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">
|
||||||
|
Org Assignments
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
Unique orgs in this Import, and their assigned entity
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
% for my $org ( $import_org_rs->all ) {
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
%= $org->org_name
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<span class="text-muted font-italic">Unassigned</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<a class="btn btn-primary">Select</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">
|
||||||
|
%= format_human_datetime $import_set->date;
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
Content listed in original order of import
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
% for my $import_value ( $import_value_rs->all ) {
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2">
|
||||||
|
<%= $import_value->user_name %>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<%= format_human_datetime $import_value->purchase_date %>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<%= $import_value->purchase_value %>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<%= $import_value->org_name %>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<a href="#" class="btn btn-danger">Ignore</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -50,6 +50,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link<%= title eq 'Organisations' ? ' active' : '' %>" href="<%= url_for '/admin/organisations' %>">Organisations</a>
|
<a class="nav-link<%= title eq 'Organisations' ? ' active' : '' %>" href="<%= url_for '/admin/organisations' %>">Organisations</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link<%= title eq 'Import' ? ' active' : '' %>" href="<%= url_for '/admin/import' %>">Import</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="<%= url_for '/admin/logout' %>">Logout</a>
|
<a class="nav-link" href="<%= url_for '/admin/logout' %>">Logout</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Reference in a new issue