##// END OF EJS Templates
Adds support for free ticket filtering and custom queries on Calendar....
Jean-Philippe Lang -
r1796:586f4e3831e3
parent child
Show More
@@ -0,0 +1,55
1 <% form_tag({}, :id => 'query_form') do %>
2 <% if @query.new_record? %>
3 <h2><%= l(:label_calendar) %></h2>
4 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
5 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
6 </fieldset>
7 <% else %>
8 <h2><%=h @query.name %></h2>
9 <% html_title @query.name %>
10 <% end %>
11
12 <fieldset id="date-range"><legend><%= l(:label_date_range) %></legend>
13 <%= select_month(@month, :prefix => "month", :discard_type => true) %>
14 <%= select_year(@year, :prefix => "year", :discard_type => true) %>
15 </fieldset>
16
17 <p style="float:right; margin:0px;">
18 <%= link_to_remote ('&#171; ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")),
19 {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) }},
20 {:href => url_for(:action => 'calendar', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1))}
21 %> |
22 <%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' &#187;'),
23 {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) }},
24 {:href => url_for(:action => 'calendar', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1))}
25 %>
26 </p>
27
28 <p class="buttons">
29 <%= link_to_remote l(:button_apply),
30 { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
31 :update => "content",
32 :with => "Form.serialize('query_form')"
33 }, :class => 'icon icon-checked' %>
34
35 <%= link_to_remote l(:button_clear),
36 { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
37 :update => "content",
38 }, :class => 'icon icon-reload' if @query.new_record? %>
39 </p>
40 <% end %>
41
42 <%= error_messages_for 'query' %>
43 <% if @query.valid? %>
44 <%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>
45
46 <%= image_tag 'arrow_from.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_begin_day) %><br />
47 <%= image_tag 'arrow_to.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_end_day) %><br />
48 <%= image_tag 'arrow_bw.png' %>&nbsp;&nbsp;<%= l(:text_tip_task_begin_end_day) %><br />
49 <% end %>
50
51 <% content_for :sidebar do %>
52 <%= render :partial => 'issues/sidebar' %>
53 <% end %>
54
55 <% html_title(l(:label_calendar)) -%>
@@ -1,464 +1,491
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
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 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => :new
20 20
21 21 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview, :gantt]
23 before_filter :find_project, :only => [:new, :update_form, :preview, :gantt, :calendar]
24 24 before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
25 25 before_filter :find_optional_project, :only => [:index, :changes]
26 26 accept_key_auth :index, :changes
27 27
28 28 helper :journals
29 29 helper :projects
30 30 include ProjectsHelper
31 31 helper :custom_fields
32 32 include CustomFieldsHelper
33 33 helper :ifpdf
34 34 include IfpdfHelper
35 35 helper :issue_relations
36 36 include IssueRelationsHelper
37 37 helper :watchers
38 38 include WatchersHelper
39 39 helper :attachments
40 40 include AttachmentsHelper
41 41 helper :queries
42 42 helper :sort
43 43 include SortHelper
44 44 include IssuesHelper
45 45 helper :timelog
46 46
47 47 def index
48 48 sort_init "#{Issue.table_name}.id", "desc"
49 49 sort_update
50 50 retrieve_query
51 51 if @query.valid?
52 52 limit = per_page_option
53 53 respond_to do |format|
54 54 format.html { }
55 55 format.atom { }
56 56 format.csv { limit = Setting.issues_export_limit.to_i }
57 57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 58 end
59 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 61 @issues = Issue.find :all, :order => sort_clause,
62 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 63 :conditions => @query.statement,
64 64 :limit => limit,
65 65 :offset => @issue_pages.current.offset
66 66 respond_to do |format|
67 67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 71 end
72 72 else
73 73 # Send html if the query is not valid
74 74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 75 end
76 76 rescue ActiveRecord::RecordNotFound
77 77 render_404
78 78 end
79 79
80 80 def changes
81 81 sort_init "#{Issue.table_name}.id", "desc"
82 82 sort_update
83 83 retrieve_query
84 84 if @query.valid?
85 85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 86 :conditions => @query.statement,
87 87 :limit => 25,
88 88 :order => "#{Journal.table_name}.created_on DESC"
89 89 end
90 90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 91 render :layout => false, :content_type => 'application/atom+xml'
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def show
97 97 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 98 @journals.each_with_index {|j,i| j.indice = i+1}
99 99 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 100 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 101 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 102 @priorities = Enumeration::get_values('IPRI')
103 103 @time_entry = TimeEntry.new
104 104 respond_to do |format|
105 105 format.html { render :template => 'issues/show.rhtml' }
106 106 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 107 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 108 end
109 109 end
110 110
111 111 # Add a new issue
112 112 # The new issue will be created from an existing one if copy_from parameter is given
113 113 def new
114 114 @issue = Issue.new
115 115 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 116 @issue.project = @project
117 117 # Tracker must be set before custom field values
118 118 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 119 if @issue.tracker.nil?
120 120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 121 render :nothing => true, :layout => true
122 122 return
123 123 end
124 124 @issue.attributes = params[:issue]
125 125 @issue.author = User.current
126 126
127 127 default_status = IssueStatus.default
128 128 unless default_status
129 129 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
130 130 render :nothing => true, :layout => true
131 131 return
132 132 end
133 133 @issue.status = default_status
134 134 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
135 135
136 136 if request.get? || request.xhr?
137 137 @issue.start_date ||= Date.today
138 138 else
139 139 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
140 140 # Check that the user is allowed to apply the requested status
141 141 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
142 142 if @issue.save
143 143 attach_files(@issue, params[:attachments])
144 144 flash[:notice] = l(:notice_successful_create)
145 145 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
146 146 redirect_to :controller => 'issues', :action => 'show', :id => @issue
147 147 return
148 148 end
149 149 end
150 150 @priorities = Enumeration::get_values('IPRI')
151 151 render :layout => !request.xhr?
152 152 end
153 153
154 154 # Attributes that can be updated on workflow transition (without :edit permission)
155 155 # TODO: make it configurable (at least per role)
156 156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157 157
158 158 def edit
159 159 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
160 160 @priorities = Enumeration::get_values('IPRI')
161 161 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
162 162 @time_entry = TimeEntry.new
163 163
164 164 @notes = params[:notes]
165 165 journal = @issue.init_journal(User.current, @notes)
166 166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
167 167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
168 168 attrs = params[:issue].dup
169 169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
170 170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
171 171 @issue.attributes = attrs
172 172 end
173 173
174 174 if request.post?
175 175 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
176 176 @time_entry.attributes = params[:time_entry]
177 177 attachments = attach_files(@issue, params[:attachments])
178 178 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
179 179 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
180 180 # Log spend time
181 181 if current_role.allowed_to?(:log_time)
182 182 @time_entry.save
183 183 end
184 184 if !journal.new_record?
185 185 # Only send notification if something was actually changed
186 186 flash[:notice] = l(:notice_successful_update)
187 187 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
188 188 end
189 189 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
190 190 end
191 191 end
192 192 rescue ActiveRecord::StaleObjectError
193 193 # Optimistic locking exception
194 194 flash.now[:error] = l(:notice_locking_conflict)
195 195 end
196 196
197 197 def reply
198 198 journal = Journal.find(params[:journal_id]) if params[:journal_id]
199 199 if journal
200 200 user = journal.user
201 201 text = journal.notes
202 202 else
203 203 user = @issue.author
204 204 text = @issue.description
205 205 end
206 206 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
207 207 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
208 208 render(:update) { |page|
209 209 page.<< "$('notes').value = \"#{content}\";"
210 210 page.show 'update'
211 211 page << "Form.Element.focus('notes');"
212 212 page << "Element.scrollTo('update');"
213 213 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
214 214 }
215 215 end
216 216
217 217 # Bulk edit a set of issues
218 218 def bulk_edit
219 219 if request.post?
220 220 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
221 221 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
222 222 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
223 223 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
224 224 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
225 225
226 226 unsaved_issue_ids = []
227 227 @issues.each do |issue|
228 228 journal = issue.init_journal(User.current, params[:notes])
229 229 issue.priority = priority if priority
230 230 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
231 231 issue.category = category if category || params[:category_id] == 'none'
232 232 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
233 233 issue.start_date = params[:start_date] unless params[:start_date].blank?
234 234 issue.due_date = params[:due_date] unless params[:due_date].blank?
235 235 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
236 236 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
237 237 # Don't save any change to the issue if the user is not authorized to apply the requested status
238 238 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
239 239 # Send notification for each issue (if changed)
240 240 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
241 241 else
242 242 # Keep unsaved issue ids to display them in flash error
243 243 unsaved_issue_ids << issue.id
244 244 end
245 245 end
246 246 if unsaved_issue_ids.empty?
247 247 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
248 248 else
249 249 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
250 250 end
251 251 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
252 252 return
253 253 end
254 254 # Find potential statuses the user could be allowed to switch issues to
255 255 @available_statuses = Workflow.find(:all, :include => :new_status,
256 256 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
257 257 end
258 258
259 259 def move
260 260 @allowed_projects = []
261 261 # find projects to which the user is allowed to move the issue
262 262 if User.current.admin?
263 263 # admin is allowed to move issues to any active (visible) project
264 264 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
265 265 else
266 266 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
267 267 end
268 268 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
269 269 @target_project ||= @project
270 270 @trackers = @target_project.trackers
271 271 if request.post?
272 272 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
273 273 unsaved_issue_ids = []
274 274 @issues.each do |issue|
275 275 issue.init_journal(User.current)
276 276 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
277 277 end
278 278 if unsaved_issue_ids.empty?
279 279 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
280 280 else
281 281 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
282 282 end
283 283 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
284 284 return
285 285 end
286 286 render :layout => false if request.xhr?
287 287 end
288 288
289 289 def destroy
290 290 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
291 291 if @hours > 0
292 292 case params[:todo]
293 293 when 'destroy'
294 294 # nothing to do
295 295 when 'nullify'
296 296 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
297 297 when 'reassign'
298 298 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
299 299 if reassign_to.nil?
300 300 flash.now[:error] = l(:error_issue_not_found_in_project)
301 301 return
302 302 else
303 303 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
304 304 end
305 305 else
306 306 # display the destroy form
307 307 return
308 308 end
309 309 end
310 310 @issues.each(&:destroy)
311 311 redirect_to :action => 'index', :project_id => @project
312 312 end
313 313
314 314 def destroy_attachment
315 315 a = @issue.attachments.find(params[:attachment_id])
316 316 a.destroy
317 317 journal = @issue.init_journal(User.current)
318 318 journal.details << JournalDetail.new(:property => 'attachment',
319 319 :prop_key => a.id,
320 320 :old_value => a.filename)
321 321 journal.save
322 322 redirect_to :action => 'show', :id => @issue
323 323 end
324 324
325 325 def gantt
326 326 @gantt = Redmine::Helpers::Gantt.new(params)
327 327 retrieve_query
328 328 if @query.valid?
329 329 events = []
330 330 # Issues that have start and due dates
331 331 events += Issue.find(:all,
332 332 :order => "start_date, due_date",
333 333 :include => [:tracker, :status, :assigned_to, :priority, :project],
334 334 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
335 335 )
336 336 # Issues that don't have a due date but that are assigned to a version with a date
337 337 events += Issue.find(:all,
338 338 :order => "start_date, effective_date",
339 339 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
340 340 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
341 341 )
342 342 # Related versions
343 343 version_ids = events.collect(&:fixed_version_id).compact.uniq
344 344 events += Version.find_all_by_id(version_ids, :include => :project,
345 345 :conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to]) unless version_ids.empty?
346 346
347 347 @gantt.events = events
348 348 end
349 349
350 350 respond_to do |format|
351 351 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
352 352 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
353 353 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-gantt.pdf") }
354 354 end
355 355 end
356 356
357 def calendar
358 if params[:year] and params[:year].to_i > 1900
359 @year = params[:year].to_i
360 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
361 @month = params[:month].to_i
362 end
363 end
364 @year ||= Date.today.year
365 @month ||= Date.today.month
366
367 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
368 retrieve_query
369 if @query.valid?
370 events = []
371 events += Issue.find(:all,
372 :include => [:tracker, :status, :assigned_to, :priority, :project],
373 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
374 )
375 events += Version.find(:all, :include => :project,
376 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
377
378 @calendar.events = events
379 end
380
381 render :layout => false if request.xhr?
382 end
383
357 384 def context_menu
358 385 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
359 386 if (@issues.size == 1)
360 387 @issue = @issues.first
361 388 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
362 389 end
363 390 projects = @issues.collect(&:project).compact.uniq
364 391 @project = projects.first if projects.size == 1
365 392
366 393 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
367 394 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
368 395 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
369 396 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
370 397 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
371 398 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
372 399 }
373 400 if @project
374 401 @assignables = @project.assignable_users
375 402 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
376 403 end
377 404
378 405 @priorities = Enumeration.get_values('IPRI').reverse
379 406 @statuses = IssueStatus.find(:all, :order => 'position')
380 407 @back = request.env['HTTP_REFERER']
381 408
382 409 render :layout => false
383 410 end
384 411
385 412 def update_form
386 413 @issue = Issue.new(params[:issue])
387 414 render :action => :new, :layout => false
388 415 end
389 416
390 417 def preview
391 418 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
392 419 @attachements = @issue.attachments if @issue
393 420 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
394 421 render :partial => 'common/preview'
395 422 end
396 423
397 424 private
398 425 def find_issue
399 426 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
400 427 @project = @issue.project
401 428 rescue ActiveRecord::RecordNotFound
402 429 render_404
403 430 end
404 431
405 432 # Filter for bulk operations
406 433 def find_issues
407 434 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
408 435 raise ActiveRecord::RecordNotFound if @issues.empty?
409 436 projects = @issues.collect(&:project).compact.uniq
410 437 if projects.size == 1
411 438 @project = projects.first
412 439 else
413 440 # TODO: let users bulk edit/move/destroy issues from different projects
414 441 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
415 442 end
416 443 rescue ActiveRecord::RecordNotFound
417 444 render_404
418 445 end
419 446
420 447 def find_project
421 448 @project = Project.find(params[:project_id])
422 449 rescue ActiveRecord::RecordNotFound
423 450 render_404
424 451 end
425 452
426 453 def find_optional_project
427 454 return true unless params[:project_id]
428 455 @project = Project.find(params[:project_id])
429 456 authorize
430 457 rescue ActiveRecord::RecordNotFound
431 458 render_404
432 459 end
433 460
434 461 # Retrieve query from session or build a new query
435 462 def retrieve_query
436 463 if !params[:query_id].blank?
437 464 cond = "project_id IS NULL"
438 465 cond << " OR project_id = #{@project.id}" if @project
439 466 @query = Query.find(params[:query_id], :conditions => cond)
440 467 @query.project = @project
441 468 session[:query] = {:id => @query.id, :project_id => @query.project_id}
442 469 else
443 470 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
444 471 # Give it a name, required to be valid
445 472 @query = Query.new(:name => "_")
446 473 @query.project = @project
447 474 if params[:fields] and params[:fields].is_a? Array
448 475 params[:fields].each do |field|
449 476 @query.add_filter(field, params[:operators][field], params[:values][field])
450 477 end
451 478 else
452 479 @query.available_filters.keys.each do |field|
453 480 @query.add_short_filter(field, params[field]) if params[field]
454 481 end
455 482 end
456 483 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
457 484 else
458 485 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
459 486 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
460 487 @query.project = @project
461 488 end
462 489 end
463 490 end
464 491 end
@@ -1,302 +1,274
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 accept_key_auth :activity, :calendar
30 accept_key_auth :activity
31 31
32 32 helper :sort
33 33 include SortHelper
34 34 helper :custom_fields
35 35 include CustomFieldsHelper
36 36 helper :ifpdf
37 37 include IfpdfHelper
38 38 helper :issues
39 39 helper IssuesHelper
40 40 helper :queries
41 41 include QueriesHelper
42 42 helper :repositories
43 43 include RepositoriesHelper
44 44 include ProjectsHelper
45 45
46 46 # Lists visible projects
47 47 def index
48 48 projects = Project.find :all,
49 49 :conditions => Project.visible_by(User.current),
50 50 :include => :parent
51 51 respond_to do |format|
52 52 format.html {
53 53 @project_tree = projects.group_by {|p| p.parent || p}
54 54 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
55 55 }
56 56 format.atom {
57 57 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
58 58 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 59 }
60 60 end
61 61 end
62 62
63 63 # Add a new project
64 64 def add
65 65 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 66 @trackers = Tracker.all
67 67 @root_projects = Project.find(:all,
68 68 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
69 69 :order => 'name')
70 70 @project = Project.new(params[:project])
71 71 if request.get?
72 72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 73 @project.trackers = Tracker.all
74 74 @project.is_public = Setting.default_projects_public?
75 75 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
76 76 else
77 77 @project.enabled_module_names = params[:enabled_modules]
78 78 if @project.save
79 79 flash[:notice] = l(:notice_successful_create)
80 80 redirect_to :controller => 'admin', :action => 'projects'
81 81 end
82 82 end
83 83 end
84 84
85 85 # Show @project
86 86 def show
87 87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
88 88 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
89 89 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
90 90 @trackers = @project.rolled_up_trackers
91 91
92 92 cond = @project.project_condition(Setting.display_subprojects_issues?)
93 93 Issue.visible_by(User.current) do
94 94 @open_issues_by_tracker = Issue.count(:group => :tracker,
95 95 :include => [:project, :status, :tracker],
96 96 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
97 97 @total_issues_by_tracker = Issue.count(:group => :tracker,
98 98 :include => [:project, :status, :tracker],
99 99 :conditions => cond)
100 100 end
101 101 TimeEntry.visible_by(User.current) do
102 102 @total_hours = TimeEntry.sum(:hours,
103 103 :include => :project,
104 104 :conditions => cond).to_f
105 105 end
106 106 @key = User.current.rss_key
107 107 end
108 108
109 109 def settings
110 110 @root_projects = Project.find(:all,
111 111 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
112 112 :order => 'name')
113 113 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
114 114 @issue_category ||= IssueCategory.new
115 115 @member ||= @project.members.new
116 116 @trackers = Tracker.all
117 117 @repository ||= @project.repository
118 118 @wiki ||= @project.wiki
119 119 end
120 120
121 121 # Edit @project
122 122 def edit
123 123 if request.post?
124 124 @project.attributes = params[:project]
125 125 if @project.save
126 126 flash[:notice] = l(:notice_successful_update)
127 127 redirect_to :action => 'settings', :id => @project
128 128 else
129 129 settings
130 130 render :action => 'settings'
131 131 end
132 132 end
133 133 end
134 134
135 135 def modules
136 136 @project.enabled_module_names = params[:enabled_modules]
137 137 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
138 138 end
139 139
140 140 def archive
141 141 @project.archive if request.post? && @project.active?
142 142 redirect_to :controller => 'admin', :action => 'projects'
143 143 end
144 144
145 145 def unarchive
146 146 @project.unarchive if request.post? && !@project.active?
147 147 redirect_to :controller => 'admin', :action => 'projects'
148 148 end
149 149
150 150 # Delete @project
151 151 def destroy
152 152 @project_to_destroy = @project
153 153 if request.post? and params[:confirm]
154 154 @project_to_destroy.destroy
155 155 redirect_to :controller => 'admin', :action => 'projects'
156 156 end
157 157 # hide project in layout
158 158 @project = nil
159 159 end
160 160
161 161 # Add a new issue category to @project
162 162 def add_issue_category
163 163 @category = @project.issue_categories.build(params[:category])
164 164 if request.post? and @category.save
165 165 respond_to do |format|
166 166 format.html do
167 167 flash[:notice] = l(:notice_successful_create)
168 168 redirect_to :action => 'settings', :tab => 'categories', :id => @project
169 169 end
170 170 format.js do
171 171 # IE doesn't support the replace_html rjs method for select box options
172 172 render(:update) {|page| page.replace "issue_category_id",
173 173 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]')
174 174 }
175 175 end
176 176 end
177 177 end
178 178 end
179 179
180 180 # Add a new version to @project
181 181 def add_version
182 182 @version = @project.versions.build(params[:version])
183 183 if request.post? and @version.save
184 184 flash[:notice] = l(:notice_successful_create)
185 185 redirect_to :action => 'settings', :tab => 'versions', :id => @project
186 186 end
187 187 end
188 188
189 189 def add_file
190 190 if request.post?
191 191 @version = @project.versions.find_by_id(params[:version_id])
192 192 attachments = attach_files(@version, params[:attachments])
193 193 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
194 194 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
195 195 end
196 196 @versions = @project.versions.sort
197 197 end
198 198
199 199 def list_files
200 200 sort_init "#{Attachment.table_name}.filename", "asc"
201 201 sort_update
202 202 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
203 203 render :layout => !request.xhr?
204 204 end
205 205
206 206 # Show changelog for @project
207 207 def changelog
208 208 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
209 209 retrieve_selected_tracker_ids(@trackers)
210 210 @versions = @project.versions.sort
211 211 end
212 212
213 213 def roadmap
214 214 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
215 215 retrieve_selected_tracker_ids(@trackers)
216 216 @versions = @project.versions.sort
217 217 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
218 218 end
219 219
220 220 def activity
221 221 @days = Setting.activity_days_default.to_i
222 222
223 223 if params[:from]
224 224 begin; @date_to = params[:from].to_date; rescue; end
225 225 end
226 226
227 227 @date_to ||= Date.today + 1
228 228 @date_from = @date_to - @days
229 229 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
230 230
231 231 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects)
232 232 @activity.scope_select {|t| !params["show_#{t}"].nil?}
233 233 @activity.default_scope! if @activity.scope.empty?
234 234
235 235 events = @activity.events(@date_from, @date_to)
236 236
237 237 respond_to do |format|
238 238 format.html {
239 239 @events_by_day = events.group_by(&:event_date)
240 240 render :layout => false if request.xhr?
241 241 }
242 242 format.atom {
243 243 title = (@activity.scope.size == 1) ? l("label_#{@activity.scope.first.singularize}_plural") : l(:label_activity)
244 244 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
245 245 }
246 246 end
247 247 end
248 248
249 def calendar
250 @trackers = @project.rolled_up_trackers
251 retrieve_selected_tracker_ids(@trackers)
252
253 if params[:year] and params[:year].to_i > 1900
254 @year = params[:year].to_i
255 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
256 @month = params[:month].to_i
257 end
258 end
259 @year ||= Date.today.year
260 @month ||= Date.today.month
261 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
262 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
263 events = []
264 @project.issues_with_subprojects(@with_subprojects) do
265 events += Issue.find(:all,
266 :include => [:tracker, :status, :assigned_to, :priority, :project],
267 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
268 ) unless @selected_tracker_ids.empty?
269 events += Version.find(:all, :include => :project,
270 :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
271 end
272 @calendar.events = events
273
274 render :layout => false if request.xhr?
275 end
276
277 249 private
278 250 # Find project of id params[:id]
279 251 # if not found, redirect to project list
280 252 # Used as a before_filter
281 253 def find_project
282 254 @project = Project.find(params[:id])
283 255 rescue ActiveRecord::RecordNotFound
284 256 render_404
285 257 end
286 258
287 259 def find_optional_project
288 260 return true unless params[:id]
289 261 @project = Project.find(params[:id])
290 262 authorize
291 263 rescue ActiveRecord::RecordNotFound
292 264 render_404
293 265 end
294 266
295 267 def retrieve_selected_tracker_ids(selectable_trackers)
296 268 if ids = params[:tracker_ids]
297 269 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
298 270 else
299 271 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
300 272 end
301 273 end
302 274 end
@@ -1,383 +1,385
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
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 class QueryColumn
19 19 attr_accessor :name, :sortable, :default_order
20 20 include GLoc
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.default_order = options[:default_order]
26 26 end
27 27
28 28 def caption
29 29 set_language_if_valid(User.current.language)
30 30 l("field_#{name}")
31 31 end
32 32 end
33 33
34 34 class QueryCustomFieldColumn < QueryColumn
35 35
36 36 def initialize(custom_field)
37 37 self.name = "cf_#{custom_field.id}".to_sym
38 38 self.sortable = false
39 39 @cf = custom_field
40 40 end
41 41
42 42 def caption
43 43 @cf.name
44 44 end
45 45
46 46 def custom_field
47 47 @cf
48 48 end
49 49 end
50 50
51 51 class Query < ActiveRecord::Base
52 52 belongs_to :project
53 53 belongs_to :user
54 54 serialize :filters
55 55 serialize :column_names
56 56
57 57 attr_protected :project_id, :user_id
58 58
59 59 validates_presence_of :name, :on => :save
60 60 validates_length_of :name, :maximum => 255
61 61
62 62 @@operators = { "=" => :label_equals,
63 63 "!" => :label_not_equals,
64 64 "o" => :label_open_issues,
65 65 "c" => :label_closed_issues,
66 66 "!*" => :label_none,
67 67 "*" => :label_all,
68 68 ">=" => '>=',
69 69 "<=" => '<=',
70 70 "<t+" => :label_in_less_than,
71 71 ">t+" => :label_in_more_than,
72 72 "t+" => :label_in,
73 73 "t" => :label_today,
74 74 "w" => :label_this_week,
75 75 ">t-" => :label_less_than_ago,
76 76 "<t-" => :label_more_than_ago,
77 77 "t-" => :label_ago,
78 78 "~" => :label_contains,
79 79 "!~" => :label_not_contains }
80 80
81 81 cattr_reader :operators
82 82
83 83 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 84 :list_status => [ "o", "=", "!", "c", "*" ],
85 85 :list_optional => [ "=", "!", "!*", "*" ],
86 86 :list_subprojects => [ "*", "!*", "=" ],
87 87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 89 :string => [ "=", "~", "!", "!~" ],
90 90 :text => [ "~", "!~" ],
91 91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
92 92
93 93 cattr_reader :operators_by_filter_type
94 94
95 95 @@available_columns = [
96 96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 100 QueryColumn.new(:author),
101 101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
102 102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
105 105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 110 ]
111 111 cattr_reader :available_columns
112 112
113 113 def initialize(attributes = nil)
114 114 super attributes
115 115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 116 set_language_if_valid(User.current.language)
117 117 end
118 118
119 119 def after_initialize
120 120 # Store the fact that project is nil (used in #editable_by?)
121 121 @is_for_all = project.nil?
122 122 end
123 123
124 124 def validate
125 125 filters.each_key do |field|
126 126 errors.add label_for(field), :activerecord_error_blank unless
127 127 # filter requires one or more values
128 128 (values_for(field) and !values_for(field).first.blank?) or
129 129 # filter doesn't require any value
130 130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
131 131 end if filters
132 132 end
133 133
134 134 def editable_by?(user)
135 135 return false unless user
136 136 # Admin can edit them all and regular users can edit their private queries
137 137 return true if user.admin? || (!is_public && self.user_id == user.id)
138 138 # Members can not edit public queries that are for all project (only admin is allowed to)
139 139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
140 140 end
141 141
142 142 def available_filters
143 143 return @available_filters if @available_filters
144 144
145 145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
146 146
147 147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
149 149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
150 150 "subject" => { :type => :text, :order => 8 },
151 151 "created_on" => { :type => :date_past, :order => 9 },
152 152 "updated_on" => { :type => :date_past, :order => 10 },
153 153 "start_date" => { :type => :date, :order => 11 },
154 154 "due_date" => { :type => :date, :order => 12 },
155 155 "estimated_hours" => { :type => :integer, :order => 13 },
156 156 "done_ratio" => { :type => :integer, :order => 14 }}
157 157
158 158 user_values = []
159 159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
160 160 if project
161 161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
162 162 else
163 163 # members of the user's projects
164 164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
165 165 end
166 166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
167 167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
168 168
169 169 if project
170 170 # project specific filters
171 171 unless @project.issue_categories.empty?
172 172 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
173 173 end
174 174 unless @project.versions.empty?
175 175 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
176 176 end
177 177 unless @project.active_children.empty?
178 178 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
179 179 end
180 180 add_custom_fields_filters(@project.all_issue_custom_fields)
181 181 else
182 182 # global filters for cross project issue list
183 183 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
184 184 end
185 185 @available_filters
186 186 end
187 187
188 188 def add_filter(field, operator, values)
189 189 # values must be an array
190 190 return unless values and values.is_a? Array # and !values.first.empty?
191 191 # check if field is defined as an available filter
192 192 if available_filters.has_key? field
193 193 filter_options = available_filters[field]
194 194 # check if operator is allowed for that filter
195 195 #if @@operators_by_filter_type[filter_options[:type]].include? operator
196 196 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
197 197 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
198 198 #end
199 199 filters[field] = {:operator => operator, :values => values }
200 200 end
201 201 end
202 202
203 203 def add_short_filter(field, expression)
204 204 return unless expression
205 205 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
206 206 add_filter field, (parms[0] || "="), [parms[1] || ""]
207 207 end
208 208
209 209 def has_filter?(field)
210 210 filters and filters[field]
211 211 end
212 212
213 213 def operator_for(field)
214 214 has_filter?(field) ? filters[field][:operator] : nil
215 215 end
216 216
217 217 def values_for(field)
218 218 has_filter?(field) ? filters[field][:values] : nil
219 219 end
220 220
221 221 def label_for(field)
222 222 label = available_filters[field][:name] if available_filters.has_key?(field)
223 223 label ||= field.gsub(/\_id$/, "")
224 224 end
225 225
226 226 def available_columns
227 227 return @available_columns if @available_columns
228 228 @available_columns = Query.available_columns
229 229 @available_columns += (project ?
230 230 project.all_issue_custom_fields :
231 231 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
232 232 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
233 233 end
234 234
235 235 def columns
236 236 if has_default_columns?
237 237 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
238 238 else
239 239 # preserve the column_names order
240 240 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
241 241 end
242 242 end
243 243
244 244 def column_names=(names)
245 245 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
246 246 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
247 247 write_attribute(:column_names, names)
248 248 end
249 249
250 250 def has_column?(column)
251 251 column_names && column_names.include?(column.name)
252 252 end
253 253
254 254 def has_default_columns?
255 255 column_names.nil? || column_names.empty?
256 256 end
257
258 def statement
259 # project/subprojects clause
257
258 def project_statement
260 259 project_clauses = []
261 260 if project && !@project.active_children.empty?
262 261 ids = [project.id]
263 262 if has_filter?("subproject_id")
264 263 case operator_for("subproject_id")
265 264 when '='
266 265 # include the selected subprojects
267 266 ids += values_for("subproject_id").each(&:to_i)
268 267 when '!*'
269 268 # main project only
270 269 else
271 270 # all subprojects
272 271 ids += project.child_ids
273 272 end
274 273 elsif Setting.display_subprojects_issues?
275 274 ids += project.child_ids
276 275 end
277 project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
276 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
278 277 elsif project
279 project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
278 project_clauses << "#{Project.table_name}.id = %d" % project.id
280 279 end
281 280 project_clauses << Project.visible_by(User.current)
282
281 project_clauses.join(' AND ')
282 end
283
284 def statement
283 285 # filters clauses
284 286 filters_clauses = []
285 287 filters.each_key do |field|
286 288 next if field == "subproject_id"
287 289 v = values_for(field).clone
288 290 next unless v and !v.empty?
289 291
290 292 sql = ''
291 293 is_custom_filter = false
292 294 if field =~ /^cf_(\d+)$/
293 295 # custom field
294 296 db_table = CustomValue.table_name
295 297 db_field = 'value'
296 298 is_custom_filter = true
297 299 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
298 300 else
299 301 # regular field
300 302 db_table = Issue.table_name
301 303 db_field = field
302 304 sql << '('
303 305 end
304 306
305 307 # "me" value subsitution
306 308 if %w(assigned_to_id author_id).include?(field)
307 309 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
308 310 end
309 311
310 312 case operator_for field
311 313 when "="
312 314 sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
313 315 when "!"
314 316 sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
315 317 when "!*"
316 318 sql = sql + "#{db_table}.#{db_field} IS NULL"
317 319 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
318 320 when "*"
319 321 sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
320 322 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
321 323 when ">="
322 324 sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
323 325 when "<="
324 326 sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
325 327 when "o"
326 328 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
327 329 when "c"
328 330 sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
329 331 when ">t-"
330 332 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
331 333 when "<t-"
332 334 sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
333 335 when "t-"
334 336 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
335 337 when ">t+"
336 338 sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
337 339 when "<t+"
338 340 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
339 341 when "t+"
340 342 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
341 343 when "t"
342 344 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
343 345 when "w"
344 346 from = l(:general_first_day_of_week) == '7' ?
345 347 # week starts on sunday
346 348 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
347 349 # week starts on monday (Rails default)
348 350 Time.now.at_beginning_of_week
349 351 sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
350 352 when "~"
351 353 sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
352 354 when "!~"
353 355 sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
354 356 end
355 357 sql << ')'
356 358 filters_clauses << sql
357 359 end if filters and valid?
358 360
359 (project_clauses + filters_clauses).join(' AND ')
361 (filters_clauses << project_statement).join(' AND ')
360 362 end
361 363
362 364 private
363 365
364 366 def add_custom_fields_filters(custom_fields)
365 367 @available_filters ||= {}
366 368
367 369 custom_fields.select(&:is_filter?).each do |field|
368 370 case field.field_format
369 371 when "text"
370 372 options = { :type => :text, :order => 20 }
371 373 when "list"
372 374 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
373 375 when "date"
374 376 options = { :type => :date, :order => 20 }
375 377 when "bool"
376 378 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
377 379 else
378 380 options = { :type => :string, :order => 20 }
379 381 end
380 382 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
381 383 end
382 384 end
383 385 end
@@ -1,24 +1,24
1 1 <h3><%= l(:label_issue_plural) %></h3>
2 2 <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
3 3 <% if @project %>
4 4 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
5 5 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
6 6
7 7 <% planning_links = []
8 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'projects', :action => 'calendar', :id => @project)
8 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :project_id => @project)
9 9 planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :project_id => @project)
10 10 planning_links.compact!
11 11 unless planning_links.empty? %>
12 12 <h3><%= l(:label_planning) %></h3>
13 13 <p><%= planning_links.join(' | ') %></p>
14 14 <% end %>
15 15
16 16 <% end %>
17 17
18 18 <% unless sidebar_queries.empty? -%>
19 19 <h3><%= l(:label_query_plural) %></h3>
20 20
21 21 <% sidebar_queries.each do |query| -%>
22 22 <%= link_to query.name, :query_id => query %><br />
23 23 <% end -%>
24 24 <% end -%>
@@ -1,258 +1,257
1 1 <% form_tag(params.merge(:month => nil, :year => nil, :months => nil), :id => 'query_form') do %>
2 2 <% if @query.new_record? %>
3 3 <h2><%=l(:label_gantt)%></h2>
4 4 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
5 5 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
6 6 </fieldset>
7 7 <% else %>
8 8 <h2><%=h @query.name %></h2>
9 <div id="query_form"></div>
10 9 <% html_title @query.name %>
11 10 <% end %>
12 11
13 12 <fieldset id="date-range"><legend><%= l(:label_date_range) %></legend>
14 13 <%= text_field_tag 'months', @gantt.months, :size => 2 %>
15 14 <%= l(:label_months_from) %>
16 15 <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
17 16 <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
18 17 <%= hidden_field_tag 'zoom', @gantt.zoom %>
19 18 </fieldset>
20 19
21 20 <p style="float:right; margin:0px;">
22 21 <%= if @gantt.zoom < 4
23 22 link_to_remote image_tag('zoom_in.png'), {:url => @gantt.params.merge(:zoom => (@gantt.zoom+1)), :update => 'content'}, {:href => url_for(@gantt.params.merge(:zoom => (@gantt.zoom+1)))}
24 23 else
25 24 image_tag 'zoom_in_g.png'
26 25 end %>
27 26 <%= if @gantt.zoom > 1
28 27 link_to_remote image_tag('zoom_out.png'), {:url => @gantt.params.merge(:zoom => (@gantt.zoom-1)), :update => 'content'}, {:href => url_for(@gantt.params.merge(:zoom => (@gantt.zoom-1)))}
29 28 else
30 29 image_tag 'zoom_out_g.png'
31 30 end %>
32 31 </p>
33 32
34 33 <p class="buttons">
35 34 <%= link_to_remote l(:button_apply),
36 35 { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
37 36 :update => "content",
38 37 :with => "Form.serialize('query_form')"
39 38 }, :class => 'icon icon-checked' %>
40 39
41 40 <%= link_to_remote l(:button_clear),
42 41 { :url => { :set_filter => (@query.new_record? ? 1 : nil) },
43 42 :update => "content",
44 43 }, :class => 'icon icon-reload' if @query.new_record? %>
45 44 </p>
46 45 <% end %>
47 46
48 47 <%= error_messages_for 'query' %>
49 48 <% if @query.valid? %>
50 49 <% zoom = 1
51 50 @gantt.zoom.times { zoom = zoom * 2 }
52 51
53 52 subject_width = 330
54 53 header_heigth = 18
55 54
56 55 headers_height = header_heigth
57 56 show_weeks = false
58 57 show_days = false
59 58
60 59 if @gantt.zoom >1
61 60 show_weeks = true
62 61 headers_height = 2*header_heigth
63 62 if @gantt.zoom > 2
64 63 show_days = true
65 64 headers_height = 3*header_heigth
66 65 end
67 66 end
68 67
69 68 g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom
70 69 g_height = [(20 * @gantt.events.length + 6)+150, 206].max
71 70 t_height = g_height + headers_height
72 71 %>
73 72
74 73 <table width="100%" style="border:0; border-collapse: collapse;">
75 74 <tr>
76 75 <td style="width:<%= subject_width %>px; padding:0px;">
77 76
78 77 <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
79 78 <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
80 79 <div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
81 80 <%
82 81 #
83 82 # Tasks subjects
84 83 #
85 84 top = headers_height + 8
86 85 @gantt.events.each do |i| %>
87 86 <div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:4px;overflow:hidden;"><small>
88 87 <% if i.is_a? Issue %>
89 88 <%= h("#{i.project} -") unless @project && @project == i.project %>
90 89 <%= link_to_issue i %>: <%=h i.subject %>
91 90 <% else %>
92 91 <span class="icon icon-package">
93 92 <%= h("#{i.project} -") unless @project && @project == i.project %>
94 93 <%= link_to_version i %>
95 94 </span>
96 95 <% end %>
97 96 </small></div>
98 97 <% top = top + 20
99 98 end %>
100 99 </div>
101 100 </td>
102 101 <td>
103 102
104 103 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
105 104 <div style="width:<%= g_width-1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
106 105 <%
107 106 #
108 107 # Months headers
109 108 #
110 109 month_f = @gantt.date_from
111 110 left = 0
112 111 height = (show_weeks ? header_heigth : header_heigth + g_height)
113 112 @gantt.months.times do
114 113 width = ((month_f >> 1) - month_f) * zoom - 1
115 114 %>
116 115 <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
117 116 <%= link_to "#{month_f.year}-#{month_f.month}", @gantt.params.merge(:year => month_f.year, :month => month_f.month), :title => "#{month_name(month_f.month)} #{month_f.year}"%>
118 117 </div>
119 118 <%
120 119 left = left + width + 1
121 120 month_f = month_f >> 1
122 121 end %>
123 122
124 123 <%
125 124 #
126 125 # Weeks headers
127 126 #
128 127 if show_weeks
129 128 left = 0
130 129 height = (show_days ? header_heigth-1 : header_heigth-1 + g_height)
131 130 if @gantt.date_from.cwday == 1
132 131 # @date_from is monday
133 132 week_f = @gantt.date_from
134 133 else
135 134 # find next monday after @date_from
136 135 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
137 136 width = (7 - @gantt.date_from.cwday + 1) * zoom-1
138 137 %>
139 138 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div>
140 139 <%
141 140 left = left + width+1
142 141 end %>
143 142 <%
144 143 while week_f <= @gantt.date_to
145 144 width = (week_f + 6 <= @gantt.date_to) ? 7 * zoom -1 : (@gantt.date_to - week_f + 1) * zoom-1
146 145 %>
147 146 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
148 147 <small><%= week_f.cweek if width >= 16 %></small>
149 148 </div>
150 149 <%
151 150 left = left + width+1
152 151 week_f = week_f+7
153 152 end
154 153 end %>
155 154
156 155 <%
157 156 #
158 157 # Days headers
159 158 #
160 159 if show_days
161 160 left = 0
162 161 height = g_height + header_heigth - 1
163 162 wday = @gantt.date_from.cwday
164 163 (@gantt.date_to - @gantt.date_from + 1).to_i.times do
165 164 width = zoom - 1
166 165 %>
167 166 <div style="left:<%= left %>px;top:37px;width:<%= width %>px;height:<%= height %>px;font-size:0.7em;<%= "background:#f1f1f1;" if wday > 5 %>" class="gantt_hdr">
168 167 <%= day_name(wday).first %>
169 168 </div>
170 169 <%
171 170 left = left + width+1
172 171 wday = wday + 1
173 172 wday = 1 if wday > 7
174 173 end
175 174 end %>
176 175
177 176 <%
178 177 #
179 178 # Tasks
180 179 #
181 180 top = headers_height + 10
182 181 @gantt.events.each do |i|
183 182 if i.is_a? Issue
184 183 i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from )
185 184 i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to )
186 185
187 186 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
188 187 i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date )
189 188 i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date )
190 189
191 190 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
192 191
193 192 i_left = ((i_start_date - @gantt.date_from)*zoom).floor
194 193 i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
195 194 d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
196 195 l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
197 196 %>
198 197 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="task task_todo">&nbsp;</div>
199 198 <% if l_width > 0 %>
200 199 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="task task_late">&nbsp;</div>
201 200 <% end %>
202 201 <% if d_width > 0 %>
203 202 <div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="task task_done">&nbsp;</div>
204 203 <% end %>
205 204 <div style="top:<%= top %>px;left:<%= i_left + i_width + 5 %>px;background:#fff;" class="task">
206 205 <%= i.status.name %>
207 206 <%= (i.done_ratio).to_i %>%
208 207 </div>
209 208 <div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
210 209 <span class="tip">
211 210 <%= render_issue_tooltip i %>
212 211 </span></div>
213 212 <% else
214 213 i_left = ((i.start_date - @gantt.date_from)*zoom).floor
215 214 %>
216 215 <div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div>
217 216 <div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
218 217 <%= h("#{i.project} -") unless @project && @project == i.project %>
219 218 <strong><%=h i %></strong>
220 219 </div>
221 220 <% end %>
222 221 <% top = top + 20
223 222 end %>
224 223
225 224 <%
226 225 #
227 226 # Today red line (excluded from cache)
228 227 #
229 228 if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
230 229 <div style="position: absolute;height:<%= g_height %>px;top:<%= headers_height + 1 %>px;left:<%= ((Date.today-@gantt.date_from+1)*zoom).floor()-1 %>px;width:10px;border-left: 1px dashed red;">&nbsp;</div>
231 230 <% end %>
232 231
233 232 </div>
234 233 </td>
235 234 </tr>
236 235 </table>
237 236
238 237 <table width="100%">
239 238 <tr>
240 239 <td align="left"><%= link_to_remote ('&#171; ' + l(:label_previous)), {:url => @gantt.params_previous, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td>
241 240 <td align="right"><%= link_to_remote (l(:label_next) + ' &#187;'), {:url => @gantt.params_next, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td>
242 241 </tr>
243 242 </table>
244 243
245 244 <p class="other-formats">
246 245 <%= l(:label_export_to) %>
247 246 <span><%= link_to 'PDF', @gantt.params.merge(:format => 'pdf'), :class => 'pdf' %></span>
248 247 <% if @gantt.respond_to?('to_image') %>
249 248 <span><%= link_to 'PNG', @gantt.params.merge(:format => 'png'), :class => 'image' %></span>
250 249 <% end %>
251 250 </p>
252 251 <% end # query.valid? %>
253 252
254 253 <% content_for :sidebar do %>
255 254 <%= render :partial => 'issues/sidebar' %>
256 255 <% end %>
257 256
258 257 <% html_title(l(:label_gantt)) -%>
@@ -1,80 +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(h(@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 <% @project.custom_values.each do |custom_value| %>
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 59 <% planning_links = []
60 planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project)
60 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
61 61 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
62 62 planning_links.compact!
63 63 unless planning_links.empty? %>
64 64 <h3><%= l(:label_planning) %></h3>
65 65 <p><%= planning_links.join(' | ') %></p>
66 66 <% end %>
67 67
68 68 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
69 69 <h3><%= l(:label_spent_time) %></h3>
70 70 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
71 71 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
72 72 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
73 73 <% end %>
74 74 <% end %>
75 75
76 76 <% content_for :header_tags do %>
77 77 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
78 78 <% end %>
79 79
80 80 <% html_title(l(:label_overview)) -%>
@@ -1,149 +1,149
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/activity'
4 4 require 'redmine/mime_type'
5 5 require 'redmine/core_ext'
6 6 require 'redmine/themes'
7 7 require 'redmine/hook'
8 8 require 'redmine/plugin'
9 9
10 10 begin
11 11 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
12 12 rescue LoadError
13 13 # RMagick is not available
14 14 end
15 15
16 16 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
17 17
18 18 # Permissions
19 19 Redmine::AccessControl.map do |map|
20 20 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
21 21 map.permission :search_project, {:search => :index}, :public => true
22 22 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
23 23 map.permission :select_project_modules, {:projects => :modules}, :require => :member
24 24 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
25 25 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
26 26
27 27 map.project_module :issue_tracking do |map|
28 28 # Issue categories
29 29 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
30 30 # Issues
31 31 map.permission :view_issues, {:projects => [:changelog, :roadmap],
32 32 :issues => [:index, :changes, :show, :context_menu],
33 33 :versions => [:show, :status_by],
34 34 :queries => :index,
35 35 :reports => :issue_report}, :public => true
36 36 map.permission :add_issues, {:issues => :new}
37 37 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
38 38 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
39 39 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
40 40 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
41 41 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
42 42 map.permission :move_issues, {:issues => :move}, :require => :loggedin
43 43 map.permission :delete_issues, {:issues => :destroy}, :require => :member
44 44 # Queries
45 45 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
46 46 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
47 47 # Gantt & calendar
48 48 map.permission :view_gantt, :issues => :gantt
49 map.permission :view_calendar, :projects => :calendar
49 map.permission :view_calendar, :issues => :calendar
50 50 # Watchers
51 51 map.permission :view_issue_watchers, {}
52 52 map.permission :add_issue_watchers, {:watchers => :new}
53 53 end
54 54
55 55 map.project_module :time_tracking do |map|
56 56 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
57 57 map.permission :view_time_entries, :timelog => [:details, :report]
58 58 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
59 59 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
60 60 end
61 61
62 62 map.project_module :news do |map|
63 63 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
64 64 map.permission :view_news, {:news => [:index, :show]}, :public => true
65 65 map.permission :comment_news, {:news => :add_comment}
66 66 end
67 67
68 68 map.project_module :documents do |map|
69 69 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
70 70 map.permission :view_documents, :documents => [:index, :show, :download]
71 71 end
72 72
73 73 map.project_module :files do |map|
74 74 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
75 75 map.permission :view_files, :projects => :list_files, :versions => :download
76 76 end
77 77
78 78 map.project_module :wiki do |map|
79 79 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
80 80 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
81 81 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
82 82 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
83 83 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
84 84 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
85 85 end
86 86
87 87 map.project_module :repository do |map|
88 88 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
89 89 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
90 90 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
91 91 end
92 92
93 93 map.project_module :boards do |map|
94 94 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
95 95 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
96 96 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
97 97 map.permission :edit_messages, {:messages => :edit}, :require => :member
98 98 map.permission :delete_messages, {:messages => :destroy}, :require => :member
99 99 end
100 100 end
101 101
102 102 Redmine::MenuManager.map :top_menu do |menu|
103 103 menu.push :home, :home_path, :html => { :class => 'home' }
104 104 menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
105 105 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
106 106 menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }, :last => true
107 107 menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }, :last => true
108 108 end
109 109
110 110 Redmine::MenuManager.map :account_menu do |menu|
111 111 menu.push :login, :signin_path, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
112 112 menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
113 113 menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
114 114 menu.push :logout, :signout_path, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
115 115 end
116 116
117 117 Redmine::MenuManager.map :application_menu do |menu|
118 118 # Empty
119 119 end
120 120
121 121 Redmine::MenuManager.map :project_menu do |menu|
122 122 menu.push :overview, { :controller => 'projects', :action => 'show' }
123 123 menu.push :activity, { :controller => 'projects', :action => 'activity' }
124 124 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
125 125 :if => Proc.new { |p| p.versions.any? }
126 126 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
127 127 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
128 128 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
129 129 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
130 130 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
131 131 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
132 132 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
133 133 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
134 134 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
135 135 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
136 136 menu.push :repository, { :controller => 'repositories', :action => 'show' },
137 137 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
138 138 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
139 139 end
140 140
141 141 Redmine::Activity.map do |activity|
142 142 activity.register :issues, :class_name => %w(Issue Journal)
143 143 activity.register :changesets
144 144 activity.register :news
145 145 activity.register :documents, :class_name => %w(Document Attachment)
146 146 activity.register :files, :class_name => 'Attachment'
147 147 activity.register :wiki_pages, :class_name => 'WikiContent::Version', :default => false
148 148 activity.register :messages, :default => false
149 149 end
@@ -1,622 +1,622
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; padding-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 28 #quick-search {float:right;}
29 29
30 30 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 31 #main-menu ul {margin: 0; padding: 0;}
32 32 #main-menu li {
33 33 float:left;
34 34 list-style-type:none;
35 35 margin: 0px 2px 0px 0px;
36 36 padding: 0px 0px 0px 0px;
37 37 white-space:nowrap;
38 38 }
39 39 #main-menu li a {
40 40 display: block;
41 41 color: #fff;
42 42 text-decoration: none;
43 43 font-weight: bold;
44 44 margin: 0;
45 45 padding: 4px 10px 4px 10px;
46 46 }
47 47 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 48 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49 49
50 50 #main {background-color:#EEEEEE;}
51 51
52 52 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
53 53 * html #sidebar{ width: 17%; }
54 54 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
55 55 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
56 56 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
57 57
58 58 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
59 59 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
60 60 html>body #content { height: auto; min-height: 600px; overflow: auto; }
61 61
62 62 #main.nosidebar #sidebar{ display: none; }
63 63 #main.nosidebar #content{ width: auto; border-right: 0; }
64 64
65 65 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
66 66
67 67 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
68 68 #login-form table td {padding: 6px;}
69 69 #login-form label {font-weight: bold;}
70 70
71 71 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
72 72
73 73 /***** Links *****/
74 74 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
75 75 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
76 76 a img{ border: 0; }
77 77
78 78 a.issue.closed { text-decoration: line-through; }
79 79
80 80 /***** Tables *****/
81 81 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
82 82 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
83 83 table.list td { vertical-align: top; }
84 84 table.list td.id { width: 2%; text-align: center;}
85 85 table.list td.checkbox { width: 15px; padding: 0px;}
86 86
87 87 tr.issue { text-align: center; white-space: nowrap; }
88 88 tr.issue td.subject, tr.issue td.category { white-space: normal; }
89 89 tr.issue td.subject { text-align: left; }
90 90 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
91 91
92 92 tr.entry { border: 1px solid #f8f8f8; }
93 93 tr.entry td { white-space: nowrap; }
94 94 tr.entry td.filename { width: 30%; }
95 95 tr.entry td.size { text-align: right; font-size: 90%; }
96 96 tr.entry td.revision, tr.entry td.author { text-align: center; }
97 97 tr.entry td.age { text-align: right; }
98 98
99 99 tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
100 100 tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
101 101 tr.entry.file td.filename a { margin-left: 16px; }
102 102
103 103 tr.changeset td.author { text-align: center; width: 15%; }
104 104 tr.changeset td.committed_on { text-align: center; width: 15%; }
105 105
106 106 tr.message { height: 2.6em; }
107 107 tr.message td.last_message { font-size: 80%; }
108 108 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
109 109 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
110 110
111 111 tr.user td { width:13%; }
112 112 tr.user td.email { width:18%; }
113 113 tr.user td { white-space: nowrap; }
114 114 tr.user.locked, tr.user.registered { color: #aaa; }
115 115 tr.user.locked a, tr.user.registered a { color: #aaa; }
116 116
117 117 tr.time-entry { text-align: center; white-space: nowrap; }
118 118 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
119 119 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
120 120 td.hours .hours-dec { font-size: 0.9em; }
121 121
122 122 table.list tbody tr:hover { background-color:#ffffdd; }
123 123 table td {padding:2px;}
124 124 table p {margin:0;}
125 125 .odd {background-color:#f6f7f8;}
126 126 .even {background-color: #fff;}
127 127
128 128 .highlight { background-color: #FCFD8D;}
129 129 .highlight.token-1 { background-color: #faa;}
130 130 .highlight.token-2 { background-color: #afa;}
131 131 .highlight.token-3 { background-color: #aaf;}
132 132
133 133 .box{
134 134 padding:6px;
135 135 margin-bottom: 10px;
136 136 background-color:#f6f6f6;
137 137 color:#505050;
138 138 line-height:1.5em;
139 139 border: 1px solid #e4e4e4;
140 140 }
141 141
142 142 div.square {
143 143 border: 1px solid #999;
144 144 float: left;
145 145 margin: .3em .4em 0 .4em;
146 146 overflow: hidden;
147 147 width: .6em; height: .6em;
148 148 }
149 149 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
150 150 .contextual input {font-size:0.9em;}
151 151
152 152 .splitcontentleft{float:left; width:49%;}
153 153 .splitcontentright{float:right; width:49%;}
154 154 form {display: inline;}
155 155 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
156 156 fieldset {border: 1px solid #e4e4e4; margin:0;}
157 157 legend {color: #484848;}
158 158 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
159 159 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
160 160 blockquote blockquote { margin-left: 0;}
161 161 textarea.wiki-edit { width: 99%; }
162 162 li p {margin-top: 0;}
163 163 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
164 164 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
165 165 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
166 166
167 167 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
168 168 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
169 fieldset#filters .buttons { font-size: 0.9em; }
170 169 fieldset#filters table { border-collapse: collapse; }
171 170 fieldset#filters table td { padding: 0; vertical-align: middle; }
172 171 fieldset#filters tr.filter { height: 2em; }
173 172 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
173 .buttons { font-size: 0.9em; }
174 174
175 175 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
176 176 div#issue-changesets .changeset { padding: 4px;}
177 177 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
178 178 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
179 179
180 180 div#activity dl, #search-results { margin-left: 2em; }
181 181 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
182 182 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
183 183 div#activity dt.me .time { border-bottom: 1px solid #999; }
184 184 div#activity dt .time { color: #777; font-size: 80%; }
185 185 div#activity dd .description, #search-results dd .description { font-style: italic; }
186 186 div#activity span.project:after, #search-results span.project:after { content: " -"; }
187 187 div#activity dd span.description, #search-results dd span.description { display:block; }
188 188
189 189 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
190 190 div#search-results-counts {float:right;}
191 191 div#search-results-counts ul { margin-top: 0.5em; }
192 192 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
193 193
194 194 dt.issue { background-image: url(../images/ticket.png); }
195 195 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
196 196 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
197 197 dt.issue-note { background-image: url(../images/ticket_note.png); }
198 198 dt.changeset { background-image: url(../images/changeset.png); }
199 199 dt.news { background-image: url(../images/news.png); }
200 200 dt.message { background-image: url(../images/message.png); }
201 201 dt.reply { background-image: url(../images/comments.png); }
202 202 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
203 203 dt.attachment { background-image: url(../images/attachment.png); }
204 204 dt.document { background-image: url(../images/document.png); }
205 205 dt.project { background-image: url(../images/projects.png); }
206 206
207 207 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
208 208 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
209 209 div#roadmap .wiki h1:first-child { display: none; }
210 210 div#roadmap .wiki h1 { font-size: 120%; }
211 211 div#roadmap .wiki h2 { font-size: 110%; }
212 212
213 213 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
214 214 div#version-summary fieldset { margin-bottom: 1em; }
215 215 div#version-summary .total-hours { text-align: right; }
216 216
217 217 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
218 218 table#time-report tbody tr { font-style: italic; color: #777; }
219 219 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
220 220 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
221 221 table#time-report .hours-dec { font-size: 0.9em; }
222 222
223 223 ul.properties {padding:0; font-size: 0.9em; color: #777;}
224 224 ul.properties li {list-style-type:none;}
225 225 ul.properties li span {font-style:italic;}
226 226
227 227 .total-hours { font-size: 110%; font-weight: bold; }
228 228 .total-hours span.hours-int { font-size: 120%; }
229 229
230 230 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
231 231 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
232 232
233 233 .pagination {font-size: 90%}
234 234 p.pagination {margin-top:8px;}
235 235
236 236 /***** Tabular forms ******/
237 237 .tabular p{
238 238 margin: 0;
239 239 padding: 5px 0 8px 0;
240 240 padding-left: 180px; /*width of left column containing the label elements*/
241 241 height: 1%;
242 242 clear:left;
243 243 }
244 244
245 245 html>body .tabular p {overflow:hidden;}
246 246
247 247 .tabular label{
248 248 font-weight: bold;
249 249 float: left;
250 250 text-align: right;
251 251 margin-left: -180px; /*width of left column*/
252 252 width: 175px; /*width of labels. Should be smaller than left column to create some right
253 253 margin*/
254 254 }
255 255
256 256 .tabular label.floating{
257 257 font-weight: normal;
258 258 margin-left: 0px;
259 259 text-align: left;
260 260 width: 200px;
261 261 }
262 262
263 263 input#time_entry_comments { width: 90%;}
264 264
265 265 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
266 266
267 267 .tabular.settings p{ padding-left: 300px; }
268 268 .tabular.settings label{ margin-left: -300px; width: 295px; }
269 269
270 270 .required {color: #bb0000;}
271 271 .summary {font-style: italic;}
272 272
273 273 #attachments_fields input[type=text] {margin-left: 8px; }
274 274
275 275 div.attachments p { margin:4px 0 2px 0; }
276 276 div.attachments img { vertical-align: middle; }
277 277 div.attachments span.author { font-size: 0.9em; color: #888; }
278 278
279 279 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
280 280 .other-formats span + span:before { content: "| "; }
281 281
282 282 a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
283 283
284 284 /***** Flash & error messages ****/
285 285 #errorExplanation, div.flash, .nodata, .warning {
286 286 padding: 4px 4px 4px 30px;
287 287 margin-bottom: 12px;
288 288 font-size: 1.1em;
289 289 border: 2px solid;
290 290 }
291 291
292 292 div.flash {margin-top: 8px;}
293 293
294 294 div.flash.error, #errorExplanation {
295 295 background: url(../images/false.png) 8px 5px no-repeat;
296 296 background-color: #ffe3e3;
297 297 border-color: #dd0000;
298 298 color: #550000;
299 299 }
300 300
301 301 div.flash.notice {
302 302 background: url(../images/true.png) 8px 5px no-repeat;
303 303 background-color: #dfffdf;
304 304 border-color: #9fcf9f;
305 305 color: #005f00;
306 306 }
307 307
308 308 .nodata, .warning {
309 309 text-align: center;
310 310 background-color: #FFEBC1;
311 311 border-color: #FDBF3B;
312 312 color: #A6750C;
313 313 }
314 314
315 315 #errorExplanation ul { font-size: 0.9em;}
316 316
317 317 /***** Ajax indicator ******/
318 318 #ajax-indicator {
319 319 position: absolute; /* fixed not supported by IE */
320 320 background-color:#eee;
321 321 border: 1px solid #bbb;
322 322 top:35%;
323 323 left:40%;
324 324 width:20%;
325 325 font-weight:bold;
326 326 text-align:center;
327 327 padding:0.6em;
328 328 z-index:100;
329 329 filter:alpha(opacity=50);
330 330 opacity: 0.5;
331 331 }
332 332
333 333 html>body #ajax-indicator { position: fixed; }
334 334
335 335 #ajax-indicator span {
336 336 background-position: 0% 40%;
337 337 background-repeat: no-repeat;
338 338 background-image: url(../images/loading.gif);
339 339 padding-left: 26px;
340 340 vertical-align: bottom;
341 341 }
342 342
343 343 /***** Calendar *****/
344 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
344 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
345 345 table.cal thead th {width: 14%;}
346 346 table.cal tbody tr {height: 100px;}
347 347 table.cal th { background-color:#EEEEEE; padding: 4px; }
348 348 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
349 349 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
350 350 table.cal td.odd p.day-num {color: #bbb;}
351 351 table.cal td.today {background:#ffffdd;}
352 352 table.cal td.today p.day-num {font-weight: bold;}
353 353
354 354 /***** Tooltips ******/
355 355 .tooltip{position:relative;z-index:24;}
356 356 .tooltip:hover{z-index:25;color:#000;}
357 357 .tooltip span.tip{display: none; text-align:left;}
358 358
359 359 div.tooltip:hover span.tip{
360 360 display:block;
361 361 position:absolute;
362 362 top:12px; left:24px; width:270px;
363 363 border:1px solid #555;
364 364 background-color:#fff;
365 365 padding: 4px;
366 366 font-size: 0.8em;
367 367 color:#505050;
368 368 }
369 369
370 370 /***** Progress bar *****/
371 371 table.progress {
372 372 border: 1px solid #D7D7D7;
373 373 border-collapse: collapse;
374 374 border-spacing: 0pt;
375 375 empty-cells: show;
376 376 text-align: center;
377 377 float:left;
378 378 margin: 1px 6px 1px 0px;
379 379 }
380 380
381 381 table.progress td { height: 0.9em; }
382 382 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
383 383 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
384 384 table.progress td.open { background: #FFF none repeat scroll 0%; }
385 385 p.pourcent {font-size: 80%;}
386 386 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
387 387
388 388 /***** Tabs *****/
389 389 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
390 390 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
391 391 #content .tabs>ul { bottom:-1px; } /* others */
392 392 #content .tabs ul li {
393 393 float:left;
394 394 list-style-type:none;
395 395 white-space:nowrap;
396 396 margin-right:8px;
397 397 background:#fff;
398 398 }
399 399 #content .tabs ul li a{
400 400 display:block;
401 401 font-size: 0.9em;
402 402 text-decoration:none;
403 403 line-height:1.3em;
404 404 padding:4px 6px 4px 6px;
405 405 border: 1px solid #ccc;
406 406 border-bottom: 1px solid #bbbbbb;
407 407 background-color: #eeeeee;
408 408 color:#777;
409 409 font-weight:bold;
410 410 }
411 411
412 412 #content .tabs ul li a:hover {
413 413 background-color: #ffffdd;
414 414 text-decoration:none;
415 415 }
416 416
417 417 #content .tabs ul li a.selected {
418 418 background-color: #fff;
419 419 border: 1px solid #bbbbbb;
420 420 border-bottom: 1px solid #fff;
421 421 }
422 422
423 423 #content .tabs ul li a.selected:hover {
424 424 background-color: #fff;
425 425 }
426 426
427 427 /***** Diff *****/
428 428 .diff_out { background: #fcc; }
429 429 .diff_in { background: #cfc; }
430 430
431 431 /***** Wiki *****/
432 432 div.wiki table {
433 433 border: 1px solid #505050;
434 434 border-collapse: collapse;
435 435 margin-bottom: 1em;
436 436 }
437 437
438 438 div.wiki table, div.wiki td, div.wiki th {
439 439 border: 1px solid #bbb;
440 440 padding: 4px;
441 441 }
442 442
443 443 div.wiki .external {
444 444 background-position: 0% 60%;
445 445 background-repeat: no-repeat;
446 446 padding-left: 12px;
447 447 background-image: url(../images/external.png);
448 448 }
449 449
450 450 div.wiki a.new {
451 451 color: #b73535;
452 452 }
453 453
454 454 div.wiki pre {
455 455 margin: 1em 1em 1em 1.6em;
456 456 padding: 2px;
457 457 background-color: #fafafa;
458 458 border: 1px solid #dadada;
459 459 width:95%;
460 460 overflow-x: auto;
461 461 }
462 462
463 463 div.wiki ul.toc {
464 464 background-color: #ffffdd;
465 465 border: 1px solid #e4e4e4;
466 466 padding: 4px;
467 467 line-height: 1.2em;
468 468 margin-bottom: 12px;
469 469 margin-right: 12px;
470 470 margin-left: 0;
471 471 display: table
472 472 }
473 473 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
474 474
475 475 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
476 476 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
477 477 div.wiki ul.toc li { list-style-type:none;}
478 478 div.wiki ul.toc li.heading2 { margin-left: 6px; }
479 479 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
480 480
481 481 div.wiki ul.toc a {
482 482 font-size: 0.9em;
483 483 font-weight: normal;
484 484 text-decoration: none;
485 485 color: #606060;
486 486 }
487 487 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
488 488
489 489 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
490 490 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
491 491 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
492 492
493 493 /***** My page layout *****/
494 494 .block-receiver {
495 495 border:1px dashed #c0c0c0;
496 496 margin-bottom: 20px;
497 497 padding: 15px 0 15px 0;
498 498 }
499 499
500 500 .mypage-box {
501 501 margin:0 0 20px 0;
502 502 color:#505050;
503 503 line-height:1.5em;
504 504 }
505 505
506 506 .handle {
507 507 cursor: move;
508 508 }
509 509
510 510 a.close-icon {
511 511 display:block;
512 512 margin-top:3px;
513 513 overflow:hidden;
514 514 width:12px;
515 515 height:12px;
516 516 background-repeat: no-repeat;
517 517 cursor:pointer;
518 518 background-image:url('../images/close.png');
519 519 }
520 520
521 521 a.close-icon:hover {
522 522 background-image:url('../images/close_hl.png');
523 523 }
524 524
525 525 /***** Gantt chart *****/
526 526 .gantt_hdr {
527 527 position:absolute;
528 528 top:0;
529 529 height:16px;
530 530 border-top: 1px solid #c0c0c0;
531 531 border-bottom: 1px solid #c0c0c0;
532 532 border-right: 1px solid #c0c0c0;
533 533 text-align: center;
534 534 overflow: hidden;
535 535 }
536 536
537 537 .task {
538 538 position: absolute;
539 539 height:8px;
540 540 font-size:0.8em;
541 541 color:#888;
542 542 padding:0;
543 543 margin:0;
544 544 line-height:0.8em;
545 545 }
546 546
547 547 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
548 548 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
549 549 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
550 550 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
551 551
552 552 /***** Icons *****/
553 553 .icon {
554 554 background-position: 0% 40%;
555 555 background-repeat: no-repeat;
556 556 padding-left: 20px;
557 557 padding-top: 2px;
558 558 padding-bottom: 3px;
559 559 }
560 560
561 561 .icon22 {
562 562 background-position: 0% 40%;
563 563 background-repeat: no-repeat;
564 564 padding-left: 26px;
565 565 line-height: 22px;
566 566 vertical-align: middle;
567 567 }
568 568
569 569 .icon-add { background-image: url(../images/add.png); }
570 570 .icon-edit { background-image: url(../images/edit.png); }
571 571 .icon-copy { background-image: url(../images/copy.png); }
572 572 .icon-del { background-image: url(../images/delete.png); }
573 573 .icon-move { background-image: url(../images/move.png); }
574 574 .icon-save { background-image: url(../images/save.png); }
575 575 .icon-cancel { background-image: url(../images/cancel.png); }
576 576 .icon-file { background-image: url(../images/file.png); }
577 577 .icon-folder { background-image: url(../images/folder.png); }
578 578 .open .icon-folder { background-image: url(../images/folder_open.png); }
579 579 .icon-package { background-image: url(../images/package.png); }
580 580 .icon-home { background-image: url(../images/home.png); }
581 581 .icon-user { background-image: url(../images/user.png); }
582 582 .icon-mypage { background-image: url(../images/user_page.png); }
583 583 .icon-admin { background-image: url(../images/admin.png); }
584 584 .icon-projects { background-image: url(../images/projects.png); }
585 585 .icon-logout { background-image: url(../images/logout.png); }
586 586 .icon-help { background-image: url(../images/help.png); }
587 587 .icon-attachment { background-image: url(../images/attachment.png); }
588 588 .icon-index { background-image: url(../images/index.png); }
589 589 .icon-history { background-image: url(../images/history.png); }
590 590 .icon-time { background-image: url(../images/time.png); }
591 591 .icon-stats { background-image: url(../images/stats.png); }
592 592 .icon-warning { background-image: url(../images/warning.png); }
593 593 .icon-fav { background-image: url(../images/fav.png); }
594 594 .icon-fav-off { background-image: url(../images/fav_off.png); }
595 595 .icon-reload { background-image: url(../images/reload.png); }
596 596 .icon-lock { background-image: url(../images/locked.png); }
597 597 .icon-unlock { background-image: url(../images/unlock.png); }
598 598 .icon-checked { background-image: url(../images/true.png); }
599 599 .icon-details { background-image: url(../images/zoom_in.png); }
600 600 .icon-report { background-image: url(../images/report.png); }
601 601 .icon-comment { background-image: url(../images/comment.png); }
602 602
603 603 .icon22-projects { background-image: url(../images/22x22/projects.png); }
604 604 .icon22-users { background-image: url(../images/22x22/users.png); }
605 605 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
606 606 .icon22-role { background-image: url(../images/22x22/role.png); }
607 607 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
608 608 .icon22-options { background-image: url(../images/22x22/options.png); }
609 609 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
610 610 .icon22-authent { background-image: url(../images/22x22/authent.png); }
611 611 .icon22-info { background-image: url(../images/22x22/info.png); }
612 612 .icon22-comment { background-image: url(../images/22x22/comment.png); }
613 613 .icon22-package { background-image: url(../images/22x22/package.png); }
614 614 .icon22-settings { background-image: url(../images/22x22/settings.png); }
615 615 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
616 616
617 617 /***** Media print specific styles *****/
618 618 @media print {
619 619 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
620 620 #main { background: #fff; }
621 621 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
622 622 }
@@ -1,682 +1,689
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
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 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :issues,
30 30 :issue_statuses,
31 31 :trackers,
32 32 :projects_trackers,
33 33 :issue_categories,
34 34 :enabled_modules,
35 35 :enumerations,
36 36 :attachments,
37 37 :workflows,
38 38 :custom_fields,
39 39 :custom_values,
40 40 :custom_fields_trackers,
41 41 :time_entries,
42 42 :journals,
43 43 :journal_details
44 44
45 45 def setup
46 46 @controller = IssuesController.new
47 47 @request = ActionController::TestRequest.new
48 48 @response = ActionController::TestResponse.new
49 49 User.current = nil
50 50 end
51 51
52 52 def test_index
53 53 get :index
54 54 assert_response :success
55 55 assert_template 'index.rhtml'
56 56 assert_not_nil assigns(:issues)
57 57 assert_nil assigns(:project)
58 58 assert_tag :tag => 'a', :content => /Can't print recipes/
59 59 assert_tag :tag => 'a', :content => /Subproject issue/
60 60 # private projects hidden
61 61 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
62 62 assert_no_tag :tag => 'a', :content => /Issue on project 2/
63 63 end
64 64
65 65 def test_index_with_project
66 66 Setting.display_subprojects_issues = 0
67 67 get :index, :project_id => 1
68 68 assert_response :success
69 69 assert_template 'index.rhtml'
70 70 assert_not_nil assigns(:issues)
71 71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 72 assert_no_tag :tag => 'a', :content => /Subproject issue/
73 73 end
74 74
75 75 def test_index_with_project_and_subprojects
76 76 Setting.display_subprojects_issues = 1
77 77 get :index, :project_id => 1
78 78 assert_response :success
79 79 assert_template 'index.rhtml'
80 80 assert_not_nil assigns(:issues)
81 81 assert_tag :tag => 'a', :content => /Can't print recipes/
82 82 assert_tag :tag => 'a', :content => /Subproject issue/
83 83 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
84 84 end
85 85
86 86 def test_index_with_project_and_subprojects_should_show_private_subprojects
87 87 @request.session[:user_id] = 2
88 88 Setting.display_subprojects_issues = 1
89 89 get :index, :project_id => 1
90 90 assert_response :success
91 91 assert_template 'index.rhtml'
92 92 assert_not_nil assigns(:issues)
93 93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 94 assert_tag :tag => 'a', :content => /Subproject issue/
95 95 assert_tag :tag => 'a', :content => /Issue of a private subproject/
96 96 end
97 97
98 98 def test_index_with_project_and_filter
99 99 get :index, :project_id => 1, :set_filter => 1
100 100 assert_response :success
101 101 assert_template 'index.rhtml'
102 102 assert_not_nil assigns(:issues)
103 103 end
104 104
105 105 def test_index_csv_with_project
106 106 get :index, :format => 'csv'
107 107 assert_response :success
108 108 assert_not_nil assigns(:issues)
109 109 assert_equal 'text/csv', @response.content_type
110 110
111 111 get :index, :project_id => 1, :format => 'csv'
112 112 assert_response :success
113 113 assert_not_nil assigns(:issues)
114 114 assert_equal 'text/csv', @response.content_type
115 115 end
116 116
117 117 def test_index_pdf
118 118 get :index, :format => 'pdf'
119 119 assert_response :success
120 120 assert_not_nil assigns(:issues)
121 121 assert_equal 'application/pdf', @response.content_type
122 122
123 123 get :index, :project_id => 1, :format => 'pdf'
124 124 assert_response :success
125 125 assert_not_nil assigns(:issues)
126 126 assert_equal 'application/pdf', @response.content_type
127 127 end
128 128
129 129 def test_gantt
130 130 get :gantt, :project_id => 1
131 131 assert_response :success
132 132 assert_template 'gantt.rhtml'
133 133 assert_not_nil assigns(:gantt)
134 134 events = assigns(:gantt).events
135 135 assert_not_nil events
136 136 # Issue with start and due dates
137 137 i = Issue.find(1)
138 138 assert_not_nil i.due_date
139 139 assert events.include?(Issue.find(1))
140 140 # Issue with without due date but targeted to a version with date
141 141 i = Issue.find(2)
142 142 assert_nil i.due_date
143 143 assert events.include?(i)
144 144 end
145 145
146 146 def test_gantt_export_to_pdf
147 147 get :gantt, :project_id => 1, :format => 'pdf'
148 148 assert_response :success
149 149 assert_template 'gantt.rfpdf'
150 150 assert_equal 'application/pdf', @response.content_type
151 151 assert_not_nil assigns(:gantt)
152 152 end
153 153
154 154 if Object.const_defined?(:Magick)
155 155 def test_gantt_image
156 156 get :gantt, :project_id => 1, :format => 'png'
157 157 assert_response :success
158 158 assert_equal 'image/png', @response.content_type
159 159 end
160 160 else
161 161 puts "RMagick not installed. Skipping tests !!!"
162 162 end
163 163
164 def test_calendar
165 get :calendar, :project_id => 1
166 assert_response :success
167 assert_template 'calendar'
168 assert_not_nil assigns(:calendar)
169 end
170
164 171 def test_changes
165 172 get :changes, :project_id => 1
166 173 assert_response :success
167 174 assert_not_nil assigns(:journals)
168 175 assert_equal 'application/atom+xml', @response.content_type
169 176 end
170 177
171 178 def test_show_by_anonymous
172 179 get :show, :id => 1
173 180 assert_response :success
174 181 assert_template 'show.rhtml'
175 182 assert_not_nil assigns(:issue)
176 183 assert_equal Issue.find(1), assigns(:issue)
177 184
178 185 # anonymous role is allowed to add a note
179 186 assert_tag :tag => 'form',
180 187 :descendant => { :tag => 'fieldset',
181 188 :child => { :tag => 'legend',
182 189 :content => /Notes/ } }
183 190 end
184 191
185 192 def test_show_by_manager
186 193 @request.session[:user_id] = 2
187 194 get :show, :id => 1
188 195 assert_response :success
189 196
190 197 assert_tag :tag => 'form',
191 198 :descendant => { :tag => 'fieldset',
192 199 :child => { :tag => 'legend',
193 200 :content => /Change properties/ } },
194 201 :descendant => { :tag => 'fieldset',
195 202 :child => { :tag => 'legend',
196 203 :content => /Log time/ } },
197 204 :descendant => { :tag => 'fieldset',
198 205 :child => { :tag => 'legend',
199 206 :content => /Notes/ } }
200 207 end
201 208
202 209 def test_get_new
203 210 @request.session[:user_id] = 2
204 211 get :new, :project_id => 1, :tracker_id => 1
205 212 assert_response :success
206 213 assert_template 'new'
207 214
208 215 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
209 216 :value => 'Default string' }
210 217 end
211 218
212 219 def test_get_new_without_tracker_id
213 220 @request.session[:user_id] = 2
214 221 get :new, :project_id => 1
215 222 assert_response :success
216 223 assert_template 'new'
217 224
218 225 issue = assigns(:issue)
219 226 assert_not_nil issue
220 227 assert_equal Project.find(1).trackers.first, issue.tracker
221 228 end
222 229
223 230 def test_update_new_form
224 231 @request.session[:user_id] = 2
225 232 xhr :post, :new, :project_id => 1,
226 233 :issue => {:tracker_id => 2,
227 234 :subject => 'This is the test_new issue',
228 235 :description => 'This is the description',
229 236 :priority_id => 5}
230 237 assert_response :success
231 238 assert_template 'new'
232 239 end
233 240
234 241 def test_post_new
235 242 @request.session[:user_id] = 2
236 243 post :new, :project_id => 1,
237 244 :issue => {:tracker_id => 3,
238 245 :subject => 'This is the test_new issue',
239 246 :description => 'This is the description',
240 247 :priority_id => 5,
241 248 :estimated_hours => '',
242 249 :custom_field_values => {'2' => 'Value for field 2'}}
243 250 assert_redirected_to 'issues/show'
244 251
245 252 issue = Issue.find_by_subject('This is the test_new issue')
246 253 assert_not_nil issue
247 254 assert_equal 2, issue.author_id
248 255 assert_equal 3, issue.tracker_id
249 256 assert_nil issue.estimated_hours
250 257 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
251 258 assert_not_nil v
252 259 assert_equal 'Value for field 2', v.value
253 260 end
254 261
255 262 def test_post_new_without_custom_fields_param
256 263 @request.session[:user_id] = 2
257 264 post :new, :project_id => 1,
258 265 :issue => {:tracker_id => 1,
259 266 :subject => 'This is the test_new issue',
260 267 :description => 'This is the description',
261 268 :priority_id => 5}
262 269 assert_redirected_to 'issues/show'
263 270 end
264 271
265 272 def test_post_new_with_required_custom_field_and_without_custom_fields_param
266 273 field = IssueCustomField.find_by_name('Database')
267 274 field.update_attribute(:is_required, true)
268 275
269 276 @request.session[:user_id] = 2
270 277 post :new, :project_id => 1,
271 278 :issue => {:tracker_id => 1,
272 279 :subject => 'This is the test_new issue',
273 280 :description => 'This is the description',
274 281 :priority_id => 5}
275 282 assert_response :success
276 283 assert_template 'new'
277 284 issue = assigns(:issue)
278 285 assert_not_nil issue
279 286 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
280 287 end
281 288
282 289 def test_post_should_preserve_fields_values_on_validation_failure
283 290 @request.session[:user_id] = 2
284 291 post :new, :project_id => 1,
285 292 :issue => {:tracker_id => 1,
286 293 :subject => 'This is the test_new issue',
287 294 # empty description
288 295 :description => '',
289 296 :priority_id => 6,
290 297 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
291 298 assert_response :success
292 299 assert_template 'new'
293 300
294 301 assert_tag :input, :attributes => { :name => 'issue[subject]',
295 302 :value => 'This is the test_new issue' }
296 303 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
297 304 :child => { :tag => 'option', :attributes => { :selected => 'selected',
298 305 :value => '6' },
299 306 :content => 'High' }
300 307 # Custom fields
301 308 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
302 309 :child => { :tag => 'option', :attributes => { :selected => 'selected',
303 310 :value => 'Oracle' },
304 311 :content => 'Oracle' }
305 312 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
306 313 :value => 'Value for field 2'}
307 314 end
308 315
309 316 def test_copy_issue
310 317 @request.session[:user_id] = 2
311 318 get :new, :project_id => 1, :copy_from => 1
312 319 assert_template 'new'
313 320 assert_not_nil assigns(:issue)
314 321 orig = Issue.find(1)
315 322 assert_equal orig.subject, assigns(:issue).subject
316 323 end
317 324
318 325 def test_get_edit
319 326 @request.session[:user_id] = 2
320 327 get :edit, :id => 1
321 328 assert_response :success
322 329 assert_template 'edit'
323 330 assert_not_nil assigns(:issue)
324 331 assert_equal Issue.find(1), assigns(:issue)
325 332 end
326 333
327 334 def test_get_edit_with_params
328 335 @request.session[:user_id] = 2
329 336 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
330 337 assert_response :success
331 338 assert_template 'edit'
332 339
333 340 issue = assigns(:issue)
334 341 assert_not_nil issue
335 342
336 343 assert_equal 5, issue.status_id
337 344 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
338 345 :child => { :tag => 'option',
339 346 :content => 'Closed',
340 347 :attributes => { :selected => 'selected' } }
341 348
342 349 assert_equal 7, issue.priority_id
343 350 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
344 351 :child => { :tag => 'option',
345 352 :content => 'Urgent',
346 353 :attributes => { :selected => 'selected' } }
347 354 end
348 355
349 356 def test_reply_to_issue
350 357 @request.session[:user_id] = 2
351 358 get :reply, :id => 1
352 359 assert_response :success
353 360 assert_select_rjs :show, "update"
354 361 end
355 362
356 363 def test_reply_to_note
357 364 @request.session[:user_id] = 2
358 365 get :reply, :id => 1, :journal_id => 2
359 366 assert_response :success
360 367 assert_select_rjs :show, "update"
361 368 end
362 369
363 370 def test_post_edit_without_custom_fields_param
364 371 @request.session[:user_id] = 2
365 372 ActionMailer::Base.deliveries.clear
366 373
367 374 issue = Issue.find(1)
368 375 assert_equal '125', issue.custom_value_for(2).value
369 376 old_subject = issue.subject
370 377 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
371 378
372 379 assert_difference('Journal.count') do
373 380 assert_difference('JournalDetail.count', 2) do
374 381 post :edit, :id => 1, :issue => {:subject => new_subject,
375 382 :priority_id => '6',
376 383 :category_id => '1' # no change
377 384 }
378 385 end
379 386 end
380 387 assert_redirected_to 'issues/show/1'
381 388 issue.reload
382 389 assert_equal new_subject, issue.subject
383 390 # Make sure custom fields were not cleared
384 391 assert_equal '125', issue.custom_value_for(2).value
385 392
386 393 mail = ActionMailer::Base.deliveries.last
387 394 assert_kind_of TMail::Mail, mail
388 395 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
389 396 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
390 397 end
391 398
392 399 def test_post_edit_with_custom_field_change
393 400 @request.session[:user_id] = 2
394 401 issue = Issue.find(1)
395 402 assert_equal '125', issue.custom_value_for(2).value
396 403
397 404 assert_difference('Journal.count') do
398 405 assert_difference('JournalDetail.count', 3) do
399 406 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
400 407 :priority_id => '6',
401 408 :category_id => '1', # no change
402 409 :custom_field_values => { '2' => 'New custom value' }
403 410 }
404 411 end
405 412 end
406 413 assert_redirected_to 'issues/show/1'
407 414 issue.reload
408 415 assert_equal 'New custom value', issue.custom_value_for(2).value
409 416
410 417 mail = ActionMailer::Base.deliveries.last
411 418 assert_kind_of TMail::Mail, mail
412 419 assert mail.body.include?("Searchable field changed from 125 to New custom value")
413 420 end
414 421
415 422 def test_post_edit_with_status_and_assignee_change
416 423 issue = Issue.find(1)
417 424 assert_equal 1, issue.status_id
418 425 @request.session[:user_id] = 2
419 426 assert_difference('TimeEntry.count', 0) do
420 427 post :edit,
421 428 :id => 1,
422 429 :issue => { :status_id => 2, :assigned_to_id => 3 },
423 430 :notes => 'Assigned to dlopper',
424 431 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
425 432 end
426 433 assert_redirected_to 'issues/show/1'
427 434 issue.reload
428 435 assert_equal 2, issue.status_id
429 436 j = issue.journals.find(:first, :order => 'id DESC')
430 437 assert_equal 'Assigned to dlopper', j.notes
431 438 assert_equal 2, j.details.size
432 439
433 440 mail = ActionMailer::Base.deliveries.last
434 441 assert mail.body.include?("Status changed from New to Assigned")
435 442 end
436 443
437 444 def test_post_edit_with_note_only
438 445 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
439 446 # anonymous user
440 447 post :edit,
441 448 :id => 1,
442 449 :notes => notes
443 450 assert_redirected_to 'issues/show/1'
444 451 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
445 452 assert_equal notes, j.notes
446 453 assert_equal 0, j.details.size
447 454 assert_equal User.anonymous, j.user
448 455
449 456 mail = ActionMailer::Base.deliveries.last
450 457 assert mail.body.include?(notes)
451 458 end
452 459
453 460 def test_post_edit_with_note_and_spent_time
454 461 @request.session[:user_id] = 2
455 462 spent_hours_before = Issue.find(1).spent_hours
456 463 assert_difference('TimeEntry.count') do
457 464 post :edit,
458 465 :id => 1,
459 466 :notes => '2.5 hours added',
460 467 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
461 468 end
462 469 assert_redirected_to 'issues/show/1'
463 470
464 471 issue = Issue.find(1)
465 472
466 473 j = issue.journals.find(:first, :order => 'id DESC')
467 474 assert_equal '2.5 hours added', j.notes
468 475 assert_equal 0, j.details.size
469 476
470 477 t = issue.time_entries.find(:first, :order => 'id DESC')
471 478 assert_not_nil t
472 479 assert_equal 2.5, t.hours
473 480 assert_equal spent_hours_before + 2.5, issue.spent_hours
474 481 end
475 482
476 483 def test_post_edit_with_attachment_only
477 484 set_tmp_attachments_directory
478 485
479 486 # anonymous user
480 487 post :edit,
481 488 :id => 1,
482 489 :notes => '',
483 490 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
484 491 assert_redirected_to 'issues/show/1'
485 492 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
486 493 assert j.notes.blank?
487 494 assert_equal 1, j.details.size
488 495 assert_equal 'testfile.txt', j.details.first.value
489 496 assert_equal User.anonymous, j.user
490 497
491 498 mail = ActionMailer::Base.deliveries.last
492 499 assert mail.body.include?('testfile.txt')
493 500 end
494 501
495 502 def test_post_edit_with_no_change
496 503 issue = Issue.find(1)
497 504 issue.journals.clear
498 505 ActionMailer::Base.deliveries.clear
499 506
500 507 post :edit,
501 508 :id => 1,
502 509 :notes => ''
503 510 assert_redirected_to 'issues/show/1'
504 511
505 512 issue.reload
506 513 assert issue.journals.empty?
507 514 # No email should be sent
508 515 assert ActionMailer::Base.deliveries.empty?
509 516 end
510 517
511 518 def test_bulk_edit
512 519 @request.session[:user_id] = 2
513 520 # update issues priority
514 521 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
515 522 assert_response 302
516 523 # check that the issues were updated
517 524 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
518 525 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
519 526 end
520 527
521 528 def test_bulk_unassign
522 529 assert_not_nil Issue.find(2).assigned_to
523 530 @request.session[:user_id] = 2
524 531 # unassign issues
525 532 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
526 533 assert_response 302
527 534 # check that the issues were updated
528 535 assert_nil Issue.find(2).assigned_to
529 536 end
530 537
531 538 def test_move_one_issue_to_another_project
532 539 @request.session[:user_id] = 1
533 540 post :move, :id => 1, :new_project_id => 2
534 541 assert_redirected_to 'projects/ecookbook/issues'
535 542 assert_equal 2, Issue.find(1).project_id
536 543 end
537 544
538 545 def test_bulk_move_to_another_project
539 546 @request.session[:user_id] = 1
540 547 post :move, :ids => [1, 2], :new_project_id => 2
541 548 assert_redirected_to 'projects/ecookbook/issues'
542 549 # Issues moved to project 2
543 550 assert_equal 2, Issue.find(1).project_id
544 551 assert_equal 2, Issue.find(2).project_id
545 552 # No tracker change
546 553 assert_equal 1, Issue.find(1).tracker_id
547 554 assert_equal 2, Issue.find(2).tracker_id
548 555 end
549 556
550 557 def test_bulk_move_to_another_tracker
551 558 @request.session[:user_id] = 1
552 559 post :move, :ids => [1, 2], :new_tracker_id => 2
553 560 assert_redirected_to 'projects/ecookbook/issues'
554 561 assert_equal 2, Issue.find(1).tracker_id
555 562 assert_equal 2, Issue.find(2).tracker_id
556 563 end
557 564
558 565 def test_context_menu_one_issue
559 566 @request.session[:user_id] = 2
560 567 get :context_menu, :ids => [1]
561 568 assert_response :success
562 569 assert_template 'context_menu'
563 570 assert_tag :tag => 'a', :content => 'Edit',
564 571 :attributes => { :href => '/issues/edit/1',
565 572 :class => 'icon-edit' }
566 573 assert_tag :tag => 'a', :content => 'Closed',
567 574 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
568 575 :class => '' }
569 576 assert_tag :tag => 'a', :content => 'Immediate',
570 577 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
571 578 :class => '' }
572 579 assert_tag :tag => 'a', :content => 'Dave Lopper',
573 580 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
574 581 :class => '' }
575 582 assert_tag :tag => 'a', :content => 'Copy',
576 583 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
577 584 :class => 'icon-copy' }
578 585 assert_tag :tag => 'a', :content => 'Move',
579 586 :attributes => { :href => '/issues/move?ids%5B%5D=1',
580 587 :class => 'icon-move' }
581 588 assert_tag :tag => 'a', :content => 'Delete',
582 589 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
583 590 :class => 'icon-del' }
584 591 end
585 592
586 593 def test_context_menu_one_issue_by_anonymous
587 594 get :context_menu, :ids => [1]
588 595 assert_response :success
589 596 assert_template 'context_menu'
590 597 assert_tag :tag => 'a', :content => 'Delete',
591 598 :attributes => { :href => '#',
592 599 :class => 'icon-del disabled' }
593 600 end
594 601
595 602 def test_context_menu_multiple_issues_of_same_project
596 603 @request.session[:user_id] = 2
597 604 get :context_menu, :ids => [1, 2]
598 605 assert_response :success
599 606 assert_template 'context_menu'
600 607 assert_tag :tag => 'a', :content => 'Edit',
601 608 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
602 609 :class => 'icon-edit' }
603 610 assert_tag :tag => 'a', :content => 'Immediate',
604 611 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
605 612 :class => '' }
606 613 assert_tag :tag => 'a', :content => 'Dave Lopper',
607 614 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
608 615 :class => '' }
609 616 assert_tag :tag => 'a', :content => 'Move',
610 617 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
611 618 :class => 'icon-move' }
612 619 assert_tag :tag => 'a', :content => 'Delete',
613 620 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
614 621 :class => 'icon-del' }
615 622 end
616 623
617 624 def test_context_menu_multiple_issues_of_different_project
618 625 @request.session[:user_id] = 2
619 626 get :context_menu, :ids => [1, 2, 4]
620 627 assert_response :success
621 628 assert_template 'context_menu'
622 629 assert_tag :tag => 'a', :content => 'Delete',
623 630 :attributes => { :href => '#',
624 631 :class => 'icon-del disabled' }
625 632 end
626 633
627 634 def test_destroy_issue_with_no_time_entries
628 635 assert_nil TimeEntry.find_by_issue_id(2)
629 636 @request.session[:user_id] = 2
630 637 post :destroy, :id => 2
631 638 assert_redirected_to 'projects/ecookbook/issues'
632 639 assert_nil Issue.find_by_id(2)
633 640 end
634 641
635 642 def test_destroy_issues_with_time_entries
636 643 @request.session[:user_id] = 2
637 644 post :destroy, :ids => [1, 3]
638 645 assert_response :success
639 646 assert_template 'destroy'
640 647 assert_not_nil assigns(:hours)
641 648 assert Issue.find_by_id(1) && Issue.find_by_id(3)
642 649 end
643 650
644 651 def test_destroy_issues_and_destroy_time_entries
645 652 @request.session[:user_id] = 2
646 653 post :destroy, :ids => [1, 3], :todo => 'destroy'
647 654 assert_redirected_to 'projects/ecookbook/issues'
648 655 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
649 656 assert_nil TimeEntry.find_by_id([1, 2])
650 657 end
651 658
652 659 def test_destroy_issues_and_assign_time_entries_to_project
653 660 @request.session[:user_id] = 2
654 661 post :destroy, :ids => [1, 3], :todo => 'nullify'
655 662 assert_redirected_to 'projects/ecookbook/issues'
656 663 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
657 664 assert_nil TimeEntry.find(1).issue_id
658 665 assert_nil TimeEntry.find(2).issue_id
659 666 end
660 667
661 668 def test_destroy_issues_and_reassign_time_entries_to_another_issue
662 669 @request.session[:user_id] = 2
663 670 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
664 671 assert_redirected_to 'projects/ecookbook/issues'
665 672 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
666 673 assert_equal 2, TimeEntry.find(1).issue_id
667 674 assert_equal 2, TimeEntry.find(2).issue_id
668 675 end
669 676
670 677 def test_destroy_attachment
671 678 issue = Issue.find(3)
672 679 a = issue.attachments.size
673 680 @request.session[:user_id] = 2
674 681 post :destroy_attachment, :id => 3, :attachment_id => 1
675 682 assert_redirected_to 'issues/show/3'
676 683 assert_nil Attachment.find_by_id(1)
677 684 issue.reload
678 685 assert_equal((a-1), issue.attachments.size)
679 686 j = issue.journals.find(:first, :order => 'created_on DESC')
680 687 assert_equal 'attachment', j.details.first.property
681 688 end
682 689 end
@@ -1,297 +1,273
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
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
28 28 def setup
29 29 @controller = ProjectsController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 @request.session[:user_id] = nil
33 33 end
34 34
35 35 def test_index
36 36 get :index
37 37 assert_response :success
38 38 assert_template 'index'
39 39 assert_not_nil assigns(:project_tree)
40 40 # Root project as hash key
41 41 assert assigns(:project_tree).keys.include?(Project.find(1))
42 42 # Subproject in corresponding value
43 43 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
44 44 end
45 45
46 46 def test_index_atom
47 47 get :index, :format => 'atom'
48 48 assert_response :success
49 49 assert_template 'common/feed.atom.rxml'
50 50 assert_select 'feed>title', :text => 'Redmine: Latest projects'
51 51 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
52 52 end
53 53
54 54 def test_show_by_id
55 55 get :show, :id => 1
56 56 assert_response :success
57 57 assert_template 'show'
58 58 assert_not_nil assigns(:project)
59 59 end
60 60
61 61 def test_show_by_identifier
62 62 get :show, :id => 'ecookbook'
63 63 assert_response :success
64 64 assert_template 'show'
65 65 assert_not_nil assigns(:project)
66 66 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
67 67 end
68 68
69 69 def test_private_subprojects_hidden
70 70 get :show, :id => 'ecookbook'
71 71 assert_response :success
72 72 assert_template 'show'
73 73 assert_no_tag :tag => 'a', :content => /Private child/
74 74 end
75 75
76 76 def test_private_subprojects_visible
77 77 @request.session[:user_id] = 2 # manager who is a member of the private subproject
78 78 get :show, :id => 'ecookbook'
79 79 assert_response :success
80 80 assert_template 'show'
81 81 assert_tag :tag => 'a', :content => /Private child/
82 82 end
83 83
84 84 def test_settings
85 85 @request.session[:user_id] = 2 # manager
86 86 get :settings, :id => 1
87 87 assert_response :success
88 88 assert_template 'settings'
89 89 end
90 90
91 91 def test_edit
92 92 @request.session[:user_id] = 2 # manager
93 93 post :edit, :id => 1, :project => {:name => 'Test changed name',
94 94 :issue_custom_field_ids => ['']}
95 95 assert_redirected_to 'projects/settings/ecookbook'
96 96 project = Project.find(1)
97 97 assert_equal 'Test changed name', project.name
98 98 end
99 99
100 100 def test_get_destroy
101 101 @request.session[:user_id] = 1 # admin
102 102 get :destroy, :id => 1
103 103 assert_response :success
104 104 assert_template 'destroy'
105 105 assert_not_nil Project.find_by_id(1)
106 106 end
107 107
108 108 def test_post_destroy
109 109 @request.session[:user_id] = 1 # admin
110 110 post :destroy, :id => 1, :confirm => 1
111 111 assert_redirected_to 'admin/projects'
112 112 assert_nil Project.find_by_id(1)
113 113 end
114 114
115 115 def test_list_files
116 116 get :list_files, :id => 1
117 117 assert_response :success
118 118 assert_template 'list_files'
119 119 assert_not_nil assigns(:versions)
120 120 end
121 121
122 122 def test_changelog
123 123 get :changelog, :id => 1
124 124 assert_response :success
125 125 assert_template 'changelog'
126 126 assert_not_nil assigns(:versions)
127 127 end
128 128
129 129 def test_roadmap
130 130 get :roadmap, :id => 1
131 131 assert_response :success
132 132 assert_template 'roadmap'
133 133 assert_not_nil assigns(:versions)
134 134 # Version with no date set appears
135 135 assert assigns(:versions).include?(Version.find(3))
136 136 # Completed version doesn't appear
137 137 assert !assigns(:versions).include?(Version.find(1))
138 138 end
139 139
140 140 def test_roadmap_with_completed_versions
141 141 get :roadmap, :id => 1, :completed => 1
142 142 assert_response :success
143 143 assert_template 'roadmap'
144 144 assert_not_nil assigns(:versions)
145 145 # Version with no date set appears
146 146 assert assigns(:versions).include?(Version.find(3))
147 147 # Completed version appears
148 148 assert assigns(:versions).include?(Version.find(1))
149 149 end
150 150
151 151 def test_project_activity
152 152 get :activity, :id => 1, :with_subprojects => 0
153 153 assert_response :success
154 154 assert_template 'activity'
155 155 assert_not_nil assigns(:events_by_day)
156 156
157 157 assert_tag :tag => "h3",
158 158 :content => /#{2.days.ago.to_date.day}/,
159 159 :sibling => { :tag => "dl",
160 160 :child => { :tag => "dt",
161 161 :attributes => { :class => /issue-edit/ },
162 162 :child => { :tag => "a",
163 163 :content => /(#{IssueStatus.find(2).name})/,
164 164 }
165 165 }
166 166 }
167 167 end
168 168
169 169 def test_previous_project_activity
170 170 get :activity, :id => 1, :from => 3.days.ago.to_date
171 171 assert_response :success
172 172 assert_template 'activity'
173 173 assert_not_nil assigns(:events_by_day)
174 174
175 175 assert_tag :tag => "h3",
176 176 :content => /#{3.day.ago.to_date.day}/,
177 177 :sibling => { :tag => "dl",
178 178 :child => { :tag => "dt",
179 179 :attributes => { :class => /issue/ },
180 180 :child => { :tag => "a",
181 181 :content => /#{Issue.find(1).subject}/,
182 182 }
183 183 }
184 184 }
185 185 end
186 186
187 187 def test_global_activity
188 188 get :activity
189 189 assert_response :success
190 190 assert_template 'activity'
191 191 assert_not_nil assigns(:events_by_day)
192 192
193 193 assert_tag :tag => "h3",
194 194 :content => /#{5.day.ago.to_date.day}/,
195 195 :sibling => { :tag => "dl",
196 196 :child => { :tag => "dt",
197 197 :attributes => { :class => /issue/ },
198 198 :child => { :tag => "a",
199 199 :content => /#{Issue.find(5).subject}/,
200 200 }
201 201 }
202 202 }
203 203 end
204 204
205 205 def test_activity_atom_feed
206 206 get :activity, :format => 'atom'
207 207 assert_response :success
208 208 assert_template 'common/feed.atom.rxml'
209 209 end
210 210
211 def test_calendar
212 get :calendar, :id => 1
213 assert_response :success
214 assert_template 'calendar'
215 assert_not_nil assigns(:calendar)
216 end
217
218 def test_calendar_with_subprojects_should_not_show_private_subprojects
219 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
220 assert_response :success
221 assert_template 'calendar'
222 assert_not_nil assigns(:calendar)
223 assert_no_tag :tag => 'a', :content => /#6/
224 end
225
226 def test_calendar_with_subprojects_should_show_private_subprojects
227 @request.session[:user_id] = 2
228 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
229 assert_response :success
230 assert_template 'calendar'
231 assert_not_nil assigns(:calendar)
232 assert_tag :tag => 'a', :content => /#6/
233 end
234
235 211 def test_archive
236 212 @request.session[:user_id] = 1 # admin
237 213 post :archive, :id => 1
238 214 assert_redirected_to 'admin/projects'
239 215 assert !Project.find(1).active?
240 216 end
241 217
242 218 def test_unarchive
243 219 @request.session[:user_id] = 1 # admin
244 220 Project.find(1).archive
245 221 post :unarchive, :id => 1
246 222 assert_redirected_to 'admin/projects'
247 223 assert Project.find(1).active?
248 224 end
249 225
250 226 def test_project_menu
251 227 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
252 228 Redmine::MenuManager.map :project_menu do |menu|
253 229 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
254 230 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
255 231 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
256 232 end
257 233
258 234 get :show, :id => 1
259 235 assert_tag :div, :attributes => { :id => 'main-menu' },
260 236 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo' } }
261 237
262 238 assert_tag :div, :attributes => { :id => 'main-menu' },
263 239 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar' },
264 240 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
265 241
266 242 assert_tag :div, :attributes => { :id => 'main-menu' },
267 243 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' },
268 244 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
269 245
270 246 # Remove the menu items
271 247 Redmine::MenuManager.map :project_menu do |menu|
272 248 menu.delete :foo
273 249 menu.delete :bar
274 250 menu.delete :hello
275 251 end
276 252 end
277 253 end
278 254
279 255 # A hook that is manually registered later
280 256 class ProjectBasedTemplate < Redmine::Hook::ViewListener
281 257 def view_layouts_base_html_head(context)
282 258 # Adds a project stylesheet
283 259 stylesheet_link_tag(context[:project].identifier) if context[:project]
284 260 end
285 261 end
286 262 # Don't use this hook now
287 263 Redmine::Hook.clear_listeners
288 264
289 265 def test_hook_response
290 266 Redmine::Hook.add_listener(ProjectBasedTemplate)
291 267 get :show, :id => 1
292 268 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
293 269 :parent => {:tag => 'head'}
294 270
295 271 Redmine::Hook.clear_listeners
296 272 end
297 273 end
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now