##// END OF EJS Templates
Merged r4645 to r4651 from trunk....
Jean-Philippe Lang -
r4540:6e695a4d1a37
parent child
Show More
@@ -1,270 +1,270
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :roadmap, :only => :roadmap
20 menu_item :roadmap, :only => :roadmap
21 menu_item :settings, :only => :settings
21 menu_item :settings, :only => :settings
22
22
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 before_filter :authorize_global, :only => [:new, :create]
25 before_filter :authorize_global, :only => [:new, :create]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 accept_key_auth :index, :show, :create, :update, :destroy
27 accept_key_auth :index, :show, :create, :update, :destroy
28
28
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 if controller.request.post?
30 if controller.request.post?
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
32 end
32 end
33 end
33 end
34
34
35 # TODO: convert to PUT only
36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
37
38 helper :sort
35 helper :sort
39 include SortHelper
36 include SortHelper
40 helper :custom_fields
37 helper :custom_fields
41 include CustomFieldsHelper
38 include CustomFieldsHelper
42 helper :issues
39 helper :issues
43 helper :queries
40 helper :queries
44 include QueriesHelper
41 include QueriesHelper
45 helper :repositories
42 helper :repositories
46 include RepositoriesHelper
43 include RepositoriesHelper
47 include ProjectsHelper
44 include ProjectsHelper
48
45
49 # Lists visible projects
46 # Lists visible projects
50 def index
47 def index
51 respond_to do |format|
48 respond_to do |format|
52 format.html {
49 format.html {
53 @projects = Project.visible.find(:all, :order => 'lft')
50 @projects = Project.visible.find(:all, :order => 'lft')
54 }
51 }
55 format.api {
52 format.api {
56 @offset, @limit = api_offset_and_limit
53 @offset, @limit = api_offset_and_limit
57 @project_count = Project.visible.count
54 @project_count = Project.visible.count
58 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
55 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
59 }
56 }
60 format.atom {
57 format.atom {
61 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 projects = Project.visible.find(:all, :order => 'created_on DESC',
62 :limit => Setting.feeds_limit.to_i)
59 :limit => Setting.feeds_limit.to_i)
63 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
64 }
61 }
65 end
62 end
66 end
63 end
67
64
68 def new
65 def new
69 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
70 @trackers = Tracker.all
67 @trackers = Tracker.all
71 @project = Project.new(params[:project])
68 @project = Project.new(params[:project])
72 end
69 end
73
70
71 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
74 def create
72 def create
75 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
73 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
76 @trackers = Tracker.all
74 @trackers = Tracker.all
77 @project = Project.new
75 @project = Project.new
78 @project.safe_attributes = params[:project]
76 @project.safe_attributes = params[:project]
79
77
80 @project.enabled_module_names = params[:enabled_modules] if params[:enabled_modules]
81 if validate_parent_id && @project.save
78 if validate_parent_id && @project.save
82 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
79 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
83 # Add current user as a project member if he is not admin
80 # Add current user as a project member if he is not admin
84 unless User.current.admin?
81 unless User.current.admin?
85 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
82 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
86 m = Member.new(:user => User.current, :roles => [r])
83 m = Member.new(:user => User.current, :roles => [r])
87 @project.members << m
84 @project.members << m
88 end
85 end
89 respond_to do |format|
86 respond_to do |format|
90 format.html {
87 format.html {
91 flash[:notice] = l(:notice_successful_create)
88 flash[:notice] = l(:notice_successful_create)
92 redirect_to :controller => 'projects', :action => 'settings', :id => @project
89 redirect_to :controller => 'projects', :action => 'settings', :id => @project
93 }
90 }
94 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
91 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
95 end
92 end
96 else
93 else
97 respond_to do |format|
94 respond_to do |format|
98 format.html { render :action => 'new' }
95 format.html { render :action => 'new' }
99 format.api { render_validation_errors(@project) }
96 format.api { render_validation_errors(@project) }
100 end
97 end
101 end
98 end
102
99
103 end
100 end
104
101
105 def copy
102 def copy
106 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
103 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
107 @trackers = Tracker.all
104 @trackers = Tracker.all
108 @root_projects = Project.find(:all,
105 @root_projects = Project.find(:all,
109 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
106 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
110 :order => 'name')
107 :order => 'name')
111 @source_project = Project.find(params[:id])
108 @source_project = Project.find(params[:id])
112 if request.get?
109 if request.get?
113 @project = Project.copy_from(@source_project)
110 @project = Project.copy_from(@source_project)
114 if @project
111 if @project
115 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
112 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
116 else
113 else
117 redirect_to :controller => 'admin', :action => 'projects'
114 redirect_to :controller => 'admin', :action => 'projects'
118 end
115 end
119 else
116 else
120 Mailer.with_deliveries(params[:notifications] == '1') do
117 Mailer.with_deliveries(params[:notifications] == '1') do
121 @project = Project.new
118 @project = Project.new
122 @project.safe_attributes = params[:project]
119 @project.safe_attributes = params[:project]
123 @project.enabled_module_names = params[:enabled_modules]
120 @project.enabled_module_names = params[:enabled_modules]
124 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
121 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
125 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
126 flash[:notice] = l(:notice_successful_create)
123 flash[:notice] = l(:notice_successful_create)
127 redirect_to :controller => 'projects', :action => 'settings', :id => @project
124 redirect_to :controller => 'projects', :action => 'settings', :id => @project
128 elsif !@project.new_record?
125 elsif !@project.new_record?
129 # Project was created
126 # Project was created
130 # But some objects were not copied due to validation failures
127 # But some objects were not copied due to validation failures
131 # (eg. issues from disabled trackers)
128 # (eg. issues from disabled trackers)
132 # TODO: inform about that
129 # TODO: inform about that
133 redirect_to :controller => 'projects', :action => 'settings', :id => @project
130 redirect_to :controller => 'projects', :action => 'settings', :id => @project
134 end
131 end
135 end
132 end
136 end
133 end
137 rescue ActiveRecord::RecordNotFound
134 rescue ActiveRecord::RecordNotFound
138 redirect_to :controller => 'admin', :action => 'projects'
135 redirect_to :controller => 'admin', :action => 'projects'
139 end
136 end
140
137
141 # Show @project
138 # Show @project
142 def show
139 def show
143 if params[:jump]
140 if params[:jump]
144 # try to redirect to the requested menu item
141 # try to redirect to the requested menu item
145 redirect_to_project_menu_item(@project, params[:jump]) && return
142 redirect_to_project_menu_item(@project, params[:jump]) && return
146 end
143 end
147
144
148 @users_by_role = @project.users_by_role
145 @users_by_role = @project.users_by_role
149 @subprojects = @project.children.visible
146 @subprojects = @project.children.visible
150 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
147 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
151 @trackers = @project.rolled_up_trackers
148 @trackers = @project.rolled_up_trackers
152
149
153 cond = @project.project_condition(Setting.display_subprojects_issues?)
150 cond = @project.project_condition(Setting.display_subprojects_issues?)
154
151
155 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
152 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
156 :include => [:project, :status, :tracker],
153 :include => [:project, :status, :tracker],
157 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
154 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
158 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
155 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
159 :include => [:project, :status, :tracker],
156 :include => [:project, :status, :tracker],
160 :conditions => cond)
157 :conditions => cond)
161
158
162 TimeEntry.visible_by(User.current) do
159 TimeEntry.visible_by(User.current) do
163 @total_hours = TimeEntry.sum(:hours,
160 @total_hours = TimeEntry.sum(:hours,
164 :include => :project,
161 :include => :project,
165 :conditions => cond).to_f
162 :conditions => cond).to_f
166 end
163 end
167 @key = User.current.rss_key
164 @key = User.current.rss_key
168
165
169 respond_to do |format|
166 respond_to do |format|
170 format.html
167 format.html
171 format.api
168 format.api
172 end
169 end
173 end
170 end
174
171
175 def settings
172 def settings
176 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
173 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
177 @issue_category ||= IssueCategory.new
174 @issue_category ||= IssueCategory.new
178 @member ||= @project.members.new
175 @member ||= @project.members.new
179 @trackers = Tracker.all
176 @trackers = Tracker.all
180 @repository ||= @project.repository
177 @repository ||= @project.repository
181 @wiki ||= @project.wiki
178 @wiki ||= @project.wiki
182 end
179 end
183
180
184 def edit
181 def edit
185 end
182 end
186
183
184 # TODO: convert to PUT only
185 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
187 def update
186 def update
188 @project.safe_attributes = params[:project]
187 @project.safe_attributes = params[:project]
189 if validate_parent_id && @project.save
188 if validate_parent_id && @project.save
190 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
189 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
191 respond_to do |format|
190 respond_to do |format|
192 format.html {
191 format.html {
193 flash[:notice] = l(:notice_successful_update)
192 flash[:notice] = l(:notice_successful_update)
194 redirect_to :action => 'settings', :id => @project
193 redirect_to :action => 'settings', :id => @project
195 }
194 }
196 format.api { head :ok }
195 format.api { head :ok }
197 end
196 end
198 else
197 else
199 respond_to do |format|
198 respond_to do |format|
200 format.html {
199 format.html {
201 settings
200 settings
202 render :action => 'settings'
201 render :action => 'settings'
203 }
202 }
204 format.api { render_validation_errors(@project) }
203 format.api { render_validation_errors(@project) }
205 end
204 end
206 end
205 end
207 end
206 end
208
207
208 verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed }
209 def modules
209 def modules
210 @project.enabled_module_names = params[:enabled_modules]
210 @project.enabled_module_names = params[:enabled_module_names]
211 flash[:notice] = l(:notice_successful_update)
211 flash[:notice] = l(:notice_successful_update)
212 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
212 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
213 end
213 end
214
214
215 def archive
215 def archive
216 if request.post?
216 if request.post?
217 unless @project.archive
217 unless @project.archive
218 flash[:error] = l(:error_can_not_archive_project)
218 flash[:error] = l(:error_can_not_archive_project)
219 end
219 end
220 end
220 end
221 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
221 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
222 end
222 end
223
223
224 def unarchive
224 def unarchive
225 @project.unarchive if request.post? && !@project.active?
225 @project.unarchive if request.post? && !@project.active?
226 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
226 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
227 end
227 end
228
228
229 # Delete @project
229 # Delete @project
230 def destroy
230 def destroy
231 @project_to_destroy = @project
231 @project_to_destroy = @project
232 if request.get?
232 if request.get?
233 # display confirmation view
233 # display confirmation view
234 else
234 else
235 if api_request? || params[:confirm]
235 if api_request? || params[:confirm]
236 @project_to_destroy.destroy
236 @project_to_destroy.destroy
237 respond_to do |format|
237 respond_to do |format|
238 format.html { redirect_to :controller => 'admin', :action => 'projects' }
238 format.html { redirect_to :controller => 'admin', :action => 'projects' }
239 format.api { head :ok }
239 format.api { head :ok }
240 end
240 end
241 end
241 end
242 end
242 end
243 # hide project in layout
243 # hide project in layout
244 @project = nil
244 @project = nil
245 end
245 end
246
246
247 private
247 private
248 def find_optional_project
248 def find_optional_project
249 return true unless params[:id]
249 return true unless params[:id]
250 @project = Project.find(params[:id])
250 @project = Project.find(params[:id])
251 authorize
251 authorize
252 rescue ActiveRecord::RecordNotFound
252 rescue ActiveRecord::RecordNotFound
253 render_404
253 render_404
254 end
254 end
255
255
256 # Validates parent_id param according to user's permissions
256 # Validates parent_id param according to user's permissions
257 # TODO: move it to Project model in a validation that depends on User.current
257 # TODO: move it to Project model in a validation that depends on User.current
258 def validate_parent_id
258 def validate_parent_id
259 return true if User.current.admin?
259 return true if User.current.admin?
260 parent_id = params[:project] && params[:project][:parent_id]
260 parent_id = params[:project] && params[:project][:parent_id]
261 if parent_id || @project.new_record?
261 if parent_id || @project.new_record?
262 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
262 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
263 unless @project.allowed_parents.include?(parent)
263 unless @project.allowed_parents.include?(parent)
264 @project.errors.add :parent_id, :invalid
264 @project.errors.add :parent_id, :invalid
265 return false
265 return false
266 end
266 end
267 end
267 end
268 true
268 true
269 end
269 end
270 end
270 end
@@ -1,838 +1,841
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :delete_all, :include => :author
46 has_many :news, :dependent => :delete_all, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy
49 has_one :repository, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :order => 'name'
59 acts_as_nested_set :order => 'name'
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status, :enabled_module_names
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 before_destroy :delete_all_members, :destroy_children
82 before_destroy :delete_all_members, :destroy_children
83
83
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] } }
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 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :all_public, { :conditions => { :is_public => true } }
86 named_scope :all_public, { :conditions => { :is_public => true } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
88
88
89 def initialize(attributes = nil)
89 def initialize(attributes = nil)
90 super
90 super
91
91
92 initialized = (attributes || {}).stringify_keys
92 initialized = (attributes || {}).stringify_keys
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 self.identifier = Project.next_identifier
94 self.identifier = Project.next_identifier
95 end
95 end
96 if !initialized.key?('is_public')
96 if !initialized.key?('is_public')
97 self.is_public = Setting.default_projects_public?
97 self.is_public = Setting.default_projects_public?
98 end
98 end
99 if !initialized.key?('enabled_module_names')
99 if !initialized.key?('enabled_module_names')
100 self.enabled_module_names = Setting.default_projects_modules
100 self.enabled_module_names = Setting.default_projects_modules
101 end
101 end
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 self.trackers = Tracker.all
103 self.trackers = Tracker.all
104 end
104 end
105 end
105 end
106
106
107 def identifier=(identifier)
107 def identifier=(identifier)
108 super unless identifier_frozen?
108 super unless identifier_frozen?
109 end
109 end
110
110
111 def identifier_frozen?
111 def identifier_frozen?
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 end
113 end
114
114
115 # returns latest created projects
115 # returns latest created projects
116 # non public projects will be returned only if user is a member of those
116 # non public projects will be returned only if user is a member of those
117 def self.latest(user=nil, count=5)
117 def self.latest(user=nil, count=5)
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
119 end
119 end
120
120
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
122 #
122 #
123 # Examples:
123 # Examples:
124 # Projects.visible_by(admin) => "projects.status = 1"
124 # Projects.visible_by(admin) => "projects.status = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
126 def self.visible_by(user=nil)
126 def self.visible_by(user=nil)
127 user ||= User.current
127 user ||= User.current
128 if user && user.admin?
128 if user && user.admin?
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
130 elsif user && user.memberships.any?
130 elsif user && user.memberships.any?
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(',')}))"
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 else
132 else
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
134 end
134 end
135 end
135 end
136
136
137 def self.allowed_to_condition(user, permission, options={})
137 def self.allowed_to_condition(user, permission, options={})
138 statements = []
138 statements = []
139 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
139 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
140 if perm = Redmine::AccessControl.permission(permission)
140 if perm = Redmine::AccessControl.permission(permission)
141 unless perm.project_module.nil?
141 unless perm.project_module.nil?
142 # If the permission belongs to a project module, make sure the module is enabled
142 # If the permission belongs to a project module, make sure the module is enabled
143 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
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 end
144 end
145 end
145 end
146 if options[:project]
146 if options[:project]
147 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
147 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
148 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
148 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
149 base_statement = "(#{project_statement}) AND (#{base_statement})"
149 base_statement = "(#{project_statement}) AND (#{base_statement})"
150 end
150 end
151 if user.admin?
151 if user.admin?
152 # no restriction
152 # no restriction
153 else
153 else
154 statements << "1=0"
154 statements << "1=0"
155 if user.logged?
155 if user.logged?
156 if Role.non_member.allowed_to?(permission) && !options[:member]
156 if Role.non_member.allowed_to?(permission) && !options[:member]
157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
158 end
158 end
159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
161 else
161 else
162 if Role.anonymous.allowed_to?(permission) && !options[:member]
162 if Role.anonymous.allowed_to?(permission) && !options[:member]
163 # anonymous user allowed on public project
163 # anonymous user allowed on public project
164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
165 end
165 end
166 end
166 end
167 end
167 end
168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
169 end
169 end
170
170
171 # Returns the Systemwide and project specific activities
171 # Returns the Systemwide and project specific activities
172 def activities(include_inactive=false)
172 def activities(include_inactive=false)
173 if include_inactive
173 if include_inactive
174 return all_activities
174 return all_activities
175 else
175 else
176 return active_activities
176 return active_activities
177 end
177 end
178 end
178 end
179
179
180 # Will create a new Project specific Activity or update an existing one
180 # Will create a new Project specific Activity or update an existing one
181 #
181 #
182 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
182 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
183 # does not successfully save.
183 # does not successfully save.
184 def update_or_create_time_entry_activity(id, activity_hash)
184 def update_or_create_time_entry_activity(id, activity_hash)
185 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
185 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
186 self.create_time_entry_activity_if_needed(activity_hash)
186 self.create_time_entry_activity_if_needed(activity_hash)
187 else
187 else
188 activity = project.time_entry_activities.find_by_id(id.to_i)
188 activity = project.time_entry_activities.find_by_id(id.to_i)
189 activity.update_attributes(activity_hash) if activity
189 activity.update_attributes(activity_hash) if activity
190 end
190 end
191 end
191 end
192
192
193 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
193 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
194 #
194 #
195 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
195 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
196 # does not successfully save.
196 # does not successfully save.
197 def create_time_entry_activity_if_needed(activity)
197 def create_time_entry_activity_if_needed(activity)
198 if activity['parent_id']
198 if activity['parent_id']
199
199
200 parent_activity = TimeEntryActivity.find(activity['parent_id'])
200 parent_activity = TimeEntryActivity.find(activity['parent_id'])
201 activity['name'] = parent_activity.name
201 activity['name'] = parent_activity.name
202 activity['position'] = parent_activity.position
202 activity['position'] = parent_activity.position
203
203
204 if Enumeration.overridding_change?(activity, parent_activity)
204 if Enumeration.overridding_change?(activity, parent_activity)
205 project_activity = self.time_entry_activities.create(activity)
205 project_activity = self.time_entry_activities.create(activity)
206
206
207 if project_activity.new_record?
207 if project_activity.new_record?
208 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
208 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
209 else
209 else
210 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
210 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
211 end
211 end
212 end
212 end
213 end
213 end
214 end
214 end
215
215
216 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
216 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
217 #
217 #
218 # Examples:
218 # Examples:
219 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
219 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
220 # project.project_condition(false) => "projects.id = 1"
220 # project.project_condition(false) => "projects.id = 1"
221 def project_condition(with_subprojects)
221 def project_condition(with_subprojects)
222 cond = "#{Project.table_name}.id = #{id}"
222 cond = "#{Project.table_name}.id = #{id}"
223 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
223 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
224 cond
224 cond
225 end
225 end
226
226
227 def self.find(*args)
227 def self.find(*args)
228 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
228 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
229 project = find_by_identifier(*args)
229 project = find_by_identifier(*args)
230 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
230 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
231 project
231 project
232 else
232 else
233 super
233 super
234 end
234 end
235 end
235 end
236
236
237 def to_param
237 def to_param
238 # id is used for projects with a numeric identifier (compatibility)
238 # id is used for projects with a numeric identifier (compatibility)
239 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
239 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
240 end
240 end
241
241
242 def active?
242 def active?
243 self.status == STATUS_ACTIVE
243 self.status == STATUS_ACTIVE
244 end
244 end
245
245
246 def archived?
246 def archived?
247 self.status == STATUS_ARCHIVED
247 self.status == STATUS_ARCHIVED
248 end
248 end
249
249
250 # Archives the project and its descendants
250 # Archives the project and its descendants
251 def archive
251 def archive
252 # Check that there is no issue of a non descendant project that is assigned
252 # Check that there is no issue of a non descendant project that is assigned
253 # to one of the project or descendant versions
253 # to one of the project or descendant versions
254 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
254 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
255 if v_ids.any? && Issue.find(:first, :include => :project,
255 if v_ids.any? && Issue.find(:first, :include => :project,
256 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
256 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
257 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
257 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
258 return false
258 return false
259 end
259 end
260 Project.transaction do
260 Project.transaction do
261 archive!
261 archive!
262 end
262 end
263 true
263 true
264 end
264 end
265
265
266 # Unarchives the project
266 # Unarchives the project
267 # All its ancestors must be active
267 # All its ancestors must be active
268 def unarchive
268 def unarchive
269 return false if ancestors.detect {|a| !a.active?}
269 return false if ancestors.detect {|a| !a.active?}
270 update_attribute :status, STATUS_ACTIVE
270 update_attribute :status, STATUS_ACTIVE
271 end
271 end
272
272
273 # Returns an array of projects the project can be moved to
273 # Returns an array of projects the project can be moved to
274 # by the current user
274 # by the current user
275 def allowed_parents
275 def allowed_parents
276 return @allowed_parents if @allowed_parents
276 return @allowed_parents if @allowed_parents
277 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
277 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
278 @allowed_parents = @allowed_parents - self_and_descendants
278 @allowed_parents = @allowed_parents - self_and_descendants
279 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
279 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
280 @allowed_parents << nil
280 @allowed_parents << nil
281 end
281 end
282 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
282 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
283 @allowed_parents << parent
283 @allowed_parents << parent
284 end
284 end
285 @allowed_parents
285 @allowed_parents
286 end
286 end
287
287
288 # Sets the parent of the project with authorization check
288 # Sets the parent of the project with authorization check
289 def set_allowed_parent!(p)
289 def set_allowed_parent!(p)
290 unless p.nil? || p.is_a?(Project)
290 unless p.nil? || p.is_a?(Project)
291 if p.to_s.blank?
291 if p.to_s.blank?
292 p = nil
292 p = nil
293 else
293 else
294 p = Project.find_by_id(p)
294 p = Project.find_by_id(p)
295 return false unless p
295 return false unless p
296 end
296 end
297 end
297 end
298 if p.nil?
298 if p.nil?
299 if !new_record? && allowed_parents.empty?
299 if !new_record? && allowed_parents.empty?
300 return false
300 return false
301 end
301 end
302 elsif !allowed_parents.include?(p)
302 elsif !allowed_parents.include?(p)
303 return false
303 return false
304 end
304 end
305 set_parent!(p)
305 set_parent!(p)
306 end
306 end
307
307
308 # Sets the parent of the project
308 # Sets the parent of the project
309 # Argument can be either a Project, a String, a Fixnum or nil
309 # Argument can be either a Project, a String, a Fixnum or nil
310 def set_parent!(p)
310 def set_parent!(p)
311 unless p.nil? || p.is_a?(Project)
311 unless p.nil? || p.is_a?(Project)
312 if p.to_s.blank?
312 if p.to_s.blank?
313 p = nil
313 p = nil
314 else
314 else
315 p = Project.find_by_id(p)
315 p = Project.find_by_id(p)
316 return false unless p
316 return false unless p
317 end
317 end
318 end
318 end
319 if p == parent && !p.nil?
319 if p == parent && !p.nil?
320 # Nothing to do
320 # Nothing to do
321 true
321 true
322 elsif p.nil? || (p.active? && move_possible?(p))
322 elsif p.nil? || (p.active? && move_possible?(p))
323 # Insert the project so that target's children or root projects stay alphabetically sorted
323 # Insert the project so that target's children or root projects stay alphabetically sorted
324 sibs = (p.nil? ? self.class.roots : p.children)
324 sibs = (p.nil? ? self.class.roots : p.children)
325 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
325 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
326 if to_be_inserted_before
326 if to_be_inserted_before
327 move_to_left_of(to_be_inserted_before)
327 move_to_left_of(to_be_inserted_before)
328 elsif p.nil?
328 elsif p.nil?
329 if sibs.empty?
329 if sibs.empty?
330 # move_to_root adds the project in first (ie. left) position
330 # move_to_root adds the project in first (ie. left) position
331 move_to_root
331 move_to_root
332 else
332 else
333 move_to_right_of(sibs.last) unless self == sibs.last
333 move_to_right_of(sibs.last) unless self == sibs.last
334 end
334 end
335 else
335 else
336 # move_to_child_of adds the project in last (ie.right) position
336 # move_to_child_of adds the project in last (ie.right) position
337 move_to_child_of(p)
337 move_to_child_of(p)
338 end
338 end
339 Issue.update_versions_from_hierarchy_change(self)
339 Issue.update_versions_from_hierarchy_change(self)
340 true
340 true
341 else
341 else
342 # Can not move to the given target
342 # Can not move to the given target
343 false
343 false
344 end
344 end
345 end
345 end
346
346
347 # Returns an array of the trackers used by the project and its active sub projects
347 # Returns an array of the trackers used by the project and its active sub projects
348 def rolled_up_trackers
348 def rolled_up_trackers
349 @rolled_up_trackers ||=
349 @rolled_up_trackers ||=
350 Tracker.find(:all, :include => :projects,
350 Tracker.find(:all, :include => :projects,
351 :select => "DISTINCT #{Tracker.table_name}.*",
351 :select => "DISTINCT #{Tracker.table_name}.*",
352 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
352 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
353 :order => "#{Tracker.table_name}.position")
353 :order => "#{Tracker.table_name}.position")
354 end
354 end
355
355
356 # Closes open and locked project versions that are completed
356 # Closes open and locked project versions that are completed
357 def close_completed_versions
357 def close_completed_versions
358 Version.transaction do
358 Version.transaction do
359 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
359 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
360 if version.completed?
360 if version.completed?
361 version.update_attribute(:status, 'closed')
361 version.update_attribute(:status, 'closed')
362 end
362 end
363 end
363 end
364 end
364 end
365 end
365 end
366
366
367 # Returns a scope of the Versions on subprojects
367 # Returns a scope of the Versions on subprojects
368 def rolled_up_versions
368 def rolled_up_versions
369 @rolled_up_versions ||=
369 @rolled_up_versions ||=
370 Version.scoped(:include => :project,
370 Version.scoped(:include => :project,
371 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
371 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
372 end
372 end
373
373
374 # Returns a scope of the Versions used by the project
374 # Returns a scope of the Versions used by the project
375 def shared_versions
375 def shared_versions
376 @shared_versions ||=
376 @shared_versions ||=
377 Version.scoped(:include => :project,
377 Version.scoped(:include => :project,
378 :conditions => "#{Project.table_name}.id = #{id}" +
378 :conditions => "#{Project.table_name}.id = #{id}" +
379 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
379 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
380 " #{Version.table_name}.sharing = 'system'" +
380 " #{Version.table_name}.sharing = 'system'" +
381 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
381 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
382 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
382 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
383 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
383 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
384 "))")
384 "))")
385 end
385 end
386
386
387 # Returns a hash of project users grouped by role
387 # Returns a hash of project users grouped by role
388 def users_by_role
388 def users_by_role
389 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
389 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
390 m.roles.each do |r|
390 m.roles.each do |r|
391 h[r] ||= []
391 h[r] ||= []
392 h[r] << m.user
392 h[r] << m.user
393 end
393 end
394 h
394 h
395 end
395 end
396 end
396 end
397
397
398 # Deletes all project's members
398 # Deletes all project's members
399 def delete_all_members
399 def delete_all_members
400 me, mr = Member.table_name, MemberRole.table_name
400 me, mr = Member.table_name, MemberRole.table_name
401 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
401 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
402 Member.delete_all(['project_id = ?', id])
402 Member.delete_all(['project_id = ?', id])
403 end
403 end
404
404
405 # Users issues can be assigned to
405 # Users issues can be assigned to
406 def assignable_users
406 def assignable_users
407 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
407 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
408 end
408 end
409
409
410 # Returns the mail adresses of users that should be always notified on project events
410 # Returns the mail adresses of users that should be always notified on project events
411 def recipients
411 def recipients
412 notified_users.collect {|user| user.mail}
412 notified_users.collect {|user| user.mail}
413 end
413 end
414
414
415 # Returns the users that should be notified on project events
415 # Returns the users that should be notified on project events
416 def notified_users
416 def notified_users
417 # TODO: User part should be extracted to User#notify_about?
417 # TODO: User part should be extracted to User#notify_about?
418 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
418 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
419 end
419 end
420
420
421 # Returns an array of all custom fields enabled for project issues
421 # Returns an array of all custom fields enabled for project issues
422 # (explictly associated custom fields and custom fields enabled for all projects)
422 # (explictly associated custom fields and custom fields enabled for all projects)
423 def all_issue_custom_fields
423 def all_issue_custom_fields
424 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
424 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
425 end
425 end
426
426
427 def project
427 def project
428 self
428 self
429 end
429 end
430
430
431 def <=>(project)
431 def <=>(project)
432 name.downcase <=> project.name.downcase
432 name.downcase <=> project.name.downcase
433 end
433 end
434
434
435 def to_s
435 def to_s
436 name
436 name
437 end
437 end
438
438
439 # Returns a short description of the projects (first lines)
439 # Returns a short description of the projects (first lines)
440 def short_description(length = 255)
440 def short_description(length = 255)
441 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
441 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
442 end
442 end
443
443
444 def css_classes
444 def css_classes
445 s = 'project'
445 s = 'project'
446 s << ' root' if root?
446 s << ' root' if root?
447 s << ' child' if child?
447 s << ' child' if child?
448 s << (leaf? ? ' leaf' : ' parent')
448 s << (leaf? ? ' leaf' : ' parent')
449 s
449 s
450 end
450 end
451
451
452 # The earliest start date of a project, based on it's issues and versions
452 # The earliest start date of a project, based on it's issues and versions
453 def start_date
453 def start_date
454 [
454 [
455 issues.minimum('start_date'),
455 issues.minimum('start_date'),
456 shared_versions.collect(&:effective_date),
456 shared_versions.collect(&:effective_date),
457 shared_versions.collect(&:start_date)
457 shared_versions.collect(&:start_date)
458 ].flatten.compact.min
458 ].flatten.compact.min
459 end
459 end
460
460
461 # The latest due date of an issue or version
461 # The latest due date of an issue or version
462 def due_date
462 def due_date
463 [
463 [
464 issues.maximum('due_date'),
464 issues.maximum('due_date'),
465 shared_versions.collect(&:effective_date),
465 shared_versions.collect(&:effective_date),
466 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
466 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
467 ].flatten.compact.max
467 ].flatten.compact.max
468 end
468 end
469
469
470 def overdue?
470 def overdue?
471 active? && !due_date.nil? && (due_date < Date.today)
471 active? && !due_date.nil? && (due_date < Date.today)
472 end
472 end
473
473
474 # Returns the percent completed for this project, based on the
474 # Returns the percent completed for this project, based on the
475 # progress on it's versions.
475 # progress on it's versions.
476 def completed_percent(options={:include_subprojects => false})
476 def completed_percent(options={:include_subprojects => false})
477 if options.delete(:include_subprojects)
477 if options.delete(:include_subprojects)
478 total = self_and_descendants.collect(&:completed_percent).sum
478 total = self_and_descendants.collect(&:completed_percent).sum
479
479
480 total / self_and_descendants.count
480 total / self_and_descendants.count
481 else
481 else
482 if versions.count > 0
482 if versions.count > 0
483 total = versions.collect(&:completed_pourcent).sum
483 total = versions.collect(&:completed_pourcent).sum
484
484
485 total / versions.count
485 total / versions.count
486 else
486 else
487 100
487 100
488 end
488 end
489 end
489 end
490 end
490 end
491
491
492 # Return true if this project is allowed to do the specified action.
492 # Return true if this project is allowed to do the specified action.
493 # action can be:
493 # action can be:
494 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
494 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
495 # * a permission Symbol (eg. :edit_project)
495 # * a permission Symbol (eg. :edit_project)
496 def allows_to?(action)
496 def allows_to?(action)
497 if action.is_a? Hash
497 if action.is_a? Hash
498 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
498 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
499 else
499 else
500 allowed_permissions.include? action
500 allowed_permissions.include? action
501 end
501 end
502 end
502 end
503
503
504 def module_enabled?(module_name)
504 def module_enabled?(module_name)
505 module_name = module_name.to_s
505 module_name = module_name.to_s
506 enabled_modules.detect {|m| m.name == module_name}
506 enabled_modules.detect {|m| m.name == module_name}
507 end
507 end
508
508
509 def enabled_module_names=(module_names)
509 def enabled_module_names=(module_names)
510 if module_names && module_names.is_a?(Array)
510 if module_names && module_names.is_a?(Array)
511 module_names = module_names.collect(&:to_s).reject(&:blank?)
511 module_names = module_names.collect(&:to_s).reject(&:blank?)
512 # remove disabled modules
512 # remove disabled modules
513 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
513 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
514 # add new modules
514 # add new modules
515 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
515 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
516 else
516 else
517 enabled_modules.clear
517 enabled_modules.clear
518 end
518 end
519 end
519 end
520
520
521 # Returns an array of the enabled modules names
521 # Returns an array of the enabled modules names
522 def enabled_module_names
522 def enabled_module_names
523 enabled_modules.collect(&:name)
523 enabled_modules.collect(&:name)
524 end
524 end
525
525
526 safe_attributes 'name',
526 safe_attributes 'name',
527 'description',
527 'description',
528 'homepage',
528 'homepage',
529 'is_public',
529 'is_public',
530 'identifier',
530 'identifier',
531 'custom_field_values',
531 'custom_field_values',
532 'custom_fields',
532 'custom_fields',
533 'tracker_ids',
533 'tracker_ids',
534 'issue_custom_field_ids'
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 # Returns an array of projects that are in this project's hierarchy
539 # Returns an array of projects that are in this project's hierarchy
537 #
540 #
538 # Example: parents, children, siblings
541 # Example: parents, children, siblings
539 def hierarchy
542 def hierarchy
540 parents = project.self_and_ancestors || []
543 parents = project.self_and_ancestors || []
541 descendants = project.descendants || []
544 descendants = project.descendants || []
542 project_hierarchy = parents | descendants # Set union
545 project_hierarchy = parents | descendants # Set union
543 end
546 end
544
547
545 # Returns an auto-generated project identifier based on the last identifier used
548 # Returns an auto-generated project identifier based on the last identifier used
546 def self.next_identifier
549 def self.next_identifier
547 p = Project.find(:first, :order => 'created_on DESC')
550 p = Project.find(:first, :order => 'created_on DESC')
548 p.nil? ? nil : p.identifier.to_s.succ
551 p.nil? ? nil : p.identifier.to_s.succ
549 end
552 end
550
553
551 # Copies and saves the Project instance based on the +project+.
554 # Copies and saves the Project instance based on the +project+.
552 # Duplicates the source project's:
555 # Duplicates the source project's:
553 # * Wiki
556 # * Wiki
554 # * Versions
557 # * Versions
555 # * Categories
558 # * Categories
556 # * Issues
559 # * Issues
557 # * Members
560 # * Members
558 # * Queries
561 # * Queries
559 #
562 #
560 # Accepts an +options+ argument to specify what to copy
563 # Accepts an +options+ argument to specify what to copy
561 #
564 #
562 # Examples:
565 # Examples:
563 # project.copy(1) # => copies everything
566 # project.copy(1) # => copies everything
564 # project.copy(1, :only => 'members') # => copies members only
567 # project.copy(1, :only => 'members') # => copies members only
565 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
568 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
566 def copy(project, options={})
569 def copy(project, options={})
567 project = project.is_a?(Project) ? project : Project.find(project)
570 project = project.is_a?(Project) ? project : Project.find(project)
568
571
569 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
572 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
570 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
573 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
571
574
572 Project.transaction do
575 Project.transaction do
573 if save
576 if save
574 reload
577 reload
575 to_be_copied.each do |name|
578 to_be_copied.each do |name|
576 send "copy_#{name}", project
579 send "copy_#{name}", project
577 end
580 end
578 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
581 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
579 save
582 save
580 end
583 end
581 end
584 end
582 end
585 end
583
586
584
587
585 # Copies +project+ and returns the new instance. This will not save
588 # Copies +project+ and returns the new instance. This will not save
586 # the copy
589 # the copy
587 def self.copy_from(project)
590 def self.copy_from(project)
588 begin
591 begin
589 project = project.is_a?(Project) ? project : Project.find(project)
592 project = project.is_a?(Project) ? project : Project.find(project)
590 if project
593 if project
591 # clear unique attributes
594 # clear unique attributes
592 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
595 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
593 copy = Project.new(attributes)
596 copy = Project.new(attributes)
594 copy.enabled_modules = project.enabled_modules
597 copy.enabled_modules = project.enabled_modules
595 copy.trackers = project.trackers
598 copy.trackers = project.trackers
596 copy.custom_values = project.custom_values.collect {|v| v.clone}
599 copy.custom_values = project.custom_values.collect {|v| v.clone}
597 copy.issue_custom_fields = project.issue_custom_fields
600 copy.issue_custom_fields = project.issue_custom_fields
598 return copy
601 return copy
599 else
602 else
600 return nil
603 return nil
601 end
604 end
602 rescue ActiveRecord::RecordNotFound
605 rescue ActiveRecord::RecordNotFound
603 return nil
606 return nil
604 end
607 end
605 end
608 end
606
609
607 # Yields the given block for each project with its level in the tree
610 # Yields the given block for each project with its level in the tree
608 def self.project_tree(projects, &block)
611 def self.project_tree(projects, &block)
609 ancestors = []
612 ancestors = []
610 projects.sort_by(&:lft).each do |project|
613 projects.sort_by(&:lft).each do |project|
611 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
614 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
612 ancestors.pop
615 ancestors.pop
613 end
616 end
614 yield project, ancestors.size
617 yield project, ancestors.size
615 ancestors << project
618 ancestors << project
616 end
619 end
617 end
620 end
618
621
619 private
622 private
620
623
621 # Destroys children before destroying self
624 # Destroys children before destroying self
622 def destroy_children
625 def destroy_children
623 children.each do |child|
626 children.each do |child|
624 child.destroy
627 child.destroy
625 end
628 end
626 end
629 end
627
630
628 # Copies wiki from +project+
631 # Copies wiki from +project+
629 def copy_wiki(project)
632 def copy_wiki(project)
630 # Check that the source project has a wiki first
633 # Check that the source project has a wiki first
631 unless project.wiki.nil?
634 unless project.wiki.nil?
632 self.wiki ||= Wiki.new
635 self.wiki ||= Wiki.new
633 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
636 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
634 wiki_pages_map = {}
637 wiki_pages_map = {}
635 project.wiki.pages.each do |page|
638 project.wiki.pages.each do |page|
636 # Skip pages without content
639 # Skip pages without content
637 next if page.content.nil?
640 next if page.content.nil?
638 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
641 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
639 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
642 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
640 new_wiki_page.content = new_wiki_content
643 new_wiki_page.content = new_wiki_content
641 wiki.pages << new_wiki_page
644 wiki.pages << new_wiki_page
642 wiki_pages_map[page.id] = new_wiki_page
645 wiki_pages_map[page.id] = new_wiki_page
643 end
646 end
644 wiki.save
647 wiki.save
645 # Reproduce page hierarchy
648 # Reproduce page hierarchy
646 project.wiki.pages.each do |page|
649 project.wiki.pages.each do |page|
647 if page.parent_id && wiki_pages_map[page.id]
650 if page.parent_id && wiki_pages_map[page.id]
648 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
651 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
649 wiki_pages_map[page.id].save
652 wiki_pages_map[page.id].save
650 end
653 end
651 end
654 end
652 end
655 end
653 end
656 end
654
657
655 # Copies versions from +project+
658 # Copies versions from +project+
656 def copy_versions(project)
659 def copy_versions(project)
657 project.versions.each do |version|
660 project.versions.each do |version|
658 new_version = Version.new
661 new_version = Version.new
659 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
662 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
660 self.versions << new_version
663 self.versions << new_version
661 end
664 end
662 end
665 end
663
666
664 # Copies issue categories from +project+
667 # Copies issue categories from +project+
665 def copy_issue_categories(project)
668 def copy_issue_categories(project)
666 project.issue_categories.each do |issue_category|
669 project.issue_categories.each do |issue_category|
667 new_issue_category = IssueCategory.new
670 new_issue_category = IssueCategory.new
668 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
671 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
669 self.issue_categories << new_issue_category
672 self.issue_categories << new_issue_category
670 end
673 end
671 end
674 end
672
675
673 # Copies issues from +project+
676 # Copies issues from +project+
674 def copy_issues(project)
677 def copy_issues(project)
675 # Stores the source issue id as a key and the copied issues as the
678 # Stores the source issue id as a key and the copied issues as the
676 # value. Used to map the two togeather for issue relations.
679 # value. Used to map the two togeather for issue relations.
677 issues_map = {}
680 issues_map = {}
678
681
679 # Get issues sorted by root_id, lft so that parent issues
682 # Get issues sorted by root_id, lft so that parent issues
680 # get copied before their children
683 # get copied before their children
681 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
684 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
682 new_issue = Issue.new
685 new_issue = Issue.new
683 new_issue.copy_from(issue)
686 new_issue.copy_from(issue)
684 new_issue.project = self
687 new_issue.project = self
685 # Reassign fixed_versions by name, since names are unique per
688 # Reassign fixed_versions by name, since names are unique per
686 # project and the versions for self are not yet saved
689 # project and the versions for self are not yet saved
687 if issue.fixed_version
690 if issue.fixed_version
688 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
691 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
689 end
692 end
690 # Reassign the category by name, since names are unique per
693 # Reassign the category by name, since names are unique per
691 # project and the categories for self are not yet saved
694 # project and the categories for self are not yet saved
692 if issue.category
695 if issue.category
693 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
696 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
694 end
697 end
695 # Parent issue
698 # Parent issue
696 if issue.parent_id
699 if issue.parent_id
697 if copied_parent = issues_map[issue.parent_id]
700 if copied_parent = issues_map[issue.parent_id]
698 new_issue.parent_issue_id = copied_parent.id
701 new_issue.parent_issue_id = copied_parent.id
699 end
702 end
700 end
703 end
701
704
702 self.issues << new_issue
705 self.issues << new_issue
703 if new_issue.new_record?
706 if new_issue.new_record?
704 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
707 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
705 else
708 else
706 issues_map[issue.id] = new_issue unless new_issue.new_record?
709 issues_map[issue.id] = new_issue unless new_issue.new_record?
707 end
710 end
708 end
711 end
709
712
710 # Relations after in case issues related each other
713 # Relations after in case issues related each other
711 project.issues.each do |issue|
714 project.issues.each do |issue|
712 new_issue = issues_map[issue.id]
715 new_issue = issues_map[issue.id]
713 unless new_issue
716 unless new_issue
714 # Issue was not copied
717 # Issue was not copied
715 next
718 next
716 end
719 end
717
720
718 # Relations
721 # Relations
719 issue.relations_from.each do |source_relation|
722 issue.relations_from.each do |source_relation|
720 new_issue_relation = IssueRelation.new
723 new_issue_relation = IssueRelation.new
721 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
724 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
722 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
725 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
723 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
726 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
724 new_issue_relation.issue_to = source_relation.issue_to
727 new_issue_relation.issue_to = source_relation.issue_to
725 end
728 end
726 new_issue.relations_from << new_issue_relation
729 new_issue.relations_from << new_issue_relation
727 end
730 end
728
731
729 issue.relations_to.each do |source_relation|
732 issue.relations_to.each do |source_relation|
730 new_issue_relation = IssueRelation.new
733 new_issue_relation = IssueRelation.new
731 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
734 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
732 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
735 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
733 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
736 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
734 new_issue_relation.issue_from = source_relation.issue_from
737 new_issue_relation.issue_from = source_relation.issue_from
735 end
738 end
736 new_issue.relations_to << new_issue_relation
739 new_issue.relations_to << new_issue_relation
737 end
740 end
738 end
741 end
739 end
742 end
740
743
741 # Copies members from +project+
744 # Copies members from +project+
742 def copy_members(project)
745 def copy_members(project)
743 # Copy users first, then groups to handle members with inherited and given roles
746 # Copy users first, then groups to handle members with inherited and given roles
744 members_to_copy = []
747 members_to_copy = []
745 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
748 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
746 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
749 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
747
750
748 members_to_copy.each do |member|
751 members_to_copy.each do |member|
749 new_member = Member.new
752 new_member = Member.new
750 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
753 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
751 # only copy non inherited roles
754 # only copy non inherited roles
752 # inherited roles will be added when copying the group membership
755 # inherited roles will be added when copying the group membership
753 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
756 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
754 next if role_ids.empty?
757 next if role_ids.empty?
755 new_member.role_ids = role_ids
758 new_member.role_ids = role_ids
756 new_member.project = self
759 new_member.project = self
757 self.members << new_member
760 self.members << new_member
758 end
761 end
759 end
762 end
760
763
761 # Copies queries from +project+
764 # Copies queries from +project+
762 def copy_queries(project)
765 def copy_queries(project)
763 project.queries.each do |query|
766 project.queries.each do |query|
764 new_query = Query.new
767 new_query = Query.new
765 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
768 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
766 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
769 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
767 new_query.project = self
770 new_query.project = self
768 self.queries << new_query
771 self.queries << new_query
769 end
772 end
770 end
773 end
771
774
772 # Copies boards from +project+
775 # Copies boards from +project+
773 def copy_boards(project)
776 def copy_boards(project)
774 project.boards.each do |board|
777 project.boards.each do |board|
775 new_board = Board.new
778 new_board = Board.new
776 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
779 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
777 new_board.project = self
780 new_board.project = self
778 self.boards << new_board
781 self.boards << new_board
779 end
782 end
780 end
783 end
781
784
782 def allowed_permissions
785 def allowed_permissions
783 @allowed_permissions ||= begin
786 @allowed_permissions ||= begin
784 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
787 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
785 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
788 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
786 end
789 end
787 end
790 end
788
791
789 def allowed_actions
792 def allowed_actions
790 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
793 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
791 end
794 end
792
795
793 # Returns all the active Systemwide and project specific activities
796 # Returns all the active Systemwide and project specific activities
794 def active_activities
797 def active_activities
795 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
798 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
796
799
797 if overridden_activity_ids.empty?
800 if overridden_activity_ids.empty?
798 return TimeEntryActivity.shared.active
801 return TimeEntryActivity.shared.active
799 else
802 else
800 return system_activities_and_project_overrides
803 return system_activities_and_project_overrides
801 end
804 end
802 end
805 end
803
806
804 # Returns all the Systemwide and project specific activities
807 # Returns all the Systemwide and project specific activities
805 # (inactive and active)
808 # (inactive and active)
806 def all_activities
809 def all_activities
807 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
810 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
808
811
809 if overridden_activity_ids.empty?
812 if overridden_activity_ids.empty?
810 return TimeEntryActivity.shared
813 return TimeEntryActivity.shared
811 else
814 else
812 return system_activities_and_project_overrides(true)
815 return system_activities_and_project_overrides(true)
813 end
816 end
814 end
817 end
815
818
816 # Returns the systemwide active activities merged with the project specific overrides
819 # Returns the systemwide active activities merged with the project specific overrides
817 def system_activities_and_project_overrides(include_inactive=false)
820 def system_activities_and_project_overrides(include_inactive=false)
818 if include_inactive
821 if include_inactive
819 return TimeEntryActivity.shared.
822 return TimeEntryActivity.shared.
820 find(:all,
823 find(:all,
821 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
824 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
822 self.time_entry_activities
825 self.time_entry_activities
823 else
826 else
824 return TimeEntryActivity.shared.active.
827 return TimeEntryActivity.shared.active.
825 find(:all,
828 find(:all,
826 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
829 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
827 self.time_entry_activities.active
830 self.time_entry_activities.active
828 end
831 end
829 end
832 end
830
833
831 # Archives subprojects recursively
834 # Archives subprojects recursively
832 def archive!
835 def archive!
833 children.each do |subproject|
836 children.each do |subproject|
834 subproject.send :archive!
837 subproject.send :archive!
835 end
838 end
836 update_attribute :status, STATUS_ARCHIVED
839 update_attribute :status, STATUS_ARCHIVED
837 end
840 end
838 end
841 end
@@ -1,49 +1,64
1 <%= error_messages_for 'project' %>
1 <%= error_messages_for 'project' %>
2
2
3 <div class="box">
3 <div class="box">
4 <!--[form:project]-->
4 <!--[form:project]-->
5 <p><%= f.text_field :name, :required => true, :size => 60 %></p>
5 <p><%= f.text_field :name, :required => true, :size => 60 %></p>
6
6
7 <% unless @project.allowed_parents.compact.empty? %>
7 <% unless @project.allowed_parents.compact.empty? %>
8 <p><%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %></p>
8 <p><%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %></p>
9 <% end %>
9 <% end %>
10
10
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
12 <p><%= f.text_field :identifier, :required => true, :size => 60, :disabled => @project.identifier_frozen? %>
12 <p><%= f.text_field :identifier, :required => true, :size => 60, :disabled => @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
14 <br /><em><%= l(:text_length_between, :min => 1, :max => Project::IDENTIFIER_MAX_LENGTH) %> <%= l(:text_project_identifier_info) %></em>
14 <br /><em><%= l(:text_length_between, :min => 1, :max => Project::IDENTIFIER_MAX_LENGTH) %> <%= l(:text_project_identifier_info) %></em>
15 <% end %></p>
15 <% end %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
17 <p><%= f.check_box :is_public %></p>
17 <p><%= f.check_box :is_public %></p>
18 <%= wikitoolbar_for 'project_description' %>
18 <%= wikitoolbar_for 'project_description' %>
19
19
20 <% @project.custom_field_values.each do |value| %>
20 <% @project.custom_field_values.each do |value| %>
21 <p><%= custom_field_tag_with_label :project, value %></p>
21 <p><%= custom_field_tag_with_label :project, value %></p>
22 <% end %>
22 <% end %>
23 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
23 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
24 </div>
24 </div>
25
25
26 <% if @project.new_record? %>
27 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
28 <% Redmine::AccessControl.available_project_modules.each do |m| %>
29 <label class="floating">
30 <%= check_box_tag 'project[enabled_module_names][]', m, @project.module_enabled?(m), :id => "project_enabled_module_names_#{m}" %>
31 <%= l_or_humanize(m, :prefix => "project_module_") %>
32 </label>
33 <% end %>
34 <%= hidden_field_tag 'project[enabled_module_names][]', '' %>
35 <%= javascript_tag 'observeProjectModules()' %>
36 </fieldset>
37 <% end %>
38
39 <% if @project.new_record? || @project.module_enabled?('issue_tracking') %>
26 <% unless @trackers.empty? %>
40 <% unless @trackers.empty? %>
27 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
41 <fieldset class="box" id="project_trackers"><legend><%=l(:label_tracker_plural)%></legend>
28 <% @trackers.each do |tracker| %>
42 <% @trackers.each do |tracker| %>
29 <label class="floating">
43 <label class="floating">
30 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
44 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
31 <%= tracker %>
45 <%= tracker %>
32 </label>
46 </label>
33 <% end %>
47 <% end %>
34 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
48 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
35 </fieldset>
49 </fieldset>
36 <% end %>
50 <% end %>
37
51
38 <% unless @issue_custom_fields.empty? %>
52 <% unless @issue_custom_fields.empty? %>
39 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
53 <fieldset class="box" id="project_issue_custom_fields"><legend><%=l(:label_custom_field_plural)%></legend>
40 <% @issue_custom_fields.each do |custom_field| %>
54 <% @issue_custom_fields.each do |custom_field| %>
41 <label class="floating">
55 <label class="floating">
42 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
56 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
43 <%= custom_field.name %>
57 <%= custom_field.name %>
44 </label>
58 </label>
45 <% end %>
59 <% end %>
46 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
60 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
47 </fieldset>
61 </fieldset>
48 <% end %>
62 <% end %>
63 <% end %>
49 <!--[eoform:project]-->
64 <!--[eoform:project]-->
@@ -1,19 +1,7
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 <label class="floating">
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 </label>
12 <% end %>
13 <%= hidden_field_tag 'enabled_modules[]', '' %>
14
15 </fieldset>
16
17 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
18 <%= javascript_tag "Form.Element.focus('project_name');" %>
6 <%= javascript_tag "Form.Element.focus('project_name');" %>
19 <% end %>
7 <% end %>
@@ -1,19 +1,19
1 <% form_for :project, @project,
1 <% form_for :project, @project,
2 :url => { :action => 'modules', :id => @project },
2 :url => { :action => 'modules', :id => @project },
3 :html => {:id => 'modules-form'} do |f| %>
3 :html => {:id => 'modules-form'} do |f| %>
4
4
5 <div class="box">
5 <div class="box">
6 <fieldset>
6 <fieldset>
7 <legend><%= l(:text_select_project_modules) %></legend>
7 <legend><%= l(:text_select_project_modules) %></legend>
8
8
9 <% Redmine::AccessControl.available_project_modules.each do |m| %>
9 <% Redmine::AccessControl.available_project_modules.each do |m| %>
10 <p><label><%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) -%>
10 <p><label><%= check_box_tag 'enabled_module_names[]', m, @project.module_enabled?(m) -%>
11 <%= l_or_humanize(m, :prefix => "project_module_") %></label></p>
11 <%= l_or_humanize(m, :prefix => "project_module_") %></label></p>
12 <% end %>
12 <% end %>
13 </fieldset>
13 </fieldset>
14 </div>
14 </div>
15
15
16 <p><%= check_all_links 'modules-form' %></p>
16 <p><%= check_all_links 'modules-form' %></p>
17 <p><%= submit_tag l(:button_save) %></p>
17 <p><%= submit_tag l(:button_save) %></p>
18
18
19 <% end %>
19 <% end %>
@@ -1,36 +1,36
1 <h2><%= @page.pretty_title %></h2>
1 <h2><%= @page.pretty_title %></h2>
2
2
3 <h3><%= l(:label_history) %></h3>
3 <h3><%= l(:label_history) %></h3>
4
4
5 <% form_tag({:action => "diff"}, :method => :get) do %>
5 <% form_tag({:action => "diff"}, :method => :get) do %>
6 <%= hidden_field_tag('project_id', h(@project.to_param)) %>
6 <%= hidden_field_tag('project_id', h(@project.to_param)) %>
7 <table class="list">
7 <table class="list wiki-page-versions">
8 <thead><tr>
8 <thead><tr>
9 <th>#</th>
9 <th>#</th>
10 <th></th>
10 <th></th>
11 <th></th>
11 <th></th>
12 <th><%= l(:field_updated_on) %></th>
12 <th><%= l(:field_updated_on) %></th>
13 <th><%= l(:field_author) %></th>
13 <th><%= l(:field_author) %></th>
14 <th><%= l(:field_comments) %></th>
14 <th><%= l(:field_comments) %></th>
15 <th></th>
15 <th></th>
16 </tr></thead>
16 </tr></thead>
17 <tbody>
17 <tbody>
18 <% show_diff = @versions.size > 1 %>
18 <% show_diff = @versions.size > 1 %>
19 <% line_num = 1 %>
19 <% line_num = 1 %>
20 <% @versions.each do |ver| %>
20 <% @versions.each do |ver| %>
21 <tr class="<%= cycle("odd", "even") %>">
21 <tr class="wiki-page-version <%= cycle("odd", "even") %>">
22 <td class="id"><%= link_to ver.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
22 <td class="id"><%= link_to ver.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
23 <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td>
23 <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td>
24 <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
24 <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
25 <td align="center"><%= format_time(ver.updated_on) %></td>
25 <td class="updated_on"><%= format_time(ver.updated_on) %></td>
26 <td><%= link_to_user ver.author %></td>
26 <td class="author"><%= link_to_user ver.author %></td>
27 <td><%=h ver.comments %></td>
27 <td class="comments"><%=h ver.comments %></td>
28 <td align="center"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
28 <td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
29 </tr>
29 </tr>
30 <% line_num += 1 %>
30 <% line_num += 1 %>
31 <% end %>
31 <% end %>
32 </tbody>
32 </tbody>
33 </table>
33 </table>
34 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
34 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
35 <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
35 <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
36 <% end %>
36 <% end %>
@@ -1,255 +1,273
1 /* redMine - project management software
1 /* redMine - project management software
2 Copyright (C) 2006-2008 Jean-Philippe Lang */
2 Copyright (C) 2006-2008 Jean-Philippe Lang */
3
3
4 function checkAll (id, checked) {
4 function checkAll (id, checked) {
5 var els = Element.descendants(id);
5 var els = Element.descendants(id);
6 for (var i = 0; i < els.length; i++) {
6 for (var i = 0; i < els.length; i++) {
7 if (els[i].disabled==false) {
7 if (els[i].disabled==false) {
8 els[i].checked = checked;
8 els[i].checked = checked;
9 }
9 }
10 }
10 }
11 }
11 }
12
12
13 function toggleCheckboxesBySelector(selector) {
13 function toggleCheckboxesBySelector(selector) {
14 boxes = $$(selector);
14 boxes = $$(selector);
15 var all_checked = true;
15 var all_checked = true;
16 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
16 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
17 for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }
17 for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }
18 }
18 }
19
19
20 function setCheckboxesBySelector(checked, selector) {
20 function setCheckboxesBySelector(checked, selector) {
21 var boxes = $$(selector);
21 var boxes = $$(selector);
22 boxes.each(function(ele) {
22 boxes.each(function(ele) {
23 ele.checked = checked;
23 ele.checked = checked;
24 });
24 });
25 }
25 }
26
26
27 function showAndScrollTo(id, focus) {
27 function showAndScrollTo(id, focus) {
28 Element.show(id);
28 Element.show(id);
29 if (focus!=null) { Form.Element.focus(focus); }
29 if (focus!=null) { Form.Element.focus(focus); }
30 Element.scrollTo(id);
30 Element.scrollTo(id);
31 }
31 }
32
32
33 function toggleRowGroup(el) {
33 function toggleRowGroup(el) {
34 var tr = Element.up(el, 'tr');
34 var tr = Element.up(el, 'tr');
35 var n = Element.next(tr);
35 var n = Element.next(tr);
36 tr.toggleClassName('open');
36 tr.toggleClassName('open');
37 while (n != undefined && !n.hasClassName('group')) {
37 while (n != undefined && !n.hasClassName('group')) {
38 Element.toggle(n);
38 Element.toggle(n);
39 n = Element.next(n);
39 n = Element.next(n);
40 }
40 }
41 }
41 }
42
42
43 function toggleFieldset(el) {
43 function toggleFieldset(el) {
44 var fieldset = Element.up(el, 'fieldset');
44 var fieldset = Element.up(el, 'fieldset');
45 fieldset.toggleClassName('collapsed');
45 fieldset.toggleClassName('collapsed');
46 Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
46 Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
47 }
47 }
48
48
49 var fileFieldCount = 1;
49 var fileFieldCount = 1;
50
50
51 function addFileField() {
51 function addFileField() {
52 if (fileFieldCount >= 10) return false
52 if (fileFieldCount >= 10) return false
53 fileFieldCount++;
53 fileFieldCount++;
54 var f = document.createElement("input");
54 var f = document.createElement("input");
55 f.type = "file";
55 f.type = "file";
56 f.name = "attachments[" + fileFieldCount + "][file]";
56 f.name = "attachments[" + fileFieldCount + "][file]";
57 f.size = 30;
57 f.size = 30;
58 var d = document.createElement("input");
58 var d = document.createElement("input");
59 d.type = "text";
59 d.type = "text";
60 d.name = "attachments[" + fileFieldCount + "][description]";
60 d.name = "attachments[" + fileFieldCount + "][description]";
61 d.size = 60;
61 d.size = 60;
62 var dLabel = new Element('label');
62 var dLabel = new Element('label');
63 dLabel.addClassName('inline');
63 dLabel.addClassName('inline');
64 // Pulls the languge value used for Optional Description
64 // Pulls the languge value used for Optional Description
65 dLabel.update($('attachment_description_label_content').innerHTML)
65 dLabel.update($('attachment_description_label_content').innerHTML)
66 p = document.getElementById("attachments_fields");
66 p = document.getElementById("attachments_fields");
67 p.appendChild(document.createElement("br"));
67 p.appendChild(document.createElement("br"));
68 p.appendChild(f);
68 p.appendChild(f);
69 p.appendChild(dLabel);
69 p.appendChild(dLabel);
70 dLabel.appendChild(d);
70 dLabel.appendChild(d);
71
71
72 }
72 }
73
73
74 function showTab(name) {
74 function showTab(name) {
75 var f = $$('div#content .tab-content');
75 var f = $$('div#content .tab-content');
76 for(var i=0; i<f.length; i++){
76 for(var i=0; i<f.length; i++){
77 Element.hide(f[i]);
77 Element.hide(f[i]);
78 }
78 }
79 var f = $$('div.tabs a');
79 var f = $$('div.tabs a');
80 for(var i=0; i<f.length; i++){
80 for(var i=0; i<f.length; i++){
81 Element.removeClassName(f[i], "selected");
81 Element.removeClassName(f[i], "selected");
82 }
82 }
83 Element.show('tab-content-' + name);
83 Element.show('tab-content-' + name);
84 Element.addClassName('tab-' + name, "selected");
84 Element.addClassName('tab-' + name, "selected");
85 return false;
85 return false;
86 }
86 }
87
87
88 function moveTabRight(el) {
88 function moveTabRight(el) {
89 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
89 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
90 var tabsWidth = 0;
90 var tabsWidth = 0;
91 var i;
91 var i;
92 for (i=0; i<lis.length; i++) {
92 for (i=0; i<lis.length; i++) {
93 if (lis[i].visible()) {
93 if (lis[i].visible()) {
94 tabsWidth += lis[i].getWidth() + 6;
94 tabsWidth += lis[i].getWidth() + 6;
95 }
95 }
96 }
96 }
97 if (tabsWidth < Element.up(el, 'div.tabs').getWidth() - 60) {
97 if (tabsWidth < Element.up(el, 'div.tabs').getWidth() - 60) {
98 return;
98 return;
99 }
99 }
100 i=0;
100 i=0;
101 while (i<lis.length && !lis[i].visible()) {
101 while (i<lis.length && !lis[i].visible()) {
102 i++;
102 i++;
103 }
103 }
104 lis[i].hide();
104 lis[i].hide();
105 }
105 }
106
106
107 function moveTabLeft(el) {
107 function moveTabLeft(el) {
108 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
108 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
109 var i = 0;
109 var i = 0;
110 while (i<lis.length && !lis[i].visible()) {
110 while (i<lis.length && !lis[i].visible()) {
111 i++;
111 i++;
112 }
112 }
113 if (i>0) {
113 if (i>0) {
114 lis[i-1].show();
114 lis[i-1].show();
115 }
115 }
116 }
116 }
117
117
118 function displayTabsButtons() {
118 function displayTabsButtons() {
119 var lis;
119 var lis;
120 var tabsWidth = 0;
120 var tabsWidth = 0;
121 var i;
121 var i;
122 $$('div.tabs').each(function(el) {
122 $$('div.tabs').each(function(el) {
123 lis = el.down('ul').childElements();
123 lis = el.down('ul').childElements();
124 for (i=0; i<lis.length; i++) {
124 for (i=0; i<lis.length; i++) {
125 if (lis[i].visible()) {
125 if (lis[i].visible()) {
126 tabsWidth += lis[i].getWidth() + 6;
126 tabsWidth += lis[i].getWidth() + 6;
127 }
127 }
128 }
128 }
129 if ((tabsWidth < el.getWidth() - 60) && (lis[0].visible())) {
129 if ((tabsWidth < el.getWidth() - 60) && (lis[0].visible())) {
130 el.down('div.tabs-buttons').hide();
130 el.down('div.tabs-buttons').hide();
131 } else {
131 } else {
132 el.down('div.tabs-buttons').show();
132 el.down('div.tabs-buttons').show();
133 }
133 }
134 });
134 });
135 }
135 }
136
136
137 function setPredecessorFieldsVisibility() {
137 function setPredecessorFieldsVisibility() {
138 relationType = $('relation_relation_type');
138 relationType = $('relation_relation_type');
139 if (relationType && (relationType.value == "precedes" || relationType.value == "follows")) {
139 if (relationType && (relationType.value == "precedes" || relationType.value == "follows")) {
140 Element.show('predecessor_fields');
140 Element.show('predecessor_fields');
141 } else {
141 } else {
142 Element.hide('predecessor_fields');
142 Element.hide('predecessor_fields');
143 }
143 }
144 }
144 }
145
145
146 function promptToRemote(text, param, url) {
146 function promptToRemote(text, param, url) {
147 value = prompt(text + ':');
147 value = prompt(text + ':');
148 if (value) {
148 if (value) {
149 new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true});
149 new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true});
150 return false;
150 return false;
151 }
151 }
152 }
152 }
153
153
154 function collapseScmEntry(id) {
154 function collapseScmEntry(id) {
155 var els = document.getElementsByClassName(id, 'browser');
155 var els = document.getElementsByClassName(id, 'browser');
156 for (var i = 0; i < els.length; i++) {
156 for (var i = 0; i < els.length; i++) {
157 if (els[i].hasClassName('open')) {
157 if (els[i].hasClassName('open')) {
158 collapseScmEntry(els[i].id);
158 collapseScmEntry(els[i].id);
159 }
159 }
160 Element.hide(els[i]);
160 Element.hide(els[i]);
161 }
161 }
162 $(id).removeClassName('open');
162 $(id).removeClassName('open');
163 }
163 }
164
164
165 function expandScmEntry(id) {
165 function expandScmEntry(id) {
166 var els = document.getElementsByClassName(id, 'browser');
166 var els = document.getElementsByClassName(id, 'browser');
167 for (var i = 0; i < els.length; i++) {
167 for (var i = 0; i < els.length; i++) {
168 Element.show(els[i]);
168 Element.show(els[i]);
169 if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) {
169 if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) {
170 expandScmEntry(els[i].id);
170 expandScmEntry(els[i].id);
171 }
171 }
172 }
172 }
173 $(id).addClassName('open');
173 $(id).addClassName('open');
174 }
174 }
175
175
176 function scmEntryClick(id) {
176 function scmEntryClick(id) {
177 el = $(id);
177 el = $(id);
178 if (el.hasClassName('open')) {
178 if (el.hasClassName('open')) {
179 collapseScmEntry(id);
179 collapseScmEntry(id);
180 el.addClassName('collapsed');
180 el.addClassName('collapsed');
181 return false;
181 return false;
182 } else if (el.hasClassName('loaded')) {
182 } else if (el.hasClassName('loaded')) {
183 expandScmEntry(id);
183 expandScmEntry(id);
184 el.removeClassName('collapsed');
184 el.removeClassName('collapsed');
185 return false;
185 return false;
186 }
186 }
187 if (el.hasClassName('loading')) {
187 if (el.hasClassName('loading')) {
188 return false;
188 return false;
189 }
189 }
190 el.addClassName('loading');
190 el.addClassName('loading');
191 return true;
191 return true;
192 }
192 }
193
193
194 function scmEntryLoaded(id) {
194 function scmEntryLoaded(id) {
195 Element.addClassName(id, 'open');
195 Element.addClassName(id, 'open');
196 Element.addClassName(id, 'loaded');
196 Element.addClassName(id, 'loaded');
197 Element.removeClassName(id, 'loading');
197 Element.removeClassName(id, 'loading');
198 }
198 }
199
199
200 function randomKey(size) {
200 function randomKey(size) {
201 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
201 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
202 var key = '';
202 var key = '';
203 for (i = 0; i < size; i++) {
203 for (i = 0; i < size; i++) {
204 key += chars[Math.floor(Math.random() * chars.length)];
204 key += chars[Math.floor(Math.random() * chars.length)];
205 }
205 }
206 return key;
206 return key;
207 }
207 }
208
208
209 function observeParentIssueField(url) {
209 function observeParentIssueField(url) {
210 new Ajax.Autocompleter('issue_parent_issue_id',
210 new Ajax.Autocompleter('issue_parent_issue_id',
211 'parent_issue_candidates',
211 'parent_issue_candidates',
212 url,
212 url,
213 { minChars: 3,
213 { minChars: 3,
214 frequency: 0.5,
214 frequency: 0.5,
215 paramName: 'q',
215 paramName: 'q',
216 updateElement: function(value) {
216 updateElement: function(value) {
217 document.getElementById('issue_parent_issue_id').value = value.id;
217 document.getElementById('issue_parent_issue_id').value = value.id;
218 }});
218 }});
219 }
219 }
220
220
221 function observeRelatedIssueField(url) {
221 function observeRelatedIssueField(url) {
222 new Ajax.Autocompleter('relation_issue_to_id',
222 new Ajax.Autocompleter('relation_issue_to_id',
223 'related_issue_candidates',
223 'related_issue_candidates',
224 url,
224 url,
225 { minChars: 3,
225 { minChars: 3,
226 frequency: 0.5,
226 frequency: 0.5,
227 paramName: 'q',
227 paramName: 'q',
228 updateElement: function(value) {
228 updateElement: function(value) {
229 document.getElementById('relation_issue_to_id').value = value.id;
229 document.getElementById('relation_issue_to_id').value = value.id;
230 },
230 },
231 parameters: 'scope=all'
231 parameters: 'scope=all'
232 });
232 });
233 }
233 }
234
234
235 function setVisible(id, visible) {
236 var el = $(id);
237 if (el) {if (visible) {el.show();} else {el.hide();}}
238 }
239
240 function observeProjectModules() {
241 var f = function() {
242 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
243 var c = ($('project_enabled_module_names_issue_tracking').checked == true);
244 setVisible('project_trackers', c);
245 setVisible('project_issue_custom_fields', c);
246 };
247
248 Event.observe(window, 'load', f);
249 Event.observe('project_enabled_module_names_issue_tracking', 'change', f);
250 }
251
252
235 /* shows and hides ajax indicator */
253 /* shows and hides ajax indicator */
236 Ajax.Responders.register({
254 Ajax.Responders.register({
237 onCreate: function(){
255 onCreate: function(){
238 if ($('ajax-indicator') && Ajax.activeRequestCount > 0) {
256 if ($('ajax-indicator') && Ajax.activeRequestCount > 0) {
239 Element.show('ajax-indicator');
257 Element.show('ajax-indicator');
240 }
258 }
241 },
259 },
242 onComplete: function(){
260 onComplete: function(){
243 if ($('ajax-indicator') && Ajax.activeRequestCount == 0) {
261 if ($('ajax-indicator') && Ajax.activeRequestCount == 0) {
244 Element.hide('ajax-indicator');
262 Element.hide('ajax-indicator');
245 }
263 }
246 }
264 }
247 });
265 });
248
266
249 function hideOnLoad() {
267 function hideOnLoad() {
250 $$('.hol').each(function(el) {
268 $$('.hol').each(function(el) {
251 el.hide();
269 el.hide();
252 });
270 });
253 }
271 }
254
272
255 Event.observe(window, 'load', hideOnLoad);
273 Event.observe(window, 'load', hideOnLoad);
@@ -1,950 +1,952
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3
3
4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
5 h1 {margin:0; padding:0; font-size: 24px;}
5 h1 {margin:0; padding:0; font-size: 24px;}
6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
9
9
10 /***** Layout *****/
10 /***** Layout *****/
11 #wrapper {background: white;}
11 #wrapper {background: white;}
12
12
13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 #top-menu ul {margin: 0; padding: 0;}
14 #top-menu ul {margin: 0; padding: 0;}
15 #top-menu li {
15 #top-menu li {
16 float:left;
16 float:left;
17 list-style-type:none;
17 list-style-type:none;
18 margin: 0px 0px 0px 0px;
18 margin: 0px 0px 0px 0px;
19 padding: 0px 0px 0px 0px;
19 padding: 0px 0px 0px 0px;
20 white-space:nowrap;
20 white-space:nowrap;
21 }
21 }
22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24
24
25 #account {float:right;}
25 #account {float:right;}
26
26
27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 #header a {color:#f8f8f8;}
28 #header a {color:#f8f8f8;}
29 #header h1 a.ancestor { font-size: 80%; }
29 #header h1 a.ancestor { font-size: 80%; }
30 #quick-search {float:right;}
30 #quick-search {float:right;}
31
31
32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
32 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 #main-menu ul {margin: 0; padding: 0;}
33 #main-menu ul {margin: 0; padding: 0;}
34 #main-menu li {
34 #main-menu li {
35 float:left;
35 float:left;
36 list-style-type:none;
36 list-style-type:none;
37 margin: 0px 2px 0px 0px;
37 margin: 0px 2px 0px 0px;
38 padding: 0px 0px 0px 0px;
38 padding: 0px 0px 0px 0px;
39 white-space:nowrap;
39 white-space:nowrap;
40 }
40 }
41 #main-menu li a {
41 #main-menu li a {
42 display: block;
42 display: block;
43 color: #fff;
43 color: #fff;
44 text-decoration: none;
44 text-decoration: none;
45 font-weight: bold;
45 font-weight: bold;
46 margin: 0;
46 margin: 0;
47 padding: 4px 10px 4px 10px;
47 padding: 4px 10px 4px 10px;
48 }
48 }
49 #main-menu li a:hover {background:#759FCF; color:#fff;}
49 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
50 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51
51
52 #admin-menu ul {margin: 0; padding: 0;}
52 #admin-menu ul {margin: 0; padding: 0;}
53 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
53 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
54
54
55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
55 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 #admin-menu a.projects { background-image: url(../images/projects.png); }
56 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 #admin-menu a.users { background-image: url(../images/user.png); }
57 #admin-menu a.users { background-image: url(../images/user.png); }
58 #admin-menu a.groups { background-image: url(../images/group.png); }
58 #admin-menu a.groups { background-image: url(../images/group.png); }
59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
59 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
60 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
61 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
62 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
63 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
64 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
65 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
66 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 #admin-menu a.info { background-image: url(../images/help.png); }
67 #admin-menu a.info { background-image: url(../images/help.png); }
68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
68 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69
69
70 #main {background-color:#EEEEEE;}
70 #main {background-color:#EEEEEE;}
71
71
72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
72 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 * html #sidebar{ width: 22%; }
73 * html #sidebar{ width: 22%; }
74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
74 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
75 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
76 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 #sidebar .contextual { margin-right: 1em; }
77 #sidebar .contextual { margin-right: 1em; }
78
78
79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
79 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
80 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 html>body #content { min-height: 600px; }
81 html>body #content { min-height: 600px; }
82 * html body #content { height: 600px; } /* IE */
82 * html body #content { height: 600px; } /* IE */
83
83
84 #main.nosidebar #sidebar{ display: none; }
84 #main.nosidebar #sidebar{ display: none; }
85 #main.nosidebar #content{ width: auto; border-right: 0; }
85 #main.nosidebar #content{ width: auto; border-right: 0; }
86
86
87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
87 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88
88
89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
89 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 #login-form table td {padding: 6px;}
90 #login-form table td {padding: 6px;}
91 #login-form label {font-weight: bold;}
91 #login-form label {font-weight: bold;}
92 #login-form input#username, #login-form input#password { width: 300px; }
92 #login-form input#username, #login-form input#password { width: 300px; }
93
93
94 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
94 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
95
95
96 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
96 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
97
97
98 /***** Links *****/
98 /***** Links *****/
99 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
99 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
100 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
100 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
101 a img{ border: 0; }
101 a img{ border: 0; }
102
102
103 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
103 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
104
104
105 /***** Tables *****/
105 /***** Tables *****/
106 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
106 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
107 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
107 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
108 table.list td { vertical-align: top; }
108 table.list td { vertical-align: top; }
109 table.list td.id { width: 2%; text-align: center;}
109 table.list td.id { width: 2%; text-align: center;}
110 table.list td.checkbox { width: 15px; padding: 0px;}
110 table.list td.checkbox { width: 15px; padding: 0px;}
111 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
111 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
112 table.list td.buttons a { padding-right: 0.6em; }
112 table.list td.buttons a { padding-right: 0.6em; }
113 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
113 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
114
114
115 tr.project td.name a { white-space:nowrap; }
115 tr.project td.name a { white-space:nowrap; }
116
116
117 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
117 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
118 tr.project.idnt-1 td.name {padding-left: 0.5em;}
118 tr.project.idnt-1 td.name {padding-left: 0.5em;}
119 tr.project.idnt-2 td.name {padding-left: 2em;}
119 tr.project.idnt-2 td.name {padding-left: 2em;}
120 tr.project.idnt-3 td.name {padding-left: 3.5em;}
120 tr.project.idnt-3 td.name {padding-left: 3.5em;}
121 tr.project.idnt-4 td.name {padding-left: 5em;}
121 tr.project.idnt-4 td.name {padding-left: 5em;}
122 tr.project.idnt-5 td.name {padding-left: 6.5em;}
122 tr.project.idnt-5 td.name {padding-left: 6.5em;}
123 tr.project.idnt-6 td.name {padding-left: 8em;}
123 tr.project.idnt-6 td.name {padding-left: 8em;}
124 tr.project.idnt-7 td.name {padding-left: 9.5em;}
124 tr.project.idnt-7 td.name {padding-left: 9.5em;}
125 tr.project.idnt-8 td.name {padding-left: 11em;}
125 tr.project.idnt-8 td.name {padding-left: 11em;}
126 tr.project.idnt-9 td.name {padding-left: 12.5em;}
126 tr.project.idnt-9 td.name {padding-left: 12.5em;}
127
127
128 tr.issue { text-align: center; white-space: nowrap; }
128 tr.issue { text-align: center; white-space: nowrap; }
129 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
129 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
130 tr.issue td.subject { text-align: left; }
130 tr.issue td.subject { text-align: left; }
131 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
131 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
132
132
133 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
133 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
134 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
134 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
135 tr.issue.idnt-2 td.subject {padding-left: 2em;}
135 tr.issue.idnt-2 td.subject {padding-left: 2em;}
136 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
136 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
137 tr.issue.idnt-4 td.subject {padding-left: 5em;}
137 tr.issue.idnt-4 td.subject {padding-left: 5em;}
138 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
138 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
139 tr.issue.idnt-6 td.subject {padding-left: 8em;}
139 tr.issue.idnt-6 td.subject {padding-left: 8em;}
140 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
140 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
141 tr.issue.idnt-8 td.subject {padding-left: 11em;}
141 tr.issue.idnt-8 td.subject {padding-left: 11em;}
142 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
142 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
143
143
144 tr.entry { border: 1px solid #f8f8f8; }
144 tr.entry { border: 1px solid #f8f8f8; }
145 tr.entry td { white-space: nowrap; }
145 tr.entry td { white-space: nowrap; }
146 tr.entry td.filename { width: 30%; }
146 tr.entry td.filename { width: 30%; }
147 tr.entry td.size { text-align: right; font-size: 90%; }
147 tr.entry td.size { text-align: right; font-size: 90%; }
148 tr.entry td.revision, tr.entry td.author { text-align: center; }
148 tr.entry td.revision, tr.entry td.author { text-align: center; }
149 tr.entry td.age { text-align: right; }
149 tr.entry td.age { text-align: right; }
150 tr.entry.file td.filename a { margin-left: 16px; }
150 tr.entry.file td.filename a { margin-left: 16px; }
151
151
152 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
152 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
153 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
153 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
154
154
155 tr.changeset td.author { text-align: center; width: 15%; }
155 tr.changeset td.author { text-align: center; width: 15%; }
156 tr.changeset td.committed_on { text-align: center; width: 15%; }
156 tr.changeset td.committed_on { text-align: center; width: 15%; }
157
157
158 table.files tr.file td { text-align: center; }
158 table.files tr.file td { text-align: center; }
159 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
159 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
160 table.files tr.file td.digest { font-size: 80%; }
160 table.files tr.file td.digest { font-size: 80%; }
161
161
162 table.members td.roles, table.memberships td.roles { width: 45%; }
162 table.members td.roles, table.memberships td.roles { width: 45%; }
163
163
164 tr.message { height: 2.6em; }
164 tr.message { height: 2.6em; }
165 tr.message td.subject { padding-left: 20px; }
165 tr.message td.subject { padding-left: 20px; }
166 tr.message td.created_on { white-space: nowrap; }
166 tr.message td.created_on { white-space: nowrap; }
167 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
167 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
168 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
168 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
169 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
169 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
170
170
171 tr.version.closed, tr.version.closed a { color: #999; }
171 tr.version.closed, tr.version.closed a { color: #999; }
172 tr.version td.name { padding-left: 20px; }
172 tr.version td.name { padding-left: 20px; }
173 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
173 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
174 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; }
174 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; }
175
175
176 tr.user td { width:13%; }
176 tr.user td { width:13%; }
177 tr.user td.email { width:18%; }
177 tr.user td.email { width:18%; }
178 tr.user td { white-space: nowrap; }
178 tr.user td { white-space: nowrap; }
179 tr.user.locked, tr.user.registered { color: #aaa; }
179 tr.user.locked, tr.user.registered { color: #aaa; }
180 tr.user.locked a, tr.user.registered a { color: #aaa; }
180 tr.user.locked a, tr.user.registered a { color: #aaa; }
181
181
182 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
183
182 tr.time-entry { text-align: center; white-space: nowrap; }
184 tr.time-entry { text-align: center; white-space: nowrap; }
183 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
185 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
184 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
186 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
185 td.hours .hours-dec { font-size: 0.9em; }
187 td.hours .hours-dec { font-size: 0.9em; }
186
188
187 table.plugins td { vertical-align: middle; }
189 table.plugins td { vertical-align: middle; }
188 table.plugins td.configure { text-align: right; padding-right: 1em; }
190 table.plugins td.configure { text-align: right; padding-right: 1em; }
189 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
191 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
190 table.plugins span.description { display: block; font-size: 0.9em; }
192 table.plugins span.description { display: block; font-size: 0.9em; }
191 table.plugins span.url { display: block; font-size: 0.9em; }
193 table.plugins span.url { display: block; font-size: 0.9em; }
192
194
193 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
195 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
194 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
196 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
195
197
196 table.list tbody tr:hover { background-color:#ffffdd; }
198 table.list tbody tr:hover { background-color:#ffffdd; }
197 table.list tbody tr.group:hover { background-color:inherit; }
199 table.list tbody tr.group:hover { background-color:inherit; }
198 table td {padding:2px;}
200 table td {padding:2px;}
199 table p {margin:0;}
201 table p {margin:0;}
200 .odd {background-color:#f6f7f8;}
202 .odd {background-color:#f6f7f8;}
201 .even {background-color: #fff;}
203 .even {background-color: #fff;}
202
204
203 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
205 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
204 a.sort.asc { background-image: url(../images/sort_asc.png); }
206 a.sort.asc { background-image: url(../images/sort_asc.png); }
205 a.sort.desc { background-image: url(../images/sort_desc.png); }
207 a.sort.desc { background-image: url(../images/sort_desc.png); }
206
208
207 table.attributes { width: 100% }
209 table.attributes { width: 100% }
208 table.attributes th { vertical-align: top; text-align: left; }
210 table.attributes th { vertical-align: top; text-align: left; }
209 table.attributes td { vertical-align: top; }
211 table.attributes td { vertical-align: top; }
210
212
211 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
213 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
212
214
213 td.center {text-align:center;}
215 td.center {text-align:center;}
214
216
215 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
217 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
216
218
217 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
219 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
218 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
220 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
219 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
221 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
220 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
222 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
221
223
222 #watchers ul {margin: 0; padding: 0;}
224 #watchers ul {margin: 0; padding: 0;}
223 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
225 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
224 #watchers select {width: 95%; display: block;}
226 #watchers select {width: 95%; display: block;}
225 #watchers a.delete {opacity: 0.4;}
227 #watchers a.delete {opacity: 0.4;}
226 #watchers a.delete:hover {opacity: 1;}
228 #watchers a.delete:hover {opacity: 1;}
227 #watchers img.gravatar {vertical-align: middle;margin: 0 4px 2px 0;}
229 #watchers img.gravatar {vertical-align: middle;margin: 0 4px 2px 0;}
228
230
229 .highlight { background-color: #FCFD8D;}
231 .highlight { background-color: #FCFD8D;}
230 .highlight.token-1 { background-color: #faa;}
232 .highlight.token-1 { background-color: #faa;}
231 .highlight.token-2 { background-color: #afa;}
233 .highlight.token-2 { background-color: #afa;}
232 .highlight.token-3 { background-color: #aaf;}
234 .highlight.token-3 { background-color: #aaf;}
233
235
234 .box{
236 .box{
235 padding:6px;
237 padding:6px;
236 margin-bottom: 10px;
238 margin-bottom: 10px;
237 background-color:#f6f6f6;
239 background-color:#f6f6f6;
238 color:#505050;
240 color:#505050;
239 line-height:1.5em;
241 line-height:1.5em;
240 border: 1px solid #e4e4e4;
242 border: 1px solid #e4e4e4;
241 }
243 }
242
244
243 div.square {
245 div.square {
244 border: 1px solid #999;
246 border: 1px solid #999;
245 float: left;
247 float: left;
246 margin: .3em .4em 0 .4em;
248 margin: .3em .4em 0 .4em;
247 overflow: hidden;
249 overflow: hidden;
248 width: .6em; height: .6em;
250 width: .6em; height: .6em;
249 }
251 }
250 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
252 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
251 .contextual input, .contextual select {font-size:0.9em;}
253 .contextual input, .contextual select {font-size:0.9em;}
252 .message .contextual { margin-top: 0; }
254 .message .contextual { margin-top: 0; }
253
255
254 .splitcontentleft{float:left; width:49%;}
256 .splitcontentleft{float:left; width:49%;}
255 .splitcontentright{float:right; width:49%;}
257 .splitcontentright{float:right; width:49%;}
256 form {display: inline;}
258 form {display: inline;}
257 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
259 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
258 fieldset {border: 1px solid #e4e4e4; margin:0;}
260 fieldset {border: 1px solid #e4e4e4; margin:0;}
259 legend {color: #484848;}
261 legend {color: #484848;}
260 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
262 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
261 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
263 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
262 blockquote blockquote { margin-left: 0;}
264 blockquote blockquote { margin-left: 0;}
263 acronym { border-bottom: 1px dotted; cursor: help; }
265 acronym { border-bottom: 1px dotted; cursor: help; }
264 textarea.wiki-edit { width: 99%; }
266 textarea.wiki-edit { width: 99%; }
265 li p {margin-top: 0;}
267 li p {margin-top: 0;}
266 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
268 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
267 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
269 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
268 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
270 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
269 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
271 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
270
272
271 div.issue div.subject div div { padding-left: 16px; }
273 div.issue div.subject div div { padding-left: 16px; }
272 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
274 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
273 div.issue div.subject>div>p { margin-top: 0.5em; }
275 div.issue div.subject>div>p { margin-top: 0.5em; }
274 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
276 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
275
277
276 #issue_tree table.issues { border: 0; }
278 #issue_tree table.issues { border: 0; }
277 #issue_tree td.checkbox {display:none;}
279 #issue_tree td.checkbox {display:none;}
278
280
279 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
281 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
280 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
282 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
281 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
283 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
282
284
283 fieldset#date-range p { margin: 2px 0 2px 0; }
285 fieldset#date-range p { margin: 2px 0 2px 0; }
284 fieldset#filters table { border-collapse: collapse; }
286 fieldset#filters table { border-collapse: collapse; }
285 fieldset#filters table td { padding: 0; vertical-align: middle; }
287 fieldset#filters table td { padding: 0; vertical-align: middle; }
286 fieldset#filters tr.filter { height: 2em; }
288 fieldset#filters tr.filter { height: 2em; }
287 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
289 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
288 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
290 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
289
291
290 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
292 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
291 div#issue-changesets div.changeset { padding: 4px;}
293 div#issue-changesets div.changeset { padding: 4px;}
292 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
294 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
293 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
295 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
294
296
295 div#activity dl, #search-results { margin-left: 2em; }
297 div#activity dl, #search-results { margin-left: 2em; }
296 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
298 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
297 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
299 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
298 div#activity dt.me .time { border-bottom: 1px solid #999; }
300 div#activity dt.me .time { border-bottom: 1px solid #999; }
299 div#activity dt .time { color: #777; font-size: 80%; }
301 div#activity dt .time { color: #777; font-size: 80%; }
300 div#activity dd .description, #search-results dd .description { font-style: italic; }
302 div#activity dd .description, #search-results dd .description { font-style: italic; }
301 div#activity span.project:after, #search-results span.project:after { content: " -"; }
303 div#activity span.project:after, #search-results span.project:after { content: " -"; }
302 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
304 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
303
305
304 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
306 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
305
307
306 div#search-results-counts {float:right;}
308 div#search-results-counts {float:right;}
307 div#search-results-counts ul { margin-top: 0.5em; }
309 div#search-results-counts ul { margin-top: 0.5em; }
308 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
310 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
309
311
310 dt.issue { background-image: url(../images/ticket.png); }
312 dt.issue { background-image: url(../images/ticket.png); }
311 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
313 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
312 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
314 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
313 dt.issue-note { background-image: url(../images/ticket_note.png); }
315 dt.issue-note { background-image: url(../images/ticket_note.png); }
314 dt.changeset { background-image: url(../images/changeset.png); }
316 dt.changeset { background-image: url(../images/changeset.png); }
315 dt.news { background-image: url(../images/news.png); }
317 dt.news { background-image: url(../images/news.png); }
316 dt.message { background-image: url(../images/message.png); }
318 dt.message { background-image: url(../images/message.png); }
317 dt.reply { background-image: url(../images/comments.png); }
319 dt.reply { background-image: url(../images/comments.png); }
318 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
320 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
319 dt.attachment { background-image: url(../images/attachment.png); }
321 dt.attachment { background-image: url(../images/attachment.png); }
320 dt.document { background-image: url(../images/document.png); }
322 dt.document { background-image: url(../images/document.png); }
321 dt.project { background-image: url(../images/projects.png); }
323 dt.project { background-image: url(../images/projects.png); }
322 dt.time-entry { background-image: url(../images/time.png); }
324 dt.time-entry { background-image: url(../images/time.png); }
323
325
324 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
326 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
325
327
326 div#roadmap .related-issues { margin-bottom: 1em; }
328 div#roadmap .related-issues { margin-bottom: 1em; }
327 div#roadmap .related-issues td.checkbox { display: none; }
329 div#roadmap .related-issues td.checkbox { display: none; }
328 div#roadmap .wiki h1:first-child { display: none; }
330 div#roadmap .wiki h1:first-child { display: none; }
329 div#roadmap .wiki h1 { font-size: 120%; }
331 div#roadmap .wiki h1 { font-size: 120%; }
330 div#roadmap .wiki h2 { font-size: 110%; }
332 div#roadmap .wiki h2 { font-size: 110%; }
331
333
332 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
334 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
333 div#version-summary fieldset { margin-bottom: 1em; }
335 div#version-summary fieldset { margin-bottom: 1em; }
334 div#version-summary .total-hours { text-align: right; }
336 div#version-summary .total-hours { text-align: right; }
335
337
336 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
338 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
337 table#time-report tbody tr { font-style: italic; color: #777; }
339 table#time-report tbody tr { font-style: italic; color: #777; }
338 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
340 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
339 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
341 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
340 table#time-report .hours-dec { font-size: 0.9em; }
342 table#time-report .hours-dec { font-size: 0.9em; }
341
343
342 form .attributes { margin-bottom: 8px; }
344 form .attributes { margin-bottom: 8px; }
343 form .attributes p { padding-top: 1px; padding-bottom: 2px; }
345 form .attributes p { padding-top: 1px; padding-bottom: 2px; }
344 form .attributes select { min-width: 50%; }
346 form .attributes select { min-width: 50%; }
345
347
346 ul.projects { margin: 0; padding-left: 1em; }
348 ul.projects { margin: 0; padding-left: 1em; }
347 ul.projects.root { margin: 0; padding: 0; }
349 ul.projects.root { margin: 0; padding: 0; }
348 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
350 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
349 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
351 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
350 ul.projects li.child { list-style-type:none; margin-top: 1em;}
352 ul.projects li.child { list-style-type:none; margin-top: 1em;}
351 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
353 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
352 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
354 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
353
355
354 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
356 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
355 #tracker_project_ids li { list-style-type:none; }
357 #tracker_project_ids li { list-style-type:none; }
356
358
357 ul.properties {padding:0; font-size: 0.9em; color: #777;}
359 ul.properties {padding:0; font-size: 0.9em; color: #777;}
358 ul.properties li {list-style-type:none;}
360 ul.properties li {list-style-type:none;}
359 ul.properties li span {font-style:italic;}
361 ul.properties li span {font-style:italic;}
360
362
361 .total-hours { font-size: 110%; font-weight: bold; }
363 .total-hours { font-size: 110%; font-weight: bold; }
362 .total-hours span.hours-int { font-size: 120%; }
364 .total-hours span.hours-int { font-size: 120%; }
363
365
364 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
366 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
365 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
367 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
366
368
367 #workflow_copy_form select { width: 200px; }
369 #workflow_copy_form select { width: 200px; }
368
370
369 .pagination {font-size: 90%}
371 .pagination {font-size: 90%}
370 p.pagination {margin-top:8px;}
372 p.pagination {margin-top:8px;}
371
373
372 /***** Tabular forms ******/
374 /***** Tabular forms ******/
373 .tabular p{
375 .tabular p{
374 margin: 0;
376 margin: 0;
375 padding: 5px 0 8px 0;
377 padding: 5px 0 8px 0;
376 padding-left: 180px; /*width of left column containing the label elements*/
378 padding-left: 180px; /*width of left column containing the label elements*/
377 height: 1%;
379 height: 1%;
378 clear:left;
380 clear:left;
379 }
381 }
380
382
381 html>body .tabular p {overflow:hidden;}
383 html>body .tabular p {overflow:hidden;}
382
384
383 .tabular label{
385 .tabular label{
384 font-weight: bold;
386 font-weight: bold;
385 float: left;
387 float: left;
386 text-align: right;
388 text-align: right;
387 margin-left: -180px; /*width of left column*/
389 margin-left: -180px; /*width of left column*/
388 width: 175px; /*width of labels. Should be smaller than left column to create some right
390 width: 175px; /*width of labels. Should be smaller than left column to create some right
389 margin*/
391 margin*/
390 }
392 }
391
393
392 .tabular label.floating{
394 .tabular label.floating{
393 font-weight: normal;
395 font-weight: normal;
394 margin-left: 0px;
396 margin-left: 0px;
395 text-align: left;
397 text-align: left;
396 width: 270px;
398 width: 270px;
397 }
399 }
398
400
399 .tabular label.block{
401 .tabular label.block{
400 font-weight: normal;
402 font-weight: normal;
401 margin-left: 0px !important;
403 margin-left: 0px !important;
402 text-align: left;
404 text-align: left;
403 float: none;
405 float: none;
404 display: block;
406 display: block;
405 width: auto;
407 width: auto;
406 }
408 }
407
409
408 .tabular label.inline{
410 .tabular label.inline{
409 float:none;
411 float:none;
410 margin-left: 5px !important;
412 margin-left: 5px !important;
411 width: auto;
413 width: auto;
412 }
414 }
413
415
414 input#time_entry_comments { width: 90%;}
416 input#time_entry_comments { width: 90%;}
415
417
416 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
418 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
417
419
418 .tabular.settings p{ padding-left: 300px; }
420 .tabular.settings p{ padding-left: 300px; }
419 .tabular.settings label{ margin-left: -300px; width: 295px; }
421 .tabular.settings label{ margin-left: -300px; width: 295px; }
420 .tabular.settings textarea { width: 99%; }
422 .tabular.settings textarea { width: 99%; }
421
423
422 fieldset.settings label { display: block; }
424 fieldset.settings label { display: block; }
423 .parent { padding-left: 20px; }
425 .parent { padding-left: 20px; }
424
426
425 .required {color: #bb0000;}
427 .required {color: #bb0000;}
426 .summary {font-style: italic;}
428 .summary {font-style: italic;}
427
429
428 #attachments_fields input[type=text] {margin-left: 8px; }
430 #attachments_fields input[type=text] {margin-left: 8px; }
429
431
430 div.attachments { margin-top: 12px; }
432 div.attachments { margin-top: 12px; }
431 div.attachments p { margin:4px 0 2px 0; }
433 div.attachments p { margin:4px 0 2px 0; }
432 div.attachments img { vertical-align: middle; }
434 div.attachments img { vertical-align: middle; }
433 div.attachments span.author { font-size: 0.9em; color: #888; }
435 div.attachments span.author { font-size: 0.9em; color: #888; }
434
436
435 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
437 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
436 .other-formats span + span:before { content: "| "; }
438 .other-formats span + span:before { content: "| "; }
437
439
438 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
440 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
439
441
440 /* Project members tab */
442 /* Project members tab */
441 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
443 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
442 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
444 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
443 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
445 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
444 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
446 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
445 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
447 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
446 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
448 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
447
449
448 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
450 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
449
451
450 input#principal_search, input#user_search {width:100%}
452 input#principal_search, input#user_search {width:100%}
451
453
452 * html div#tab-content-members fieldset div { height: 450px; }
454 * html div#tab-content-members fieldset div { height: 450px; }
453
455
454 /***** Flash & error messages ****/
456 /***** Flash & error messages ****/
455 #errorExplanation, div.flash, .nodata, .warning {
457 #errorExplanation, div.flash, .nodata, .warning {
456 padding: 4px 4px 4px 30px;
458 padding: 4px 4px 4px 30px;
457 margin-bottom: 12px;
459 margin-bottom: 12px;
458 font-size: 1.1em;
460 font-size: 1.1em;
459 border: 2px solid;
461 border: 2px solid;
460 }
462 }
461
463
462 div.flash {margin-top: 8px;}
464 div.flash {margin-top: 8px;}
463
465
464 div.flash.error, #errorExplanation {
466 div.flash.error, #errorExplanation {
465 background: url(../images/exclamation.png) 8px 50% no-repeat;
467 background: url(../images/exclamation.png) 8px 50% no-repeat;
466 background-color: #ffe3e3;
468 background-color: #ffe3e3;
467 border-color: #dd0000;
469 border-color: #dd0000;
468 color: #880000;
470 color: #880000;
469 }
471 }
470
472
471 div.flash.notice {
473 div.flash.notice {
472 background: url(../images/true.png) 8px 5px no-repeat;
474 background: url(../images/true.png) 8px 5px no-repeat;
473 background-color: #dfffdf;
475 background-color: #dfffdf;
474 border-color: #9fcf9f;
476 border-color: #9fcf9f;
475 color: #005f00;
477 color: #005f00;
476 }
478 }
477
479
478 div.flash.warning {
480 div.flash.warning {
479 background: url(../images/warning.png) 8px 5px no-repeat;
481 background: url(../images/warning.png) 8px 5px no-repeat;
480 background-color: #FFEBC1;
482 background-color: #FFEBC1;
481 border-color: #FDBF3B;
483 border-color: #FDBF3B;
482 color: #A6750C;
484 color: #A6750C;
483 text-align: left;
485 text-align: left;
484 }
486 }
485
487
486 .nodata, .warning {
488 .nodata, .warning {
487 text-align: center;
489 text-align: center;
488 background-color: #FFEBC1;
490 background-color: #FFEBC1;
489 border-color: #FDBF3B;
491 border-color: #FDBF3B;
490 color: #A6750C;
492 color: #A6750C;
491 }
493 }
492
494
493 #errorExplanation ul { font-size: 0.9em;}
495 #errorExplanation ul { font-size: 0.9em;}
494 #errorExplanation h2, #errorExplanation p { display: none; }
496 #errorExplanation h2, #errorExplanation p { display: none; }
495
497
496 /***** Ajax indicator ******/
498 /***** Ajax indicator ******/
497 #ajax-indicator {
499 #ajax-indicator {
498 position: absolute; /* fixed not supported by IE */
500 position: absolute; /* fixed not supported by IE */
499 background-color:#eee;
501 background-color:#eee;
500 border: 1px solid #bbb;
502 border: 1px solid #bbb;
501 top:35%;
503 top:35%;
502 left:40%;
504 left:40%;
503 width:20%;
505 width:20%;
504 font-weight:bold;
506 font-weight:bold;
505 text-align:center;
507 text-align:center;
506 padding:0.6em;
508 padding:0.6em;
507 z-index:100;
509 z-index:100;
508 filter:alpha(opacity=50);
510 filter:alpha(opacity=50);
509 opacity: 0.5;
511 opacity: 0.5;
510 }
512 }
511
513
512 html>body #ajax-indicator { position: fixed; }
514 html>body #ajax-indicator { position: fixed; }
513
515
514 #ajax-indicator span {
516 #ajax-indicator span {
515 background-position: 0% 40%;
517 background-position: 0% 40%;
516 background-repeat: no-repeat;
518 background-repeat: no-repeat;
517 background-image: url(../images/loading.gif);
519 background-image: url(../images/loading.gif);
518 padding-left: 26px;
520 padding-left: 26px;
519 vertical-align: bottom;
521 vertical-align: bottom;
520 }
522 }
521
523
522 /***** Calendar *****/
524 /***** Calendar *****/
523 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
525 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
524 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
526 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
525 table.cal thead th.week-number {width: auto;}
527 table.cal thead th.week-number {width: auto;}
526 table.cal tbody tr {height: 100px;}
528 table.cal tbody tr {height: 100px;}
527 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
529 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
528 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
530 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
529 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
531 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
530 table.cal td.odd p.day-num {color: #bbb;}
532 table.cal td.odd p.day-num {color: #bbb;}
531 table.cal td.today {background:#ffffdd;}
533 table.cal td.today {background:#ffffdd;}
532 table.cal td.today p.day-num {font-weight: bold;}
534 table.cal td.today p.day-num {font-weight: bold;}
533 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
535 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
534 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
536 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
535 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
537 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
536 p.cal.legend span {display:block;}
538 p.cal.legend span {display:block;}
537
539
538 /***** Tooltips ******/
540 /***** Tooltips ******/
539 .tooltip{position:relative;z-index:24;}
541 .tooltip{position:relative;z-index:24;}
540 .tooltip:hover{z-index:25;color:#000;}
542 .tooltip:hover{z-index:25;color:#000;}
541 .tooltip span.tip{display: none; text-align:left;}
543 .tooltip span.tip{display: none; text-align:left;}
542
544
543 div.tooltip:hover span.tip{
545 div.tooltip:hover span.tip{
544 display:block;
546 display:block;
545 position:absolute;
547 position:absolute;
546 top:12px; left:24px; width:270px;
548 top:12px; left:24px; width:270px;
547 border:1px solid #555;
549 border:1px solid #555;
548 background-color:#fff;
550 background-color:#fff;
549 padding: 4px;
551 padding: 4px;
550 font-size: 0.8em;
552 font-size: 0.8em;
551 color:#505050;
553 color:#505050;
552 }
554 }
553
555
554 /***** Progress bar *****/
556 /***** Progress bar *****/
555 table.progress {
557 table.progress {
556 border: 1px solid #D7D7D7;
558 border: 1px solid #D7D7D7;
557 border-collapse: collapse;
559 border-collapse: collapse;
558 border-spacing: 0pt;
560 border-spacing: 0pt;
559 empty-cells: show;
561 empty-cells: show;
560 text-align: center;
562 text-align: center;
561 float:left;
563 float:left;
562 margin: 1px 6px 1px 0px;
564 margin: 1px 6px 1px 0px;
563 }
565 }
564
566
565 table.progress td { height: 0.9em; }
567 table.progress td { height: 0.9em; }
566 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
568 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
567 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
569 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
568 table.progress td.open { background: #FFF none repeat scroll 0%; }
570 table.progress td.open { background: #FFF none repeat scroll 0%; }
569 p.pourcent {font-size: 80%;}
571 p.pourcent {font-size: 80%;}
570 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
572 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
571
573
572 /***** Tabs *****/
574 /***** Tabs *****/
573 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
575 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
574 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
576 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
575 #content .tabs ul li {
577 #content .tabs ul li {
576 float:left;
578 float:left;
577 list-style-type:none;
579 list-style-type:none;
578 white-space:nowrap;
580 white-space:nowrap;
579 margin-right:8px;
581 margin-right:8px;
580 background:#fff;
582 background:#fff;
581 position:relative;
583 position:relative;
582 margin-bottom:-1px;
584 margin-bottom:-1px;
583 }
585 }
584 #content .tabs ul li a{
586 #content .tabs ul li a{
585 display:block;
587 display:block;
586 font-size: 0.9em;
588 font-size: 0.9em;
587 text-decoration:none;
589 text-decoration:none;
588 line-height:1.3em;
590 line-height:1.3em;
589 padding:4px 6px 4px 6px;
591 padding:4px 6px 4px 6px;
590 border: 1px solid #ccc;
592 border: 1px solid #ccc;
591 border-bottom: 1px solid #bbbbbb;
593 border-bottom: 1px solid #bbbbbb;
592 background-color: #eeeeee;
594 background-color: #eeeeee;
593 color:#777;
595 color:#777;
594 font-weight:bold;
596 font-weight:bold;
595 }
597 }
596
598
597 #content .tabs ul li a:hover {
599 #content .tabs ul li a:hover {
598 background-color: #ffffdd;
600 background-color: #ffffdd;
599 text-decoration:none;
601 text-decoration:none;
600 }
602 }
601
603
602 #content .tabs ul li a.selected {
604 #content .tabs ul li a.selected {
603 background-color: #fff;
605 background-color: #fff;
604 border: 1px solid #bbbbbb;
606 border: 1px solid #bbbbbb;
605 border-bottom: 1px solid #fff;
607 border-bottom: 1px solid #fff;
606 }
608 }
607
609
608 #content .tabs ul li a.selected:hover {
610 #content .tabs ul li a.selected:hover {
609 background-color: #fff;
611 background-color: #fff;
610 }
612 }
611
613
612 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
614 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
613
615
614 button.tab-left, button.tab-right {
616 button.tab-left, button.tab-right {
615 font-size: 0.9em;
617 font-size: 0.9em;
616 cursor: pointer;
618 cursor: pointer;
617 height:24px;
619 height:24px;
618 border: 1px solid #ccc;
620 border: 1px solid #ccc;
619 border-bottom: 1px solid #bbbbbb;
621 border-bottom: 1px solid #bbbbbb;
620 position:absolute;
622 position:absolute;
621 padding:4px;
623 padding:4px;
622 width: 20px;
624 width: 20px;
623 bottom: -1px;
625 bottom: -1px;
624 }
626 }
625
627
626 button.tab-left {
628 button.tab-left {
627 right: 20px;
629 right: 20px;
628 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
630 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
629 }
631 }
630
632
631 button.tab-right {
633 button.tab-right {
632 right: 0;
634 right: 0;
633 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
635 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
634 }
636 }
635
637
636 /***** Auto-complete *****/
638 /***** Auto-complete *****/
637 div.autocomplete {
639 div.autocomplete {
638 position:absolute;
640 position:absolute;
639 width:400px;
641 width:400px;
640 margin:0;
642 margin:0;
641 padding:0;
643 padding:0;
642 }
644 }
643 div.autocomplete ul {
645 div.autocomplete ul {
644 list-style-type:none;
646 list-style-type:none;
645 margin:0;
647 margin:0;
646 padding:0;
648 padding:0;
647 }
649 }
648 div.autocomplete ul li {
650 div.autocomplete ul li {
649 list-style-type:none;
651 list-style-type:none;
650 display:block;
652 display:block;
651 margin:-1px 0 0 0;
653 margin:-1px 0 0 0;
652 padding:2px;
654 padding:2px;
653 cursor:pointer;
655 cursor:pointer;
654 font-size: 90%;
656 font-size: 90%;
655 border: 1px solid #ccc;
657 border: 1px solid #ccc;
656 border-left: 1px solid #ccc;
658 border-left: 1px solid #ccc;
657 border-right: 1px solid #ccc;
659 border-right: 1px solid #ccc;
658 background-color:white;
660 background-color:white;
659 }
661 }
660 div.autocomplete ul li.selected { background-color: #ffb;}
662 div.autocomplete ul li.selected { background-color: #ffb;}
661 div.autocomplete ul li span.informal {
663 div.autocomplete ul li span.informal {
662 font-size: 80%;
664 font-size: 80%;
663 color: #aaa;
665 color: #aaa;
664 }
666 }
665
667
666 #parent_issue_candidates ul li {width: 500px;}
668 #parent_issue_candidates ul li {width: 500px;}
667 #related_issue_candidates ul li {width: 500px;}
669 #related_issue_candidates ul li {width: 500px;}
668
670
669 /***** Diff *****/
671 /***** Diff *****/
670 .diff_out { background: #fcc; }
672 .diff_out { background: #fcc; }
671 .diff_in { background: #cfc; }
673 .diff_in { background: #cfc; }
672
674
673 /***** Wiki *****/
675 /***** Wiki *****/
674 div.wiki table {
676 div.wiki table {
675 border: 1px solid #505050;
677 border: 1px solid #505050;
676 border-collapse: collapse;
678 border-collapse: collapse;
677 margin-bottom: 1em;
679 margin-bottom: 1em;
678 }
680 }
679
681
680 div.wiki table, div.wiki td, div.wiki th {
682 div.wiki table, div.wiki td, div.wiki th {
681 border: 1px solid #bbb;
683 border: 1px solid #bbb;
682 padding: 4px;
684 padding: 4px;
683 }
685 }
684
686
685 div.wiki .external {
687 div.wiki .external {
686 background-position: 0% 60%;
688 background-position: 0% 60%;
687 background-repeat: no-repeat;
689 background-repeat: no-repeat;
688 padding-left: 12px;
690 padding-left: 12px;
689 background-image: url(../images/external.png);
691 background-image: url(../images/external.png);
690 }
692 }
691
693
692 div.wiki a.new {
694 div.wiki a.new {
693 color: #b73535;
695 color: #b73535;
694 }
696 }
695
697
696 div.wiki pre {
698 div.wiki pre {
697 margin: 1em 1em 1em 1.6em;
699 margin: 1em 1em 1em 1.6em;
698 padding: 2px 2px 2px 0;
700 padding: 2px 2px 2px 0;
699 background-color: #fafafa;
701 background-color: #fafafa;
700 border: 1px solid #dadada;
702 border: 1px solid #dadada;
701 width:auto;
703 width:auto;
702 overflow-x: auto;
704 overflow-x: auto;
703 overflow-y: hidden;
705 overflow-y: hidden;
704 }
706 }
705
707
706 div.wiki ul.toc {
708 div.wiki ul.toc {
707 background-color: #ffffdd;
709 background-color: #ffffdd;
708 border: 1px solid #e4e4e4;
710 border: 1px solid #e4e4e4;
709 padding: 4px;
711 padding: 4px;
710 line-height: 1.2em;
712 line-height: 1.2em;
711 margin-bottom: 12px;
713 margin-bottom: 12px;
712 margin-right: 12px;
714 margin-right: 12px;
713 margin-left: 0;
715 margin-left: 0;
714 display: table
716 display: table
715 }
717 }
716 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
718 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
717
719
718 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
720 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
719 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
721 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
720 div.wiki ul.toc ul { margin: 0; padding: 0; }
722 div.wiki ul.toc ul { margin: 0; padding: 0; }
721 div.wiki ul.toc li { list-style-type:none; margin: 0;}
723 div.wiki ul.toc li { list-style-type:none; margin: 0;}
722 div.wiki ul.toc li li { margin-left: 1.5em; }
724 div.wiki ul.toc li li { margin-left: 1.5em; }
723 div.wiki ul.toc li li li { font-size: 0.8em; }
725 div.wiki ul.toc li li li { font-size: 0.8em; }
724
726
725 div.wiki ul.toc a {
727 div.wiki ul.toc a {
726 font-size: 0.9em;
728 font-size: 0.9em;
727 font-weight: normal;
729 font-weight: normal;
728 text-decoration: none;
730 text-decoration: none;
729 color: #606060;
731 color: #606060;
730 }
732 }
731 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
733 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
732
734
733 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
735 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
734 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
736 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
735 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
737 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
736
738
737 div.wiki img { vertical-align: middle; }
739 div.wiki img { vertical-align: middle; }
738
740
739 /***** My page layout *****/
741 /***** My page layout *****/
740 .block-receiver {
742 .block-receiver {
741 border:1px dashed #c0c0c0;
743 border:1px dashed #c0c0c0;
742 margin-bottom: 20px;
744 margin-bottom: 20px;
743 padding: 15px 0 15px 0;
745 padding: 15px 0 15px 0;
744 }
746 }
745
747
746 .mypage-box {
748 .mypage-box {
747 margin:0 0 20px 0;
749 margin:0 0 20px 0;
748 color:#505050;
750 color:#505050;
749 line-height:1.5em;
751 line-height:1.5em;
750 }
752 }
751
753
752 .handle {
754 .handle {
753 cursor: move;
755 cursor: move;
754 }
756 }
755
757
756 a.close-icon {
758 a.close-icon {
757 display:block;
759 display:block;
758 margin-top:3px;
760 margin-top:3px;
759 overflow:hidden;
761 overflow:hidden;
760 width:12px;
762 width:12px;
761 height:12px;
763 height:12px;
762 background-repeat: no-repeat;
764 background-repeat: no-repeat;
763 cursor:pointer;
765 cursor:pointer;
764 background-image:url('../images/close.png');
766 background-image:url('../images/close.png');
765 }
767 }
766
768
767 a.close-icon:hover {
769 a.close-icon:hover {
768 background-image:url('../images/close_hl.png');
770 background-image:url('../images/close_hl.png');
769 }
771 }
770
772
771 /***** Gantt chart *****/
773 /***** Gantt chart *****/
772 .gantt_hdr {
774 .gantt_hdr {
773 position:absolute;
775 position:absolute;
774 top:0;
776 top:0;
775 height:16px;
777 height:16px;
776 border-top: 1px solid #c0c0c0;
778 border-top: 1px solid #c0c0c0;
777 border-bottom: 1px solid #c0c0c0;
779 border-bottom: 1px solid #c0c0c0;
778 border-right: 1px solid #c0c0c0;
780 border-right: 1px solid #c0c0c0;
779 text-align: center;
781 text-align: center;
780 overflow: hidden;
782 overflow: hidden;
781 }
783 }
782
784
783 .gantt_subjects { font-size: 0.8em; }
785 .gantt_subjects { font-size: 0.8em; }
784
786
785 .task {
787 .task {
786 position: absolute;
788 position: absolute;
787 height:8px;
789 height:8px;
788 font-size:0.8em;
790 font-size:0.8em;
789 color:#888;
791 color:#888;
790 padding:0;
792 padding:0;
791 margin:0;
793 margin:0;
792 line-height:0.8em;
794 line-height:0.8em;
793 white-space:nowrap;
795 white-space:nowrap;
794 }
796 }
795
797
796 .task.label {width:100%;}
798 .task.label {width:100%;}
797 .task.label.project, .task.label.version { font-weight: bold; }
799 .task.label.project, .task.label.version { font-weight: bold; }
798
800
799 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
801 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
800 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
802 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
801 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
803 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
802
804
803 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
805 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
804 .task_late.parent, .task_done.parent { height: 3px;}
806 .task_late.parent, .task_done.parent { height: 3px;}
805 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
807 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
806 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
808 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
807
809
808 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
810 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
809 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
811 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
810 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
812 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
811 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
813 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
812
814
813 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
815 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
814 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
816 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
815 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
817 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
816 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
818 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
817
819
818 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
820 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
819 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
821 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
820
822
821 /***** Icons *****/
823 /***** Icons *****/
822 .icon {
824 .icon {
823 background-position: 0% 50%;
825 background-position: 0% 50%;
824 background-repeat: no-repeat;
826 background-repeat: no-repeat;
825 padding-left: 20px;
827 padding-left: 20px;
826 padding-top: 2px;
828 padding-top: 2px;
827 padding-bottom: 3px;
829 padding-bottom: 3px;
828 }
830 }
829
831
830 .icon-add { background-image: url(../images/add.png); }
832 .icon-add { background-image: url(../images/add.png); }
831 .icon-edit { background-image: url(../images/edit.png); }
833 .icon-edit { background-image: url(../images/edit.png); }
832 .icon-copy { background-image: url(../images/copy.png); }
834 .icon-copy { background-image: url(../images/copy.png); }
833 .icon-duplicate { background-image: url(../images/duplicate.png); }
835 .icon-duplicate { background-image: url(../images/duplicate.png); }
834 .icon-del { background-image: url(../images/delete.png); }
836 .icon-del { background-image: url(../images/delete.png); }
835 .icon-move { background-image: url(../images/move.png); }
837 .icon-move { background-image: url(../images/move.png); }
836 .icon-save { background-image: url(../images/save.png); }
838 .icon-save { background-image: url(../images/save.png); }
837 .icon-cancel { background-image: url(../images/cancel.png); }
839 .icon-cancel { background-image: url(../images/cancel.png); }
838 .icon-multiple { background-image: url(../images/table_multiple.png); }
840 .icon-multiple { background-image: url(../images/table_multiple.png); }
839 .icon-folder { background-image: url(../images/folder.png); }
841 .icon-folder { background-image: url(../images/folder.png); }
840 .open .icon-folder { background-image: url(../images/folder_open.png); }
842 .open .icon-folder { background-image: url(../images/folder_open.png); }
841 .icon-package { background-image: url(../images/package.png); }
843 .icon-package { background-image: url(../images/package.png); }
842 .icon-home { background-image: url(../images/home.png); }
844 .icon-home { background-image: url(../images/home.png); }
843 .icon-user { background-image: url(../images/user.png); }
845 .icon-user { background-image: url(../images/user.png); }
844 .icon-projects { background-image: url(../images/projects.png); }
846 .icon-projects { background-image: url(../images/projects.png); }
845 .icon-help { background-image: url(../images/help.png); }
847 .icon-help { background-image: url(../images/help.png); }
846 .icon-attachment { background-image: url(../images/attachment.png); }
848 .icon-attachment { background-image: url(../images/attachment.png); }
847 .icon-history { background-image: url(../images/history.png); }
849 .icon-history { background-image: url(../images/history.png); }
848 .icon-time { background-image: url(../images/time.png); }
850 .icon-time { background-image: url(../images/time.png); }
849 .icon-time-add { background-image: url(../images/time_add.png); }
851 .icon-time-add { background-image: url(../images/time_add.png); }
850 .icon-stats { background-image: url(../images/stats.png); }
852 .icon-stats { background-image: url(../images/stats.png); }
851 .icon-warning { background-image: url(../images/warning.png); }
853 .icon-warning { background-image: url(../images/warning.png); }
852 .icon-fav { background-image: url(../images/fav.png); }
854 .icon-fav { background-image: url(../images/fav.png); }
853 .icon-fav-off { background-image: url(../images/fav_off.png); }
855 .icon-fav-off { background-image: url(../images/fav_off.png); }
854 .icon-reload { background-image: url(../images/reload.png); }
856 .icon-reload { background-image: url(../images/reload.png); }
855 .icon-lock { background-image: url(../images/locked.png); }
857 .icon-lock { background-image: url(../images/locked.png); }
856 .icon-unlock { background-image: url(../images/unlock.png); }
858 .icon-unlock { background-image: url(../images/unlock.png); }
857 .icon-checked { background-image: url(../images/true.png); }
859 .icon-checked { background-image: url(../images/true.png); }
858 .icon-details { background-image: url(../images/zoom_in.png); }
860 .icon-details { background-image: url(../images/zoom_in.png); }
859 .icon-report { background-image: url(../images/report.png); }
861 .icon-report { background-image: url(../images/report.png); }
860 .icon-comment { background-image: url(../images/comment.png); }
862 .icon-comment { background-image: url(../images/comment.png); }
861 .icon-summary { background-image: url(../images/lightning.png); }
863 .icon-summary { background-image: url(../images/lightning.png); }
862 .icon-server-authentication { background-image: url(../images/server_key.png); }
864 .icon-server-authentication { background-image: url(../images/server_key.png); }
863 .icon-issue { background-image: url(../images/ticket.png); }
865 .icon-issue { background-image: url(../images/ticket.png); }
864 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
866 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
865 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
867 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
866
868
867 .icon-file { background-image: url(../images/files/default.png); }
869 .icon-file { background-image: url(../images/files/default.png); }
868 .icon-file.text-plain { background-image: url(../images/files/text.png); }
870 .icon-file.text-plain { background-image: url(../images/files/text.png); }
869 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
871 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
870 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
872 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
871 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
873 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
872 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
874 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
873 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
875 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
874 .icon-file.image-gif { background-image: url(../images/files/image.png); }
876 .icon-file.image-gif { background-image: url(../images/files/image.png); }
875 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
877 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
876 .icon-file.image-png { background-image: url(../images/files/image.png); }
878 .icon-file.image-png { background-image: url(../images/files/image.png); }
877 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
879 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
878 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
880 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
879 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
881 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
880 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
882 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
881
883
882 img.gravatar {
884 img.gravatar {
883 padding: 2px;
885 padding: 2px;
884 border: solid 1px #d5d5d5;
886 border: solid 1px #d5d5d5;
885 background: #fff;
887 background: #fff;
886 }
888 }
887
889
888 div.issue img.gravatar {
890 div.issue img.gravatar {
889 float: right;
891 float: right;
890 margin: 0 0 0 1em;
892 margin: 0 0 0 1em;
891 padding: 5px;
893 padding: 5px;
892 }
894 }
893
895
894 div.issue table img.gravatar {
896 div.issue table img.gravatar {
895 height: 14px;
897 height: 14px;
896 width: 14px;
898 width: 14px;
897 padding: 2px;
899 padding: 2px;
898 float: left;
900 float: left;
899 margin: 0 0.5em 0 0;
901 margin: 0 0.5em 0 0;
900 }
902 }
901
903
902 h2 img.gravatar {
904 h2 img.gravatar {
903 padding: 3px;
905 padding: 3px;
904 margin: -2px 4px -4px 0;
906 margin: -2px 4px -4px 0;
905 vertical-align: top;
907 vertical-align: top;
906 }
908 }
907
909
908 h4 img.gravatar {
910 h4 img.gravatar {
909 padding: 3px;
911 padding: 3px;
910 margin: -6px 0 -4px 0;
912 margin: -6px 0 -4px 0;
911 vertical-align: top;
913 vertical-align: top;
912 }
914 }
913
915
914 td.username img.gravatar {
916 td.username img.gravatar {
915 float: left;
917 float: left;
916 margin: 0 1em 0 0;
918 margin: 0 1em 0 0;
917 }
919 }
918
920
919 #activity dt img.gravatar {
921 #activity dt img.gravatar {
920 float: left;
922 float: left;
921 margin: 0 1em 1em 0;
923 margin: 0 1em 1em 0;
922 }
924 }
923
925
924 /* Used on 12px Gravatar img tags without the icon background */
926 /* Used on 12px Gravatar img tags without the icon background */
925 .icon-gravatar {
927 .icon-gravatar {
926 float: left;
928 float: left;
927 margin-right: 4px;
929 margin-right: 4px;
928 }
930 }
929
931
930 #activity dt,
932 #activity dt,
931 .journal {
933 .journal {
932 clear: left;
934 clear: left;
933 }
935 }
934
936
935 .journal-link {
937 .journal-link {
936 float: right;
938 float: right;
937 }
939 }
938
940
939 h2 img { vertical-align:middle; }
941 h2 img { vertical-align:middle; }
940
942
941 .hascontextmenu { cursor: context-menu; }
943 .hascontextmenu { cursor: context-menu; }
942
944
943 /***** Media print specific styles *****/
945 /***** Media print specific styles *****/
944 @media print {
946 @media print {
945 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
947 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
946 #main { background: #fff; }
948 #main { background: #fff; }
947 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
949 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
948 #wiki_add_attachment { display:none; }
950 #wiki_add_attachment { display:none; }
949 .hide-when-print { display: none; }
951 .hide-when-print { display: none; }
950 }
952 }
@@ -1,187 +1,188
1 ---
1 ---
2 roles_001:
2 roles_001:
3 name: Manager
3 name: Manager
4 id: 1
4 id: 1
5 builtin: 0
5 builtin: 0
6 permissions: |
6 permissions: |
7 ---
7 ---
8 - :add_project
8 - :add_project
9 - :edit_project
9 - :edit_project
10 - :select_project_modules
10 - :manage_members
11 - :manage_members
11 - :manage_versions
12 - :manage_versions
12 - :manage_categories
13 - :manage_categories
13 - :view_issues
14 - :view_issues
14 - :add_issues
15 - :add_issues
15 - :edit_issues
16 - :edit_issues
16 - :manage_issue_relations
17 - :manage_issue_relations
17 - :manage_subtasks
18 - :manage_subtasks
18 - :add_issue_notes
19 - :add_issue_notes
19 - :move_issues
20 - :move_issues
20 - :delete_issues
21 - :delete_issues
21 - :view_issue_watchers
22 - :view_issue_watchers
22 - :add_issue_watchers
23 - :add_issue_watchers
23 - :delete_issue_watchers
24 - :delete_issue_watchers
24 - :manage_public_queries
25 - :manage_public_queries
25 - :save_queries
26 - :save_queries
26 - :view_gantt
27 - :view_gantt
27 - :view_calendar
28 - :view_calendar
28 - :log_time
29 - :log_time
29 - :view_time_entries
30 - :view_time_entries
30 - :edit_time_entries
31 - :edit_time_entries
31 - :delete_time_entries
32 - :delete_time_entries
32 - :manage_news
33 - :manage_news
33 - :comment_news
34 - :comment_news
34 - :view_documents
35 - :view_documents
35 - :manage_documents
36 - :manage_documents
36 - :view_wiki_pages
37 - :view_wiki_pages
37 - :export_wiki_pages
38 - :export_wiki_pages
38 - :view_wiki_edits
39 - :view_wiki_edits
39 - :edit_wiki_pages
40 - :edit_wiki_pages
40 - :delete_wiki_pages_attachments
41 - :delete_wiki_pages_attachments
41 - :protect_wiki_pages
42 - :protect_wiki_pages
42 - :delete_wiki_pages
43 - :delete_wiki_pages
43 - :rename_wiki_pages
44 - :rename_wiki_pages
44 - :add_messages
45 - :add_messages
45 - :edit_messages
46 - :edit_messages
46 - :delete_messages
47 - :delete_messages
47 - :manage_boards
48 - :manage_boards
48 - :view_files
49 - :view_files
49 - :manage_files
50 - :manage_files
50 - :browse_repository
51 - :browse_repository
51 - :manage_repository
52 - :manage_repository
52 - :view_changesets
53 - :view_changesets
53 - :manage_project_activities
54 - :manage_project_activities
54
55
55 position: 1
56 position: 1
56 roles_002:
57 roles_002:
57 name: Developer
58 name: Developer
58 id: 2
59 id: 2
59 builtin: 0
60 builtin: 0
60 permissions: |
61 permissions: |
61 ---
62 ---
62 - :edit_project
63 - :edit_project
63 - :manage_members
64 - :manage_members
64 - :manage_versions
65 - :manage_versions
65 - :manage_categories
66 - :manage_categories
66 - :view_issues
67 - :view_issues
67 - :add_issues
68 - :add_issues
68 - :edit_issues
69 - :edit_issues
69 - :manage_issue_relations
70 - :manage_issue_relations
70 - :manage_subtasks
71 - :manage_subtasks
71 - :add_issue_notes
72 - :add_issue_notes
72 - :move_issues
73 - :move_issues
73 - :delete_issues
74 - :delete_issues
74 - :view_issue_watchers
75 - :view_issue_watchers
75 - :save_queries
76 - :save_queries
76 - :view_gantt
77 - :view_gantt
77 - :view_calendar
78 - :view_calendar
78 - :log_time
79 - :log_time
79 - :view_time_entries
80 - :view_time_entries
80 - :edit_own_time_entries
81 - :edit_own_time_entries
81 - :manage_news
82 - :manage_news
82 - :comment_news
83 - :comment_news
83 - :view_documents
84 - :view_documents
84 - :manage_documents
85 - :manage_documents
85 - :view_wiki_pages
86 - :view_wiki_pages
86 - :view_wiki_edits
87 - :view_wiki_edits
87 - :edit_wiki_pages
88 - :edit_wiki_pages
88 - :protect_wiki_pages
89 - :protect_wiki_pages
89 - :delete_wiki_pages
90 - :delete_wiki_pages
90 - :add_messages
91 - :add_messages
91 - :edit_own_messages
92 - :edit_own_messages
92 - :delete_own_messages
93 - :delete_own_messages
93 - :manage_boards
94 - :manage_boards
94 - :view_files
95 - :view_files
95 - :manage_files
96 - :manage_files
96 - :browse_repository
97 - :browse_repository
97 - :view_changesets
98 - :view_changesets
98
99
99 position: 2
100 position: 2
100 roles_003:
101 roles_003:
101 name: Reporter
102 name: Reporter
102 id: 3
103 id: 3
103 builtin: 0
104 builtin: 0
104 permissions: |
105 permissions: |
105 ---
106 ---
106 - :edit_project
107 - :edit_project
107 - :manage_members
108 - :manage_members
108 - :manage_versions
109 - :manage_versions
109 - :manage_categories
110 - :manage_categories
110 - :view_issues
111 - :view_issues
111 - :add_issues
112 - :add_issues
112 - :edit_issues
113 - :edit_issues
113 - :manage_issue_relations
114 - :manage_issue_relations
114 - :add_issue_notes
115 - :add_issue_notes
115 - :move_issues
116 - :move_issues
116 - :view_issue_watchers
117 - :view_issue_watchers
117 - :save_queries
118 - :save_queries
118 - :view_gantt
119 - :view_gantt
119 - :view_calendar
120 - :view_calendar
120 - :log_time
121 - :log_time
121 - :view_time_entries
122 - :view_time_entries
122 - :manage_news
123 - :manage_news
123 - :comment_news
124 - :comment_news
124 - :view_documents
125 - :view_documents
125 - :manage_documents
126 - :manage_documents
126 - :view_wiki_pages
127 - :view_wiki_pages
127 - :view_wiki_edits
128 - :view_wiki_edits
128 - :edit_wiki_pages
129 - :edit_wiki_pages
129 - :delete_wiki_pages
130 - :delete_wiki_pages
130 - :add_messages
131 - :add_messages
131 - :manage_boards
132 - :manage_boards
132 - :view_files
133 - :view_files
133 - :manage_files
134 - :manage_files
134 - :browse_repository
135 - :browse_repository
135 - :view_changesets
136 - :view_changesets
136
137
137 position: 3
138 position: 3
138 roles_004:
139 roles_004:
139 name: Non member
140 name: Non member
140 id: 4
141 id: 4
141 builtin: 1
142 builtin: 1
142 permissions: |
143 permissions: |
143 ---
144 ---
144 - :view_issues
145 - :view_issues
145 - :add_issues
146 - :add_issues
146 - :edit_issues
147 - :edit_issues
147 - :manage_issue_relations
148 - :manage_issue_relations
148 - :add_issue_notes
149 - :add_issue_notes
149 - :move_issues
150 - :move_issues
150 - :save_queries
151 - :save_queries
151 - :view_gantt
152 - :view_gantt
152 - :view_calendar
153 - :view_calendar
153 - :log_time
154 - :log_time
154 - :view_time_entries
155 - :view_time_entries
155 - :comment_news
156 - :comment_news
156 - :view_documents
157 - :view_documents
157 - :manage_documents
158 - :manage_documents
158 - :view_wiki_pages
159 - :view_wiki_pages
159 - :view_wiki_edits
160 - :view_wiki_edits
160 - :edit_wiki_pages
161 - :edit_wiki_pages
161 - :add_messages
162 - :add_messages
162 - :view_files
163 - :view_files
163 - :manage_files
164 - :manage_files
164 - :browse_repository
165 - :browse_repository
165 - :view_changesets
166 - :view_changesets
166
167
167 position: 4
168 position: 4
168 roles_005:
169 roles_005:
169 name: Anonymous
170 name: Anonymous
170 id: 5
171 id: 5
171 builtin: 2
172 builtin: 2
172 permissions: |
173 permissions: |
173 ---
174 ---
174 - :view_issues
175 - :view_issues
175 - :add_issue_notes
176 - :add_issue_notes
176 - :view_gantt
177 - :view_gantt
177 - :view_calendar
178 - :view_calendar
178 - :view_time_entries
179 - :view_time_entries
179 - :view_documents
180 - :view_documents
180 - :view_wiki_pages
181 - :view_wiki_pages
181 - :view_wiki_edits
182 - :view_wiki_edits
182 - :view_files
183 - :view_files
183 - :browse_repository
184 - :browse_repository
184 - :view_changesets
185 - :view_changesets
185
186
186 position: 5
187 position: 5
187
188
@@ -1,471 +1,498
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < ActionController::TestCase
24 class ProjectsControllerTest < ActionController::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments, :custom_fields, :custom_values, :time_entries
27 :attachments, :custom_fields, :custom_values, :time_entries
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index
37 def test_index
38 get :index
38 get :index
39 assert_response :success
39 assert_response :success
40 assert_template 'index'
40 assert_template 'index'
41 assert_not_nil assigns(:projects)
41 assert_not_nil assigns(:projects)
42
42
43 assert_tag :ul, :child => {:tag => 'li',
43 assert_tag :ul, :child => {:tag => 'li',
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 :child => { :tag => 'ul',
45 :child => { :tag => 'ul',
46 :descendant => { :tag => 'a',
46 :descendant => { :tag => 'a',
47 :content => 'Child of private child'
47 :content => 'Child of private child'
48 }
48 }
49 }
49 }
50 }
50 }
51
51
52 assert_no_tag :a, :content => /Private child of eCookbook/
52 assert_no_tag :a, :content => /Private child of eCookbook/
53 end
53 end
54
54
55 def test_index_atom
55 def test_index_atom
56 get :index, :format => 'atom'
56 get :index, :format => 'atom'
57 assert_response :success
57 assert_response :success
58 assert_template 'common/feed.atom.rxml'
58 assert_template 'common/feed.atom.rxml'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
61 end
61 end
62
62
63 context "#index" do
63 context "#index" do
64 context "by non-admin user with view_time_entries permission" do
64 context "by non-admin user with view_time_entries permission" do
65 setup do
65 setup do
66 @request.session[:user_id] = 3
66 @request.session[:user_id] = 3
67 end
67 end
68 should "show overall spent time link" do
68 should "show overall spent time link" do
69 get :index
69 get :index
70 assert_template 'index'
70 assert_template 'index'
71 assert_tag :a, :attributes => {:href => '/time_entries'}
71 assert_tag :a, :attributes => {:href => '/time_entries'}
72 end
72 end
73 end
73 end
74
74
75 context "by non-admin user without view_time_entries permission" do
75 context "by non-admin user without view_time_entries permission" do
76 setup do
76 setup do
77 Role.find(2).remove_permission! :view_time_entries
77 Role.find(2).remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
80 @request.session[:user_id] = 3
80 @request.session[:user_id] = 3
81 end
81 end
82 should "not show overall spent time link" do
82 should "not show overall spent time link" do
83 get :index
83 get :index
84 assert_template 'index'
84 assert_template 'index'
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
86 end
86 end
87 end
87 end
88 end
88 end
89
89
90 context "#new" do
90 context "#new" do
91 context "by admin user" do
91 context "by admin user" do
92 setup do
92 setup do
93 @request.session[:user_id] = 1
93 @request.session[:user_id] = 1
94 end
94 end
95
95
96 should "accept get" do
96 should "accept get" do
97 get :new
97 get :new
98 assert_response :success
98 assert_response :success
99 assert_template 'new'
99 assert_template 'new'
100 end
100 end
101
101
102 end
102 end
103
103
104 context "by non-admin user with add_project permission" do
104 context "by non-admin user with add_project permission" do
105 setup do
105 setup do
106 Role.non_member.add_permission! :add_project
106 Role.non_member.add_permission! :add_project
107 @request.session[:user_id] = 9
107 @request.session[:user_id] = 9
108 end
108 end
109
109
110 should "accept get" do
110 should "accept get" do
111 get :new
111 get :new
112 assert_response :success
112 assert_response :success
113 assert_template 'new'
113 assert_template 'new'
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
115 end
115 end
116 end
116 end
117
117
118 context "by non-admin user with add_subprojects permission" do
118 context "by non-admin user with add_subprojects permission" do
119 setup do
119 setup do
120 Role.find(1).remove_permission! :add_project
120 Role.find(1).remove_permission! :add_project
121 Role.find(1).add_permission! :add_subprojects
121 Role.find(1).add_permission! :add_subprojects
122 @request.session[:user_id] = 2
122 @request.session[:user_id] = 2
123 end
123 end
124
124
125 should "accept get" do
125 should "accept get" do
126 get :new, :parent_id => 'ecookbook'
126 get :new, :parent_id => 'ecookbook'
127 assert_response :success
127 assert_response :success
128 assert_template 'new'
128 assert_template 'new'
129 # parent project selected
129 # parent project selected
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
132 # no empty value
132 # no empty value
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
134 :child => {:tag => 'option', :attributes => {:value => ''}}
134 :child => {:tag => 'option', :attributes => {:value => ''}}
135 end
135 end
136 end
136 end
137
137
138 end
138 end
139
139
140 context "POST :create" do
140 context "POST :create" do
141 context "by admin user" do
141 context "by admin user" do
142 setup do
142 setup do
143 @request.session[:user_id] = 1
143 @request.session[:user_id] = 1
144 end
144 end
145
145
146 should "create a new project" do
146 should "create a new project" do
147 post :create,
147 post :create,
148 :project => {
148 :project => {
149 :name => "blog",
149 :name => "blog",
150 :description => "weblog",
150 :description => "weblog",
151 :homepage => 'http://weblog',
151 :homepage => 'http://weblog',
152 :identifier => "blog",
152 :identifier => "blog",
153 :is_public => 1,
153 :is_public => 1,
154 :custom_field_values => { '3' => 'Beta' },
154 :custom_field_values => { '3' => 'Beta' },
155 :tracker_ids => ['1', '3'],
155 :tracker_ids => ['1', '3'],
156 # an issue custom field that is not for all project
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 assert_redirected_to '/projects/blog/settings'
160 assert_redirected_to '/projects/blog/settings'
160
161
161 project = Project.find_by_name('blog')
162 project = Project.find_by_name('blog')
162 assert_kind_of Project, project
163 assert_kind_of Project, project
163 assert project.active?
164 assert project.active?
164 assert_equal 'weblog', project.description
165 assert_equal 'weblog', project.description
165 assert_equal 'http://weblog', project.homepage
166 assert_equal 'http://weblog', project.homepage
166 assert_equal true, project.is_public?
167 assert_equal true, project.is_public?
167 assert_nil project.parent
168 assert_nil project.parent
168 assert_equal 'Beta', project.custom_value_for(3).value
169 assert_equal 'Beta', project.custom_value_for(3).value
169 assert_equal [1, 3], project.trackers.map(&:id).sort
170 assert_equal [1, 3], project.trackers.map(&:id).sort
171 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
170 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
172 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
171 end
173 end
172
174
173 should "create a new subproject" do
175 should "create a new subproject" do
174 post :create, :project => { :name => "blog",
176 post :create, :project => { :name => "blog",
175 :description => "weblog",
177 :description => "weblog",
176 :identifier => "blog",
178 :identifier => "blog",
177 :is_public => 1,
179 :is_public => 1,
178 :custom_field_values => { '3' => 'Beta' },
180 :custom_field_values => { '3' => 'Beta' },
179 :parent_id => 1
181 :parent_id => 1
180 }
182 }
181 assert_redirected_to '/projects/blog/settings'
183 assert_redirected_to '/projects/blog/settings'
182
184
183 project = Project.find_by_name('blog')
185 project = Project.find_by_name('blog')
184 assert_kind_of Project, project
186 assert_kind_of Project, project
185 assert_equal Project.find(1), project.parent
187 assert_equal Project.find(1), project.parent
186 end
188 end
187 end
189 end
188
190
189 context "by non-admin user with add_project permission" do
191 context "by non-admin user with add_project permission" do
190 setup do
192 setup do
191 Role.non_member.add_permission! :add_project
193 Role.non_member.add_permission! :add_project
192 @request.session[:user_id] = 9
194 @request.session[:user_id] = 9
193 end
195 end
194
196
195 should "accept create a Project" do
197 should "accept create a Project" do
196 post :create, :project => { :name => "blog",
198 post :create, :project => { :name => "blog",
197 :description => "weblog",
199 :description => "weblog",
198 :identifier => "blog",
200 :identifier => "blog",
199 :is_public => 1,
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 assert_redirected_to '/projects/blog/settings'
207 assert_redirected_to '/projects/blog/settings'
204
208
205 project = Project.find_by_name('blog')
209 project = Project.find_by_name('blog')
206 assert_kind_of Project, project
210 assert_kind_of Project, project
207 assert_equal 'weblog', project.description
211 assert_equal 'weblog', project.description
208 assert_equal true, project.is_public?
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 # User should be added as a project member
216 # User should be added as a project member
211 assert User.find(9).member_of?(project)
217 assert User.find(9).member_of?(project)
212 assert_equal 1, project.members.size
218 assert_equal 1, project.members.size
213 end
219 end
214
220
215 should "fail with parent_id" do
221 should "fail with parent_id" do
216 assert_no_difference 'Project.count' do
222 assert_no_difference 'Project.count' do
217 post :create, :project => { :name => "blog",
223 post :create, :project => { :name => "blog",
218 :description => "weblog",
224 :description => "weblog",
219 :identifier => "blog",
225 :identifier => "blog",
220 :is_public => 1,
226 :is_public => 1,
221 :custom_field_values => { '3' => 'Beta' },
227 :custom_field_values => { '3' => 'Beta' },
222 :parent_id => 1
228 :parent_id => 1
223 }
229 }
224 end
230 end
225 assert_response :success
231 assert_response :success
226 project = assigns(:project)
232 project = assigns(:project)
227 assert_kind_of Project, project
233 assert_kind_of Project, project
228 assert_not_nil project.errors.on(:parent_id)
234 assert_not_nil project.errors.on(:parent_id)
229 end
235 end
230 end
236 end
231
237
232 context "by non-admin user with add_subprojects permission" do
238 context "by non-admin user with add_subprojects permission" do
233 setup do
239 setup do
234 Role.find(1).remove_permission! :add_project
240 Role.find(1).remove_permission! :add_project
235 Role.find(1).add_permission! :add_subprojects
241 Role.find(1).add_permission! :add_subprojects
236 @request.session[:user_id] = 2
242 @request.session[:user_id] = 2
237 end
243 end
238
244
239 should "create a project with a parent_id" do
245 should "create a project with a parent_id" do
240 post :create, :project => { :name => "blog",
246 post :create, :project => { :name => "blog",
241 :description => "weblog",
247 :description => "weblog",
242 :identifier => "blog",
248 :identifier => "blog",
243 :is_public => 1,
249 :is_public => 1,
244 :custom_field_values => { '3' => 'Beta' },
250 :custom_field_values => { '3' => 'Beta' },
245 :parent_id => 1
251 :parent_id => 1
246 }
252 }
247 assert_redirected_to '/projects/blog/settings'
253 assert_redirected_to '/projects/blog/settings'
248 project = Project.find_by_name('blog')
254 project = Project.find_by_name('blog')
249 end
255 end
250
256
251 should "fail without parent_id" do
257 should "fail without parent_id" do
252 assert_no_difference 'Project.count' do
258 assert_no_difference 'Project.count' do
253 post :create, :project => { :name => "blog",
259 post :create, :project => { :name => "blog",
254 :description => "weblog",
260 :description => "weblog",
255 :identifier => "blog",
261 :identifier => "blog",
256 :is_public => 1,
262 :is_public => 1,
257 :custom_field_values => { '3' => 'Beta' }
263 :custom_field_values => { '3' => 'Beta' }
258 }
264 }
259 end
265 end
260 assert_response :success
266 assert_response :success
261 project = assigns(:project)
267 project = assigns(:project)
262 assert_kind_of Project, project
268 assert_kind_of Project, project
263 assert_not_nil project.errors.on(:parent_id)
269 assert_not_nil project.errors.on(:parent_id)
264 end
270 end
265
271
266 should "fail with unauthorized parent_id" do
272 should "fail with unauthorized parent_id" do
267 assert !User.find(2).member_of?(Project.find(6))
273 assert !User.find(2).member_of?(Project.find(6))
268 assert_no_difference 'Project.count' do
274 assert_no_difference 'Project.count' do
269 post :create, :project => { :name => "blog",
275 post :create, :project => { :name => "blog",
270 :description => "weblog",
276 :description => "weblog",
271 :identifier => "blog",
277 :identifier => "blog",
272 :is_public => 1,
278 :is_public => 1,
273 :custom_field_values => { '3' => 'Beta' },
279 :custom_field_values => { '3' => 'Beta' },
274 :parent_id => 6
280 :parent_id => 6
275 }
281 }
276 end
282 end
277 assert_response :success
283 assert_response :success
278 project = assigns(:project)
284 project = assigns(:project)
279 assert_kind_of Project, project
285 assert_kind_of Project, project
280 assert_not_nil project.errors.on(:parent_id)
286 assert_not_nil project.errors.on(:parent_id)
281 end
287 end
282 end
288 end
283 end
289 end
284
290
291 def test_create_should_not_accept_get
292 @request.session[:user_id] = 1
293 get :create
294 assert_response :method_not_allowed
295 end
296
285 def test_show_by_id
297 def test_show_by_id
286 get :show, :id => 1
298 get :show, :id => 1
287 assert_response :success
299 assert_response :success
288 assert_template 'show'
300 assert_template 'show'
289 assert_not_nil assigns(:project)
301 assert_not_nil assigns(:project)
290 end
302 end
291
303
292 def test_show_by_identifier
304 def test_show_by_identifier
293 get :show, :id => 'ecookbook'
305 get :show, :id => 'ecookbook'
294 assert_response :success
306 assert_response :success
295 assert_template 'show'
307 assert_template 'show'
296 assert_not_nil assigns(:project)
308 assert_not_nil assigns(:project)
297 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
309 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
298
310
299 assert_tag 'li', :content => /Development status/
311 assert_tag 'li', :content => /Development status/
300 end
312 end
301
313
302 def test_show_should_not_display_hidden_custom_fields
314 def test_show_should_not_display_hidden_custom_fields
303 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
315 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
304 get :show, :id => 'ecookbook'
316 get :show, :id => 'ecookbook'
305 assert_response :success
317 assert_response :success
306 assert_template 'show'
318 assert_template 'show'
307 assert_not_nil assigns(:project)
319 assert_not_nil assigns(:project)
308
320
309 assert_no_tag 'li', :content => /Development status/
321 assert_no_tag 'li', :content => /Development status/
310 end
322 end
311
323
312 def test_show_should_not_fail_when_custom_values_are_nil
324 def test_show_should_not_fail_when_custom_values_are_nil
313 project = Project.find_by_identifier('ecookbook')
325 project = Project.find_by_identifier('ecookbook')
314 project.custom_values.first.update_attribute(:value, nil)
326 project.custom_values.first.update_attribute(:value, nil)
315 get :show, :id => 'ecookbook'
327 get :show, :id => 'ecookbook'
316 assert_response :success
328 assert_response :success
317 assert_template 'show'
329 assert_template 'show'
318 assert_not_nil assigns(:project)
330 assert_not_nil assigns(:project)
319 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
331 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
320 end
332 end
321
333
322 def show_archived_project_should_be_denied
334 def show_archived_project_should_be_denied
323 project = Project.find_by_identifier('ecookbook')
335 project = Project.find_by_identifier('ecookbook')
324 project.archive!
336 project.archive!
325
337
326 get :show, :id => 'ecookbook'
338 get :show, :id => 'ecookbook'
327 assert_response 403
339 assert_response 403
328 assert_nil assigns(:project)
340 assert_nil assigns(:project)
329 assert_tag :tag => 'p', :content => /archived/
341 assert_tag :tag => 'p', :content => /archived/
330 end
342 end
331
343
332 def test_private_subprojects_hidden
344 def test_private_subprojects_hidden
333 get :show, :id => 'ecookbook'
345 get :show, :id => 'ecookbook'
334 assert_response :success
346 assert_response :success
335 assert_template 'show'
347 assert_template 'show'
336 assert_no_tag :tag => 'a', :content => /Private child/
348 assert_no_tag :tag => 'a', :content => /Private child/
337 end
349 end
338
350
339 def test_private_subprojects_visible
351 def test_private_subprojects_visible
340 @request.session[:user_id] = 2 # manager who is a member of the private subproject
352 @request.session[:user_id] = 2 # manager who is a member of the private subproject
341 get :show, :id => 'ecookbook'
353 get :show, :id => 'ecookbook'
342 assert_response :success
354 assert_response :success
343 assert_template 'show'
355 assert_template 'show'
344 assert_tag :tag => 'a', :content => /Private child/
356 assert_tag :tag => 'a', :content => /Private child/
345 end
357 end
346
358
347 def test_settings
359 def test_settings
348 @request.session[:user_id] = 2 # manager
360 @request.session[:user_id] = 2 # manager
349 get :settings, :id => 1
361 get :settings, :id => 1
350 assert_response :success
362 assert_response :success
351 assert_template 'settings'
363 assert_template 'settings'
352 end
364 end
353
365
354 def test_update
366 def test_update
355 @request.session[:user_id] = 2 # manager
367 @request.session[:user_id] = 2 # manager
356 post :update, :id => 1, :project => {:name => 'Test changed name',
368 post :update, :id => 1, :project => {:name => 'Test changed name',
357 :issue_custom_field_ids => ['']}
369 :issue_custom_field_ids => ['']}
358 assert_redirected_to '/projects/ecookbook/settings'
370 assert_redirected_to '/projects/ecookbook/settings'
359 project = Project.find(1)
371 project = Project.find(1)
360 assert_equal 'Test changed name', project.name
372 assert_equal 'Test changed name', project.name
361 end
373 end
374
375 def test_modules
376 @request.session[:user_id] = 2
377 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
378
379 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
380 assert_redirected_to '/projects/ecookbook/settings/modules'
381 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
382 end
383
384 def test_modules_should_not_allow_get
385 @request.session[:user_id] = 1
386 get :modules, :id => 1
387 assert_response :method_not_allowed
388 end
362
389
363 def test_get_destroy
390 def test_get_destroy
364 @request.session[:user_id] = 1 # admin
391 @request.session[:user_id] = 1 # admin
365 get :destroy, :id => 1
392 get :destroy, :id => 1
366 assert_response :success
393 assert_response :success
367 assert_template 'destroy'
394 assert_template 'destroy'
368 assert_not_nil Project.find_by_id(1)
395 assert_not_nil Project.find_by_id(1)
369 end
396 end
370
397
371 def test_post_destroy
398 def test_post_destroy
372 @request.session[:user_id] = 1 # admin
399 @request.session[:user_id] = 1 # admin
373 post :destroy, :id => 1, :confirm => 1
400 post :destroy, :id => 1, :confirm => 1
374 assert_redirected_to '/admin/projects'
401 assert_redirected_to '/admin/projects'
375 assert_nil Project.find_by_id(1)
402 assert_nil Project.find_by_id(1)
376 end
403 end
377
404
378 def test_archive
405 def test_archive
379 @request.session[:user_id] = 1 # admin
406 @request.session[:user_id] = 1 # admin
380 post :archive, :id => 1
407 post :archive, :id => 1
381 assert_redirected_to '/admin/projects'
408 assert_redirected_to '/admin/projects'
382 assert !Project.find(1).active?
409 assert !Project.find(1).active?
383 end
410 end
384
411
385 def test_unarchive
412 def test_unarchive
386 @request.session[:user_id] = 1 # admin
413 @request.session[:user_id] = 1 # admin
387 Project.find(1).archive
414 Project.find(1).archive
388 post :unarchive, :id => 1
415 post :unarchive, :id => 1
389 assert_redirected_to '/admin/projects'
416 assert_redirected_to '/admin/projects'
390 assert Project.find(1).active?
417 assert Project.find(1).active?
391 end
418 end
392
419
393 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
420 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
394 CustomField.delete_all
421 CustomField.delete_all
395 parent = nil
422 parent = nil
396 6.times do |i|
423 6.times do |i|
397 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
424 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
398 p.set_parent!(parent)
425 p.set_parent!(parent)
399 get :show, :id => p
426 get :show, :id => p
400 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
427 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
401 :children => { :count => [i, 3].min,
428 :children => { :count => [i, 3].min,
402 :only => { :tag => 'a' } }
429 :only => { :tag => 'a' } }
403
430
404 parent = p
431 parent = p
405 end
432 end
406 end
433 end
407
434
408 def test_copy_with_project
435 def test_copy_with_project
409 @request.session[:user_id] = 1 # admin
436 @request.session[:user_id] = 1 # admin
410 get :copy, :id => 1
437 get :copy, :id => 1
411 assert_response :success
438 assert_response :success
412 assert_template 'copy'
439 assert_template 'copy'
413 assert assigns(:project)
440 assert assigns(:project)
414 assert_equal Project.find(1).description, assigns(:project).description
441 assert_equal Project.find(1).description, assigns(:project).description
415 assert_nil assigns(:project).id
442 assert_nil assigns(:project).id
416 end
443 end
417
444
418 def test_copy_without_project
445 def test_copy_without_project
419 @request.session[:user_id] = 1 # admin
446 @request.session[:user_id] = 1 # admin
420 get :copy
447 get :copy
421 assert_response :redirect
448 assert_response :redirect
422 assert_redirected_to :controller => 'admin', :action => 'projects'
449 assert_redirected_to :controller => 'admin', :action => 'projects'
423 end
450 end
424
451
425 context "POST :copy" do
452 context "POST :copy" do
426 should "TODO: test the rest of the method"
453 should "TODO: test the rest of the method"
427
454
428 should "redirect to the project settings when successful" do
455 should "redirect to the project settings when successful" do
429 @request.session[:user_id] = 1 # admin
456 @request.session[:user_id] = 1 # admin
430 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
457 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
431 assert_response :redirect
458 assert_response :redirect
432 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
459 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
433 end
460 end
434 end
461 end
435
462
436 def test_jump_should_redirect_to_active_tab
463 def test_jump_should_redirect_to_active_tab
437 get :show, :id => 1, :jump => 'issues'
464 get :show, :id => 1, :jump => 'issues'
438 assert_redirected_to '/projects/ecookbook/issues'
465 assert_redirected_to '/projects/ecookbook/issues'
439 end
466 end
440
467
441 def test_jump_should_not_redirect_to_inactive_tab
468 def test_jump_should_not_redirect_to_inactive_tab
442 get :show, :id => 3, :jump => 'documents'
469 get :show, :id => 3, :jump => 'documents'
443 assert_response :success
470 assert_response :success
444 assert_template 'show'
471 assert_template 'show'
445 end
472 end
446
473
447 def test_jump_should_not_redirect_to_unknown_tab
474 def test_jump_should_not_redirect_to_unknown_tab
448 get :show, :id => 3, :jump => 'foobar'
475 get :show, :id => 3, :jump => 'foobar'
449 assert_response :success
476 assert_response :success
450 assert_template 'show'
477 assert_template 'show'
451 end
478 end
452
479
453 # A hook that is manually registered later
480 # A hook that is manually registered later
454 class ProjectBasedTemplate < Redmine::Hook::ViewListener
481 class ProjectBasedTemplate < Redmine::Hook::ViewListener
455 def view_layouts_base_html_head(context)
482 def view_layouts_base_html_head(context)
456 # Adds a project stylesheet
483 # Adds a project stylesheet
457 stylesheet_link_tag(context[:project].identifier) if context[:project]
484 stylesheet_link_tag(context[:project].identifier) if context[:project]
458 end
485 end
459 end
486 end
460 # Don't use this hook now
487 # Don't use this hook now
461 Redmine::Hook.clear_listeners
488 Redmine::Hook.clear_listeners
462
489
463 def test_hook_response
490 def test_hook_response
464 Redmine::Hook.add_listener(ProjectBasedTemplate)
491 Redmine::Hook.add_listener(ProjectBasedTemplate)
465 get :show, :id => 1
492 get :show, :id => 1
466 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
493 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
467 :parent => {:tag => 'head'}
494 :parent => {:tag => 'head'}
468
495
469 Redmine::Hook.clear_listeners
496 Redmine::Hook.clear_listeners
470 end
497 end
471 end
498 end
@@ -1,216 +1,261
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
23 :attachments, :custom_fields, :custom_values, :time_entries
23 :attachments, :custom_fields, :custom_values, :time_entries
24
24
25 def setup
25 def setup
26 Setting.rest_api_enabled = '1'
26 Setting.rest_api_enabled = '1'
27 end
27 end
28
28
29 context "GET /projects" do
29 context "GET /projects" do
30 context ".xml" do
30 context ".xml" do
31 should "return projects" do
31 should "return projects" do
32 get '/projects.xml'
32 get '/projects.xml'
33 assert_response :success
33 assert_response :success
34 assert_equal 'application/xml', @response.content_type
34 assert_equal 'application/xml', @response.content_type
35
35
36 assert_tag :tag => 'projects',
36 assert_tag :tag => 'projects',
37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
38 end
38 end
39 end
39 end
40
40
41 context ".json" do
41 context ".json" do
42 should "return projects" do
42 should "return projects" do
43 get '/projects.json'
43 get '/projects.json'
44 assert_response :success
44 assert_response :success
45 assert_equal 'application/json', @response.content_type
45 assert_equal 'application/json', @response.content_type
46
46
47 json = ActiveSupport::JSON.decode(response.body)
47 json = ActiveSupport::JSON.decode(response.body)
48 assert_kind_of Hash, json
48 assert_kind_of Hash, json
49 assert_kind_of Array, json['projects']
49 assert_kind_of Array, json['projects']
50 assert_kind_of Hash, json['projects'].first
50 assert_kind_of Hash, json['projects'].first
51 assert json['projects'].first.has_key?('id')
51 assert json['projects'].first.has_key?('id')
52 end
52 end
53 end
53 end
54 end
54 end
55
55
56 context "GET /projects/:id" do
56 context "GET /projects/:id" do
57 context ".xml" do
57 context ".xml" do
58 # TODO: A private project is needed because should_allow_api_authentication
58 # TODO: A private project is needed because should_allow_api_authentication
59 # actually tests that authentication is *required*, not just allowed
59 # actually tests that authentication is *required*, not just allowed
60 should_allow_api_authentication(:get, "/projects/2.xml")
60 should_allow_api_authentication(:get, "/projects/2.xml")
61
61
62 should "return requested project" do
62 should "return requested project" do
63 get '/projects/1.xml'
63 get '/projects/1.xml'
64 assert_response :success
64 assert_response :success
65 assert_equal 'application/xml', @response.content_type
65 assert_equal 'application/xml', @response.content_type
66
66
67 assert_tag :tag => 'project',
67 assert_tag :tag => 'project',
68 :child => {:tag => 'id', :content => '1'}
68 :child => {:tag => 'id', :content => '1'}
69 assert_tag :tag => 'custom_field',
69 assert_tag :tag => 'custom_field',
70 :attributes => {:name => 'Development status'}, :content => 'Stable'
70 :attributes => {:name => 'Development status'}, :content => 'Stable'
71 end
71 end
72
72
73 context "with hidden custom fields" do
73 context "with hidden custom fields" do
74 setup do
74 setup do
75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
76 end
76 end
77
77
78 should "not display hidden custom fields" do
78 should "not display hidden custom fields" do
79 get '/projects/1.xml'
79 get '/projects/1.xml'
80 assert_response :success
80 assert_response :success
81 assert_equal 'application/xml', @response.content_type
81 assert_equal 'application/xml', @response.content_type
82
82
83 assert_no_tag 'custom_field',
83 assert_no_tag 'custom_field',
84 :attributes => {:name => 'Development status'}
84 :attributes => {:name => 'Development status'}
85 end
85 end
86 end
86 end
87 end
87 end
88
88
89 context ".json" do
89 context ".json" do
90 should_allow_api_authentication(:get, "/projects/2.json")
90 should_allow_api_authentication(:get, "/projects/2.json")
91
91
92 should "return requested project" do
92 should "return requested project" do
93 get '/projects/1.json'
93 get '/projects/1.json'
94
94
95 json = ActiveSupport::JSON.decode(response.body)
95 json = ActiveSupport::JSON.decode(response.body)
96 assert_kind_of Hash, json
96 assert_kind_of Hash, json
97 assert_kind_of Hash, json['project']
97 assert_kind_of Hash, json['project']
98 assert_equal 1, json['project']['id']
98 assert_equal 1, json['project']['id']
99 end
99 end
100 end
100 end
101 end
101 end
102
102
103 context "POST /projects" do
103 context "POST /projects" do
104 context "with valid parameters" do
104 context "with valid parameters" do
105 setup do
105 setup do
106 Setting.default_projects_modules = ['issue_tracking', 'repository']
106 Setting.default_projects_modules = ['issue_tracking', 'repository']
107 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
107 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
108 end
108 end
109
109
110 context ".xml" do
110 context ".xml" do
111 should_allow_api_authentication(:post,
111 should_allow_api_authentication(:post,
112 '/projects.xml',
112 '/projects.xml',
113 {:project => {:name => 'API test', :identifier => 'api-test'}},
113 {:project => {:name => 'API test', :identifier => 'api-test'}},
114 {:success_code => :created})
114 {:success_code => :created})
115
115
116
116
117 should "create a project with the attributes" do
117 should "create a project with the attributes" do
118 assert_difference('Project.count') do
118 assert_difference('Project.count') do
119 post '/projects.xml', @parameters, :authorization => credentials('admin')
119 post '/projects.xml', @parameters, :authorization => credentials('admin')
120 end
120 end
121
121
122 project = Project.first(:order => 'id DESC')
122 project = Project.first(:order => 'id DESC')
123 assert_equal 'API test', project.name
123 assert_equal 'API test', project.name
124 assert_equal 'api-test', project.identifier
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 assert_response :created
128 assert_response :created
128 assert_equal 'application/xml', @response.content_type
129 assert_equal 'application/xml', @response.content_type
129 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
130 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
130 end
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 end
154 end
132 end
155 end
133
156
134 context "with invalid parameters" do
157 context "with invalid parameters" do
135 setup do
158 setup do
136 @parameters = {:project => {:name => 'API test'}}
159 @parameters = {:project => {:name => 'API test'}}
137 end
160 end
138
161
139 context ".xml" do
162 context ".xml" do
140 should "return errors" do
163 should "return errors" do
141 assert_no_difference('Project.count') do
164 assert_no_difference('Project.count') do
142 post '/projects.xml', @parameters, :authorization => credentials('admin')
165 post '/projects.xml', @parameters, :authorization => credentials('admin')
143 end
166 end
144
167
145 assert_response :unprocessable_entity
168 assert_response :unprocessable_entity
146 assert_equal 'application/xml', @response.content_type
169 assert_equal 'application/xml', @response.content_type
147 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
170 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
148 end
171 end
149 end
172 end
150 end
173 end
151 end
174 end
152
175
153 context "PUT /projects/:id" do
176 context "PUT /projects/:id" do
154 context "with valid parameters" do
177 context "with valid parameters" do
155 setup do
178 setup do
156 @parameters = {:project => {:name => 'API update'}}
179 @parameters = {:project => {:name => 'API update'}}
157 end
180 end
158
181
159 context ".xml" do
182 context ".xml" do
160 should_allow_api_authentication(:put,
183 should_allow_api_authentication(:put,
161 '/projects/2.xml',
184 '/projects/2.xml',
162 {:project => {:name => 'API update'}},
185 {:project => {:name => 'API update'}},
163 {:success_code => :ok})
186 {:success_code => :ok})
164
187
165 should "update the project" do
188 should "update the project" do
166 assert_no_difference 'Project.count' do
189 assert_no_difference 'Project.count' do
167 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
190 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
168 end
191 end
169 assert_response :ok
192 assert_response :ok
170 assert_equal 'application/xml', @response.content_type
193 assert_equal 'application/xml', @response.content_type
171 project = Project.find(2)
194 project = Project.find(2)
172 assert_equal 'API update', project.name
195 assert_equal 'API update', project.name
173 end
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 end
219 end
175 end
220 end
176
221
177 context "with invalid parameters" do
222 context "with invalid parameters" do
178 setup do
223 setup do
179 @parameters = {:project => {:name => ''}}
224 @parameters = {:project => {:name => ''}}
180 end
225 end
181
226
182 context ".xml" do
227 context ".xml" do
183 should "return errors" do
228 should "return errors" do
184 assert_no_difference('Project.count') do
229 assert_no_difference('Project.count') do
185 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
230 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
186 end
231 end
187
232
188 assert_response :unprocessable_entity
233 assert_response :unprocessable_entity
189 assert_equal 'application/xml', @response.content_type
234 assert_equal 'application/xml', @response.content_type
190 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
235 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
191 end
236 end
192 end
237 end
193 end
238 end
194 end
239 end
195
240
196 context "DELETE /projects/:id" do
241 context "DELETE /projects/:id" do
197 context ".xml" do
242 context ".xml" do
198 should_allow_api_authentication(:delete,
243 should_allow_api_authentication(:delete,
199 '/projects/2.xml',
244 '/projects/2.xml',
200 {},
245 {},
201 {:success_code => :ok})
246 {:success_code => :ok})
202
247
203 should "delete the project" do
248 should "delete the project" do
204 assert_difference('Project.count',-1) do
249 assert_difference('Project.count',-1) do
205 delete '/projects/2.xml', {}, :authorization => credentials('admin')
250 delete '/projects/2.xml', {}, :authorization => credentials('admin')
206 end
251 end
207 assert_response :ok
252 assert_response :ok
208 assert_nil Project.find_by_id(2)
253 assert_nil Project.find_by_id(2)
209 end
254 end
210 end
255 end
211 end
256 end
212
257
213 def credentials(user, password=nil)
258 def credentials(user, password=nil)
214 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
259 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
215 end
260 end
216 end
261 end
@@ -1,58 +1,118
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class ProjectNestedSetTest < ActiveSupport::TestCase
20 class ProjectNestedSetTest < ActiveSupport::TestCase
21
21
22 def setup
22 context "nested set" do
23 Project.delete_all
23 setup do
24 end
24 Project.delete_all
25
25
26 def test_destroy_root_and_chldren_should_not_mess_up_the_tree
26 @a = Project.create!(:name => 'Project A', :identifier => 'projecta')
27 a = Project.create!(:name => 'Project A', :identifier => 'projecta')
27 @a1 = Project.create!(:name => 'Project A1', :identifier => 'projecta1')
28 a1 = Project.create!(:name => 'Project A1', :identifier => 'projecta1')
28 @a1.set_parent!(@a)
29 a2 = Project.create!(:name => 'Project A2', :identifier => 'projecta2')
29 @a2 = Project.create!(:name => 'Project A2', :identifier => 'projecta2')
30 a1.set_parent!(a)
30 @a2.set_parent!(@a)
31 a2.set_parent!(a)
31
32 b = Project.create!(:name => 'Project B', :identifier => 'projectb')
32 @b = Project.create!(:name => 'Project B', :identifier => 'projectb')
33 b1 = Project.create!(:name => 'Project B1', :identifier => 'projectb1')
33 @b1 = Project.create!(:name => 'Project B1', :identifier => 'projectb1')
34 b1.set_parent!(b)
34 @b1.set_parent!(@b)
35
35 @b11 = Project.create!(:name => 'Project B11', :identifier => 'projectb11')
36 a.reload
36 @b11.set_parent!(@b1)
37 a1.reload
37 @b2 = Project.create!(:name => 'Project B2', :identifier => 'projectb2')
38 a2.reload
38 @b2.set_parent!(@b)
39 b.reload
39
40 b1.reload
40 @c = Project.create!(:name => 'Project C', :identifier => 'projectc')
41
41 @c1 = Project.create!(:name => 'Project C1', :identifier => 'projectc1')
42 assert_equal [nil, 1, 6], [a.parent_id, a.lft, a.rgt]
42 @c1.set_parent!(@c)
43 assert_equal [a.id, 2, 3], [a1.parent_id, a1.lft, a1.rgt]
43
44 assert_equal [a.id, 4, 5], [a2.parent_id, a2.lft, a2.rgt]
44 [@a, @a1, @a2, @b, @b1, @b11, @b2, @c, @c1].each(&:reload)
45 assert_equal [nil, 7, 10], [b.parent_id, b.lft, b.rgt]
45 end
46 assert_equal [b.id, 8, 9], [b1.parent_id, b1.lft, b1.rgt]
47
46
48 assert_difference 'Project.count', -3 do
47 context "#create" do
49 a.destroy
48 should "build valid tree" do
49 assert_nested_set_values({
50 @a => [nil, 1, 6],
51 @a1 => [@a.id, 2, 3],
52 @a2 => [@a.id, 4, 5],
53 @b => [nil, 7, 14],
54 @b1 => [@b.id, 8, 11],
55 @b11 => [@b1.id,9, 10],
56 @b2 => [@b.id,12, 13],
57 @c => [nil, 15, 18],
58 @c1 => [@c.id,16, 17]
59 })
60 end
50 end
61 end
51
62
52 b.reload
63 context "#set_parent!" do
53 b1.reload
64 should "keep valid tree" do
54
65 assert_no_difference 'Project.count' do
55 assert_equal [nil, 1, 4], [b.parent_id, b.lft, b.rgt]
66 Project.find_by_name('Project B1').set_parent!(Project.find_by_name('Project A2'))
56 assert_equal [b.id, 2, 3], [b1.parent_id, b1.lft, b1.rgt]
67 end
68 assert_nested_set_values({
69 @a => [nil, 1, 10],
70 @a2 => [@a.id, 4, 9],
71 @b1 => [@a2.id,5, 8],
72 @b11 => [@b1.id,6, 7],
73 @b => [nil, 11, 14],
74 @c => [nil, 15, 18]
75 })
76 end
77 end
78
79 context "#destroy" do
80 context "a root with children" do
81 should "not mess up the tree" do
82 assert_difference 'Project.count', -4 do
83 Project.find_by_name('Project B').destroy
84 end
85 assert_nested_set_values({
86 @a => [nil, 1, 6],
87 @a1 => [@a.id, 2, 3],
88 @a2 => [@a.id, 4, 5],
89 @c => [nil, 7, 10],
90 @c1 => [@c.id, 8, 9]
91 })
92 end
93 end
94
95 context "a child with children" do
96 should "not mess up the tree" do
97 assert_difference 'Project.count', -2 do
98 Project.find_by_name('Project B1').destroy
99 end
100 assert_nested_set_values({
101 @a => [nil, 1, 6],
102 @b => [nil, 7, 10],
103 @b2 => [@b.id, 8, 9],
104 @c => [nil, 11, 14]
105 })
106 end
107 end
108 end
109 end
110
111 def assert_nested_set_values(h)
112 assert Project.valid?
113 h.each do |project, expected|
114 project.reload
115 assert_equal expected, [project.parent_id, project.lft, project.rgt], "Unexpected nested set values for #{project.name}"
116 end
57 end
117 end
58 end No newline at end of file
118 end
General Comments 0
You need to be logged in to leave comments. Login now