##// END OF EJS Templates
Moves enabled_module_names param to project attribute so that it can be set through the Project API....
Jean-Philippe Lang -
r4525:9fb770ba503b
parent child
Show More
@@ -1,270 +1,269
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :roadmap, :only => :roadmap
21 21 menu_item :settings, :only => :settings
22 22
23 23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 25 before_filter :authorize_global, :only => [:new, :create]
26 26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 27 accept_key_auth :index, :show, :create, :update, :destroy
28 28
29 29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 30 if controller.request.post?
31 31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
32 32 end
33 33 end
34 34
35 35 # TODO: convert to PUT only
36 36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
37 37
38 38 helper :sort
39 39 include SortHelper
40 40 helper :custom_fields
41 41 include CustomFieldsHelper
42 42 helper :issues
43 43 helper :queries
44 44 include QueriesHelper
45 45 helper :repositories
46 46 include RepositoriesHelper
47 47 include ProjectsHelper
48 48
49 49 # Lists visible projects
50 50 def index
51 51 respond_to do |format|
52 52 format.html {
53 53 @projects = Project.visible.find(:all, :order => 'lft')
54 54 }
55 55 format.api {
56 56 @offset, @limit = api_offset_and_limit
57 57 @project_count = Project.visible.count
58 58 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
59 59 }
60 60 format.atom {
61 61 projects = Project.visible.find(:all, :order => 'created_on DESC',
62 62 :limit => Setting.feeds_limit.to_i)
63 63 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
64 64 }
65 65 end
66 66 end
67 67
68 68 def new
69 69 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
70 70 @trackers = Tracker.all
71 71 @project = Project.new(params[:project])
72 72 end
73 73
74 74 def create
75 75 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
76 76 @trackers = Tracker.all
77 77 @project = Project.new
78 78 @project.safe_attributes = params[:project]
79 79
80 @project.enabled_module_names = params[:enabled_modules] if params[:enabled_modules]
81 80 if validate_parent_id && @project.save
82 81 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
83 82 # Add current user as a project member if he is not admin
84 83 unless User.current.admin?
85 84 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
86 85 m = Member.new(:user => User.current, :roles => [r])
87 86 @project.members << m
88 87 end
89 88 respond_to do |format|
90 89 format.html {
91 90 flash[:notice] = l(:notice_successful_create)
92 91 redirect_to :controller => 'projects', :action => 'settings', :id => @project
93 92 }
94 93 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
95 94 end
96 95 else
97 96 respond_to do |format|
98 97 format.html { render :action => 'new' }
99 98 format.api { render_validation_errors(@project) }
100 99 end
101 100 end
102 101
103 102 end
104 103
105 104 def copy
106 105 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
107 106 @trackers = Tracker.all
108 107 @root_projects = Project.find(:all,
109 108 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
110 109 :order => 'name')
111 110 @source_project = Project.find(params[:id])
112 111 if request.get?
113 112 @project = Project.copy_from(@source_project)
114 113 if @project
115 114 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
116 115 else
117 116 redirect_to :controller => 'admin', :action => 'projects'
118 117 end
119 118 else
120 119 Mailer.with_deliveries(params[:notifications] == '1') do
121 120 @project = Project.new
122 121 @project.safe_attributes = params[:project]
123 122 @project.enabled_module_names = params[:enabled_modules]
124 123 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
125 124 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
126 125 flash[:notice] = l(:notice_successful_create)
127 126 redirect_to :controller => 'projects', :action => 'settings', :id => @project
128 127 elsif !@project.new_record?
129 128 # Project was created
130 129 # But some objects were not copied due to validation failures
131 130 # (eg. issues from disabled trackers)
132 131 # TODO: inform about that
133 132 redirect_to :controller => 'projects', :action => 'settings', :id => @project
134 133 end
135 134 end
136 135 end
137 136 rescue ActiveRecord::RecordNotFound
138 137 redirect_to :controller => 'admin', :action => 'projects'
139 138 end
140 139
141 140 # Show @project
142 141 def show
143 142 if params[:jump]
144 143 # try to redirect to the requested menu item
145 144 redirect_to_project_menu_item(@project, params[:jump]) && return
146 145 end
147 146
148 147 @users_by_role = @project.users_by_role
149 148 @subprojects = @project.children.visible
150 149 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
151 150 @trackers = @project.rolled_up_trackers
152 151
153 152 cond = @project.project_condition(Setting.display_subprojects_issues?)
154 153
155 154 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
156 155 :include => [:project, :status, :tracker],
157 156 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
158 157 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
159 158 :include => [:project, :status, :tracker],
160 159 :conditions => cond)
161 160
162 161 TimeEntry.visible_by(User.current) do
163 162 @total_hours = TimeEntry.sum(:hours,
164 163 :include => :project,
165 164 :conditions => cond).to_f
166 165 end
167 166 @key = User.current.rss_key
168 167
169 168 respond_to do |format|
170 169 format.html
171 170 format.api
172 171 end
173 172 end
174 173
175 174 def settings
176 175 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
177 176 @issue_category ||= IssueCategory.new
178 177 @member ||= @project.members.new
179 178 @trackers = Tracker.all
180 179 @repository ||= @project.repository
181 180 @wiki ||= @project.wiki
182 181 end
183 182
184 183 def edit
185 184 end
186 185
187 186 def update
188 187 @project.safe_attributes = params[:project]
189 188 if validate_parent_id && @project.save
190 189 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
191 190 respond_to do |format|
192 191 format.html {
193 192 flash[:notice] = l(:notice_successful_update)
194 193 redirect_to :action => 'settings', :id => @project
195 194 }
196 195 format.api { head :ok }
197 196 end
198 197 else
199 198 respond_to do |format|
200 199 format.html {
201 200 settings
202 201 render :action => 'settings'
203 202 }
204 203 format.api { render_validation_errors(@project) }
205 204 end
206 205 end
207 206 end
208 207
209 208 def modules
210 209 @project.enabled_module_names = params[:enabled_modules]
211 210 flash[:notice] = l(:notice_successful_update)
212 211 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
213 212 end
214 213
215 214 def archive
216 215 if request.post?
217 216 unless @project.archive
218 217 flash[:error] = l(:error_can_not_archive_project)
219 218 end
220 219 end
221 220 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
222 221 end
223 222
224 223 def unarchive
225 224 @project.unarchive if request.post? && !@project.active?
226 225 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
227 226 end
228 227
229 228 # Delete @project
230 229 def destroy
231 230 @project_to_destroy = @project
232 231 if request.get?
233 232 # display confirmation view
234 233 else
235 234 if api_request? || params[:confirm]
236 235 @project_to_destroy.destroy
237 236 respond_to do |format|
238 237 format.html { redirect_to :controller => 'admin', :action => 'projects' }
239 238 format.api { head :ok }
240 239 end
241 240 end
242 241 end
243 242 # hide project in layout
244 243 @project = nil
245 244 end
246 245
247 246 private
248 247 def find_optional_project
249 248 return true unless params[:id]
250 249 @project = Project.find(params[:id])
251 250 authorize
252 251 rescue ActiveRecord::RecordNotFound
253 252 render_404
254 253 end
255 254
256 255 # Validates parent_id param according to user's permissions
257 256 # TODO: move it to Project model in a validation that depends on User.current
258 257 def validate_parent_id
259 258 return true if User.current.admin?
260 259 parent_id = params[:project] && params[:project][:parent_id]
261 260 if parent_id || @project.new_record?
262 261 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
263 262 unless @project.allowed_parents.include?(parent)
264 263 @project.errors.add :parent_id, :invalid
265 264 return false
266 265 end
267 266 end
268 267 true
269 268 end
270 269 end
@@ -1,838 +1,841
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_ARCHIVED = 9
24 24
25 25 # Maximum length for project identifiers
26 26 IDENTIFIER_MAX_LENGTH = 100
27 27
28 28 # Specific overidden Activities
29 29 has_many :time_entry_activities
30 30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 31 has_many :memberships, :class_name => 'Member'
32 32 has_many :member_principals, :class_name => 'Member',
33 33 :include => :principal,
34 34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 35 has_many :users, :through => :members
36 36 has_many :principals, :through => :member_principals, :source => :principal
37 37
38 38 has_many :enabled_modules, :dependent => :delete_all
39 39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 41 has_many :issue_changes, :through => :issues, :source => :journals
42 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 43 has_many :time_entries, :dependent => :delete_all
44 44 has_many :queries, :dependent => :delete_all
45 45 has_many :documents, :dependent => :destroy
46 46 has_many :news, :dependent => :delete_all, :include => :author
47 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 49 has_one :repository, :dependent => :destroy
50 50 has_many :changesets, :through => :repository
51 51 has_one :wiki, :dependent => :destroy
52 52 # Custom field for the project issues
53 53 has_and_belongs_to_many :issue_custom_fields,
54 54 :class_name => 'IssueCustomField',
55 55 :order => "#{CustomField.table_name}.position",
56 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 57 :association_foreign_key => 'custom_field_id'
58 58
59 59 acts_as_nested_set :order => 'name'
60 60 acts_as_attachable :view_permission => :view_files,
61 61 :delete_permission => :manage_files
62 62
63 63 acts_as_customizable
64 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 67 :author => nil
68 68
69 attr_protected :status, :enabled_module_names
69 attr_protected :status
70 70
71 71 validates_presence_of :name, :identifier
72 72 validates_uniqueness_of :identifier
73 73 validates_associated :repository, :wiki
74 74 validates_length_of :name, :maximum => 255
75 75 validates_length_of :homepage, :maximum => 255
76 76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 77 # donwcase letters, digits, dashes but not digits only
78 78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 79 # reserved words
80 80 validates_exclusion_of :identifier, :in => %w( new )
81 81
82 82 before_destroy :delete_all_members, :destroy_children
83 83
84 84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 86 named_scope :all_public, { :conditions => { :is_public => true } }
87 87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
88 88
89 89 def initialize(attributes = nil)
90 90 super
91 91
92 92 initialized = (attributes || {}).stringify_keys
93 93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 94 self.identifier = Project.next_identifier
95 95 end
96 96 if !initialized.key?('is_public')
97 97 self.is_public = Setting.default_projects_public?
98 98 end
99 99 if !initialized.key?('enabled_module_names')
100 100 self.enabled_module_names = Setting.default_projects_modules
101 101 end
102 102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 103 self.trackers = Tracker.all
104 104 end
105 105 end
106 106
107 107 def identifier=(identifier)
108 108 super unless identifier_frozen?
109 109 end
110 110
111 111 def identifier_frozen?
112 112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 113 end
114 114
115 115 # returns latest created projects
116 116 # non public projects will be returned only if user is a member of those
117 117 def self.latest(user=nil, count=5)
118 118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
119 119 end
120 120
121 121 # Returns a SQL :conditions string used to find all active projects for the specified user.
122 122 #
123 123 # Examples:
124 124 # Projects.visible_by(admin) => "projects.status = 1"
125 125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
126 126 def self.visible_by(user=nil)
127 127 user ||= User.current
128 128 if user && user.admin?
129 129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
130 130 elsif user && user.memberships.any?
131 131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
132 132 else
133 133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
134 134 end
135 135 end
136 136
137 137 def self.allowed_to_condition(user, permission, options={})
138 138 statements = []
139 139 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
140 140 if perm = Redmine::AccessControl.permission(permission)
141 141 unless perm.project_module.nil?
142 142 # If the permission belongs to a project module, make sure the module is enabled
143 143 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
144 144 end
145 145 end
146 146 if options[:project]
147 147 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
148 148 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
149 149 base_statement = "(#{project_statement}) AND (#{base_statement})"
150 150 end
151 151 if user.admin?
152 152 # no restriction
153 153 else
154 154 statements << "1=0"
155 155 if user.logged?
156 156 if Role.non_member.allowed_to?(permission) && !options[:member]
157 157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
158 158 end
159 159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
160 160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
161 161 else
162 162 if Role.anonymous.allowed_to?(permission) && !options[:member]
163 163 # anonymous user allowed on public project
164 164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
165 165 end
166 166 end
167 167 end
168 168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
169 169 end
170 170
171 171 # Returns the Systemwide and project specific activities
172 172 def activities(include_inactive=false)
173 173 if include_inactive
174 174 return all_activities
175 175 else
176 176 return active_activities
177 177 end
178 178 end
179 179
180 180 # Will create a new Project specific Activity or update an existing one
181 181 #
182 182 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
183 183 # does not successfully save.
184 184 def update_or_create_time_entry_activity(id, activity_hash)
185 185 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
186 186 self.create_time_entry_activity_if_needed(activity_hash)
187 187 else
188 188 activity = project.time_entry_activities.find_by_id(id.to_i)
189 189 activity.update_attributes(activity_hash) if activity
190 190 end
191 191 end
192 192
193 193 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
194 194 #
195 195 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
196 196 # does not successfully save.
197 197 def create_time_entry_activity_if_needed(activity)
198 198 if activity['parent_id']
199 199
200 200 parent_activity = TimeEntryActivity.find(activity['parent_id'])
201 201 activity['name'] = parent_activity.name
202 202 activity['position'] = parent_activity.position
203 203
204 204 if Enumeration.overridding_change?(activity, parent_activity)
205 205 project_activity = self.time_entry_activities.create(activity)
206 206
207 207 if project_activity.new_record?
208 208 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
209 209 else
210 210 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
211 211 end
212 212 end
213 213 end
214 214 end
215 215
216 216 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
217 217 #
218 218 # Examples:
219 219 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
220 220 # project.project_condition(false) => "projects.id = 1"
221 221 def project_condition(with_subprojects)
222 222 cond = "#{Project.table_name}.id = #{id}"
223 223 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
224 224 cond
225 225 end
226 226
227 227 def self.find(*args)
228 228 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
229 229 project = find_by_identifier(*args)
230 230 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
231 231 project
232 232 else
233 233 super
234 234 end
235 235 end
236 236
237 237 def to_param
238 238 # id is used for projects with a numeric identifier (compatibility)
239 239 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
240 240 end
241 241
242 242 def active?
243 243 self.status == STATUS_ACTIVE
244 244 end
245 245
246 246 def archived?
247 247 self.status == STATUS_ARCHIVED
248 248 end
249 249
250 250 # Archives the project and its descendants
251 251 def archive
252 252 # Check that there is no issue of a non descendant project that is assigned
253 253 # to one of the project or descendant versions
254 254 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
255 255 if v_ids.any? && Issue.find(:first, :include => :project,
256 256 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
257 257 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
258 258 return false
259 259 end
260 260 Project.transaction do
261 261 archive!
262 262 end
263 263 true
264 264 end
265 265
266 266 # Unarchives the project
267 267 # All its ancestors must be active
268 268 def unarchive
269 269 return false if ancestors.detect {|a| !a.active?}
270 270 update_attribute :status, STATUS_ACTIVE
271 271 end
272 272
273 273 # Returns an array of projects the project can be moved to
274 274 # by the current user
275 275 def allowed_parents
276 276 return @allowed_parents if @allowed_parents
277 277 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
278 278 @allowed_parents = @allowed_parents - self_and_descendants
279 279 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
280 280 @allowed_parents << nil
281 281 end
282 282 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
283 283 @allowed_parents << parent
284 284 end
285 285 @allowed_parents
286 286 end
287 287
288 288 # Sets the parent of the project with authorization check
289 289 def set_allowed_parent!(p)
290 290 unless p.nil? || p.is_a?(Project)
291 291 if p.to_s.blank?
292 292 p = nil
293 293 else
294 294 p = Project.find_by_id(p)
295 295 return false unless p
296 296 end
297 297 end
298 298 if p.nil?
299 299 if !new_record? && allowed_parents.empty?
300 300 return false
301 301 end
302 302 elsif !allowed_parents.include?(p)
303 303 return false
304 304 end
305 305 set_parent!(p)
306 306 end
307 307
308 308 # Sets the parent of the project
309 309 # Argument can be either a Project, a String, a Fixnum or nil
310 310 def set_parent!(p)
311 311 unless p.nil? || p.is_a?(Project)
312 312 if p.to_s.blank?
313 313 p = nil
314 314 else
315 315 p = Project.find_by_id(p)
316 316 return false unless p
317 317 end
318 318 end
319 319 if p == parent && !p.nil?
320 320 # Nothing to do
321 321 true
322 322 elsif p.nil? || (p.active? && move_possible?(p))
323 323 # Insert the project so that target's children or root projects stay alphabetically sorted
324 324 sibs = (p.nil? ? self.class.roots : p.children)
325 325 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
326 326 if to_be_inserted_before
327 327 move_to_left_of(to_be_inserted_before)
328 328 elsif p.nil?
329 329 if sibs.empty?
330 330 # move_to_root adds the project in first (ie. left) position
331 331 move_to_root
332 332 else
333 333 move_to_right_of(sibs.last) unless self == sibs.last
334 334 end
335 335 else
336 336 # move_to_child_of adds the project in last (ie.right) position
337 337 move_to_child_of(p)
338 338 end
339 339 Issue.update_versions_from_hierarchy_change(self)
340 340 true
341 341 else
342 342 # Can not move to the given target
343 343 false
344 344 end
345 345 end
346 346
347 347 # Returns an array of the trackers used by the project and its active sub projects
348 348 def rolled_up_trackers
349 349 @rolled_up_trackers ||=
350 350 Tracker.find(:all, :include => :projects,
351 351 :select => "DISTINCT #{Tracker.table_name}.*",
352 352 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
353 353 :order => "#{Tracker.table_name}.position")
354 354 end
355 355
356 356 # Closes open and locked project versions that are completed
357 357 def close_completed_versions
358 358 Version.transaction do
359 359 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
360 360 if version.completed?
361 361 version.update_attribute(:status, 'closed')
362 362 end
363 363 end
364 364 end
365 365 end
366 366
367 367 # Returns a scope of the Versions on subprojects
368 368 def rolled_up_versions
369 369 @rolled_up_versions ||=
370 370 Version.scoped(:include => :project,
371 371 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
372 372 end
373 373
374 374 # Returns a scope of the Versions used by the project
375 375 def shared_versions
376 376 @shared_versions ||=
377 377 Version.scoped(:include => :project,
378 378 :conditions => "#{Project.table_name}.id = #{id}" +
379 379 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
380 380 " #{Version.table_name}.sharing = 'system'" +
381 381 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
382 382 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
383 383 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
384 384 "))")
385 385 end
386 386
387 387 # Returns a hash of project users grouped by role
388 388 def users_by_role
389 389 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
390 390 m.roles.each do |r|
391 391 h[r] ||= []
392 392 h[r] << m.user
393 393 end
394 394 h
395 395 end
396 396 end
397 397
398 398 # Deletes all project's members
399 399 def delete_all_members
400 400 me, mr = Member.table_name, MemberRole.table_name
401 401 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
402 402 Member.delete_all(['project_id = ?', id])
403 403 end
404 404
405 405 # Users issues can be assigned to
406 406 def assignable_users
407 407 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
408 408 end
409 409
410 410 # Returns the mail adresses of users that should be always notified on project events
411 411 def recipients
412 412 notified_users.collect {|user| user.mail}
413 413 end
414 414
415 415 # Returns the users that should be notified on project events
416 416 def notified_users
417 417 # TODO: User part should be extracted to User#notify_about?
418 418 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
419 419 end
420 420
421 421 # Returns an array of all custom fields enabled for project issues
422 422 # (explictly associated custom fields and custom fields enabled for all projects)
423 423 def all_issue_custom_fields
424 424 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
425 425 end
426 426
427 427 def project
428 428 self
429 429 end
430 430
431 431 def <=>(project)
432 432 name.downcase <=> project.name.downcase
433 433 end
434 434
435 435 def to_s
436 436 name
437 437 end
438 438
439 439 # Returns a short description of the projects (first lines)
440 440 def short_description(length = 255)
441 441 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
442 442 end
443 443
444 444 def css_classes
445 445 s = 'project'
446 446 s << ' root' if root?
447 447 s << ' child' if child?
448 448 s << (leaf? ? ' leaf' : ' parent')
449 449 s
450 450 end
451 451
452 452 # The earliest start date of a project, based on it's issues and versions
453 453 def start_date
454 454 [
455 455 issues.minimum('start_date'),
456 456 shared_versions.collect(&:effective_date),
457 457 shared_versions.collect(&:start_date)
458 458 ].flatten.compact.min
459 459 end
460 460
461 461 # The latest due date of an issue or version
462 462 def due_date
463 463 [
464 464 issues.maximum('due_date'),
465 465 shared_versions.collect(&:effective_date),
466 466 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
467 467 ].flatten.compact.max
468 468 end
469 469
470 470 def overdue?
471 471 active? && !due_date.nil? && (due_date < Date.today)
472 472 end
473 473
474 474 # Returns the percent completed for this project, based on the
475 475 # progress on it's versions.
476 476 def completed_percent(options={:include_subprojects => false})
477 477 if options.delete(:include_subprojects)
478 478 total = self_and_descendants.collect(&:completed_percent).sum
479 479
480 480 total / self_and_descendants.count
481 481 else
482 482 if versions.count > 0
483 483 total = versions.collect(&:completed_pourcent).sum
484 484
485 485 total / versions.count
486 486 else
487 487 100
488 488 end
489 489 end
490 490 end
491 491
492 492 # Return true if this project is allowed to do the specified action.
493 493 # action can be:
494 494 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
495 495 # * a permission Symbol (eg. :edit_project)
496 496 def allows_to?(action)
497 497 if action.is_a? Hash
498 498 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
499 499 else
500 500 allowed_permissions.include? action
501 501 end
502 502 end
503 503
504 504 def module_enabled?(module_name)
505 505 module_name = module_name.to_s
506 506 enabled_modules.detect {|m| m.name == module_name}
507 507 end
508 508
509 509 def enabled_module_names=(module_names)
510 510 if module_names && module_names.is_a?(Array)
511 511 module_names = module_names.collect(&:to_s).reject(&:blank?)
512 512 # remove disabled modules
513 513 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
514 514 # add new modules
515 515 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
516 516 else
517 517 enabled_modules.clear
518 518 end
519 519 end
520 520
521 521 # Returns an array of the enabled modules names
522 522 def enabled_module_names
523 523 enabled_modules.collect(&:name)
524 524 end
525 525
526 526 safe_attributes 'name',
527 527 'description',
528 528 'homepage',
529 529 'is_public',
530 530 'identifier',
531 531 'custom_field_values',
532 532 'custom_fields',
533 533 'tracker_ids',
534 534 'issue_custom_field_ids'
535 535
536 safe_attributes 'enabled_module_names',
537 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
538
536 539 # Returns an array of projects that are in this project's hierarchy
537 540 #
538 541 # Example: parents, children, siblings
539 542 def hierarchy
540 543 parents = project.self_and_ancestors || []
541 544 descendants = project.descendants || []
542 545 project_hierarchy = parents | descendants # Set union
543 546 end
544 547
545 548 # Returns an auto-generated project identifier based on the last identifier used
546 549 def self.next_identifier
547 550 p = Project.find(:first, :order => 'created_on DESC')
548 551 p.nil? ? nil : p.identifier.to_s.succ
549 552 end
550 553
551 554 # Copies and saves the Project instance based on the +project+.
552 555 # Duplicates the source project's:
553 556 # * Wiki
554 557 # * Versions
555 558 # * Categories
556 559 # * Issues
557 560 # * Members
558 561 # * Queries
559 562 #
560 563 # Accepts an +options+ argument to specify what to copy
561 564 #
562 565 # Examples:
563 566 # project.copy(1) # => copies everything
564 567 # project.copy(1, :only => 'members') # => copies members only
565 568 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
566 569 def copy(project, options={})
567 570 project = project.is_a?(Project) ? project : Project.find(project)
568 571
569 572 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
570 573 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
571 574
572 575 Project.transaction do
573 576 if save
574 577 reload
575 578 to_be_copied.each do |name|
576 579 send "copy_#{name}", project
577 580 end
578 581 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
579 582 save
580 583 end
581 584 end
582 585 end
583 586
584 587
585 588 # Copies +project+ and returns the new instance. This will not save
586 589 # the copy
587 590 def self.copy_from(project)
588 591 begin
589 592 project = project.is_a?(Project) ? project : Project.find(project)
590 593 if project
591 594 # clear unique attributes
592 595 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
593 596 copy = Project.new(attributes)
594 597 copy.enabled_modules = project.enabled_modules
595 598 copy.trackers = project.trackers
596 599 copy.custom_values = project.custom_values.collect {|v| v.clone}
597 600 copy.issue_custom_fields = project.issue_custom_fields
598 601 return copy
599 602 else
600 603 return nil
601 604 end
602 605 rescue ActiveRecord::RecordNotFound
603 606 return nil
604 607 end
605 608 end
606 609
607 610 # Yields the given block for each project with its level in the tree
608 611 def self.project_tree(projects, &block)
609 612 ancestors = []
610 613 projects.sort_by(&:lft).each do |project|
611 614 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
612 615 ancestors.pop
613 616 end
614 617 yield project, ancestors.size
615 618 ancestors << project
616 619 end
617 620 end
618 621
619 622 private
620 623
621 624 # Destroys children before destroying self
622 625 def destroy_children
623 626 children.each do |child|
624 627 child.destroy
625 628 end
626 629 end
627 630
628 631 # Copies wiki from +project+
629 632 def copy_wiki(project)
630 633 # Check that the source project has a wiki first
631 634 unless project.wiki.nil?
632 635 self.wiki ||= Wiki.new
633 636 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
634 637 wiki_pages_map = {}
635 638 project.wiki.pages.each do |page|
636 639 # Skip pages without content
637 640 next if page.content.nil?
638 641 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
639 642 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
640 643 new_wiki_page.content = new_wiki_content
641 644 wiki.pages << new_wiki_page
642 645 wiki_pages_map[page.id] = new_wiki_page
643 646 end
644 647 wiki.save
645 648 # Reproduce page hierarchy
646 649 project.wiki.pages.each do |page|
647 650 if page.parent_id && wiki_pages_map[page.id]
648 651 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
649 652 wiki_pages_map[page.id].save
650 653 end
651 654 end
652 655 end
653 656 end
654 657
655 658 # Copies versions from +project+
656 659 def copy_versions(project)
657 660 project.versions.each do |version|
658 661 new_version = Version.new
659 662 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
660 663 self.versions << new_version
661 664 end
662 665 end
663 666
664 667 # Copies issue categories from +project+
665 668 def copy_issue_categories(project)
666 669 project.issue_categories.each do |issue_category|
667 670 new_issue_category = IssueCategory.new
668 671 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
669 672 self.issue_categories << new_issue_category
670 673 end
671 674 end
672 675
673 676 # Copies issues from +project+
674 677 def copy_issues(project)
675 678 # Stores the source issue id as a key and the copied issues as the
676 679 # value. Used to map the two togeather for issue relations.
677 680 issues_map = {}
678 681
679 682 # Get issues sorted by root_id, lft so that parent issues
680 683 # get copied before their children
681 684 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
682 685 new_issue = Issue.new
683 686 new_issue.copy_from(issue)
684 687 new_issue.project = self
685 688 # Reassign fixed_versions by name, since names are unique per
686 689 # project and the versions for self are not yet saved
687 690 if issue.fixed_version
688 691 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
689 692 end
690 693 # Reassign the category by name, since names are unique per
691 694 # project and the categories for self are not yet saved
692 695 if issue.category
693 696 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
694 697 end
695 698 # Parent issue
696 699 if issue.parent_id
697 700 if copied_parent = issues_map[issue.parent_id]
698 701 new_issue.parent_issue_id = copied_parent.id
699 702 end
700 703 end
701 704
702 705 self.issues << new_issue
703 706 if new_issue.new_record?
704 707 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
705 708 else
706 709 issues_map[issue.id] = new_issue unless new_issue.new_record?
707 710 end
708 711 end
709 712
710 713 # Relations after in case issues related each other
711 714 project.issues.each do |issue|
712 715 new_issue = issues_map[issue.id]
713 716 unless new_issue
714 717 # Issue was not copied
715 718 next
716 719 end
717 720
718 721 # Relations
719 722 issue.relations_from.each do |source_relation|
720 723 new_issue_relation = IssueRelation.new
721 724 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
722 725 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
723 726 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
724 727 new_issue_relation.issue_to = source_relation.issue_to
725 728 end
726 729 new_issue.relations_from << new_issue_relation
727 730 end
728 731
729 732 issue.relations_to.each do |source_relation|
730 733 new_issue_relation = IssueRelation.new
731 734 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
732 735 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
733 736 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
734 737 new_issue_relation.issue_from = source_relation.issue_from
735 738 end
736 739 new_issue.relations_to << new_issue_relation
737 740 end
738 741 end
739 742 end
740 743
741 744 # Copies members from +project+
742 745 def copy_members(project)
743 746 # Copy users first, then groups to handle members with inherited and given roles
744 747 members_to_copy = []
745 748 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
746 749 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
747 750
748 751 members_to_copy.each do |member|
749 752 new_member = Member.new
750 753 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
751 754 # only copy non inherited roles
752 755 # inherited roles will be added when copying the group membership
753 756 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
754 757 next if role_ids.empty?
755 758 new_member.role_ids = role_ids
756 759 new_member.project = self
757 760 self.members << new_member
758 761 end
759 762 end
760 763
761 764 # Copies queries from +project+
762 765 def copy_queries(project)
763 766 project.queries.each do |query|
764 767 new_query = Query.new
765 768 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
766 769 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
767 770 new_query.project = self
768 771 self.queries << new_query
769 772 end
770 773 end
771 774
772 775 # Copies boards from +project+
773 776 def copy_boards(project)
774 777 project.boards.each do |board|
775 778 new_board = Board.new
776 779 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
777 780 new_board.project = self
778 781 self.boards << new_board
779 782 end
780 783 end
781 784
782 785 def allowed_permissions
783 786 @allowed_permissions ||= begin
784 787 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
785 788 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
786 789 end
787 790 end
788 791
789 792 def allowed_actions
790 793 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
791 794 end
792 795
793 796 # Returns all the active Systemwide and project specific activities
794 797 def active_activities
795 798 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
796 799
797 800 if overridden_activity_ids.empty?
798 801 return TimeEntryActivity.shared.active
799 802 else
800 803 return system_activities_and_project_overrides
801 804 end
802 805 end
803 806
804 807 # Returns all the Systemwide and project specific activities
805 808 # (inactive and active)
806 809 def all_activities
807 810 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
808 811
809 812 if overridden_activity_ids.empty?
810 813 return TimeEntryActivity.shared
811 814 else
812 815 return system_activities_and_project_overrides(true)
813 816 end
814 817 end
815 818
816 819 # Returns the systemwide active activities merged with the project specific overrides
817 820 def system_activities_and_project_overrides(include_inactive=false)
818 821 if include_inactive
819 822 return TimeEntryActivity.shared.
820 823 find(:all,
821 824 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
822 825 self.time_entry_activities
823 826 else
824 827 return TimeEntryActivity.shared.active.
825 828 find(:all,
826 829 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
827 830 self.time_entry_activities.active
828 831 end
829 832 end
830 833
831 834 # Archives subprojects recursively
832 835 def archive!
833 836 children.each do |subproject|
834 837 subproject.send :archive!
835 838 end
836 839 update_attribute :status, STATUS_ARCHIVED
837 840 end
838 841 end
@@ -1,19 +1,19
1 1 <h2><%=l(:label_project_new)%></h2>
2 2
3 3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5
6 6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 8 <label class="floating">
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
9 <%= check_box_tag 'project[enabled_module_names][]', m, @project.module_enabled?(m) %>
10 10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 11 </label>
12 12 <% end %>
13 <%= hidden_field_tag 'enabled_modules[]', '' %>
13 <%= hidden_field_tag 'project[enabled_module_names][]', '' %>
14 14
15 15 </fieldset>
16 16
17 17 <%= submit_tag l(:button_save) %>
18 18 <%= javascript_tag "Form.Element.focus('project_name');" %>
19 19 <% end %>
@@ -1,187 +1,188
1 1 ---
2 2 roles_001:
3 3 name: Manager
4 4 id: 1
5 5 builtin: 0
6 6 permissions: |
7 7 ---
8 8 - :add_project
9 9 - :edit_project
10 - :select_project_modules
10 11 - :manage_members
11 12 - :manage_versions
12 13 - :manage_categories
13 14 - :view_issues
14 15 - :add_issues
15 16 - :edit_issues
16 17 - :manage_issue_relations
17 18 - :manage_subtasks
18 19 - :add_issue_notes
19 20 - :move_issues
20 21 - :delete_issues
21 22 - :view_issue_watchers
22 23 - :add_issue_watchers
23 24 - :delete_issue_watchers
24 25 - :manage_public_queries
25 26 - :save_queries
26 27 - :view_gantt
27 28 - :view_calendar
28 29 - :log_time
29 30 - :view_time_entries
30 31 - :edit_time_entries
31 32 - :delete_time_entries
32 33 - :manage_news
33 34 - :comment_news
34 35 - :view_documents
35 36 - :manage_documents
36 37 - :view_wiki_pages
37 38 - :export_wiki_pages
38 39 - :view_wiki_edits
39 40 - :edit_wiki_pages
40 41 - :delete_wiki_pages_attachments
41 42 - :protect_wiki_pages
42 43 - :delete_wiki_pages
43 44 - :rename_wiki_pages
44 45 - :add_messages
45 46 - :edit_messages
46 47 - :delete_messages
47 48 - :manage_boards
48 49 - :view_files
49 50 - :manage_files
50 51 - :browse_repository
51 52 - :manage_repository
52 53 - :view_changesets
53 54 - :manage_project_activities
54 55
55 56 position: 1
56 57 roles_002:
57 58 name: Developer
58 59 id: 2
59 60 builtin: 0
60 61 permissions: |
61 62 ---
62 63 - :edit_project
63 64 - :manage_members
64 65 - :manage_versions
65 66 - :manage_categories
66 67 - :view_issues
67 68 - :add_issues
68 69 - :edit_issues
69 70 - :manage_issue_relations
70 71 - :manage_subtasks
71 72 - :add_issue_notes
72 73 - :move_issues
73 74 - :delete_issues
74 75 - :view_issue_watchers
75 76 - :save_queries
76 77 - :view_gantt
77 78 - :view_calendar
78 79 - :log_time
79 80 - :view_time_entries
80 81 - :edit_own_time_entries
81 82 - :manage_news
82 83 - :comment_news
83 84 - :view_documents
84 85 - :manage_documents
85 86 - :view_wiki_pages
86 87 - :view_wiki_edits
87 88 - :edit_wiki_pages
88 89 - :protect_wiki_pages
89 90 - :delete_wiki_pages
90 91 - :add_messages
91 92 - :edit_own_messages
92 93 - :delete_own_messages
93 94 - :manage_boards
94 95 - :view_files
95 96 - :manage_files
96 97 - :browse_repository
97 98 - :view_changesets
98 99
99 100 position: 2
100 101 roles_003:
101 102 name: Reporter
102 103 id: 3
103 104 builtin: 0
104 105 permissions: |
105 106 ---
106 107 - :edit_project
107 108 - :manage_members
108 109 - :manage_versions
109 110 - :manage_categories
110 111 - :view_issues
111 112 - :add_issues
112 113 - :edit_issues
113 114 - :manage_issue_relations
114 115 - :add_issue_notes
115 116 - :move_issues
116 117 - :view_issue_watchers
117 118 - :save_queries
118 119 - :view_gantt
119 120 - :view_calendar
120 121 - :log_time
121 122 - :view_time_entries
122 123 - :manage_news
123 124 - :comment_news
124 125 - :view_documents
125 126 - :manage_documents
126 127 - :view_wiki_pages
127 128 - :view_wiki_edits
128 129 - :edit_wiki_pages
129 130 - :delete_wiki_pages
130 131 - :add_messages
131 132 - :manage_boards
132 133 - :view_files
133 134 - :manage_files
134 135 - :browse_repository
135 136 - :view_changesets
136 137
137 138 position: 3
138 139 roles_004:
139 140 name: Non member
140 141 id: 4
141 142 builtin: 1
142 143 permissions: |
143 144 ---
144 145 - :view_issues
145 146 - :add_issues
146 147 - :edit_issues
147 148 - :manage_issue_relations
148 149 - :add_issue_notes
149 150 - :move_issues
150 151 - :save_queries
151 152 - :view_gantt
152 153 - :view_calendar
153 154 - :log_time
154 155 - :view_time_entries
155 156 - :comment_news
156 157 - :view_documents
157 158 - :manage_documents
158 159 - :view_wiki_pages
159 160 - :view_wiki_edits
160 161 - :edit_wiki_pages
161 162 - :add_messages
162 163 - :view_files
163 164 - :manage_files
164 165 - :browse_repository
165 166 - :view_changesets
166 167
167 168 position: 4
168 169 roles_005:
169 170 name: Anonymous
170 171 id: 5
171 172 builtin: 2
172 173 permissions: |
173 174 ---
174 175 - :view_issues
175 176 - :add_issue_notes
176 177 - :view_gantt
177 178 - :view_calendar
178 179 - :view_time_entries
179 180 - :view_documents
180 181 - :view_wiki_pages
181 182 - :view_wiki_edits
182 183 - :view_files
183 184 - :browse_repository
184 185 - :view_changesets
185 186
186 187 position: 5
187 188
@@ -1,471 +1,477
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'projects_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class ProjectsController; def rescue_action(e) raise e end; end
23 23
24 24 class ProjectsControllerTest < ActionController::TestCase
25 25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 27 :attachments, :custom_fields, :custom_values, :time_entries
28 28
29 29 def setup
30 30 @controller = ProjectsController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 @request.session[:user_id] = nil
34 34 Setting.default_language = 'en'
35 35 end
36 36
37 37 def test_index
38 38 get :index
39 39 assert_response :success
40 40 assert_template 'index'
41 41 assert_not_nil assigns(:projects)
42 42
43 43 assert_tag :ul, :child => {:tag => 'li',
44 44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 45 :child => { :tag => 'ul',
46 46 :descendant => { :tag => 'a',
47 47 :content => 'Child of private child'
48 48 }
49 49 }
50 50 }
51 51
52 52 assert_no_tag :a, :content => /Private child of eCookbook/
53 53 end
54 54
55 55 def test_index_atom
56 56 get :index, :format => 'atom'
57 57 assert_response :success
58 58 assert_template 'common/feed.atom.rxml'
59 59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
61 61 end
62 62
63 63 context "#index" do
64 64 context "by non-admin user with view_time_entries permission" do
65 65 setup do
66 66 @request.session[:user_id] = 3
67 67 end
68 68 should "show overall spent time link" do
69 69 get :index
70 70 assert_template 'index'
71 71 assert_tag :a, :attributes => {:href => '/time_entries'}
72 72 end
73 73 end
74 74
75 75 context "by non-admin user without view_time_entries permission" do
76 76 setup do
77 77 Role.find(2).remove_permission! :view_time_entries
78 78 Role.non_member.remove_permission! :view_time_entries
79 79 Role.anonymous.remove_permission! :view_time_entries
80 80 @request.session[:user_id] = 3
81 81 end
82 82 should "not show overall spent time link" do
83 83 get :index
84 84 assert_template 'index'
85 85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
86 86 end
87 87 end
88 88 end
89 89
90 90 context "#new" do
91 91 context "by admin user" do
92 92 setup do
93 93 @request.session[:user_id] = 1
94 94 end
95 95
96 96 should "accept get" do
97 97 get :new
98 98 assert_response :success
99 99 assert_template 'new'
100 100 end
101 101
102 102 end
103 103
104 104 context "by non-admin user with add_project permission" do
105 105 setup do
106 106 Role.non_member.add_permission! :add_project
107 107 @request.session[:user_id] = 9
108 108 end
109 109
110 110 should "accept get" do
111 111 get :new
112 112 assert_response :success
113 113 assert_template 'new'
114 114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
115 115 end
116 116 end
117 117
118 118 context "by non-admin user with add_subprojects permission" do
119 119 setup do
120 120 Role.find(1).remove_permission! :add_project
121 121 Role.find(1).add_permission! :add_subprojects
122 122 @request.session[:user_id] = 2
123 123 end
124 124
125 125 should "accept get" do
126 126 get :new, :parent_id => 'ecookbook'
127 127 assert_response :success
128 128 assert_template 'new'
129 129 # parent project selected
130 130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
131 131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
132 132 # no empty value
133 133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
134 134 :child => {:tag => 'option', :attributes => {:value => ''}}
135 135 end
136 136 end
137 137
138 138 end
139 139
140 140 context "POST :create" do
141 141 context "by admin user" do
142 142 setup do
143 143 @request.session[:user_id] = 1
144 144 end
145 145
146 146 should "create a new project" do
147 147 post :create,
148 148 :project => {
149 149 :name => "blog",
150 150 :description => "weblog",
151 151 :homepage => 'http://weblog',
152 152 :identifier => "blog",
153 153 :is_public => 1,
154 154 :custom_field_values => { '3' => 'Beta' },
155 155 :tracker_ids => ['1', '3'],
156 156 # an issue custom field that is not for all project
157 :issue_custom_field_ids => ['9']
157 :issue_custom_field_ids => ['9'],
158 :enabled_module_names => ['issue_tracking', 'news', 'repository']
158 159 }
159 160 assert_redirected_to '/projects/blog/settings'
160 161
161 162 project = Project.find_by_name('blog')
162 163 assert_kind_of Project, project
163 164 assert project.active?
164 165 assert_equal 'weblog', project.description
165 166 assert_equal 'http://weblog', project.homepage
166 167 assert_equal true, project.is_public?
167 168 assert_nil project.parent
168 169 assert_equal 'Beta', project.custom_value_for(3).value
169 170 assert_equal [1, 3], project.trackers.map(&:id).sort
171 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
170 172 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
171 173 end
172 174
173 175 should "create a new subproject" do
174 176 post :create, :project => { :name => "blog",
175 177 :description => "weblog",
176 178 :identifier => "blog",
177 179 :is_public => 1,
178 180 :custom_field_values => { '3' => 'Beta' },
179 181 :parent_id => 1
180 182 }
181 183 assert_redirected_to '/projects/blog/settings'
182 184
183 185 project = Project.find_by_name('blog')
184 186 assert_kind_of Project, project
185 187 assert_equal Project.find(1), project.parent
186 188 end
187 189 end
188 190
189 191 context "by non-admin user with add_project permission" do
190 192 setup do
191 193 Role.non_member.add_permission! :add_project
192 194 @request.session[:user_id] = 9
193 195 end
194 196
195 197 should "accept create a Project" do
196 198 post :create, :project => { :name => "blog",
197 199 :description => "weblog",
198 200 :identifier => "blog",
199 201 :is_public => 1,
200 :custom_field_values => { '3' => 'Beta' }
202 :custom_field_values => { '3' => 'Beta' },
203 :tracker_ids => ['1', '3'],
204 :enabled_module_names => ['issue_tracking', 'news', 'repository']
201 205 }
202 206
203 207 assert_redirected_to '/projects/blog/settings'
204 208
205 209 project = Project.find_by_name('blog')
206 210 assert_kind_of Project, project
207 211 assert_equal 'weblog', project.description
208 212 assert_equal true, project.is_public?
213 assert_equal [1, 3], project.trackers.map(&:id).sort
214 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
209 215
210 216 # User should be added as a project member
211 217 assert User.find(9).member_of?(project)
212 218 assert_equal 1, project.members.size
213 219 end
214 220
215 221 should "fail with parent_id" do
216 222 assert_no_difference 'Project.count' do
217 223 post :create, :project => { :name => "blog",
218 224 :description => "weblog",
219 225 :identifier => "blog",
220 226 :is_public => 1,
221 227 :custom_field_values => { '3' => 'Beta' },
222 228 :parent_id => 1
223 229 }
224 230 end
225 231 assert_response :success
226 232 project = assigns(:project)
227 233 assert_kind_of Project, project
228 234 assert_not_nil project.errors.on(:parent_id)
229 235 end
230 236 end
231 237
232 238 context "by non-admin user with add_subprojects permission" do
233 239 setup do
234 240 Role.find(1).remove_permission! :add_project
235 241 Role.find(1).add_permission! :add_subprojects
236 242 @request.session[:user_id] = 2
237 243 end
238 244
239 245 should "create a project with a parent_id" do
240 246 post :create, :project => { :name => "blog",
241 247 :description => "weblog",
242 248 :identifier => "blog",
243 249 :is_public => 1,
244 250 :custom_field_values => { '3' => 'Beta' },
245 251 :parent_id => 1
246 252 }
247 253 assert_redirected_to '/projects/blog/settings'
248 254 project = Project.find_by_name('blog')
249 255 end
250 256
251 257 should "fail without parent_id" do
252 258 assert_no_difference 'Project.count' do
253 259 post :create, :project => { :name => "blog",
254 260 :description => "weblog",
255 261 :identifier => "blog",
256 262 :is_public => 1,
257 263 :custom_field_values => { '3' => 'Beta' }
258 264 }
259 265 end
260 266 assert_response :success
261 267 project = assigns(:project)
262 268 assert_kind_of Project, project
263 269 assert_not_nil project.errors.on(:parent_id)
264 270 end
265 271
266 272 should "fail with unauthorized parent_id" do
267 273 assert !User.find(2).member_of?(Project.find(6))
268 274 assert_no_difference 'Project.count' do
269 275 post :create, :project => { :name => "blog",
270 276 :description => "weblog",
271 277 :identifier => "blog",
272 278 :is_public => 1,
273 279 :custom_field_values => { '3' => 'Beta' },
274 280 :parent_id => 6
275 281 }
276 282 end
277 283 assert_response :success
278 284 project = assigns(:project)
279 285 assert_kind_of Project, project
280 286 assert_not_nil project.errors.on(:parent_id)
281 287 end
282 288 end
283 289 end
284 290
285 291 def test_show_by_id
286 292 get :show, :id => 1
287 293 assert_response :success
288 294 assert_template 'show'
289 295 assert_not_nil assigns(:project)
290 296 end
291 297
292 298 def test_show_by_identifier
293 299 get :show, :id => 'ecookbook'
294 300 assert_response :success
295 301 assert_template 'show'
296 302 assert_not_nil assigns(:project)
297 303 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
298 304
299 305 assert_tag 'li', :content => /Development status/
300 306 end
301 307
302 308 def test_show_should_not_display_hidden_custom_fields
303 309 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
304 310 get :show, :id => 'ecookbook'
305 311 assert_response :success
306 312 assert_template 'show'
307 313 assert_not_nil assigns(:project)
308 314
309 315 assert_no_tag 'li', :content => /Development status/
310 316 end
311 317
312 318 def test_show_should_not_fail_when_custom_values_are_nil
313 319 project = Project.find_by_identifier('ecookbook')
314 320 project.custom_values.first.update_attribute(:value, nil)
315 321 get :show, :id => 'ecookbook'
316 322 assert_response :success
317 323 assert_template 'show'
318 324 assert_not_nil assigns(:project)
319 325 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
320 326 end
321 327
322 328 def show_archived_project_should_be_denied
323 329 project = Project.find_by_identifier('ecookbook')
324 330 project.archive!
325 331
326 332 get :show, :id => 'ecookbook'
327 333 assert_response 403
328 334 assert_nil assigns(:project)
329 335 assert_tag :tag => 'p', :content => /archived/
330 336 end
331 337
332 338 def test_private_subprojects_hidden
333 339 get :show, :id => 'ecookbook'
334 340 assert_response :success
335 341 assert_template 'show'
336 342 assert_no_tag :tag => 'a', :content => /Private child/
337 343 end
338 344
339 345 def test_private_subprojects_visible
340 346 @request.session[:user_id] = 2 # manager who is a member of the private subproject
341 347 get :show, :id => 'ecookbook'
342 348 assert_response :success
343 349 assert_template 'show'
344 350 assert_tag :tag => 'a', :content => /Private child/
345 351 end
346 352
347 353 def test_settings
348 354 @request.session[:user_id] = 2 # manager
349 355 get :settings, :id => 1
350 356 assert_response :success
351 357 assert_template 'settings'
352 358 end
353 359
354 360 def test_update
355 361 @request.session[:user_id] = 2 # manager
356 362 post :update, :id => 1, :project => {:name => 'Test changed name',
357 363 :issue_custom_field_ids => ['']}
358 364 assert_redirected_to '/projects/ecookbook/settings'
359 365 project = Project.find(1)
360 366 assert_equal 'Test changed name', project.name
361 367 end
362 368
363 369 def test_get_destroy
364 370 @request.session[:user_id] = 1 # admin
365 371 get :destroy, :id => 1
366 372 assert_response :success
367 373 assert_template 'destroy'
368 374 assert_not_nil Project.find_by_id(1)
369 375 end
370 376
371 377 def test_post_destroy
372 378 @request.session[:user_id] = 1 # admin
373 379 post :destroy, :id => 1, :confirm => 1
374 380 assert_redirected_to '/admin/projects'
375 381 assert_nil Project.find_by_id(1)
376 382 end
377 383
378 384 def test_archive
379 385 @request.session[:user_id] = 1 # admin
380 386 post :archive, :id => 1
381 387 assert_redirected_to '/admin/projects'
382 388 assert !Project.find(1).active?
383 389 end
384 390
385 391 def test_unarchive
386 392 @request.session[:user_id] = 1 # admin
387 393 Project.find(1).archive
388 394 post :unarchive, :id => 1
389 395 assert_redirected_to '/admin/projects'
390 396 assert Project.find(1).active?
391 397 end
392 398
393 399 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
394 400 CustomField.delete_all
395 401 parent = nil
396 402 6.times do |i|
397 403 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
398 404 p.set_parent!(parent)
399 405 get :show, :id => p
400 406 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
401 407 :children => { :count => [i, 3].min,
402 408 :only => { :tag => 'a' } }
403 409
404 410 parent = p
405 411 end
406 412 end
407 413
408 414 def test_copy_with_project
409 415 @request.session[:user_id] = 1 # admin
410 416 get :copy, :id => 1
411 417 assert_response :success
412 418 assert_template 'copy'
413 419 assert assigns(:project)
414 420 assert_equal Project.find(1).description, assigns(:project).description
415 421 assert_nil assigns(:project).id
416 422 end
417 423
418 424 def test_copy_without_project
419 425 @request.session[:user_id] = 1 # admin
420 426 get :copy
421 427 assert_response :redirect
422 428 assert_redirected_to :controller => 'admin', :action => 'projects'
423 429 end
424 430
425 431 context "POST :copy" do
426 432 should "TODO: test the rest of the method"
427 433
428 434 should "redirect to the project settings when successful" do
429 435 @request.session[:user_id] = 1 # admin
430 436 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
431 437 assert_response :redirect
432 438 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
433 439 end
434 440 end
435 441
436 442 def test_jump_should_redirect_to_active_tab
437 443 get :show, :id => 1, :jump => 'issues'
438 444 assert_redirected_to '/projects/ecookbook/issues'
439 445 end
440 446
441 447 def test_jump_should_not_redirect_to_inactive_tab
442 448 get :show, :id => 3, :jump => 'documents'
443 449 assert_response :success
444 450 assert_template 'show'
445 451 end
446 452
447 453 def test_jump_should_not_redirect_to_unknown_tab
448 454 get :show, :id => 3, :jump => 'foobar'
449 455 assert_response :success
450 456 assert_template 'show'
451 457 end
452 458
453 459 # A hook that is manually registered later
454 460 class ProjectBasedTemplate < Redmine::Hook::ViewListener
455 461 def view_layouts_base_html_head(context)
456 462 # Adds a project stylesheet
457 463 stylesheet_link_tag(context[:project].identifier) if context[:project]
458 464 end
459 465 end
460 466 # Don't use this hook now
461 467 Redmine::Hook.clear_listeners
462 468
463 469 def test_hook_response
464 470 Redmine::Hook.add_listener(ProjectBasedTemplate)
465 471 get :show, :id => 1
466 472 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
467 473 :parent => {:tag => 'head'}
468 474
469 475 Redmine::Hook.clear_listeners
470 476 end
471 477 end
@@ -1,216 +1,261
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
21 21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
23 23 :attachments, :custom_fields, :custom_values, :time_entries
24 24
25 25 def setup
26 26 Setting.rest_api_enabled = '1'
27 27 end
28 28
29 29 context "GET /projects" do
30 30 context ".xml" do
31 31 should "return projects" do
32 32 get '/projects.xml'
33 33 assert_response :success
34 34 assert_equal 'application/xml', @response.content_type
35 35
36 36 assert_tag :tag => 'projects',
37 37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
38 38 end
39 39 end
40 40
41 41 context ".json" do
42 42 should "return projects" do
43 43 get '/projects.json'
44 44 assert_response :success
45 45 assert_equal 'application/json', @response.content_type
46 46
47 47 json = ActiveSupport::JSON.decode(response.body)
48 48 assert_kind_of Hash, json
49 49 assert_kind_of Array, json['projects']
50 50 assert_kind_of Hash, json['projects'].first
51 51 assert json['projects'].first.has_key?('id')
52 52 end
53 53 end
54 54 end
55 55
56 56 context "GET /projects/:id" do
57 57 context ".xml" do
58 58 # TODO: A private project is needed because should_allow_api_authentication
59 59 # actually tests that authentication is *required*, not just allowed
60 60 should_allow_api_authentication(:get, "/projects/2.xml")
61 61
62 62 should "return requested project" do
63 63 get '/projects/1.xml'
64 64 assert_response :success
65 65 assert_equal 'application/xml', @response.content_type
66 66
67 67 assert_tag :tag => 'project',
68 68 :child => {:tag => 'id', :content => '1'}
69 69 assert_tag :tag => 'custom_field',
70 70 :attributes => {:name => 'Development status'}, :content => 'Stable'
71 71 end
72 72
73 73 context "with hidden custom fields" do
74 74 setup do
75 75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
76 76 end
77 77
78 78 should "not display hidden custom fields" do
79 79 get '/projects/1.xml'
80 80 assert_response :success
81 81 assert_equal 'application/xml', @response.content_type
82 82
83 83 assert_no_tag 'custom_field',
84 84 :attributes => {:name => 'Development status'}
85 85 end
86 86 end
87 87 end
88 88
89 89 context ".json" do
90 90 should_allow_api_authentication(:get, "/projects/2.json")
91 91
92 92 should "return requested project" do
93 93 get '/projects/1.json'
94 94
95 95 json = ActiveSupport::JSON.decode(response.body)
96 96 assert_kind_of Hash, json
97 97 assert_kind_of Hash, json['project']
98 98 assert_equal 1, json['project']['id']
99 99 end
100 100 end
101 101 end
102 102
103 103 context "POST /projects" do
104 104 context "with valid parameters" do
105 105 setup do
106 106 Setting.default_projects_modules = ['issue_tracking', 'repository']
107 107 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
108 108 end
109 109
110 110 context ".xml" do
111 111 should_allow_api_authentication(:post,
112 112 '/projects.xml',
113 113 {:project => {:name => 'API test', :identifier => 'api-test'}},
114 114 {:success_code => :created})
115 115
116 116
117 117 should "create a project with the attributes" do
118 118 assert_difference('Project.count') do
119 119 post '/projects.xml', @parameters, :authorization => credentials('admin')
120 120 end
121 121
122 122 project = Project.first(:order => 'id DESC')
123 123 assert_equal 'API test', project.name
124 124 assert_equal 'api-test', project.identifier
125 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names
125 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort
126 assert_equal Tracker.all.size, project.trackers.size
126 127
127 128 assert_response :created
128 129 assert_equal 'application/xml', @response.content_type
129 130 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
130 131 end
132
133 should "accept enabled_module_names attribute" do
134 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
135
136 assert_difference('Project.count') do
137 post '/projects.xml', @parameters, :authorization => credentials('admin')
138 end
139
140 project = Project.first(:order => 'id DESC')
141 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
142 end
143
144 should "accept tracker_ids attribute" do
145 @parameters[:project].merge!({:tracker_ids => [1, 3]})
146
147 assert_difference('Project.count') do
148 post '/projects.xml', @parameters, :authorization => credentials('admin')
149 end
150
151 project = Project.first(:order => 'id DESC')
152 assert_equal [1, 3], project.trackers.map(&:id).sort
153 end
131 154 end
132 155 end
133 156
134 157 context "with invalid parameters" do
135 158 setup do
136 159 @parameters = {:project => {:name => 'API test'}}
137 160 end
138 161
139 162 context ".xml" do
140 163 should "return errors" do
141 164 assert_no_difference('Project.count') do
142 165 post '/projects.xml', @parameters, :authorization => credentials('admin')
143 166 end
144 167
145 168 assert_response :unprocessable_entity
146 169 assert_equal 'application/xml', @response.content_type
147 170 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
148 171 end
149 172 end
150 173 end
151 174 end
152 175
153 176 context "PUT /projects/:id" do
154 177 context "with valid parameters" do
155 178 setup do
156 179 @parameters = {:project => {:name => 'API update'}}
157 180 end
158 181
159 182 context ".xml" do
160 183 should_allow_api_authentication(:put,
161 184 '/projects/2.xml',
162 185 {:project => {:name => 'API update'}},
163 186 {:success_code => :ok})
164 187
165 188 should "update the project" do
166 189 assert_no_difference 'Project.count' do
167 190 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
168 191 end
169 192 assert_response :ok
170 193 assert_equal 'application/xml', @response.content_type
171 194 project = Project.find(2)
172 195 assert_equal 'API update', project.name
173 196 end
197
198 should "accept enabled_module_names attribute" do
199 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
200
201 assert_no_difference 'Project.count' do
202 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
203 end
204 assert_response :ok
205 project = Project.find(2)
206 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
207 end
208
209 should "accept tracker_ids attribute" do
210 @parameters[:project].merge!({:tracker_ids => [1, 3]})
211
212 assert_no_difference 'Project.count' do
213 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
214 end
215 assert_response :ok
216 project = Project.find(2)
217 assert_equal [1, 3], project.trackers.map(&:id).sort
218 end
174 219 end
175 220 end
176 221
177 222 context "with invalid parameters" do
178 223 setup do
179 224 @parameters = {:project => {:name => ''}}
180 225 end
181 226
182 227 context ".xml" do
183 228 should "return errors" do
184 229 assert_no_difference('Project.count') do
185 230 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
186 231 end
187 232
188 233 assert_response :unprocessable_entity
189 234 assert_equal 'application/xml', @response.content_type
190 235 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
191 236 end
192 237 end
193 238 end
194 239 end
195 240
196 241 context "DELETE /projects/:id" do
197 242 context ".xml" do
198 243 should_allow_api_authentication(:delete,
199 244 '/projects/2.xml',
200 245 {},
201 246 {:success_code => :ok})
202 247
203 248 should "delete the project" do
204 249 assert_difference('Project.count',-1) do
205 250 delete '/projects/2.xml', {}, :authorization => credentials('admin')
206 251 end
207 252 assert_response :ok
208 253 assert_nil Project.find_by_id(2)
209 254 end
210 255 end
211 256 end
212 257
213 258 def credentials(user, password=nil)
214 259 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
215 260 end
216 261 end
General Comments 0
You need to be logged in to leave comments. Login now