From 0a4028620288ea0af5138de45f6dafdd0d4e9ea5 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 4 Mar 2009 10:59:14 +0000 Subject: [PATCH] * Put the project-related actions in a separate controller. Put the 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//jobstatus for free. --- .../lib/Hydra/Base/Controller/ListBuilds.pm | 38 +++ src/Hydra/lib/Hydra/Base/Controller/Nix.pm | 2 +- src/Hydra/lib/Hydra/Controller/Build.pm | 6 +- src/Hydra/lib/Hydra/Controller/Project.pm | 237 +++++++++++++ src/Hydra/lib/Hydra/Controller/Root.pm | 311 +----------------- src/Hydra/lib/Hydra/Helper/CatalystUtils.pm | 70 +++- src/Hydra/root/error.tt | 8 +- src/Hydra/root/layout.tt | 2 +- src/Hydra/root/project.tt | 2 +- 9 files changed, 363 insertions(+), 313 deletions(-) create mode 100644 src/Hydra/lib/Hydra/Base/Controller/ListBuilds.pm create mode 100644 src/Hydra/lib/Hydra/Controller/Project.pm diff --git a/src/Hydra/lib/Hydra/Base/Controller/ListBuilds.pm b/src/Hydra/lib/Hydra/Base/Controller/ListBuilds.pm new file mode 100644 index 00000000..363b2438 --- /dev/null +++ b/src/Hydra/lib/Hydra/Base/Controller/ListBuilds.pm @@ -0,0 +1,38 @@ +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; diff --git a/src/Hydra/lib/Hydra/Base/Controller/Nix.pm b/src/Hydra/lib/Hydra/Base/Controller/Nix.pm index ed6e6efb..29bd1a39 100644 --- a/src/Hydra/lib/Hydra/Base/Controller/Nix.pm +++ b/src/Hydra/lib/Hydra/Base/Controller/Nix.pm @@ -2,7 +2,7 @@ package Hydra::Base::Controller::Nix; use strict; use warnings; -use parent 'Catalyst::Controller'; +use base 'Catalyst::Controller'; use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; diff --git a/src/Hydra/lib/Hydra/Controller/Build.pm b/src/Hydra/lib/Hydra/Controller/Build.pm index b706847a..dfb08473 100644 --- a/src/Hydra/lib/Hydra/Controller/Build.pm +++ b/src/Hydra/lib/Hydra/Controller/Build.pm @@ -119,7 +119,7 @@ sub runtimedeps : Chained('build') PathPart('runtime-deps') { $c->stash->{current_view} = 'Hydra::View::NixDepGraph'; $c->stash->{storePaths} = [$build->outpath]; - $c->response->content_type('image/png'); # !!! + $c->res->content_type('image/png'); # !!! } @@ -134,7 +134,7 @@ sub buildtimedeps : Chained('build') PathPart('buildtime-deps') { $c->stash->{current_view} = 'Hydra::View::NixDepGraph'; $c->stash->{storePaths} = [$build->drvpath]; - $c->response->content_type('image/png'); # !!! + $c->res->content_type('image/png'); # !!! } @@ -183,7 +183,7 @@ sub restart : Chained('build') PathPart('restart') Args(0) { $c->flash->{afterRestart} = "Build has been restarted."; - $c->response->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures)); + $c->res->redirect($c->uri_for($self->action_for("view_build"), $c->req->captures)); } diff --git a/src/Hydra/lib/Hydra/Controller/Project.pm b/src/Hydra/lib/Hydra/Controller/Project.pm new file mode 100644 index 00000000..0226b7ef --- /dev/null +++ b/src/Hydra/lib/Hydra/Controller/Project.pm @@ -0,0 +1,237 @@ +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; diff --git a/src/Hydra/lib/Hydra/Controller/Root.pm b/src/Hydra/lib/Hydra/Controller/Root.pm index 6cbb4bc7..181dfb1a 100644 --- a/src/Hydra/lib/Hydra/Controller/Root.pm +++ b/src/Hydra/lib/Hydra/Controller/Root.pm @@ -3,6 +3,7 @@ package Hydra::Controller::Root; use strict; use warnings; use base 'Hydra::Base::Controller::Nix'; +use base 'Hydra::Base::Controller::ListBuilds'; use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; @@ -18,34 +19,6 @@ sub begin :Private { } -sub trim { - my $s = shift; - $s =~ s/^\s+|\s+$//g; - return $s; -} - - -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; -} - - sub index :Path :Args(0) { my ($self, $c) = @_; $c->stash->{template} = 'index.tt'; @@ -90,71 +63,6 @@ sub queue :Local { } -# 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 releasesets :Local { my ($self, $c, $projectName) = @_; $c->stash->{template} = 'releasesets.tt'; @@ -312,216 +220,6 @@ sub release :Local { } -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$/; - - my $nixExprInput = trim $c->request->params->{"jobset-$baseName-nixexprinput"}; - die "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"}; - 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 job :Local { my ($self, $c, $projectName, $jobName) = @_; $c->stash->{template} = 'job.tt'; @@ -558,6 +256,13 @@ sub nix : Chained('/') PathPart('channel/latest') CaptureArgs(0) { } +# Hydra::Base::Controller::ListBuilds needs this. +sub get_builds : Chained('/') PathPart('') CaptureArgs(0) { + my ($self, $c) = @_; + $c->stash->{allBuilds} = $c->model('DB::Builds'); +} + + sub default :Path { my ($self, $c) = @_; notFound($c, "Page not found."); diff --git a/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm b/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm index 58d9e81e..456a2bb8 100644 --- a/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm +++ b/src/Hydra/lib/Hydra/Helper/CatalystUtils.pm @@ -6,8 +6,10 @@ use Readonly; our @ISA = qw(Exporter); our @EXPORT = qw( - getBuild error notFound - requireLogin requireProjectOwner + getBuild getBuildStats getLatestBuilds + error notFound + requireLogin requireProjectOwner requireAdmin + trim $pathCompRE $relPathRE ); @@ -19,6 +21,51 @@ sub getBuild { } +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 error { my ($c, $msg) = @_; $c->error($msg); @@ -46,11 +93,28 @@ sub requireProjectOwner { requireLogin($c) if !$c->user_exists; - error($c, "Only the project owner or the administrator can perform this operation.") + error($c, "Only the project owner or administrators can perform this operation.") unless $c->check_user_roles('admin') || $c->user->username eq $project->owner->username; } +sub requireAdmin { + my ($c) = @_; + + requireLogin($c) if !$c->user_exists; + + error($c, "Only administrators can perform this operation.") + unless $c->check_user_roles('admin'); +} + + +sub trim { + my $s = shift; + $s =~ s/^\s+|\s+$//g; + return $s; +} + + # Security checking of filenames. Readonly::Scalar our $pathCompRE => "(?:[A-Za-z0-9-\+][A-Za-z0-9-\+\._]*)"; Readonly::Scalar our $relPathRE => "(?:$pathCompRE(?:\/$pathCompRE)*)"; diff --git a/src/Hydra/root/error.tt b/src/Hydra/root/error.tt index f194d5bc..1538415d 100644 --- a/src/Hydra/root/error.tt +++ b/src/Hydra/root/error.tt @@ -3,6 +3,12 @@

[% IF httpStatus %][% httpStatus %][% ELSE %]Error[% END %]

-

I'm very sorry, but an error occurred: [% FOREACH error IN errors %][% HTML.escape(error) %][% END %]

+

I'm very sorry, but the following error(s) occurred:

+ + [% END %] diff --git a/src/Hydra/root/layout.tt b/src/Hydra/root/layout.tt index a46e7f45..04b213c6 100644 --- a/src/Hydra/root/layout.tt +++ b/src/Hydra/root/layout.tt @@ -132,7 +132,7 @@ [% ELSE %] [% INCLUDE makeLink uri = c.uri_for('/login') title = "Login" %] [% END %] - [% INCLUDE makeLink uri = c.uri_for('/createproject') title = "Create project" %] + [% INCLUDE makeLink uri = c.uri_for('/create-project') title = "Create project" %] diff --git a/src/Hydra/root/project.tt b/src/Hydra/root/project.tt index f7ce7f17..7bf8e77c 100644 --- a/src/Hydra/root/project.tt +++ b/src/Hydra/root/project.tt @@ -139,7 +139,7 @@ [% IF edit %] -
+ [% END %]