Add non-DBIC versions of all methods and a benchmark script
[gruntmaster-data.git] / lib / Gruntmaster / Data.pm
1 use utf8;
2 package Gruntmaster::Data;
3
4 # Created by DBIx::Class::Schema::Loader
5 # DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7 use strict;
8 use warnings;
9
10 use base 'DBIx::Class::Schema';
11
12 __PACKAGE__->load_namespaces;
13
14
15 # Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-03-05 13:11:39
16 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dAEmtAexvUaNXLgYz2rNEg
17
18 use parent qw/Exporter/;
19 our $VERSION = '5999.000_013';
20 our @EXPORT = qw/purge/; ## no critic (ProhibitAutomaticExportation)
21
22 use Lingua::EN::Inflect qw/PL_N/;
23 use JSON::MaybeXS qw/decode_json/;
24 use HTTP::Tiny;
25 use PerlX::Maybe qw/maybe/;
26 use Sub::Name qw/subname/;
27 use Class::Method::Modifiers qw/around/;
28
29 use DBI;
30 use DBIx::Simple;
31 use List::Util qw/sum/;
32 use SQL::Abstract;
33
34 use constant CONTEST_PUBLIC_COLUMNS => [qw/id name description start stop owner/];
35 use constant PROBLEM_PUBLIC_COLUMNS => [qw/id author writer level name owner private timeout olimit value/];
36 use constant USER_PUBLIC_COLUMNS => [qw/id admin name town university country level/];
37 use constant JOBS_PER_PAGE => 50;
38
39 sub dynsub{
40 our ($name, $sub) = @_;
41 no strict 'refs'; ## no critic (Strict)
42 *$name = subname $name => $sub
43 }
44
45 BEGIN {
46 for my $rs (qw/contest contest_problem job open limit problem user problem_status contest_status/) {
47 my $rsname = ucfirst $rs;
48 $rsname =~ s/_([a-z])/\u$1/gs;
49 dynsub PL_N($rs) => sub { $_[0]->resultset($rsname) };
50 dynsub $rs => sub { $_[0]->resultset($rsname)->find($_[1]) };
51 }
52 }
53
54 my %statements = (
55 user_list_sth => 'SELECT * FROM user_list LIMIT 200',
56 user_entry_sth => 'SELECT * FROM user_data WHERE id = ?',
57
58 problem_status_sth => 'SELECT problem,solved FROM problem_status WHERE owner = ?',
59 contest_status_sth => 'SELECT contest,score,rank FROM contest_status WHERE owner = ?',
60
61 contest_list_sth => 'SELECT * FROM contest_entry',
62 contest_entry_sth => 'SELECT * FROM contest_entry WHERE id = ?',
63 contest_full_sth => 'SELECT * FROM contests WHERE id = ?',
64 contest_problems_sth => 'SELECT problem FROM contest_problems JOIN problems pb ON problem=pb.id WHERE contest = ? ORDER BY pb.value',
65 contest_has_problem_sth => 'SELECT EXISTS(SELECT 1 FROM contest_problems WHERE contest = ? AND problem = ?)',
66 opens_sth => 'SELECT problem,owner,time FROM opens WHERE contest = ?',
67
68 problem_entry_sth => 'SELECT ' . (join ',', @{PROBLEM_PUBLIC_COLUMNS()}, 'statement', 'solution') . ' FROM problems WHERE id = ?',
69 limits_sth => 'SELECT format,timeout FROM limits WHERE problem = ?',
70 problem_values_sth => 'SELECT id,value FROM problems',
71
72 job_entry_sth => 'SELECT * FROM job_entry WHERE id = ?',
73 job_full_sth => 'SELECT * FROM jobs WHERE id = ?',
74 );
75
76 around connect => sub {
77 my $orig = shift;
78 my $self = $orig->(@_);
79 $self->{dbh} = DBI->connect($_[1]);
80 $self->{dbis} = DBIx::Simple->new($self->{dbh});
81 $self->{dbis}->keep_statements = 100;
82 $self
83 };
84
85 sub purge;
86
87 sub query {
88 my ($self, $stat, @extra) = @_;
89 $self->{dbis}->query($statements{$stat} // $stat, @extra)
90 }
91
92 my (%name_cache, %name_cache_time);
93 use constant NAME_CACHE_MAX_AGE => 5;
94
95 sub object_name {
96 my ($self, $table, $id) = @_;
97 $name_cache_time{$table} //= 0;
98 if (time - $name_cache_time{$table} > NAME_CACHE_MAX_AGE) {
99 $name_cache_time{$table} = time;
100 $name_cache{$table} = {};
101 $name_cache{$table} = $self->{dbis}->select($table, 'id,name')->map;
102 }
103
104 $name_cache{$table}{$id}
105 }
106
107
108 sub add_names {
109 my ($self, $el) = @_;
110 if (ref $el eq 'ARRAY') {
111 $self->add_names($_) for @$el
112 } else {
113 for my $object (qw/contest owner problem/) {
114 my $table = $object eq 'owner' ? 'users' : "${object}s";
115 $el->{"${object}_name"} = $self->object_name($table, $el->{$object}) if defined $el->{$object}
116 }
117 }
118
119 $el
120 }
121
122 sub user_list_orig {
123 my ($self) = @_;
124 my $rs = $self->users->search(undef, {columns => USER_PUBLIC_COLUMNS} );
125 my (%solved, %attempted, %contests);
126
127 for my $row ($self->problem_statuses->all) {
128 $solved {$row->rawowner}++ if $row->solved;
129 $attempted{$row->rawowner}++ unless $row->solved;
130 }
131 $contests{$_->rawowner}++ for $self->contest_statuses->all;
132
133 my @users = sort { $b->{solved} <=> $a->{solved} or $b->{attempted} <=> $a->{attempted} } ## no critic (ProhibitReverseSort)
134 map {
135 my $id = $_->id;
136 +{ $_->get_columns,
137 solved => ($solved{$id} // 0),
138 attempted => ($attempted{$id} // 0),
139 contests => ($contests{$id} // 0) }
140 } $rs->all;
141 @users = @users[0 .. 199] if @users > 200;
142 \@users
143 }
144
145 sub user_entry_orig {
146 my ($self, $id) = @_;
147 my $user = $self->users->find($id, {columns => USER_PUBLIC_COLUMNS, prefetch => [qw/problem_statuses contest_statuses/]});
148 my @problems = map { {problem => $_->get_column('problem'), solved => $_->solved} } $user->problem_statuses->search(undef, {order_by => 'problem'});
149 my @contests = map { {contest => $_->contest->id, contest_name => $_->contest->name, rank => $_->rank, score => $_->score} } $user->contest_statuses->search(undef, {prefetch => 'contest', order_by => 'contest.start DESC'});
150 +{ $user->get_columns, problems => \@problems, contests => \@contests }
151 }
152
153 sub user_list {
154 my ($self) = @_;
155 scalar $self->query('user_list_sth')->hashes
156 }
157
158 sub user_entry {
159 my ($self, $id) = @_;
160 my $ret = $self->query('user_entry_sth', $id)->hash;
161 $ret->{problems} = $self->query('problem_status_sth', $id)->hashes;
162 $ret->{contests} = $self->query('contest_status_sth', $id)->hashes;
163
164 $self->add_names($ret->{problems});
165 $self->add_names($ret->{contests});
166 $ret;
167 }
168
169 sub problem_list_orig {
170 my ($self, %args) = @_;
171 my @columns = @{PROBLEM_PUBLIC_COLUMNS()};
172 push @columns, 'solution' if $args{solution} && $args{contest} && !$self->contest($args{contest})->is_running;
173 my $rs = $self->problems->search(undef, {order_by => 'me.name', columns => \@columns, prefetch => 'owner'});
174 $rs = $rs->search({'private' => 0}) unless $args{contest} || $args{private};
175 $rs = $rs->search({'contest_problems.contest' => $args{contest}}, {join => 'contest_problems'}) if $args{contest};
176 $rs = $rs->search({'me.owner' => $args{owner}}) if $args{owner};
177 my %params;
178 $params{contest} = $args{contest} if $args{contest} && $self->contest($args{contest})->is_running;
179 for ($rs->all) {
180 $params{$_->level} //= [];
181 push @{$params{$_->level}}, {$_->get_columns, owner_name => $_->owner->name} ;
182 }
183 \%params
184 }
185
186 sub problem_list {
187 my ($self, %args) = @_;
188 my @columns = @{PROBLEM_PUBLIC_COLUMNS()};
189 push @columns, 'solution' if $args{solution};
190 my %where;
191 $where{private} = 0 unless $args{contest} || $args{private};
192 $where{'cp.contest'} = $args{contest} if $args{contest};
193 $where{owner} = $args{owner} if $args{owner};
194
195 my $table = $args{contest} ? 'problems JOIN contest_problems cp ON cp.problem = id' : 'problems';
196 my $ret = $self->{dbis}->select(\$table, \@columns, \%where, 'name')->hashes;
197 $self->add_names($ret);
198
199 my %params;
200 for (@$ret) {
201 $params{$_->{level}} //= [];
202 push @{$params{$_->{level}}}, $_
203 }
204 \%params
205 }
206
207 sub problem_entry_orig {
208 my ($self, $id, $contest, $user) = @_;
209 my $running = $contest && $self->contest($contest)->is_running;
210 my @columns = @{PROBLEM_PUBLIC_COLUMNS()};
211 push @columns, 'statement';
212 push @columns, 'solution' unless $running;
213 my $pb = $self->problems->find($id, {columns => \@columns, prefetch => 'owner'});
214 my @limits = map { +{
215 format => $_->format,
216 timeout => $_->timeout,
217 } } $self->limits->search({problem => $id});
218 my $open;
219 $open = $self->opens->find_or_create({
220 contest => $contest,
221 problem => $id,
222 owner => $user,
223 time => time,
224 }) if $running;
225 $contest &&= $self->contest($contest);
226 +{
227 $pb->get_columns,
228 @limits ? (limits => \@limits) : (),
229 owner_name => $pb->owner->name,
230 cansubmit => !$contest || !$contest->is_finished,
231 $running ? (
232 contest_start => $contest->start,
233 contest_stop => $contest->stop,
234 open_time => $open->time
235 ) : (),
236 }
237 }
238
239 sub problem_entry {
240 my ($self, $id, $contest, $user) = @_;
241 $contest &&= $self->contest_entry($contest);
242 my $ret = $self->query(problem_entry_sth => $id)->hash;
243 $self->add_names($ret);
244 my $limits = $self->query(limits_sth => $id)->hashes;
245 $ret->{limits} = $limits if @$limits;
246
247 if ($contest) {
248 $ret->{contest_start} = $contest->{start};
249 $ret->{contest_stop} = $contest->{stop};
250 }
251
252 $ret
253 }
254
255 sub contest_list_orig {
256 my ($self, %args) = @_;
257 my $rs = $self->contests->search(undef, {columns => CONTEST_PUBLIC_COLUMNS, order_by => {-desc => 'start'}, prefetch => 'owner'});
258 $rs = $rs->search({owner => $args{owner}}) if $args{owner};
259 my %params;
260 for ($rs->all) {
261 my $state = $_->is_pending ? 'pending' : $_->is_running ? 'running' : 'finished';
262 $params{$state} //= [];
263 push @{$params{$state}}, { $_->get_columns, owner_name => $_->owner->name };
264 }
265 \%params
266 }
267
268 sub contest_entry_orig {
269 my ($self, $id) = @_;
270 my $ct = $self->contests->find($id,{columns => CONTEST_PUBLIC_COLUMNS});
271 +{ $ct->get_columns, started => !$ct->is_pending, finished => $ct->is_finished, owner_name => $ct->owner->name }
272 }
273
274 sub contest_list {
275 my ($self) = @_;
276 my $ret = $self->query('contest_list_sth')->hashes;
277 $self->add_names($ret);
278
279 my %ret;
280 for (@$ret) {
281 my $state = $_->{finished} ? 'finished' : $_->{started} ? 'running' : 'pending';
282 $ret{$state} //= [];
283 push @{$ret{$state}}, $_;
284 }
285
286 \%ret
287 }
288
289 sub contest_entry {
290 my ($self, $id) = @_;
291 my $ret = $self->query(contest_entry_sth => $id)->hash;
292 $self->add_names($ret);
293 }
294
295 sub contest_full {
296 my ($self, $id) = @_;
297 scalar $self->query(contest_full_sth => $id)->hash;
298 }
299
300 sub contest_has_problem {
301 my ($self, $contest, $problem) = @_;
302 $self->query('contest_has_problem_sth')->flat
303 }
304
305 sub job_list_orig {
306 my ($self, %args) = @_;
307 $args{page} //= 1;
308 my $rs = $self->jobs->search(undef, {order_by => {-desc => 'me.id'}, prefetch => ['problem', 'owner', 'contest'], rows => JOBS_PER_PAGE, page => $args{page}});
309 $rs = $rs->search({contest => $args{contest} || undef}) if exists $args{contest};
310 $rs = $rs->search({'me.private'=> 0}) unless $args{private};
311 $rs = $rs->search({'me.owner' => $args{owner}}) if $args{owner};
312 $rs = $rs->search({problem => $args{problem}}) if $args{problem};
313 $rs = $rs->search({result => $args{result}}) if defined $args{result};
314 return {
315 log => [map {
316 my %params = $_->get_columns;
317 $params{owner_name} = $_->owner->name;
318 $params{problem_name} = $_->problem->name;
319 $params{contest_name} = $_->contest->name if $params{contest};
320 $params{results} &&= decode_json $params{results};
321 $params{size} = length $params{source};
322 delete $params{source};
323 \%params
324 } $rs->all],
325 current_page => $rs->pager->current_page,
326 maybe previous_page => $rs->pager->previous_page,
327 maybe next_page => $rs->pager->next_page,
328 maybe last_page => $rs->pager->last_page,
329 }
330 }
331
332 sub job_entry_orig {
333 my ($self, $id) = @_;
334 my $job = $self->jobs->find($id, {prefetch => ['problem', 'owner', 'contest']});
335 my %params = $job->get_columns;
336 $params{owner_name} = $job->owner->name;
337 $params{problem_name} = $job->problem->name;
338 $params{contest_name} = $job->contest->name if $params{contest};
339 $params{results} &&= decode_json $params{results};
340 $params{size} = length $params{source};
341 delete $params{source};
342 \%params
343 }
344
345 sub job_list {
346 my ($self, %args) = @_;
347 $args{page} //= 1;
348 my %where = (
349 maybe contest => $args{contest},
350 maybe owner => $args{owner},
351 maybe problem => $args{problem},
352 maybe result => $args{result},
353 );
354 $where{private} = 0 unless $args{private};
355
356 my $rows = $self->{dbis}->select('job_entry', 'COUNT(*)', \%where)->list;
357 my $pages = int (($rows + JOBS_PER_PAGE - 1) / JOBS_PER_PAGE);
358 my ($stmt, @bind) = $self->{dbis}->abstract->select('job_entry', '*', \%where, {-desc => 'id'});
359 my $jobs = $self->{dbis}->query("$stmt LIMIT " . JOBS_PER_PAGE . ' OFFSET ' . ($args{page} - 1) * JOBS_PER_PAGE, @bind)->hashes;
360 my %ret = (
361 log => $jobs,
362 current_page => $args{page},
363 last_page => $pages,
364 );
365 $self->add_names($ret{log});
366 $ret{previous_page} = $args{page} - 1 if $args{page} - 1;
367 $ret{next_page} = $args{page} + 1 if $args{page} < $pages;
368
369 \%ret;
370 }
371
372 sub job_entry {
373 my ($self, $id) = @_;
374 my $ret = $self->query(job_entry_sth => $id)->hash;
375 $ret->{results} &&= decode_json $ret->{results};
376 $self->add_names($ret);
377 }
378
379 sub job_full {
380 my ($self, $id) = @_;
381 scalar $self->query(job_full_sth => $id)->hash
382 }
383
384 sub create_job {
385 my ($self, %args) = @_;
386 $self->{dbis}->update('users', {lastjob => time});
387 purge '/log/';
388 scalar $self->{dbis}->insert('jobs', \%args, {returning => 'id'})->list
389 }
390
391 sub calc_score {
392 my ($mxscore, $time, $tries, $totaltime) = @_;
393 my $score = $mxscore;
394 $time = 0 if $time < 0;
395 $time = 300 if $time > $totaltime;
396 $score = ($totaltime - $time) / $totaltime * $score;
397 $score -= $tries / 10 * $mxscore;
398 $score = $mxscore * 3 / 10 if $score < $mxscore * 3 / 10;
399 int $score + 0.5
400 }
401
402 sub standings {
403 my ($self, $ct) = @_;
404 $ct = $self->contest_entry($ct);
405
406 my @problems = $self->query(contest_problems_sth => $ct->{id})->flat;
407 my $pblist = $self->problem_list;
408 my %values = $self->query('problem_values_sth')->map;
409 # $values{$_} = $values{$_}->{value} for keys %values;
410
411 my (%scores, %tries, %opens);
412 my $opens = $self->query(opens_sth => $ct->{id});
413 while ($opens->into(my ($problem, $owner, $time))) {
414 $opens{$problem, $owner} = $time;
415 }
416
417 my $jobs = $self->{dbis}->select('job_entry', '*', {contest => $ct->{id}}, 'id');
418
419 while (my $job = $jobs->hash) {
420 my $open = $opens{$job->{problem}, $job->{owner}} // $ct->{start};
421 my $time = $job->{date} - $open;
422 next if $time < 0;
423 my $value = $values{$job->{problem}};
424 my $factor = $job->{result} ? 0 : 1;
425 $factor = $1 / 100 if $job->{result_text} =~ /^(\d+ )/s;
426 $scores{$job->{owner}}{$job->{problem}} = int ($factor * calc_score ($value, $time, $tries{$job->{owner}}{$job->{problem}}++, $ct->{stop} - $ct->{start}));
427 }
428
429 my @st = sort { $b->{score} <=> $a->{score} or $a->{user} cmp $b->{user} } map { ## no critic (ProhibitReverseSortBlock)
430 my $user = $_;
431 +{
432 user => $user,
433 user_name => $self->object_name(users => $user),
434 score => sum (values %{$scores{$user}}),
435 scores => [map { $scores{$user}{$_} // '-'} @problems],
436 }
437 } keys %scores;
438
439 $st[0]->{rank} = 1 if @st;
440 $st[$_]->{rank} = $st[$_ - 1]->{rank} + ($st[$_]->{score} < $st[$_ - 1]->{score}) for 1 .. $#st;
441 +{
442 st => \@st,
443 problems => [map { [ $_, $self->object_name(problems => $_)] } @problems],
444 }
445 }
446
447 sub update_status_orig {
448 my ($self) = @_;
449 my @jobs = $self->jobs->search({'me.private' => 0}, {cache => 1, prefetch => 'problem', order_by => 'me.id'})->all;
450
451 my %private;
452 my %hash;
453 for (@jobs) {
454 my $pb = $_->get_column('problem');
455 $private{$pb} //= $_->problem->private;
456 next if $private{$pb};
457 $hash{$pb, $_->get_column('owner')} = [$_->id, $_->result ? 0 : 1];
458 }
459
460 my @problem_statuses = map { [split ($;), @{$hash{$_}} ] } keys %hash;
461
462 my @contest_statuses = map {
463 my $contest = $_->id;
464 map { [$contest, $_->{user}, $_->{score}, $_->{rank}] } $_->standings
465 } $self->contests->all;
466
467 my $txn = sub {
468 $self->problem_statuses->delete;
469 $self->problem_statuses->populate([[qw/problem owner job solved/], @problem_statuses]);
470 $self->contest_statuses->delete;
471 $self->contest_statuses->populate([[qw/contest owner score rank/], @contest_statuses]);
472 };
473
474 $self->txn_do($txn);
475 }
476
477 sub update_status {
478 my ($self) = @_;
479 my $jobs = $self->{dbis}->select('jobs', 'id,owner,problem,result', {}, 'id');
480
481 my %hash;
482 while ($jobs->into(my ($id, $owner, $problem, $result))) {
483 $hash{$problem, $owner} = [$id, $result ? 0 : 1];
484 }
485
486 my @problem_statuses = map { [split ($;), @{$hash{$_}} ] } keys %hash;
487
488 my @contest_statuses = map {
489 my $ct = $_;
490 map { [$ct, $_->{user}, $_->{score}, $_->{rank}] } @{$self->standings($ct)->{st}}
491 } $self->{dbis}->select('contests', 'id')->flat;
492
493 $self->{dbis}->begin;
494 $self->{dbis}->delete('problem_status');
495 $self->{dbis}->query('INSERT INTO problem_status (problem,owner,job,solved) VALUES (??)', @$_) for @problem_statuses;
496 $self->{dbis}->delete('contest_status');
497 $self->{dbis}->query('INSERT INTO contest_status (contest,owner,score,rank) VALUES (??)', @$_) for @contest_statuses;
498 $self->{dbis}->commit
499 }
500
501 my @PURGE_HOSTS = exists $ENV{PURGE_HOSTS} ? split ' ', $ENV{PURGE_HOSTS} : ();
502 my $ht = HTTP::Tiny->new;
503
504 sub purge {
505 $ht->request(PURGE => "http://$_$_[0]") for @PURGE_HOSTS;
506 }
507
508 1;
509
510 __END__
511
512 =encoding utf-8
513
514 =head1 NAME
515
516 Gruntmaster::Data - Gruntmaster 6000 Online Judge -- database interface and tools
517
518 =head1 SYNOPSIS
519
520 my $db = Gruntmaster::Data->connect('dbi:Pg:');
521
522 my $problem = $db->problem('my_problem');
523 $problem->update({timeout => 2.5}); # Set time limit to 2.5 seconds
524 $problem->rerun; # And rerun all jobs for this problem
525
526 # ...
527
528 my $contest = $db->contests->create({ # Create a new contest
529 id => 'my_contest',
530 name => 'My Awesome Contest',
531 start => time + 100,
532 end => time + 1900,
533 });
534 $db->contest_problems->create({ # Add a problem to the contest
535 contest => 'my_contest',
536 problem => 'my_problem',
537 });
538
539 say 'The contest has not started yet' if $contest->is_pending;
540
541 # ...
542
543 my @jobs = $db->jobs->search({contest => 'my_contest', owner => 'MGV'})->all;
544 $_->rerun for @jobs; # Rerun all jobs sent by MGV in my_contest
545
546 =head1 DESCRIPTION
547
548 Gruntmaster::Data is the interface to the Gruntmaster 6000 database. Read the L<DBIx::Class> documentation for usage information.
549
550 In addition to the typical DBIx::Class::Schema methods, this module contains several convenience methods:
551
552 =over
553
554 =item contests
555
556 Equivalent to C<< $schema->resultset('Contest') >>
557
558 =item contest_problems
559
560 Equivalent to C<< $schema->resultset('ContestProblem') >>
561
562 =item jobs
563
564 Equivalent to C<< $schema->resultset('Job') >>
565
566 =item problems
567
568 Equivalent to C<< $schema->resultset('Problem') >>
569
570 =item users
571
572 Equivalent to C<< $schema->resultset('User') >>
573
574 =item contest($id)
575
576 Equivalent to C<< $schema->resultset('Contest')->find($id) >>
577
578 =item job($id)
579
580 Equivalent to C<< $schema->resultset('Job')->find($id) >>
581
582 =item problem($id)
583
584 Equivalent to C<< $schema->resultset('Problem')->find($id) >>
585
586 =item user($id)
587
588 Equivalent to C<< $schema->resultset('User')->find($id) >>
589
590 =item user_list
591
592 Returns a list of users as an arrayref containing hashrefs.
593
594 =item user_entry($id)
595
596 Returns a hashref with information about the user $id.
597
598 =item problem_list([%args])
599
600 Returns a list of problems grouped by level. A hashref with levels as keys.
601
602 Takes the following arguments:
603
604 =over
605
606 =item owner
607
608 Only show problems owned by this user
609
610 =item contest
611
612 Only show problems in this contest
613
614 =back
615
616 =item problem_entry($id, [$contest, $user])
617
618 Returns a hashref with information about the problem $id. If $contest and $user are present, problem open data is updated.
619
620 =item contest_list([%args])
621
622 Returns a list of contests grouped by state. A hashref with the following keys:
623
624 =over
625
626 =item pending
627
628 An arrayref of hashrefs representing pending contests
629
630 =item running
631
632 An arrayref of hashrefs representing running contests
633
634 =item finished
635
636 An arrayref of hashrefs representing finished contests
637
638 =back
639
640 Takes the following arguments:
641
642 =over
643
644 =item owner
645
646 Only show contests owned by this user.
647
648 =back
649
650 =item contest_entry($id)
651
652 Returns a hashref with information about the contest $id.
653
654 =item job_list([%args])
655
656 Returns a list of jobs as an arrayref containing hashrefs. Takes the following arguments:
657
658 =over
659
660 =item owner
661
662 Only show jobs submitted by this user.
663
664 =item contest
665
666 Only show jobs submitted in this contest.
667
668 =item problem
669
670 Only show jobs submitted for this problem.
671
672 =item page
673
674 Show this page of results. Defaults to 1. Pages have 10 entries, and the first page has the most recent jobs.
675
676 =back
677
678 =item job_entry($id)
679
680 Returns a hashref with information about the job $id.
681
682 =back
683
684 =head1 AUTHOR
685
686 Marius Gavrilescu E<lt>marius@ieval.roE<gt>
687
688 =head1 COPYRIGHT AND LICENSE
689
690 Copyright (C) 2014 by Marius Gavrilescu
691
692 This library is free software; you can redistribute it and/or modify
693 it under the same terms as Perl itself, either Perl version 5.18.1 or,
694 at your option, any later version of Perl 5 you may have available.
695
696
697 =cut
This page took 0.066954 seconds and 5 git commands to generate.