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