##// END OF EJS Templates
Adds (a maximum of 3) links to project ancestors in the page title (#2788)....
Jean-Philippe Lang -
r2423:33e7ae96adcb
parent child
Show More
@@ -1,296 +1,295
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24 menu_item :issues, :only => [:changelog]
24 menu_item :issues, :only => [:changelog]
25
25
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
27 before_filter :find_optional_project, :only => :activity
27 before_filter :find_optional_project, :only => :activity
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 if controller.request.post?
33 if controller.request.post?
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 end
35 end
36 end
36 end
37
37
38 helper :sort
38 helper :sort
39 include SortHelper
39 include SortHelper
40 helper :custom_fields
40 helper :custom_fields
41 include CustomFieldsHelper
41 include CustomFieldsHelper
42 helper :issues
42 helper :issues
43 helper IssuesHelper
43 helper IssuesHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :repositories
46 helper :repositories
47 include RepositoriesHelper
47 include RepositoriesHelper
48 include ProjectsHelper
48 include ProjectsHelper
49
49
50 # Lists visible projects
50 # Lists visible projects
51 def index
51 def index
52 respond_to do |format|
52 respond_to do |format|
53 format.html {
53 format.html {
54 @projects = Project.visible.find(:all, :order => 'lft')
54 @projects = Project.visible.find(:all, :order => 'lft')
55 }
55 }
56 format.atom {
56 format.atom {
57 projects = Project.visible.find(:all, :order => 'created_on DESC',
57 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 :limit => Setting.feeds_limit.to_i)
58 :limit => Setting.feeds_limit.to_i)
59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 }
60 }
61 end
61 end
62 end
62 end
63
63
64 # Add a new project
64 # Add a new project
65 def add
65 def add
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 @trackers = Tracker.all
67 @trackers = Tracker.all
68 @project = Project.new(params[:project])
68 @project = Project.new(params[:project])
69 if request.get?
69 if request.get?
70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 @project.trackers = Tracker.all
71 @project.trackers = Tracker.all
72 @project.is_public = Setting.default_projects_public?
72 @project.is_public = Setting.default_projects_public?
73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
74 else
74 else
75 @project.enabled_module_names = params[:enabled_modules]
75 @project.enabled_module_names = params[:enabled_modules]
76 if @project.save
76 if @project.save
77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
78 flash[:notice] = l(:notice_successful_create)
78 flash[:notice] = l(:notice_successful_create)
79 redirect_to :controller => 'admin', :action => 'projects'
79 redirect_to :controller => 'admin', :action => 'projects'
80 end
80 end
81 end
81 end
82 end
82 end
83
83
84 # Show @project
84 # Show @project
85 def show
85 def show
86 if params[:jump]
86 if params[:jump]
87 # try to redirect to the requested menu item
87 # try to redirect to the requested menu item
88 redirect_to_project_menu_item(@project, params[:jump]) && return
88 redirect_to_project_menu_item(@project, params[:jump]) && return
89 end
89 end
90
90
91 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
91 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
92 @subprojects = @project.children.visible
92 @subprojects = @project.children.visible
93 @ancestors = @project.ancestors.visible
94 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
93 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
95 @trackers = @project.rolled_up_trackers
94 @trackers = @project.rolled_up_trackers
96
95
97 cond = @project.project_condition(Setting.display_subprojects_issues?)
96 cond = @project.project_condition(Setting.display_subprojects_issues?)
98
97
99 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
98 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
100 :include => [:project, :status, :tracker],
99 :include => [:project, :status, :tracker],
101 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
100 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
102 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
101 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
103 :include => [:project, :status, :tracker],
102 :include => [:project, :status, :tracker],
104 :conditions => cond)
103 :conditions => cond)
105
104
106 TimeEntry.visible_by(User.current) do
105 TimeEntry.visible_by(User.current) do
107 @total_hours = TimeEntry.sum(:hours,
106 @total_hours = TimeEntry.sum(:hours,
108 :include => :project,
107 :include => :project,
109 :conditions => cond).to_f
108 :conditions => cond).to_f
110 end
109 end
111 @key = User.current.rss_key
110 @key = User.current.rss_key
112 end
111 end
113
112
114 def settings
113 def settings
115 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
114 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
116 @issue_category ||= IssueCategory.new
115 @issue_category ||= IssueCategory.new
117 @member ||= @project.members.new
116 @member ||= @project.members.new
118 @trackers = Tracker.all
117 @trackers = Tracker.all
119 @repository ||= @project.repository
118 @repository ||= @project.repository
120 @wiki ||= @project.wiki
119 @wiki ||= @project.wiki
121 end
120 end
122
121
123 # Edit @project
122 # Edit @project
124 def edit
123 def edit
125 if request.post?
124 if request.post?
126 @project.attributes = params[:project]
125 @project.attributes = params[:project]
127 if @project.save
126 if @project.save
128 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
127 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
129 flash[:notice] = l(:notice_successful_update)
128 flash[:notice] = l(:notice_successful_update)
130 redirect_to :action => 'settings', :id => @project
129 redirect_to :action => 'settings', :id => @project
131 else
130 else
132 settings
131 settings
133 render :action => 'settings'
132 render :action => 'settings'
134 end
133 end
135 end
134 end
136 end
135 end
137
136
138 def modules
137 def modules
139 @project.enabled_module_names = params[:enabled_modules]
138 @project.enabled_module_names = params[:enabled_modules]
140 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
139 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
141 end
140 end
142
141
143 def archive
142 def archive
144 @project.archive if request.post? && @project.active?
143 @project.archive if request.post? && @project.active?
145 redirect_to :controller => 'admin', :action => 'projects'
144 redirect_to :controller => 'admin', :action => 'projects'
146 end
145 end
147
146
148 def unarchive
147 def unarchive
149 @project.unarchive if request.post? && !@project.active?
148 @project.unarchive if request.post? && !@project.active?
150 redirect_to :controller => 'admin', :action => 'projects'
149 redirect_to :controller => 'admin', :action => 'projects'
151 end
150 end
152
151
153 # Delete @project
152 # Delete @project
154 def destroy
153 def destroy
155 @project_to_destroy = @project
154 @project_to_destroy = @project
156 if request.post? and params[:confirm]
155 if request.post? and params[:confirm]
157 @project_to_destroy.destroy
156 @project_to_destroy.destroy
158 redirect_to :controller => 'admin', :action => 'projects'
157 redirect_to :controller => 'admin', :action => 'projects'
159 end
158 end
160 # hide project in layout
159 # hide project in layout
161 @project = nil
160 @project = nil
162 end
161 end
163
162
164 # Add a new issue category to @project
163 # Add a new issue category to @project
165 def add_issue_category
164 def add_issue_category
166 @category = @project.issue_categories.build(params[:category])
165 @category = @project.issue_categories.build(params[:category])
167 if request.post? and @category.save
166 if request.post? and @category.save
168 respond_to do |format|
167 respond_to do |format|
169 format.html do
168 format.html do
170 flash[:notice] = l(:notice_successful_create)
169 flash[:notice] = l(:notice_successful_create)
171 redirect_to :action => 'settings', :tab => 'categories', :id => @project
170 redirect_to :action => 'settings', :tab => 'categories', :id => @project
172 end
171 end
173 format.js do
172 format.js do
174 # IE doesn't support the replace_html rjs method for select box options
173 # IE doesn't support the replace_html rjs method for select box options
175 render(:update) {|page| page.replace "issue_category_id",
174 render(:update) {|page| page.replace "issue_category_id",
176 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
175 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
177 }
176 }
178 end
177 end
179 end
178 end
180 end
179 end
181 end
180 end
182
181
183 # Add a new version to @project
182 # Add a new version to @project
184 def add_version
183 def add_version
185 @version = @project.versions.build(params[:version])
184 @version = @project.versions.build(params[:version])
186 if request.post? and @version.save
185 if request.post? and @version.save
187 flash[:notice] = l(:notice_successful_create)
186 flash[:notice] = l(:notice_successful_create)
188 redirect_to :action => 'settings', :tab => 'versions', :id => @project
187 redirect_to :action => 'settings', :tab => 'versions', :id => @project
189 end
188 end
190 end
189 end
191
190
192 def add_file
191 def add_file
193 if request.post?
192 if request.post?
194 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
193 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
195 attachments = attach_files(container, params[:attachments])
194 attachments = attach_files(container, params[:attachments])
196 if !attachments.empty? && Setting.notified_events.include?('file_added')
195 if !attachments.empty? && Setting.notified_events.include?('file_added')
197 Mailer.deliver_attachments_added(attachments)
196 Mailer.deliver_attachments_added(attachments)
198 end
197 end
199 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
198 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
200 return
199 return
201 end
200 end
202 @versions = @project.versions.sort
201 @versions = @project.versions.sort
203 end
202 end
204
203
205 def list_files
204 def list_files
206 sort_init 'filename', 'asc'
205 sort_init 'filename', 'asc'
207 sort_update 'filename' => "#{Attachment.table_name}.filename",
206 sort_update 'filename' => "#{Attachment.table_name}.filename",
208 'created_on' => "#{Attachment.table_name}.created_on",
207 'created_on' => "#{Attachment.table_name}.created_on",
209 'size' => "#{Attachment.table_name}.filesize",
208 'size' => "#{Attachment.table_name}.filesize",
210 'downloads' => "#{Attachment.table_name}.downloads"
209 'downloads' => "#{Attachment.table_name}.downloads"
211
210
212 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
211 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
213 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
212 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
214 render :layout => !request.xhr?
213 render :layout => !request.xhr?
215 end
214 end
216
215
217 # Show changelog for @project
216 # Show changelog for @project
218 def changelog
217 def changelog
219 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
218 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
220 retrieve_selected_tracker_ids(@trackers)
219 retrieve_selected_tracker_ids(@trackers)
221 @versions = @project.versions.sort
220 @versions = @project.versions.sort
222 end
221 end
223
222
224 def roadmap
223 def roadmap
225 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
224 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
226 retrieve_selected_tracker_ids(@trackers)
225 retrieve_selected_tracker_ids(@trackers)
227 @versions = @project.versions.sort
226 @versions = @project.versions.sort
228 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
227 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
229 end
228 end
230
229
231 def activity
230 def activity
232 @days = Setting.activity_days_default.to_i
231 @days = Setting.activity_days_default.to_i
233
232
234 if params[:from]
233 if params[:from]
235 begin; @date_to = params[:from].to_date + 1; rescue; end
234 begin; @date_to = params[:from].to_date + 1; rescue; end
236 end
235 end
237
236
238 @date_to ||= Date.today + 1
237 @date_to ||= Date.today + 1
239 @date_from = @date_to - @days
238 @date_from = @date_to - @days
240 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
239 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
241 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
240 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
242
241
243 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
242 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
244 :with_subprojects => @with_subprojects,
243 :with_subprojects => @with_subprojects,
245 :author => @author)
244 :author => @author)
246 @activity.scope_select {|t| !params["show_#{t}"].nil?}
245 @activity.scope_select {|t| !params["show_#{t}"].nil?}
247 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
246 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
248
247
249 events = @activity.events(@date_from, @date_to)
248 events = @activity.events(@date_from, @date_to)
250
249
251 respond_to do |format|
250 respond_to do |format|
252 format.html {
251 format.html {
253 @events_by_day = events.group_by(&:event_date)
252 @events_by_day = events.group_by(&:event_date)
254 render :layout => false if request.xhr?
253 render :layout => false if request.xhr?
255 }
254 }
256 format.atom {
255 format.atom {
257 title = l(:label_activity)
256 title = l(:label_activity)
258 if @author
257 if @author
259 title = @author.name
258 title = @author.name
260 elsif @activity.scope.size == 1
259 elsif @activity.scope.size == 1
261 title = l("label_#{@activity.scope.first.singularize}_plural")
260 title = l("label_#{@activity.scope.first.singularize}_plural")
262 end
261 end
263 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
262 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
264 }
263 }
265 end
264 end
266
265
267 rescue ActiveRecord::RecordNotFound
266 rescue ActiveRecord::RecordNotFound
268 render_404
267 render_404
269 end
268 end
270
269
271 private
270 private
272 # Find project of id params[:id]
271 # Find project of id params[:id]
273 # if not found, redirect to project list
272 # if not found, redirect to project list
274 # Used as a before_filter
273 # Used as a before_filter
275 def find_project
274 def find_project
276 @project = Project.find(params[:id])
275 @project = Project.find(params[:id])
277 rescue ActiveRecord::RecordNotFound
276 rescue ActiveRecord::RecordNotFound
278 render_404
277 render_404
279 end
278 end
280
279
281 def find_optional_project
280 def find_optional_project
282 return true unless params[:id]
281 return true unless params[:id]
283 @project = Project.find(params[:id])
282 @project = Project.find(params[:id])
284 authorize
283 authorize
285 rescue ActiveRecord::RecordNotFound
284 rescue ActiveRecord::RecordNotFound
286 render_404
285 render_404
287 end
286 end
288
287
289 def retrieve_selected_tracker_ids(selectable_trackers)
288 def retrieve_selected_tracker_ids(selectable_trackers)
290 if ids = params[:tracker_ids]
289 if ids = params[:tracker_ids]
291 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
290 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
292 else
291 else
293 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
292 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
294 end
293 end
295 end
294 end
296 end
295 end
@@ -1,701 +1,721
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'coderay'
18 require 'coderay'
19 require 'coderay/helpers/file_type'
19 require 'coderay/helpers/file_type'
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include GravatarHelper::PublicMethods
25 include GravatarHelper::PublicMethods
26
26
27 extend Forwardable
27 extend Forwardable
28 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29
29
30 def current_role
30 def current_role
31 @current_role ||= User.current.role_for_project(@project)
31 @current_role ||= User.current.role_for_project(@project)
32 end
32 end
33
33
34 # Return true if user is authorized for controller/action, otherwise false
34 # Return true if user is authorized for controller/action, otherwise false
35 def authorize_for(controller, action)
35 def authorize_for(controller, action)
36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 end
37 end
38
38
39 # Display a link if user is authorized
39 # Display a link if user is authorized
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
42 end
43
43
44 # Display a link to remote if user is authorized
44 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
46 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
48 end
49
49
50 # Display a link to user's account page
50 # Display a link to user's account page
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
52 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
53 end
53 end
54
54
55 def link_to_issue(issue, options={})
55 def link_to_issue(issue, options={})
56 options[:class] ||= ''
56 options[:class] ||= ''
57 options[:class] << ' issue'
57 options[:class] << ' issue'
58 options[:class] << ' closed' if issue.closed?
58 options[:class] << ' closed' if issue.closed?
59 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
59 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
60 end
60 end
61
61
62 # Generates a link to an attachment.
62 # Generates a link to an attachment.
63 # Options:
63 # Options:
64 # * :text - Link text (default to attachment filename)
64 # * :text - Link text (default to attachment filename)
65 # * :download - Force download (default: false)
65 # * :download - Force download (default: false)
66 def link_to_attachment(attachment, options={})
66 def link_to_attachment(attachment, options={})
67 text = options.delete(:text) || attachment.filename
67 text = options.delete(:text) || attachment.filename
68 action = options.delete(:download) ? 'download' : 'show'
68 action = options.delete(:download) ? 'download' : 'show'
69
69
70 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
70 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
71 end
71 end
72
72
73 def toggle_link(name, id, options={})
73 def toggle_link(name, id, options={})
74 onclick = "Element.toggle('#{id}'); "
74 onclick = "Element.toggle('#{id}'); "
75 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
75 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
76 onclick << "return false;"
76 onclick << "return false;"
77 link_to(name, "#", :onclick => onclick)
77 link_to(name, "#", :onclick => onclick)
78 end
78 end
79
79
80 def image_to_function(name, function, html_options = {})
80 def image_to_function(name, function, html_options = {})
81 html_options.symbolize_keys!
81 html_options.symbolize_keys!
82 tag(:input, html_options.merge({
82 tag(:input, html_options.merge({
83 :type => "image", :src => image_path(name),
83 :type => "image", :src => image_path(name),
84 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
84 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
85 }))
85 }))
86 end
86 end
87
87
88 def prompt_to_remote(name, text, param, url, html_options = {})
88 def prompt_to_remote(name, text, param, url, html_options = {})
89 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
89 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
90 link_to name, {}, html_options
90 link_to name, {}, html_options
91 end
91 end
92
92
93 def format_date(date)
93 def format_date(date)
94 return nil unless date
94 return nil unless date
95 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
95 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
97 date.strftime(@date_format)
97 date.strftime(@date_format)
98 end
98 end
99
99
100 def format_time(time, include_date = true)
100 def format_time(time, include_date = true)
101 return nil unless time
101 return nil unless time
102 time = time.to_time if time.is_a?(String)
102 time = time.to_time if time.is_a?(String)
103 zone = User.current.time_zone
103 zone = User.current.time_zone
104 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
104 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
105 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
105 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
106 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
106 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
107 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
107 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
108 end
108 end
109
109
110 def format_activity_title(text)
110 def format_activity_title(text)
111 h(truncate_single_line(text, 100))
111 h(truncate_single_line(text, 100))
112 end
112 end
113
113
114 def format_activity_day(date)
114 def format_activity_day(date)
115 date == Date.today ? l(:label_today).titleize : format_date(date)
115 date == Date.today ? l(:label_today).titleize : format_date(date)
116 end
116 end
117
117
118 def format_activity_description(text)
118 def format_activity_description(text)
119 h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
119 h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
120 end
120 end
121
121
122 def distance_of_date_in_words(from_date, to_date = 0)
122 def distance_of_date_in_words(from_date, to_date = 0)
123 from_date = from_date.to_date if from_date.respond_to?(:to_date)
123 from_date = from_date.to_date if from_date.respond_to?(:to_date)
124 to_date = to_date.to_date if to_date.respond_to?(:to_date)
124 to_date = to_date.to_date if to_date.respond_to?(:to_date)
125 distance_in_days = (to_date - from_date).abs
125 distance_in_days = (to_date - from_date).abs
126 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
126 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
127 end
127 end
128
128
129 def due_date_distance_in_words(date)
129 def due_date_distance_in_words(date)
130 if date
130 if date
131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
132 end
132 end
133 end
133 end
134
134
135 def render_page_hierarchy(pages, node=nil)
135 def render_page_hierarchy(pages, node=nil)
136 content = ''
136 content = ''
137 if pages[node]
137 if pages[node]
138 content << "<ul class=\"pages-hierarchy\">\n"
138 content << "<ul class=\"pages-hierarchy\">\n"
139 pages[node].each do |page|
139 pages[node].each do |page|
140 content << "<li>"
140 content << "<li>"
141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
144 content << "</li>\n"
144 content << "</li>\n"
145 end
145 end
146 content << "</ul>\n"
146 content << "</ul>\n"
147 end
147 end
148 content
148 content
149 end
149 end
150
150
151 # Renders flash messages
151 # Renders flash messages
152 def render_flash_messages
152 def render_flash_messages
153 s = ''
153 s = ''
154 flash.each do |k,v|
154 flash.each do |k,v|
155 s << content_tag('div', v, :class => "flash #{k}")
155 s << content_tag('div', v, :class => "flash #{k}")
156 end
156 end
157 s
157 s
158 end
158 end
159
159
160 # Renders the project quick-jump box
160 # Renders the project quick-jump box
161 def render_project_jump_box
161 def render_project_jump_box
162 # Retrieve them now to avoid a COUNT query
162 # Retrieve them now to avoid a COUNT query
163 projects = User.current.projects.all
163 projects = User.current.projects.all
164 if projects.any?
164 if projects.any?
165 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
165 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
166 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
166 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
167 '<option disabled="disabled">---</option>'
167 '<option disabled="disabled">---</option>'
168 s << project_tree_options_for_select(projects) do |p|
168 s << project_tree_options_for_select(projects) do |p|
169 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
169 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
170 end
170 end
171 s << '</select>'
171 s << '</select>'
172 s
172 s
173 end
173 end
174 end
174 end
175
175
176 def project_tree_options_for_select(projects, options = {})
176 def project_tree_options_for_select(projects, options = {})
177 s = ''
177 s = ''
178 project_tree(projects) do |project, level|
178 project_tree(projects) do |project, level|
179 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
179 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
180 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
180 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
181 tag_options.merge!(yield(project)) if block_given?
181 tag_options.merge!(yield(project)) if block_given?
182 s << content_tag('option', name_prefix + h(project), tag_options)
182 s << content_tag('option', name_prefix + h(project), tag_options)
183 end
183 end
184 s
184 s
185 end
185 end
186
186
187 # Yields the given block for each project with its level in the tree
187 # Yields the given block for each project with its level in the tree
188 def project_tree(projects, &block)
188 def project_tree(projects, &block)
189 ancestors = []
189 ancestors = []
190 projects.sort_by(&:lft).each do |project|
190 projects.sort_by(&:lft).each do |project|
191 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
191 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
192 ancestors.pop
192 ancestors.pop
193 end
193 end
194 yield project, ancestors.size
194 yield project, ancestors.size
195 ancestors << project
195 ancestors << project
196 end
196 end
197 end
197 end
198
198
199 def project_nested_ul(projects, &block)
199 def project_nested_ul(projects, &block)
200 s = ''
200 s = ''
201 if projects.any?
201 if projects.any?
202 ancestors = []
202 ancestors = []
203 projects.sort_by(&:lft).each do |project|
203 projects.sort_by(&:lft).each do |project|
204 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
204 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
205 s << "<ul>\n"
205 s << "<ul>\n"
206 else
206 else
207 ancestors.pop
207 ancestors.pop
208 s << "</li>"
208 s << "</li>"
209 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
209 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
210 ancestors.pop
210 ancestors.pop
211 s << "</ul></li>\n"
211 s << "</ul></li>\n"
212 end
212 end
213 end
213 end
214 s << "<li>"
214 s << "<li>"
215 s << yield(project).to_s
215 s << yield(project).to_s
216 ancestors << project
216 ancestors << project
217 end
217 end
218 s << ("</li></ul>\n" * ancestors.size)
218 s << ("</li></ul>\n" * ancestors.size)
219 end
219 end
220 s
220 s
221 end
221 end
222
222
223 # Truncates and returns the string as a single line
223 # Truncates and returns the string as a single line
224 def truncate_single_line(string, *args)
224 def truncate_single_line(string, *args)
225 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
225 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
226 end
226 end
227
227
228 def html_hours(text)
228 def html_hours(text)
229 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
229 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
230 end
230 end
231
231
232 def authoring(created, author, options={})
232 def authoring(created, author, options={})
233 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
233 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
234 link_to(distance_of_time_in_words(Time.now, created),
234 link_to(distance_of_time_in_words(Time.now, created),
235 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
235 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
236 :title => format_time(created))
236 :title => format_time(created))
237 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
237 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
238 l(options[:label] || :label_added_time_by, author_tag, time_tag)
238 l(options[:label] || :label_added_time_by, author_tag, time_tag)
239 end
239 end
240
240
241 def l_or_humanize(s, options={})
241 def l_or_humanize(s, options={})
242 k = "#{options[:prefix]}#{s}".to_sym
242 k = "#{options[:prefix]}#{s}".to_sym
243 l_has_string?(k) ? l(k) : s.to_s.humanize
243 l_has_string?(k) ? l(k) : s.to_s.humanize
244 end
244 end
245
245
246 def day_name(day)
246 def day_name(day)
247 l(:general_day_names).split(',')[day-1]
247 l(:general_day_names).split(',')[day-1]
248 end
248 end
249
249
250 def month_name(month)
250 def month_name(month)
251 l(:actionview_datehelper_select_month_names).split(',')[month-1]
251 l(:actionview_datehelper_select_month_names).split(',')[month-1]
252 end
252 end
253
253
254 def syntax_highlight(name, content)
254 def syntax_highlight(name, content)
255 type = CodeRay::FileType[name]
255 type = CodeRay::FileType[name]
256 type ? CodeRay.scan(content, type).html : h(content)
256 type ? CodeRay.scan(content, type).html : h(content)
257 end
257 end
258
258
259 def to_path_param(path)
259 def to_path_param(path)
260 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
260 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
261 end
261 end
262
262
263 def pagination_links_full(paginator, count=nil, options={})
263 def pagination_links_full(paginator, count=nil, options={})
264 page_param = options.delete(:page_param) || :page
264 page_param = options.delete(:page_param) || :page
265 url_param = params.dup
265 url_param = params.dup
266 # don't reuse query params if filters are present
266 # don't reuse query params if filters are present
267 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
267 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
268
268
269 html = ''
269 html = ''
270 if paginator.current.previous
270 if paginator.current.previous
271 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
271 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
272 end
272 end
273
273
274 html << (pagination_links_each(paginator, options) do |n|
274 html << (pagination_links_each(paginator, options) do |n|
275 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
275 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
276 end || '')
276 end || '')
277
277
278 if paginator.current.next
278 if paginator.current.next
279 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
279 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
280 end
280 end
281
281
282 unless count.nil?
282 unless count.nil?
283 html << [
283 html << [
284 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
284 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
285 per_page_links(paginator.items_per_page)
285 per_page_links(paginator.items_per_page)
286 ].compact.join(' | ')
286 ].compact.join(' | ')
287 end
287 end
288
288
289 html
289 html
290 end
290 end
291
291
292 def per_page_links(selected=nil)
292 def per_page_links(selected=nil)
293 url_param = params.dup
293 url_param = params.dup
294 url_param.clear if url_param.has_key?(:set_filter)
294 url_param.clear if url_param.has_key?(:set_filter)
295
295
296 links = Setting.per_page_options_array.collect do |n|
296 links = Setting.per_page_options_array.collect do |n|
297 n == selected ? n : link_to_remote(n, {:update => "content",
297 n == selected ? n : link_to_remote(n, {:update => "content",
298 :url => params.dup.merge(:per_page => n),
298 :url => params.dup.merge(:per_page => n),
299 :method => :get},
299 :method => :get},
300 {:href => url_for(url_param.merge(:per_page => n))})
300 {:href => url_for(url_param.merge(:per_page => n))})
301 end
301 end
302 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
302 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
303 end
303 end
304
304
305 def breadcrumb(*args)
305 def breadcrumb(*args)
306 elements = args.flatten
306 elements = args.flatten
307 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
307 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
308 end
308 end
309
309
310 def other_formats_links(&block)
310 def other_formats_links(&block)
311 concat('<p class="other-formats">' + l(:label_export_to), block.binding)
311 concat('<p class="other-formats">' + l(:label_export_to), block.binding)
312 yield Redmine::Views::OtherFormatsBuilder.new(self)
312 yield Redmine::Views::OtherFormatsBuilder.new(self)
313 concat('</p>', block.binding)
313 concat('</p>', block.binding)
314 end
314 end
315
315
316 def page_header_title
317 if @project.nil? || @project.new_record?
318 h(Setting.app_title)
319 else
320 b = []
321 ancestors = (@project.root? ? [] : @project.ancestors.visible)
322 if ancestors.any?
323 root = ancestors.shift
324 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
325 if ancestors.size > 2
326 b << '&#8230;'
327 ancestors = ancestors[-2, 2]
328 end
329 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
330 end
331 b << h(@project)
332 b.join(' &#187; ')
333 end
334 end
335
316 def html_title(*args)
336 def html_title(*args)
317 if args.empty?
337 if args.empty?
318 title = []
338 title = []
319 title << @project.name if @project
339 title << @project.name if @project
320 title += @html_title if @html_title
340 title += @html_title if @html_title
321 title << Setting.app_title
341 title << Setting.app_title
322 title.compact.join(' - ')
342 title.compact.join(' - ')
323 else
343 else
324 @html_title ||= []
344 @html_title ||= []
325 @html_title += args
345 @html_title += args
326 end
346 end
327 end
347 end
328
348
329 def accesskey(s)
349 def accesskey(s)
330 Redmine::AccessKeys.key_for s
350 Redmine::AccessKeys.key_for s
331 end
351 end
332
352
333 # Formats text according to system settings.
353 # Formats text according to system settings.
334 # 2 ways to call this method:
354 # 2 ways to call this method:
335 # * with a String: textilizable(text, options)
355 # * with a String: textilizable(text, options)
336 # * with an object and one of its attribute: textilizable(issue, :description, options)
356 # * with an object and one of its attribute: textilizable(issue, :description, options)
337 def textilizable(*args)
357 def textilizable(*args)
338 options = args.last.is_a?(Hash) ? args.pop : {}
358 options = args.last.is_a?(Hash) ? args.pop : {}
339 case args.size
359 case args.size
340 when 1
360 when 1
341 obj = options[:object]
361 obj = options[:object]
342 text = args.shift
362 text = args.shift
343 when 2
363 when 2
344 obj = args.shift
364 obj = args.shift
345 text = obj.send(args.shift).to_s
365 text = obj.send(args.shift).to_s
346 else
366 else
347 raise ArgumentError, 'invalid arguments to textilizable'
367 raise ArgumentError, 'invalid arguments to textilizable'
348 end
368 end
349 return '' if text.blank?
369 return '' if text.blank?
350
370
351 only_path = options.delete(:only_path) == false ? false : true
371 only_path = options.delete(:only_path) == false ? false : true
352
372
353 # when using an image link, try to use an attachment, if possible
373 # when using an image link, try to use an attachment, if possible
354 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
374 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
355
375
356 if attachments
376 if attachments
357 attachments = attachments.sort_by(&:created_on).reverse
377 attachments = attachments.sort_by(&:created_on).reverse
358 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
378 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
359 style = $1
379 style = $1
360 filename = $6.downcase
380 filename = $6.downcase
361 # search for the picture in attachments
381 # search for the picture in attachments
362 if found = attachments.detect { |att| att.filename.downcase == filename }
382 if found = attachments.detect { |att| att.filename.downcase == filename }
363 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
383 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
364 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
384 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
365 alt = desc.blank? ? nil : "(#{desc})"
385 alt = desc.blank? ? nil : "(#{desc})"
366 "!#{style}#{image_url}#{alt}!"
386 "!#{style}#{image_url}#{alt}!"
367 else
387 else
368 m
388 m
369 end
389 end
370 end
390 end
371 end
391 end
372
392
373 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
393 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
374
394
375 # different methods for formatting wiki links
395 # different methods for formatting wiki links
376 case options[:wiki_links]
396 case options[:wiki_links]
377 when :local
397 when :local
378 # used for local links to html files
398 # used for local links to html files
379 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
399 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
380 when :anchor
400 when :anchor
381 # used for single-file wiki export
401 # used for single-file wiki export
382 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
402 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
383 else
403 else
384 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
404 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
385 end
405 end
386
406
387 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
407 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
388
408
389 # Wiki links
409 # Wiki links
390 #
410 #
391 # Examples:
411 # Examples:
392 # [[mypage]]
412 # [[mypage]]
393 # [[mypage|mytext]]
413 # [[mypage|mytext]]
394 # wiki links can refer other project wikis, using project name or identifier:
414 # wiki links can refer other project wikis, using project name or identifier:
395 # [[project:]] -> wiki starting page
415 # [[project:]] -> wiki starting page
396 # [[project:|mytext]]
416 # [[project:|mytext]]
397 # [[project:mypage]]
417 # [[project:mypage]]
398 # [[project:mypage|mytext]]
418 # [[project:mypage|mytext]]
399 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
419 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
400 link_project = project
420 link_project = project
401 esc, all, page, title = $1, $2, $3, $5
421 esc, all, page, title = $1, $2, $3, $5
402 if esc.nil?
422 if esc.nil?
403 if page =~ /^([^\:]+)\:(.*)$/
423 if page =~ /^([^\:]+)\:(.*)$/
404 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
424 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
405 page = $2
425 page = $2
406 title ||= $1 if page.blank?
426 title ||= $1 if page.blank?
407 end
427 end
408
428
409 if link_project && link_project.wiki
429 if link_project && link_project.wiki
410 # extract anchor
430 # extract anchor
411 anchor = nil
431 anchor = nil
412 if page =~ /^(.+?)\#(.+)$/
432 if page =~ /^(.+?)\#(.+)$/
413 page, anchor = $1, $2
433 page, anchor = $1, $2
414 end
434 end
415 # check if page exists
435 # check if page exists
416 wiki_page = link_project.wiki.find_page(page)
436 wiki_page = link_project.wiki.find_page(page)
417 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
437 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
418 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
438 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
419 else
439 else
420 # project or wiki doesn't exist
440 # project or wiki doesn't exist
421 all
441 all
422 end
442 end
423 else
443 else
424 all
444 all
425 end
445 end
426 end
446 end
427
447
428 # Redmine links
448 # Redmine links
429 #
449 #
430 # Examples:
450 # Examples:
431 # Issues:
451 # Issues:
432 # #52 -> Link to issue #52
452 # #52 -> Link to issue #52
433 # Changesets:
453 # Changesets:
434 # r52 -> Link to revision 52
454 # r52 -> Link to revision 52
435 # commit:a85130f -> Link to scmid starting with a85130f
455 # commit:a85130f -> Link to scmid starting with a85130f
436 # Documents:
456 # Documents:
437 # document#17 -> Link to document with id 17
457 # document#17 -> Link to document with id 17
438 # document:Greetings -> Link to the document with title "Greetings"
458 # document:Greetings -> Link to the document with title "Greetings"
439 # document:"Some document" -> Link to the document with title "Some document"
459 # document:"Some document" -> Link to the document with title "Some document"
440 # Versions:
460 # Versions:
441 # version#3 -> Link to version with id 3
461 # version#3 -> Link to version with id 3
442 # version:1.0.0 -> Link to version named "1.0.0"
462 # version:1.0.0 -> Link to version named "1.0.0"
443 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
463 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
444 # Attachments:
464 # Attachments:
445 # attachment:file.zip -> Link to the attachment of the current object named file.zip
465 # attachment:file.zip -> Link to the attachment of the current object named file.zip
446 # Source files:
466 # Source files:
447 # source:some/file -> Link to the file located at /some/file in the project's repository
467 # source:some/file -> Link to the file located at /some/file in the project's repository
448 # source:some/file@52 -> Link to the file's revision 52
468 # source:some/file@52 -> Link to the file's revision 52
449 # source:some/file#L120 -> Link to line 120 of the file
469 # source:some/file#L120 -> Link to line 120 of the file
450 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
470 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
451 # export:some/file -> Force the download of the file
471 # export:some/file -> Force the download of the file
452 # Forum messages:
472 # Forum messages:
453 # message#1218 -> Link to message with id 1218
473 # message#1218 -> Link to message with id 1218
454 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
474 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
455 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
475 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
456 link = nil
476 link = nil
457 if esc.nil?
477 if esc.nil?
458 if prefix.nil? && sep == 'r'
478 if prefix.nil? && sep == 'r'
459 if project && (changeset = project.changesets.find_by_revision(oid))
479 if project && (changeset = project.changesets.find_by_revision(oid))
460 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
480 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
461 :class => 'changeset',
481 :class => 'changeset',
462 :title => truncate_single_line(changeset.comments, 100))
482 :title => truncate_single_line(changeset.comments, 100))
463 end
483 end
464 elsif sep == '#'
484 elsif sep == '#'
465 oid = oid.to_i
485 oid = oid.to_i
466 case prefix
486 case prefix
467 when nil
487 when nil
468 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
488 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
469 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
489 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
470 :class => (issue.closed? ? 'issue closed' : 'issue'),
490 :class => (issue.closed? ? 'issue closed' : 'issue'),
471 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
491 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
472 link = content_tag('del', link) if issue.closed?
492 link = content_tag('del', link) if issue.closed?
473 end
493 end
474 when 'document'
494 when 'document'
475 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
495 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
476 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
496 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
477 :class => 'document'
497 :class => 'document'
478 end
498 end
479 when 'version'
499 when 'version'
480 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
500 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
481 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
501 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
482 :class => 'version'
502 :class => 'version'
483 end
503 end
484 when 'message'
504 when 'message'
485 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
505 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
486 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
506 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
487 :controller => 'messages',
507 :controller => 'messages',
488 :action => 'show',
508 :action => 'show',
489 :board_id => message.board,
509 :board_id => message.board,
490 :id => message.root,
510 :id => message.root,
491 :anchor => (message.parent ? "message-#{message.id}" : nil)},
511 :anchor => (message.parent ? "message-#{message.id}" : nil)},
492 :class => 'message'
512 :class => 'message'
493 end
513 end
494 end
514 end
495 elsif sep == ':'
515 elsif sep == ':'
496 # removes the double quotes if any
516 # removes the double quotes if any
497 name = oid.gsub(%r{^"(.*)"$}, "\\1")
517 name = oid.gsub(%r{^"(.*)"$}, "\\1")
498 case prefix
518 case prefix
499 when 'document'
519 when 'document'
500 if project && document = project.documents.find_by_title(name)
520 if project && document = project.documents.find_by_title(name)
501 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
521 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
502 :class => 'document'
522 :class => 'document'
503 end
523 end
504 when 'version'
524 when 'version'
505 if project && version = project.versions.find_by_name(name)
525 if project && version = project.versions.find_by_name(name)
506 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
526 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
507 :class => 'version'
527 :class => 'version'
508 end
528 end
509 when 'commit'
529 when 'commit'
510 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
530 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
511 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
531 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
512 :class => 'changeset',
532 :class => 'changeset',
513 :title => truncate_single_line(changeset.comments, 100)
533 :title => truncate_single_line(changeset.comments, 100)
514 end
534 end
515 when 'source', 'export'
535 when 'source', 'export'
516 if project && project.repository
536 if project && project.repository
517 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
537 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
518 path, rev, anchor = $1, $3, $5
538 path, rev, anchor = $1, $3, $5
519 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
539 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
520 :path => to_path_param(path),
540 :path => to_path_param(path),
521 :rev => rev,
541 :rev => rev,
522 :anchor => anchor,
542 :anchor => anchor,
523 :format => (prefix == 'export' ? 'raw' : nil)},
543 :format => (prefix == 'export' ? 'raw' : nil)},
524 :class => (prefix == 'export' ? 'source download' : 'source')
544 :class => (prefix == 'export' ? 'source download' : 'source')
525 end
545 end
526 when 'attachment'
546 when 'attachment'
527 if attachments && attachment = attachments.detect {|a| a.filename == name }
547 if attachments && attachment = attachments.detect {|a| a.filename == name }
528 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
548 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
529 :class => 'attachment'
549 :class => 'attachment'
530 end
550 end
531 end
551 end
532 end
552 end
533 end
553 end
534 leading + (link || "#{prefix}#{sep}#{oid}")
554 leading + (link || "#{prefix}#{sep}#{oid}")
535 end
555 end
536
556
537 text
557 text
538 end
558 end
539
559
540 # Same as Rails' simple_format helper without using paragraphs
560 # Same as Rails' simple_format helper without using paragraphs
541 def simple_format_without_paragraph(text)
561 def simple_format_without_paragraph(text)
542 text.to_s.
562 text.to_s.
543 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
563 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
544 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
564 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
545 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
565 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
546 end
566 end
547
567
548 def error_messages_for(object_name, options = {})
568 def error_messages_for(object_name, options = {})
549 options = options.symbolize_keys
569 options = options.symbolize_keys
550 object = instance_variable_get("@#{object_name}")
570 object = instance_variable_get("@#{object_name}")
551 if object && !object.errors.empty?
571 if object && !object.errors.empty?
552 # build full_messages here with controller current language
572 # build full_messages here with controller current language
553 full_messages = []
573 full_messages = []
554 object.errors.each do |attr, msg|
574 object.errors.each do |attr, msg|
555 next if msg.nil?
575 next if msg.nil?
556 msg = msg.first if msg.is_a? Array
576 msg = msg.first if msg.is_a? Array
557 if attr == "base"
577 if attr == "base"
558 full_messages << l(msg)
578 full_messages << l(msg)
559 else
579 else
560 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
580 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
561 end
581 end
562 end
582 end
563 # retrieve custom values error messages
583 # retrieve custom values error messages
564 if object.errors[:custom_values]
584 if object.errors[:custom_values]
565 object.custom_values.each do |v|
585 object.custom_values.each do |v|
566 v.errors.each do |attr, msg|
586 v.errors.each do |attr, msg|
567 next if msg.nil?
587 next if msg.nil?
568 msg = msg.first if msg.is_a? Array
588 msg = msg.first if msg.is_a? Array
569 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
589 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
570 end
590 end
571 end
591 end
572 end
592 end
573 content_tag("div",
593 content_tag("div",
574 content_tag(
594 content_tag(
575 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
595 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
576 ) +
596 ) +
577 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
597 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
578 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
598 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
579 )
599 )
580 else
600 else
581 ""
601 ""
582 end
602 end
583 end
603 end
584
604
585 def lang_options_for_select(blank=true)
605 def lang_options_for_select(blank=true)
586 (blank ? [["(auto)", ""]] : []) +
606 (blank ? [["(auto)", ""]] : []) +
587 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
607 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
588 end
608 end
589
609
590 def label_tag_for(name, option_tags = nil, options = {})
610 def label_tag_for(name, option_tags = nil, options = {})
591 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
611 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
592 content_tag("label", label_text)
612 content_tag("label", label_text)
593 end
613 end
594
614
595 def labelled_tabular_form_for(name, object, options, &proc)
615 def labelled_tabular_form_for(name, object, options, &proc)
596 options[:html] ||= {}
616 options[:html] ||= {}
597 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
617 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
598 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
618 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
599 end
619 end
600
620
601 def back_url_hidden_field_tag
621 def back_url_hidden_field_tag
602 back_url = params[:back_url] || request.env['HTTP_REFERER']
622 back_url = params[:back_url] || request.env['HTTP_REFERER']
603 back_url = CGI.unescape(back_url.to_s)
623 back_url = CGI.unescape(back_url.to_s)
604 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
624 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
605 end
625 end
606
626
607 def check_all_links(form_name)
627 def check_all_links(form_name)
608 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
628 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
609 " | " +
629 " | " +
610 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
630 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
611 end
631 end
612
632
613 def progress_bar(pcts, options={})
633 def progress_bar(pcts, options={})
614 pcts = [pcts, pcts] unless pcts.is_a?(Array)
634 pcts = [pcts, pcts] unless pcts.is_a?(Array)
615 pcts[1] = pcts[1] - pcts[0]
635 pcts[1] = pcts[1] - pcts[0]
616 pcts << (100 - pcts[1] - pcts[0])
636 pcts << (100 - pcts[1] - pcts[0])
617 width = options[:width] || '100px;'
637 width = options[:width] || '100px;'
618 legend = options[:legend] || ''
638 legend = options[:legend] || ''
619 content_tag('table',
639 content_tag('table',
620 content_tag('tr',
640 content_tag('tr',
621 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
641 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
622 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
642 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
623 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
643 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
624 ), :class => 'progress', :style => "width: #{width};") +
644 ), :class => 'progress', :style => "width: #{width};") +
625 content_tag('p', legend, :class => 'pourcent')
645 content_tag('p', legend, :class => 'pourcent')
626 end
646 end
627
647
628 def context_menu_link(name, url, options={})
648 def context_menu_link(name, url, options={})
629 options[:class] ||= ''
649 options[:class] ||= ''
630 if options.delete(:selected)
650 if options.delete(:selected)
631 options[:class] << ' icon-checked disabled'
651 options[:class] << ' icon-checked disabled'
632 options[:disabled] = true
652 options[:disabled] = true
633 end
653 end
634 if options.delete(:disabled)
654 if options.delete(:disabled)
635 options.delete(:method)
655 options.delete(:method)
636 options.delete(:confirm)
656 options.delete(:confirm)
637 options.delete(:onclick)
657 options.delete(:onclick)
638 options[:class] << ' disabled'
658 options[:class] << ' disabled'
639 url = '#'
659 url = '#'
640 end
660 end
641 link_to name, url, options
661 link_to name, url, options
642 end
662 end
643
663
644 def calendar_for(field_id)
664 def calendar_for(field_id)
645 include_calendar_headers_tags
665 include_calendar_headers_tags
646 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
666 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
647 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
667 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
648 end
668 end
649
669
650 def include_calendar_headers_tags
670 def include_calendar_headers_tags
651 unless @calendar_headers_tags_included
671 unless @calendar_headers_tags_included
652 @calendar_headers_tags_included = true
672 @calendar_headers_tags_included = true
653 content_for :header_tags do
673 content_for :header_tags do
654 javascript_include_tag('calendar/calendar') +
674 javascript_include_tag('calendar/calendar') +
655 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
675 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
656 javascript_include_tag('calendar/calendar-setup') +
676 javascript_include_tag('calendar/calendar-setup') +
657 stylesheet_link_tag('calendar')
677 stylesheet_link_tag('calendar')
658 end
678 end
659 end
679 end
660 end
680 end
661
681
662 def content_for(name, content = nil, &block)
682 def content_for(name, content = nil, &block)
663 @has_content ||= {}
683 @has_content ||= {}
664 @has_content[name] = true
684 @has_content[name] = true
665 super(name, content, &block)
685 super(name, content, &block)
666 end
686 end
667
687
668 def has_content?(name)
688 def has_content?(name)
669 (@has_content && @has_content[name]) || false
689 (@has_content && @has_content[name]) || false
670 end
690 end
671
691
672 # Returns the avatar image tag for the given +user+ if avatars are enabled
692 # Returns the avatar image tag for the given +user+ if avatars are enabled
673 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
693 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
674 def avatar(user, options = { })
694 def avatar(user, options = { })
675 if Setting.gravatar_enabled?
695 if Setting.gravatar_enabled?
676 email = nil
696 email = nil
677 if user.respond_to?(:mail)
697 if user.respond_to?(:mail)
678 email = user.mail
698 email = user.mail
679 elsif user.to_s =~ %r{<(.+?)>}
699 elsif user.to_s =~ %r{<(.+?)>}
680 email = $1
700 email = $1
681 end
701 end
682 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
702 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
683 end
703 end
684 end
704 end
685
705
686 private
706 private
687
707
688 def wiki_helper
708 def wiki_helper
689 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
709 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
690 extend helper
710 extend helper
691 return self
711 return self
692 end
712 end
693
713
694 def link_to_remote_content_update(text, url_params)
714 def link_to_remote_content_update(text, url_params)
695 link_to_remote(text,
715 link_to_remote(text,
696 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
716 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
697 {:href => url_for(:params => url_params)}
717 {:href => url_for(:params => url_params)}
698 )
718 )
699 end
719 end
700
720
701 end
721 end
@@ -1,68 +1,68
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
3 <head>
4 <title><%=h html_title %></title>
4 <title><%=h html_title %></title>
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 <meta name="keywords" content="issue,bug,tracker" />
7 <meta name="keywords" content="issue,bug,tracker" />
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 <%= javascript_include_tag :defaults %>
9 <%= javascript_include_tag :defaults %>
10 <%= heads_for_wiki_formatter %>
10 <%= heads_for_wiki_formatter %>
11 <!--[if IE]>
11 <!--[if IE]>
12 <style type="text/css">
12 <style type="text/css">
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
15 </style>
15 </style>
16 <![endif]-->
16 <![endif]-->
17 <%= call_hook :view_layouts_base_html_head %>
17 <%= call_hook :view_layouts_base_html_head %>
18 <!-- page specific tags -->
18 <!-- page specific tags -->
19 <%= yield :header_tags -%>
19 <%= yield :header_tags -%>
20 </head>
20 </head>
21 <body>
21 <body>
22 <div id="wrapper">
22 <div id="wrapper">
23 <div id="top-menu">
23 <div id="top-menu">
24 <div id="account">
24 <div id="account">
25 <%= render_menu :account_menu -%>
25 <%= render_menu :account_menu -%>
26 </div>
26 </div>
27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
28 <%= render_menu :top_menu -%>
28 <%= render_menu :top_menu -%>
29 </div>
29 </div>
30
30
31 <div id="header">
31 <div id="header">
32 <div id="quick-search">
32 <div id="quick-search">
33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
34 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
34 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
35 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
35 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
36 <% end %>
36 <% end %>
37 <%= render_project_jump_box %>
37 <%= render_project_jump_box %>
38 </div>
38 </div>
39
39
40 <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
40 <h1><%= page_header_title %></h1>
41
41
42 <div id="main-menu">
42 <div id="main-menu">
43 <%= render_main_menu(@project) %>
43 <%= render_main_menu(@project) %>
44 </div>
44 </div>
45 </div>
45 </div>
46
46
47 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
47 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
48 <div id="sidebar">
48 <div id="sidebar">
49 <%= yield :sidebar %>
49 <%= yield :sidebar %>
50 <%= call_hook :view_layouts_base_sidebar %>
50 <%= call_hook :view_layouts_base_sidebar %>
51 </div>
51 </div>
52
52
53 <div id="content">
53 <div id="content">
54 <%= render_flash_messages %>
54 <%= render_flash_messages %>
55 <%= yield %>
55 <%= yield %>
56 <%= call_hook :view_layouts_base_content %>
56 <%= call_hook :view_layouts_base_content %>
57 </div>
57 </div>
58 </div>
58 </div>
59
59
60 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
60 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
61
61
62 <div id="footer">
62 <div id="footer">
63 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2009 Jean-Philippe Lang
63 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2009 Jean-Philippe Lang
64 </div>
64 </div>
65 </div>
65 </div>
66 <%= call_hook :view_layouts_base_body_bottom %>
66 <%= call_hook :view_layouts_base_body_bottom %>
67 </body>
67 </body>
68 </html>
68 </html>
@@ -1,82 +1,78
1 <h2><%=l(:label_overview)%></h2>
1 <h2><%=l(:label_overview)%></h2>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <%= textilizable @project.description %>
4 <%= textilizable @project.description %>
5 <ul>
5 <ul>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
7 <% if @subprojects.any? %>
7 <% if @subprojects.any? %>
8 <li><%=l(:label_subproject_plural)%>:
8 <li><%=l(:label_subproject_plural)%>:
9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
10 <% end %>
10 <% end %>
11 <% if @ancestors.any? %>
12 <li><%=l(:field_parent)%>:
13 <%= @ancestors.collect {|p| link_to(h(p), :action => 'show', :id => p)}.join(" &#187; ") %></li>
14 <% end %>
15 <% @project.custom_values.each do |custom_value| %>
11 <% @project.custom_values.each do |custom_value| %>
16 <% if !custom_value.value.empty? %>
12 <% if !custom_value.value.empty? %>
17 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
13 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
18 <% end %>
14 <% end %>
19 <% end %>
15 <% end %>
20 </ul>
16 </ul>
21
17
22 <% if User.current.allowed_to?(:view_issues, @project) %>
18 <% if User.current.allowed_to?(:view_issues, @project) %>
23 <div class="box">
19 <div class="box">
24 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
20 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
25 <ul>
21 <ul>
26 <% for tracker in @trackers %>
22 <% for tracker in @trackers %>
27 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
23 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
28 :set_filter => 1,
24 :set_filter => 1,
29 "tracker_id" => tracker.id %>:
25 "tracker_id" => tracker.id %>:
30 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
26 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
31 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
27 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
32 <% end %>
28 <% end %>
33 </ul>
29 </ul>
34 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
30 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
35 </div>
31 </div>
36 <% end %>
32 <% end %>
37 </div>
33 </div>
38
34
39 <div class="splitcontentright">
35 <div class="splitcontentright">
40 <% if @members_by_role.any? %>
36 <% if @members_by_role.any? %>
41 <div class="box">
37 <div class="box">
42 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
38 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
43 <p><% @members_by_role.keys.sort.each do |role| %>
39 <p><% @members_by_role.keys.sort.each do |role| %>
44 <%= role.name %>:
40 <%= role.name %>:
45 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
41 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
46 <br />
42 <br />
47 <% end %></p>
43 <% end %></p>
48 </div>
44 </div>
49 <% end %>
45 <% end %>
50
46
51 <% if @news.any? && authorize_for('news', 'index') %>
47 <% if @news.any? && authorize_for('news', 'index') %>
52 <div class="box">
48 <div class="box">
53 <h3><%=l(:label_news_latest)%></h3>
49 <h3><%=l(:label_news_latest)%></h3>
54 <%= render :partial => 'news/news', :collection => @news %>
50 <%= render :partial => 'news/news', :collection => @news %>
55 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
51 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
56 </div>
52 </div>
57 <% end %>
53 <% end %>
58 </div>
54 </div>
59
55
60 <% content_for :sidebar do %>
56 <% content_for :sidebar do %>
61 <% planning_links = []
57 <% planning_links = []
62 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
58 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
63 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
59 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
64 planning_links.compact!
60 planning_links.compact!
65 unless planning_links.empty? %>
61 unless planning_links.empty? %>
66 <h3><%= l(:label_planning) %></h3>
62 <h3><%= l(:label_planning) %></h3>
67 <p><%= planning_links.join(' | ') %></p>
63 <p><%= planning_links.join(' | ') %></p>
68 <% end %>
64 <% end %>
69
65
70 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
66 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
71 <h3><%= l(:label_spent_time) %></h3>
67 <h3><%= l(:label_spent_time) %></h3>
72 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
68 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
73 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
69 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
74 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
70 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
75 <% end %>
71 <% end %>
76 <% end %>
72 <% end %>
77
73
78 <% content_for :header_tags do %>
74 <% content_for :header_tags do %>
79 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
75 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
80 <% end %>
76 <% end %>
81
77
82 <% html_title(l(:label_overview)) -%>
78 <% html_title(l(:label_overview)) -%>
@@ -1,708 +1,709
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2
2
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 h1 {margin:0; padding:0; font-size: 24px;}
4 h1 {margin:0; padding:0; font-size: 24px;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8
8
9 /***** Layout *****/
9 /***** Layout *****/
10 #wrapper {background: white;}
10 #wrapper {background: white;}
11
11
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 #top-menu ul {margin: 0; padding: 0;}
13 #top-menu ul {margin: 0; padding: 0;}
14 #top-menu li {
14 #top-menu li {
15 float:left;
15 float:left;
16 list-style-type:none;
16 list-style-type:none;
17 margin: 0px 0px 0px 0px;
17 margin: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
19 white-space:nowrap;
19 white-space:nowrap;
20 }
20 }
21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23
23
24 #account {float:right;}
24 #account {float:right;}
25
25
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 #header a {color:#f8f8f8;}
27 #header a {color:#f8f8f8;}
28 #header h1 a.ancestor { font-size: 80%; }
28 #quick-search {float:right;}
29 #quick-search {float:right;}
29
30
30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 #main-menu ul {margin: 0; padding: 0;}
32 #main-menu ul {margin: 0; padding: 0;}
32 #main-menu li {
33 #main-menu li {
33 float:left;
34 float:left;
34 list-style-type:none;
35 list-style-type:none;
35 margin: 0px 2px 0px 0px;
36 margin: 0px 2px 0px 0px;
36 padding: 0px 0px 0px 0px;
37 padding: 0px 0px 0px 0px;
37 white-space:nowrap;
38 white-space:nowrap;
38 }
39 }
39 #main-menu li a {
40 #main-menu li a {
40 display: block;
41 display: block;
41 color: #fff;
42 color: #fff;
42 text-decoration: none;
43 text-decoration: none;
43 font-weight: bold;
44 font-weight: bold;
44 margin: 0;
45 margin: 0;
45 padding: 4px 10px 4px 10px;
46 padding: 4px 10px 4px 10px;
46 }
47 }
47 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49
50
50 #main {background-color:#EEEEEE;}
51 #main {background-color:#EEEEEE;}
51
52
52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
53 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
53 * html #sidebar{ width: 17%; }
54 * html #sidebar{ width: 17%; }
54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
55 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
56 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
57 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
57
58
58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
59 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
60 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
60 html>body #content { min-height: 600px; }
61 html>body #content { min-height: 600px; }
61 * html body #content { height: 600px; } /* IE */
62 * html body #content { height: 600px; } /* IE */
62
63
63 #main.nosidebar #sidebar{ display: none; }
64 #main.nosidebar #sidebar{ display: none; }
64 #main.nosidebar #content{ width: auto; border-right: 0; }
65 #main.nosidebar #content{ width: auto; border-right: 0; }
65
66
66 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
67 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
67
68
68 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
69 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
69 #login-form table td {padding: 6px;}
70 #login-form table td {padding: 6px;}
70 #login-form label {font-weight: bold;}
71 #login-form label {font-weight: bold;}
71
72
72 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
73 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
73
74
74 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
75 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
75
76
76 /***** Links *****/
77 /***** Links *****/
77 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
78 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
78 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
79 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
79 a img{ border: 0; }
80 a img{ border: 0; }
80
81
81 a.issue.closed { text-decoration: line-through; }
82 a.issue.closed { text-decoration: line-through; }
82
83
83 /***** Tables *****/
84 /***** Tables *****/
84 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
85 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
85 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
86 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
86 table.list td { vertical-align: top; }
87 table.list td { vertical-align: top; }
87 table.list td.id { width: 2%; text-align: center;}
88 table.list td.id { width: 2%; text-align: center;}
88 table.list td.checkbox { width: 15px; padding: 0px;}
89 table.list td.checkbox { width: 15px; padding: 0px;}
89
90
90 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
91 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
91 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
92 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
92
93
93 tr.issue { text-align: center; white-space: nowrap; }
94 tr.issue { text-align: center; white-space: nowrap; }
94 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
95 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
95 tr.issue td.subject { text-align: left; }
96 tr.issue td.subject { text-align: left; }
96 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
97 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
97
98
98 tr.entry { border: 1px solid #f8f8f8; }
99 tr.entry { border: 1px solid #f8f8f8; }
99 tr.entry td { white-space: nowrap; }
100 tr.entry td { white-space: nowrap; }
100 tr.entry td.filename { width: 30%; }
101 tr.entry td.filename { width: 30%; }
101 tr.entry td.size { text-align: right; font-size: 90%; }
102 tr.entry td.size { text-align: right; font-size: 90%; }
102 tr.entry td.revision, tr.entry td.author { text-align: center; }
103 tr.entry td.revision, tr.entry td.author { text-align: center; }
103 tr.entry td.age { text-align: right; }
104 tr.entry td.age { text-align: right; }
104
105
105 tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
106 tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
106 tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
107 tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
107 tr.entry.file td.filename a { margin-left: 16px; }
108 tr.entry.file td.filename a { margin-left: 16px; }
108
109
109 tr.changeset td.author { text-align: center; width: 15%; }
110 tr.changeset td.author { text-align: center; width: 15%; }
110 tr.changeset td.committed_on { text-align: center; width: 15%; }
111 tr.changeset td.committed_on { text-align: center; width: 15%; }
111
112
112 tr.message { height: 2.6em; }
113 tr.message { height: 2.6em; }
113 tr.message td.last_message { font-size: 80%; }
114 tr.message td.last_message { font-size: 80%; }
114 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
115 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
115 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
116 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
116
117
117 tr.user td { width:13%; }
118 tr.user td { width:13%; }
118 tr.user td.email { width:18%; }
119 tr.user td.email { width:18%; }
119 tr.user td { white-space: nowrap; }
120 tr.user td { white-space: nowrap; }
120 tr.user.locked, tr.user.registered { color: #aaa; }
121 tr.user.locked, tr.user.registered { color: #aaa; }
121 tr.user.locked a, tr.user.registered a { color: #aaa; }
122 tr.user.locked a, tr.user.registered a { color: #aaa; }
122
123
123 tr.time-entry { text-align: center; white-space: nowrap; }
124 tr.time-entry { text-align: center; white-space: nowrap; }
124 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
125 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
125 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
126 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
126 td.hours .hours-dec { font-size: 0.9em; }
127 td.hours .hours-dec { font-size: 0.9em; }
127
128
128 table.plugins td { vertical-align: middle; }
129 table.plugins td { vertical-align: middle; }
129 table.plugins td.configure { text-align: right; padding-right: 1em; }
130 table.plugins td.configure { text-align: right; padding-right: 1em; }
130 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
131 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
131 table.plugins span.description { display: block; font-size: 0.9em; }
132 table.plugins span.description { display: block; font-size: 0.9em; }
132 table.plugins span.url { display: block; font-size: 0.9em; }
133 table.plugins span.url { display: block; font-size: 0.9em; }
133
134
134 table.list tbody tr:hover { background-color:#ffffdd; }
135 table.list tbody tr:hover { background-color:#ffffdd; }
135 table td {padding:2px;}
136 table td {padding:2px;}
136 table p {margin:0;}
137 table p {margin:0;}
137 .odd {background-color:#f6f7f8;}
138 .odd {background-color:#f6f7f8;}
138 .even {background-color: #fff;}
139 .even {background-color: #fff;}
139
140
140 .highlight { background-color: #FCFD8D;}
141 .highlight { background-color: #FCFD8D;}
141 .highlight.token-1 { background-color: #faa;}
142 .highlight.token-1 { background-color: #faa;}
142 .highlight.token-2 { background-color: #afa;}
143 .highlight.token-2 { background-color: #afa;}
143 .highlight.token-3 { background-color: #aaf;}
144 .highlight.token-3 { background-color: #aaf;}
144
145
145 .box{
146 .box{
146 padding:6px;
147 padding:6px;
147 margin-bottom: 10px;
148 margin-bottom: 10px;
148 background-color:#f6f6f6;
149 background-color:#f6f6f6;
149 color:#505050;
150 color:#505050;
150 line-height:1.5em;
151 line-height:1.5em;
151 border: 1px solid #e4e4e4;
152 border: 1px solid #e4e4e4;
152 }
153 }
153
154
154 div.square {
155 div.square {
155 border: 1px solid #999;
156 border: 1px solid #999;
156 float: left;
157 float: left;
157 margin: .3em .4em 0 .4em;
158 margin: .3em .4em 0 .4em;
158 overflow: hidden;
159 overflow: hidden;
159 width: .6em; height: .6em;
160 width: .6em; height: .6em;
160 }
161 }
161 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
162 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
162 .contextual input {font-size:0.9em;}
163 .contextual input {font-size:0.9em;}
163 .message .contextual { margin-top: 0; }
164 .message .contextual { margin-top: 0; }
164
165
165 .splitcontentleft{float:left; width:49%;}
166 .splitcontentleft{float:left; width:49%;}
166 .splitcontentright{float:right; width:49%;}
167 .splitcontentright{float:right; width:49%;}
167 form {display: inline;}
168 form {display: inline;}
168 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
169 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
169 fieldset {border: 1px solid #e4e4e4; margin:0;}
170 fieldset {border: 1px solid #e4e4e4; margin:0;}
170 legend {color: #484848;}
171 legend {color: #484848;}
171 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
172 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
172 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
173 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
173 blockquote blockquote { margin-left: 0;}
174 blockquote blockquote { margin-left: 0;}
174 textarea.wiki-edit { width: 99%; }
175 textarea.wiki-edit { width: 99%; }
175 li p {margin-top: 0;}
176 li p {margin-top: 0;}
176 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
177 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
177 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
178 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
178 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
179 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
179 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
180 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
180
181
181 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
182 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
182 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
183 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
183 fieldset#filters table { border-collapse: collapse; }
184 fieldset#filters table { border-collapse: collapse; }
184 fieldset#filters table td { padding: 0; vertical-align: middle; }
185 fieldset#filters table td { padding: 0; vertical-align: middle; }
185 fieldset#filters tr.filter { height: 2em; }
186 fieldset#filters tr.filter { height: 2em; }
186 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
187 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
187 .buttons { font-size: 0.9em; }
188 .buttons { font-size: 0.9em; }
188
189
189 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
190 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
190 div#issue-changesets .changeset { padding: 4px;}
191 div#issue-changesets .changeset { padding: 4px;}
191 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
192 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
192 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
193 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
193
194
194 div#activity dl, #search-results { margin-left: 2em; }
195 div#activity dl, #search-results { margin-left: 2em; }
195 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
196 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
196 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
197 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
197 div#activity dt.me .time { border-bottom: 1px solid #999; }
198 div#activity dt.me .time { border-bottom: 1px solid #999; }
198 div#activity dt .time { color: #777; font-size: 80%; }
199 div#activity dt .time { color: #777; font-size: 80%; }
199 div#activity dd .description, #search-results dd .description { font-style: italic; }
200 div#activity dd .description, #search-results dd .description { font-style: italic; }
200 div#activity span.project:after, #search-results span.project:after { content: " -"; }
201 div#activity span.project:after, #search-results span.project:after { content: " -"; }
201 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
202 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
202
203
203 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
204 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
204
205
205 div#search-results-counts {float:right;}
206 div#search-results-counts {float:right;}
206 div#search-results-counts ul { margin-top: 0.5em; }
207 div#search-results-counts ul { margin-top: 0.5em; }
207 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
208 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
208
209
209 dt.issue { background-image: url(../images/ticket.png); }
210 dt.issue { background-image: url(../images/ticket.png); }
210 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
211 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
211 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
212 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
212 dt.issue-note { background-image: url(../images/ticket_note.png); }
213 dt.issue-note { background-image: url(../images/ticket_note.png); }
213 dt.changeset { background-image: url(../images/changeset.png); }
214 dt.changeset { background-image: url(../images/changeset.png); }
214 dt.news { background-image: url(../images/news.png); }
215 dt.news { background-image: url(../images/news.png); }
215 dt.message { background-image: url(../images/message.png); }
216 dt.message { background-image: url(../images/message.png); }
216 dt.reply { background-image: url(../images/comments.png); }
217 dt.reply { background-image: url(../images/comments.png); }
217 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
218 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
218 dt.attachment { background-image: url(../images/attachment.png); }
219 dt.attachment { background-image: url(../images/attachment.png); }
219 dt.document { background-image: url(../images/document.png); }
220 dt.document { background-image: url(../images/document.png); }
220 dt.project { background-image: url(../images/projects.png); }
221 dt.project { background-image: url(../images/projects.png); }
221
222
222 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
223 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
223
224
224 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
225 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
225 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
226 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
226 div#roadmap .wiki h1:first-child { display: none; }
227 div#roadmap .wiki h1:first-child { display: none; }
227 div#roadmap .wiki h1 { font-size: 120%; }
228 div#roadmap .wiki h1 { font-size: 120%; }
228 div#roadmap .wiki h2 { font-size: 110%; }
229 div#roadmap .wiki h2 { font-size: 110%; }
229
230
230 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
231 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
231 div#version-summary fieldset { margin-bottom: 1em; }
232 div#version-summary fieldset { margin-bottom: 1em; }
232 div#version-summary .total-hours { text-align: right; }
233 div#version-summary .total-hours { text-align: right; }
233
234
234 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
235 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
235 table#time-report tbody tr { font-style: italic; color: #777; }
236 table#time-report tbody tr { font-style: italic; color: #777; }
236 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
237 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
237 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
238 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
238 table#time-report .hours-dec { font-size: 0.9em; }
239 table#time-report .hours-dec { font-size: 0.9em; }
239
240
240 form#issue-form .attributes { margin-bottom: 8px; }
241 form#issue-form .attributes { margin-bottom: 8px; }
241 form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
242 form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
242 form#issue-form .attributes select { min-width: 30%; }
243 form#issue-form .attributes select { min-width: 30%; }
243
244
244 ul.projects { margin: 0; padding-left: 1em; }
245 ul.projects { margin: 0; padding-left: 1em; }
245 ul.projects.root { margin: 0; padding: 0; }
246 ul.projects.root { margin: 0; padding: 0; }
246 ul.projects ul { border-left: 3px solid #e0e0e0; }
247 ul.projects ul { border-left: 3px solid #e0e0e0; }
247 ul.projects li { list-style-type:none; }
248 ul.projects li { list-style-type:none; }
248 ul.projects li.root { margin-bottom: 1em; }
249 ul.projects li.root { margin-bottom: 1em; }
249 ul.projects li.child { margin-top: 1em;}
250 ul.projects li.child { margin-top: 1em;}
250 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
251 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
251 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
252 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
252
253
253 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
254 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
254 #tracker_project_ids li { list-style-type:none; }
255 #tracker_project_ids li { list-style-type:none; }
255
256
256 ul.properties {padding:0; font-size: 0.9em; color: #777;}
257 ul.properties {padding:0; font-size: 0.9em; color: #777;}
257 ul.properties li {list-style-type:none;}
258 ul.properties li {list-style-type:none;}
258 ul.properties li span {font-style:italic;}
259 ul.properties li span {font-style:italic;}
259
260
260 .total-hours { font-size: 110%; font-weight: bold; }
261 .total-hours { font-size: 110%; font-weight: bold; }
261 .total-hours span.hours-int { font-size: 120%; }
262 .total-hours span.hours-int { font-size: 120%; }
262
263
263 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
264 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
264 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
265 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
265
266
266 .pagination {font-size: 90%}
267 .pagination {font-size: 90%}
267 p.pagination {margin-top:8px;}
268 p.pagination {margin-top:8px;}
268
269
269 /***** Tabular forms ******/
270 /***** Tabular forms ******/
270 .tabular p{
271 .tabular p{
271 margin: 0;
272 margin: 0;
272 padding: 5px 0 8px 0;
273 padding: 5px 0 8px 0;
273 padding-left: 180px; /*width of left column containing the label elements*/
274 padding-left: 180px; /*width of left column containing the label elements*/
274 height: 1%;
275 height: 1%;
275 clear:left;
276 clear:left;
276 }
277 }
277
278
278 html>body .tabular p {overflow:hidden;}
279 html>body .tabular p {overflow:hidden;}
279
280
280 .tabular label{
281 .tabular label{
281 font-weight: bold;
282 font-weight: bold;
282 float: left;
283 float: left;
283 text-align: right;
284 text-align: right;
284 margin-left: -180px; /*width of left column*/
285 margin-left: -180px; /*width of left column*/
285 width: 175px; /*width of labels. Should be smaller than left column to create some right
286 width: 175px; /*width of labels. Should be smaller than left column to create some right
286 margin*/
287 margin*/
287 }
288 }
288
289
289 .tabular label.floating{
290 .tabular label.floating{
290 font-weight: normal;
291 font-weight: normal;
291 margin-left: 0px;
292 margin-left: 0px;
292 text-align: left;
293 text-align: left;
293 width: 270px;
294 width: 270px;
294 }
295 }
295
296
296 input#time_entry_comments { width: 90%;}
297 input#time_entry_comments { width: 90%;}
297
298
298 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
299 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
299
300
300 .tabular.settings p{ padding-left: 300px; }
301 .tabular.settings p{ padding-left: 300px; }
301 .tabular.settings label{ margin-left: -300px; width: 295px; }
302 .tabular.settings label{ margin-left: -300px; width: 295px; }
302
303
303 .required {color: #bb0000;}
304 .required {color: #bb0000;}
304 .summary {font-style: italic;}
305 .summary {font-style: italic;}
305
306
306 #attachments_fields input[type=text] {margin-left: 8px; }
307 #attachments_fields input[type=text] {margin-left: 8px; }
307
308
308 div.attachments { margin-top: 12px; }
309 div.attachments { margin-top: 12px; }
309 div.attachments p { margin:4px 0 2px 0; }
310 div.attachments p { margin:4px 0 2px 0; }
310 div.attachments img { vertical-align: middle; }
311 div.attachments img { vertical-align: middle; }
311 div.attachments span.author { font-size: 0.9em; color: #888; }
312 div.attachments span.author { font-size: 0.9em; color: #888; }
312
313
313 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
314 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
314 .other-formats span + span:before { content: "| "; }
315 .other-formats span + span:before { content: "| "; }
315
316
316 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
317 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
317
318
318 /***** Flash & error messages ****/
319 /***** Flash & error messages ****/
319 #errorExplanation, div.flash, .nodata, .warning {
320 #errorExplanation, div.flash, .nodata, .warning {
320 padding: 4px 4px 4px 30px;
321 padding: 4px 4px 4px 30px;
321 margin-bottom: 12px;
322 margin-bottom: 12px;
322 font-size: 1.1em;
323 font-size: 1.1em;
323 border: 2px solid;
324 border: 2px solid;
324 }
325 }
325
326
326 div.flash {margin-top: 8px;}
327 div.flash {margin-top: 8px;}
327
328
328 div.flash.error, #errorExplanation {
329 div.flash.error, #errorExplanation {
329 background: url(../images/false.png) 8px 5px no-repeat;
330 background: url(../images/false.png) 8px 5px no-repeat;
330 background-color: #ffe3e3;
331 background-color: #ffe3e3;
331 border-color: #dd0000;
332 border-color: #dd0000;
332 color: #550000;
333 color: #550000;
333 }
334 }
334
335
335 div.flash.notice {
336 div.flash.notice {
336 background: url(../images/true.png) 8px 5px no-repeat;
337 background: url(../images/true.png) 8px 5px no-repeat;
337 background-color: #dfffdf;
338 background-color: #dfffdf;
338 border-color: #9fcf9f;
339 border-color: #9fcf9f;
339 color: #005f00;
340 color: #005f00;
340 }
341 }
341
342
342 div.flash.warning {
343 div.flash.warning {
343 background: url(../images/warning.png) 8px 5px no-repeat;
344 background: url(../images/warning.png) 8px 5px no-repeat;
344 background-color: #FFEBC1;
345 background-color: #FFEBC1;
345 border-color: #FDBF3B;
346 border-color: #FDBF3B;
346 color: #A6750C;
347 color: #A6750C;
347 text-align: left;
348 text-align: left;
348 }
349 }
349
350
350 .nodata, .warning {
351 .nodata, .warning {
351 text-align: center;
352 text-align: center;
352 background-color: #FFEBC1;
353 background-color: #FFEBC1;
353 border-color: #FDBF3B;
354 border-color: #FDBF3B;
354 color: #A6750C;
355 color: #A6750C;
355 }
356 }
356
357
357 #errorExplanation ul { font-size: 0.9em;}
358 #errorExplanation ul { font-size: 0.9em;}
358
359
359 /***** Ajax indicator ******/
360 /***** Ajax indicator ******/
360 #ajax-indicator {
361 #ajax-indicator {
361 position: absolute; /* fixed not supported by IE */
362 position: absolute; /* fixed not supported by IE */
362 background-color:#eee;
363 background-color:#eee;
363 border: 1px solid #bbb;
364 border: 1px solid #bbb;
364 top:35%;
365 top:35%;
365 left:40%;
366 left:40%;
366 width:20%;
367 width:20%;
367 font-weight:bold;
368 font-weight:bold;
368 text-align:center;
369 text-align:center;
369 padding:0.6em;
370 padding:0.6em;
370 z-index:100;
371 z-index:100;
371 filter:alpha(opacity=50);
372 filter:alpha(opacity=50);
372 opacity: 0.5;
373 opacity: 0.5;
373 }
374 }
374
375
375 html>body #ajax-indicator { position: fixed; }
376 html>body #ajax-indicator { position: fixed; }
376
377
377 #ajax-indicator span {
378 #ajax-indicator span {
378 background-position: 0% 40%;
379 background-position: 0% 40%;
379 background-repeat: no-repeat;
380 background-repeat: no-repeat;
380 background-image: url(../images/loading.gif);
381 background-image: url(../images/loading.gif);
381 padding-left: 26px;
382 padding-left: 26px;
382 vertical-align: bottom;
383 vertical-align: bottom;
383 }
384 }
384
385
385 /***** Calendar *****/
386 /***** Calendar *****/
386 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
387 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
387 table.cal thead th {width: 14%;}
388 table.cal thead th {width: 14%;}
388 table.cal tbody tr {height: 100px;}
389 table.cal tbody tr {height: 100px;}
389 table.cal th { background-color:#EEEEEE; padding: 4px; }
390 table.cal th { background-color:#EEEEEE; padding: 4px; }
390 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
391 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
391 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
392 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
392 table.cal td.odd p.day-num {color: #bbb;}
393 table.cal td.odd p.day-num {color: #bbb;}
393 table.cal td.today {background:#ffffdd;}
394 table.cal td.today {background:#ffffdd;}
394 table.cal td.today p.day-num {font-weight: bold;}
395 table.cal td.today p.day-num {font-weight: bold;}
395
396
396 /***** Tooltips ******/
397 /***** Tooltips ******/
397 .tooltip{position:relative;z-index:24;}
398 .tooltip{position:relative;z-index:24;}
398 .tooltip:hover{z-index:25;color:#000;}
399 .tooltip:hover{z-index:25;color:#000;}
399 .tooltip span.tip{display: none; text-align:left;}
400 .tooltip span.tip{display: none; text-align:left;}
400
401
401 div.tooltip:hover span.tip{
402 div.tooltip:hover span.tip{
402 display:block;
403 display:block;
403 position:absolute;
404 position:absolute;
404 top:12px; left:24px; width:270px;
405 top:12px; left:24px; width:270px;
405 border:1px solid #555;
406 border:1px solid #555;
406 background-color:#fff;
407 background-color:#fff;
407 padding: 4px;
408 padding: 4px;
408 font-size: 0.8em;
409 font-size: 0.8em;
409 color:#505050;
410 color:#505050;
410 }
411 }
411
412
412 /***** Progress bar *****/
413 /***** Progress bar *****/
413 table.progress {
414 table.progress {
414 border: 1px solid #D7D7D7;
415 border: 1px solid #D7D7D7;
415 border-collapse: collapse;
416 border-collapse: collapse;
416 border-spacing: 0pt;
417 border-spacing: 0pt;
417 empty-cells: show;
418 empty-cells: show;
418 text-align: center;
419 text-align: center;
419 float:left;
420 float:left;
420 margin: 1px 6px 1px 0px;
421 margin: 1px 6px 1px 0px;
421 }
422 }
422
423
423 table.progress td { height: 0.9em; }
424 table.progress td { height: 0.9em; }
424 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
425 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
425 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
426 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
426 table.progress td.open { background: #FFF none repeat scroll 0%; }
427 table.progress td.open { background: #FFF none repeat scroll 0%; }
427 p.pourcent {font-size: 80%;}
428 p.pourcent {font-size: 80%;}
428 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
429 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
429
430
430 /***** Tabs *****/
431 /***** Tabs *****/
431 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
432 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
432 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
433 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
433 #content .tabs>ul { bottom:-1px; } /* others */
434 #content .tabs>ul { bottom:-1px; } /* others */
434 #content .tabs ul li {
435 #content .tabs ul li {
435 float:left;
436 float:left;
436 list-style-type:none;
437 list-style-type:none;
437 white-space:nowrap;
438 white-space:nowrap;
438 margin-right:8px;
439 margin-right:8px;
439 background:#fff;
440 background:#fff;
440 }
441 }
441 #content .tabs ul li a{
442 #content .tabs ul li a{
442 display:block;
443 display:block;
443 font-size: 0.9em;
444 font-size: 0.9em;
444 text-decoration:none;
445 text-decoration:none;
445 line-height:1.3em;
446 line-height:1.3em;
446 padding:4px 6px 4px 6px;
447 padding:4px 6px 4px 6px;
447 border: 1px solid #ccc;
448 border: 1px solid #ccc;
448 border-bottom: 1px solid #bbbbbb;
449 border-bottom: 1px solid #bbbbbb;
449 background-color: #eeeeee;
450 background-color: #eeeeee;
450 color:#777;
451 color:#777;
451 font-weight:bold;
452 font-weight:bold;
452 }
453 }
453
454
454 #content .tabs ul li a:hover {
455 #content .tabs ul li a:hover {
455 background-color: #ffffdd;
456 background-color: #ffffdd;
456 text-decoration:none;
457 text-decoration:none;
457 }
458 }
458
459
459 #content .tabs ul li a.selected {
460 #content .tabs ul li a.selected {
460 background-color: #fff;
461 background-color: #fff;
461 border: 1px solid #bbbbbb;
462 border: 1px solid #bbbbbb;
462 border-bottom: 1px solid #fff;
463 border-bottom: 1px solid #fff;
463 }
464 }
464
465
465 #content .tabs ul li a.selected:hover {
466 #content .tabs ul li a.selected:hover {
466 background-color: #fff;
467 background-color: #fff;
467 }
468 }
468
469
469 /***** Diff *****/
470 /***** Diff *****/
470 .diff_out { background: #fcc; }
471 .diff_out { background: #fcc; }
471 .diff_in { background: #cfc; }
472 .diff_in { background: #cfc; }
472
473
473 /***** Wiki *****/
474 /***** Wiki *****/
474 div.wiki table {
475 div.wiki table {
475 border: 1px solid #505050;
476 border: 1px solid #505050;
476 border-collapse: collapse;
477 border-collapse: collapse;
477 margin-bottom: 1em;
478 margin-bottom: 1em;
478 }
479 }
479
480
480 div.wiki table, div.wiki td, div.wiki th {
481 div.wiki table, div.wiki td, div.wiki th {
481 border: 1px solid #bbb;
482 border: 1px solid #bbb;
482 padding: 4px;
483 padding: 4px;
483 }
484 }
484
485
485 div.wiki .external {
486 div.wiki .external {
486 background-position: 0% 60%;
487 background-position: 0% 60%;
487 background-repeat: no-repeat;
488 background-repeat: no-repeat;
488 padding-left: 12px;
489 padding-left: 12px;
489 background-image: url(../images/external.png);
490 background-image: url(../images/external.png);
490 }
491 }
491
492
492 div.wiki a.new {
493 div.wiki a.new {
493 color: #b73535;
494 color: #b73535;
494 }
495 }
495
496
496 div.wiki pre {
497 div.wiki pre {
497 margin: 1em 1em 1em 1.6em;
498 margin: 1em 1em 1em 1.6em;
498 padding: 2px;
499 padding: 2px;
499 background-color: #fafafa;
500 background-color: #fafafa;
500 border: 1px solid #dadada;
501 border: 1px solid #dadada;
501 width:95%;
502 width:95%;
502 overflow-x: auto;
503 overflow-x: auto;
503 }
504 }
504
505
505 div.wiki ul.toc {
506 div.wiki ul.toc {
506 background-color: #ffffdd;
507 background-color: #ffffdd;
507 border: 1px solid #e4e4e4;
508 border: 1px solid #e4e4e4;
508 padding: 4px;
509 padding: 4px;
509 line-height: 1.2em;
510 line-height: 1.2em;
510 margin-bottom: 12px;
511 margin-bottom: 12px;
511 margin-right: 12px;
512 margin-right: 12px;
512 margin-left: 0;
513 margin-left: 0;
513 display: table
514 display: table
514 }
515 }
515 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
516 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
516
517
517 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
518 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
518 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
519 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
519 div.wiki ul.toc li { list-style-type:none;}
520 div.wiki ul.toc li { list-style-type:none;}
520 div.wiki ul.toc li.heading2 { margin-left: 6px; }
521 div.wiki ul.toc li.heading2 { margin-left: 6px; }
521 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
522 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
522
523
523 div.wiki ul.toc a {
524 div.wiki ul.toc a {
524 font-size: 0.9em;
525 font-size: 0.9em;
525 font-weight: normal;
526 font-weight: normal;
526 text-decoration: none;
527 text-decoration: none;
527 color: #606060;
528 color: #606060;
528 }
529 }
529 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
530 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
530
531
531 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
532 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
532 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
533 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
533 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
534 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
534
535
535 /***** My page layout *****/
536 /***** My page layout *****/
536 .block-receiver {
537 .block-receiver {
537 border:1px dashed #c0c0c0;
538 border:1px dashed #c0c0c0;
538 margin-bottom: 20px;
539 margin-bottom: 20px;
539 padding: 15px 0 15px 0;
540 padding: 15px 0 15px 0;
540 }
541 }
541
542
542 .mypage-box {
543 .mypage-box {
543 margin:0 0 20px 0;
544 margin:0 0 20px 0;
544 color:#505050;
545 color:#505050;
545 line-height:1.5em;
546 line-height:1.5em;
546 }
547 }
547
548
548 .handle {
549 .handle {
549 cursor: move;
550 cursor: move;
550 }
551 }
551
552
552 a.close-icon {
553 a.close-icon {
553 display:block;
554 display:block;
554 margin-top:3px;
555 margin-top:3px;
555 overflow:hidden;
556 overflow:hidden;
556 width:12px;
557 width:12px;
557 height:12px;
558 height:12px;
558 background-repeat: no-repeat;
559 background-repeat: no-repeat;
559 cursor:pointer;
560 cursor:pointer;
560 background-image:url('../images/close.png');
561 background-image:url('../images/close.png');
561 }
562 }
562
563
563 a.close-icon:hover {
564 a.close-icon:hover {
564 background-image:url('../images/close_hl.png');
565 background-image:url('../images/close_hl.png');
565 }
566 }
566
567
567 /***** Gantt chart *****/
568 /***** Gantt chart *****/
568 .gantt_hdr {
569 .gantt_hdr {
569 position:absolute;
570 position:absolute;
570 top:0;
571 top:0;
571 height:16px;
572 height:16px;
572 border-top: 1px solid #c0c0c0;
573 border-top: 1px solid #c0c0c0;
573 border-bottom: 1px solid #c0c0c0;
574 border-bottom: 1px solid #c0c0c0;
574 border-right: 1px solid #c0c0c0;
575 border-right: 1px solid #c0c0c0;
575 text-align: center;
576 text-align: center;
576 overflow: hidden;
577 overflow: hidden;
577 }
578 }
578
579
579 .task {
580 .task {
580 position: absolute;
581 position: absolute;
581 height:8px;
582 height:8px;
582 font-size:0.8em;
583 font-size:0.8em;
583 color:#888;
584 color:#888;
584 padding:0;
585 padding:0;
585 margin:0;
586 margin:0;
586 line-height:0.8em;
587 line-height:0.8em;
587 }
588 }
588
589
589 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
590 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
590 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
591 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
591 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
592 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
592 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
593 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
593
594
594 /***** Icons *****/
595 /***** Icons *****/
595 .icon {
596 .icon {
596 background-position: 0% 40%;
597 background-position: 0% 40%;
597 background-repeat: no-repeat;
598 background-repeat: no-repeat;
598 padding-left: 20px;
599 padding-left: 20px;
599 padding-top: 2px;
600 padding-top: 2px;
600 padding-bottom: 3px;
601 padding-bottom: 3px;
601 }
602 }
602
603
603 .icon22 {
604 .icon22 {
604 background-position: 0% 40%;
605 background-position: 0% 40%;
605 background-repeat: no-repeat;
606 background-repeat: no-repeat;
606 padding-left: 26px;
607 padding-left: 26px;
607 line-height: 22px;
608 line-height: 22px;
608 vertical-align: middle;
609 vertical-align: middle;
609 }
610 }
610
611
611 .icon-add { background-image: url(../images/add.png); }
612 .icon-add { background-image: url(../images/add.png); }
612 .icon-edit { background-image: url(../images/edit.png); }
613 .icon-edit { background-image: url(../images/edit.png); }
613 .icon-copy { background-image: url(../images/copy.png); }
614 .icon-copy { background-image: url(../images/copy.png); }
614 .icon-del { background-image: url(../images/delete.png); }
615 .icon-del { background-image: url(../images/delete.png); }
615 .icon-move { background-image: url(../images/move.png); }
616 .icon-move { background-image: url(../images/move.png); }
616 .icon-save { background-image: url(../images/save.png); }
617 .icon-save { background-image: url(../images/save.png); }
617 .icon-cancel { background-image: url(../images/cancel.png); }
618 .icon-cancel { background-image: url(../images/cancel.png); }
618 .icon-file { background-image: url(../images/file.png); }
619 .icon-file { background-image: url(../images/file.png); }
619 .icon-folder { background-image: url(../images/folder.png); }
620 .icon-folder { background-image: url(../images/folder.png); }
620 .open .icon-folder { background-image: url(../images/folder_open.png); }
621 .open .icon-folder { background-image: url(../images/folder_open.png); }
621 .icon-package { background-image: url(../images/package.png); }
622 .icon-package { background-image: url(../images/package.png); }
622 .icon-home { background-image: url(../images/home.png); }
623 .icon-home { background-image: url(../images/home.png); }
623 .icon-user { background-image: url(../images/user.png); }
624 .icon-user { background-image: url(../images/user.png); }
624 .icon-mypage { background-image: url(../images/user_page.png); }
625 .icon-mypage { background-image: url(../images/user_page.png); }
625 .icon-admin { background-image: url(../images/admin.png); }
626 .icon-admin { background-image: url(../images/admin.png); }
626 .icon-projects { background-image: url(../images/projects.png); }
627 .icon-projects { background-image: url(../images/projects.png); }
627 .icon-help { background-image: url(../images/help.png); }
628 .icon-help { background-image: url(../images/help.png); }
628 .icon-attachment { background-image: url(../images/attachment.png); }
629 .icon-attachment { background-image: url(../images/attachment.png); }
629 .icon-index { background-image: url(../images/index.png); }
630 .icon-index { background-image: url(../images/index.png); }
630 .icon-history { background-image: url(../images/history.png); }
631 .icon-history { background-image: url(../images/history.png); }
631 .icon-time { background-image: url(../images/time.png); }
632 .icon-time { background-image: url(../images/time.png); }
632 .icon-time-add { background-image: url(../images/time_add.png); }
633 .icon-time-add { background-image: url(../images/time_add.png); }
633 .icon-stats { background-image: url(../images/stats.png); }
634 .icon-stats { background-image: url(../images/stats.png); }
634 .icon-warning { background-image: url(../images/warning.png); }
635 .icon-warning { background-image: url(../images/warning.png); }
635 .icon-fav { background-image: url(../images/fav.png); }
636 .icon-fav { background-image: url(../images/fav.png); }
636 .icon-fav-off { background-image: url(../images/fav_off.png); }
637 .icon-fav-off { background-image: url(../images/fav_off.png); }
637 .icon-reload { background-image: url(../images/reload.png); }
638 .icon-reload { background-image: url(../images/reload.png); }
638 .icon-lock { background-image: url(../images/locked.png); }
639 .icon-lock { background-image: url(../images/locked.png); }
639 .icon-unlock { background-image: url(../images/unlock.png); }
640 .icon-unlock { background-image: url(../images/unlock.png); }
640 .icon-checked { background-image: url(../images/true.png); }
641 .icon-checked { background-image: url(../images/true.png); }
641 .icon-details { background-image: url(../images/zoom_in.png); }
642 .icon-details { background-image: url(../images/zoom_in.png); }
642 .icon-report { background-image: url(../images/report.png); }
643 .icon-report { background-image: url(../images/report.png); }
643 .icon-comment { background-image: url(../images/comment.png); }
644 .icon-comment { background-image: url(../images/comment.png); }
644
645
645 .icon22-projects { background-image: url(../images/22x22/projects.png); }
646 .icon22-projects { background-image: url(../images/22x22/projects.png); }
646 .icon22-users { background-image: url(../images/22x22/users.png); }
647 .icon22-users { background-image: url(../images/22x22/users.png); }
647 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
648 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
648 .icon22-role { background-image: url(../images/22x22/role.png); }
649 .icon22-role { background-image: url(../images/22x22/role.png); }
649 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
650 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
650 .icon22-options { background-image: url(../images/22x22/options.png); }
651 .icon22-options { background-image: url(../images/22x22/options.png); }
651 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
652 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
652 .icon22-authent { background-image: url(../images/22x22/authent.png); }
653 .icon22-authent { background-image: url(../images/22x22/authent.png); }
653 .icon22-info { background-image: url(../images/22x22/info.png); }
654 .icon22-info { background-image: url(../images/22x22/info.png); }
654 .icon22-comment { background-image: url(../images/22x22/comment.png); }
655 .icon22-comment { background-image: url(../images/22x22/comment.png); }
655 .icon22-package { background-image: url(../images/22x22/package.png); }
656 .icon22-package { background-image: url(../images/22x22/package.png); }
656 .icon22-settings { background-image: url(../images/22x22/settings.png); }
657 .icon22-settings { background-image: url(../images/22x22/settings.png); }
657 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
658 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
658
659
659 img.gravatar {
660 img.gravatar {
660 padding: 2px;
661 padding: 2px;
661 border: solid 1px #d5d5d5;
662 border: solid 1px #d5d5d5;
662 background: #fff;
663 background: #fff;
663 }
664 }
664
665
665 div.issue img.gravatar {
666 div.issue img.gravatar {
666 float: right;
667 float: right;
667 margin: 0 0 0 1em;
668 margin: 0 0 0 1em;
668 padding: 5px;
669 padding: 5px;
669 }
670 }
670
671
671 div.issue table img.gravatar {
672 div.issue table img.gravatar {
672 height: 14px;
673 height: 14px;
673 width: 14px;
674 width: 14px;
674 padding: 2px;
675 padding: 2px;
675 float: left;
676 float: left;
676 margin: 0 0.5em 0 0;
677 margin: 0 0.5em 0 0;
677 }
678 }
678
679
679 #history img.gravatar {
680 #history img.gravatar {
680 padding: 3px;
681 padding: 3px;
681 margin: 0 1.5em 1em 0;
682 margin: 0 1.5em 1em 0;
682 float: left;
683 float: left;
683 }
684 }
684
685
685 td.username img.gravatar {
686 td.username img.gravatar {
686 float: left;
687 float: left;
687 margin: 0 1em 0 0;
688 margin: 0 1em 0 0;
688 }
689 }
689
690
690 #activity dt img.gravatar {
691 #activity dt img.gravatar {
691 float: left;
692 float: left;
692 margin: 0 1em 1em 0;
693 margin: 0 1em 1em 0;
693 }
694 }
694
695
695 #activity dt,
696 #activity dt,
696 .journal {
697 .journal {
697 clear: left;
698 clear: left;
698 }
699 }
699
700
700 h2 img { vertical-align:middle; }
701 h2 img { vertical-align:middle; }
701
702
702
703
703 /***** Media print specific styles *****/
704 /***** Media print specific styles *****/
704 @media print {
705 @media print {
705 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
706 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
706 #main { background: #fff; }
707 #main { background: #fff; }
707 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
708 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
708 }
709 }
@@ -1,517 +1,533
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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
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 < Test::Unit::TestCase
24 class ProjectsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :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
27 :attachments
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_routing
37 def test_index_routing
38 assert_routing(
38 assert_routing(
39 {:method => :get, :path => '/projects'},
39 {:method => :get, :path => '/projects'},
40 :controller => 'projects', :action => 'index'
40 :controller => 'projects', :action => 'index'
41 )
41 )
42 end
42 end
43
43
44 def test_index
44 def test_index
45 get :index
45 get :index
46 assert_response :success
46 assert_response :success
47 assert_template 'index'
47 assert_template 'index'
48 assert_not_nil assigns(:projects)
48 assert_not_nil assigns(:projects)
49
49
50 assert_tag :ul, :child => {:tag => 'li',
50 assert_tag :ul, :child => {:tag => 'li',
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
52 :child => { :tag => 'ul',
52 :child => { :tag => 'ul',
53 :descendant => { :tag => 'a',
53 :descendant => { :tag => 'a',
54 :content => 'Child of private child'
54 :content => 'Child of private child'
55 }
55 }
56 }
56 }
57 }
57 }
58
58
59 assert_no_tag :a, :content => /Private child of eCookbook/
59 assert_no_tag :a, :content => /Private child of eCookbook/
60 end
60 end
61
61
62 def test_index_atom_routing
62 def test_index_atom_routing
63 assert_routing(
63 assert_routing(
64 {:method => :get, :path => '/projects.atom'},
64 {:method => :get, :path => '/projects.atom'},
65 :controller => 'projects', :action => 'index', :format => 'atom'
65 :controller => 'projects', :action => 'index', :format => 'atom'
66 )
66 )
67 end
67 end
68
68
69 def test_index_atom
69 def test_index_atom
70 get :index, :format => 'atom'
70 get :index, :format => 'atom'
71 assert_response :success
71 assert_response :success
72 assert_template 'common/feed.atom.rxml'
72 assert_template 'common/feed.atom.rxml'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
75 end
75 end
76
76
77 def test_add_routing
77 def test_add_routing
78 assert_routing(
78 assert_routing(
79 {:method => :get, :path => '/projects/new'},
79 {:method => :get, :path => '/projects/new'},
80 :controller => 'projects', :action => 'add'
80 :controller => 'projects', :action => 'add'
81 )
81 )
82 assert_recognizes(
82 assert_recognizes(
83 {:controller => 'projects', :action => 'add'},
83 {:controller => 'projects', :action => 'add'},
84 {:method => :post, :path => '/projects/new'}
84 {:method => :post, :path => '/projects/new'}
85 )
85 )
86 assert_recognizes(
86 assert_recognizes(
87 {:controller => 'projects', :action => 'add'},
87 {:controller => 'projects', :action => 'add'},
88 {:method => :post, :path => '/projects'}
88 {:method => :post, :path => '/projects'}
89 )
89 )
90 end
90 end
91
91
92 def test_show_routing
92 def test_show_routing
93 assert_routing(
93 assert_routing(
94 {:method => :get, :path => '/projects/test'},
94 {:method => :get, :path => '/projects/test'},
95 :controller => 'projects', :action => 'show', :id => 'test'
95 :controller => 'projects', :action => 'show', :id => 'test'
96 )
96 )
97 end
97 end
98
98
99 def test_show_by_id
99 def test_show_by_id
100 get :show, :id => 1
100 get :show, :id => 1
101 assert_response :success
101 assert_response :success
102 assert_template 'show'
102 assert_template 'show'
103 assert_not_nil assigns(:project)
103 assert_not_nil assigns(:project)
104 end
104 end
105
105
106 def test_show_by_identifier
106 def test_show_by_identifier
107 get :show, :id => 'ecookbook'
107 get :show, :id => 'ecookbook'
108 assert_response :success
108 assert_response :success
109 assert_template 'show'
109 assert_template 'show'
110 assert_not_nil assigns(:project)
110 assert_not_nil assigns(:project)
111 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
111 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
112 end
112 end
113
113
114 def test_private_subprojects_hidden
114 def test_private_subprojects_hidden
115 get :show, :id => 'ecookbook'
115 get :show, :id => 'ecookbook'
116 assert_response :success
116 assert_response :success
117 assert_template 'show'
117 assert_template 'show'
118 assert_no_tag :tag => 'a', :content => /Private child/
118 assert_no_tag :tag => 'a', :content => /Private child/
119 end
119 end
120
120
121 def test_private_subprojects_visible
121 def test_private_subprojects_visible
122 @request.session[:user_id] = 2 # manager who is a member of the private subproject
122 @request.session[:user_id] = 2 # manager who is a member of the private subproject
123 get :show, :id => 'ecookbook'
123 get :show, :id => 'ecookbook'
124 assert_response :success
124 assert_response :success
125 assert_template 'show'
125 assert_template 'show'
126 assert_tag :tag => 'a', :content => /Private child/
126 assert_tag :tag => 'a', :content => /Private child/
127 end
127 end
128
128
129 def test_settings_routing
129 def test_settings_routing
130 assert_routing(
130 assert_routing(
131 {:method => :get, :path => '/projects/4223/settings'},
131 {:method => :get, :path => '/projects/4223/settings'},
132 :controller => 'projects', :action => 'settings', :id => '4223'
132 :controller => 'projects', :action => 'settings', :id => '4223'
133 )
133 )
134 assert_routing(
134 assert_routing(
135 {:method => :get, :path => '/projects/4223/settings/members'},
135 {:method => :get, :path => '/projects/4223/settings/members'},
136 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
136 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
137 )
137 )
138 end
138 end
139
139
140 def test_settings
140 def test_settings
141 @request.session[:user_id] = 2 # manager
141 @request.session[:user_id] = 2 # manager
142 get :settings, :id => 1
142 get :settings, :id => 1
143 assert_response :success
143 assert_response :success
144 assert_template 'settings'
144 assert_template 'settings'
145 end
145 end
146
146
147 def test_edit
147 def test_edit
148 @request.session[:user_id] = 2 # manager
148 @request.session[:user_id] = 2 # manager
149 post :edit, :id => 1, :project => {:name => 'Test changed name',
149 post :edit, :id => 1, :project => {:name => 'Test changed name',
150 :issue_custom_field_ids => ['']}
150 :issue_custom_field_ids => ['']}
151 assert_redirected_to 'projects/settings/ecookbook'
151 assert_redirected_to 'projects/settings/ecookbook'
152 project = Project.find(1)
152 project = Project.find(1)
153 assert_equal 'Test changed name', project.name
153 assert_equal 'Test changed name', project.name
154 end
154 end
155
155
156 def test_add_version_routing
156 def test_add_version_routing
157 assert_routing(
157 assert_routing(
158 {:method => :get, :path => 'projects/64/versions/new'},
158 {:method => :get, :path => 'projects/64/versions/new'},
159 :controller => 'projects', :action => 'add_version', :id => '64'
159 :controller => 'projects', :action => 'add_version', :id => '64'
160 )
160 )
161 assert_routing(
161 assert_routing(
162 #TODO: use PUT
162 #TODO: use PUT
163 {:method => :post, :path => 'projects/64/versions/new'},
163 {:method => :post, :path => 'projects/64/versions/new'},
164 :controller => 'projects', :action => 'add_version', :id => '64'
164 :controller => 'projects', :action => 'add_version', :id => '64'
165 )
165 )
166 end
166 end
167
167
168 def test_add_issue_category_routing
168 def test_add_issue_category_routing
169 assert_routing(
169 assert_routing(
170 {:method => :get, :path => 'projects/test/categories/new'},
170 {:method => :get, :path => 'projects/test/categories/new'},
171 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
171 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
172 )
172 )
173 assert_routing(
173 assert_routing(
174 #TODO: use PUT and update form
174 #TODO: use PUT and update form
175 {:method => :post, :path => 'projects/64/categories/new'},
175 {:method => :post, :path => 'projects/64/categories/new'},
176 :controller => 'projects', :action => 'add_issue_category', :id => '64'
176 :controller => 'projects', :action => 'add_issue_category', :id => '64'
177 )
177 )
178 end
178 end
179
179
180 def test_destroy_routing
180 def test_destroy_routing
181 assert_routing(
181 assert_routing(
182 {:method => :get, :path => '/projects/567/destroy'},
182 {:method => :get, :path => '/projects/567/destroy'},
183 :controller => 'projects', :action => 'destroy', :id => '567'
183 :controller => 'projects', :action => 'destroy', :id => '567'
184 )
184 )
185 assert_routing(
185 assert_routing(
186 #TODO: use DELETE and update form
186 #TODO: use DELETE and update form
187 {:method => :post, :path => 'projects/64/destroy'},
187 {:method => :post, :path => 'projects/64/destroy'},
188 :controller => 'projects', :action => 'destroy', :id => '64'
188 :controller => 'projects', :action => 'destroy', :id => '64'
189 )
189 )
190 end
190 end
191
191
192 def test_get_destroy
192 def test_get_destroy
193 @request.session[:user_id] = 1 # admin
193 @request.session[:user_id] = 1 # admin
194 get :destroy, :id => 1
194 get :destroy, :id => 1
195 assert_response :success
195 assert_response :success
196 assert_template 'destroy'
196 assert_template 'destroy'
197 assert_not_nil Project.find_by_id(1)
197 assert_not_nil Project.find_by_id(1)
198 end
198 end
199
199
200 def test_post_destroy
200 def test_post_destroy
201 @request.session[:user_id] = 1 # admin
201 @request.session[:user_id] = 1 # admin
202 post :destroy, :id => 1, :confirm => 1
202 post :destroy, :id => 1, :confirm => 1
203 assert_redirected_to 'admin/projects'
203 assert_redirected_to 'admin/projects'
204 assert_nil Project.find_by_id(1)
204 assert_nil Project.find_by_id(1)
205 end
205 end
206
206
207 def test_add_file
207 def test_add_file
208 set_tmp_attachments_directory
208 set_tmp_attachments_directory
209 @request.session[:user_id] = 2
209 @request.session[:user_id] = 2
210 Setting.notified_events = ['file_added']
210 Setting.notified_events = ['file_added']
211 ActionMailer::Base.deliveries.clear
211 ActionMailer::Base.deliveries.clear
212
212
213 assert_difference 'Attachment.count' do
213 assert_difference 'Attachment.count' do
214 post :add_file, :id => 1, :version_id => '',
214 post :add_file, :id => 1, :version_id => '',
215 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
215 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
216 end
216 end
217 assert_redirected_to 'projects/list_files/ecookbook'
217 assert_redirected_to 'projects/list_files/ecookbook'
218 a = Attachment.find(:first, :order => 'created_on DESC')
218 a = Attachment.find(:first, :order => 'created_on DESC')
219 assert_equal 'testfile.txt', a.filename
219 assert_equal 'testfile.txt', a.filename
220 assert_equal Project.find(1), a.container
220 assert_equal Project.find(1), a.container
221
221
222 mail = ActionMailer::Base.deliveries.last
222 mail = ActionMailer::Base.deliveries.last
223 assert_kind_of TMail::Mail, mail
223 assert_kind_of TMail::Mail, mail
224 assert_equal "[eCookbook] New file", mail.subject
224 assert_equal "[eCookbook] New file", mail.subject
225 assert mail.body.include?('testfile.txt')
225 assert mail.body.include?('testfile.txt')
226 end
226 end
227
227
228 def test_add_file_routing
228 def test_add_file_routing
229 assert_routing(
229 assert_routing(
230 {:method => :get, :path => '/projects/33/files/new'},
230 {:method => :get, :path => '/projects/33/files/new'},
231 :controller => 'projects', :action => 'add_file', :id => '33'
231 :controller => 'projects', :action => 'add_file', :id => '33'
232 )
232 )
233 assert_routing(
233 assert_routing(
234 {:method => :post, :path => '/projects/33/files/new'},
234 {:method => :post, :path => '/projects/33/files/new'},
235 :controller => 'projects', :action => 'add_file', :id => '33'
235 :controller => 'projects', :action => 'add_file', :id => '33'
236 )
236 )
237 end
237 end
238
238
239 def test_add_version_file
239 def test_add_version_file
240 set_tmp_attachments_directory
240 set_tmp_attachments_directory
241 @request.session[:user_id] = 2
241 @request.session[:user_id] = 2
242 Setting.notified_events = ['file_added']
242 Setting.notified_events = ['file_added']
243
243
244 assert_difference 'Attachment.count' do
244 assert_difference 'Attachment.count' do
245 post :add_file, :id => 1, :version_id => '2',
245 post :add_file, :id => 1, :version_id => '2',
246 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
246 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
247 end
247 end
248 assert_redirected_to 'projects/list_files/ecookbook'
248 assert_redirected_to 'projects/list_files/ecookbook'
249 a = Attachment.find(:first, :order => 'created_on DESC')
249 a = Attachment.find(:first, :order => 'created_on DESC')
250 assert_equal 'testfile.txt', a.filename
250 assert_equal 'testfile.txt', a.filename
251 assert_equal Version.find(2), a.container
251 assert_equal Version.find(2), a.container
252 end
252 end
253
253
254 def test_list_files
254 def test_list_files
255 get :list_files, :id => 1
255 get :list_files, :id => 1
256 assert_response :success
256 assert_response :success
257 assert_template 'list_files'
257 assert_template 'list_files'
258 assert_not_nil assigns(:containers)
258 assert_not_nil assigns(:containers)
259
259
260 # file attached to the project
260 # file attached to the project
261 assert_tag :a, :content => 'project_file.zip',
261 assert_tag :a, :content => 'project_file.zip',
262 :attributes => { :href => '/attachments/download/8/project_file.zip' }
262 :attributes => { :href => '/attachments/download/8/project_file.zip' }
263
263
264 # file attached to a project's version
264 # file attached to a project's version
265 assert_tag :a, :content => 'version_file.zip',
265 assert_tag :a, :content => 'version_file.zip',
266 :attributes => { :href => '/attachments/download/9/version_file.zip' }
266 :attributes => { :href => '/attachments/download/9/version_file.zip' }
267 end
267 end
268
268
269 def test_list_files_routing
269 def test_list_files_routing
270 assert_routing(
270 assert_routing(
271 {:method => :get, :path => '/projects/33/files'},
271 {:method => :get, :path => '/projects/33/files'},
272 :controller => 'projects', :action => 'list_files', :id => '33'
272 :controller => 'projects', :action => 'list_files', :id => '33'
273 )
273 )
274 end
274 end
275
275
276 def test_changelog_routing
276 def test_changelog_routing
277 assert_routing(
277 assert_routing(
278 {:method => :get, :path => '/projects/44/changelog'},
278 {:method => :get, :path => '/projects/44/changelog'},
279 :controller => 'projects', :action => 'changelog', :id => '44'
279 :controller => 'projects', :action => 'changelog', :id => '44'
280 )
280 )
281 end
281 end
282
282
283 def test_changelog
283 def test_changelog
284 get :changelog, :id => 1
284 get :changelog, :id => 1
285 assert_response :success
285 assert_response :success
286 assert_template 'changelog'
286 assert_template 'changelog'
287 assert_not_nil assigns(:versions)
287 assert_not_nil assigns(:versions)
288 end
288 end
289
289
290 def test_roadmap_routing
290 def test_roadmap_routing
291 assert_routing(
291 assert_routing(
292 {:method => :get, :path => 'projects/33/roadmap'},
292 {:method => :get, :path => 'projects/33/roadmap'},
293 :controller => 'projects', :action => 'roadmap', :id => '33'
293 :controller => 'projects', :action => 'roadmap', :id => '33'
294 )
294 )
295 end
295 end
296
296
297 def test_roadmap
297 def test_roadmap
298 get :roadmap, :id => 1
298 get :roadmap, :id => 1
299 assert_response :success
299 assert_response :success
300 assert_template 'roadmap'
300 assert_template 'roadmap'
301 assert_not_nil assigns(:versions)
301 assert_not_nil assigns(:versions)
302 # Version with no date set appears
302 # Version with no date set appears
303 assert assigns(:versions).include?(Version.find(3))
303 assert assigns(:versions).include?(Version.find(3))
304 # Completed version doesn't appear
304 # Completed version doesn't appear
305 assert !assigns(:versions).include?(Version.find(1))
305 assert !assigns(:versions).include?(Version.find(1))
306 end
306 end
307
307
308 def test_roadmap_with_completed_versions
308 def test_roadmap_with_completed_versions
309 get :roadmap, :id => 1, :completed => 1
309 get :roadmap, :id => 1, :completed => 1
310 assert_response :success
310 assert_response :success
311 assert_template 'roadmap'
311 assert_template 'roadmap'
312 assert_not_nil assigns(:versions)
312 assert_not_nil assigns(:versions)
313 # Version with no date set appears
313 # Version with no date set appears
314 assert assigns(:versions).include?(Version.find(3))
314 assert assigns(:versions).include?(Version.find(3))
315 # Completed version appears
315 # Completed version appears
316 assert assigns(:versions).include?(Version.find(1))
316 assert assigns(:versions).include?(Version.find(1))
317 end
317 end
318
318
319 def test_project_activity_routing
319 def test_project_activity_routing
320 assert_routing(
320 assert_routing(
321 {:method => :get, :path => '/projects/1/activity'},
321 {:method => :get, :path => '/projects/1/activity'},
322 :controller => 'projects', :action => 'activity', :id => '1'
322 :controller => 'projects', :action => 'activity', :id => '1'
323 )
323 )
324 end
324 end
325
325
326 def test_project_activity_atom_routing
326 def test_project_activity_atom_routing
327 assert_routing(
327 assert_routing(
328 {:method => :get, :path => '/projects/1/activity.atom'},
328 {:method => :get, :path => '/projects/1/activity.atom'},
329 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
329 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
330 )
330 )
331 end
331 end
332
332
333 def test_project_activity
333 def test_project_activity
334 get :activity, :id => 1, :with_subprojects => 0
334 get :activity, :id => 1, :with_subprojects => 0
335 assert_response :success
335 assert_response :success
336 assert_template 'activity'
336 assert_template 'activity'
337 assert_not_nil assigns(:events_by_day)
337 assert_not_nil assigns(:events_by_day)
338
338
339 assert_tag :tag => "h3",
339 assert_tag :tag => "h3",
340 :content => /#{2.days.ago.to_date.day}/,
340 :content => /#{2.days.ago.to_date.day}/,
341 :sibling => { :tag => "dl",
341 :sibling => { :tag => "dl",
342 :child => { :tag => "dt",
342 :child => { :tag => "dt",
343 :attributes => { :class => /issue-edit/ },
343 :attributes => { :class => /issue-edit/ },
344 :child => { :tag => "a",
344 :child => { :tag => "a",
345 :content => /(#{IssueStatus.find(2).name})/,
345 :content => /(#{IssueStatus.find(2).name})/,
346 }
346 }
347 }
347 }
348 }
348 }
349 end
349 end
350
350
351 def test_previous_project_activity
351 def test_previous_project_activity
352 get :activity, :id => 1, :from => 3.days.ago.to_date
352 get :activity, :id => 1, :from => 3.days.ago.to_date
353 assert_response :success
353 assert_response :success
354 assert_template 'activity'
354 assert_template 'activity'
355 assert_not_nil assigns(:events_by_day)
355 assert_not_nil assigns(:events_by_day)
356
356
357 assert_tag :tag => "h3",
357 assert_tag :tag => "h3",
358 :content => /#{3.day.ago.to_date.day}/,
358 :content => /#{3.day.ago.to_date.day}/,
359 :sibling => { :tag => "dl",
359 :sibling => { :tag => "dl",
360 :child => { :tag => "dt",
360 :child => { :tag => "dt",
361 :attributes => { :class => /issue/ },
361 :attributes => { :class => /issue/ },
362 :child => { :tag => "a",
362 :child => { :tag => "a",
363 :content => /#{Issue.find(1).subject}/,
363 :content => /#{Issue.find(1).subject}/,
364 }
364 }
365 }
365 }
366 }
366 }
367 end
367 end
368
368
369 def test_global_activity_routing
369 def test_global_activity_routing
370 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity')
370 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity')
371 end
371 end
372
372
373 def test_global_activity
373 def test_global_activity
374 get :activity
374 get :activity
375 assert_response :success
375 assert_response :success
376 assert_template 'activity'
376 assert_template 'activity'
377 assert_not_nil assigns(:events_by_day)
377 assert_not_nil assigns(:events_by_day)
378
378
379 assert_tag :tag => "h3",
379 assert_tag :tag => "h3",
380 :content => /#{5.day.ago.to_date.day}/,
380 :content => /#{5.day.ago.to_date.day}/,
381 :sibling => { :tag => "dl",
381 :sibling => { :tag => "dl",
382 :child => { :tag => "dt",
382 :child => { :tag => "dt",
383 :attributes => { :class => /issue/ },
383 :attributes => { :class => /issue/ },
384 :child => { :tag => "a",
384 :child => { :tag => "a",
385 :content => /#{Issue.find(5).subject}/,
385 :content => /#{Issue.find(5).subject}/,
386 }
386 }
387 }
387 }
388 }
388 }
389 end
389 end
390
390
391 def test_user_activity
391 def test_user_activity
392 get :activity, :user_id => 2
392 get :activity, :user_id => 2
393 assert_response :success
393 assert_response :success
394 assert_template 'activity'
394 assert_template 'activity'
395 assert_not_nil assigns(:events_by_day)
395 assert_not_nil assigns(:events_by_day)
396
396
397 assert_tag :tag => "h3",
397 assert_tag :tag => "h3",
398 :content => /#{3.day.ago.to_date.day}/,
398 :content => /#{3.day.ago.to_date.day}/,
399 :sibling => { :tag => "dl",
399 :sibling => { :tag => "dl",
400 :child => { :tag => "dt",
400 :child => { :tag => "dt",
401 :attributes => { :class => /issue/ },
401 :attributes => { :class => /issue/ },
402 :child => { :tag => "a",
402 :child => { :tag => "a",
403 :content => /#{Issue.find(1).subject}/,
403 :content => /#{Issue.find(1).subject}/,
404 }
404 }
405 }
405 }
406 }
406 }
407 end
407 end
408
408
409 def test_global_activity_atom_routing
409 def test_global_activity_atom_routing
410 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :format => 'atom')
410 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :format => 'atom')
411 end
411 end
412
412
413 def test_activity_atom_feed
413 def test_activity_atom_feed
414 get :activity, :format => 'atom'
414 get :activity, :format => 'atom'
415 assert_response :success
415 assert_response :success
416 assert_template 'common/feed.atom.rxml'
416 assert_template 'common/feed.atom.rxml'
417 end
417 end
418
418
419 def test_archive_routing
419 def test_archive_routing
420 assert_routing(
420 assert_routing(
421 #TODO: use PUT to project path and modify form
421 #TODO: use PUT to project path and modify form
422 {:method => :post, :path => 'projects/64/archive'},
422 {:method => :post, :path => 'projects/64/archive'},
423 :controller => 'projects', :action => 'archive', :id => '64'
423 :controller => 'projects', :action => 'archive', :id => '64'
424 )
424 )
425 end
425 end
426
426
427 def test_archive
427 def test_archive
428 @request.session[:user_id] = 1 # admin
428 @request.session[:user_id] = 1 # admin
429 post :archive, :id => 1
429 post :archive, :id => 1
430 assert_redirected_to 'admin/projects'
430 assert_redirected_to 'admin/projects'
431 assert !Project.find(1).active?
431 assert !Project.find(1).active?
432 end
432 end
433
433
434 def test_unarchive_routing
434 def test_unarchive_routing
435 assert_routing(
435 assert_routing(
436 #TODO: use PUT to project path and modify form
436 #TODO: use PUT to project path and modify form
437 {:method => :post, :path => '/projects/567/unarchive'},
437 {:method => :post, :path => '/projects/567/unarchive'},
438 :controller => 'projects', :action => 'unarchive', :id => '567'
438 :controller => 'projects', :action => 'unarchive', :id => '567'
439 )
439 )
440 end
440 end
441
441
442 def test_unarchive
442 def test_unarchive
443 @request.session[:user_id] = 1 # admin
443 @request.session[:user_id] = 1 # admin
444 Project.find(1).archive
444 Project.find(1).archive
445 post :unarchive, :id => 1
445 post :unarchive, :id => 1
446 assert_redirected_to 'admin/projects'
446 assert_redirected_to 'admin/projects'
447 assert Project.find(1).active?
447 assert Project.find(1).active?
448 end
448 end
449
449
450 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
451 CustomField.delete_all
452 parent = nil
453 6.times do |i|
454 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
455 p.set_parent!(parent)
456
457 get :show, :id => p
458 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
459 :children => { :count => [i, 3].min,
460 :only => { :tag => 'a' } }
461
462 parent = p
463 end
464 end
465
450 def test_jump_should_redirect_to_active_tab
466 def test_jump_should_redirect_to_active_tab
451 get :show, :id => 1, :jump => 'issues'
467 get :show, :id => 1, :jump => 'issues'
452 assert_redirected_to 'projects/ecookbook/issues'
468 assert_redirected_to 'projects/ecookbook/issues'
453 end
469 end
454
470
455 def test_jump_should_not_redirect_to_inactive_tab
471 def test_jump_should_not_redirect_to_inactive_tab
456 get :show, :id => 3, :jump => 'documents'
472 get :show, :id => 3, :jump => 'documents'
457 assert_response :success
473 assert_response :success
458 assert_template 'show'
474 assert_template 'show'
459 end
475 end
460
476
461 def test_jump_should_not_redirect_to_unknown_tab
477 def test_jump_should_not_redirect_to_unknown_tab
462 get :show, :id => 3, :jump => 'foobar'
478 get :show, :id => 3, :jump => 'foobar'
463 assert_response :success
479 assert_response :success
464 assert_template 'show'
480 assert_template 'show'
465 end
481 end
466
482
467 def test_project_menu
483 def test_project_menu
468 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
484 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
469 Redmine::MenuManager.map :project_menu do |menu|
485 Redmine::MenuManager.map :project_menu do |menu|
470 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
486 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
471 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
487 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
472 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
488 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
473 end
489 end
474
490
475 get :show, :id => 1
491 get :show, :id => 1
476 assert_tag :div, :attributes => { :id => 'main-menu' },
492 assert_tag :div, :attributes => { :id => 'main-menu' },
477 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
493 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
478 :attributes => { :class => 'foo' } } }
494 :attributes => { :class => 'foo' } } }
479
495
480 assert_tag :div, :attributes => { :id => 'main-menu' },
496 assert_tag :div, :attributes => { :id => 'main-menu' },
481 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
497 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
482 :attributes => { :class => 'bar' } },
498 :attributes => { :class => 'bar' } },
483 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
499 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
484
500
485 assert_tag :div, :attributes => { :id => 'main-menu' },
501 assert_tag :div, :attributes => { :id => 'main-menu' },
486 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
502 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
487 :attributes => { :class => 'hello' } },
503 :attributes => { :class => 'hello' } },
488 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
504 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
489
505
490 # Remove the menu items
506 # Remove the menu items
491 Redmine::MenuManager.map :project_menu do |menu|
507 Redmine::MenuManager.map :project_menu do |menu|
492 menu.delete :foo
508 menu.delete :foo
493 menu.delete :bar
509 menu.delete :bar
494 menu.delete :hello
510 menu.delete :hello
495 end
511 end
496 end
512 end
497 end
513 end
498
514
499 # A hook that is manually registered later
515 # A hook that is manually registered later
500 class ProjectBasedTemplate < Redmine::Hook::ViewListener
516 class ProjectBasedTemplate < Redmine::Hook::ViewListener
501 def view_layouts_base_html_head(context)
517 def view_layouts_base_html_head(context)
502 # Adds a project stylesheet
518 # Adds a project stylesheet
503 stylesheet_link_tag(context[:project].identifier) if context[:project]
519 stylesheet_link_tag(context[:project].identifier) if context[:project]
504 end
520 end
505 end
521 end
506 # Don't use this hook now
522 # Don't use this hook now
507 Redmine::Hook.clear_listeners
523 Redmine::Hook.clear_listeners
508
524
509 def test_hook_response
525 def test_hook_response
510 Redmine::Hook.add_listener(ProjectBasedTemplate)
526 Redmine::Hook.add_listener(ProjectBasedTemplate)
511 get :show, :id => 1
527 get :show, :id => 1
512 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
528 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
513 :parent => {:tag => 'head'}
529 :parent => {:tag => 'head'}
514
530
515 Redmine::Hook.clear_listeners
531 Redmine::Hook.clear_listeners
516 end
532 end
517 end
533 end
General Comments 0
You need to be logged in to leave comments. Login now