@@ -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 |
|
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 |
|
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 |
|
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 |
|
|
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