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