From bc12dbf8b01b85ee8ff14b2a778813e5d1282484 Mon Sep 17 00:00:00 2001 From: Tom Bloor Date: Tue, 23 May 2017 23:06:07 +0100 Subject: [PATCH] Added leaderboard first pass stuff --- .../LocalLoop/Schema/Result/Leaderboard.pm | 202 ++++++++++++++++++ .../LocalLoop/Schema/Result/LeaderboardSet.pm | 52 +++++ .../Schema/Result/LeaderboardValue.pm | 61 ++++++ lib/Test/Pear/LocalLoop.pm | 12 ++ script/deploy_db | 12 ++ t/schema/leaderboard.t | 178 +++++++++++++++ 6 files changed, 517 insertions(+) create mode 100644 lib/Pear/LocalLoop/Schema/Result/Leaderboard.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/LeaderboardSet.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/LeaderboardValue.pm create mode 100644 t/schema/leaderboard.t diff --git a/lib/Pear/LocalLoop/Schema/Result/Leaderboard.pm b/lib/Pear/LocalLoop/Schema/Result/Leaderboard.pm new file mode 100644 index 0000000..80da852 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/Leaderboard.pm @@ -0,0 +1,202 @@ +package Pear::LocalLoop::Schema::Result::Leaderboard; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +use DateTime; + +__PACKAGE__->table("leaderboards"); + +__PACKAGE__->add_columns( + "id" => { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + }, + "name" => { + data_type => "varchar", + size => 255, + is_nullable => 0, + }, + "type" => { + data_type => "varchar", + size => 255, + is_nullable => 0, + }, +); + +__PACKAGE__->set_primary_key("id"); + +__PACKAGE__->add_unique_constraint(["type"]); + +__PACKAGE__->has_many( + "sets", + "Pear::LocalLoop::Schema::Result::LeaderboardSet", + { "foreign.leaderboard_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +sub create_new { + my $self = shift; + my $start = shift; + + my $type = $self->type; + + if ( $type eq 'daily_total' ) { + return $self->_create_total_set( $start, $start->clone->add( days => 1 ) ); + } elsif ( $type eq 'weekly_total' ) { + return $self->_create_total_set( $start, $start->clone->add( days => 7 ) ); + } elsif ( $type eq 'monthly_total' ) { + return $self->_create_total_set( $start, $start->clone->add( months => 1 ) ); + } elsif ( $type eq 'all_time_total' ) { + return $self->_create_total_all_time; + } elsif ( $type eq 'daily_count' ) { + return $self->_create_count_set( $start, $start->clone->add( days => 1 ) ); + } elsif ( $type eq 'weekly_count' ) { + return $self->_create_count_set( $start, $start->clone->add( days => 7 ) ); + } elsif ( $type eq 'monthly_count' ) { + return $self->_create_count_set( $start, $start->clone->add( months => 1 ) ); + } elsif ( $type eq 'all_time_count' ) { + return $self->_create_count_all_time; + } + warn "Unrecognised type"; + return $self; +} + +sub _create_total_set { + my ( $self, $start, $end ) = @_; + + my $schema = $self->result_source->schema; + + my $user_rs = $schema->resultset('User'); + + my @leaderboard; + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions->search_between( $start, $end ); + + my $transaction_sum = $transaction_rs->get_column('value')->sum; + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_sum || 0, + }; + } + + $self->create_related( + 'sets', + { + date => $start, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub _create_count_set { + my ( $self, $start, $end ) = @_; + + my $schema = $self->result_source->schema; + + my $user_rs = $schema->resultset('User'); + + my @leaderboard; + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions->search_between( $start, $end ); + + my $transaction_count = $transaction_rs->count; + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_count || 0, + }; + } + + $self->create_related( + 'sets', + { + date => $start, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub _create_total_all_time { + my ( $self ) = @_; + + my $schema = $self->result_source->schema; + + my $user_rs = $schema->resultset('User'); + + my $end = DateTime->today; + + my @leaderboard; + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions; + + my $transaction_sum = $transaction_rs->get_column('value')->sum; + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_sum || 0, + }; + } + + $self->create_related( + 'sets', + { + date => $end, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub _create_count_all_time { + my ( $self ) = @_; + + my $schema = $self->result_source->schema; + + my $user_rs = $schema->resultset('User'); + + my $end = DateTime->today; + + my @leaderboard; + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions; + + my $transaction_count = $transaction_rs->count; + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_count || 0, + }; + } + + $self->create_related( + 'sets', + { + date => $end, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub get_latest { + my $self = shift; + + return $self; +} + +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/LeaderboardSet.pm b/lib/Pear/LocalLoop/Schema/Result/LeaderboardSet.pm new file mode 100644 index 0000000..f1a072f --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/LeaderboardSet.pm @@ -0,0 +1,52 @@ +package Pear::LocalLoop::Schema::Result::LeaderboardSet; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->load_components( qw/ + InflateColumn::DateTime +/); + +__PACKAGE__->table("leaderboard_sets"); + +__PACKAGE__->add_columns( + "id" => { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + }, + "leaderboard_id" => { + data_type => "integer", + is_foreign_key => 1, + is_nullable => 0, + }, + "date" => { + data_type => "datetime", + is_nullable => 0, + }, +); + +__PACKAGE__->set_primary_key("id"); + +__PACKAGE__->belongs_to( + "leaderboard", + "Pear::LocalLoop::Schema::Result::Leaderboard", + { "foreign.id" => "self.leaderboard_id" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +__PACKAGE__->has_many( + "values", + "Pear::LocalLoop::Schema::Result::LeaderboardValue", + { "foreign.set_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/LeaderboardValue.pm b/lib/Pear/LocalLoop/Schema/Result/LeaderboardValue.pm new file mode 100644 index 0000000..29aa59e --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/LeaderboardValue.pm @@ -0,0 +1,61 @@ +package Pear::LocalLoop::Schema::Result::LeaderboardValue; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->table("leaderboard_values"); + +__PACKAGE__->add_columns( + "id" => { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + }, + "user_id" => { + data_type => "integer", + is_foreign_key => 1, + is_nullable => 0, + }, + "set_id" => { + data_type => "integer", + is_foreign_key => 1, + is_nullable => 0, + }, + "value" => { + data_type => "decimal", + size => [ 16, 2 ], + is_nullable => 0, + }, +); + +__PACKAGE__->set_primary_key("id"); + +__PACKAGE__->add_unique_constraint([qw/ user_id set_id /]); + +__PACKAGE__->belongs_to( + "set", + "Pear::LocalLoop::Schema::Result::LeaderboardSet", + { "foreign.id" => "self.set_id" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +__PACKAGE__->belongs_to( + "user", + "Pear::LocalLoop::Schema::Result::User", + { "foreign.id" => "self.user_id" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +1; diff --git a/lib/Test/Pear/LocalLoop.pm b/lib/Test/Pear/LocalLoop.pm index 7955370..9581b02 100644 --- a/lib/Test/Pear/LocalLoop.pm +++ b/lib/Test/Pear/LocalLoop.pm @@ -37,6 +37,18 @@ has framework => sub { [ '50+' ], ]); + $schema->resultset('Leaderboard')->populate([ + [ qw/ name type / ], + [ 'Daily Total', 'daily_total' ], + [ 'Daily Count', 'daily_count' ], + [ 'Weekly Total', 'weekly_total' ], + [ 'Weekly Count', 'weekly_count' ], + [ 'Monthly Total', 'monthly_total' ], + [ 'Monthly Count', 'monthly_count' ], + [ 'All Time Total', 'all_time_total' ], + [ 'All Time Count', 'all_time_count' ], + ]); + return $t; }; diff --git a/script/deploy_db b/script/deploy_db index 9354a6d..d0e4c19 100755 --- a/script/deploy_db +++ b/script/deploy_db @@ -21,6 +21,18 @@ $schema->resultset('AgeRange')->populate([ [ '50+' ], ]); +$schema->resultset('Leaderboard')->populate([ + [ qw/ name type / ], + [ 'Daily Total', 'daily_total' ], + [ 'Daily Count', 'daily_count' ], + [ 'Weekly Total', 'weekly_total' ], + [ 'Weekly Count', 'weekly_count' ], + [ 'Monthly Total', 'monthly_total' ], + [ 'Monthly Count', 'monthly_count' ], + [ 'All Time Total', 'all_time_total' ], + [ 'All Time Count', 'all_time_count' ], +]); + if (defined $ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'development' ) { $schema->resultset('User')->create({ diff --git a/t/schema/leaderboard.t b/t/schema/leaderboard.t new file mode 100644 index 0000000..2cf96f8 --- /dev/null +++ b/t/schema/leaderboard.t @@ -0,0 +1,178 @@ +use Mojo::Base -strict; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new; +my $t = $framework->framework; +my $schema = $t->app->schema; +my $dtf = $schema->storage->datetime_parser; + +my $user1 = { + token => 'a', + full_name => 'Test User1', + display_name => 'Test User1', + email => 'test1@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user2 = { + token => 'b', + full_name => 'Test User2', + display_name => 'Test User2', + email => 'test2@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user3 = { + token => 'c', + full_name => 'Test User3', + display_name => 'Test User3', + email => 'test3@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user4 = { + token => 'd', + full_name => 'Test User4', + display_name => 'Test User4', + email => 'test4@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $org = { + token => 'e', + email => 'test5@example.com', + name => 'Test Org', + street_name => 'Test Street', + town => 'Lancaster', + postcode => 'LA1 1AA', + password => 'abc123', +}; + +$schema->resultset('AccountToken')->create({ name => $_->{token} }) + for ( $user1, $user2, $user3, $user4, $org ); + +$framework->register_customer($_) + for ( $user1, $user2, $user3, $user4 ); + +$framework->register_organisation($org); + +my $org_result = $schema->resultset('Organisation')->find({ name => $org->{name} }); + +my $tweak = 0; + +for my $user ( $user1, $user2, $user3, $user4 ) { + $tweak ++; + my $user_result = $schema->resultset('User')->find({ email => $user->{email} }); + for ( 1 .. 10 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + }); + } + + for ( 11 .. 20 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime(DateTime->today()->subtract( days => 5 )), + }); + } + + for ( 21 .. 30 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime(DateTime->today()->subtract( days => 25 )), + }); + } + + for ( 31 .. 40 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime(DateTime->today()->subtract( days => 50 )), + }); + } + + is $user_result->transactions->count, 40, 'correct count for user' . $tweak; +} + +sub test_leaderboard { + my ( $title, $name, $date, $expected ) = @_; + + subtest $title => sub { + my $leaderboard_rs = $schema->resultset('Leaderboard'); + + my $today_board = $leaderboard_rs->find({ type => $name })->create_new($date)->sets->first; + + is $today_board->values->count, 5, 'correct value count for today'; + + my $today_values = $today_board->values->search( + {}, + { + order_by => { -desc => 'value' }, + columns => [ qw/ user_id value / ], + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + is_deeply [ $today_values->all ], $expected, 'Today as expected'; + }; +} + +test_leaderboard( + 'Daily Total', + 'daily_total', + DateTime->today, + [ + { user_id => 4, value => 95 }, + { user_id => 3, value => 85 }, + { user_id => 2, value => 75 }, + { user_id => 1, value => 65 }, + { user_id => 5, value => 0 }, + ] +); + +test_leaderboard( + 'Daily Count', + 'daily_count', + DateTime->today, + [ + { user_id => 1, value => 10 }, + { user_id => 2, value => 10 }, + { user_id => 3, value => 10 }, + { user_id => 4, value => 10 }, + { user_id => 5, value => 0 }, + ] +); + +test_leaderboard( + 'Weekly Total', + 'weekly_total', + DateTime->today->subtract( days => 7 ), + [ + { user_id => 4, value => 195 }, + { user_id => 3, value => 185 }, + { user_id => 2, value => 175 }, + { user_id => 1, value => 165 }, + { user_id => 5, value => 0 }, + ] +); + +done_testing;