actions for viewing the job status and all builds in a separate base class that's inherited both by Root.pm and Project.pm so that we get URIs like /jobstatus and /project/<name>/jobstatus for free.
FPK5LF53CFUEKFYJ3IYXT4UTVC6IITWJOCFATMC4PLHEUP5SIEAAC U5KXLUJTRP2RQH6RNQMC35DZ4LGNEMA5KX3P4L557OU5WMEQF3VQC DSSX3RXD7ZUY7GQPLHHIGSDYHRTWJ54LANTMVTECLAK242I4NZHAC 7ADDUADTHMMPYXLMEHFYU52WQ2MENJIVOPI26652PLDAM4PA2PIAC LVAJ2TIZIWBLUELFTJQJ6OCLQNFY5P5KJHU655ZJDTPFC3JQ7CKAC V4RNHJNR3WIBINRVD5MADTLUQWMCOMWHBD43MDV2XD7S6BRLL2YAC A52HEFHQNVNF2OUSWDSUYVVXLYR2UFCGOORPCN27CJJYA4UDJA3AC K2NXFFKHZL7JUHSIIHJCWPWZOS4KUAJ7R7HH6JXC4TLLICMSVQCQC C2DCO6ASDTDVI6UZUGCLF7ZMG4X4RWG7CA5MAVCHBTZIFAYWXZQQC 5NO7NCKTMM5ZW7JYOETUFOSWK2ACTXWDZGJAFXZN6L3OF6BFTNOQC OD5FSS5AJ3XRTV5Q4UQQF6JMBT6UUL7UUOMPPUBNOM6ZMVIHW35QC LBNVQXUBEZ45SOTGVXK5UEZXIAIZTJLWZNUYFI4JZ6J65N3KPDVQC PKPWUHUXLGPQFQUTNHLVGWNT6AB3H2VMDCBKT6IPZDC53CEL4W7QC 2JQ7WEFMU5IPCUKLGRLVVT4MYMKVHJDZVMOOOPL6K5RLEZ532LGQC SHBLLAVHMMHOPCJ5NPGDZQPVRAMPMGXSFEMX7H7BETW6GTRGUUBAC J5UVLXOK6EDIL5I7VKWH4V2QDS4DPD7FHRK6XBWSXFRQS4JKXFZQC SMM4HQTPAY45254O7GQOJVLHP5LZ6BXDKGNE7IKUJHB5XKH244ZAC GCHNNFZPCYM3BPOCU5GASALJ6ONWMEVEEGVD7KKLTWC7YQKJ5KZAC IE3SRMWZ4YT6EYKAOG2HH54MXG5TYYBW7OEBHNYJUGW7KILFXX2AC JLDUSNUOOQNL63BOPXIWZOWFRQ5X35RWG33PJB3J3KMR6QR7TN7QC YTSIRIMKV2EN6CMO6LFX5J3NBJJYWBJKYDU3Z53CGXCIWP3FYI2QC 2AIIYGI54TEFYBCGWMUA7JG2DMJXBKMYUZ7CG6ETIXF4ENNP74CQC FSE6N6S3PH6YJ6VGBVLYG5P44MEV3OBB5GFX2ZKZTNXI7BLZG3NAC 2AUODJBTW2UJ47UB3SREZ7O7UTTJDQ4AGUQMPZAI4F3Y6BUF6LUQC L2E6EVE2RVFVDCUNRJ4CZYSQNS2DZUA5DTBETHBDUQUV2KQQRAOQC GC4LTHDIFUNBZET7CV6WDHA2JZ2VH4XBGLUR2R2ZOCLB2SJPU77QC IK53RV4VGOHLCZGQCCIKPB45M3C7M7YMNBOJFBGZJ4LWIZNU4QNQC W6DC6K4INJQOJYR553ISCKZV7YIOGHEM3FZQPOLAPSZQ3KSJDMRQC RU7AQO7U4HCWJNQTR2KRGDLLG24WYD47MWIHREV6SIAPCPDQHAWQC HK32XC42XOS7JKJMFT3XJKPXVSI44SFGKUUSJIL5VGPIHYNNWXLAC CS7T2XFIISZ7TPQ6FFRIDYPR6BDCQHURP4JE4YKLI4OYSNWKZCHAC YTZOC7C5N3JI3AOFPLT2UM7567XIKPSCMFKSESB6J4LNVVW3DOCAC SZZ3PFYFHYC6YUGS3VMTUZ7CKKKJHRBTA52P7ONZRSDJWW23SO5QC YAPITGB3ENS6PXRBFC647ESCQUYG442DKHUPM46PJKCKTMHWGVXAC K5BEBWKMPMTXDZTZECMJ32LCL354WMOM3RPEFWGHJFB3QH7FH6WAC 7LKUAIGCURNGNBBOAGK4EYIUF6KNWHJNLA6ZJEXFLA5HUADAJP6QC JD27RBKM7ROBUKBEQDFSCPO3MDE2AFZ6DSSKFUPJ7IC2MCGDVFGAC FHF6IZJQPUQHY5QWQYRPZVDBRLHREWRHGNKVQDT7F3GQKKLZXJKQC EYNG4EL4N7LDQD4XKVVLYCDBCRHJZIEEYU7KQWDTAVADVE34TVZQC G6HJY2V4CSSZF6BPIMRMRAJYRKPLTCBUFTJWZWK3BSNT235CZQ6QC LQNBKF3DKVYK3NPDGK7OLOTIWIF4HR3EKLRYNLZJB4G3NMIVBCEAC PBFZEQLZXMOBQIWL5WQWH7D62XG6M7ROEEAH6PWAKKKOM3W5I3RQC UVMFS73TI6RARMAAGY2UVS5LCPZUKLQECXQQVER4F7S4BNUXQQ3AC WYN733STK5DUQSWHSS6EYZK32KPZII64HLX4NS7TYUSFZ6AAFLGAC ELCI5T2ALF37VXYM5POHPO3CAMMPRB65BXGDRVUAZG53NKKLGIEAC S66BOMVUACAUDSGSDWP7ZIXVMZSQHWXOZYVTB7ILUCWZ7DDFAKVAC TQKGQ5R36VOLLJ3SR2WWIQ6VSN245N3QG4JHI2ZCP5T6RXN3PJWAC 7YBYT2LQML2PKEO6UO4444AGSASS664UCDXW2YO3ALB7THQHCEBQC package Hydra::Base::Controller::ListBuilds;use strict;use warnings;use base 'Catalyst::Controller';use Hydra::Helper::Nix;use Hydra::Helper::CatalystUtils;sub jobstatus : Chained('get_builds') PathPart Args(0) {my ($self, $c) = @_;$c->stash->{latestBuilds} = getLatestBuilds($c, $c->stash->{allBuilds}, {});}sub all : Chained('get_builds') PathPart {my ($self, $c, $page) = @_;$c->stash->{template} = 'all.tt';$page = (defined $page ? int($page) : 1) || 1;my $resultsPerPage = 50;my $nrBuilds = scalar($c->stash->{allBuilds}->search({finished => 1}));$c->stash->{baseUri} = $c->uri_for($self->action_for("all"), $c->req->captures);$c->stash->{page} = $page;$c->stash->{resultsPerPage} = $resultsPerPage;$c->stash->{totalBuilds} = $nrBuilds;$c->stash->{builds} = [$c->stash->{allBuilds}->search({finished => 1}, {order_by => "timestamp DESC", rows => $resultsPerPage, page => $page})];}1;
package Hydra::Controller::Project;use strict;use warnings;use base 'Hydra::Base::Controller::ListBuilds';use Hydra::Helper::Nix;use Hydra::Helper::CatalystUtils;__PACKAGE__->config->{namespace} = '';sub project : Chained('/') PathPart('project') CaptureArgs(1) {my ($self, $c, $projectName) = @_;my $project = $c->model('DB::Projects')->find($projectName);notFound($c, "Project $projectName doesn't exist.") unless defined $project;$c->stash->{curProject} = $project;}sub view : Chained('project') PathPart('') Args(0) {my ($self, $c) = @_;$c->stash->{template} = 'project.tt';getBuildStats($c, scalar $c->stash->{curProject}->builds);}sub edit : Chained('project') PathPart Args(0) {my ($self, $c) = @_;requireProjectOwner($c, $c->stash->{curProject});$c->stash->{template} = 'project.tt';$c->stash->{edit} = 1;}sub submit : Chained('project') PathPart Args(0) {my ($self, $c) = @_;requireProjectOwner($c, $c->stash->{curProject});error($c, "Request must be POSTed.") if $c->request->method ne "POST";$c->model('DB')->schema->txn_do(sub {updateProject($c, $c->stash->{curProject});});$c->res->redirect($c->uri_for($self->action_for("view"), $c->req->captures));}sub delete : Chained('project') PathPart Args(0) {my ($self, $c) = @_;requireProjectOwner($c, $c->stash->{curProject});error($c, "Request must be POSTed.") if $c->request->method ne "POST";$c->model('DB')->schema->txn_do(sub {$c->stash->{curProject}->delete;});$c->res->redirect($c->uri_for("/"));}sub create : Path('create-project') {my ($self, $c) = @_;requireAdmin($c);$c->stash->{template} = 'project.tt';$c->stash->{create} = 1;$c->stash->{edit} = 1;}sub create_submit : Path('create-project/submit') {my ($self, $c) = @_;requireAdmin($c);my $projectName = trim $c->request->params->{name};$c->model('DB')->schema->txn_do(sub {# Note: $projectName is validated in updateProject,# which will abort the transaction if the name isn't# valid. Idem for the owner.my $project = $c->model('DB::Projects')->create({name => $projectName, displayname => "", owner => trim $c->request->params->{owner}});updateProject($c, $project);});$c->res->redirect($c->uri_for($self->action_for("view"), [$projectName]));}sub updateProject {my ($c, $project) = @_;my $projectName = trim $c->request->params->{name};error($c, "Invalid project name: " . ($projectName || "(empty)")) unless $projectName =~ /^[[:alpha:]]\w*$/;my $displayName = trim $c->request->params->{displayname};error($c, "Invalid display name: $displayName") if $displayName eq "";$project->name($projectName);$project->displayname($displayName);$project->description(trim $c->request->params->{description});$project->homepage(trim $c->request->params->{homepage});$project->enabled(trim($c->request->params->{enabled}) eq "1" ? 1 : 0);if ($c->check_user_roles('admin')) {my $owner = trim $c->request->params->{owner};error($c, "Invalid owner: $owner")unless defined $c->model('DB::Users')->find({username => $owner});$project->owner($owner);}$project->update;my %jobsetNames;foreach my $param (keys %{$c->request->params}) {next unless $param =~ /^jobset-(\w+)-name$/;my $baseName = $1;next if $baseName eq "template";my $jobsetName = trim $c->request->params->{"jobset-$baseName-name"};error($c, "Invalid jobset name: $jobsetName") unless $jobsetName =~ /^[[:alpha:]]\w*$/;# The Nix expression path must be relative and can't contain ".." elements.my $nixExprPath = trim $c->request->params->{"jobset-$baseName-nixexprpath"};error($c, "Invalid Nix expression path: $nixExprPath") if $nixExprPath !~ /^$relPathRE$/;my $nixExprInput = trim $c->request->params->{"jobset-$baseName-nixexprinput"};error($c, "Invalid Nix expression input name: $nixExprInput") unless $nixExprInput =~ /^\w+$/;$jobsetNames{$jobsetName} = 1;my $jobset;my $description = trim $c->request->params->{"jobset-$baseName-description"};if ($baseName =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry$jobset = $project->jobsets->create({ name => $jobsetName, description => $description, nixexprpath => $nixExprPath, nixexprinput => $nixExprInput});} else { # it's an existing jobset$jobset = ($project->jobsets->search({name => $baseName}))[0];die unless defined $jobset;$jobset->name($jobsetName);$jobset->description($description);$jobset->nixexprpath($nixExprPath);$jobset->nixexprinput($nixExprInput);$jobset->update;}my %inputNames;# Process the inputs of this jobset.foreach my $param (keys %{$c->request->params}) {next unless $param =~ /^jobset-$baseName-input-(\w+)-name$/;my $baseName2 = $1;next if $baseName2 eq "template";print STDERR "GOT INPUT: $baseName2\n";my $inputName = trim $c->request->params->{"jobset-$baseName-input-$baseName2-name"};error($c, "Invalid input name: $inputName") unless $inputName =~ /^[[:alpha:]]\w*$/;my $inputType = trim $c->request->params->{"jobset-$baseName-input-$baseName2-type"};error($c, "Invalid input type: $inputType") unless$inputType eq "svn" || $inputType eq "cvs" || $inputType eq "tarball" ||$inputType eq "string" || $inputType eq "path" || $inputType eq "boolean";$inputNames{$inputName} = 1;my $input;if ($baseName2 =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry$input = $jobset->jobsetinputs->create({ name => $inputName, type => $inputType});} else { # it's an existing jobset$input = ($jobset->jobsetinputs->search({name => $baseName2}))[0];die unless defined $input;$input->name($inputName);$input->type($inputType);$input->update;}# Update the values for this input. Just delete all the# current ones, then create the new values.$input->jobsetinputalts->delete_all;my $values = $c->request->params->{"jobset-$baseName-input-$baseName2-values"};$values = [] unless defined $values;$values = [$values] unless ref($values) eq 'ARRAY';my $altnr = 0;foreach my $value (@{$values}) {print STDERR "VALUE: $value\n";my $value = trim $value;error($c, "Invalid Boolean value: $value") if$inputType eq "boolean" && !($value eq "true" || $value eq "false");$input->jobsetinputalts->create({altnr => $altnr++, value => $value});}}# Get rid of deleted inputs.my @inputs = $jobset->jobsetinputs->all;foreach my $input (@inputs) {$input->delete unless defined $inputNames{$input->name};}}# Get rid of deleted jobsets, i.e., ones that are no longer submitted in the parameters.my @jobsets = $project->jobsets->all;foreach my $jobset (@jobsets) {$jobset->delete unless defined $jobsetNames{$jobset->name};}}# Hydra::Base::Controller::ListBuilds needs this.sub get_builds : Chained('project') PathPart('') CaptureArgs(0) {my ($self, $c) = @_;$c->stash->{allBuilds} = $c->stash->{curProject}->builds;}1;
sub getBuildStats {my ($c, $builds) = @_;$c->stash->{finishedBuilds} = $builds->search({finished => 1}) || 0;$c->stash->{succeededBuilds} = $builds->search({finished => 1, buildStatus => 0},{join => 'resultInfo'}) || 0;$c->stash->{scheduledBuilds} = $builds->search({finished => 0}) || 0;$c->stash->{busyBuilds} = $builds->search({finished => 0, busy => 1},{join => 'schedulingInfo'}) || 0;$c->stash->{totalBuildTime} = $builds->search({},{join => 'resultInfo', select => {sum => 'stoptime - starttime'}, as => ['sum']})->first->get_column('sum') || 0;}
}# Return the latest build for each job.sub getLatestBuilds {my ($c, $builds, $extraAttrs) = @_;my @res = ();foreach my $job ($builds->search({},{group_by => ['project', 'attrname', 'system']})){my $attrs ={ project => $job->get_column('project'), attrname => $job->attrname, system => $job->system, finished => 1};my ($build) = $builds->search({ %$attrs, %$extraAttrs },{ join => 'resultInfo', order_by => 'timestamp DESC', rows => 1 } );push @res, $build if defined $build;}return [@res];}sub showJobStatus {my ($c, $builds) = @_;$c->stash->{template} = 'jobstatus.tt';# Get the latest finished build for each unique job.$c->stash->{latestBuilds} = getLatestBuilds($c, $builds, {});}sub jobstatus :Local {my ($self, $c) = @_;showJobStatus($c, $c->model('DB::Builds'));}sub showAllBuilds {my ($c, $baseUri, $page, $builds) = @_;$c->stash->{template} = 'all.tt';$page = (defined $page ? int($page) : 1) || 1;my $resultsPerPage = 50;my $nrBuilds = scalar($builds->search({finished => 1}));$c->stash->{baseUri} = $baseUri;$c->stash->{page} = $page;$c->stash->{resultsPerPage} = $resultsPerPage;$c->stash->{totalBuilds} = $nrBuilds;$c->stash->{builds} = [$builds->search({finished => 1}, {order_by => "timestamp DESC", rows => $resultsPerPage, page => $page})];}sub all :Local {my ($self, $c, $page) = @_;showAllBuilds($c, $c->uri_for("/all"), $page, $c->model('DB::Builds'));
sub updateProject {my ($c, $project) = @_;my $projectName = trim $c->request->params->{name};die "Invalid project name: $projectName" unless $projectName =~ /^[[:alpha:]]\w*$/;my $displayName = trim $c->request->params->{displayname};die "Invalid display name: $displayName" if $displayName eq "";$project->name($projectName);$project->displayname($displayName);$project->description(trim $c->request->params->{description});$project->homepage(trim $c->request->params->{homepage});$project->enabled(trim($c->request->params->{enabled}) eq "1" ? 1 : 0);if ($c->check_user_roles('admin')) {my $owner = trim $c->request->params->{owner};die "Invalid owner: $owner"unless defined $c->model('DB::Users')->find({username => $owner});$project->owner($owner);}$project->update;my %jobsetNames;foreach my $param (keys %{$c->request->params}) {next unless $param =~ /^jobset-(\w+)-name$/;my $baseName = $1;next if $baseName eq "template";my $jobsetName = trim $c->request->params->{"jobset-$baseName-name"};die "Invalid jobset name: $jobsetName" unless $jobsetName =~ /^[[:alpha:]]\w*$/;# The Nix expression path must be relative and can't contain ".." elements.my $nixExprPath = trim $c->request->params->{"jobset-$baseName-nixexprpath"};die "Invalid Nix expression path: $nixExprPath" if $nixExprPath !~ /^$relPathRE$/;
$jobsetNames{$jobsetName} = 1;my $jobset;my $description = trim $c->request->params->{"jobset-$baseName-description"};if ($baseName =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry$jobset = $project->jobsets->create({ name => $jobsetName, description => $description, nixexprpath => $nixExprPath, nixexprinput => $nixExprInput});} else { # it's an existing jobset$jobset = ($project->jobsets->search({name => $baseName}))[0];die unless defined $jobset;$jobset->name($jobsetName);$jobset->description($description);$jobset->nixexprpath($nixExprPath);$jobset->nixexprinput($nixExprInput);$jobset->update;}my %inputNames;# Process the inputs of this jobset.foreach my $param (keys %{$c->request->params}) {next unless $param =~ /^jobset-$baseName-input-(\w+)-name$/;my $baseName2 = $1;next if $baseName2 eq "template";print STDERR "GOT INPUT: $baseName2\n";my $inputName = trim $c->request->params->{"jobset-$baseName-input-$baseName2-name"};die "Invalid input name: $inputName" unless $inputName =~ /^[[:alpha:]]\w*$/;my $inputType = trim $c->request->params->{"jobset-$baseName-input-$baseName2-type"};die "Invalid input type: $inputType" unless$inputType eq "svn" || $inputType eq "cvs" || $inputType eq "tarball" ||$inputType eq "string" || $inputType eq "path" || $inputType eq "boolean";$inputNames{$inputName} = 1;my $input;if ($baseName2 =~ /^\d+$/) { # numeric base name is auto-generated, i.e. a new entry$input = $jobset->jobsetinputs->create({ name => $inputName, type => $inputType});} else { # it's an existing jobset$input = ($jobset->jobsetinputs->search({name => $baseName2}))[0];die unless defined $input;$input->name($inputName);$input->type($inputType);$input->update;}# Update the values for this input. Just delete all the# current ones, then create the new values.$input->jobsetinputalts->delete_all;my $values = $c->request->params->{"jobset-$baseName-input-$baseName2-values"};$values = [] unless defined $values;$values = [$values] unless ref($values) eq 'ARRAY';my $altnr = 0;foreach my $value (@{$values}) {print STDERR "VALUE: $value\n";my $value = trim $value;die "Invalid Boolean value: $value" if$inputType eq "boolean" && !($value eq "true" || $value eq "false");$input->jobsetinputalts->create({altnr => $altnr++, value => $value});}}# Get rid of deleted inputs.my @inputs = $jobset->jobsetinputs->all;foreach my $input (@inputs) {$input->delete unless defined $inputNames{$input->name};}}# Get rid of deleted jobsets, i.e., ones that are no longer submitted in the parameters.my @jobsets = $project->jobsets->all;foreach my $jobset (@jobsets) {$jobset->delete unless defined $jobsetNames{$jobset->name};}}sub project :Local {my ($self, $c, $projectName, $subcommand, $arg) = @_;$c->stash->{template} = 'project.tt';my $project = $c->model('DB::Projects')->find($projectName);notFound($c, "Project $projectName doesn't exist.") if !defined $project;my $isPosted = $c->request->method eq "POST";$c->stash->{curProject} = $project;$subcommand = "" unless defined $subcommand;if ($subcommand eq "jobstatus") {return showJobStatus($c, scalar $project->builds);}elsif ($subcommand eq "all") {return showAllBuilds($c, $c->uri_for("/project", $projectName, "all"),$arg, scalar $project->builds);}elsif ($subcommand ne "") {requireProjectOwner($c, $project);if ($subcommand eq "edit") {$c->stash->{edit} = 1;}elsif ($subcommand eq "submit" && $isPosted) {$c->model('DB')->schema->txn_do(sub {updateProject($c, $project);});return $c->res->redirect($c->uri_for("/project", $project->name));}elsif ($subcommand eq "delete" && $isPosted) {$c->model('DB')->schema->txn_do(sub {$project->delete;});return $c->res->redirect($c->uri_for("/"));}else {error($c, "Unknown subcommand $subcommand.");}}getBuildStats($c, scalar $project->builds);$c->stash->{jobNames} =[$c->model('DB::Builds')->search({project => $projectName}, {select => [{distinct => 'attrname'}], as => ['attrname']})];}sub createproject :Local {my ($self, $c, $subcommand) = @_;requireLogin($c) if !$c->user_exists;error($c, "Only administrators can create projects.")unless $c->check_user_roles('admin');if (defined $subcommand && $subcommand eq "submit") {my $projectName = trim $c->request->params->{name};$c->model('DB')->schema->txn_do(sub {# Note: $projectName is validated in updateProject,# which will abort the transaction if the name isn't# valid. Idem for the owner.my $project = $c->model('DB::Projects')->create({name => $projectName, displayname => "", owner => trim $c->request->params->{owner}});updateProject($c, $project);});return $c->res->redirect($c->uri_for("/project", $projectName));}$c->stash->{template} = 'project.tt';$c->stash->{create} = 1;$c->stash->{edit} = 1;}
}sub getBuildStats {my ($c, $builds) = @_;$c->stash->{finishedBuilds} = $builds->search({finished => 1}) || 0;$c->stash->{succeededBuilds} = $builds->search({finished => 1, buildStatus => 0},{join => 'resultInfo'}) || 0;$c->stash->{scheduledBuilds} = $builds->search({finished => 0}) || 0;$c->stash->{busyBuilds} = $builds->search({finished => 0, busy => 1},{join => 'schedulingInfo'}) || 0;$c->stash->{totalBuildTime} = $builds->search({},{join => 'resultInfo', select => {sum => 'stoptime - starttime'}, as => ['sum']})->first->get_column('sum') || 0;}# Return the latest build for each job.sub getLatestBuilds {my ($c, $builds, $extraAttrs) = @_;my @res = ();foreach my $job ($builds->search({},{group_by => ['project', 'attrname', 'system']})){my $attrs ={ project => $job->get_column('project'), attrname => $job->attrname, system => $job->system, finished => 1};my ($build) = $builds->search({ %$attrs, %$extraAttrs },{ join => 'resultInfo', order_by => 'timestamp DESC', rows => 1 } );push @res, $build if defined $build;}return [@res];
<p>I'm very sorry, but an error occurred: <span class="error-msg">[% FOREACH error IN errors %][% HTML.escape(error) %][% END %]</span></p>
<p>I'm very sorry, but the following error(s) occurred:</p><ul>[% FOREACH error IN errors %]<li><div class="error-msg">[% HTML.escape(error) %]</div></li>[% END %]</ul>
<form action="[% IF create %][% c.uri_for('/createproject/submit') %][% ELSE %][% c.uri_for('/project' curProject.name 'submit') %][% END %]" method="post">
<form action="[% IF create %][% c.uri_for('/create-project/submit') %][% ELSE %][% c.uri_for('/project' curProject.name 'submit') %][% END %]" method="post">