+a/account.en
+a/account.en.title
+a/index.en
+a/index.en.title
+app.psgi
Changes
-gruntmaster-genallpages
-gruntmaster-genpage
-lib/Gruntmaster/Page/Common.pm
-lib/Gruntmaster/Page/Ct/Entry.pm
-lib/Gruntmaster/Page/Ct.pm
-lib/Gruntmaster/Page/Index.pm
-lib/Gruntmaster/Page/Log/Entry.pm
+css/custom.css
+css/orig/cerulean.css
+css/orig/cosmo.css
+css/orig/cyborg.css
+css/orig/slate.css
+css/themes/cerulean.css
+css/themes/cosmo.css
+css/themes/cyborg.css
+css/themes/slate.css
+js/00-zepto.js
+js/10-bootstrap-dropdown.js
+js/90-custom.js
+js/90-form.js
+lib/Gruntmaster/Page/Base.pm
+lib/Gruntmaster/Page/CSS.pm
+lib/Gruntmaster/Page/Generic.pm
+lib/Gruntmaster/Page/JS.pm
lib/Gruntmaster/Page/Log.pm
+lib/Gruntmaster/Page/Passwd.pm
lib/Gruntmaster/Page/Pb/Entry.pm
-lib/Gruntmaster/Page/Pb.pm
-lib/Gruntmaster/Page.pm
+lib/Gruntmaster/Page/Register.pm
+lib/Gruntmaster/Page/Src.pm
+lib/Gruntmaster/Page/St.pm
lib/Gruntmaster/Page/Submit.pm
+lib/Plack/App/Gruntmaster.pm
+log.conf
Makefile.PL
MANIFEST
README
+run
+static/gm.css
t/Gruntmaster-Page.t
+t/mech.t
+tmpl/ct.en
+tmpl/ct_entry.en
+tmpl/footer.en
+tmpl/header.en
+tmpl/log.en
+tmpl/log_entry.en
+tmpl/pb.en
+tmpl/pb_entry.en
+tmpl/st.en
+tmpl/us.en
+tmpl/us_entry.en
+
use ExtUtils::MakeMaker;
WriteMakefile(
- NAME => 'Gruntmaster::Page',
- VERSION_FROM => 'lib/Gruntmaster/Page.pm',
- ABSTRACT_FROM => 'lib/Gruntmaster/Page.pm',
- AUTHOR => 'Marius Gavrilescu <marius@ieval.ro>',
- MIN_PERL_VERSION => '5.14.0',
- LICENSE => 'perl',
- SIGN => 1,
- PREREQ_PM => {
- qw/Fcntl 0
- File::Basename 0
- IO::File 0
- POSIX 0
+ NAME => 'Gruntmaster::Page',
+ VERSION_FROM => 'lib/Plack/App/Gruntmaster.pm',
+ ABSTRACT_FROM => 'lib/Plack/App/Gruntmaster.pm',
+ AUTHOR => 'Marius Gavrilescu <marius@ieval.ro>',
+ MIN_PERL_VERSION => '5.14.0',
+ LICENSE => 'perl',
+ SIGN => 1,
+ BUILD_REQUIRES => {
+ qw/Test::More 0
+ Test::WWW::Mechanize::PSGI 0/,
+ },
+ PREREQ_PM => {
+ qw/Carp 0
+ Digest::SHA 0
+ List::Util 0
+ POSIX 0
+ constant 0
+ feature 0
+ parent 0
+ strict 0
+ warnings 0
- File::Slurp 0
- HTML::Template::Compiled 0
- IO::Compress::Gzip 0
- YAML::Any 0/,
- },
- META_MERGE => {
- dynamic_config => 0,
- }
-);
+ Apache2::Authen::Passphrase 0
+ Apache2::AuthzCaps 0
+ CSS::Minifier::XS 0
+ File::Slurp 0
+ Gruntmaster::Data 0
+ HTML::Template::Compiled 0
+ HTTP::Negotiate 0
+ JavaScript::Minifier::XS 0
+ JSON 0
+ Log::Log4perl 0
+ LWP::UserAgent 0
+ Plack::Builder 0
+ Plack::Request 0/,
+ },
+ META_MERGE => {
+ dynamic_config => 0,
+ }
+)
sub admin_required {
local $_ = $_[0];
- return 1 if m,^/pb/$word, && problem_private $1;
- return 1 if m,^/log/(?:job|src)/$word, && job_private $1;
- return 1 if m,^/ct/$word/(?:pb|log), && time < contest_start $1;
- return 1 if m,^/ct/$word/log/src, && time < contest_end $1;
+ return problem_owner $1 if m,^/pb/$word, && problem_private $1;
+ return job_owner $1 if m,^/log/(?:job|src)/$word, && job_private $1;
+ return contest_owner $1 if m,^/ct/$word/(?:pb|log), && time < contest_start $1;
+ if (m,^/ct/$word/log/(?:job|src)/$word, && time < contest_end $1){
+ local $Gruntmaster::Data::contest = $1;
+ return job_owner $2;
+ }
0
}
local *__ANON__ = "require_admin_middleware";
my $env = $_[0];
my $r = Plack::Request->new($env);
- $env->{'gruntmaster.reqadmin'} = 1 if admin_required $r->path;
+ $env->{'gruntmaster.reqadmin'} = admin_required $r->path;
$app->($env)
}
}
};
$authen_cache{$cache_key} = time;
- return if $env->{'gruntmaster.reqadmin'} && !hascaps $user, 'gmadm';
+ return if $env->{'gruntmaster.reqadmin'} && $env->{'gruntmaster.reqadmin'} ne $user && !hascaps $user, 'gmadm';
1
}
use 5.014000;
use strict;
use warnings;
+our $VERSION = '0.001';
use File::Slurp qw/read_file/;
use HTML::Template::Compiled;
use Gruntmaster::Data ();
use List::Util ();
use LWP::UserAgent;
+use Plack::Request ();
+use feature ();
my $ua = LWP::UserAgent->new;
my %templates;
-sub import {
- my $caller = caller;
- my ($self, $name, $title) = @_;
+use Carp qw/cluck/;
+
+sub import_to {
+ my ($self, $caller, $name, $title) = @_;
+ strict->import;
+ feature->import(':5.14');
+ warnings->import;
+ File::Slurp->export_to_level(1, $caller, qw/read_file/);
Gruntmaster::Data->export_to_level(1, $caller);
List::Util->export_to_level(1, $caller, qw/sum/);
no strict 'refs';
+ *{"${caller}::ISA"} = [__PACKAGE__];
+ *{"${caller}::VERSION"} = $VERSION;
*{"${caller}::strftime"} = \&POSIX::strftime;
*{"${caller}::debug"} = sub {
local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 1;
}
}
+sub import {
+ return unless $_[0] eq __PACKAGE__;
+ splice @_, 1, 0, scalar caller;
+ goto &import_to
+}
+
##################################################
sub generate{
my ($self, $lang, @args) = @_;
- my $htc = HTML::Template::Compiled->new(scalarref => \$templates{$self}{$lang}, default_escape => 'HTML',);
+ my $htc = HTML::Template::Compiled->new(scalarref => \$templates{$self}{$lang}, default_escape => 'HTML', use_perl => 1);
$self->_generate($htc, $lang, @args);
my $out = $htc->output;
utf8::downgrade($out);
sub _generate {}
-sub vary {}
+sub vary { '' }
sub max_age { 60 }
sub variants {
+ return [] unless exists $templates{$_[0]};
[ map { [ $_, 1, 'text/html', undef, undef, $_, undef ]} keys $templates{$_[0]} ]
}
package Gruntmaster::Page::CSS;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base;
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-use File::Slurp qw/read_file/;
use CSS::Minifier::XS qw/minify/;
sub generate{
[200, ['Content-Type' => 'text/css', 'Cache-Control' => 'public, max-age=604800', 'X-Forever' => 1], [minify $css] ]
}
-sub variants{ [[css => 1, 'text/css', undef, undef, undef, undef]] }
-
1
+++ /dev/null
-package Gruntmaster::Page::Ct;
-
-use 5.014000;
-use strict;
-use warnings;
-use Gruntmaster::Page::Base ct => 'Contests';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-sub _generate{
- my ($self, $htc, $lang, $env) = @_;
- debug $env => "language is '$lang'";
-
- my (@running, @pending, @finished);
- for (sort {contest_start $a <=> contest_start $b}contests) {
- my $ct = { id => $_,
- name => contest_name,
- start => strftime ('%c', localtime contest_start),
- end => strftime ('%c', localtime contest_end),
- owner => contest_owner };
-
- my $time = time;
- push @pending, $ct if time < contest_start;
- push @running, $ct if time >= contest_start && time < contest_end;
- push @finished, $ct if time > contest_end;
- }
-
- $htc->param(running => \@running) if @running;
- $htc->param(pending => \@pending) if @pending;
- $htc->param(finished => \@finished) if @finished;
-}
-
-1
+++ /dev/null
-package Gruntmaster::Page::Ct::Entry;
-
-use 5.014000;
-use strict;
-use warnings;
-use Gruntmaster::Page::Base ct_entry => '<tmpl_var name>';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-sub _generate{
- my ($self, $htc, $lang, $env, $id) = @_;
- debug $env => "language is '$lang' and id is '$id'";
-
- $htc->param(id => $id);
- $htc->param(name => contest_name $id);
- $htc->param(start => strftime '%c', localtime contest_start $id);
- $htc->param(end => strftime '%c', localtime contest_end $id);
- $htc->param(started => time >= contest_start $id);
-}
-
-1
--- /dev/null
+package Gruntmaster::Page::Generic;
+
+use 5.014000;
+use strict;
+use warnings;
+our $VERSION = '0.001';
+
+use Gruntmaster::Data;
+use Gruntmaster::Page::Base;
+use JSON qw/encode_json decode_json/;
+
+sub hgetall {
+ my $hash = shift;
+ my $cp = $Gruntmaster::Data::contest ? "contest.$Gruntmaster::Data::contest." : '';
+ map { { id => $_, HGETALL "$cp$hash.$_" } } SMEMBERS "$cp$hash"
+}
+
+sub putsym {
+ my ($key, $value) = @_;
+ no strict 'refs';
+ *{"$key"} = $value;
+}
+
+sub makepkg {
+ my ($pkg, $id, $title) = @_;
+ my $fn = $pkg =~ s,::,/,gr;
+ return if $INC{"$fn.pm"};
+ $INC{"$fn.pm"} = 1;
+ Gruntmaster::Page::Base->import_to($pkg, $id, $title);
+ 1
+}
+
+sub list {
+ my ($thing, $lang, $env, $ct) = @_;
+ my %thing = %$thing;
+ undef $ct unless $thing{contest};
+ debug $env => "Contest is $ct";
+ local $Gruntmaster::Data::contest = $ct if $ct;
+ my @thing = hgetall $thing{hash};
+ @thing = map { $thing{mangle}->(); $_ } @thing if exists $thing{mangle};
+ @thing = grep { $thing{choose}->() } @thing if exists $thing{choose};
+ @thing = sort { $thing{sortby}->() } @thing if exists $thing{sortby};
+ my %params;
+ $thing{group} //= sub { $thing{id} };
+ for (@thing) {
+ my $group = $thing{group}->();
+ $params{$group} //= [];
+ push $params{$group}, $_
+ }
+ wantarray ? %params : \%params
+}
+
+sub entry {
+ my ($thing, $lang, $env, $id, $ct) = @_;
+ my %thing = %$thing;
+ ($id, $ct) = ($ct, $id) if $thing{contest};
+ local $Gruntmaster::Data::contest = $ct if $ct;
+ debug $env => "Hash is $thing{hash} and id is $id";
+ my %params = HGETALL "$thing{hash}.$id";
+ $thing{mangle}->(local $_ = \%params) if exists $thing{mangle};
+ wantarray ? %params : \%params
+}
+
+sub headers ($) { ['Content-Type' => 'application/json', 'Cache-Control' => 'max-age=' . $_[0]->max_age] }
+
+sub create_thing {
+ my %thing = @_;
+ my $ucid = ucfirst $thing{id};
+ my $pkg = "Gruntmaster::Page::$ucid";
+
+ putsym "${pkg}::_generate", sub { $_[1]->param(list \%thing, @_[2..$#_]) } if makepkg $pkg, @thing{qw/id title/};
+ putsym "${pkg}::Entry::_generate", sub { $_[1]->param(entry \%thing, @_[2..$#_]) } if makepkg "${pkg}::Entry", "$thing{id}_entry", '<tmpl_var name>';
+ putsym "${pkg}::Read::generate", sub { [200, headers shift, [encode_json list \%thing, @_]] } if makepkg "${pkg}::Read";
+ putsym "${pkg}::Entry::Read::generate", sub { [200, headers shift, [encode_json entry \%thing, @_]] } if makepkg "${pkg}::Entry::Read";
+}
+
+sub params;
+sub contest;
+sub choose (&);
+sub sortby (&);
+sub group (&);
+sub mangle (&);
+
+sub thing (&){
+ my %thing;
+ no strict 'refs';
+ local *{"params"} = sub { @thing{qw/id hash title/} = @_ };
+ local *{"choose"} = sub { $thing{choose} = shift };
+ local *{"sortby"} = sub { $thing{sortby} = shift };
+ local *{"mangle"} = sub { $thing{mangle} = shift };
+ local *{"group"} = sub { $thing{group} = shift };
+ local *{"contest"} = sub { $thing{contest} = 1 };
+ use strict 'refs';
+
+ shift->();
+ create_thing %thing
+}
+
+##################################################
+
+thing {
+ params qw/us user Users/;
+ choose { $_->{name} =~ /\w/ };
+ sortby { lc $a->{name} cmp lc $b->{name} };
+};
+
+thing {
+ params qw/pb problem Problems/;
+ contest;
+ sortby { $a->{name} cmp $b->{name} };
+ group { $_->{level} };
+};
+
+thing {
+ params qw/ct contest Contests/;
+ sortby { $a->{start} <=> $b->{start} };
+ group { time < $_->{start} ? 'pending' : time > $_->{end} ? 'finished' : 'running' };
+ mangle { $_->{started} = time >= $_->{start} };
+};
+
+thing {
+ params qw/log job/, 'Job log';
+ contest;
+ mangle { $_->{results} &&= decode_json $_->{results}; }
+};
+
+1
package Gruntmaster::Page::JS;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base;
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-use File::Slurp qw/read_file/;
use JavaScript::Minifier::XS qw/minify/;
sub generate{
[200, ['Content-Type' => 'application/javascript', 'Cache-Control' => 'public, max-age=604800', 'X-Forever' => 1], [minify $js] ]
}
-sub variants{ [[js => 1, 'application/javascript', undef, undef, undef, undef]] }
-
1
package Gruntmaster::Page::Log;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base log => 'Job log';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
use constant PAGE_SIZE => 10;
+++ /dev/null
-package Gruntmaster::Page::Log::Entry;
-
-use 5.014000;
-use strict;
-use warnings;
-use Gruntmaster::Page::Base log_entry => 'Job <tmpl_var id>';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-sub _generate{
- my ($self, $htc, $lang, $env, $ct, $id) = @_;
- debug $env => "language is '$lang', contest is '$ct' and id is '$id'";
- local $Gruntmaster::Data::contest = $ct if $ct;
-
- my @tests = ();
-
- eval {
- @tests = map {
- $_->{time} = sprintf "%.4fs", $_->{time};
- $_
- } @{job_results $id};
- };
-
- $htc->param(id => $id);
- $htc->param(tests => \@tests);
- $htc->param(errors => job_errors $id)
-}
-
-1
package Gruntmaster::Page::Passwd;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base;
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
use Apache2::Authen::Passphrase qw/pwcheck pwset/;
-use Plack::Request;
sub generate{
my ($self, $format, $env) = @_;
reply 'Password changed successfully';
}
-sub variants{ [[reply => 1, undef, undef, undef, undef, undef]] }
-
1
+++ /dev/null
-package Gruntmaster::Page::Pb;
-
-use 5.014000;
-use strict;
-use warnings;
-use Gruntmaster::Page::Base pb => 'Problems';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-sub _generate{
- my ($self, $htc, $lang, $env, $ct) = @_;
- debug $env => "language is '$lang' and contest is '$ct'";
- local $Gruntmaster::Data::contest = $ct if $ct;
-
- my @problems = sort { $b->{name} cmp $a->{name} } map +{
- id => $_,
- name => problem_name,
- level => problem_level}, problems;
-
- for my $d (qw/beginner easy medium advanced hard/) {
- $htc->param($d => [grep {$_->{level} and $_->{level} eq $d} @problems]);
- }
- $htc->param(levels => grep { $_->{level} } @problems);
- $htc->param(problems => \@problems);
-}
-
-1
package Gruntmaster::Page::Pb::Entry;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base pb_entry => '<tmpl_var name>';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
use constant FORMATS => [qw/C CPP JAVA PERL PYTHON/];
package Gruntmaster::Page::Register;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base;
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
use Apache2::Authen::Passphrase qw/pwcheck pwset USER_REGEX/;
-use Plack::Request;
sub generate{
my ($self, $format, $env) = @_;
reply 'Registered successfully';
}
-sub variants{ [[reply => 1, undef, undef, undef, undef, undef]] }
-
1
package Gruntmaster::Page::Src;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base;
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
use constant CONTENT_TYPES => +{
c => 'text/x-csrc',
[200, ['Content-Type' => CONTENT_TYPES->{$ext}, 'Cache-Control' => 'max-age=604800', 'X-Forever' => 1], [job_inmeta($job)->{files}{prog}{content}] ]
}
-sub variants{ [[file => 1, undef, undef, undef, undef, undef]] }
-
1
package Gruntmaster::Page::St;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base st => 'Standings';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
use constant LEVEL_VALUES => {
beginner => 100,
$score = ($totaltime - $timetaken) / $totaltime * $score;
$score -= $tries / 10 * $mxscore;
$score = $mxscore * 3 / 10 if $score < $mxscore * 3 / 10;
- int $score
+ int $score + 0.5
}
sub _generate{
@problems = sort @problems;
my (%scores, %tries);
for (1 .. jobcard) {
+ next unless defined job_user && defined job_problem && defined job_result;
+ next if $Gruntmaster::Data::contest && job_date() < $start;
+
if ($Gruntmaster::Data::contest) {
- $scores{job_user()}{job_problem()} = job_result() ? 0 : calc_score (job_user(), job_problem(), job_date(), $tries{job_user()}{job_problem()}++, $totaltime) if job_date() > $start;
- } elsif (defined job_user && defined job_problem && defined job_result) {
- if (defined job_result_text && job_result_text =~ m/^(\scores+)/) {
- $scores{job_user()}{job_problem()} = $ct;
- } else {
- $scores{job_user()}{job_problem()} = job_result() ? 0 : 100;
- }
+ $scores{job_user()}{job_problem()} = job_result() ? 0 : calc_score (job_user(), job_problem(), job_date(), $tries{job_user()}{job_problem()}, $totaltime);
+ $tries{job_user()}{job_problem()}++;
+ } else {
+ no warnings 'numeric';
+ $scores{job_user()}{job_problem()} = 0 + job_result_text() || (job_result() ? 0 : 100)
}
}
package Gruntmaster::Page::Submit;
-use 5.014000;
-use strict;
-use warnings;
use Gruntmaster::Page::Base;
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-use Apache2::Authen::Passphrase qw/pwcheck pwset USER_REGEX/;
-use File::Slurp qw/read_file/;
-use Plack::Request;
use constant FORMAT_EXTENSION => {
C => 'c',
[303, [Location => $r->path =~ s,/pb/\w+/submit$,/log/,r], ['']]
}
-sub variants{ [[reply => 1, undef, undef, undef, undef, undef]] }
-
1
+++ /dev/null
-package Gruntmaster::Page::Us;
-
-use 5.014000;
-use strict;
-use warnings;
-use Gruntmaster::Page::Base us => 'Users';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-sub _generate{
- my ($self, $htc, $lang, $env) = @_;
- debug $env => "language is '$lang'";
-
- $htc->param(users => [ sort { lc $a->{name} cmp lc $b->{name} }
- map { {id => $_, name => user_name} }
- grep { user_name =~ /\w/ } users ]);
-}
-
-1
+++ /dev/null
-package Gruntmaster::Page::Us::Entry;
-
-use 5.014000;
-use strict;
-use warnings;
-use Gruntmaster::Page::Base us_entry => '<tmpl_var name>';
-our @ISA = qw/Gruntmaster::Page::Base/;
-our $VERSION = '0.001';
-
-sub _generate{
- my ($self, $htc, $lang, $env, $us) = @_;
- debug $env => "language is '$lang', user is '$us'";
- local $_ = $us;
-
- $htc->param(name => user_name);
- $htc->param(town => user_town);
- $htc->param(university => user_university);
- $htc->param(level => user_level);
-}
-
-1
use File::Slurp qw/read_file/;
use HTTP::Negotiate qw/choose/;
use Plack::Request;
+use Gruntmaster::Page::Log;
+use Gruntmaster::Page::Pb::Entry;
+use Gruntmaster::Page::Generic;
my %handlers;
my $word = qr,(\w+),a;
my $ct = qr,(?:\/ct/$word)?,a;
+ sub generic {
+ my ($thing, $ct, $fs) = @_;
+ $ct //= '', $fs //= '';
+ my $pkg = ucfirst $thing;
+ get qr,$ct/$thing/, => $pkg;
+ get qr,$ct/$thing/$word$fs, => "${pkg}::Entry";
+
+ get qr,$ct/$thing/read, => "${pkg}::Read";
+# post qr,$ct/$thing/$word/create, => "${pkg}::Entry::Create";
+ get qr,$ct/$thing/$word/read, => "${pkg}::Entry::Read";
+# post qr,$ct/$thing/$word/update, => "${pkg}::Entry::Update";
+# post qr,$ct/$thing/$word/delete, => "${pkg}::Entry::Delete";
+ }
+
get qr,/css/$word\.css, => 'CSS';
get qr,/js\.js, => 'JS';
- get qr,/ct/, => 'Ct';
- get qr,/ct/$word/, => 'Ct::Entry';
- get qr,/us/, => 'Us';
- get qr,/us/$word, => 'Us::Entry';
+ generic 'us';
+ generic ct => '', '/';
+ generic pb => $ct;
+ #generic log => $ct;
get qr,$ct/log/(\d+)?, => 'Log';
get qr,$ct/log/st, => 'St';
get qr,$ct/log/job/$word, => 'Log::Entry';
+ get qr,$ct/log/job/$word/read, => 'Log::Entry::Read';
get qr,$ct/log/src/$word\.$word, => 'Src';
- get qr,$ct/pb/, => 'Pb';
- get qr,$ct/pb/$word, => 'Pb::Entry';
post qr,$ct/pb/$word/submit, => 'Submit';
post qr,/action/register, => 'Register';
--- /dev/null
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+use Test::More tests => 5;
+use Test::WWW::Mechanize::PSGI;
+
+my $mech = Test::WWW::Mechanize::PSGI->new(app => do 'app.psgi');
+$mech->get_ok('/');
+$mech->title_is('Gruntmaster 6000');
+
+$mech->get_ok('/pb/');
+$mech->title_is('Problems');
+$mech->content_contains('Spell');
<tr><th>Name<th>Start date<th>End date<th>Owner
<tbody>
<tmpl_loop running><tr><td><a href="<tmpl_var id>/"><tmpl_var name></a>
-<td><tmpl_var start>
-<td><tmpl_var end>
+<td><%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{start}; %>
+<td><%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{end}; %>
<td><tmpl_var owner>
</tmpl_loop>
</table>
<tr><th>Name<th>Start date<th>End date<th>Owner
<tbody>
<tmpl_loop pending><tr><td><a href="<tmpl_var id>/"><tmpl_var name></a>
-<td><tmpl_var start>
-<td><tmpl_var end>
+<td><%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{start}; %>
+<td><%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{end}; %>
<td><tmpl_var owner>
</tmpl_loop>
</table>
<tr><th>Name<th>Start date<th>End date<th>Owner
<tbody>
<tmpl_loop finished><tr><td><a href="<tmpl_var id>/"><tmpl_var name></a>
-<td><tmpl_var start>
-<td><tmpl_var end>
+<td><%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{start}; %>
+<td><%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{end}; %>
<td><tmpl_var owner>
</tmpl_loop>
</table>
-Contest start time: <tmpl_var start><br>
-Contest end time: <tmpl_var end><p>
+Contest start time: <%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{start}; %><br>
+Contest end time: <%perl __OUT__ POSIX::strftime '%c', localtime __CURRENT__->{end}; %><p>
<tmpl_if started><a href="pb/">Problems</a><br>
<a href="log/">Job log</a><br>
-Compiler output:
+<tmpl_if errors>
+<h2>Compiler output</h2>
<pre><tmpl_var errors></pre>
+</tmpl_if>
-Results:
+<tmpl_if results>
+<h2>Results</h2>
<table border class="table table-border table-striped">
<thead>
<tr><th>Test number<th>Result<th>Time
<tbody>
-<tmpl_loop tests><tr><td><tmpl_var id><td class="r<tmpl_var result>"><tmpl_var result_text><td><tmpl_var time>
+<tmpl_loop results><tr><td><tmpl_var id><td class="r<tmpl_var result>"><tmpl_var result_text><td><%perl __OUT__ sprintf "%.4fs", __CURRENT__->{time}; %>
</tmpl_loop>
</table>
+</tmpl_if>
\ No newline at end of file
-<tmpl_if levels>
+<tmpl_if beginner>
<h2>Beginner</h2>
<div class="list-group">
<tmpl_loop beginner><a class="list-group-item" href="<tmpl_var id>"><tmpl_var name></a>
</tmpl_loop></div>
+</tmpl_if>
+<tmpl_if beginner>
<h2>Easy</h2>
<div class="list-group">
<tmpl_loop easy><a class="list-group-item" href="<tmpl_var id>"><tmpl_var name></a>
</tmpl_loop></div>
+</tmpl_if>
+<tmpl_if beginner>
<h2>Medium</h2>
<div class="list-group">
<tmpl_loop medium><a class="list-group-item" href="<tmpl_var id>"><tmpl_var name></a>
</tmpl_loop></div>
+</tmpl_if>
+<tmpl_if beginner>
<h2>Hard</h2>
<div class="list-group">
<tmpl_loop hard><a class="list-group-item" href="<tmpl_var id>"><tmpl_var name></a>
</tmpl_loop></div>
-
-<tmpl_else>
-<div class="list-group">
-<tmpl_loop problems><a class="list-group-item" href="<tmpl_var id>"><tmpl_var name></a>
-</tmpl_loop></div>
</tmpl_if>
-<div class="list-group"><tmpl_loop users><a class="list-group-item" href="<tmpl_var id>"><tmpl_var name></a>
+<div class="list-group"><tmpl_loop us><a class="list-group-item" href="<tmpl_var id>"><tmpl_var name></a>
</tmpl_loop></ol>