X-Git-Url: http://git.ieval.ro/?p=gruntmaster-data.git;a=blobdiff_plain;f=lib%2FGruntmaster%2FData.pm;h=c46a358964777ec70df4fbfdb0a2b129b104de7d;hp=635719422c5fc427d52e7fca02b6329f0010da82;hb=625a7bd077ef2781d055a77bffa19dd9d6c0d1f2;hpb=f7386aabf2077e1067b14cdcd5162e8c9b762bc6 diff --git a/lib/Gruntmaster/Data.pm b/lib/Gruntmaster/Data.pm index 6357194..c46a358 100644 --- a/lib/Gruntmaster/Data.pm +++ b/lib/Gruntmaster/Data.pm @@ -1,105 +1,177 @@ +use utf8; package Gruntmaster::Data; -use v5.14; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; use warnings; -use parent qw/Exporter/; -use JSON qw/encode_json decode_json/; -use Redis; -use Sub::Name qw/subname/; +use base 'DBIx::Class::Schema'; + +__PACKAGE__->load_namespaces; + -our $VERSION = '5999.000_001'; +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-05 13:11:39 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dAEmtAexvUaNXLgYz2rNEg -our $contest; -my $redis = Redis->new; -my $pubsub = Redis->new; +our $VERSION = '5999.000_012'; + +use Lingua::EN::Inflect qw/PL_N/; +use JSON::MaybeXS qw/decode_json/; +use PerlX::Maybe qw/maybe/; +use Sub::Name qw/subname/; + +use constant PROBLEM_PUBLIC_COLUMNS => [qw/id author writer level name owner private statement timeout olimit value/]; +use constant USER_PUBLIC_COLUMNS => [qw/id admin name town university level/]; +use constant JOBS_PER_PAGE => 10; sub dynsub{ our ($name, $sub) = @_; - no strict 'refs'; + no strict 'refs'; ## no critic (Strict) *$name = subname $name => $sub } BEGIN { - for my $cmd (qw/multi exec smembers get hget hdel hset sadd srem incr hmset hsetnx publish del/) { - dynsub uc $cmd, sub { $redis->$cmd(@_) }; - } - - for my $cmd (qw/subscribe wait_for_messages/) { - dynsub uc $cmd, sub { $pubsub->$cmd(@_) }; + for my $rs (qw/contest contest_problem job open problem user problem_status contest_status/) { + my $rsname = ucfirst $rs; + $rsname =~ s/_([a-z])/\u$1/gs; + dynsub PL_N($rs) => sub { $_[0]->resultset($rsname) }; + dynsub $rs => sub { $_[0]->resultset($rsname)->find($_[1]) }; } } -sub cp { defined $contest ? "contest.$contest." : '' } - -sub problems () { SMEMBERS cp . 'problem' } -sub contests () { SMEMBERS cp . 'contest' } -sub users () { SMEMBERS cp . 'user' } -sub jobcard () { GET cp . 'job' } - -sub job_results (_) { decode_json HGET cp . "job.$_[0]", 'results' } -sub set_job_results ($+) { HSET cp . "job.$_[0]", 'results', encode_json $_[1] } -sub job_inmeta (_) { decode_json HGET cp . "job.$_[0]", 'inmeta' } -sub set_job_inmeta ($+) { HSET cp . "job.$_[0]", 'inmeta', encode_json $_[1] } -sub problem_meta (_) { decode_json HGET cp . "problem.$_[0]", 'meta' } -sub set_problem_meta ($+) { HSET cp . "problem.$_[0]", 'meta', encode_json $_[1] } -sub job_daemon (_) { HGET cp . "job.$_[0]", 'daemon' } -sub set_job_daemon ($$) { HSETNX cp . "job.$_[0]", 'daemon', $_[1] }; - -sub defhash{ - my ($name, @keys) = @_; - for my $key (@keys) { - dynsub "${name}_$key", sub (_) { HGET cp . "$name.$_[0]", $key }; - dynsub "set_${name}_$key", sub ($$) { HSET cp . "$name.$_[0]", $key, $_[1] }; - } +sub user_list { + my $rs = $_[0]->users->search(undef, {columns => USER_PUBLIC_COLUMNS, prefetch => [qw/problem_statuses contest_statuses/]} ); + [ sort { $b->{solved} <=> $a->{solved} or $b->{attempted} <=> $a->{attempted} } map { ## no critic (ProhibitReverseSort) + my $solved = $_->problem_statuses->count(solved => 1); + my $attempted = $_->problem_statuses->count(solved => 0); + my $contests = $_->contest_statuses->count; + +{ $_->get_columns, solved => $solved, attempted => $attempted, contests => $contests } + } $rs->all ] +} - dynsub "edit_$name", sub { - my ($key, %values) = @_; - HMSET cp . "$name.$key", %values; - }; +sub user_entry { + my ($self, $id) = @_; + my $user = $self->users->find($id, {columns => USER_PUBLIC_COLUMNS, prefetch => [qw/problem_statuses contest_statuses/]}); + my @problems = map { {problem => $_->get_column('problem'), solved => $_->solved} } $user->problem_statuses; + my @contests = map { {contest => $_->contest->id, contest_name => $_->contest->name, rank => $_->rank, score => $_->score} } $user->contest_statuses->search(undef, {prefetch => 'contest'}); + +{ $user->get_columns, problems => \@problems, contests => \@contests } +} - dynsub "insert_$name", sub { - my ($key, %values) = @_; - SADD cp . $name, $key or return; - HMSET cp . "$name.$key", %values; - }; - dynsub "remove_$name", sub (_) { - my $key = shift; - SREM cp . $name, $key; - DEL cp . "$name.$key"; - }; +sub problem_list { + my ($self, %args) = @_; + my $rs = $self->problems->search(undef, {order_by => 'me.name', columns => PROBLEM_PUBLIC_COLUMNS, prefetch => 'owner'}); + $rs = $rs->search({-or => ['contest_problems.contest' => undef, 'contest.stop' => {'<=', time}], 'me.private' => 0}, {join => {'contest_problems' => 'contest'}, distinct => 1}) unless $args{contest}; + $rs = $rs->search({'contest_problems.contest' => $args{contest}}, {join => 'contest_problems'}) if $args{contest}; + $rs = $rs->search({'me.owner' => $args{owner}}) if $args{owner}; + my %params; + $params{contest} = $args{contest} if $args{contest}; + for ($rs->all) { + $params{$_->level} //= []; + push @{$params{$_->level}}, {$_->get_columns, owner_name => $_->owner->name} ; + } + \%params +} - dynsub "push_$name", sub { - my $nr = INCR cp . $name; - HMSET cp . "$name.$nr", @_; - $nr - }; +sub problem_entry { + my ($self, $id, $contest, $user) = @_; + my $running = $contest && $self->contest($contest)->is_running; + my $columns = PROBLEM_PUBLIC_COLUMNS; + push @$columns, 'solution' unless $running; + my $pb = $self->problems->find($id, {columns => $columns, prefetch => 'owner'}); + eval { ## no critic (RequireCheckingReturnValueOfEval) + $self->opens->create({ + contest => $contest, + problem => $id, + owner => $user, + time => time, + }) + } if $running; + $contest &&= $self->contest($contest); + +{ $pb->get_columns, owner_name => $pb->owner->name, cansubmit => $contest ? $running : 1, $running ? (contest_start => $contest->start, contest_stop => $contest->stop) : () } } -defhash problem => qw/name level statement owner author/; -defhash contest => qw/start end name owner/; -defhash job => qw/date errors extension filesize private problem result result_text user/; -defhash user => qw/name email town university level/; +sub contest_list { + my ($self, %args) = @_; + my $rs = $self->contests->search(undef, {order_by => {-desc => 'start'}, prefetch => 'owner'}); + $rs = $rs->search({owner => $args{owner}}) if $args{owner}; + my %params; + for ($rs->all) { + my $state = $_->is_pending ? 'pending' : $_->is_running ? 'running' : 'finished'; + $params{$state} //= []; + push @{$params{$state}}, { $_->get_columns, started => !$_->is_pending, owner_name => $_->owner->name }; + } + \%params +} -sub clean_job (_){ - HDEL cp . "job.$_[0]", qw/result result_text results daemon/ +sub contest_entry { + my ($self, $id) = @_; + my $ct = $self->contest($id); + +{ $ct->get_columns, started => !$ct->is_pending, owner_name => $ct->owner->name } } -sub mark_open { - my ($problem, $user) = @_; - HSETNX cp . 'open', "$problem.$user", time; +sub job_list { + my ($self, %args) = @_; + $args{page} //= 1; + my $rs = $self->jobs->search(undef, {order_by => {-desc => 'me.id'}, prefetch => ['problem', 'owner'], rows => JOBS_PER_PAGE, page => $args{page}}); + $rs = $rs->search({'me.owner' => $args{owner}}) if $args{owner}; + $rs = $rs->search({contest => $args{contest}}) if $args{contest}; + $rs = $rs->search({problem => $args{problem}}) if $args{problem}; + return { + log => [map { + my %params = $_->get_columns; + $params{owner_name} = $_->owner->name; + $params{problem_name} = $_->problem->name; + $params{results} &&= decode_json $params{results}; + $params{size} = length $params{source}; + delete $params{source}; + \%params + } $rs->all], + current_page => $rs->pager->current_page, + maybe previous_page => $rs->pager->previous_page, + maybe next_page => $rs->pager->next_page, + maybe last_page => $rs->pager->last_page, + } } -sub get_open { - my ($problem, $user) = @_; - HGET cp . 'open', "$problem.$user"; +sub job_entry { + my ($self, $id) = @_; + my $job = $self->jobs->find($id, {prefetch => ['problem', 'owner']}); + my %params = $job->get_columns; + $params{owner_name} = $job->owner->name; + $params{problem_name} = $job->problem->name; + $params{results} &&= decode_json $params{results}; + $params{size} = length $params{source}; + delete $params{source}; + \%params } -our @EXPORT = do { - no strict 'refs'; - grep { $_ =~ /^[a-zA-Z]/ and exists &$_ } keys %{__PACKAGE__ . '::'}; -}; +sub update_status { + my ($self) = @_; + my @jobs = $self->jobs->search(undef, {cache => 1})->all; + + my %hash; + $hash{$_->get_column('problem'), $_->get_column('owner')} = [$_->id, $_->result ? 0 : 1] for @jobs; + my @problem_statuses = map { [split ($;), @{$hash{$_}} ] } keys %hash; + + my @contest_statuses = map { + my $contest = $_->id; + map { [$contest, $_->{user}, $_->{score}, $_->{rank}] } $_->standings + } $self->contests->all; + + my $txn = sub { + $self->problem_statuses->delete; + $self->problem_statuses->populate([[qw/problem owner job solved/], @problem_statuses]); + $self->contest_statuses->delete; + $self->contest_statuses->populate([[qw/contest owner score rank/], @contest_statuses]); + }; + + $self->txn_do($txn); +} 1; + __END__ =encoding utf-8 @@ -110,346 +182,167 @@ Gruntmaster::Data - Gruntmaster 6000 Online Judge -- database interface and tool =head1 SYNOPSIS - for my $problem (problems) { - say "Problem name: " . problem_name $problem; - say "Problem level: " . problem_level $problem; - ... - } + my $db = Gruntmaster::Data->connect('dbi:Pg:'); -=head1 DESCRIPTION + my $problem = $db->problem('my_problem'); + $problem->update({timeout => 2.5}); # Set time limit to 2.5 seconds + $problem->rerun; # And rerun all jobs for this problem -Gruntmaster::Data is the Redis interface used by the Gruntmaster 6000 Online Judge. It exports many functions for talking to the database. All functions are exported by default. + # ... -The current contest is selected by setting the C<< $Gruntmaster::Data::contest >> variable. + my $contest = $db->contests->create({ # Create a new contest + id => 'my_contest', + name => 'My Awesome Contest', + start => time + 100, + end => time + 1900, + }); + $db->contest_problems->create({ # Add a problem to the contest + contest => 'my_contest', + problem => 'my_problem', + }); - local $Gruntmaster::Data::contest = 'mycontest'; - say 'There are' . jobcard . ' jobs in my contest'; + say 'The contest has not started yet' if $contest->is_pending; -=head1 FUNCTIONS + # ... -=head2 Redis + my @jobs = $db->jobs->search({contest => 'my_contest', owner => 'MGV'})->all; + $_->rerun for @jobs; # Rerun all jobs sent by MGV in my_contest -Gruntmaster::Data exports some functions for talking directly to the Redis server. These functions should not normally be used, except for B, B, B, B and B. +=head1 DESCRIPTION -These functions correspond to Redis commands. The current list is: B<< MULTI EXEC SMEMBERS GET HGET HDEL HSET SADD SREM INCR HMSET HSETNX DEL PUBLISH SUBSCRIBE WAIT_FOR_MESSAGES >>. +Gruntmaster::Data is the interface to the Gruntmaster 6000 database. Read the L documentation for usage information. -=head2 Problems +In addition to the typical DBIx::Class::Schema methods, this module contains several convenience methods: =over -=item B - -Returns a list of problems in the current contest. - -=item B I<$problem> +=item contests -Returns a problem's meta. +Equivalent to C<< $schema->resultset('Contest') >> -=item B I<$problem>, I<$meta> +=item contest_problems -Sets a problem's meta. +Equivalent to C<< $schema->resultset('ContestProblem') >> -=item B I<$problem> +=item jobs -Returns a problem's name. +Equivalent to C<< $schema->resultset('Job') >> -=item B I<$problem>, I<$name> +=item problems -Sets a problem's name. +Equivalent to C<< $schema->resultset('Problem') >> -=item B I<$problem> +=item users -Returns a problem's level. The levels are beginner, easy, medium, hard. +Equivalent to C<< $schema->resultset('User') >> -=item B I<$problem>, I<$level> +=item contest($id) -Sets a problem's level. The levels are beginner, easy, medium, hard. +Equivalent to C<< $schema->resultset('Contest')->find($id) >> -=item B I<$problem> +=item job($id) -Returns a problem's statement. +Equivalent to C<< $schema->resultset('Job')->find($id) >> -=item B I<$problem>, I<$statement> +=item problem($id) -Sets a problem's statement. +Equivalent to C<< $schema->resultset('Problem')->find($id) >> -=item B I<$problem> +=item user($id) -Returns a problem's owner. +Equivalent to C<< $schema->resultset('User')->find($id) >> -=item B I<$problem>, I<$owner> +=item user_list -Sets a problem's owner. +Returns a list of users as an arrayref containing hashrefs. -=item B I<$problem> +=item user_entry($id) -Returns a problem's author. +Returns a hashref with information about the user $id. -=item B I<$problem>, I<$author> +=item problem_list([%args]) -Sets a problem's author. +Returns a list of problems grouped by level. A hashref with levels as keys. -=item B I<$problem>, I<$user> - -Returns the time when I<$user> opened I<$problem>. - -=item B I<$problem>, I<$user> - -Sets the time when I<$user> opened I<$problem> to the current time. Does nothing if I<$user> has already opened I<$problem>. - -=item B I<$id>, I<$key> => I<$value>, ... - -Inserts a problem with id I<$id> and the given initial configuration. Does nothing if a problem with id I<$id> already exists. Returns true if the problem was added, false otherwise. - -=item B I<$id>, I<$key> => I<$value>, ... - -Updates the configuration of a problem. The values of the given keys are updated. All other keys/values are left intact. - -=item B I<$id> - -Removes a problem. - -=back - -=head2 Contests - -B<<< WARNING: these functions only work correctly when C<< $Gruntmaster::Data::contest >> is undef >>> +Takes the following arguments: =over -=item B - -Returns a list of contests. - -=item B I<$contest> - -Returns a contest's start time. - -=item B I<$contest>, I<$start> - -Sets a contest's start time. - -=item B I<$contest> - -Returns a contest's end time. - -=item B I<$contest>, I<$end> - -Sets a contest's end time. - -=item B I<$contest> - -Returns a contest's name. - -=item B I<$contest>, I<$name> - -Sets a contest's name. - -=item B I<$contest> - -Returns a contest's owner. - -=item B I<$contest>, I<$owner> - -Sets a contest's owner. - -=item B I<$id>, I<$key> => I<$value>, ... - -Inserts a contest with id I<$id> and the given initial configuration. Does nothing if a contest with id I<$id> already exists. Returns true if the contest was added, false otherwise. +=item owner -=item B I<$id>, I<$key> => I<$value>, ... +Only show problems owned by this user -Updates the configuration of a contest. The values of the given keys are updated. All other keys/values are left intact. +=item contest -=item B I<$id> - -Removes a contest. +Only show problems in this contest =back -=head2 Jobs - -=over - -=item B - -Returns the number of jobs in the database. - -=item B I<$job> - -Returns an array of job results. Each element corresponds to a test and is a hashref with keys B (test number), B (result code, see L), B (result description) and B