If a project is not approved, it will show up in the list only when
viewed by a user with project admin rights. In the actions column will
appear links to either approve or reject the project.
If the project admin clicks on the "Approve" link, then the project is
marked as approved. Any user can then view it.
If the project admin clicks on the "Reject" link, then the project is
deleted.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/projects_controller.rb | 54 +++++++++++-
app/models/project.rb | 7 +-
app/views/projects/index.html.erb | 23 ++++--
config/routes.rb | 7 ++-
doc/ChangeLog | 9 ++-
public/images/icons/approve.png | Bin 0 -> 619 bytes
public/images/icons/reject.png | Bin 0 -> 601 bytes
test/fixtures/projects.yml | 7 ++
test/fixtures/user_privileges.yml | 6 ++
test/fixtures/users.yml | 7 ++
test/functional/projects_controller_test.rb | 121 ++++++++++++++++++++-------
test/unit/project_test.rb | 19 ++++-
12 files changed, 214 insertions(+), 46 deletions(-)
create mode 100644 public/images/icons/approve.png
create mode 100644 public/images/icons/reject.png
diff --git a/app/controllers/projects_controller.rb
b/app/controllers/projects_controller.rb
index 109224f..7a29e96 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -19,9 +19,11 @@
# of +Product+s.
#
class ProjectsController < ApplicationController
- before_filter :authenticated, :only => [:new, :create, :edit, :update]
- before_filter :load_project, :except => [:index, :new, :create, :destroy]
- before_filter :setup_for_edit, :only => [:new, :create, :edit]
+ before_filter :authenticated, :except => [:index, :show]
+ before_filter :load_project, :except => [:index, :new, :create, :destroy]
+ before_filter :filter_approved, :only => [:approve, :reject]
+ before_filter :can_approve, :only => [:approve, :reject]
+ before_filter :setup_for_edit, :only => [:new, :create, :edit]
# GET /projects
def index
@@ -125,10 +127,34 @@ class ProjectsController < ApplicationController
end
end
+ # POST /project/1/approve
+ def approve
+ Project.transaction do
+ @project.approved = true
+ @project.save!
+
+ flash[:message] = "#{(a)project.name} has been approved."
+ respond_to do |format|
+ format.html { redirect_to project_path(@project) }
+ end
+ end
+ end
+
+ # POST /project/1/reject
+ def reject
+ Project.transaction do
+ @project.destroy
+
+ flash[:message] = "#{(a)project.name} has been rejected/deleted."
+ respond_to do |format|
+ format.html {redirect_to projects_path}
+ end
+ end
+ end
+
private
# Loads the requested project.
- #
def load_project
@project = Project.find_by_id(params[:id])
@@ -147,4 +173,24 @@ class ProjectsController < ApplicationController
@users = [@user]
end
end
+
+ # Ensures that the project is unapproved.
+ def filter_approved
+ if @project.approved
+ flash[:message] = "This project has already been approved."
+ respond_to do |format|
+ format.html { redirect_to project_path(@project)}
+ end
+ end
+ end
+
+ # Ensures that the user can approve projects.
+ def can_approve
+ unless @project.can_approve?(@user)
+ flash[:message] = "You do not have project administration rights."
+ respond_to do |format|
+ format.html {redirect_to error_path}
+ end
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index fd4556c..a31e710 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -35,14 +35,17 @@ class Project < ActiveRecord::Base
has_many :products
# Returns whether the user can modify this project.
- #
def can_edit?(user)
user && (user.privileges.admin_projects || (user.id == owner.id))
end
# Returns whether the user can create products for this project.
- #
def can_create_products?(user)
(user != nil) && (user.id == owner.id)
end
+
+ # Returns whether the user can approve the project.
+ def can_approve?(user)
+ (user != nil) && user.privileges.admin_projects
+ end
end
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index d6b5447..828abb3 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -56,12 +56,23 @@
<td><%= link_to project.owner.display_name, user_path(project.owner)
%></td>
<td><%= show_date(project.created_at) %></td>
<td>
- <%= link_to(
- image_tag("icons/view.png", :title => "View this
project..."),
- project_path(project)) %>
- <%= link_to(
- image_tag("icons/edit.png", :title => "Edit this
project..."),
- edit_project_path(project, :source => request.request_uri)) if
project.can_edit?(@user)%>
+ <%= link_to(image_tag("icons/view.png", :title => "View
this project..."),
+ project_path(project)) %>
+
+ <%= link_to(image_tag("icons/edit.png", :title => "Edit
this project..."),
+ edit_project_path(project, :source => request.request_uri)) if
project.can_edit?(@user)%>
+
+ <% unless project.approved %>
+ <%= link_to(image_tag("icons/approve.png", :title => "Approve
this project..."),
+ approve_project_path(project), :method => :put,
+ :confirm => "Approve this project? Are you sure?") if
project.can_approve?(@user)
+ %>
+
+ <%= link_to(image_tag("icons/reject.png", :title => "Reject
this project..."),
+ reject_project_path(project), :method => :put,
+ :confirm => "Reject this project? Are you sure?") if
project.can_approve?(@user)
+ %>
+ <% end %>
</td>
</tr>
diff --git a/config/routes.rb b/config/routes.rb
index db4de9c..c667c1f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -21,7 +21,12 @@ ActionController::Routing::Routes.draw do |map|
admin.resource :server, :controller => 'ServerSettings'
end
- map.resources :projects
+ map.resources :projects, :member =>
+ {
+ :approve => :put,
+ :reject => :put
+ }
+
map.resources :products do |product|
product.resources :roles
product.resources :stories
diff --git a/doc/ChangeLog b/doc/ChangeLog
index bf0f244..7e2abdc 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -1,3 +1,8 @@
Change Log (0.2.0):
- * #125 Added a system title to all pages. - Darryl L. Pierce <mcpierce(a)gmail.com>
- * #136 System title can be changed by the admin. - Darryl L. Pierce
<mcpierce(a)gmail.com>
+ * #97 - Users can request new projects. - Darryl L. Pierce <mcpierce(a)gmail.com>
+ * #98 - Admins can approve projects. - Darryl L. Pierce <mcpierce(a)gmail.com>
+ * #99 - Sprints can have team leads. - Darryl L. Pierce <mcpierce(a)gmail.com>
+ * #100 - Team leads can reopen items. - Darryl L. Pierce <mcpierce(a)gmail.com>
+ * #125 - Added a system title to all pages. - Darryl L. Pierce
<mcpierce(a)gmail.com>
+ * #131 - Admins can filter out unapproved projects. - Darryl L. Pierce
<mcpierce(a)gmail.com>
+ * #136 - System title can be changed by the admin. - Darryl L. Pierce
<mcpierce(a)gmail.com>
diff --git a/public/images/icons/approve.png b/public/images/icons/approve.png
new file mode 100644
index 0000000000000000000000000000000000000000..2bd16ccf294bd944c6bf17fa0345885469980820
GIT binary patch
literal 619
zcmV-x0+juUP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz{YgYYR5;6(
zlR;|}Q4q%ywK*uB`~V(A58^?=k7B`|1LCO_4}zCs4T7aDg-Qh#6xy2?LjyqywQXvQ
z8xl6Gq05F18+Mmnc701Wq*CjlKM!whj5W|6=J4Lk|2H#l=B0qtk*RS8HF|(<f#xj_
z`9|3Ah2~wNX<NiS(9NHy&dlims<?m_yv6q+?ty0Zsk##<2S5rXy3KCu7f}~N0g9=z
z>EAdBAPt<OvwxAAGlT2MDr_6d)@wGr^P>T3aD}=*%{NbB*ZGcS?IC*5jinhDl=TC)
z^a(zA>EnR9^9j#^IEMZn^!id5#SnEtL|{9h>X0|4DCpP8H?Ix=ni&xHWU$qNT21yZ
z5^*RX2qmP^fMQ*vP*nzi&BkZ)YfvhI(E`_#urPuk5&HpnzVw-Gc(=*`uwjl<Z^+W4
zt_<kL65xm61>h!P+m)3pz5ENXE;8&W2W*v~T!vOF>0KmQeQ*+S#{tWRxh00jSgfv7
zu5dK}P{{s0ADkp>$Chu@w&3}~KY;h%H7W`eKSHG<MIw^f|A^ZiXtklK!{hlM!+;g{
z3@eR06bfJ%QXB{gGEDfQ!0h57xmn2OV(#Xw<E#A2NHbr*#oXF9Z&nX^u?*8s;Nhcv
z?oWN?-eivK<W0t(j*(p$`~P2%D}5Ho@NDsNavu4aegpALNqNzBB)0$n002ovPDHLk
FV1f!F7%cz*
literal 0
HcmV?d00001
diff --git a/public/images/icons/reject.png b/public/images/icons/reject.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c832d4c83cc0f7869a83f88833699daff52fcf2
GIT binary patch
literal 601
zcmV-f0;c_mP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz>q$gGR5;6x
zld)?PVHk%KwHXvA{{RQkK^zqPQ*`Jo3NFS)5G+C!og7>$D6NEu!3M;jM7RtZQ)9g&
zq2Y3PT)1!{94U9^xG!8xj6)xXFPclGw!?4uK6oB}yu5D$NbE`yI1Hp{cTP^<iM=xa
zQIC=|1{W~BPdM%loi11{Fs(6$`4z*wk8Yetvlr0pg?WKCIz%J5M5_g12;&Ki#xNX#
z7=RyuZtP%MP&WmemNPHd9G#)v0Yw2-1<#X$Xb2*bl2FL_0lY5#m=}~ACF)xe2!#ZO
zBN)WsKtK>eKY;ujW&%y5(3%q}8&Iu+Z9^2vgn!@=`cv5NLoOc^Y;-QuG-PEVz;+}o
zcENrCULQOkUgTneivF3xTc}jz)O*_!j#u{k5_Y%2vSBq76X>=lfe@Sy)M^rlSN41<
zFBak9y3SHYoe91;AFxb0c`@LxEMB>3K(+u^@@dlfv)n11o(YQ9DYCV@ysWAe)bA*@
zA?~zt*M)Z<VJR~qy|&2pYI3hlOaez}1ji{^%UIvQ?LoN=x7VL>wRm*@RfYubyBDcy
zzi@ld(BZ+WEz+fP2fGEuIfi0A$28@nAFW2*$}b-Lm=yg4isoRq;qlX-+{iBf5B^t>
nvr??6H@Wu&uC1-?2Lp-UJe5p>RuR>I00000NkvXXu0mjfVs`!d
literal 0
HcmV?d00001
diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml
index 509017f..0929cfc 100644
--- a/test/fixtures/projects.yml
+++ b/test/fixtures/projects.yml
@@ -11,3 +11,10 @@ teatime:
url:
http://teatime.tigris.org/
description: A small app for brewing tea.
approved: false
+
+unapproved_project:
+ owner_id: <%= Fixtures.identify(:projxp_owner) %>
+ name: Some other project
+ url:
http://localhosted.com/
+ description: A project that sounds cool.
+ approved: false
diff --git a/test/fixtures/user_privileges.yml b/test/fixtures/user_privileges.yml
index ba57357..0ca79ac 100644
--- a/test/fixtures/user_privileges.yml
+++ b/test/fixtures/user_privileges.yml
@@ -6,6 +6,12 @@ admin_privileges:
admin_users: true
admin_site: true
+project_admin_privileges:
+ user_id: <%= Fixtures.identify(:project_admin) %>
+ admin_projects: true
+ admin_users: false
+ admin_site: false
+
projxp_owner_privileges:
user_id: <%= Fixtures.identify(:projxp_owner) %>
admin_projects: false
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
index 6a204c8..9981281 100644
--- a/test/fixtures/users.yml
+++ b/test/fixtures/users.yml
@@ -7,6 +7,13 @@ admin:
salt: <%= SALT %>
hashed_password: <%= User.encrypted_password('admin', SALT) %>
+project_admin:
+ email: project_admin(a)localhost.com
+ display_name: Project Administrator
+ introduction: What's the project's name?
+ salt: <%= SALT %>
+ hashed_password: <%= User.encrypted_password('project_admin', SALT) %>
+
projxp_owner:
email: admin(a)projxp.org
display_name: ProjXP Admin
diff --git a/test/functional/projects_controller_test.rb
b/test/functional/projects_controller_test.rb
index 708deb7..e0a229c 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -19,27 +19,29 @@ require File.dirname(__FILE__) + '/../test_helper'
# +ProjectsControllerTest+ performs tests on +ProjectController+ to ensure that
# it works as expected.
-#
class ProjectsControllerTest < ActionController::TestCase
fixtures :projects
fixtures :users
def setup
- @project = projects(:projxp)
- @owner = @project.owner
- @nonowner = users(:mcpierce)
+ @project = projects(:projxp)
+ @owner = @project.owner
+ @unapproved_project = projects(:unapproved_project)
+
+ raise "Unapproved project cannot be approved!" if
@unapproved_project.approved?
+
+ @nonowner = users(:mcpierce)
raise "Owner and nonowner cannot be the same user!" if @owner.id ==
@nonowner.id
@nonadmin = @nonowner
raise "Nonadmin cannot have project admin rights!" if
@nonadmin.can_create_projects?
- @admin = users(:admin)
+ @admin = users(:project_admin)
raise "Nonadmin and admin cannot be the same user!" if @nonadmin.id ==
@admin.id
raise "Admin must have project admin rights!" unless
@admin.can_create_projects?
end
# Ensures that showing the project index works as expected.
- #
def test_index
get :index
@@ -59,7 +61,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that showing a single project requires an id.
- #
def test_show_without_id
get :show
@@ -67,7 +68,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that showing a project fails on invalid ids.
- #
def test_show_with_invalid_id
get :show, {:id => 9999}
@@ -75,7 +75,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures tha show a project works as expected.
- #
def test_show
get :show, {:id => @project.id}
@@ -86,7 +85,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that an anonymous user cannot create a new project.
- #
def test_new_as_anonymous
get :new
@@ -95,7 +93,6 @@ class ProjectsControllerTest < ActionController::TestCase
# Ensures that a user without project admin privileges can
# create a project but that it is not approved.
- #
def test_new_as_nonadmin
get :new, {}, {:user_id => @nonadmin.id}
@@ -105,7 +102,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that starting off a new project works as expected.
- #
def test_new
get :new, {}, {:user_id => @admin.id}
@@ -113,7 +109,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that the url is held.
- #
def test_new_with_url
get :new, {"source" => "/farkle"}, {:user_id => @admin.id}
@@ -127,7 +122,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures an anonymous user cannot start editing a project.
- #
def test_edit_as_anonymous
get :edit
@@ -135,7 +129,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that edit fails on an invalid id.
- #
def test_edit_with_invalid_id
get :edit,
{:id => 9999},
@@ -145,7 +138,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that a nonowner cannot edit a project.
- #
def test_edit_as_nonowner
get :edit,
{:id => @project.id},
@@ -155,7 +147,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that the owner can edit a project.
- #
def test_edit
get :edit,
{:id => @project.id},
@@ -169,7 +160,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that the source url is stored.
- #
def test_edit_with_url
get :edit,
{:id => @project.id, :source => "/farkle"},
@@ -184,7 +174,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that an anonymous user cannot create a project.
- #
def test_create_as_anonymous
post :create
@@ -192,7 +181,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that a non-admin can create an unapproved project.
- #
def test_create_as_nonadmin
post :create,
{:project => {:name => 'test', :owner_id => @admin.id}},
@@ -205,7 +193,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that an invalid project returns the user to the edit page.
- #
def test_create_with_invalid_project
post :create,
{:project => {}},
@@ -215,7 +202,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that a project is created when one is submitted.
- #
def test_create
post :create,
{:project => {:name => 'test', :owner_id => @admin.id}},
@@ -228,7 +214,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures the user is returned to the origin page if a url was provided.
- #
def test_create_with_url
post :create,
{:project => {:name => 'test', :owner_id => @admin.id},
@@ -239,7 +224,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that an anonymous user cannot update a project.
- #
def test_update_as_anonymous
put :update
@@ -247,7 +231,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that a valid project id must be provided.
- #
def test_update_with_invalid_id
put :update,
{:id => 9999},
@@ -257,7 +240,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that a nonadmin cannot update a project.
- #
def test_update_by_nonowner
put :update,
{:id => @project.id,
@@ -271,7 +253,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that the owner can edit a project.
- #
def test_update
put :update,
{:id => @project.id,
@@ -285,7 +266,6 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that the user is returned to the source url.
- #
def test_update_with_url
put :update,
{:id => @project.id,
@@ -300,10 +280,91 @@ class ProjectsControllerTest < ActionController::TestCase
end
# Ensures that the destroy method just exists.
- #
def test_destroy
- delete :destroy
+ delete :destroy, {:id => @project.id}, {:user_id => @admin.id}
assert_redirected_to error_url
end
+
+ # Ensures that anonymous users can't approve projects.
+ def test_approve_as_anonymous
+ put :approve
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a project id is required to approve a project.
+ def test_approve_requires_project_id
+ put :approve, { }, {:user_id => @admin.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that a project cannot be approved by a non-admin.
+ def test_approve_requires_admin_rights
+ put :approve, {:id => @unapproved_project.id}, {:user_id => @nonadmin.id}
+
+ assert_redirected_to error_path
+ assert !Project.find_by_id((a)unapproved_project.id).approved,
+ "Project should not have been marked as approved."
+ end
+
+ # Ensures that an approved project raises an error.
+ def test_approve_project_already_approved
+ put :approve, {:id => @project.id}, {:user_id => @admin.id}
+
+ assert_redirected_to project_path(@project)
+ assert_equal @project.approved, Project.find_by_id((a)project.id).approved,
+ "Approval status should not have changed."
+ end
+
+ # Ensures that approvals work as expected.
+ def test_approve
+ put :approve, {:id => @unapproved_project.id},{:user_id => @admin.id}
+
+ assert_redirected_to project_path(@unapproved_project)
+ assert Project.find_by_id((a)unapproved_project.id).approved,
+ "Project should have been marked as approved."
+ end
+
+ # Ensures that anonymous users can't reject a project.
+ def test_reject_as_anonymous
+ put :reject
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that rejecting a project requires a valid id.
+ def test_reject_requires_a_valid_id
+ put :reject, {}, {:user_id => @admin.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that rejecting a project requires admin rights.
+ def test_reject_requries_admin_rights
+ put :reject, {:id => @unapproved_project.id}, {:user_id => @nonadmin.id}
+
+ assert_redirected_to error_path
+ assert Project.find_by_id((a)unapproved_project.id),
+ "Project should not have been rejected."
+ end
+
+ # Ensures that an approved project cannot be rejected.
+ def test_reject_project_already_approved
+ put :reject, {:id => @project.id}, {:user_id => @admin.id}
+
+ assert_redirected_to project_path(@project)
+ assert Project.find_by_id((a)project.id),
+ "Project should not have been deleted."
+ end
+
+ # Ensures that a rejected project is deleted.
+ def test_reject
+ put :reject, {:id => @unapproved_project.id}, {:user_id => @admin.id}
+
+ assert_redirected_to projects_path
+ assert_nil Project.find_by_id((a)unapproved_project.id),
+ "Rejected project should have been deleted."
+ end
end
diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb
index 55f0ecb..120a24e 100644
--- a/test/unit/project_test.rb
+++ b/test/unit/project_test.rb
@@ -22,12 +22,19 @@ class ProjectTest < ActiveSupport::TestCase
fixtures :users
def setup
- @existing_project = projects(:projxp)
+ @existing_project = projects(:projxp)
+ @unapproved_project = projects(:unapproved_project)
+ raise "Unapproved project cannot be approved!" if
@unapproved_project.approved
@owner = @existing_project.owner
@nonowner = users(:mcpierce)
raise "Owner and nonowner cannot be the same person!" if @owner.id ==
@nonowner.id
+ @admin = users(:project_admin)
+ raise "Admin must have project admin rights!" unless
@admin.privileges.admin_projects
+ @nonadmin = users(:mcpierce)
+ raise "Nonadmin must not have project admin rights!" if
@nonadmin.privileges.admin_projects
+
@new_project = Project.new(
:name => "New Projects",
:owner_id => @nonowner.id,
@@ -69,4 +76,14 @@ class ProjectTest < ActiveSupport::TestCase
def test_create_products_for_project_owner
flunk "A project owner should be able to create products." unless
@existing_project.can_create_products?(@owner)
end
+
+ # Ensures that non admins cannot approve projects.
+ def test_nonadmin_cannot_approve_projects
+ flunk "Non-admins must not be able to approve projects." if
@unapproved_project.can_approve?(@nonadmin)
+ end
+
+ # Ensures that a user with project admin rights can approve projects.
+ def test_admin_can_approve_projects
+ flunk "Admins must be able to approve projects." unless
@unapproved_project.can_approve?(@admin)
+ end
end
--
1.6.0.6