##// END OF EJS Templates
Added a 'New issue' link in the main menu (accesskey 7)....
Jean-Philippe Lang -
r1067:16e9ffce0d0e
parent child
Show More
@@ -0,0 +1,31
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module AccessKeys
20 ACCESSKEYS = {:edit => 'e',
21 :preview => 'r',
22 :quick_search => 'f',
23 :search => '4',
24 :new_issue => '7'
25 }.freeze unless const_defined?(:ACCESSKEYS)
26
27 def self.key_for(action)
28 ACCESSKEYS[action]
29 end
30 end
31 end
@@ -1,295 +1,297
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 IssuesController < ApplicationController
19 19 layout 'base'
20 menu_item :new_issue, :only => :new
21
20 22 before_filter :find_issue, :except => [:index, :changes, :preview, :new, :update_form]
21 23 before_filter :find_project, :only => [:new, :update_form]
22 24 before_filter :authorize, :except => [:index, :changes, :preview, :update_form]
23 25 before_filter :find_optional_project, :only => [:index, :changes]
24 26 accept_key_auth :index, :changes
25 27
26 28 cache_sweeper :issue_sweeper, :only => [ :new, :edit, :update, :destroy ]
27 29
28 30 helper :projects
29 31 include ProjectsHelper
30 32 helper :custom_fields
31 33 include CustomFieldsHelper
32 34 helper :ifpdf
33 35 include IfpdfHelper
34 36 helper :issue_relations
35 37 include IssueRelationsHelper
36 38 helper :watchers
37 39 include WatchersHelper
38 40 helper :attachments
39 41 include AttachmentsHelper
40 42 helper :queries
41 43 helper :sort
42 44 include SortHelper
43 45 include IssuesHelper
44 46
45 47 def index
46 48 sort_init "#{Issue.table_name}.id", "desc"
47 49 sort_update
48 50 retrieve_query
49 51 if @query.valid?
50 52 limit = %w(pdf csv).include?(params[:format]) ? Setting.issues_export_limit.to_i : per_page_option
51 53 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
52 54 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
53 55 @issues = Issue.find :all, :order => sort_clause,
54 56 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
55 57 :conditions => @query.statement,
56 58 :limit => limit,
57 59 :offset => @issue_pages.current.offset
58 60 respond_to do |format|
59 61 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
60 62 format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
61 63 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
62 64 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
63 65 end
64 66 else
65 67 # Send html if the query is not valid
66 68 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
67 69 end
68 70 end
69 71
70 72 def changes
71 73 sort_init "#{Issue.table_name}.id", "desc"
72 74 sort_update
73 75 retrieve_query
74 76 if @query.valid?
75 77 @changes = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
76 78 :conditions => @query.statement,
77 79 :limit => 25,
78 80 :order => "#{Journal.table_name}.created_on DESC"
79 81 end
80 82 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
81 83 render :layout => false, :content_type => 'application/atom+xml'
82 84 end
83 85
84 86 def show
85 87 @custom_values = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
86 88 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
87 89 @status_options = @issue.new_statuses_allowed_to(User.current)
88 90 @activities = Enumeration::get_values('ACTI')
89 91 respond_to do |format|
90 92 format.html { render :template => 'issues/show.rhtml' }
91 93 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
92 94 end
93 95 end
94 96
95 97 # Add a new issue
96 98 # The new issue will be created from an existing one if copy_from parameter is given
97 99 def new
98 100 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
99 101 @issue.project = @project
100 102 @issue.author = User.current
101 103 @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
102 104 if @issue.tracker.nil?
103 105 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
104 106 render :nothing => true, :layout => true
105 107 return
106 108 end
107 109
108 110 default_status = IssueStatus.default
109 111 unless default_status
110 112 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
111 113 render :nothing => true, :layout => true
112 114 return
113 115 end
114 116 @issue.status = default_status
115 117 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker))
116 118
117 119 if request.get? || request.xhr?
118 120 @issue.start_date ||= Date.today
119 121 @custom_values = @issue.custom_values.empty? ?
120 122 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
121 123 @issue.custom_values
122 124 else
123 125 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
124 126 # Check that the user is allowed to apply the requested status
125 127 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
126 128 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
127 129 @issue.custom_values = @custom_values
128 130 if @issue.save
129 131 attach_files(@issue, params[:attachments])
130 132 flash[:notice] = l(:notice_successful_create)
131 133 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
132 134 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
133 135 return
134 136 end
135 137 end
136 138 @priorities = Enumeration::get_values('IPRI')
137 139 render :layout => !request.xhr?
138 140 end
139 141
140 142 def edit
141 143 @priorities = Enumeration::get_values('IPRI')
142 144 @custom_values = []
143 145 if request.get?
144 146 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
145 147 else
146 148 begin
147 149 journal = @issue.init_journal(User.current)
148 150 # Retrieve custom fields and values
149 151 if params["custom_fields"]
150 152 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
151 153 @issue.custom_values = @custom_values
152 154 end
153 155 @issue.attributes = params[:issue]
154 156 if @issue.save
155 157 flash[:notice] = l(:notice_successful_update)
156 158 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
157 159 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
158 160 end
159 161 rescue ActiveRecord::StaleObjectError
160 162 # Optimistic locking exception
161 163 flash[:error] = l(:notice_locking_conflict)
162 164 end
163 165 end
164 166 end
165 167
166 168 # Attributes that can be updated on workflow transition
167 169 # TODO: make it configurable (at least per role)
168 170 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
169 171
170 172 def update
171 173 @status_options = @issue.new_statuses_allowed_to(User.current)
172 174 @activities = Enumeration::get_values('ACTI')
173 175 journal = @issue.init_journal(User.current, params[:notes])
174 176 # User can change issue attributes only if a workflow transition is allowed
175 177 if !@status_options.empty? && params[:issue]
176 178 attrs = params[:issue].dup
177 179 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) }
178 180 attrs.delete(:status_id) unless @status_options.detect {|s| s.id.to_s == attrs[:status_id].to_s}
179 181 @issue.attributes = attrs
180 182 end
181 183 if request.post?
182 184 attachments = attach_files(@issue, params[:attachments])
183 185 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
184 186 if @issue.save
185 187 # Log spend time
186 188 if current_role.allowed_to?(:log_time)
187 189 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
188 190 @time_entry.attributes = params[:time_entry]
189 191 @time_entry.save
190 192 end
191 193 if !journal.new_record?
192 194 # Only send notification if something was actually changed
193 195 flash[:notice] = l(:notice_successful_update)
194 196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
195 197 end
196 198 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
197 199 end
198 200 end
199 201 rescue ActiveRecord::StaleObjectError
200 202 # Optimistic locking exception
201 203 flash.now[:error] = l(:notice_locking_conflict)
202 204 end
203 205
204 206 def destroy
205 207 @issue.destroy
206 208 redirect_to :action => 'index', :project_id => @project
207 209 end
208 210
209 211 def destroy_attachment
210 212 a = @issue.attachments.find(params[:attachment_id])
211 213 a.destroy
212 214 journal = @issue.init_journal(User.current)
213 215 journal.details << JournalDetail.new(:property => 'attachment',
214 216 :prop_key => a.id,
215 217 :old_value => a.filename)
216 218 journal.save
217 219 redirect_to :action => 'show', :id => @issue
218 220 end
219 221
220 222 def context_menu
221 223 @priorities = Enumeration.get_values('IPRI').reverse
222 224 @statuses = IssueStatus.find(:all, :order => 'position')
223 225 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
224 226 @assignables = @issue.assignable_users
225 227 @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
226 228 @can = {:edit => User.current.allowed_to?(:edit_issues, @project),
227 229 :assign => (@allowed_statuses.any? || User.current.allowed_to?(:edit_issues, @project)),
228 230 :add => User.current.allowed_to?(:add_issues, @project),
229 231 :move => User.current.allowed_to?(:move_issues, @project),
230 232 :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
231 233 :delete => User.current.allowed_to?(:delete_issues, @project)}
232 234 render :layout => false
233 235 end
234 236
235 237 def update_form
236 238 @issue = Issue.new(params[:issue])
237 239 render :action => :new, :layout => false
238 240 end
239 241
240 242 def preview
241 243 issue = Issue.find_by_id(params[:id])
242 244 @attachements = issue.attachments if issue
243 245 @text = params[:issue][:description]
244 246 render :partial => 'common/preview'
245 247 end
246 248
247 249 private
248 250 def find_issue
249 251 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
250 252 @project = @issue.project
251 253 rescue ActiveRecord::RecordNotFound
252 254 render_404
253 255 end
254 256
255 257 def find_project
256 258 @project = Project.find(params[:project_id])
257 259 rescue ActiveRecord::RecordNotFound
258 260 render_404
259 261 end
260 262
261 263 def find_optional_project
262 264 return true unless params[:project_id]
263 265 @project = Project.find(params[:project_id])
264 266 authorize
265 267 rescue ActiveRecord::RecordNotFound
266 268 render_404
267 269 end
268 270
269 271 # Retrieve query from session or build a new query
270 272 def retrieve_query
271 273 if params[:query_id]
272 274 @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)})
273 275 session[:query] = {:id => @query.id, :project_id => @query.project_id}
274 276 else
275 277 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
276 278 # Give it a name, required to be valid
277 279 @query = Query.new(:name => "_")
278 280 @query.project = @project
279 281 if params[:fields] and params[:fields].is_a? Array
280 282 params[:fields].each do |field|
281 283 @query.add_filter(field, params[:operators][field], params[:values][field])
282 284 end
283 285 else
284 286 @query.available_filters.keys.each do |field|
285 287 @query.add_short_filter(field, params[field]) if params[field]
286 288 end
287 289 end
288 290 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
289 291 else
290 292 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
291 293 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
292 294 end
293 295 end
294 296 end
295 297 end
@@ -1,450 +1,444
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 module ApplicationHelper
19 19 include Redmine::WikiFormatting::Macros::Definitions
20 20
21 21 def current_role
22 22 @current_role ||= User.current.role_for_project(@project)
23 23 end
24 24
25 25 # Return true if user is authorized for controller/action, otherwise false
26 26 def authorize_for(controller, action)
27 27 User.current.allowed_to?({:controller => controller, :action => action}, @project)
28 28 end
29 29
30 30 # Display a link if user is authorized
31 31 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
32 32 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
33 33 end
34 34
35 35 def link_to_signin
36 36 link_to l(:label_login), { :controller => 'account', :action => 'login' }, :class => 'signin'
37 37 end
38 38
39 39 def link_to_signout
40 40 link_to l(:label_logout), { :controller => 'account', :action => 'logout' }, :class => 'logout'
41 41 end
42 42
43 43 # Display a link to user's account page
44 44 def link_to_user(user)
45 45 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
46 46 end
47 47
48 48 def link_to_issue(issue)
49 49 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
50 50 end
51 51
52 52 def toggle_link(name, id, options={})
53 53 onclick = "Element.toggle('#{id}'); "
54 54 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
55 55 onclick << "return false;"
56 56 link_to(name, "#", :onclick => onclick)
57 57 end
58 58
59 59 def show_and_goto_link(name, id, options={})
60 60 onclick = "Element.show('#{id}'); "
61 61 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
62 62 onclick << "location.href='##{id}-anchor'; "
63 63 onclick << "return false;"
64 64 link_to(name, "#", options.merge(:onclick => onclick))
65 65 end
66 66
67 67 def image_to_function(name, function, html_options = {})
68 68 html_options.symbolize_keys!
69 69 tag(:input, html_options.merge({
70 70 :type => "image", :src => image_path(name),
71 71 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
72 72 }))
73 73 end
74 74
75 75 def prompt_to_remote(name, text, param, url, html_options = {})
76 76 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
77 77 link_to name, {}, html_options
78 78 end
79 79
80 80 def format_date(date)
81 81 return nil unless date
82 82 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
83 83 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
84 84 date.strftime(@date_format)
85 85 end
86 86
87 87 def format_time(time, include_date = true)
88 88 return nil unless time
89 89 time = time.to_time if time.is_a?(String)
90 90 zone = User.current.time_zone
91 91 if time.utc?
92 92 local = zone ? zone.adjust(time) : time.getlocal
93 93 else
94 94 local = zone ? zone.adjust(time.getutc) : time
95 95 end
96 96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
97 97 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
98 98 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
99 99 end
100 100
101 101 def authoring(created, author)
102 102 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
103 103 l(:label_added_time_by, author || 'Anonymous', time_tag)
104 104 end
105 105
106 106 def day_name(day)
107 107 l(:general_day_names).split(',')[day-1]
108 108 end
109 109
110 110 def month_name(month)
111 111 l(:actionview_datehelper_select_month_names).split(',')[month-1]
112 112 end
113 113
114 114 def pagination_links_full(paginator, count=nil, options={})
115 115 page_param = options.delete(:page_param) || :page
116 116 url_param = params.dup
117 117 # don't reuse params if filters are present
118 118 url_param.clear if url_param.has_key?(:set_filter)
119 119
120 120 html = ''
121 121 html << link_to_remote(('&#171; ' + l(:label_previous)),
122 122 {:update => "content", :url => url_param.merge(page_param => paginator.current.previous)},
123 123 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
124 124
125 125 html << (pagination_links_each(paginator, options) do |n|
126 126 link_to_remote(n.to_s,
127 127 {:url => {:params => url_param.merge(page_param => n)}, :update => 'content'},
128 128 {:href => url_for(:params => url_param.merge(page_param => n))})
129 129 end || '')
130 130
131 131 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
132 132 {:update => "content", :url => url_param.merge(page_param => paginator.current.next)},
133 133 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
134 134
135 135 unless count.nil?
136 136 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
137 137 end
138 138
139 139 html
140 140 end
141 141
142 142 def per_page_links(selected=nil)
143 143 url_param = params.dup
144 144 url_param.clear if url_param.has_key?(:set_filter)
145 145
146 146 links = Setting.per_page_options_array.collect do |n|
147 147 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
148 148 {:href => url_for(url_param.merge(:per_page => n))})
149 149 end
150 150 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
151 151 end
152 152
153 153 def html_title(*args)
154 154 if args.empty?
155 155 title = []
156 156 title << @project.name if @project
157 157 title += @html_title if @html_title
158 158 title << Setting.app_title
159 159 title.compact.join(' - ')
160 160 else
161 161 @html_title ||= []
162 162 @html_title += args
163 163 end
164 164 end
165
166 ACCESSKEYS = {:edit => 'e',
167 :preview => 'r',
168 :quick_search => 'f',
169 :search => '4',
170 }.freeze unless const_defined?(:ACCESSKEYS)
171 165
172 166 def accesskey(s)
173 ACCESSKEYS[s]
167 Redmine::AccessKeys.key_for s
174 168 end
175 169
176 170 # Formats text according to system settings.
177 171 # 2 ways to call this method:
178 172 # * with a String: textilizable(text, options)
179 173 # * with an object and one of its attribute: textilizable(issue, :description, options)
180 174 def textilizable(*args)
181 175 options = args.last.is_a?(Hash) ? args.pop : {}
182 176 case args.size
183 177 when 1
184 178 obj = nil
185 179 text = args.shift || ''
186 180 when 2
187 181 obj = args.shift
188 182 text = obj.send(args.shift)
189 183 else
190 184 raise ArgumentError, 'invalid arguments to textilizable'
191 185 end
192 186
193 187 # when using an image link, try to use an attachment, if possible
194 188 attachments = options[:attachments]
195 189 if attachments
196 190 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
197 191 style = $1
198 192 filename = $6
199 193 rf = Regexp.new(filename, Regexp::IGNORECASE)
200 194 # search for the picture in attachments
201 195 if found = attachments.detect { |att| att.filename =~ rf }
202 196 image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
203 197 "!#{style}#{image_url}!"
204 198 else
205 199 "!#{style}#{filename}!"
206 200 end
207 201 end
208 202 end
209 203
210 204 text = (Setting.text_formatting == 'textile') ?
211 205 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
212 206 simple_format(auto_link(h(text)))
213 207
214 208 # different methods for formatting wiki links
215 209 case options[:wiki_links]
216 210 when :local
217 211 # used for local links to html files
218 212 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
219 213 when :anchor
220 214 # used for single-file wiki export
221 215 format_wiki_link = Proc.new {|project, title| "##{title}" }
222 216 else
223 217 format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
224 218 end
225 219
226 220 project = options[:project] || @project
227 221
228 222 # Wiki links
229 223 #
230 224 # Examples:
231 225 # [[mypage]]
232 226 # [[mypage|mytext]]
233 227 # wiki links can refer other project wikis, using project name or identifier:
234 228 # [[project:]] -> wiki starting page
235 229 # [[project:|mytext]]
236 230 # [[project:mypage]]
237 231 # [[project:mypage|mytext]]
238 232 text = text.gsub(/(!)?(\[\[([^\]\|]+)(\|([^\]\|]+))?\]\])/) do |m|
239 233 link_project = project
240 234 esc, all, page, title = $1, $2, $3, $5
241 235 if esc.nil?
242 236 if page =~ /^([^\:]+)\:(.*)$/
243 237 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
244 238 page = $2
245 239 title ||= $1 if page.blank?
246 240 end
247 241
248 242 if link_project && link_project.wiki
249 243 # check if page exists
250 244 wiki_page = link_project.wiki.find_page(page)
251 245 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
252 246 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
253 247 else
254 248 # project or wiki doesn't exist
255 249 title || page
256 250 end
257 251 else
258 252 all
259 253 end
260 254 end
261 255
262 256 # Redmine links
263 257 #
264 258 # Examples:
265 259 # Issues:
266 260 # #52 -> Link to issue #52
267 261 # Changesets:
268 262 # r52 -> Link to revision 52
269 263 # Documents:
270 264 # document#17 -> Link to document with id 17
271 265 # document:Greetings -> Link to the document with title "Greetings"
272 266 # document:"Some document" -> Link to the document with title "Some document"
273 267 # Versions:
274 268 # version#3 -> Link to version with id 3
275 269 # version:1.0.0 -> Link to version named "1.0.0"
276 270 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
277 271 # Attachments:
278 272 # attachment:file.zip -> Link to the attachment of the current object named file.zip
279 273 text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
280 274 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
281 275 link = nil
282 276 if esc.nil?
283 277 if prefix.nil? && sep == 'r'
284 278 if project && (changeset = project.changesets.find_by_revision(oid))
285 279 link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset',
286 280 :title => truncate(changeset.comments, 100))
287 281 end
288 282 elsif sep == '#'
289 283 oid = oid.to_i
290 284 case prefix
291 285 when nil
292 286 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
293 287 link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue',
294 288 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
295 289 link = content_tag('del', link) if issue.closed?
296 290 end
297 291 when 'document'
298 292 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
299 293 link = link_to h(document.title), {:controller => 'documents', :action => 'show', :id => document}, :class => 'document'
300 294 end
301 295 when 'version'
302 296 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
303 297 link = link_to h(version.name), {:controller => 'versions', :action => 'show', :id => version}, :class => 'version'
304 298 end
305 299 end
306 300 elsif sep == ':'
307 301 # removes the double quotes if any
308 302 name = oid.gsub(%r{^"(.*)"$}, "\\1")
309 303 case prefix
310 304 when 'document'
311 305 if project && document = project.documents.find_by_title(name)
312 306 link = link_to h(document.title), {:controller => 'documents', :action => 'show', :id => document}, :class => 'document'
313 307 end
314 308 when 'version'
315 309 if project && version = project.versions.find_by_name(name)
316 310 link = link_to h(version.name), {:controller => 'versions', :action => 'show', :id => version}, :class => 'version'
317 311 end
318 312 when 'attachment'
319 313 if attachments && attachment = attachments.detect {|a| a.filename == name }
320 314 link = link_to h(attachment.filename), {:controller => 'attachments', :action => 'download', :id => attachment}, :class => 'attachment'
321 315 end
322 316 end
323 317 end
324 318 end
325 319 leading + (link || "#{prefix}#{sep}#{oid}")
326 320 end
327 321
328 322 text
329 323 end
330 324
331 325 # Same as Rails' simple_format helper without using paragraphs
332 326 def simple_format_without_paragraph(text)
333 327 text.to_s.
334 328 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
335 329 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
336 330 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
337 331 end
338 332
339 333 def error_messages_for(object_name, options = {})
340 334 options = options.symbolize_keys
341 335 object = instance_variable_get("@#{object_name}")
342 336 if object && !object.errors.empty?
343 337 # build full_messages here with controller current language
344 338 full_messages = []
345 339 object.errors.each do |attr, msg|
346 340 next if msg.nil?
347 341 msg = msg.first if msg.is_a? Array
348 342 if attr == "base"
349 343 full_messages << l(msg)
350 344 else
351 345 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
352 346 end
353 347 end
354 348 # retrieve custom values error messages
355 349 if object.errors[:custom_values]
356 350 object.custom_values.each do |v|
357 351 v.errors.each do |attr, msg|
358 352 next if msg.nil?
359 353 msg = msg.first if msg.is_a? Array
360 354 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
361 355 end
362 356 end
363 357 end
364 358 content_tag("div",
365 359 content_tag(
366 360 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
367 361 ) +
368 362 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
369 363 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
370 364 )
371 365 else
372 366 ""
373 367 end
374 368 end
375 369
376 370 def lang_options_for_select(blank=true)
377 371 (blank ? [["(auto)", ""]] : []) +
378 372 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
379 373 end
380 374
381 375 def label_tag_for(name, option_tags = nil, options = {})
382 376 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
383 377 content_tag("label", label_text)
384 378 end
385 379
386 380 def labelled_tabular_form_for(name, object, options, &proc)
387 381 options[:html] ||= {}
388 382 options[:html].store :class, "tabular"
389 383 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
390 384 end
391 385
392 386 def check_all_links(form_name)
393 387 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
394 388 " | " +
395 389 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
396 390 end
397 391
398 392 def progress_bar(pcts, options={})
399 393 pcts = [pcts, pcts] unless pcts.is_a?(Array)
400 394 pcts[1] = pcts[1] - pcts[0]
401 395 pcts << (100 - pcts[1] - pcts[0])
402 396 width = options[:width] || '100px;'
403 397 legend = options[:legend] || ''
404 398 content_tag('table',
405 399 content_tag('tr',
406 400 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
407 401 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
408 402 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
409 403 ), :class => 'progress', :style => "width: #{width};") +
410 404 content_tag('p', legend, :class => 'pourcent')
411 405 end
412 406
413 407 def context_menu_link(name, url, options={})
414 408 options[:class] ||= ''
415 409 if options.delete(:selected)
416 410 options[:class] << ' icon-checked disabled'
417 411 options[:disabled] = true
418 412 end
419 413 if options.delete(:disabled)
420 414 options.delete(:method)
421 415 options.delete(:confirm)
422 416 options.delete(:onclick)
423 417 options[:class] << ' disabled'
424 418 url = '#'
425 419 end
426 420 link_to name, url, options
427 421 end
428 422
429 423 def calendar_for(field_id)
430 424 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
431 425 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
432 426 end
433 427
434 428 def wikitoolbar_for(field_id)
435 429 return '' unless Setting.text_formatting == 'textile'
436 430 javascript_include_tag('jstoolbar/jstoolbar') +
437 431 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
438 432 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
439 433 end
440 434
441 435 def content_for(name, content = nil, &block)
442 436 @has_content ||= {}
443 437 @has_content[name] = true
444 438 super(name, content, &block)
445 439 end
446 440
447 441 def has_content?(name)
448 442 (@has_content && @has_content[name]) || false
449 443 end
450 444 end
@@ -1,199 +1,191
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module ProjectsHelper
19 19 def link_to_version(version, options = {})
20 20 return '' unless version && version.is_a?(Version)
21 21 link_to version.name, {:controller => 'projects',
22 22 :action => 'roadmap',
23 23 :id => version.project_id,
24 24 :completed => (version.completed? ? 1 : nil),
25 25 :anchor => version.name
26 26 }, options
27 27 end
28 28
29 29 def project_settings_tabs
30 30 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
31 31 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
32 32 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
33 33 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
34 34 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
35 35 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
36 36 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
37 37 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
38 38 ]
39 39 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
40 40 end
41 41
42 42 # Generates a gantt image
43 43 # Only defined if RMagick is avalaible
44 44 def gantt_image(events, date_from, months, zoom)
45 45 date_to = (date_from >> months)-1
46 46 show_weeks = zoom > 1
47 47 show_days = zoom > 2
48 48
49 49 subject_width = 320
50 50 header_heigth = 18
51 51 # width of one day in pixels
52 52 zoom = zoom*2
53 53 g_width = (date_to - date_from + 1)*zoom
54 54 g_height = 20 * events.length + 20
55 55 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
56 56 height = g_height + headers_heigth
57 57
58 58 imgl = Magick::ImageList.new
59 59 imgl.new_image(subject_width+g_width+1, height)
60 60 gc = Magick::Draw.new
61 61
62 62 # Subjects
63 63 top = headers_heigth + 20
64 64 gc.fill('black')
65 65 gc.stroke('transparent')
66 66 gc.stroke_width(1)
67 67 events.each do |i|
68 68 gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
69 69 top = top + 20
70 70 end
71 71
72 72 # Months headers
73 73 month_f = date_from
74 74 left = subject_width
75 75 months.times do
76 76 width = ((month_f >> 1) - month_f) * zoom
77 77 gc.fill('white')
78 78 gc.stroke('grey')
79 79 gc.stroke_width(1)
80 80 gc.rectangle(left, 0, left + width, height)
81 81 gc.fill('black')
82 82 gc.stroke('transparent')
83 83 gc.stroke_width(1)
84 84 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
85 85 left = left + width
86 86 month_f = month_f >> 1
87 87 end
88 88
89 89 # Weeks headers
90 90 if show_weeks
91 91 left = subject_width
92 92 height = header_heigth
93 93 if date_from.cwday == 1
94 94 # date_from is monday
95 95 week_f = date_from
96 96 else
97 97 # find next monday after date_from
98 98 week_f = date_from + (7 - date_from.cwday + 1)
99 99 width = (7 - date_from.cwday + 1) * zoom
100 100 gc.fill('white')
101 101 gc.stroke('grey')
102 102 gc.stroke_width(1)
103 103 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
104 104 left = left + width
105 105 end
106 106 while week_f <= date_to
107 107 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
108 108 gc.fill('white')
109 109 gc.stroke('grey')
110 110 gc.stroke_width(1)
111 111 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
112 112 gc.fill('black')
113 113 gc.stroke('transparent')
114 114 gc.stroke_width(1)
115 115 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
116 116 left = left + width
117 117 week_f = week_f+7
118 118 end
119 119 end
120 120
121 121 # Days details (week-end in grey)
122 122 if show_days
123 123 left = subject_width
124 124 height = g_height + header_heigth - 1
125 125 wday = date_from.cwday
126 126 (date_to - date_from + 1).to_i.times do
127 127 width = zoom
128 128 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
129 129 gc.stroke('grey')
130 130 gc.stroke_width(1)
131 131 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
132 132 left = left + width
133 133 wday = wday + 1
134 134 wday = 1 if wday > 7
135 135 end
136 136 end
137 137
138 138 # border
139 139 gc.fill('transparent')
140 140 gc.stroke('grey')
141 141 gc.stroke_width(1)
142 142 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
143 143 gc.stroke('black')
144 144 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
145 145
146 146 # content
147 147 top = headers_heigth + 20
148 148 gc.stroke('transparent')
149 149 events.each do |i|
150 150 if i.is_a?(Issue)
151 151 i_start_date = (i.start_date >= date_from ? i.start_date : date_from )
152 152 i_end_date = (i.due_date <= date_to ? i.due_date : date_to )
153 153 i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
154 154 i_done_date = (i_done_date <= date_from ? date_from : i_done_date )
155 155 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
156 156 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
157 157
158 158 i_left = subject_width + ((i_start_date - date_from)*zoom).floor
159 159 i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
160 160 d_width = ((i_done_date - i_start_date)*zoom).floor # done width
161 161 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
162 162
163 163 gc.fill('grey')
164 164 gc.rectangle(i_left, top, i_left + i_width, top - 6)
165 165 gc.fill('red')
166 166 gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
167 167 gc.fill('blue')
168 168 gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
169 169 gc.fill('black')
170 170 gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
171 171 else
172 172 i_left = subject_width + ((i.start_date - date_from)*zoom).floor
173 173 gc.fill('green')
174 174 gc.rectangle(i_left, top, i_left + 6, top - 6)
175 175 gc.fill('black')
176 176 gc.text(i_left + 11, top + 1, i.name)
177 177 end
178 178 top = top + 20
179 179 end
180 180
181 181 # today red line
182 182 if Date.today >= date_from and Date.today <= date_to
183 183 gc.stroke('red')
184 184 x = (Date.today-date_from+1)*zoom + subject_width
185 185 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
186 186 end
187 187
188 188 gc.draw(imgl)
189 189 imgl
190 190 end if Object.const_defined?(:Magick)
191
192 def new_issue_selector
193 trackers = @project.trackers
194 # can't use form tag inside helper
195 content_tag('form',
196 select_tag('tracker_id', '<option></option>' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"),
197 :action => url_for(:controller => 'issues', :action => 'new', :project_id => @project), :method => 'get')
198 end
199 191 end
@@ -1,18 +1,13
1 <% if authorize_for('issues', 'new') && @project.trackers.any? %>
2 <h3><%= l(:label_issue_new) %></h3>
3 <%= l(:label_tracker) %>: <%= new_issue_selector %>
4 <% end %>
5
6 1 <h3><%= l(:label_issue_plural) %></h3>
7 2 <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
8 3 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
9 4 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
10 5
11 6 <h3><%= l(:label_query_plural) %></h3>
12 7
13 8 <% queries = @project.queries.find(:all,
14 9 :order => "name ASC",
15 10 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
16 11 queries.each do |query| %>
17 12 <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
18 13 <% end %>
@@ -1,85 +1,80
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)%>: <%= auto_link @project.homepage %></li><% end %>
7 7 <% if @subprojects.any? %>
8 8 <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li>
9 9 <% end %>
10 10 <% if @project.parent %>
11 11 <li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li>
12 12 <% end %>
13 13 <% for custom_value in @custom_values %>
14 14 <% if !custom_value.value.empty? %>
15 15 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
16 16 <% end %>
17 17 <% end %>
18 18 </ul>
19 19
20 20 <% if User.current.allowed_to?(:view_issues, @project) %>
21 21 <div class="box">
22 22 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
23 23 <ul>
24 24 <% for tracker in @trackers %>
25 25 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
26 26 :set_filter => 1,
27 27 "tracker_id" => tracker.id %>:
28 28 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
29 29 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
30 30 <% end %>
31 31 </ul>
32 32 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
33 33 </div>
34 34 <% end %>
35 35 </div>
36 36
37 37 <div class="splitcontentright">
38 38 <% if @members_by_role.any? %>
39 39 <div class="box">
40 40 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
41 41 <p><% @members_by_role.keys.sort.each do |role| %>
42 42 <%= role.name %>:
43 43 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
44 44 <br />
45 45 <% end %></p>
46 46 </div>
47 47 <% end %>
48 48
49 49 <% if @news.any? && authorize_for('news', 'index') %>
50 50 <div class="box">
51 51 <h3><%=l(:label_news_latest)%></h3>
52 52 <%= render :partial => 'news/news', :collection => @news %>
53 53 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
54 54 </div>
55 55 <% end %>
56 56 </div>
57 57
58 58 <% content_for :sidebar do %>
59 <% if authorize_for('issues', 'new') && @project.trackers.any? %>
60 <h3><%= l(:label_issue_new) %></h3>
61 <%= l(:label_tracker) %>: <%= new_issue_selector %>
62 <% end %>
63
64 59 <% planning_links = []
65 60 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project)
66 61 planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :id => @project)
67 62 planning_links.compact!
68 63 unless planning_links.empty? %>
69 64 <h3>Planning</h3>
70 65 <p><%= planning_links.join(' | ') %></p>
71 66 <% end %>
72 67
73 68 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
74 69 <h3><%= l(:label_spent_time) %></h3>
75 70 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
76 71 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
77 72 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
78 73 <% end %>
79 74 <% end %>
80 75
81 76 <% content_for :header_tags do %>
82 77 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
83 78 <% end %>
84 79
85 80 <% html_title(l(:label_overview)) -%>
@@ -1,108 +1,110
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/mime_type'
4 4 require 'redmine/themes'
5 5 require 'redmine/plugin'
6 6
7 7 begin
8 8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
9 9 rescue LoadError
10 10 # RMagick is not available
11 11 end
12 12
13 13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar )
14 14
15 15 # Permissions
16 16 Redmine::AccessControl.map do |map|
17 17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
18 18 map.permission :search_project, {:search => :index}, :public => true
19 19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
20 20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
21 21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
22 22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
23 23
24 24 map.project_module :issue_tracking do |map|
25 25 # Issue categories
26 26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
27 27 # Issues
28 28 map.permission :view_issues, {:projects => [:changelog, :roadmap],
29 29 :issues => [:index, :changes, :show, :context_menu],
30 30 :versions => [:show, :status_by],
31 31 :queries => :index,
32 32 :reports => :issue_report}, :public => true
33 33 map.permission :add_issues, {:issues => :new}
34 34 map.permission :edit_issues, {:projects => :bulk_edit_issues,
35 35 :issues => [:edit, :update, :destroy_attachment]}
36 36 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
37 37 map.permission :add_issue_notes, {:issues => :update}
38 38 map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin
39 39 map.permission :delete_issues, {:issues => :destroy}, :require => :member
40 40 # Queries
41 41 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
42 42 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
43 43 # Gantt & calendar
44 44 map.permission :view_gantt, :projects => :gantt
45 45 map.permission :view_calendar, :projects => :calendar
46 46 end
47 47
48 48 map.project_module :time_tracking do |map|
49 49 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
50 50 map.permission :view_time_entries, :timelog => [:details, :report]
51 51 end
52 52
53 53 map.project_module :news do |map|
54 54 map.permission :manage_news, {:projects => :add_news, :news => [:edit, :destroy, :destroy_comment]}, :require => :member
55 55 map.permission :view_news, {:news => [:index, :show]}, :public => true
56 56 map.permission :comment_news, {:news => :add_comment}
57 57 end
58 58
59 59 map.project_module :documents do |map|
60 60 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
61 61 map.permission :view_documents, :documents => [:index, :show, :download]
62 62 end
63 63
64 64 map.project_module :files do |map|
65 65 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
66 66 map.permission :view_files, :projects => :list_files, :versions => :download
67 67 end
68 68
69 69 map.project_module :wiki do |map|
70 70 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
71 71 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
72 72 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
73 73 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
74 74 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
75 75 end
76 76
77 77 map.project_module :repository do |map|
78 78 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
79 79 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
80 80 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
81 81 end
82 82
83 83 map.project_module :boards do |map|
84 84 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
85 85 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
86 86 map.permission :add_messages, {:messages => [:new, :reply]}
87 87 map.permission :edit_messages, {:messages => :edit}, :require => :member
88 88 map.permission :delete_messages, {:messages => :destroy}, :require => :member
89 89 end
90 90 end
91 91
92 92 # Project menu configuration
93 93 Redmine::MenuManager.map :project_menu do |menu|
94 94 menu.push :overview, { :controller => 'projects', :action => 'show' }, :caption => :label_overview
95 95 menu.push :activity, { :controller => 'projects', :action => 'activity' }, :caption => :label_activity
96 96 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' }, :caption => :label_roadmap
97 97 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
98 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
99 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
98 100 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
99 101 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
100 102 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
101 103 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }, :caption => :label_wiki
102 104 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
103 105 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
104 106 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
105 107 menu.push :repository, { :controller => 'repositories', :action => 'show' },
106 108 :if => Proc.new { |p| p.repository && !p.repository.new_record? }, :caption => :label_repository
107 109 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :caption => :label_settings
108 110 end
@@ -1,119 +1,125
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 module Redmine
19 19 module MenuManager
20 20 module MenuController
21 21 def self.included(base)
22 22 base.extend(ClassMethods)
23 23 end
24 24
25 25 module ClassMethods
26 26 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
27 27 mattr_accessor :menu_items
28 28
29 29 # Set the menu item name for a controller or specific actions
30 30 # Examples:
31 31 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
32 32 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
33 33 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
34 34 #
35 35 # The default menu item name for a controller is controller_name by default
36 36 # Eg. the default menu item name for ProjectsController is :projects
37 37 def menu_item(id, options = {})
38 38 if actions = options[:only]
39 39 actions = [] << actions unless actions.is_a?(Array)
40 40 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
41 41 else
42 42 menu_items[controller_name.to_sym][:default] = id
43 43 end
44 44 end
45 45 end
46 46
47 47 def menu_items
48 48 self.class.menu_items
49 49 end
50 50
51 51 # Returns the menu item name according to the current action
52 52 def current_menu_item
53 53 menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
54 54 menu_items[controller_name.to_sym][:default]
55 55 end
56 56 end
57 57
58 58 module MenuHelper
59 59 # Returns the current menu item name
60 60 def current_menu_item
61 61 @controller.current_menu_item
62 62 end
63 63
64 64 # Renders the application main menu as a ul element
65 65 def render_main_menu(project)
66 66 links = []
67 67 Redmine::MenuManager.allowed_items(:project_menu, User.current, project).each do |item|
68 68 unless item.condition && !item.condition.call(project)
69 69 links << content_tag('li',
70 70 link_to(l(item.caption), {item.param => project}.merge(item.url),
71 :class => (current_menu_item == item.name ? 'selected' : nil)))
71 (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options)))
72 72 end
73 73 end if project && !project.new_record?
74 74 links.empty? ? nil : content_tag('ul', links.join("\n"))
75 75 end
76 76 end
77 77
78 78 class << self
79 79 def map(menu_name)
80 80 mapper = Mapper.new
81 81 yield mapper
82 82 @items ||= {}
83 83 @items[menu_name.to_sym] ||= []
84 84 @items[menu_name.to_sym] += mapper.items
85 85 end
86 86
87 87 def items(menu_name)
88 88 @items[menu_name.to_sym] || []
89 89 end
90 90
91 91 def allowed_items(menu_name, user, project)
92 92 items(menu_name).select {|item| user && user.allowed_to?(item.url, project)}
93 93 end
94 94 end
95 95
96 96 class Mapper
97 # Adds an item at the end of the menu. Available options:
98 # * param: the parameter name that is used for the project id (default is :id)
99 # * condition: a proc that is called before rendering the item, the item is displayed only if it returns true
100 # * caption: the localized string key that is used as the item label
101 # * html_options: a hash of html options that are passed to link_to
97 102 def push(name, url, options={})
98 103 @items ||= []
99 104 @items << MenuItem.new(name, url, options)
100 105 end
101 106
102 107 def items
103 108 @items
104 109 end
105 110 end
106 111
107 112 class MenuItem
108 attr_reader :name, :url, :param, :condition, :caption
113 attr_reader :name, :url, :param, :condition, :caption, :html_options
109 114
110 115 def initialize(name, url, options)
111 116 @name = name
112 117 @url = url
113 118 @condition = options[:if]
114 119 @param = options[:param] || :id
115 120 @caption = options[:caption] || name.to_s.humanize
121 @html_options = options[:html] || {}
116 122 end
117 123 end
118 124 end
119 125 end
@@ -1,520 +1,520
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: 12px;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.5em; padding: 2px 6px 0px 6px;}
13 13 #top-menu a {color: #fff; padding-right: 4px;}
14 14 #account {float:right;}
15 15
16 16 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
17 17 #header a {color:#f8f8f8;}
18 18 #quick-search {float:right;}
19 19
20 #main-menu {position: absolute; bottom: 0px; left:6px;}
20 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
21 21 #main-menu ul {margin: 0; padding: 0;}
22 22 #main-menu li {
23 23 float:left;
24 24 list-style-type:none;
25 25 margin: 0px 10px 0px 0px;
26 26 padding: 0px 0px 0px 0px;
27 27 white-space:nowrap;
28 28 }
29 29 #main-menu li a {
30 30 display: block;
31 31 color: #fff;
32 32 text-decoration: none;
33 33 margin: 0;
34 34 padding: 4px 4px 4px 4px;
35 35 background: #2C4056;
36 36 }
37 37 #main-menu li a:hover, #main-menu li a.selected {background:#759FCF;}
38 38
39 39 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
40 40
41 41 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
42 42 * html #sidebar{ width: 17%; }
43 43 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
44 44 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
45 45 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
46 46
47 47 #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
48 48 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
49 49 html>body #content {
50 50 height: auto;
51 51 min-height: 600px;
52 52 }
53 53
54 54 #main.nosidebar #sidebar{ display: none; }
55 55 #main.nosidebar #content{ width: auto; border-right: 0; }
56 56
57 57 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
58 58
59 59 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
60 60 #login-form table td {padding: 6px;}
61 61 #login-form label {font-weight: bold;}
62 62
63 63 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
64 64
65 65 /***** Links *****/
66 66 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
67 67 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
68 68 a img{ border: 0; }
69 69
70 70 /***** Tables *****/
71 71 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
72 72 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
73 73 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
74 74 table.list td.id { width: 2%; text-align: center;}
75 75 table.list td.checkbox { width: 15px; padding: 0px;}
76 76
77 77 tr.issue { text-align: center; white-space: nowrap; }
78 78 tr.issue td.subject, tr.issue td.category { white-space: normal; }
79 79 tr.issue td.subject { text-align: left; }
80 80 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
81 81
82 82 tr.entry { border: 1px solid #f8f8f8; }
83 83 tr.entry td { white-space: nowrap; }
84 84 tr.entry td.filename { width: 30%; }
85 85 tr.entry td.size { text-align: right; font-size: 90%; }
86 86 tr.entry td.revision, tr.entry td.author { text-align: center; }
87 87 tr.entry td.age { text-align: right; }
88 88
89 89 tr.changeset td.author { text-align: center; width: 15%; }
90 90 tr.changeset td.committed_on { text-align: center; width: 15%; }
91 91
92 92 tr.message { height: 2.6em; }
93 93 tr.message td.last_message { font-size: 80%; }
94 94 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
95 95 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
96 96
97 97 table.list tbody tr:hover { background-color:#ffffdd; }
98 98 table td {padding:2px;}
99 99 table p {margin:0;}
100 100 .odd {background-color:#f6f7f8;}
101 101 .even {background-color: #fff;}
102 102
103 103 .highlight { background-color: #FCFD8D;}
104 104 .highlight.token-1 { background-color: #faa;}
105 105 .highlight.token-2 { background-color: #afa;}
106 106 .highlight.token-3 { background-color: #aaf;}
107 107
108 108 .box{
109 109 padding:6px;
110 110 margin-bottom: 10px;
111 111 background-color:#f6f6f6;
112 112 color:#505050;
113 113 line-height:1.5em;
114 114 border: 1px solid #e4e4e4;
115 115 }
116 116
117 117 div.square {
118 118 border: 1px solid #999;
119 119 float: left;
120 120 margin: .3em .4em 0 .4em;
121 121 overflow: hidden;
122 122 width: .6em; height: .6em;
123 123 }
124 124
125 125 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;}
126 126 .contextual input {font-size:0.9em;}
127 127
128 128 .splitcontentleft{float:left; width:49%;}
129 129 .splitcontentright{float:right; width:49%;}
130 130 form {display: inline;}
131 131 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
132 132 fieldset {border: 1px solid #e4e4e4; margin:0;}
133 133 legend {color: #484848;}
134 134 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
135 135 textarea.wiki-edit { width: 99%; }
136 136 li p {margin-top: 0;}
137 137 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
138 138 .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;}
139 139 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
140 140
141 141 .pagination {font-size: 90%}
142 142 p.pagination {margin-top:8px;}
143 143
144 144 /***** Tabular forms ******/
145 145 .tabular p{
146 146 margin: 0;
147 147 padding: 5px 0 8px 0;
148 148 padding-left: 180px; /*width of left column containing the label elements*/
149 149 height: 1%;
150 150 clear:left;
151 151 }
152 152
153 153 .tabular label{
154 154 font-weight: bold;
155 155 float: left;
156 156 text-align: right;
157 157 margin-left: -180px; /*width of left column*/
158 158 width: 175px; /*width of labels. Should be smaller than left column to create some right
159 159 margin*/
160 160 }
161 161
162 162 .tabular label.floating{
163 163 font-weight: normal;
164 164 margin-left: 0px;
165 165 text-align: left;
166 166 width: 200px;
167 167 }
168 168
169 169 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
170 170
171 171 .tabular.settings p{ padding-left: 300px; }
172 172 .tabular.settings label{ margin-left: -300px; width: 295px; }
173 173
174 174 .required {color: #bb0000;}
175 175 .summary {font-style: italic;}
176 176
177 177 div.attachments p { margin:4px 0 2px 0; }
178 178
179 179 /***** Flash & error messages ****/
180 180 #errorExplanation, div.flash, .nodata {
181 181 padding: 4px 4px 4px 30px;
182 182 margin-bottom: 12px;
183 183 font-size: 1.1em;
184 184 border: 2px solid;
185 185 }
186 186
187 187 div.flash {margin-top: 8px;}
188 188
189 189 div.flash.error, #errorExplanation {
190 190 background: url(../images/false.png) 8px 5px no-repeat;
191 191 background-color: #ffe3e3;
192 192 border-color: #dd0000;
193 193 color: #550000;
194 194 }
195 195
196 196 div.flash.notice {
197 197 background: url(../images/true.png) 8px 5px no-repeat;
198 198 background-color: #dfffdf;
199 199 border-color: #9fcf9f;
200 200 color: #005f00;
201 201 }
202 202
203 203 .nodata {
204 204 text-align: center;
205 205 background-color: #FFEBC1;
206 206 border-color: #FDBF3B;
207 207 color: #A6750C;
208 208 }
209 209
210 210 #errorExplanation ul { font-size: 0.9em;}
211 211
212 212 /***** Ajax indicator ******/
213 213 #ajax-indicator {
214 214 position: absolute; /* fixed not supported by IE */
215 215 background-color:#eee;
216 216 border: 1px solid #bbb;
217 217 top:35%;
218 218 left:40%;
219 219 width:20%;
220 220 font-weight:bold;
221 221 text-align:center;
222 222 padding:0.6em;
223 223 z-index:100;
224 224 filter:alpha(opacity=50);
225 225 -moz-opacity:0.5;
226 226 opacity: 0.5;
227 227 -khtml-opacity: 0.5;
228 228 }
229 229
230 230 html>body #ajax-indicator { position: fixed; }
231 231
232 232 #ajax-indicator span {
233 233 background-position: 0% 40%;
234 234 background-repeat: no-repeat;
235 235 background-image: url(../images/loading.gif);
236 236 padding-left: 26px;
237 237 vertical-align: bottom;
238 238 }
239 239
240 240 /***** Calendar *****/
241 241 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
242 242 table.cal thead th {width: 14%;}
243 243 table.cal tbody tr {height: 100px;}
244 244 table.cal th { background-color:#EEEEEE; padding: 4px; }
245 245 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
246 246 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
247 247 table.cal td.odd p.day-num {color: #bbb;}
248 248 table.cal td.today {background:#ffffdd;}
249 249 table.cal td.today p.day-num {font-weight: bold;}
250 250
251 251 /***** Tooltips ******/
252 252 .tooltip{position:relative;z-index:24;}
253 253 .tooltip:hover{z-index:25;color:#000;}
254 254 .tooltip span.tip{display: none; text-align:left;}
255 255
256 256 div.tooltip:hover span.tip{
257 257 display:block;
258 258 position:absolute;
259 259 top:12px; left:24px; width:270px;
260 260 border:1px solid #555;
261 261 background-color:#fff;
262 262 padding: 4px;
263 263 font-size: 0.8em;
264 264 color:#505050;
265 265 }
266 266
267 267 /***** Progress bar *****/
268 268 table.progress {
269 269 border: 1px solid #D7D7D7;
270 270 border-collapse: collapse;
271 271 border-spacing: 0pt;
272 272 empty-cells: show;
273 273 text-align: center;
274 274 float:left;
275 275 margin: 1px 6px 1px 0px;
276 276 }
277 277
278 278 table.progress td { height: 0.9em; }
279 279 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
280 280 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
281 281 table.progress td.open { background: #FFF none repeat scroll 0%; }
282 282 p.pourcent {font-size: 80%;}
283 283 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
284 284
285 285 div#status_by { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; }
286 286
287 287 /***** Tabs *****/
288 288 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
289 289 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
290 290 #content .tabs>ul { bottom:-1px; } /* others */
291 291 #content .tabs ul li {
292 292 float:left;
293 293 list-style-type:none;
294 294 white-space:nowrap;
295 295 margin-right:8px;
296 296 background:#fff;
297 297 }
298 298 #content .tabs ul li a{
299 299 display:block;
300 300 font-size: 0.9em;
301 301 text-decoration:none;
302 302 line-height:1.3em;
303 303 padding:4px 6px 4px 6px;
304 304 border: 1px solid #ccc;
305 305 border-bottom: 1px solid #bbbbbb;
306 306 background-color: #eeeeee;
307 307 color:#777;
308 308 font-weight:bold;
309 309 }
310 310
311 311 #content .tabs ul li a:hover {
312 312 background-color: #ffffdd;
313 313 text-decoration:none;
314 314 }
315 315
316 316 #content .tabs ul li a.selected {
317 317 background-color: #fff;
318 318 border: 1px solid #bbbbbb;
319 319 border-bottom: 1px solid #fff;
320 320 }
321 321
322 322 #content .tabs ul li a.selected:hover {
323 323 background-color: #fff;
324 324 }
325 325
326 326 /***** Diff *****/
327 327 .diff_out { background: #fcc; }
328 328 .diff_in { background: #cfc; }
329 329
330 330 /***** Wiki *****/
331 331 div.wiki table {
332 332 border: 1px solid #505050;
333 333 border-collapse: collapse;
334 334 }
335 335
336 336 div.wiki table, div.wiki td, div.wiki th {
337 337 border: 1px solid #bbb;
338 338 padding: 4px;
339 339 }
340 340
341 341 div.wiki .external {
342 342 background-position: 0% 60%;
343 343 background-repeat: no-repeat;
344 344 padding-left: 12px;
345 345 background-image: url(../images/external.png);
346 346 }
347 347
348 348 div.wiki a.new {
349 349 color: #b73535;
350 350 }
351 351
352 352 div.wiki pre {
353 353 margin: 1em 1em 1em 1.6em;
354 354 padding: 2px;
355 355 background-color: #fafafa;
356 356 border: 1px solid #dadada;
357 357 width:95%;
358 358 overflow-x: auto;
359 359 }
360 360
361 361 div.wiki div.toc {
362 362 background-color: #ffffdd;
363 363 border: 1px solid #e4e4e4;
364 364 padding: 4px;
365 365 line-height: 1.2em;
366 366 margin-bottom: 12px;
367 367 margin-right: 12px;
368 368 display: table
369 369 }
370 370 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
371 371
372 372 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
373 373 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
374 374
375 375 div.wiki div.toc a {
376 376 display: block;
377 377 font-size: 0.9em;
378 378 font-weight: normal;
379 379 text-decoration: none;
380 380 color: #606060;
381 381 }
382 382 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
383 383
384 384 div.wiki div.toc a.heading2 { margin-left: 6px; }
385 385 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
386 386
387 387 /***** My page layout *****/
388 388 .block-receiver {
389 389 border:1px dashed #c0c0c0;
390 390 margin-bottom: 20px;
391 391 padding: 15px 0 15px 0;
392 392 }
393 393
394 394 .mypage-box {
395 395 margin:0 0 20px 0;
396 396 color:#505050;
397 397 line-height:1.5em;
398 398 }
399 399
400 400 .handle {
401 401 cursor: move;
402 402 }
403 403
404 404 a.close-icon {
405 405 display:block;
406 406 margin-top:3px;
407 407 overflow:hidden;
408 408 width:12px;
409 409 height:12px;
410 410 background-repeat: no-repeat;
411 411 cursor:pointer;
412 412 background-image:url('../images/close.png');
413 413 }
414 414
415 415 a.close-icon:hover {
416 416 background-image:url('../images/close_hl.png');
417 417 }
418 418
419 419 /***** Gantt chart *****/
420 420 .gantt_hdr {
421 421 position:absolute;
422 422 top:0;
423 423 height:16px;
424 424 border-top: 1px solid #c0c0c0;
425 425 border-bottom: 1px solid #c0c0c0;
426 426 border-right: 1px solid #c0c0c0;
427 427 text-align: center;
428 428 overflow: hidden;
429 429 }
430 430
431 431 .task {
432 432 position: absolute;
433 433 height:8px;
434 434 font-size:0.8em;
435 435 color:#888;
436 436 padding:0;
437 437 margin:0;
438 438 line-height:0.8em;
439 439 }
440 440
441 441 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
442 442 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
443 443 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
444 444 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
445 445
446 446 /***** Icons *****/
447 447 .icon {
448 448 background-position: 0% 40%;
449 449 background-repeat: no-repeat;
450 450 padding-left: 20px;
451 451 padding-top: 2px;
452 452 padding-bottom: 3px;
453 453 }
454 454
455 455 .icon22 {
456 456 background-position: 0% 40%;
457 457 background-repeat: no-repeat;
458 458 padding-left: 26px;
459 459 line-height: 22px;
460 460 vertical-align: middle;
461 461 }
462 462
463 463 .icon-add { background-image: url(../images/add.png); }
464 464 .icon-edit { background-image: url(../images/edit.png); }
465 465 .icon-copy { background-image: url(../images/copy.png); }
466 466 .icon-del { background-image: url(../images/delete.png); }
467 467 .icon-move { background-image: url(../images/move.png); }
468 468 .icon-save { background-image: url(../images/save.png); }
469 469 .icon-cancel { background-image: url(../images/cancel.png); }
470 470 .icon-pdf { background-image: url(../images/pdf.png); }
471 471 .icon-csv { background-image: url(../images/csv.png); }
472 472 .icon-html { background-image: url(../images/html.png); }
473 473 .icon-image { background-image: url(../images/image.png); }
474 474 .icon-txt { background-image: url(../images/txt.png); }
475 475 .icon-file { background-image: url(../images/file.png); }
476 476 .icon-folder { background-image: url(../images/folder.png); }
477 477 .open .icon-folder { background-image: url(../images/folder_open.png); }
478 478 .icon-package { background-image: url(../images/package.png); }
479 479 .icon-home { background-image: url(../images/home.png); }
480 480 .icon-user { background-image: url(../images/user.png); }
481 481 .icon-mypage { background-image: url(../images/user_page.png); }
482 482 .icon-admin { background-image: url(../images/admin.png); }
483 483 .icon-projects { background-image: url(../images/projects.png); }
484 484 .icon-logout { background-image: url(../images/logout.png); }
485 485 .icon-help { background-image: url(../images/help.png); }
486 486 .icon-attachment { background-image: url(../images/attachment.png); }
487 487 .icon-index { background-image: url(../images/index.png); }
488 488 .icon-history { background-image: url(../images/history.png); }
489 489 .icon-feed { background-image: url(../images/feed.png); }
490 490 .icon-time { background-image: url(../images/time.png); }
491 491 .icon-stats { background-image: url(../images/stats.png); }
492 492 .icon-warning { background-image: url(../images/warning.png); }
493 493 .icon-fav { background-image: url(../images/fav.png); }
494 494 .icon-fav-off { background-image: url(../images/fav_off.png); }
495 495 .icon-reload { background-image: url(../images/reload.png); }
496 496 .icon-lock { background-image: url(../images/locked.png); }
497 497 .icon-unlock { background-image: url(../images/unlock.png); }
498 498 .icon-note { background-image: url(../images/note.png); }
499 499 .icon-checked { background-image: url(../images/true.png); }
500 500
501 501 .icon22-projects { background-image: url(../images/22x22/projects.png); }
502 502 .icon22-users { background-image: url(../images/22x22/users.png); }
503 503 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
504 504 .icon22-role { background-image: url(../images/22x22/role.png); }
505 505 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
506 506 .icon22-options { background-image: url(../images/22x22/options.png); }
507 507 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
508 508 .icon22-authent { background-image: url(../images/22x22/authent.png); }
509 509 .icon22-info { background-image: url(../images/22x22/info.png); }
510 510 .icon22-comment { background-image: url(../images/22x22/comment.png); }
511 511 .icon22-package { background-image: url(../images/22x22/package.png); }
512 512 .icon22-settings { background-image: url(../images/22x22/settings.png); }
513 513 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
514 514
515 515 /***** Media print specific styles *****/
516 516 @media print {
517 517 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
518 518 #main { background: #fff; }
519 519 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
520 520 }
General Comments 0
You need to be logged in to leave comments. Login now