##// END OF EJS Templates
Allows multiple roles on the same project (#706). Prerequisite for user groups feature....
Jean-Philippe Lang -
r2627:7dccf9fda6f3
parent child
Show More
@@ -0,0 +1,27
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class MemberRole < ActiveRecord::Base
19 belongs_to :member
20 belongs_to :role
21
22 validates_presence_of :role
23
24 def validate
25 errors.add :role_id, :invalid if role && !role.member?
26 end
27 end
@@ -0,0 +1,12
1 class CreateMemberRoles < ActiveRecord::Migration
2 def self.up
3 create_table :member_roles do |t|
4 t.column :member_id, :integer, :null => false
5 t.column :role_id, :integer, :null => false
6 end
7 end
8
9 def self.down
10 drop_table :member_roles
11 end
12 end
@@ -0,0 +1,11
1 class PopulateMemberRoles < ActiveRecord::Migration
2 def self.up
3 Member.find(:all).each do |member|
4 MemberRole.create!(:member_id => member.id, :role_id => member.role_id)
5 end
6 end
7
8 def self.down
9 MemberRole.delete_all
10 end
11 end
@@ -0,0 +1,9
1 class DropMembersRoleId < ActiveRecord::Migration
2 def self.up
3 remove_column :members, :role_id
4 end
5
6 def self.down
7 raise IrreversibleMigration
8 end
9 end
@@ -0,0 +1,23
1 ---
2 member_roles_001:
3 id: 1
4 role_id: 1
5 member_id: 1
6 member_roles_002:
7 id: 2
8 role_id: 2
9 member_id: 2
10 member_roles_003:
11 id: 3
12 role_id: 2
13 member_id: 3
14 member_roles_004:
15 id: 4
16 role_id: 2
17 member_id: 4
18 member_roles_005:
19 id: 5
20 role_id: 1
21 member_id: 5
22
23 No newline at end of file
@@ -1,252 +1,248
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19 require 'cgi'
20 20
21 21 class ApplicationController < ActionController::Base
22 22 include Redmine::I18n
23 23
24 24 # In case the cookie store secret changes
25 25 rescue_from CGI::Session::CookieStore::TamperedWithCookie do |exception|
26 26 render :text => 'Your session was invalid and has been reset. Please, reload this page.', :status => 500
27 27 end
28 28
29 29 layout 'base'
30 30
31 31 before_filter :user_setup, :check_if_login_required, :set_localization
32 32 filter_parameter_logging :password
33 33
34 34 include Redmine::MenuManager::MenuController
35 35 helper Redmine::MenuManager::MenuHelper
36 36
37 37 REDMINE_SUPPORTED_SCM.each do |scm|
38 38 require_dependency "repository/#{scm.underscore}"
39 39 end
40 40
41 def current_role
42 @current_role ||= User.current.role_for_project(@project)
43 end
44
45 41 def user_setup
46 42 # Check the settings cache for each request
47 43 Setting.check_cache
48 44 # Find the current user
49 45 self.logged_user = find_current_user
50 46 end
51 47
52 48 # Returns the current user or nil if no user is logged in
53 49 def find_current_user
54 50 if session[:user_id]
55 51 # existing session
56 52 (User.active.find(session[:user_id]) rescue nil)
57 53 elsif cookies[:autologin] && Setting.autologin?
58 54 # auto-login feature
59 55 User.try_to_autologin(cookies[:autologin])
60 56 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
61 57 # RSS key authentication
62 58 User.find_by_rss_key(params[:key])
63 59 end
64 60 end
65 61
66 62 # Sets the logged in user
67 63 def logged_user=(user)
68 64 if user && user.is_a?(User)
69 65 User.current = user
70 66 session[:user_id] = user.id
71 67 else
72 68 User.current = User.anonymous
73 69 session[:user_id] = nil
74 70 end
75 71 end
76 72
77 73 # check if login is globally required to access the application
78 74 def check_if_login_required
79 75 # no check needed if user is already logged in
80 76 return true if User.current.logged?
81 77 require_login if Setting.login_required?
82 78 end
83 79
84 80 def set_localization
85 81 lang = nil
86 82 if User.current.logged?
87 83 lang = find_language(User.current.language)
88 84 end
89 85 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
90 86 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
91 87 if !accept_lang.blank?
92 88 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
93 89 end
94 90 end
95 91 lang ||= Setting.default_language
96 92 set_language_if_valid(lang)
97 93 end
98 94
99 95 def require_login
100 96 if !User.current.logged?
101 97 redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
102 98 return false
103 99 end
104 100 true
105 101 end
106 102
107 103 def require_admin
108 104 return unless require_login
109 105 if !User.current.admin?
110 106 render_403
111 107 return false
112 108 end
113 109 true
114 110 end
115 111
116 112 def deny_access
117 113 User.current.logged? ? render_403 : require_login
118 114 end
119 115
120 116 # Authorize the user for the requested action
121 117 def authorize(ctrl = params[:controller], action = params[:action])
122 118 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
123 119 allowed ? true : deny_access
124 120 end
125 121
126 122 # make sure that the user is a member of the project (or admin) if project is private
127 123 # used as a before_filter for actions that do not require any particular permission on the project
128 124 def check_project_privacy
129 125 if @project && @project.active?
130 126 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
131 127 true
132 128 else
133 129 User.current.logged? ? render_403 : require_login
134 130 end
135 131 else
136 132 @project = nil
137 133 render_404
138 134 false
139 135 end
140 136 end
141 137
142 138 def redirect_back_or_default(default)
143 139 back_url = CGI.unescape(params[:back_url].to_s)
144 140 if !back_url.blank?
145 141 begin
146 142 uri = URI.parse(back_url)
147 143 # do not redirect user to another host or to the login or register page
148 144 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
149 145 redirect_to(back_url) and return
150 146 end
151 147 rescue URI::InvalidURIError
152 148 # redirect to default
153 149 end
154 150 end
155 151 redirect_to default
156 152 end
157 153
158 154 def render_403
159 155 @project = nil
160 156 render :template => "common/403", :layout => !request.xhr?, :status => 403
161 157 return false
162 158 end
163 159
164 160 def render_404
165 161 render :template => "common/404", :layout => !request.xhr?, :status => 404
166 162 return false
167 163 end
168 164
169 165 def render_error(msg)
170 166 flash.now[:error] = msg
171 167 render :text => '', :layout => !request.xhr?, :status => 500
172 168 end
173 169
174 170 def render_feed(items, options={})
175 171 @items = items || []
176 172 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
177 173 @items = @items.slice(0, Setting.feeds_limit.to_i)
178 174 @title = options[:title] || Setting.app_title
179 175 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
180 176 end
181 177
182 178 def self.accept_key_auth(*actions)
183 179 actions = actions.flatten.map(&:to_s)
184 180 write_inheritable_attribute('accept_key_auth_actions', actions)
185 181 end
186 182
187 183 def accept_key_auth_actions
188 184 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
189 185 end
190 186
191 187 # TODO: move to model
192 188 def attach_files(obj, attachments)
193 189 attached = []
194 190 unsaved = []
195 191 if attachments && attachments.is_a?(Hash)
196 192 attachments.each_value do |attachment|
197 193 file = attachment['file']
198 194 next unless file && file.size > 0
199 195 a = Attachment.create(:container => obj,
200 196 :file => file,
201 197 :description => attachment['description'].to_s.strip,
202 198 :author => User.current)
203 199 a.new_record? ? (unsaved << a) : (attached << a)
204 200 end
205 201 if unsaved.any?
206 202 flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
207 203 end
208 204 end
209 205 attached
210 206 end
211 207
212 208 # Returns the number of objects that should be displayed
213 209 # on the paginated list
214 210 def per_page_option
215 211 per_page = nil
216 212 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
217 213 per_page = params[:per_page].to_s.to_i
218 214 session[:per_page] = per_page
219 215 elsif session[:per_page]
220 216 per_page = session[:per_page]
221 217 else
222 218 per_page = Setting.per_page_options_array.first || 25
223 219 end
224 220 per_page
225 221 end
226 222
227 223 # qvalues http header parser
228 224 # code taken from webrick
229 225 def parse_qvalues(value)
230 226 tmp = []
231 227 if value
232 228 parts = value.split(/,\s*/)
233 229 parts.each {|part|
234 230 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
235 231 val = m[1]
236 232 q = (m[2] or 1).to_f
237 233 tmp.push([val, q])
238 234 end
239 235 }
240 236 tmp = tmp.sort_by{|val, q| -q}
241 237 tmp.collect!{|val, q| val}
242 238 end
243 239 return tmp
244 240 rescue
245 241 nil
246 242 end
247 243
248 244 # Returns a string that can be used as filename value in Content-Disposition header
249 245 def filename_for_content_disposition(name)
250 246 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
251 247 end
252 248 end
@@ -1,506 +1,506
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => :new
20 20
21 21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 26 accept_key_auth :index, :changes
27 27
28 28 helper :journals
29 29 helper :projects
30 30 include ProjectsHelper
31 31 helper :custom_fields
32 32 include CustomFieldsHelper
33 33 helper :issue_relations
34 34 include IssueRelationsHelper
35 35 helper :watchers
36 36 include WatchersHelper
37 37 helper :attachments
38 38 include AttachmentsHelper
39 39 helper :queries
40 40 helper :sort
41 41 include SortHelper
42 42 include IssuesHelper
43 43 helper :timelog
44 44 include Redmine::Export::PDF
45 45
46 46 def index
47 47 retrieve_query
48 48 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
49 49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50 50
51 51 if @query.valid?
52 52 limit = per_page_option
53 53 respond_to do |format|
54 54 format.html { }
55 55 format.atom { }
56 56 format.csv { limit = Setting.issues_export_limit.to_i }
57 57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 58 end
59 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 61 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
62 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 63 :conditions => @query.statement,
64 64 :limit => limit,
65 65 :offset => @issue_pages.current.offset
66 66 respond_to do |format|
67 67 format.html {
68 68 if @query.grouped?
69 69 # Retrieve the issue count by group
70 70 @issue_count_by_group = begin
71 71 Issue.count(:group => @query.group_by, :include => [:status, :project], :conditions => @query.statement)
72 72 # Rails will raise an (unexpected) error if there's only a nil group value
73 73 rescue ActiveRecord::RecordNotFound
74 74 {nil => @issue_count}
75 75 end
76 76 end
77 77 render :template => 'issues/index.rhtml', :layout => !request.xhr?
78 78 }
79 79 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 80 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
81 81 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 82 end
83 83 else
84 84 # Send html if the query is not valid
85 85 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 86 end
87 87 rescue ActiveRecord::RecordNotFound
88 88 render_404
89 89 end
90 90
91 91 def changes
92 92 retrieve_query
93 93 sort_init 'id', 'desc'
94 94 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95 95
96 96 if @query.valid?
97 97 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
98 98 :conditions => @query.statement,
99 99 :limit => 25,
100 100 :order => "#{Journal.table_name}.created_on DESC"
101 101 end
102 102 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
103 103 render :layout => false, :content_type => 'application/atom+xml'
104 104 rescue ActiveRecord::RecordNotFound
105 105 render_404
106 106 end
107 107
108 108 def show
109 109 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
110 110 @journals.each_with_index {|j,i| j.indice = i+1}
111 111 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 112 @changesets = @issue.changesets
113 113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 114 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
115 115 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
116 116 @priorities = Enumeration.priorities
117 117 @time_entry = TimeEntry.new
118 118 respond_to do |format|
119 119 format.html { render :template => 'issues/show.rhtml' }
120 120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
121 121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 122 end
123 123 end
124 124
125 125 # Add a new issue
126 126 # The new issue will be created from an existing one if copy_from parameter is given
127 127 def new
128 128 @issue = Issue.new
129 129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
130 130 @issue.project = @project
131 131 # Tracker must be set before custom field values
132 132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
133 133 if @issue.tracker.nil?
134 134 render_error 'No tracker is associated to this project. Please check the Project settings.'
135 135 return
136 136 end
137 137 if params[:issue].is_a?(Hash)
138 138 @issue.attributes = params[:issue]
139 139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
140 140 end
141 141 @issue.author = User.current
142 142
143 143 default_status = IssueStatus.default
144 144 unless default_status
145 145 render_error 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
146 146 return
147 147 end
148 148 @issue.status = default_status
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
150 150
151 151 if request.get? || request.xhr?
152 152 @issue.start_date ||= Date.today
153 153 else
154 154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
155 155 # Check that the user is allowed to apply the requested status
156 156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
157 157 if @issue.save
158 158 attach_files(@issue, params[:attachments])
159 159 flash[:notice] = l(:notice_successful_create)
160 160 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 161 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
162 162 { :action => 'show', :id => @issue })
163 163 return
164 164 end
165 165 end
166 166 @priorities = Enumeration.priorities
167 167 render :layout => !request.xhr?
168 168 end
169 169
170 170 # Attributes that can be updated on workflow transition (without :edit permission)
171 171 # TODO: make it configurable (at least per role)
172 172 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
173 173
174 174 def edit
175 175 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
176 176 @priorities = Enumeration.priorities
177 177 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
178 178 @time_entry = TimeEntry.new
179 179
180 180 @notes = params[:notes]
181 181 journal = @issue.init_journal(User.current, @notes)
182 182 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
183 183 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
184 184 attrs = params[:issue].dup
185 185 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
186 186 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
187 187 @issue.attributes = attrs
188 188 end
189 189
190 190 if request.post?
191 191 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
192 192 @time_entry.attributes = params[:time_entry]
193 193 attachments = attach_files(@issue, params[:attachments])
194 194 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
195 195
196 196 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
197 197
198 198 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
199 199 # Log spend time
200 200 if User.current.allowed_to?(:log_time, @project)
201 201 @time_entry.save
202 202 end
203 203 if !journal.new_record?
204 204 # Only send notification if something was actually changed
205 205 flash[:notice] = l(:notice_successful_update)
206 206 end
207 207 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
208 208 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
209 209 end
210 210 end
211 211 rescue ActiveRecord::StaleObjectError
212 212 # Optimistic locking exception
213 213 flash.now[:error] = l(:notice_locking_conflict)
214 214 end
215 215
216 216 def reply
217 217 journal = Journal.find(params[:journal_id]) if params[:journal_id]
218 218 if journal
219 219 user = journal.user
220 220 text = journal.notes
221 221 else
222 222 user = @issue.author
223 223 text = @issue.description
224 224 end
225 225 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
226 226 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
227 227 render(:update) { |page|
228 228 page.<< "$('notes').value = \"#{content}\";"
229 229 page.show 'update'
230 230 page << "Form.Element.focus('notes');"
231 231 page << "Element.scrollTo('update');"
232 232 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
233 233 }
234 234 end
235 235
236 236 # Bulk edit a set of issues
237 237 def bulk_edit
238 238 if request.post?
239 239 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
240 240 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
241 241 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
242 242 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
243 243 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
244 244 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
245 245
246 246 unsaved_issue_ids = []
247 247 @issues.each do |issue|
248 248 journal = issue.init_journal(User.current, params[:notes])
249 249 issue.priority = priority if priority
250 250 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
251 251 issue.category = category if category || params[:category_id] == 'none'
252 252 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
253 253 issue.start_date = params[:start_date] unless params[:start_date].blank?
254 254 issue.due_date = params[:due_date] unless params[:due_date].blank?
255 255 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
256 256 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
257 257 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
258 258 # Don't save any change to the issue if the user is not authorized to apply the requested status
259 259 unless (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
260 260 # Keep unsaved issue ids to display them in flash error
261 261 unsaved_issue_ids << issue.id
262 262 end
263 263 end
264 264 if unsaved_issue_ids.empty?
265 265 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
266 266 else
267 267 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
268 268 :total => @issues.size,
269 269 :ids => '#' + unsaved_issue_ids.join(', #'))
270 270 end
271 271 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
272 272 return
273 273 end
274 274 # Find potential statuses the user could be allowed to switch issues to
275 275 @available_statuses = Workflow.find(:all, :include => :new_status,
276 276 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
277 277 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
278 278 end
279 279
280 280 def move
281 281 @allowed_projects = []
282 282 # find projects to which the user is allowed to move the issue
283 283 if User.current.admin?
284 284 # admin is allowed to move issues to any active (visible) project
285 285 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
286 286 else
287 287 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
288 288 end
289 289 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
290 290 @target_project ||= @project
291 291 @trackers = @target_project.trackers
292 292 if request.post?
293 293 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
294 294 unsaved_issue_ids = []
295 295 @issues.each do |issue|
296 296 issue.init_journal(User.current)
297 297 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
298 298 end
299 299 if unsaved_issue_ids.empty?
300 300 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
301 301 else
302 302 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
303 303 :total => @issues.size,
304 304 :ids => '#' + unsaved_issue_ids.join(', #'))
305 305 end
306 306 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
307 307 return
308 308 end
309 309 render :layout => false if request.xhr?
310 310 end
311 311
312 312 def destroy
313 313 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
314 314 if @hours > 0
315 315 case params[:todo]
316 316 when 'destroy'
317 317 # nothing to do
318 318 when 'nullify'
319 319 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
320 320 when 'reassign'
321 321 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
322 322 if reassign_to.nil?
323 323 flash.now[:error] = l(:error_issue_not_found_in_project)
324 324 return
325 325 else
326 326 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
327 327 end
328 328 else
329 329 # display the destroy form
330 330 return
331 331 end
332 332 end
333 333 @issues.each(&:destroy)
334 334 redirect_to :action => 'index', :project_id => @project
335 335 end
336 336
337 337 def gantt
338 338 @gantt = Redmine::Helpers::Gantt.new(params)
339 339 retrieve_query
340 340 if @query.valid?
341 341 events = []
342 342 # Issues that have start and due dates
343 343 events += Issue.find(:all,
344 344 :order => "start_date, due_date",
345 345 :include => [:tracker, :status, :assigned_to, :priority, :project],
346 346 :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]
347 347 )
348 348 # Issues that don't have a due date but that are assigned to a version with a date
349 349 events += Issue.find(:all,
350 350 :order => "start_date, effective_date",
351 351 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
352 352 :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]
353 353 )
354 354 # Versions
355 355 events += Version.find(:all, :include => :project,
356 356 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
357 357
358 358 @gantt.events = events
359 359 end
360 360
361 361 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
362 362
363 363 respond_to do |format|
364 364 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
365 365 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
366 366 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
367 367 end
368 368 end
369 369
370 370 def calendar
371 371 if params[:year] and params[:year].to_i > 1900
372 372 @year = params[:year].to_i
373 373 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
374 374 @month = params[:month].to_i
375 375 end
376 376 end
377 377 @year ||= Date.today.year
378 378 @month ||= Date.today.month
379 379
380 380 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
381 381 retrieve_query
382 382 if @query.valid?
383 383 events = []
384 384 events += Issue.find(:all,
385 385 :include => [:tracker, :status, :assigned_to, :priority, :project],
386 386 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
387 387 )
388 388 events += Version.find(:all, :include => :project,
389 389 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
390 390
391 391 @calendar.events = events
392 392 end
393 393
394 394 render :layout => false if request.xhr?
395 395 end
396 396
397 397 def context_menu
398 398 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
399 399 if (@issues.size == 1)
400 400 @issue = @issues.first
401 401 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
402 402 end
403 403 projects = @issues.collect(&:project).compact.uniq
404 404 @project = projects.first if projects.size == 1
405 405
406 406 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
407 407 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
408 408 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
409 409 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
410 410 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
411 411 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
412 412 }
413 413 if @project
414 414 @assignables = @project.assignable_users
415 415 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
416 416 end
417 417
418 418 @priorities = Enumeration.priorities.reverse
419 419 @statuses = IssueStatus.find(:all, :order => 'position')
420 420 @back = request.env['HTTP_REFERER']
421 421
422 422 render :layout => false
423 423 end
424 424
425 425 def update_form
426 426 @issue = Issue.new(params[:issue])
427 427 render :action => :new, :layout => false
428 428 end
429 429
430 430 def preview
431 431 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
432 432 @attachements = @issue.attachments if @issue
433 433 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
434 434 render :partial => 'common/preview'
435 435 end
436 436
437 437 private
438 438 def find_issue
439 439 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
440 440 @project = @issue.project
441 441 rescue ActiveRecord::RecordNotFound
442 442 render_404
443 443 end
444 444
445 445 # Filter for bulk operations
446 446 def find_issues
447 447 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
448 448 raise ActiveRecord::RecordNotFound if @issues.empty?
449 449 projects = @issues.collect(&:project).compact.uniq
450 450 if projects.size == 1
451 451 @project = projects.first
452 452 else
453 453 # TODO: let users bulk edit/move/destroy issues from different projects
454 454 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
455 455 end
456 456 rescue ActiveRecord::RecordNotFound
457 457 render_404
458 458 end
459 459
460 460 def find_project
461 461 @project = Project.find(params[:project_id])
462 462 rescue ActiveRecord::RecordNotFound
463 463 render_404
464 464 end
465 465
466 466 def find_optional_project
467 467 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
468 468 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
469 469 allowed ? true : deny_access
470 470 rescue ActiveRecord::RecordNotFound
471 471 render_404
472 472 end
473 473
474 474 # Retrieve query from session or build a new query
475 475 def retrieve_query
476 476 if !params[:query_id].blank?
477 477 cond = "project_id IS NULL"
478 478 cond << " OR project_id = #{@project.id}" if @project
479 479 @query = Query.find(params[:query_id], :conditions => cond)
480 480 @query.project = @project
481 481 session[:query] = {:id => @query.id, :project_id => @query.project_id}
482 482 sort_clear
483 483 else
484 484 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
485 485 # Give it a name, required to be valid
486 486 @query = Query.new(:name => "_")
487 487 @query.project = @project
488 488 if params[:fields] and params[:fields].is_a? Array
489 489 params[:fields].each do |field|
490 490 @query.add_filter(field, params[:operators][field], params[:values][field])
491 491 end
492 492 else
493 493 @query.available_filters.keys.each do |field|
494 494 @query.add_short_filter(field, params[field]) if params[field]
495 495 end
496 496 end
497 497 @query.group_by = params[:group_by]
498 498 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
499 499 else
500 500 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
501 501 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
502 502 @query.project = @project
503 503 end
504 504 end
505 505 end
506 506 end
@@ -1,84 +1,89
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MembersController < ApplicationController
19 19 before_filter :find_member, :except => [:new, :autocomplete_for_member_login]
20 20 before_filter :find_project, :only => [:new, :autocomplete_for_member_login]
21 21 before_filter :authorize
22 22
23 23 def new
24 24 members = []
25 25 if params[:member] && request.post?
26 26 attrs = params[:member].dup
27 27 if (user_ids = attrs.delete(:user_ids))
28 28 user_ids.each do |user_id|
29 29 members << Member.new(attrs.merge(:user_id => user_id))
30 30 end
31 31 else
32 32 members << Member.new(attrs)
33 33 end
34 34 @project.members << members
35 35 end
36 36 respond_to do |format|
37 37 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
38 38 format.js {
39 39 render(:update) {|page|
40 40 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
41 41 members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
42 42 }
43 43 }
44 44 end
45 45 end
46 46
47 47 def edit
48 48 if request.post? and @member.update_attributes(params[:member])
49 49 respond_to do |format|
50 50 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
51 format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
51 format.js {
52 render(:update) {|page|
53 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
54 page.visual_effect(:highlight, "member-#{@member.id}")
55 }
56 }
52 57 end
53 58 end
54 59 end
55 60
56 61 def destroy
57 62 @member.destroy
58 63 respond_to do |format|
59 64 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
60 65 format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
61 66 end
62 67 end
63 68
64 69 def autocomplete_for_member_login
65 70 @users = User.active.find(:all, :conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", "#{params[:user]}%", "#{params[:user]}%", "#{params[:user]}%"],
66 71 :limit => 10,
67 72 :order => 'login ASC') - @project.users
68 73 render :layout => false
69 74 end
70 75
71 76 private
72 77 def find_project
73 78 @project = Project.find(params[:id])
74 79 rescue ActiveRecord::RecordNotFound
75 80 render_404
76 81 end
77 82
78 83 def find_member
79 84 @member = Member.find(params[:id])
80 85 @project = @member.project
81 86 rescue ActiveRecord::RecordNotFound
82 87 render_404
83 88 end
84 89 end
@@ -1,319 +1,319
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :activity, :only => :activity
21 21 menu_item :roadmap, :only => :roadmap
22 22 menu_item :files, :only => [:list_files, :add_file]
23 23 menu_item :settings, :only => :settings
24 24 menu_item :issues, :only => [:changelog]
25 25
26 26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
27 27 before_filter :find_optional_project, :only => :activity
28 28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
29 29 before_filter :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ]
30 30 accept_key_auth :activity
31 31
32 32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 33 if controller.request.post?
34 34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 35 end
36 36 end
37 37
38 38 helper :sort
39 39 include SortHelper
40 40 helper :custom_fields
41 41 include CustomFieldsHelper
42 42 helper :issues
43 43 helper IssuesHelper
44 44 helper :queries
45 45 include QueriesHelper
46 46 helper :repositories
47 47 include RepositoriesHelper
48 48 include ProjectsHelper
49 49
50 50 # Lists visible projects
51 51 def index
52 52 respond_to do |format|
53 53 format.html {
54 54 @projects = Project.visible.find(:all, :order => 'lft')
55 55 }
56 56 format.atom {
57 57 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 58 :limit => Setting.feeds_limit.to_i)
59 59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 60 }
61 61 end
62 62 end
63 63
64 64 # Add a new project
65 65 def add
66 66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 67 @trackers = Tracker.all
68 68 @project = Project.new(params[:project])
69 69 if request.get?
70 70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 71 @project.trackers = Tracker.all
72 72 @project.is_public = Setting.default_projects_public?
73 73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
74 74 else
75 75 @project.enabled_module_names = params[:enabled_modules]
76 76 if @project.save
77 77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
78 78 flash[:notice] = l(:notice_successful_create)
79 79 redirect_to :controller => 'admin', :action => 'projects'
80 80 end
81 81 end
82 82 end
83 83
84 84 def copy
85 85 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
86 86 @trackers = Tracker.all
87 87 @root_projects = Project.find(:all,
88 88 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
89 89 :order => 'name')
90 90 if request.get?
91 91 @project = Project.copy_from(params[:id])
92 92 if @project
93 93 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
94 94 else
95 95 redirect_to :controller => 'admin', :action => 'projects'
96 96 end
97 97 else
98 98 @project = Project.new(params[:project])
99 99 @project.enabled_module_names = params[:enabled_modules]
100 100 if @project.copy(params[:id])
101 101 flash[:notice] = l(:notice_successful_create)
102 102 redirect_to :controller => 'admin', :action => 'projects'
103 103 end
104 104 end
105 105 end
106 106
107 107
108 108 # Show @project
109 109 def show
110 110 if params[:jump]
111 111 # try to redirect to the requested menu item
112 112 redirect_to_project_menu_item(@project, params[:jump]) && return
113 113 end
114 114
115 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
115 @members_by_role = @project.members.find(:all, :include => [:user, :roles], :order => 'position').group_by {|m| m.roles.first}
116 116 @subprojects = @project.children.visible
117 117 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
118 118 @trackers = @project.rolled_up_trackers
119 119
120 120 cond = @project.project_condition(Setting.display_subprojects_issues?)
121 121
122 122 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
123 123 :include => [:project, :status, :tracker],
124 124 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
125 125 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
126 126 :include => [:project, :status, :tracker],
127 127 :conditions => cond)
128 128
129 129 TimeEntry.visible_by(User.current) do
130 130 @total_hours = TimeEntry.sum(:hours,
131 131 :include => :project,
132 132 :conditions => cond).to_f
133 133 end
134 134 @key = User.current.rss_key
135 135 end
136 136
137 137 def settings
138 138 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
139 139 @issue_category ||= IssueCategory.new
140 140 @member ||= @project.members.new
141 141 @trackers = Tracker.all
142 142 @repository ||= @project.repository
143 143 @wiki ||= @project.wiki
144 144 end
145 145
146 146 # Edit @project
147 147 def edit
148 148 if request.post?
149 149 @project.attributes = params[:project]
150 150 if @project.save
151 151 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
152 152 flash[:notice] = l(:notice_successful_update)
153 153 redirect_to :action => 'settings', :id => @project
154 154 else
155 155 settings
156 156 render :action => 'settings'
157 157 end
158 158 end
159 159 end
160 160
161 161 def modules
162 162 @project.enabled_module_names = params[:enabled_modules]
163 163 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
164 164 end
165 165
166 166 def archive
167 167 @project.archive if request.post? && @project.active?
168 168 redirect_to :controller => 'admin', :action => 'projects'
169 169 end
170 170
171 171 def unarchive
172 172 @project.unarchive if request.post? && !@project.active?
173 173 redirect_to :controller => 'admin', :action => 'projects'
174 174 end
175 175
176 176 # Delete @project
177 177 def destroy
178 178 @project_to_destroy = @project
179 179 if request.post? and params[:confirm]
180 180 @project_to_destroy.destroy
181 181 redirect_to :controller => 'admin', :action => 'projects'
182 182 end
183 183 # hide project in layout
184 184 @project = nil
185 185 end
186 186
187 187 # Add a new issue category to @project
188 188 def add_issue_category
189 189 @category = @project.issue_categories.build(params[:category])
190 190 if request.post? and @category.save
191 191 respond_to do |format|
192 192 format.html do
193 193 flash[:notice] = l(:notice_successful_create)
194 194 redirect_to :action => 'settings', :tab => 'categories', :id => @project
195 195 end
196 196 format.js do
197 197 # IE doesn't support the replace_html rjs method for select box options
198 198 render(:update) {|page| page.replace "issue_category_id",
199 199 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]')
200 200 }
201 201 end
202 202 end
203 203 end
204 204 end
205 205
206 206 # Add a new version to @project
207 207 def add_version
208 208 @version = @project.versions.build(params[:version])
209 209 if request.post? and @version.save
210 210 flash[:notice] = l(:notice_successful_create)
211 211 redirect_to :action => 'settings', :tab => 'versions', :id => @project
212 212 end
213 213 end
214 214
215 215 def add_file
216 216 if request.post?
217 217 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
218 218 attachments = attach_files(container, params[:attachments])
219 219 if !attachments.empty? && Setting.notified_events.include?('file_added')
220 220 Mailer.deliver_attachments_added(attachments)
221 221 end
222 222 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
223 223 return
224 224 end
225 225 @versions = @project.versions.sort
226 226 end
227 227
228 228 def list_files
229 229 sort_init 'filename', 'asc'
230 230 sort_update 'filename' => "#{Attachment.table_name}.filename",
231 231 'created_on' => "#{Attachment.table_name}.created_on",
232 232 'size' => "#{Attachment.table_name}.filesize",
233 233 'downloads' => "#{Attachment.table_name}.downloads"
234 234
235 235 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
236 236 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
237 237 render :layout => !request.xhr?
238 238 end
239 239
240 240 # Show changelog for @project
241 241 def changelog
242 242 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
243 243 retrieve_selected_tracker_ids(@trackers)
244 244 @versions = @project.versions.sort
245 245 end
246 246
247 247 def roadmap
248 248 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
249 249 retrieve_selected_tracker_ids(@trackers)
250 250 @versions = @project.versions.sort
251 251 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
252 252 end
253 253
254 254 def activity
255 255 @days = Setting.activity_days_default.to_i
256 256
257 257 if params[:from]
258 258 begin; @date_to = params[:from].to_date + 1; rescue; end
259 259 end
260 260
261 261 @date_to ||= Date.today + 1
262 262 @date_from = @date_to - @days
263 263 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
264 264 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
265 265
266 266 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
267 267 :with_subprojects => @with_subprojects,
268 268 :author => @author)
269 269 @activity.scope_select {|t| !params["show_#{t}"].nil?}
270 270 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
271 271
272 272 events = @activity.events(@date_from, @date_to)
273 273
274 274 respond_to do |format|
275 275 format.html {
276 276 @events_by_day = events.group_by(&:event_date)
277 277 render :layout => false if request.xhr?
278 278 }
279 279 format.atom {
280 280 title = l(:label_activity)
281 281 if @author
282 282 title = @author.name
283 283 elsif @activity.scope.size == 1
284 284 title = l("label_#{@activity.scope.first.singularize}_plural")
285 285 end
286 286 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
287 287 }
288 288 end
289 289
290 290 rescue ActiveRecord::RecordNotFound
291 291 render_404
292 292 end
293 293
294 294 private
295 295 # Find project of id params[:id]
296 296 # if not found, redirect to project list
297 297 # Used as a before_filter
298 298 def find_project
299 299 @project = Project.find(params[:id])
300 300 rescue ActiveRecord::RecordNotFound
301 301 render_404
302 302 end
303 303
304 304 def find_optional_project
305 305 return true unless params[:id]
306 306 @project = Project.find(params[:id])
307 307 authorize
308 308 rescue ActiveRecord::RecordNotFound
309 309 render_404
310 310 end
311 311
312 312 def retrieve_selected_tracker_ids(selectable_trackers)
313 313 if ids = params[:tracker_ids]
314 314 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
315 315 else
316 316 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
317 317 end
318 318 end
319 319 end
@@ -1,81 +1,81
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_query, :except => :new
21 21 before_filter :find_optional_project, :only => :new
22 22
23 23 def new
24 24 @query = Query.new(params[:query])
25 25 @query.project = params[:query_is_for_all] ? nil : @project
26 26 @query.user = User.current
27 @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
27 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
28 28 @query.column_names = nil if params[:default_columns]
29 29
30 30 params[:fields].each do |field|
31 31 @query.add_filter(field, params[:operators][field], params[:values][field])
32 32 end if params[:fields]
33 33 @query.group_by ||= params[:group_by]
34 34
35 35 if request.post? && params[:confirm] && @query.save
36 36 flash[:notice] = l(:notice_successful_create)
37 37 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
38 38 return
39 39 end
40 40 render :layout => false if request.xhr?
41 41 end
42 42
43 43 def edit
44 44 if request.post?
45 45 @query.filters = {}
46 46 params[:fields].each do |field|
47 47 @query.add_filter(field, params[:operators][field], params[:values][field])
48 48 end if params[:fields]
49 49 @query.attributes = params[:query]
50 50 @query.project = nil if params[:query_is_for_all]
51 @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
51 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
52 52 @query.column_names = nil if params[:default_columns]
53 53
54 54 if @query.save
55 55 flash[:notice] = l(:notice_successful_update)
56 56 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
57 57 end
58 58 end
59 59 end
60 60
61 61 def destroy
62 62 @query.destroy if request.post?
63 63 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
64 64 end
65 65
66 66 private
67 67 def find_query
68 68 @query = Query.find(params[:id])
69 69 @project = @query.project
70 70 render_403 unless @query.editable_by?(User.current)
71 71 rescue ActiveRecord::RecordNotFound
72 72 render_404
73 73 end
74 74
75 75 def find_optional_project
76 76 @project = Project.find(params[:project_id]) if params[:project_id]
77 77 User.current.allowed_to?(:save_queries, @project, :global => true)
78 78 rescue ActiveRecord::RecordNotFound
79 79 render_404
80 80 end
81 81 end
@@ -1,79 +1,79
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class RolesController < ApplicationController
19 19 before_filter :require_admin
20 20
21 21 verify :method => :post, :only => [ :destroy, :move ],
22 22 :redirect_to => { :action => :list }
23 23
24 24 def index
25 25 list
26 26 render :action => 'list' unless request.xhr?
27 27 end
28 28
29 29 def list
30 30 @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
31 31 render :action => "list", :layout => false if request.xhr?
32 32 end
33 33
34 34 def new
35 35 # Prefills the form with 'Non member' role permissions
36 36 @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
37 37 if request.post? && @role.save
38 38 # workflow copy
39 39 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
40 40 @role.workflows.copy(copy_from)
41 41 end
42 42 flash[:notice] = l(:notice_successful_create)
43 redirect_to :action => 'list'
43 redirect_to :action => 'index'
44 44 end
45 45 @permissions = @role.setable_permissions
46 46 @roles = Role.find :all, :order => 'builtin, position'
47 47 end
48 48
49 49 def edit
50 50 @role = Role.find(params[:id])
51 51 if request.post? and @role.update_attributes(params[:role])
52 52 flash[:notice] = l(:notice_successful_update)
53 redirect_to :action => 'list'
53 redirect_to :action => 'index'
54 54 end
55 55 @permissions = @role.setable_permissions
56 56 end
57 57
58 58 def destroy
59 59 @role = Role.find(params[:id])
60 60 @role.destroy
61 redirect_to :action => 'list'
61 redirect_to :action => 'index'
62 62 rescue
63 63 flash[:error] = 'This role is in use and can not be deleted.'
64 64 redirect_to :action => 'index'
65 65 end
66 66
67 67 def report
68 68 @roles = Role.find(:all, :order => 'builtin, position')
69 69 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
70 70 if request.post?
71 71 @roles.each do |role|
72 72 role.permissions = params[:permissions][role.id.to_s]
73 73 role.save
74 74 end
75 75 flash[:notice] = l(:notice_successful_update)
76 redirect_to :action => 'list'
76 redirect_to :action => 'index'
77 77 end
78 78 end
79 79 end
@@ -1,108 +1,116
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class UsersController < ApplicationController
19 19 before_filter :require_admin
20 20
21 21 helper :sort
22 22 include SortHelper
23 23 helper :custom_fields
24 24 include CustomFieldsHelper
25 25
26 26 def index
27 27 list
28 28 render :action => 'list' unless request.xhr?
29 29 end
30 30
31 31 def list
32 32 sort_init 'login', 'asc'
33 33 sort_update %w(login firstname lastname mail admin created_on last_login_on)
34 34
35 35 @status = params[:status] ? params[:status].to_i : 1
36 36 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
37 37
38 38 unless params[:name].blank?
39 39 name = "%#{params[:name].strip.downcase}%"
40 40 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
41 41 end
42 42
43 43 @user_count = User.count(:conditions => c.conditions)
44 44 @user_pages = Paginator.new self, @user_count,
45 45 per_page_option,
46 46 params['page']
47 47 @users = User.find :all,:order => sort_clause,
48 48 :conditions => c.conditions,
49 49 :limit => @user_pages.items_per_page,
50 50 :offset => @user_pages.current.offset
51 51
52 52 render :action => "list", :layout => false if request.xhr?
53 53 end
54 54
55 55 def add
56 56 if request.get?
57 57 @user = User.new(:language => Setting.default_language)
58 58 else
59 59 @user = User.new(params[:user])
60 60 @user.admin = params[:user][:admin] || false
61 61 @user.login = params[:user][:login]
62 62 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
63 63 if @user.save
64 64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
65 65 flash[:notice] = l(:notice_successful_create)
66 66 redirect_to :action => 'list'
67 67 end
68 68 end
69 69 @auth_sources = AuthSource.find(:all)
70 70 end
71 71
72 72 def edit
73 73 @user = User.find(params[:id])
74 74 if request.post?
75 75 @user.admin = params[:user][:admin] if params[:user][:admin]
76 76 @user.login = params[:user][:login] if params[:user][:login]
77 77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
78 78 @user.attributes = params[:user]
79 79 # Was the account actived ? (do it before User#save clears the change)
80 80 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
81 81 if @user.save
82 82 Mailer.deliver_account_activated(@user) if was_activated
83 83 flash[:notice] = l(:notice_successful_update)
84 84 # Give a string to redirect_to otherwise it would use status param as the response code
85 85 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
86 86 end
87 87 end
88 88 @auth_sources = AuthSource.find(:all)
89 @roles = Role.find_all_givable
90 @projects = Project.active.find(:all, :order => 'lft')
91 89 @membership ||= Member.new
92 @memberships = @user.memberships
93 90 end
94 91
95 92 def edit_membership
96 93 @user = User.find(params[:id])
97 94 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
98 95 @membership.attributes = params[:membership]
99 96 @membership.save if request.post?
100 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
97 respond_to do |format|
98 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
99 format.js {
100 render(:update) {|page|
101 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
102 page.visual_effect(:highlight, "member-#{@membership.id}")
103 }
104 }
105 end
101 106 end
102 107
103 108 def destroy_membership
104 109 @user = User.find(params[:id])
105 110 Member.find(params[:membership_id]).destroy if request.post?
106 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
111 respond_to do |format|
112 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
113 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
114 end
107 115 end
108 116 end
@@ -1,653 +1,649
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 def current_role
32 @current_role ||= User.current.role_for_project(@project)
33 end
34
35 31 # Return true if user is authorized for controller/action, otherwise false
36 32 def authorize_for(controller, action)
37 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 34 end
39 35
40 36 # Display a link if user is authorized
41 37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 39 end
44 40
45 41 # Display a link to remote if user is authorized
46 42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 43 url = options[:url] || {}
48 44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 45 end
50 46
51 47 # Display a link to user's account page
52 48 def link_to_user(user, options={})
53 49 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
54 50 end
55 51
56 52 def link_to_issue(issue, options={})
57 53 options[:class] ||= issue.css_classes
58 54 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
59 55 end
60 56
61 57 # Generates a link to an attachment.
62 58 # Options:
63 59 # * :text - Link text (default to attachment filename)
64 60 # * :download - Force download (default: false)
65 61 def link_to_attachment(attachment, options={})
66 62 text = options.delete(:text) || attachment.filename
67 63 action = options.delete(:download) ? 'download' : 'show'
68 64
69 65 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
70 66 end
71 67
72 68 def toggle_link(name, id, options={})
73 69 onclick = "Element.toggle('#{id}'); "
74 70 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
75 71 onclick << "return false;"
76 72 link_to(name, "#", :onclick => onclick)
77 73 end
78 74
79 75 def image_to_function(name, function, html_options = {})
80 76 html_options.symbolize_keys!
81 77 tag(:input, html_options.merge({
82 78 :type => "image", :src => image_path(name),
83 79 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
84 80 }))
85 81 end
86 82
87 83 def prompt_to_remote(name, text, param, url, html_options = {})
88 84 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
89 85 link_to name, {}, html_options
90 86 end
91 87
92 88 def format_activity_title(text)
93 89 h(truncate_single_line(text, :length => 100))
94 90 end
95 91
96 92 def format_activity_day(date)
97 93 date == Date.today ? l(:label_today).titleize : format_date(date)
98 94 end
99 95
100 96 def format_activity_description(text)
101 97 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
102 98 end
103 99
104 100 def due_date_distance_in_words(date)
105 101 if date
106 102 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
107 103 end
108 104 end
109 105
110 106 def render_page_hierarchy(pages, node=nil)
111 107 content = ''
112 108 if pages[node]
113 109 content << "<ul class=\"pages-hierarchy\">\n"
114 110 pages[node].each do |page|
115 111 content << "<li>"
116 112 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
117 113 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
118 114 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
119 115 content << "</li>\n"
120 116 end
121 117 content << "</ul>\n"
122 118 end
123 119 content
124 120 end
125 121
126 122 # Renders flash messages
127 123 def render_flash_messages
128 124 s = ''
129 125 flash.each do |k,v|
130 126 s << content_tag('div', v, :class => "flash #{k}")
131 127 end
132 128 s
133 129 end
134 130
135 131 # Renders the project quick-jump box
136 132 def render_project_jump_box
137 133 # Retrieve them now to avoid a COUNT query
138 134 projects = User.current.projects.all
139 135 if projects.any?
140 136 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
141 137 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
142 138 '<option disabled="disabled">---</option>'
143 139 s << project_tree_options_for_select(projects) do |p|
144 140 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
145 141 end
146 142 s << '</select>'
147 143 s
148 144 end
149 145 end
150 146
151 147 def project_tree_options_for_select(projects, options = {})
152 148 s = ''
153 149 project_tree(projects) do |project, level|
154 150 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
155 151 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
156 152 tag_options.merge!(yield(project)) if block_given?
157 153 s << content_tag('option', name_prefix + h(project), tag_options)
158 154 end
159 155 s
160 156 end
161 157
162 158 # Yields the given block for each project with its level in the tree
163 159 def project_tree(projects, &block)
164 160 ancestors = []
165 161 projects.sort_by(&:lft).each do |project|
166 162 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
167 163 ancestors.pop
168 164 end
169 165 yield project, ancestors.size
170 166 ancestors << project
171 167 end
172 168 end
173 169
174 170 def project_nested_ul(projects, &block)
175 171 s = ''
176 172 if projects.any?
177 173 ancestors = []
178 174 projects.sort_by(&:lft).each do |project|
179 175 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
180 176 s << "<ul>\n"
181 177 else
182 178 ancestors.pop
183 179 s << "</li>"
184 180 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
185 181 ancestors.pop
186 182 s << "</ul></li>\n"
187 183 end
188 184 end
189 185 s << "<li>"
190 186 s << yield(project).to_s
191 187 ancestors << project
192 188 end
193 189 s << ("</li></ul>\n" * ancestors.size)
194 190 end
195 191 s
196 192 end
197 193
198 194 # Truncates and returns the string as a single line
199 195 def truncate_single_line(string, *args)
200 196 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
201 197 end
202 198
203 199 def html_hours(text)
204 200 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
205 201 end
206 202
207 203 def authoring(created, author, options={})
208 204 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
209 205 link_to(distance_of_time_in_words(Time.now, created),
210 206 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
211 207 :title => format_time(created))
212 208 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
213 209 l(options[:label] || :label_added_time_by, :author => author_tag, :age => time_tag)
214 210 end
215 211
216 212 def syntax_highlight(name, content)
217 213 type = CodeRay::FileType[name]
218 214 type ? CodeRay.scan(content, type).html : h(content)
219 215 end
220 216
221 217 def to_path_param(path)
222 218 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
223 219 end
224 220
225 221 def pagination_links_full(paginator, count=nil, options={})
226 222 page_param = options.delete(:page_param) || :page
227 223 url_param = params.dup
228 224 # don't reuse query params if filters are present
229 225 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
230 226
231 227 html = ''
232 228 if paginator.current.previous
233 229 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
234 230 end
235 231
236 232 html << (pagination_links_each(paginator, options) do |n|
237 233 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
238 234 end || '')
239 235
240 236 if paginator.current.next
241 237 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
242 238 end
243 239
244 240 unless count.nil?
245 241 html << [
246 242 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
247 243 per_page_links(paginator.items_per_page)
248 244 ].compact.join(' | ')
249 245 end
250 246
251 247 html
252 248 end
253 249
254 250 def per_page_links(selected=nil)
255 251 url_param = params.dup
256 252 url_param.clear if url_param.has_key?(:set_filter)
257 253
258 254 links = Setting.per_page_options_array.collect do |n|
259 255 n == selected ? n : link_to_remote(n, {:update => "content",
260 256 :url => params.dup.merge(:per_page => n),
261 257 :method => :get},
262 258 {:href => url_for(url_param.merge(:per_page => n))})
263 259 end
264 260 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
265 261 end
266 262
267 263 def reorder_links(name, url)
268 264 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
269 265 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
270 266 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
271 267 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
272 268 end
273 269
274 270 def breadcrumb(*args)
275 271 elements = args.flatten
276 272 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
277 273 end
278 274
279 275 def other_formats_links(&block)
280 276 concat('<p class="other-formats">' + l(:label_export_to))
281 277 yield Redmine::Views::OtherFormatsBuilder.new(self)
282 278 concat('</p>')
283 279 end
284 280
285 281 def page_header_title
286 282 if @project.nil? || @project.new_record?
287 283 h(Setting.app_title)
288 284 else
289 285 b = []
290 286 ancestors = (@project.root? ? [] : @project.ancestors.visible)
291 287 if ancestors.any?
292 288 root = ancestors.shift
293 289 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
294 290 if ancestors.size > 2
295 291 b << '&#8230;'
296 292 ancestors = ancestors[-2, 2]
297 293 end
298 294 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
299 295 end
300 296 b << h(@project)
301 297 b.join(' &#187; ')
302 298 end
303 299 end
304 300
305 301 def html_title(*args)
306 302 if args.empty?
307 303 title = []
308 304 title << @project.name if @project
309 305 title += @html_title if @html_title
310 306 title << Setting.app_title
311 307 title.compact.join(' - ')
312 308 else
313 309 @html_title ||= []
314 310 @html_title += args
315 311 end
316 312 end
317 313
318 314 def accesskey(s)
319 315 Redmine::AccessKeys.key_for s
320 316 end
321 317
322 318 # Formats text according to system settings.
323 319 # 2 ways to call this method:
324 320 # * with a String: textilizable(text, options)
325 321 # * with an object and one of its attribute: textilizable(issue, :description, options)
326 322 def textilizable(*args)
327 323 options = args.last.is_a?(Hash) ? args.pop : {}
328 324 case args.size
329 325 when 1
330 326 obj = options[:object]
331 327 text = args.shift
332 328 when 2
333 329 obj = args.shift
334 330 text = obj.send(args.shift).to_s
335 331 else
336 332 raise ArgumentError, 'invalid arguments to textilizable'
337 333 end
338 334 return '' if text.blank?
339 335
340 336 only_path = options.delete(:only_path) == false ? false : true
341 337
342 338 # when using an image link, try to use an attachment, if possible
343 339 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
344 340
345 341 if attachments
346 342 attachments = attachments.sort_by(&:created_on).reverse
347 343 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
348 344 style = $1
349 345 filename = $6.downcase
350 346 # search for the picture in attachments
351 347 if found = attachments.detect { |att| att.filename.downcase == filename }
352 348 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
353 349 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
354 350 alt = desc.blank? ? nil : "(#{desc})"
355 351 "!#{style}#{image_url}#{alt}!"
356 352 else
357 353 m
358 354 end
359 355 end
360 356 end
361 357
362 358 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
363 359
364 360 # different methods for formatting wiki links
365 361 case options[:wiki_links]
366 362 when :local
367 363 # used for local links to html files
368 364 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
369 365 when :anchor
370 366 # used for single-file wiki export
371 367 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
372 368 else
373 369 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
374 370 end
375 371
376 372 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
377 373
378 374 # Wiki links
379 375 #
380 376 # Examples:
381 377 # [[mypage]]
382 378 # [[mypage|mytext]]
383 379 # wiki links can refer other project wikis, using project name or identifier:
384 380 # [[project:]] -> wiki starting page
385 381 # [[project:|mytext]]
386 382 # [[project:mypage]]
387 383 # [[project:mypage|mytext]]
388 384 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
389 385 link_project = project
390 386 esc, all, page, title = $1, $2, $3, $5
391 387 if esc.nil?
392 388 if page =~ /^([^\:]+)\:(.*)$/
393 389 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
394 390 page = $2
395 391 title ||= $1 if page.blank?
396 392 end
397 393
398 394 if link_project && link_project.wiki
399 395 # extract anchor
400 396 anchor = nil
401 397 if page =~ /^(.+?)\#(.+)$/
402 398 page, anchor = $1, $2
403 399 end
404 400 # check if page exists
405 401 wiki_page = link_project.wiki.find_page(page)
406 402 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
407 403 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
408 404 else
409 405 # project or wiki doesn't exist
410 406 all
411 407 end
412 408 else
413 409 all
414 410 end
415 411 end
416 412
417 413 # Redmine links
418 414 #
419 415 # Examples:
420 416 # Issues:
421 417 # #52 -> Link to issue #52
422 418 # Changesets:
423 419 # r52 -> Link to revision 52
424 420 # commit:a85130f -> Link to scmid starting with a85130f
425 421 # Documents:
426 422 # document#17 -> Link to document with id 17
427 423 # document:Greetings -> Link to the document with title "Greetings"
428 424 # document:"Some document" -> Link to the document with title "Some document"
429 425 # Versions:
430 426 # version#3 -> Link to version with id 3
431 427 # version:1.0.0 -> Link to version named "1.0.0"
432 428 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
433 429 # Attachments:
434 430 # attachment:file.zip -> Link to the attachment of the current object named file.zip
435 431 # Source files:
436 432 # source:some/file -> Link to the file located at /some/file in the project's repository
437 433 # source:some/file@52 -> Link to the file's revision 52
438 434 # source:some/file#L120 -> Link to line 120 of the file
439 435 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
440 436 # export:some/file -> Force the download of the file
441 437 # Forum messages:
442 438 # message#1218 -> Link to message with id 1218
443 439 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
444 440 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
445 441 link = nil
446 442 if esc.nil?
447 443 if prefix.nil? && sep == 'r'
448 444 if project && (changeset = project.changesets.find_by_revision(oid))
449 445 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
450 446 :class => 'changeset',
451 447 :title => truncate_single_line(changeset.comments, :length => 100))
452 448 end
453 449 elsif sep == '#'
454 450 oid = oid.to_i
455 451 case prefix
456 452 when nil
457 453 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
458 454 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
459 455 :class => (issue.closed? ? 'issue closed' : 'issue'),
460 456 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
461 457 link = content_tag('del', link) if issue.closed?
462 458 end
463 459 when 'document'
464 460 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
465 461 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
466 462 :class => 'document'
467 463 end
468 464 when 'version'
469 465 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
470 466 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
471 467 :class => 'version'
472 468 end
473 469 when 'message'
474 470 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
475 471 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
476 472 :controller => 'messages',
477 473 :action => 'show',
478 474 :board_id => message.board,
479 475 :id => message.root,
480 476 :anchor => (message.parent ? "message-#{message.id}" : nil)},
481 477 :class => 'message'
482 478 end
483 479 end
484 480 elsif sep == ':'
485 481 # removes the double quotes if any
486 482 name = oid.gsub(%r{^"(.*)"$}, "\\1")
487 483 case prefix
488 484 when 'document'
489 485 if project && document = project.documents.find_by_title(name)
490 486 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
491 487 :class => 'document'
492 488 end
493 489 when 'version'
494 490 if project && version = project.versions.find_by_name(name)
495 491 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
496 492 :class => 'version'
497 493 end
498 494 when 'commit'
499 495 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
500 496 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
501 497 :class => 'changeset',
502 498 :title => truncate_single_line(changeset.comments, :length => 100)
503 499 end
504 500 when 'source', 'export'
505 501 if project && project.repository
506 502 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
507 503 path, rev, anchor = $1, $3, $5
508 504 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
509 505 :path => to_path_param(path),
510 506 :rev => rev,
511 507 :anchor => anchor,
512 508 :format => (prefix == 'export' ? 'raw' : nil)},
513 509 :class => (prefix == 'export' ? 'source download' : 'source')
514 510 end
515 511 when 'attachment'
516 512 if attachments && attachment = attachments.detect {|a| a.filename == name }
517 513 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
518 514 :class => 'attachment'
519 515 end
520 516 end
521 517 end
522 518 end
523 519 leading + (link || "#{prefix}#{sep}#{oid}")
524 520 end
525 521
526 522 text
527 523 end
528 524
529 525 # Same as Rails' simple_format helper without using paragraphs
530 526 def simple_format_without_paragraph(text)
531 527 text.to_s.
532 528 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
533 529 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
534 530 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
535 531 end
536 532
537 533 def lang_options_for_select(blank=true)
538 534 (blank ? [["(auto)", ""]] : []) +
539 535 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
540 536 end
541 537
542 538 def label_tag_for(name, option_tags = nil, options = {})
543 539 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
544 540 content_tag("label", label_text)
545 541 end
546 542
547 543 def labelled_tabular_form_for(name, object, options, &proc)
548 544 options[:html] ||= {}
549 545 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
550 546 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
551 547 end
552 548
553 549 def back_url_hidden_field_tag
554 550 back_url = params[:back_url] || request.env['HTTP_REFERER']
555 551 back_url = CGI.unescape(back_url.to_s)
556 552 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
557 553 end
558 554
559 555 def check_all_links(form_name)
560 556 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
561 557 " | " +
562 558 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
563 559 end
564 560
565 561 def progress_bar(pcts, options={})
566 562 pcts = [pcts, pcts] unless pcts.is_a?(Array)
567 563 pcts[1] = pcts[1] - pcts[0]
568 564 pcts << (100 - pcts[1] - pcts[0])
569 565 width = options[:width] || '100px;'
570 566 legend = options[:legend] || ''
571 567 content_tag('table',
572 568 content_tag('tr',
573 569 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
574 570 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
575 571 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
576 572 ), :class => 'progress', :style => "width: #{width};") +
577 573 content_tag('p', legend, :class => 'pourcent')
578 574 end
579 575
580 576 def context_menu_link(name, url, options={})
581 577 options[:class] ||= ''
582 578 if options.delete(:selected)
583 579 options[:class] << ' icon-checked disabled'
584 580 options[:disabled] = true
585 581 end
586 582 if options.delete(:disabled)
587 583 options.delete(:method)
588 584 options.delete(:confirm)
589 585 options.delete(:onclick)
590 586 options[:class] << ' disabled'
591 587 url = '#'
592 588 end
593 589 link_to name, url, options
594 590 end
595 591
596 592 def calendar_for(field_id)
597 593 include_calendar_headers_tags
598 594 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
599 595 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
600 596 end
601 597
602 598 def include_calendar_headers_tags
603 599 unless @calendar_headers_tags_included
604 600 @calendar_headers_tags_included = true
605 601 content_for :header_tags do
606 602 javascript_include_tag('calendar/calendar') +
607 603 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
608 604 javascript_include_tag('calendar/calendar-setup') +
609 605 stylesheet_link_tag('calendar')
610 606 end
611 607 end
612 608 end
613 609
614 610 def content_for(name, content = nil, &block)
615 611 @has_content ||= {}
616 612 @has_content[name] = true
617 613 super(name, content, &block)
618 614 end
619 615
620 616 def has_content?(name)
621 617 (@has_content && @has_content[name]) || false
622 618 end
623 619
624 620 # Returns the avatar image tag for the given +user+ if avatars are enabled
625 621 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
626 622 def avatar(user, options = { })
627 623 if Setting.gravatar_enabled?
628 624 email = nil
629 625 if user.respond_to?(:mail)
630 626 email = user.mail
631 627 elsif user.to_s =~ %r{<(.+?)>}
632 628 email = $1
633 629 end
634 630 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
635 631 end
636 632 end
637 633
638 634 private
639 635
640 636 def wiki_helper
641 637 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
642 638 extend helper
643 639 return self
644 640 end
645 641
646 642 def link_to_remote_content_update(text, url_params)
647 643 link_to_remote(text,
648 644 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
649 645 {:href => url_for(:params => url_params)}
650 646 )
651 647 end
652 648
653 649 end
@@ -1,316 +1,316
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 29 has_many :time_entries, :dependent => :delete_all
30 30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31 31
32 32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34 34
35 35 acts_as_attachable :after_remove => :attachment_removed
36 36 acts_as_customizable
37 37 acts_as_watchable
38 38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 39 :include => [:project, :journals],
40 40 # sort by id so that limited eager loading doesn't break with postgresql
41 41 :order_column => "#{table_name}.id"
42 42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
43 43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 45
46 46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 47 :author_key => :author_id
48 48
49 49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 50 validates_length_of :subject, :maximum => 255
51 51 validates_inclusion_of :done_ratio, :in => 0..100
52 52 validates_numericality_of :estimated_hours, :allow_nil => true
53 53
54 54 named_scope :visible, lambda {|*args| { :include => :project,
55 55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56 56
57 57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
58 58
59 59 after_save :create_journal
60 60
61 61 # Returns true if usr or current user is allowed to view the issue
62 62 def visible?(usr=nil)
63 63 (usr || User.current).allowed_to?(:view_issues, self.project)
64 64 end
65 65
66 66 def after_initialize
67 67 if new_record?
68 68 # set default values for new records only
69 69 self.status ||= IssueStatus.default
70 70 self.priority ||= Enumeration.priorities.default
71 71 end
72 72 end
73 73
74 74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
75 75 def available_custom_fields
76 76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
77 77 end
78 78
79 79 def copy_from(arg)
80 80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
81 81 self.attributes = issue.attributes.dup
82 82 self.custom_values = issue.custom_values.collect {|v| v.clone}
83 83 self
84 84 end
85 85
86 86 # Moves/copies an issue to a new project and tracker
87 87 # Returns the moved/copied issue on success, false on failure
88 88 def move_to(new_project, new_tracker = nil, options = {})
89 89 options ||= {}
90 90 issue = options[:copy] ? self.clone : self
91 91 transaction do
92 92 if new_project && issue.project_id != new_project.id
93 93 # delete issue relations
94 94 unless Setting.cross_project_issue_relations?
95 95 issue.relations_from.clear
96 96 issue.relations_to.clear
97 97 end
98 98 # issue is moved to another project
99 99 # reassign to the category with same name if any
100 100 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
101 101 issue.category = new_category
102 102 issue.fixed_version = nil
103 103 issue.project = new_project
104 104 end
105 105 if new_tracker
106 106 issue.tracker = new_tracker
107 107 end
108 108 if options[:copy]
109 109 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
110 110 issue.status = self.status
111 111 end
112 112 if issue.save
113 113 unless options[:copy]
114 114 # Manually update project_id on related time entries
115 115 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
116 116 end
117 117 else
118 118 Issue.connection.rollback_db_transaction
119 119 return false
120 120 end
121 121 end
122 122 return issue
123 123 end
124 124
125 125 def priority_id=(pid)
126 126 self.priority = nil
127 127 write_attribute(:priority_id, pid)
128 128 end
129 129
130 130 def estimated_hours=(h)
131 131 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
132 132 end
133 133
134 134 def validate
135 135 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
136 136 errors.add :due_date, :not_a_date
137 137 end
138 138
139 139 if self.due_date and self.start_date and self.due_date < self.start_date
140 140 errors.add :due_date, :greater_than_start_date
141 141 end
142 142
143 143 if start_date && soonest_start && start_date < soonest_start
144 144 errors.add :start_date, :invalid
145 145 end
146 146 end
147 147
148 148 def validate_on_create
149 149 errors.add :tracker_id, :invalid unless project.trackers.include?(tracker)
150 150 end
151 151
152 152 def before_create
153 153 # default assignment based on category
154 154 if assigned_to.nil? && category && category.assigned_to
155 155 self.assigned_to = category.assigned_to
156 156 end
157 157 end
158 158
159 159 def after_save
160 160 # Reload is needed in order to get the right status
161 161 reload
162 162
163 163 # Update start/due dates of following issues
164 164 relations_from.each(&:set_issue_to_dates)
165 165
166 166 # Close duplicates if the issue was closed
167 167 if @issue_before_change && !@issue_before_change.closed? && self.closed?
168 168 duplicates.each do |duplicate|
169 169 # Reload is need in case the duplicate was updated by a previous duplicate
170 170 duplicate.reload
171 171 # Don't re-close it if it's already closed
172 172 next if duplicate.closed?
173 173 # Same user and notes
174 174 duplicate.init_journal(@current_journal.user, @current_journal.notes)
175 175 duplicate.update_attribute :status, self.status
176 176 end
177 177 end
178 178 end
179 179
180 180 def init_journal(user, notes = "")
181 181 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
182 182 @issue_before_change = self.clone
183 183 @issue_before_change.status = self.status
184 184 @custom_values_before_change = {}
185 185 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
186 186 # Make sure updated_on is updated when adding a note.
187 187 updated_on_will_change!
188 188 @current_journal
189 189 end
190 190
191 191 # Return true if the issue is closed, otherwise false
192 192 def closed?
193 193 self.status.is_closed?
194 194 end
195 195
196 196 # Returns true if the issue is overdue
197 197 def overdue?
198 198 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
199 199 end
200 200
201 201 # Users the issue can be assigned to
202 202 def assignable_users
203 203 project.assignable_users
204 204 end
205 205
206 206 # Returns an array of status that user is able to apply
207 207 def new_statuses_allowed_to(user)
208 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
208 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
209 209 statuses << status unless statuses.empty?
210 210 statuses.uniq.sort
211 211 end
212 212
213 213 # Returns the mail adresses of users that should be notified for the issue
214 214 def recipients
215 215 recipients = project.recipients
216 216 # Author and assignee are always notified unless they have been locked
217 217 recipients << author.mail if author && author.active?
218 218 recipients << assigned_to.mail if assigned_to && assigned_to.active?
219 219 recipients.compact.uniq
220 220 end
221 221
222 222 # Returns the total number of hours spent on this issue.
223 223 #
224 224 # Example:
225 225 # spent_hours => 0
226 226 # spent_hours => 50
227 227 def spent_hours
228 228 @spent_hours ||= time_entries.sum(:hours) || 0
229 229 end
230 230
231 231 def relations
232 232 (relations_from + relations_to).sort
233 233 end
234 234
235 235 def all_dependent_issues
236 236 dependencies = []
237 237 relations_from.each do |relation|
238 238 dependencies << relation.issue_to
239 239 dependencies += relation.issue_to.all_dependent_issues
240 240 end
241 241 dependencies
242 242 end
243 243
244 244 # Returns an array of issues that duplicate this one
245 245 def duplicates
246 246 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
247 247 end
248 248
249 249 # Returns the due date or the target due date if any
250 250 # Used on gantt chart
251 251 def due_before
252 252 due_date || (fixed_version ? fixed_version.effective_date : nil)
253 253 end
254 254
255 255 # Returns the time scheduled for this issue.
256 256 #
257 257 # Example:
258 258 # Start Date: 2/26/09, End Date: 3/04/09
259 259 # duration => 6
260 260 def duration
261 261 (start_date && due_date) ? due_date - start_date : 0
262 262 end
263 263
264 264 def soonest_start
265 265 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
266 266 end
267 267
268 268 def to_s
269 269 "#{tracker} ##{id}: #{subject}"
270 270 end
271 271
272 272 # Returns a string of css classes that apply to the issue
273 273 def css_classes
274 274 s = "issue status-#{status.position} priority-#{priority.position}"
275 275 s << ' closed' if closed?
276 276 s << ' overdue' if overdue?
277 277 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
278 278 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
279 279 s
280 280 end
281 281
282 282 private
283 283
284 284 # Callback on attachment deletion
285 285 def attachment_removed(obj)
286 286 journal = init_journal(User.current)
287 287 journal.details << JournalDetail.new(:property => 'attachment',
288 288 :prop_key => obj.id,
289 289 :old_value => obj.filename)
290 290 journal.save
291 291 end
292 292
293 293 # Saves the changes in a Journal
294 294 # Called after_save
295 295 def create_journal
296 296 if @current_journal
297 297 # attributes changes
298 298 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
299 299 @current_journal.details << JournalDetail.new(:property => 'attr',
300 300 :prop_key => c,
301 301 :old_value => @issue_before_change.send(c),
302 302 :value => send(c)) unless send(c)==@issue_before_change.send(c)
303 303 }
304 304 # custom fields changes
305 305 custom_values.each {|c|
306 306 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
307 307 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
308 308 @current_journal.details << JournalDetail.new(:property => 'cf',
309 309 :prop_key => c.custom_field_id,
310 310 :old_value => @custom_values_before_change[c.custom_field_id],
311 311 :value => c.value)
312 312 }
313 313 @current_journal.save
314 314 end
315 315 end
316 316 end
@@ -1,69 +1,79
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueStatus < ActiveRecord::Base
19 19 before_destroy :check_integrity
20 20 has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all
21 21 acts_as_list
22 22
23 23 validates_presence_of :name
24 24 validates_uniqueness_of :name
25 25 validates_length_of :name, :maximum => 30
26 26 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
27 27
28 28 def after_save
29 29 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
30 30 end
31 31
32 32 # Returns the default status for new issues
33 33 def self.default
34 34 find(:first, :conditions =>["is_default=?", true])
35 35 end
36 36
37 37 # Returns an array of all statuses the given role can switch to
38 38 # Uses association cache when called more than one time
39 def new_statuses_allowed_to(role, tracker)
40 new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker
41 new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : []
39 def new_statuses_allowed_to(roles, tracker)
40 if roles && tracker
41 role_ids = roles.collect(&:id)
42 new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
43 else
44 []
45 end
42 46 end
43 47
44 48 # Same thing as above but uses a database query
45 49 # More efficient than the previous method if called just once
46 def find_new_statuses_allowed_to(role, tracker)
47 new_statuses = workflows.find(:all,
48 :include => :new_status,
49 :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker
50 new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : []
50 def find_new_statuses_allowed_to(roles, tracker)
51 if roles && tracker
52 workflows.find(:all,
53 :include => :new_status,
54 :conditions => { :role_id => roles.collect(&:id),
55 :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
56 else
57 []
58 end
51 59 end
52 60
53 def new_status_allowed_to?(status, role, tracker)
54 status && role && tracker ?
55 !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? :
61 def new_status_allowed_to?(status, roles, tracker)
62 if status && roles && tracker
63 !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
64 else
56 65 false
66 end
57 67 end
58 68
59 69 def <=>(status)
60 70 position <=> status.position
61 71 end
62 72
63 73 def to_s; name end
64 74
65 75 private
66 76 def check_integrity
67 77 raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
68 78 end
69 79 end
@@ -1,52 +1,56
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Member < ActiveRecord::Base
19 19 belongs_to :user
20 belongs_to :role
20 has_many :member_roles, :dependent => :delete_all
21 has_many :roles, :through => :member_roles
21 22 belongs_to :project
22 23
23 validates_presence_of :role, :user, :project
24 validates_presence_of :user, :project
24 25 validates_uniqueness_of :user_id, :scope => :project_id
25
26 def validate
27 errors.add :role_id, :invalid if role && !role.member?
28 end
29 26
30 27 def name
31 28 self.user.name
32 29 end
33 30
34 31 # Sets user by login
35 32 def user_login=(login)
36 33 login = login.to_s
37 34 unless login.blank?
38 35 if (u = User.find_by_login(login))
39 36 self.user = u
40 37 end
41 38 end
42 39 end
43 40
44 41 def <=>(member)
45 role == member.role ? (user <=> member.user) : (role <=> member.role)
42 a, b = roles.sort.first, member.roles.sort.first
43 a == b ? (user <=> member.user) : (a <=> b)
46 44 end
47 45
48 46 def before_destroy
49 47 # remove category based auto assignments for this member
50 48 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
51 49 end
50
51 protected
52
53 def validate
54 errors.add_to_base "Role can't be blank" if roles.empty?
55 end
52 56 end
@@ -1,397 +1,399
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 24 has_many :users, :through => :members
25 25 has_many :enabled_modules, :dependent => :delete_all
26 26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 28 has_many :issue_changes, :through => :issues, :source => :journals
29 29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 30 has_many :time_entries, :dependent => :delete_all
31 31 has_many :queries, :dependent => :delete_all
32 32 has_many :documents, :dependent => :destroy
33 33 has_many :news, :dependent => :delete_all, :include => :author
34 34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 36 has_one :repository, :dependent => :destroy
37 37 has_many :changesets, :through => :repository
38 38 has_one :wiki, :dependent => :destroy
39 39 # Custom field for the project issues
40 40 has_and_belongs_to_many :issue_custom_fields,
41 41 :class_name => 'IssueCustomField',
42 42 :order => "#{CustomField.table_name}.position",
43 43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 44 :association_foreign_key => 'custom_field_id'
45 45
46 46 acts_as_nested_set :order => 'name', :dependent => :destroy
47 47 acts_as_attachable :view_permission => :view_files,
48 48 :delete_permission => :manage_files
49 49
50 50 acts_as_customizable
51 51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
52 52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
53 53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
54 54 :author => nil
55 55
56 56 attr_protected :status, :enabled_module_names
57 57
58 58 validates_presence_of :name, :identifier
59 59 validates_uniqueness_of :name, :identifier
60 60 validates_associated :repository, :wiki
61 61 validates_length_of :name, :maximum => 30
62 62 validates_length_of :homepage, :maximum => 255
63 63 validates_length_of :identifier, :in => 1..20
64 64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
65 65
66 66 before_destroy :delete_all_members
67 67
68 68 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
69 69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
70 70 named_scope :public, { :conditions => { :is_public => true } }
71 71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
72 72
73 73 def identifier=(identifier)
74 74 super unless identifier_frozen?
75 75 end
76 76
77 77 def identifier_frozen?
78 78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
79 79 end
80 80
81 81 def issues_with_subprojects(include_subprojects=false)
82 82 conditions = nil
83 83 if include_subprojects
84 84 ids = [id] + descendants.collect(&:id)
85 85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
86 86 end
87 87 conditions ||= ["#{Project.table_name}.id = ?", id]
88 88 # Quick and dirty fix for Rails 2 compatibility
89 89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
90 90 Version.send(:with_scope, :find => { :conditions => conditions }) do
91 91 yield
92 92 end
93 93 end
94 94 end
95 95
96 96 # returns latest created projects
97 97 # non public projects will be returned only if user is a member of those
98 98 def self.latest(user=nil, count=5)
99 99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
100 100 end
101 101
102 102 # Returns a SQL :conditions string used to find all active projects for the specified user.
103 103 #
104 104 # Examples:
105 105 # Projects.visible_by(admin) => "projects.status = 1"
106 106 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
107 107 def self.visible_by(user=nil)
108 108 user ||= User.current
109 109 if user && user.admin?
110 110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
111 111 elsif user && user.memberships.any?
112 112 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
113 113 else
114 114 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
115 115 end
116 116 end
117 117
118 118 def self.allowed_to_condition(user, permission, options={})
119 119 statements = []
120 120 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
121 121 if perm = Redmine::AccessControl.permission(permission)
122 122 unless perm.project_module.nil?
123 123 # If the permission belongs to a project module, make sure the module is enabled
124 124 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
125 125 end
126 126 end
127 127 if options[:project]
128 128 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
129 129 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
130 130 base_statement = "(#{project_statement}) AND (#{base_statement})"
131 131 end
132 132 if user.admin?
133 133 # no restriction
134 134 else
135 135 statements << "1=0"
136 136 if user.logged?
137 137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
138 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
138 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
139 139 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
140 140 elsif Role.anonymous.allowed_to?(permission)
141 141 # anonymous user allowed on public project
142 142 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
143 143 else
144 144 # anonymous user is not authorized
145 145 end
146 146 end
147 147 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
148 148 end
149 149
150 150 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
151 151 #
152 152 # Examples:
153 153 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
154 154 # project.project_condition(false) => "projects.id = 1"
155 155 def project_condition(with_subprojects)
156 156 cond = "#{Project.table_name}.id = #{id}"
157 157 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
158 158 cond
159 159 end
160 160
161 161 def self.find(*args)
162 162 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
163 163 project = find_by_identifier(*args)
164 164 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
165 165 project
166 166 else
167 167 super
168 168 end
169 169 end
170 170
171 171 def to_param
172 172 # id is used for projects with a numeric identifier (compatibility)
173 173 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
174 174 end
175 175
176 176 def active?
177 177 self.status == STATUS_ACTIVE
178 178 end
179 179
180 180 # Archives the project and its descendants recursively
181 181 def archive
182 182 # Archive subprojects if any
183 183 children.each do |subproject|
184 184 subproject.archive
185 185 end
186 186 update_attribute :status, STATUS_ARCHIVED
187 187 end
188 188
189 189 # Unarchives the project
190 190 # All its ancestors must be active
191 191 def unarchive
192 192 return false if ancestors.detect {|a| !a.active?}
193 193 update_attribute :status, STATUS_ACTIVE
194 194 end
195 195
196 196 # Returns an array of projects the project can be moved to
197 197 def possible_parents
198 198 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
199 199 end
200 200
201 201 # Sets the parent of the project
202 202 # Argument can be either a Project, a String, a Fixnum or nil
203 203 def set_parent!(p)
204 204 unless p.nil? || p.is_a?(Project)
205 205 if p.to_s.blank?
206 206 p = nil
207 207 else
208 208 p = Project.find_by_id(p)
209 209 return false unless p
210 210 end
211 211 end
212 212 if p == parent && !p.nil?
213 213 # Nothing to do
214 214 true
215 215 elsif p.nil? || (p.active? && move_possible?(p))
216 216 # Insert the project so that target's children or root projects stay alphabetically sorted
217 217 sibs = (p.nil? ? self.class.roots : p.children)
218 218 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
219 219 if to_be_inserted_before
220 220 move_to_left_of(to_be_inserted_before)
221 221 elsif p.nil?
222 222 if sibs.empty?
223 223 # move_to_root adds the project in first (ie. left) position
224 224 move_to_root
225 225 else
226 226 move_to_right_of(sibs.last) unless self == sibs.last
227 227 end
228 228 else
229 229 # move_to_child_of adds the project in last (ie.right) position
230 230 move_to_child_of(p)
231 231 end
232 232 true
233 233 else
234 234 # Can not move to the given target
235 235 false
236 236 end
237 237 end
238 238
239 239 # Returns an array of the trackers used by the project and its active sub projects
240 240 def rolled_up_trackers
241 241 @rolled_up_trackers ||=
242 242 Tracker.find(:all, :include => :projects,
243 243 :select => "DISTINCT #{Tracker.table_name}.*",
244 244 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
245 245 :order => "#{Tracker.table_name}.position")
246 246 end
247 247
248 248 # Deletes all project's members
249 249 def delete_all_members
250 me, mr = Member.table_name, MemberRole.table_name
251 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
250 252 Member.delete_all(['project_id = ?', id])
251 253 end
252 254
253 255 # Users issues can be assigned to
254 256 def assignable_users
255 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
257 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
256 258 end
257 259
258 260 # Returns the mail adresses of users that should be always notified on project events
259 261 def recipients
260 262 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
261 263 end
262 264
263 265 # Returns an array of all custom fields enabled for project issues
264 266 # (explictly associated custom fields and custom fields enabled for all projects)
265 267 def all_issue_custom_fields
266 268 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
267 269 end
268 270
269 271 def project
270 272 self
271 273 end
272 274
273 275 def <=>(project)
274 276 name.downcase <=> project.name.downcase
275 277 end
276 278
277 279 def to_s
278 280 name
279 281 end
280 282
281 283 # Returns a short description of the projects (first lines)
282 284 def short_description(length = 255)
283 285 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
284 286 end
285 287
286 288 # Return true if this project is allowed to do the specified action.
287 289 # action can be:
288 290 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
289 291 # * a permission Symbol (eg. :edit_project)
290 292 def allows_to?(action)
291 293 if action.is_a? Hash
292 294 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
293 295 else
294 296 allowed_permissions.include? action
295 297 end
296 298 end
297 299
298 300 def module_enabled?(module_name)
299 301 module_name = module_name.to_s
300 302 enabled_modules.detect {|m| m.name == module_name}
301 303 end
302 304
303 305 def enabled_module_names=(module_names)
304 306 if module_names && module_names.is_a?(Array)
305 307 module_names = module_names.collect(&:to_s)
306 308 # remove disabled modules
307 309 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
308 310 # add new modules
309 311 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
310 312 else
311 313 enabled_modules.clear
312 314 end
313 315 end
314 316
315 317 # Returns an auto-generated project identifier based on the last identifier used
316 318 def self.next_identifier
317 319 p = Project.find(:first, :order => 'created_on DESC')
318 320 p.nil? ? nil : p.identifier.to_s.succ
319 321 end
320 322
321 323 # Copies and saves the Project instance based on the +project+.
322 324 # Will duplicate the source project's:
323 325 # * Issues
324 326 # * Members
325 327 # * Queries
326 328 def copy(project)
327 329 project = project.is_a?(Project) ? project : Project.find(project)
328 330
329 331 Project.transaction do
330 332 # Issues
331 333 project.issues.each do |issue|
332 334 new_issue = Issue.new
333 335 new_issue.copy_from(issue)
334 336 self.issues << new_issue
335 337 end
336 338
337 339 # Members
338 340 project.members.each do |member|
339 341 new_member = Member.new
340 342 new_member.attributes = member.attributes.dup.except("project_id")
341 343 new_member.project = self
342 344 self.members << new_member
343 345 end
344 346
345 347 # Queries
346 348 project.queries.each do |query|
347 349 new_query = Query.new
348 350 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
349 351 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
350 352 new_query.project = self
351 353 self.queries << new_query
352 354 end
353 355
354 356 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
355 357 self.save
356 358 end
357 359 end
358 360
359 361
360 362 # Copies +project+ and returns the new instance. This will not save
361 363 # the copy
362 364 def self.copy_from(project)
363 365 begin
364 366 project = project.is_a?(Project) ? project : Project.find(project)
365 367 if project
366 368 # clear unique attributes
367 369 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
368 370 copy = Project.new(attributes)
369 371 copy.enabled_modules = project.enabled_modules
370 372 copy.trackers = project.trackers
371 373 copy.custom_values = project.custom_values.collect {|v| v.clone}
372 374 return copy
373 375 else
374 376 return nil
375 377 end
376 378 rescue ActiveRecord::RecordNotFound
377 379 return nil
378 380 end
379 381 end
380 382
381 383 protected
382 384 def validate
383 385 errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
384 386 end
385 387
386 388 private
387 389 def allowed_permissions
388 390 @allowed_permissions ||= begin
389 391 module_names = enabled_modules.collect {|m| m.name}
390 392 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
391 393 end
392 394 end
393 395
394 396 def allowed_actions
395 397 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
396 398 end
397 399 end
@@ -1,147 +1,152
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Role < ActiveRecord::Base
19 19 # Built-in roles
20 20 BUILTIN_NON_MEMBER = 1
21 21 BUILTIN_ANONYMOUS = 2
22 22
23 23 named_scope :builtin, lambda { |*args|
24 24 compare = 'not' if args.first == true
25 25 { :conditions => "#{compare} builtin = 0" }
26 26 }
27 27
28 28 before_destroy :check_deletable
29 29 has_many :workflows, :dependent => :delete_all do
30 30 def copy(role)
31 31 raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
32 32 raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
33 33 clear
34 34 connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
35 35 " SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
36 36 " FROM #{Workflow.table_name}" +
37 37 " WHERE role_id = #{role.id}"
38 38 end
39 39 end
40 40
41 has_many :members
41 has_many :member_roles, :dependent => :destroy
42 has_many :members, :through => :member_roles
42 43 acts_as_list
43 44
44 45 serialize :permissions, Array
45 46 attr_protected :builtin
46 47
47 48 validates_presence_of :name
48 49 validates_uniqueness_of :name
49 50 validates_length_of :name, :maximum => 30
50 51 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
51 52
52 53 def permissions
53 54 read_attribute(:permissions) || []
54 55 end
55 56
56 57 def permissions=(perms)
57 58 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
58 59 write_attribute(:permissions, perms)
59 60 end
60 61
61 62 def add_permission!(*perms)
62 63 self.permissions = [] unless permissions.is_a?(Array)
63 64
64 65 permissions_will_change!
65 66 perms.each do |p|
66 67 p = p.to_sym
67 68 permissions << p unless permissions.include?(p)
68 69 end
69 70 save!
70 71 end
71 72
72 73 def remove_permission!(*perms)
73 74 return unless permissions.is_a?(Array)
74 75 permissions_will_change!
75 76 perms.each { |p| permissions.delete(p.to_sym) }
76 77 save!
77 78 end
78 79
79 80 # Returns true if the role has the given permission
80 81 def has_permission?(perm)
81 82 !permissions.nil? && permissions.include?(perm.to_sym)
82 83 end
83 84
84 85 def <=>(role)
85 position <=> role.position
86 role ? position <=> role.position : -1
87 end
88
89 def to_s
90 name
86 91 end
87 92
88 93 # Return true if the role is a builtin role
89 94 def builtin?
90 95 self.builtin != 0
91 96 end
92 97
93 98 # Return true if the role is a project member role
94 99 def member?
95 100 !self.builtin?
96 101 end
97 102
98 103 # Return true if role is allowed to do the specified action
99 104 # action can be:
100 105 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
101 106 # * a permission Symbol (eg. :edit_project)
102 107 def allowed_to?(action)
103 108 if action.is_a? Hash
104 109 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
105 110 else
106 111 allowed_permissions.include? action
107 112 end
108 113 end
109 114
110 115 # Return all the permissions that can be given to the role
111 116 def setable_permissions
112 117 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
113 118 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
114 119 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
115 120 setable_permissions
116 121 end
117 122
118 123 # Find all the roles that can be given to a project member
119 124 def self.find_all_givable
120 125 find(:all, :conditions => {:builtin => 0}, :order => 'position')
121 126 end
122 127
123 128 # Return the builtin 'non member' role
124 129 def self.non_member
125 130 find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER}) || raise('Missing non-member builtin role.')
126 131 end
127 132
128 133 # Return the builtin 'anonymous' role
129 134 def self.anonymous
130 135 find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS}) || raise('Missing anonymous builtin role.')
131 136 end
132 137
133 138
134 139 private
135 140 def allowed_permissions
136 141 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
137 142 end
138 143
139 144 def allowed_actions
140 145 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
141 146 end
142 147
143 148 def check_deletable
144 149 raise "Can't delete role" if members.any?
145 150 raise "Can't delete builtin role" if builtin?
146 151 end
147 152 end
@@ -1,334 +1,338
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < ActiveRecord::Base
21 21
22 22 # Account statuses
23 23 STATUS_ANONYMOUS = 0
24 24 STATUS_ACTIVE = 1
25 25 STATUS_REGISTERED = 2
26 26 STATUS_LOCKED = 3
27 27
28 28 USER_FORMATS = {
29 29 :firstname_lastname => '#{firstname} #{lastname}',
30 30 :firstname => '#{firstname}',
31 31 :lastname_firstname => '#{lastname} #{firstname}',
32 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 33 :username => '#{login}'
34 34 }
35 35
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
37 37 has_many :members, :dependent => :delete_all
38 38 has_many :projects, :through => :memberships
39 39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 40 has_many :changesets, :dependent => :nullify
41 41 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
42 42 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
43 43 belongs_to :auth_source
44 44
45 45 # Active non-anonymous users scope
46 46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47 47
48 48 acts_as_customizable
49 49
50 50 attr_accessor :password, :password_confirmation
51 51 attr_accessor :last_before_login_on
52 52 # Prevents unauthorized assignments
53 53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
54 54
55 55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
57 57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
58 58 # Login must contain lettres, numbers, underscores only
59 59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 60 validates_length_of :login, :maximum => 30
61 61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 62 validates_length_of :firstname, :lastname, :maximum => 30
63 63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 64 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 65 validates_confirmation_of :password, :allow_nil => true
66 66
67 67 def before_create
68 68 self.mail_notification = false
69 69 true
70 70 end
71 71
72 72 def before_save
73 73 # update hashed_password if password was set
74 74 self.hashed_password = User.hash_password(self.password) if self.password
75 75 end
76 76
77 77 def reload(*args)
78 78 @name = nil
79 79 super
80 80 end
81 81
82 82 def identity_url=(url)
83 83 if url.blank?
84 84 write_attribute(:identity_url, '')
85 85 else
86 86 begin
87 87 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
88 88 rescue OpenIdAuthentication::InvalidOpenId
89 89 # Invlaid url, don't save
90 90 end
91 91 end
92 92 self.read_attribute(:identity_url)
93 93 end
94 94
95 95 # Returns the user that matches provided login and password, or nil
96 96 def self.try_to_login(login, password)
97 97 # Make sure no one can sign in with an empty password
98 98 return nil if password.to_s.empty?
99 99 user = find(:first, :conditions => ["login=?", login])
100 100 if user
101 101 # user is already in local database
102 102 return nil if !user.active?
103 103 if user.auth_source
104 104 # user has an external authentication method
105 105 return nil unless user.auth_source.authenticate(login, password)
106 106 else
107 107 # authentication with local password
108 108 return nil unless User.hash_password(password) == user.hashed_password
109 109 end
110 110 else
111 111 # user is not yet registered, try to authenticate with available sources
112 112 attrs = AuthSource.authenticate(login, password)
113 113 if attrs
114 114 user = new(*attrs)
115 115 user.login = login
116 116 user.language = Setting.default_language
117 117 if user.save
118 118 user.reload
119 119 logger.info("User '#{user.login}' created from the LDAP") if logger
120 120 end
121 121 end
122 122 end
123 123 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
124 124 user
125 125 rescue => text
126 126 raise text
127 127 end
128 128
129 129 # Returns the user who matches the given autologin +key+ or nil
130 130 def self.try_to_autologin(key)
131 131 token = Token.find_by_action_and_value('autologin', key)
132 132 if token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
133 133 token.user.update_attribute(:last_login_on, Time.now)
134 134 token.user
135 135 end
136 136 end
137 137
138 138 # Return user's full name for display
139 139 def name(formatter = nil)
140 140 if formatter
141 141 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
142 142 else
143 143 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
144 144 end
145 145 end
146 146
147 147 def active?
148 148 self.status == STATUS_ACTIVE
149 149 end
150 150
151 151 def registered?
152 152 self.status == STATUS_REGISTERED
153 153 end
154 154
155 155 def locked?
156 156 self.status == STATUS_LOCKED
157 157 end
158 158
159 159 def check_password?(clear_password)
160 160 User.hash_password(clear_password) == self.hashed_password
161 161 end
162 162
163 163 # Generate and set a random password. Useful for automated user creation
164 164 # Based on Token#generate_token_value
165 165 #
166 166 def random_password
167 167 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
168 168 password = ''
169 169 40.times { |i| password << chars[rand(chars.size-1)] }
170 170 self.password = password
171 171 self.password_confirmation = password
172 172 self
173 173 end
174 174
175 175 def pref
176 176 self.preference ||= UserPreference.new(:user => self)
177 177 end
178 178
179 179 def time_zone
180 180 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
181 181 end
182 182
183 183 def wants_comments_in_reverse_order?
184 184 self.pref[:comments_sorting] == 'desc'
185 185 end
186 186
187 187 # Return user's RSS key (a 40 chars long string), used to access feeds
188 188 def rss_key
189 189 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
190 190 token.value
191 191 end
192 192
193 193 # Return an array of project ids for which the user has explicitly turned mail notifications on
194 194 def notified_projects_ids
195 195 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
196 196 end
197 197
198 198 def notified_project_ids=(ids)
199 199 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
200 200 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
201 201 @notified_projects_ids = nil
202 202 notified_projects_ids
203 203 end
204 204
205 205 def self.find_by_rss_key(key)
206 206 token = Token.find_by_value(key)
207 207 token && token.user.active? ? token.user : nil
208 208 end
209 209
210 210 # Makes find_by_mail case-insensitive
211 211 def self.find_by_mail(mail)
212 212 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
213 213 end
214 214
215 215 # Sort users by their display names
216 216 def <=>(user)
217 217 self.to_s.downcase <=> user.to_s.downcase
218 218 end
219 219
220 220 def to_s
221 221 name
222 222 end
223 223
224 224 def logged?
225 225 true
226 226 end
227 227
228 228 def anonymous?
229 229 !logged?
230 230 end
231 231
232 # Return user's role for project
233 def role_for_project(project)
232 # Return user's roles for project
233 def roles_for_project(project)
234 roles = []
234 235 # No role on archived projects
235 return nil unless project && project.active?
236 return roles unless project && project.active?
236 237 if logged?
237 238 # Find project membership
238 239 membership = memberships.detect {|m| m.project_id == project.id}
239 240 if membership
240 membership.role
241 roles = membership.roles
241 242 else
242 243 @role_non_member ||= Role.non_member
244 roles << @role_non_member
243 245 end
244 246 else
245 247 @role_anonymous ||= Role.anonymous
248 roles << @role_anonymous
246 249 end
250 roles
247 251 end
248 252
249 253 # Return true if the user is a member of project
250 254 def member_of?(project)
251 role_for_project(project).member?
255 !roles_for_project(project).detect {|role| role.member?}.nil?
252 256 end
253 257
254 258 # Return true if the user is allowed to do the specified action on project
255 259 # action can be:
256 260 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
257 261 # * a permission Symbol (eg. :edit_project)
258 262 def allowed_to?(action, project, options={})
259 263 if project
260 264 # No action allowed on archived projects
261 265 return false unless project.active?
262 266 # No action allowed on disabled modules
263 267 return false unless project.allows_to?(action)
264 268 # Admin users are authorized for anything else
265 269 return true if admin?
266 270
267 role = role_for_project(project)
268 return false unless role
269 role.allowed_to?(action) && (project.is_public? || role.member?)
271 roles = roles_for_project(project)
272 return false unless roles
273 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
270 274
271 275 elsif options[:global]
272 276 # authorize if user has at least one role that has this permission
273 roles = memberships.collect {|m| m.role}.uniq
277 roles = memberships.collect {|m| m.roles}.flatten.uniq
274 278 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
275 279 else
276 280 false
277 281 end
278 282 end
279 283
280 284 def self.current=(user)
281 285 @current_user = user
282 286 end
283 287
284 288 def self.current
285 289 @current_user ||= User.anonymous
286 290 end
287 291
288 292 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
289 293 # one anonymous user per database.
290 294 def self.anonymous
291 295 anonymous_user = AnonymousUser.find(:first)
292 296 if anonymous_user.nil?
293 297 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
294 298 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
295 299 end
296 300 anonymous_user
297 301 end
298 302
299 303 protected
300 304
301 305 def validate
302 306 # Password length validation based on setting
303 307 if !password.nil? && password.size < Setting.password_min_length.to_i
304 308 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
305 309 end
306 310 end
307 311
308 312 private
309 313
310 314 # Return password digest
311 315 def self.hash_password(clear_password)
312 316 Digest::SHA1.hexdigest(clear_password || "")
313 317 end
314 318 end
315 319
316 320 class AnonymousUser < User
317 321
318 322 def validate_on_create
319 323 # There should be only one AnonymousUser in the database
320 324 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
321 325 end
322 326
323 327 def available_custom_fields
324 328 []
325 329 end
326 330
327 331 # Overrides a few properties
328 332 def logged?; false end
329 333 def admin; false end
330 334 def name; 'Anonymous' end
331 335 def mail; nil end
332 336 def time_zone; nil end
333 337 def rss_key; nil end
334 338 end
@@ -1,68 +1,68
1 1 <div class="contextual">
2 2 <%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %>
3 3 </div>
4 4
5 5 <h2><%= avatar @user %> <%=h @user.name %></h2>
6 6
7 7 <div class="splitcontentleft">
8 8 <ul>
9 9 <% unless @user.pref.hide_mail %>
10 10 <li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li>
11 11 <% end %>
12 12 <% for custom_value in @custom_values %>
13 13 <% if !custom_value.value.empty? %>
14 14 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
15 15 <% end %>
16 16 <% end %>
17 17 <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
18 18 <% unless @user.last_login_on.nil? %>
19 19 <li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li>
20 20 <% end %>
21 21 </ul>
22 22
23 23 <% unless @memberships.empty? %>
24 24 <h3><%=l(:label_project_plural)%></h3>
25 25 <ul>
26 26 <% for membership in @memberships %>
27 27 <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %>
28 (<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)</li>
28 (<%=h membership.roles.collect(&:to_s).join(', ') %>, <%= format_date(membership.created_on) %>)</li>
29 29 <% end %>
30 30 </ul>
31 31 <% end %>
32 32 </div>
33 33
34 34 <div class="splitcontentright">
35 35
36 36 <% unless @events_by_day.empty? %>
37 37 <h3><%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :from => @events_by_day.keys.first %></h3>
38 38
39 39 <p>
40 40 <%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
41 41 </p>
42 42
43 43 <div id="activity">
44 44 <% @events_by_day.keys.sort.reverse.each do |day| %>
45 45 <h4><%= format_activity_day(day) %></h4>
46 46 <dl>
47 47 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
48 48 <dt class="<%= e.event_type %>">
49 49 <span class="time"><%= format_time(e.event_datetime, false) %></span>
50 50 <%= content_tag('span', h(e.project), :class => 'project') %>
51 51 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
52 52 <dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd>
53 53 <% end -%>
54 54 </dl>
55 55 <% end -%>
56 56 </div>
57 57
58 58 <% other_formats_links do |f| %>
59 59 <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :key => User.current.rss_key} %>
60 60 <% end %>
61 61
62 62 <% content_for :header_tags do %>
63 63 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %>
64 64 <% end %>
65 65 <% end %>
66 66 </div>
67 67
68 68 <% html_title @user.name %>
@@ -1,66 +1,76
1 1 <%= error_messages_for 'member' %>
2 2 <% roles = Role.find_all_givable
3 members = @project.members.find(:all, :include => [:role, :user]).sort %>
3 members = @project.members.find(:all, :include => [:roles, :user]).sort %>
4 4
5 5 <div class="splitcontentleft">
6 6 <% if members.any? %>
7 <table class="list">
7 <table class="list members">
8 8 <thead>
9 9 <th><%= l(:label_user) %></th>
10 <th><%= l(:label_role) %></th>
10 <th><%= l(:label_role_plural) %></th>
11 11 <th style="width:15%"></th>
12 12 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
13 13 </thead>
14 14 <tbody>
15 15 <% members.each do |member| %>
16 16 <% next if member.new_record? %>
17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %>">
18 <td><%=h member.name %></td>
19 <td align="center">
17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
18 <td class="user"><%= link_to_user member.user %></td>
19 <td class="roles">
20 <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
20 21 <% if authorize_for('members', 'edit') %>
21 <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member}, :method => :post) do |f| %>
22 <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, {}, :class => "small" %>
23 <%= submit_tag l(:button_change), :class => "small" %>
22 <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member},
23 :method => :post,
24 :html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %>
25 <p><% roles.each do |role| %>
26 <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role) %> <%=h role %></label><br />
27 <% end %></p>
28 <p><%= submit_tag l(:button_change), :class => "small" %>
29 <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p>
24 30 <% end %>
25 31 <% end %>
26 </td>
27 <td align="center">
32 </td>
33 <td class="buttons">
34 <%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
28 35 <%= link_to_remote l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
29 36 :method => :post
30 37 }, :title => l(:button_delete),
31 38 :class => 'icon icon-del' %>
32 </td>
33 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
39 </td>
40 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
34 41 </tr>
35 42 </tbody>
36 43 <% end; reset_cycle %>
37 44 </table>
38 45 <% else %>
39 46 <p class="nodata"><%= l(:label_no_data) %></p>
40 47 <% end %>
41 48 </div>
42 49
43 50
44 51 <% users_count = User.active.count - @project.users.count
45 52 users = (users_count < 300) ? User.active.find(:all, :limit => 200).sort - @project.users : [] %>
46 53
47 54 <div class="splitcontentright">
48 55 <% if roles.any? && users_count > 0 %>
49 56 <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
50 57 <fieldset><legend><%=l(:label_member_new)%></legend>
51 58 <p><%= text_field_tag 'member[user_login]', nil, :size => "40" %></p>
52 59 <div id="member_user_login_choices" class="autocomplete">sqd</div>
53 60 <%= javascript_tag "new Ajax.Autocompleter('member_user_login', 'member_user_login_choices', '#{ url_for(:controller => 'members', :action => 'autocomplete_for_member_login', :id => @project) }', { minChars: 1, frequency: 0.5, paramName: 'user' });" %>
54 61 <% unless users.empty? %>
55 62 <div>
56 63 <% users.each do |user| -%>
57 64 <label><%= check_box_tag 'member[user_ids][]', user.id, false %> <%= user %></label>
58 65 <% end -%>
59 66 </div>
60 67 <% end %>
61 <p><%= l(:label_role) %>: <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, :selected => nil %>
62 <%= submit_tag l(:button_add) %></p>
68 <p><%= l(:label_role_plural) %>:
69 <% roles.each do |role| %>
70 <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label>
71 <% end %></p>
72 <p><%= submit_tag l(:button_add) %></p>
63 73 </fieldset>
64 74 <% end %>
65 75 <% end %>
66 76 </div>
@@ -1,41 +1,41
1 1 <%= error_messages_for 'query' %>
2 2 <%= hidden_field_tag 'confirm', 1 %>
3 3
4 4 <div class="box">
5 5 <div class="tabular">
6 6 <p><label for="query_name"><%=l(:field_name)%></label>
7 7 <%= text_field 'query', 'name', :size => 80 %></p>
8 8
9 <% if User.current.admin? || (@project && current_role.allowed_to?(:manage_public_queries)) %>
9 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
10 10 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
11 11 <%= check_box 'query', 'is_public',
12 12 :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("query_is_for_all").checked = false; $("query_is_for_all").disabled = true;} else {$("query_is_for_all").disabled = false;}') %></p>
13 13 <% end %>
14 14
15 15 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
16 16 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
17 17 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
18 18
19 19 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
20 20 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
21 21 :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
22 22
23 23 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
24 24 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
25 25 </div>
26 26
27 27 <fieldset><legend><%= l(:label_filter_plural) %></legend>
28 28 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
29 29 </fieldset>
30 30
31 31 <fieldset><legend><%= l(:label_sort) %></legend>
32 32 <% 3.times do |i| %>
33 33 <%= i+1 %>: <%= select_tag("query[sort_criteria][#{i}][]",
34 34 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i))) %>
35 35 <%= select_tag("query[sort_criteria][#{i}][]",
36 36 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i))) %><br />
37 37 <% end %>
38 38 </fieldset>
39 39
40 40 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
41 41 </div>
@@ -1,40 +1,56
1 <% if @memberships.any? %>
1 <% roles = Role.find_all_givable %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
3
4 <div class="splitcontentleft">
5 <% if @user.memberships.any? %>
2 6 <table class="list memberships">
3 7 <thead>
4 8 <th><%= l(:label_project) %></th>
5 <th><%= l(:label_role) %></th>
9 <th><%= l(:label_role_plural) %></th>
6 10 <th style="width:15%"></th>
7 11 </thead>
8 12 <tbody>
9 <% @memberships.each do |membership| %>
13 <% @user.memberships.each do |membership| %>
10 14 <% next if membership.new_record? %>
11 <tr class="<%= cycle 'odd', 'even' %>">
12 <td><%=h membership.project %></td>
13 <td align="center">
14 <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %>
15 <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %>
16 <%= submit_tag l(:button_change), :class => "small" %>
15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 <td class="project"><%=h membership.project %></td>
17 <td class="roles">
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
19 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership },
20 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 <p><% roles.each do |role| %>
22 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <% end %></p>
24 <p><%= submit_tag l(:button_change) %>
25 <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
17 26 <% end %>
18 </td>
19 <td align="center">
20 <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %>
21 </td>
27 </td>
28 <td class="buttons">
29 <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
30 <%= link_to_remote l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
31 :method => :post },
32 :class => 'icon icon-del' %>
33 </td>
22 34 </tr>
23 35 </tbody>
24 36 <% end; reset_cycle %>
25 37 </table>
26 38 <% else %>
27 39 <p class="nodata"><%= l(:label_no_data) %></p>
28 40 <% end %>
41 </div>
29 42
30 <% if @projects.any? %>
31 <p>
32 <label><%=l(:label_project_new)%></label><br/>
33 <% form_tag({ :action => 'edit_membership', :id => @user }) do %>
34 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, @projects) %>
35 <%= l(:label_role) %>:
36 <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
37 <%= submit_tag l(:button_add) %>
43 <div class="splitcontentright">
44 <% if projects.any? %>
45 <fieldset><legend><%=l(:label_project_new)%></legend>
46 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user }) do %>
47 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
48 <p><%= l(:label_role_plural) %>:
49 <% roles.each do |role| %>
50 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
51 <% end %></p>
52 <p><%= submit_tag l(:button_add) %></p>
38 53 <% end %>
39 </p>
54 </fieldset>
40 55 <% end %>
56 </div>
@@ -1,66 +1,66
1 1 <div class="contextual">
2 2 <%= link_to l(:field_summary), :action => 'index' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_workflow)%></h2>
6 6
7 7 <p><%=l(:text_workflow_edit)%>:</p>
8 8
9 9 <% form_tag({}, :method => 'get') do %>
10 10 <p><label for="role_id"><%=l(:label_role)%>:</label>
11 11 <select name="role_id">
12 12 <%= options_from_collection_for_select @roles, "id", "name", (@role.id unless @role.nil?) %>
13 13 </select>
14 14
15 15 <label for="tracker_id"><%=l(:label_tracker)%>:</label>
16 16 <select name="tracker_id">
17 17 <%= options_from_collection_for_select @trackers, "id", "name", (@tracker.id unless @tracker.nil?) %>
18 18 </select>
19 19 <%= submit_tag l(:button_edit), :name => nil %>
20 20 </p>
21 21 <% end %>
22 22
23 23
24 24
25 25 <% unless @tracker.nil? or @role.nil? or @statuses.empty? %>
26 26 <% form_tag({}, :id => 'workflow_form' ) do %>
27 27 <%= hidden_field_tag 'tracker_id', @tracker.id %>
28 28 <%= hidden_field_tag 'role_id', @role.id %>
29 29 <table class="list">
30 30 <thead>
31 31 <tr>
32 32 <th align="left"><%=l(:label_current_status)%></th>
33 33 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
34 34 </tr>
35 35 <tr>
36 36 <td></td>
37 37 <% for new_status in @statuses %>
38 38 <td width="<%= 75 / @statuses.size %>%" align="center"><%= new_status.name %></td>
39 39 <% end %>
40 40 </tr>
41 41 </thead>
42 42 <tbody>
43 43 <% for old_status in @statuses %>
44 44 <tr class="<%= cycle("odd", "even") %>">
45 45 <td><%= old_status.name %></td>
46 <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to(@role, @tracker).collect(&:id) -%>
46 <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to([@role], @tracker).collect(&:id) -%>
47 47 <% for new_status in @statuses -%>
48 48 <td align="center">
49 49 <input type="checkbox"
50 50 name="issue_status[<%= old_status.id %>][]"
51 51 value="<%= new_status.id %>"
52 52 <%= 'checked="checked"' if new_status_ids_allowed.include? new_status.id %> />
53 53 </td>
54 54 <% end -%>
55 55 </tr>
56 56 <% end %>
57 57 </tbody>
58 58 </table>
59 59 <p><%= check_all_links 'workflow_form' %></p>
60 60
61 61 <%= submit_tag l(:button_save) %>
62 62 <% end %>
63 63
64 64 <% end %>
65 65
66 66 <% html_title(l(:label_workflow)) -%>
@@ -1,781 +1,785
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #wrapper {background: white;}
11 11
12 12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 13 #top-menu ul {margin: 0; padding: 0;}
14 14 #top-menu li {
15 15 float:left;
16 16 list-style-type:none;
17 17 margin: 0px 0px 0px 0px;
18 18 padding: 0px 0px 0px 0px;
19 19 white-space:nowrap;
20 20 }
21 21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 23
24 24 #account {float:right;}
25 25
26 26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 27 #header a {color:#f8f8f8;}
28 28 #header h1 a.ancestor { font-size: 80%; }
29 29 #quick-search {float:right;}
30 30
31 31 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
32 32 #main-menu ul {margin: 0; padding: 0;}
33 33 #main-menu li {
34 34 float:left;
35 35 list-style-type:none;
36 36 margin: 0px 2px 0px 0px;
37 37 padding: 0px 0px 0px 0px;
38 38 white-space:nowrap;
39 39 }
40 40 #main-menu li a {
41 41 display: block;
42 42 color: #fff;
43 43 text-decoration: none;
44 44 font-weight: bold;
45 45 margin: 0;
46 46 padding: 4px 10px 4px 10px;
47 47 }
48 48 #main-menu li a:hover {background:#759FCF; color:#fff;}
49 49 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
50 50
51 51 #main {background-color:#EEEEEE;}
52 52
53 53 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
54 54 * html #sidebar{ width: 17%; }
55 55 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
56 56 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
57 57 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
58 58
59 59 #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
60 60 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
61 61 html>body #content { min-height: 600px; }
62 62 * html body #content { height: 600px; } /* IE */
63 63
64 64 #main.nosidebar #sidebar{ display: none; }
65 65 #main.nosidebar #content{ width: auto; border-right: 0; }
66 66
67 67 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
68 68
69 69 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
70 70 #login-form table td {padding: 6px;}
71 71 #login-form label {font-weight: bold;}
72 72
73 73 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
74 74
75 75 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
76 76
77 77 /***** Links *****/
78 78 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
79 79 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
80 80 a img{ border: 0; }
81 81
82 82 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { text-decoration: line-through; }
83 83
84 84 /***** Tables *****/
85 85 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
86 86 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
87 87 table.list td { vertical-align: top; }
88 88 table.list td.id { width: 2%; text-align: center;}
89 89 table.list td.checkbox { width: 15px; padding: 0px;}
90 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
91 table.list td.buttons a { padding-right: 0.6em; }
90 92
91 93 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
92 94 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
93 95
94 96 tr.issue { text-align: center; white-space: nowrap; }
95 97 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
96 98 tr.issue td.subject { text-align: left; }
97 99 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
98 100
99 101 tr.entry { border: 1px solid #f8f8f8; }
100 102 tr.entry td { white-space: nowrap; }
101 103 tr.entry td.filename { width: 30%; }
102 104 tr.entry td.size { text-align: right; font-size: 90%; }
103 105 tr.entry td.revision, tr.entry td.author { text-align: center; }
104 106 tr.entry td.age { text-align: right; }
105 107 tr.entry.file td.filename a { margin-left: 16px; }
106 108
107 109 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
108 110 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
109 111
110 112 tr.changeset td.author { text-align: center; width: 15%; }
111 113 tr.changeset td.committed_on { text-align: center; width: 15%; }
112 114
113 115 table.files tr.file td { text-align: center; }
114 116 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
115 117 table.files tr.file td.digest { font-size: 80%; }
116 118
119 table.members td.roles, table.memberships td.roles { width: 45%; }
120
117 121 tr.message { height: 2.6em; }
118 122 tr.message td.last_message { font-size: 80%; }
119 123 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
120 124 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
121 125
122 126 tr.user td { width:13%; }
123 127 tr.user td.email { width:18%; }
124 128 tr.user td { white-space: nowrap; }
125 129 tr.user.locked, tr.user.registered { color: #aaa; }
126 130 tr.user.locked a, tr.user.registered a { color: #aaa; }
127 131
128 132 tr.time-entry { text-align: center; white-space: nowrap; }
129 133 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
130 134 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
131 135 td.hours .hours-dec { font-size: 0.9em; }
132 136
133 137 table.plugins td { vertical-align: middle; }
134 138 table.plugins td.configure { text-align: right; padding-right: 1em; }
135 139 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
136 140 table.plugins span.description { display: block; font-size: 0.9em; }
137 141 table.plugins span.url { display: block; font-size: 0.9em; }
138 142
139 143 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
140 144 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
141 145
142 146 table.list tbody tr:hover { background-color:#ffffdd; }
143 147 table.list tbody tr.group:hover { background-color:inherit; }
144 148 table td {padding:2px;}
145 149 table p {margin:0;}
146 150 .odd {background-color:#f6f7f8;}
147 151 .even {background-color: #fff;}
148 152
149 153 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
150 154 a.sort.asc { background-image: url(../images/sort_asc.png); }
151 155 a.sort.desc { background-image: url(../images/sort_desc.png); }
152 156
153 157 .highlight { background-color: #FCFD8D;}
154 158 .highlight.token-1 { background-color: #faa;}
155 159 .highlight.token-2 { background-color: #afa;}
156 160 .highlight.token-3 { background-color: #aaf;}
157 161
158 162 .box{
159 163 padding:6px;
160 164 margin-bottom: 10px;
161 165 background-color:#f6f6f6;
162 166 color:#505050;
163 167 line-height:1.5em;
164 168 border: 1px solid #e4e4e4;
165 169 }
166 170
167 171 div.square {
168 172 border: 1px solid #999;
169 173 float: left;
170 174 margin: .3em .4em 0 .4em;
171 175 overflow: hidden;
172 176 width: .6em; height: .6em;
173 177 }
174 178 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
175 179 .contextual input {font-size:0.9em;}
176 180 .message .contextual { margin-top: 0; }
177 181
178 182 .splitcontentleft{float:left; width:49%;}
179 183 .splitcontentright{float:right; width:49%;}
180 184 form {display: inline;}
181 185 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
182 186 fieldset {border: 1px solid #e4e4e4; margin:0;}
183 187 legend {color: #484848;}
184 188 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
185 189 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
186 190 blockquote blockquote { margin-left: 0;}
187 191 textarea.wiki-edit { width: 99%; }
188 192 li p {margin-top: 0;}
189 193 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
190 194 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
191 195 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
192 196 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
193 197
194 198 #query_form_content { font-size: 0.9em; padding: 4px; background: #f6f6f6; border: 1px solid #e4e4e4; }
195 199 #query_form_content fieldset#filters { border-left: 0; border-right: 0; }
196 200 #query_form_content p { margin-top: 0.5em; margin-bottom: 0.5em; }
197 201
198 202 fieldset#filters, fieldset#date-range { padding: 0.7em; margin-bottom: 8px; }
199 203 fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
200 204 fieldset#filters table { border-collapse: collapse; }
201 205 fieldset#filters table td { padding: 0; vertical-align: middle; }
202 206 fieldset#filters tr.filter { height: 2em; }
203 207 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
204 208 .buttons { font-size: 0.9em; margin-bottom: 1.4em; }
205 209
206 210 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
207 211 div#issue-changesets .changeset { padding: 4px;}
208 212 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
209 213 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
210 214
211 215 div#activity dl, #search-results { margin-left: 2em; }
212 216 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
213 217 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
214 218 div#activity dt.me .time { border-bottom: 1px solid #999; }
215 219 div#activity dt .time { color: #777; font-size: 80%; }
216 220 div#activity dd .description, #search-results dd .description { font-style: italic; }
217 221 div#activity span.project:after, #search-results span.project:after { content: " -"; }
218 222 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
219 223
220 224 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
221 225
222 226 div#search-results-counts {float:right;}
223 227 div#search-results-counts ul { margin-top: 0.5em; }
224 228 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
225 229
226 230 dt.issue { background-image: url(../images/ticket.png); }
227 231 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
228 232 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
229 233 dt.issue-note { background-image: url(../images/ticket_note.png); }
230 234 dt.changeset { background-image: url(../images/changeset.png); }
231 235 dt.news { background-image: url(../images/news.png); }
232 236 dt.message { background-image: url(../images/message.png); }
233 237 dt.reply { background-image: url(../images/comments.png); }
234 238 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
235 239 dt.attachment { background-image: url(../images/attachment.png); }
236 240 dt.document { background-image: url(../images/document.png); }
237 241 dt.project { background-image: url(../images/projects.png); }
238 242
239 243 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
240 244
241 245 div#roadmap fieldset.related-issues { margin-bottom: 1em; }
242 246 div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
243 247 div#roadmap .wiki h1:first-child { display: none; }
244 248 div#roadmap .wiki h1 { font-size: 120%; }
245 249 div#roadmap .wiki h2 { font-size: 110%; }
246 250
247 251 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
248 252 div#version-summary fieldset { margin-bottom: 1em; }
249 253 div#version-summary .total-hours { text-align: right; }
250 254
251 255 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
252 256 table#time-report tbody tr { font-style: italic; color: #777; }
253 257 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
254 258 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
255 259 table#time-report .hours-dec { font-size: 0.9em; }
256 260
257 261 form#issue-form .attributes { margin-bottom: 8px; }
258 262 form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
259 263 form#issue-form .attributes select { min-width: 30%; }
260 264
261 265 ul.projects { margin: 0; padding-left: 1em; }
262 266 ul.projects.root { margin: 0; padding: 0; }
263 267 ul.projects ul { border-left: 3px solid #e0e0e0; }
264 268 ul.projects li { list-style-type:none; }
265 269 ul.projects li.root { margin-bottom: 1em; }
266 270 ul.projects li.child { margin-top: 1em;}
267 271 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
268 272 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
269 273
270 274 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
271 275 #tracker_project_ids li { list-style-type:none; }
272 276
273 277 ul.properties {padding:0; font-size: 0.9em; color: #777;}
274 278 ul.properties li {list-style-type:none;}
275 279 ul.properties li span {font-style:italic;}
276 280
277 281 .total-hours { font-size: 110%; font-weight: bold; }
278 282 .total-hours span.hours-int { font-size: 120%; }
279 283
280 284 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
281 285 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
282 286
283 287 .pagination {font-size: 90%}
284 288 p.pagination {margin-top:8px;}
285 289
286 290 /***** Tabular forms ******/
287 291 .tabular p{
288 292 margin: 0;
289 293 padding: 5px 0 8px 0;
290 294 padding-left: 180px; /*width of left column containing the label elements*/
291 295 height: 1%;
292 296 clear:left;
293 297 }
294 298
295 299 html>body .tabular p {overflow:hidden;}
296 300
297 301 .tabular label{
298 302 font-weight: bold;
299 303 float: left;
300 304 text-align: right;
301 305 margin-left: -180px; /*width of left column*/
302 306 width: 175px; /*width of labels. Should be smaller than left column to create some right
303 307 margin*/
304 308 }
305 309
306 310 .tabular label.floating{
307 311 font-weight: normal;
308 312 margin-left: 0px;
309 313 text-align: left;
310 314 width: 270px;
311 315 }
312 316
313 317 input#time_entry_comments { width: 90%;}
314 318
315 319 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
316 320
317 321 .tabular.settings p{ padding-left: 300px; }
318 322 .tabular.settings label{ margin-left: -300px; width: 295px; }
319 323
320 324 .required {color: #bb0000;}
321 325 .summary {font-style: italic;}
322 326
323 327 #attachments_fields input[type=text] {margin-left: 8px; }
324 328
325 329 div.attachments { margin-top: 12px; }
326 330 div.attachments p { margin:4px 0 2px 0; }
327 331 div.attachments img { vertical-align: middle; }
328 332 div.attachments span.author { font-size: 0.9em; color: #888; }
329 333
330 334 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
331 335 .other-formats span + span:before { content: "| "; }
332 336
333 337 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
334 338
335 339 /* Project members tab */
336 div#tab-content-members .splitcontentleft { width: 64% }
337 div#tab-content-members .splitcontentright { width: 34% }
338 div#tab-content-members fieldset { padding:1em; margin-bottom: 1em; }
339 div#tab-content-members fieldset legend { font-weight: bold; }
340 div#tab-content-members fieldset label { display: block; }
340 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft { width: 64% }
341 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright { width: 34% }
342 div#tab-content-members fieldset, div#tab-content-memberships fieldset { padding:1em; margin-bottom: 1em; }
343 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend { font-weight: bold; }
344 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label { display: block; }
341 345 div#tab-content-members fieldset div { max-height: 400px; overflow:auto; }
342 346
343 347 * html div#tab-content-members fieldset div { height: 450px; }
344 348
345 349 /***** Flash & error messages ****/
346 350 #errorExplanation, div.flash, .nodata, .warning {
347 351 padding: 4px 4px 4px 30px;
348 352 margin-bottom: 12px;
349 353 font-size: 1.1em;
350 354 border: 2px solid;
351 355 }
352 356
353 357 div.flash {margin-top: 8px;}
354 358
355 359 div.flash.error, #errorExplanation {
356 360 background: url(../images/false.png) 8px 5px no-repeat;
357 361 background-color: #ffe3e3;
358 362 border-color: #dd0000;
359 363 color: #550000;
360 364 }
361 365
362 366 div.flash.notice {
363 367 background: url(../images/true.png) 8px 5px no-repeat;
364 368 background-color: #dfffdf;
365 369 border-color: #9fcf9f;
366 370 color: #005f00;
367 371 }
368 372
369 373 div.flash.warning {
370 374 background: url(../images/warning.png) 8px 5px no-repeat;
371 375 background-color: #FFEBC1;
372 376 border-color: #FDBF3B;
373 377 color: #A6750C;
374 378 text-align: left;
375 379 }
376 380
377 381 .nodata, .warning {
378 382 text-align: center;
379 383 background-color: #FFEBC1;
380 384 border-color: #FDBF3B;
381 385 color: #A6750C;
382 386 }
383 387
384 388 #errorExplanation ul { font-size: 0.9em;}
385 389 #errorExplanation h2, #errorExplanation p { display: none; }
386 390
387 391 /***** Ajax indicator ******/
388 392 #ajax-indicator {
389 393 position: absolute; /* fixed not supported by IE */
390 394 background-color:#eee;
391 395 border: 1px solid #bbb;
392 396 top:35%;
393 397 left:40%;
394 398 width:20%;
395 399 font-weight:bold;
396 400 text-align:center;
397 401 padding:0.6em;
398 402 z-index:100;
399 403 filter:alpha(opacity=50);
400 404 opacity: 0.5;
401 405 }
402 406
403 407 html>body #ajax-indicator { position: fixed; }
404 408
405 409 #ajax-indicator span {
406 410 background-position: 0% 40%;
407 411 background-repeat: no-repeat;
408 412 background-image: url(../images/loading.gif);
409 413 padding-left: 26px;
410 414 vertical-align: bottom;
411 415 }
412 416
413 417 /***** Calendar *****/
414 418 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
415 419 table.cal thead th {width: 14%;}
416 420 table.cal tbody tr {height: 100px;}
417 421 table.cal th { background-color:#EEEEEE; padding: 4px; }
418 422 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
419 423 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
420 424 table.cal td.odd p.day-num {color: #bbb;}
421 425 table.cal td.today {background:#ffffdd;}
422 426 table.cal td.today p.day-num {font-weight: bold;}
423 427
424 428 /***** Tooltips ******/
425 429 .tooltip{position:relative;z-index:24;}
426 430 .tooltip:hover{z-index:25;color:#000;}
427 431 .tooltip span.tip{display: none; text-align:left;}
428 432
429 433 div.tooltip:hover span.tip{
430 434 display:block;
431 435 position:absolute;
432 436 top:12px; left:24px; width:270px;
433 437 border:1px solid #555;
434 438 background-color:#fff;
435 439 padding: 4px;
436 440 font-size: 0.8em;
437 441 color:#505050;
438 442 }
439 443
440 444 /***** Progress bar *****/
441 445 table.progress {
442 446 border: 1px solid #D7D7D7;
443 447 border-collapse: collapse;
444 448 border-spacing: 0pt;
445 449 empty-cells: show;
446 450 text-align: center;
447 451 float:left;
448 452 margin: 1px 6px 1px 0px;
449 453 }
450 454
451 455 table.progress td { height: 0.9em; }
452 456 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
453 457 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
454 458 table.progress td.open { background: #FFF none repeat scroll 0%; }
455 459 p.pourcent {font-size: 80%;}
456 460 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
457 461
458 462 /***** Tabs *****/
459 463 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
460 464 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
461 465 #content .tabs>ul { bottom:-1px; } /* others */
462 466 #content .tabs ul li {
463 467 float:left;
464 468 list-style-type:none;
465 469 white-space:nowrap;
466 470 margin-right:8px;
467 471 background:#fff;
468 472 }
469 473 #content .tabs ul li a{
470 474 display:block;
471 475 font-size: 0.9em;
472 476 text-decoration:none;
473 477 line-height:1.3em;
474 478 padding:4px 6px 4px 6px;
475 479 border: 1px solid #ccc;
476 480 border-bottom: 1px solid #bbbbbb;
477 481 background-color: #eeeeee;
478 482 color:#777;
479 483 font-weight:bold;
480 484 }
481 485
482 486 #content .tabs ul li a:hover {
483 487 background-color: #ffffdd;
484 488 text-decoration:none;
485 489 }
486 490
487 491 #content .tabs ul li a.selected {
488 492 background-color: #fff;
489 493 border: 1px solid #bbbbbb;
490 494 border-bottom: 1px solid #fff;
491 495 }
492 496
493 497 #content .tabs ul li a.selected:hover {
494 498 background-color: #fff;
495 499 }
496 500
497 501 /***** Auto-complete *****/
498 502 div.autocomplete {
499 503 position:absolute;
500 504 width:250px;
501 505 background-color:white;
502 506 margin:0;
503 507 padding:0;
504 508 }
505 509 div.autocomplete ul {
506 510 list-style-type:none;
507 511 margin:0;
508 512 padding:0;
509 513 }
510 514 div.autocomplete ul li.selected { background-color: #ffb;}
511 515 div.autocomplete ul li {
512 516 list-style-type:none;
513 517 display:block;
514 518 margin:0;
515 519 padding:2px;
516 520 cursor:pointer;
517 521 font-size: 90%;
518 522 border-bottom: 1px solid #ccc;
519 523 border-left: 1px solid #ccc;
520 524 border-right: 1px solid #ccc;
521 525 }
522 526 div.autocomplete ul li span.informal {
523 527 font-size: 80%;
524 528 color: #aaa;
525 529 }
526 530
527 531 /***** Diff *****/
528 532 .diff_out { background: #fcc; }
529 533 .diff_in { background: #cfc; }
530 534
531 535 /***** Wiki *****/
532 536 div.wiki table {
533 537 border: 1px solid #505050;
534 538 border-collapse: collapse;
535 539 margin-bottom: 1em;
536 540 }
537 541
538 542 div.wiki table, div.wiki td, div.wiki th {
539 543 border: 1px solid #bbb;
540 544 padding: 4px;
541 545 }
542 546
543 547 div.wiki .external {
544 548 background-position: 0% 60%;
545 549 background-repeat: no-repeat;
546 550 padding-left: 12px;
547 551 background-image: url(../images/external.png);
548 552 }
549 553
550 554 div.wiki a.new {
551 555 color: #b73535;
552 556 }
553 557
554 558 div.wiki pre {
555 559 margin: 1em 1em 1em 1.6em;
556 560 padding: 2px;
557 561 background-color: #fafafa;
558 562 border: 1px solid #dadada;
559 563 width:95%;
560 564 overflow-x: auto;
561 565 }
562 566
563 567 div.wiki ul.toc {
564 568 background-color: #ffffdd;
565 569 border: 1px solid #e4e4e4;
566 570 padding: 4px;
567 571 line-height: 1.2em;
568 572 margin-bottom: 12px;
569 573 margin-right: 12px;
570 574 margin-left: 0;
571 575 display: table
572 576 }
573 577 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
574 578
575 579 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
576 580 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
577 581 div.wiki ul.toc li { list-style-type:none;}
578 582 div.wiki ul.toc li.heading2 { margin-left: 6px; }
579 583 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
580 584
581 585 div.wiki ul.toc a {
582 586 font-size: 0.9em;
583 587 font-weight: normal;
584 588 text-decoration: none;
585 589 color: #606060;
586 590 }
587 591 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
588 592
589 593 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
590 594 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
591 595 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
592 596
593 597 /***** My page layout *****/
594 598 .block-receiver {
595 599 border:1px dashed #c0c0c0;
596 600 margin-bottom: 20px;
597 601 padding: 15px 0 15px 0;
598 602 }
599 603
600 604 .mypage-box {
601 605 margin:0 0 20px 0;
602 606 color:#505050;
603 607 line-height:1.5em;
604 608 }
605 609
606 610 .handle {
607 611 cursor: move;
608 612 }
609 613
610 614 a.close-icon {
611 615 display:block;
612 616 margin-top:3px;
613 617 overflow:hidden;
614 618 width:12px;
615 619 height:12px;
616 620 background-repeat: no-repeat;
617 621 cursor:pointer;
618 622 background-image:url('../images/close.png');
619 623 }
620 624
621 625 a.close-icon:hover {
622 626 background-image:url('../images/close_hl.png');
623 627 }
624 628
625 629 /***** Gantt chart *****/
626 630 .gantt_hdr {
627 631 position:absolute;
628 632 top:0;
629 633 height:16px;
630 634 border-top: 1px solid #c0c0c0;
631 635 border-bottom: 1px solid #c0c0c0;
632 636 border-right: 1px solid #c0c0c0;
633 637 text-align: center;
634 638 overflow: hidden;
635 639 }
636 640
637 641 .task {
638 642 position: absolute;
639 643 height:8px;
640 644 font-size:0.8em;
641 645 color:#888;
642 646 padding:0;
643 647 margin:0;
644 648 line-height:0.8em;
645 649 }
646 650
647 651 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
648 652 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
649 653 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
650 654 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
651 655
652 656 /***** Icons *****/
653 657 .icon {
654 658 background-position: 0% 40%;
655 659 background-repeat: no-repeat;
656 660 padding-left: 20px;
657 661 padding-top: 2px;
658 662 padding-bottom: 3px;
659 663 }
660 664
661 665 .icon22 {
662 666 background-position: 0% 40%;
663 667 background-repeat: no-repeat;
664 668 padding-left: 26px;
665 669 line-height: 22px;
666 670 vertical-align: middle;
667 671 }
668 672
669 673 .icon-add { background-image: url(../images/add.png); }
670 674 .icon-edit { background-image: url(../images/edit.png); }
671 675 .icon-copy { background-image: url(../images/copy.png); }
672 676 .icon-del { background-image: url(../images/delete.png); }
673 677 .icon-move { background-image: url(../images/move.png); }
674 678 .icon-save { background-image: url(../images/save.png); }
675 679 .icon-cancel { background-image: url(../images/cancel.png); }
676 680 .icon-folder { background-image: url(../images/folder.png); }
677 681 .open .icon-folder { background-image: url(../images/folder_open.png); }
678 682 .icon-package { background-image: url(../images/package.png); }
679 683 .icon-home { background-image: url(../images/home.png); }
680 684 .icon-user { background-image: url(../images/user.png); }
681 685 .icon-mypage { background-image: url(../images/user_page.png); }
682 686 .icon-admin { background-image: url(../images/admin.png); }
683 687 .icon-projects { background-image: url(../images/projects.png); }
684 688 .icon-help { background-image: url(../images/help.png); }
685 689 .icon-attachment { background-image: url(../images/attachment.png); }
686 690 .icon-index { background-image: url(../images/index.png); }
687 691 .icon-history { background-image: url(../images/history.png); }
688 692 .icon-time { background-image: url(../images/time.png); }
689 693 .icon-time-add { background-image: url(../images/time_add.png); }
690 694 .icon-stats { background-image: url(../images/stats.png); }
691 695 .icon-warning { background-image: url(../images/warning.png); }
692 696 .icon-fav { background-image: url(../images/fav.png); }
693 697 .icon-fav-off { background-image: url(../images/fav_off.png); }
694 698 .icon-reload { background-image: url(../images/reload.png); }
695 699 .icon-lock { background-image: url(../images/locked.png); }
696 700 .icon-unlock { background-image: url(../images/unlock.png); }
697 701 .icon-checked { background-image: url(../images/true.png); }
698 702 .icon-details { background-image: url(../images/zoom_in.png); }
699 703 .icon-report { background-image: url(../images/report.png); }
700 704 .icon-comment { background-image: url(../images/comment.png); }
701 705
702 706 .icon-file { background-image: url(../images/files/default.png); }
703 707 .icon-file.text-plain { background-image: url(../images/files/text.png); }
704 708 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
705 709 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
706 710 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
707 711 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
708 712 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
709 713 .icon-file.image-gif { background-image: url(../images/files/image.png); }
710 714 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
711 715 .icon-file.image-png { background-image: url(../images/files/image.png); }
712 716 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
713 717 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
714 718 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
715 719 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
716 720
717 721 .icon22-projects { background-image: url(../images/22x22/projects.png); }
718 722 .icon22-users { background-image: url(../images/22x22/users.png); }
719 723 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
720 724 .icon22-role { background-image: url(../images/22x22/role.png); }
721 725 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
722 726 .icon22-options { background-image: url(../images/22x22/options.png); }
723 727 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
724 728 .icon22-authent { background-image: url(../images/22x22/authent.png); }
725 729 .icon22-info { background-image: url(../images/22x22/info.png); }
726 730 .icon22-comment { background-image: url(../images/22x22/comment.png); }
727 731 .icon22-package { background-image: url(../images/22x22/package.png); }
728 732 .icon22-settings { background-image: url(../images/22x22/settings.png); }
729 733 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
730 734
731 735 img.gravatar {
732 736 padding: 2px;
733 737 border: solid 1px #d5d5d5;
734 738 background: #fff;
735 739 }
736 740
737 741 div.issue img.gravatar {
738 742 float: right;
739 743 margin: 0 0 0 1em;
740 744 padding: 5px;
741 745 }
742 746
743 747 div.issue table img.gravatar {
744 748 height: 14px;
745 749 width: 14px;
746 750 padding: 2px;
747 751 float: left;
748 752 margin: 0 0.5em 0 0;
749 753 }
750 754
751 755 #history img.gravatar {
752 756 padding: 3px;
753 757 margin: 0 1.5em 1em 0;
754 758 float: left;
755 759 }
756 760
757 761 td.username img.gravatar {
758 762 float: left;
759 763 margin: 0 1em 0 0;
760 764 }
761 765
762 766 #activity dt img.gravatar {
763 767 float: left;
764 768 margin: 0 1em 1em 0;
765 769 }
766 770
767 771 #activity dt,
768 772 .journal {
769 773 clear: left;
770 774 }
771 775
772 776 h2 img { vertical-align:middle; }
773 777
774 778
775 779 /***** Media print specific styles *****/
776 780 @media print {
777 781 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
778 782 #main { background: #fff; }
779 783 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
780 784 #wiki_add_attachment { display:none; }
781 785 }
@@ -1,38 +1,33
1 1 ---
2 2 members_001:
3 3 created_on: 2006-07-19 19:35:33 +02:00
4 4 project_id: 1
5 role_id: 1
6 5 id: 1
7 6 user_id: 2
8 7 mail_notification: true
9 8 members_002:
10 9 created_on: 2006-07-19 19:35:36 +02:00
11 10 project_id: 1
12 role_id: 2
13 11 id: 2
14 12 user_id: 3
15 13 mail_notification: true
16 14 members_003:
17 15 created_on: 2006-07-19 19:35:36 +02:00
18 16 project_id: 2
19 role_id: 2
20 17 id: 3
21 18 user_id: 2
22 19 mail_notification: true
23 20 members_004:
24 21 id: 4
25 22 created_on: 2006-07-19 19:35:36 +02:00
26 23 project_id: 1
27 role_id: 2
28 24 # Locked user
29 25 user_id: 5
30 26 mail_notification: true
31 27 members_005:
32 28 id: 5
33 29 created_on: 2006-07-19 19:35:33 +02:00
34 30 project_id: 5
35 role_id: 1
36 31 user_id: 2
37 32 mail_notification: true
38 33 No newline at end of file
@@ -1,142 +1,142
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'attachments_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class AttachmentsController; def rescue_action(e) raise e end; end
23 23
24 24
25 25 class AttachmentsControllerTest < Test::Unit::TestCase
26 fixtures :users, :projects, :roles, :members, :enabled_modules, :issues, :trackers, :attachments,
26 fixtures :users, :projects, :roles, :members, :member_roles, :enabled_modules, :issues, :trackers, :attachments,
27 27 :versions, :wiki_pages, :wikis, :documents
28 28
29 29 def setup
30 30 @controller = AttachmentsController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
34 34 User.current = nil
35 35 end
36 36
37 37 def test_routing
38 38 assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
39 39 assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
40 40 assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
41 41 assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
42 42 end
43 43
44 44 def test_recognizes
45 45 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
46 46 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
47 47 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
48 48 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
49 49 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
50 50 end
51 51
52 52 def test_show_diff
53 53 get :show, :id => 5
54 54 assert_response :success
55 55 assert_template 'diff'
56 56 assert_equal 'text/html', @response.content_type
57 57 end
58 58
59 59 def test_show_text_file
60 60 get :show, :id => 4
61 61 assert_response :success
62 62 assert_template 'file'
63 63 assert_equal 'text/html', @response.content_type
64 64 end
65 65
66 66 def test_show_text_file_should_send_if_too_big
67 67 Setting.file_max_size_displayed = 512
68 68 Attachment.find(4).update_attribute :filesize, 754.kilobyte
69 69
70 70 get :show, :id => 4
71 71 assert_response :success
72 72 assert_equal 'application/x-ruby', @response.content_type
73 73 end
74 74
75 75 def test_show_other
76 76 get :show, :id => 6
77 77 assert_response :success
78 78 assert_equal 'application/octet-stream', @response.content_type
79 79 end
80 80
81 81 def test_download_text_file
82 82 get :download, :id => 4
83 83 assert_response :success
84 84 assert_equal 'application/x-ruby', @response.content_type
85 85 end
86 86
87 87 def test_download_missing_file
88 88 get :download, :id => 2
89 89 assert_response 404
90 90 end
91 91
92 92 def test_anonymous_on_private_private
93 93 get :download, :id => 7
94 94 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
95 95 end
96 96
97 97 def test_destroy_issue_attachment
98 98 issue = Issue.find(3)
99 99 @request.session[:user_id] = 2
100 100
101 101 assert_difference 'issue.attachments.count', -1 do
102 102 post :destroy, :id => 1
103 103 end
104 104 # no referrer
105 105 assert_redirected_to 'projects/ecookbook'
106 106 assert_nil Attachment.find_by_id(1)
107 107 j = issue.journals.find(:first, :order => 'created_on DESC')
108 108 assert_equal 'attachment', j.details.first.property
109 109 assert_equal '1', j.details.first.prop_key
110 110 assert_equal 'error281.txt', j.details.first.old_value
111 111 end
112 112
113 113 def test_destroy_wiki_page_attachment
114 114 @request.session[:user_id] = 2
115 115 assert_difference 'Attachment.count', -1 do
116 116 post :destroy, :id => 3
117 117 assert_response 302
118 118 end
119 119 end
120 120
121 121 def test_destroy_project_attachment
122 122 @request.session[:user_id] = 2
123 123 assert_difference 'Attachment.count', -1 do
124 124 post :destroy, :id => 8
125 125 assert_response 302
126 126 end
127 127 end
128 128
129 129 def test_destroy_version_attachment
130 130 @request.session[:user_id] = 2
131 131 assert_difference 'Attachment.count', -1 do
132 132 post :destroy, :id => 9
133 133 assert_response 302
134 134 end
135 135 end
136 136
137 137 def test_destroy_without_permission
138 138 post :destroy, :id => 3
139 139 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdestroy%2F3'
140 140 assert Attachment.find_by_id(3)
141 141 end
142 142 end
@@ -1,146 +1,146
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'boards_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class BoardsController; def rescue_action(e) raise e end; end
23 23
24 24 class BoardsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :boards, :messages, :enabled_modules
25 fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
26 26
27 27 def setup
28 28 @controller = BoardsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_index_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/projects/world_domination/boards'},
37 37 :controller => 'boards', :action => 'index', :project_id => 'world_domination'
38 38 )
39 39 end
40 40
41 41 def test_index
42 42 get :index, :project_id => 1
43 43 assert_response :success
44 44 assert_template 'index'
45 45 assert_not_nil assigns(:boards)
46 46 assert_not_nil assigns(:project)
47 47 end
48 48
49 49 def test_index_not_found
50 50 get :index, :project_id => 97
51 51 assert_response 404
52 52 end
53 53
54 54 def test_index_should_show_messages_if_only_one_board
55 55 Project.find(1).boards.slice(1..-1).each(&:destroy)
56 56
57 57 get :index, :project_id => 1
58 58 assert_response :success
59 59 assert_template 'show'
60 60 assert_not_nil assigns(:topics)
61 61 end
62 62
63 63 def test_new_routing
64 64 assert_routing(
65 65 {:method => :get, :path => '/projects/world_domination/boards/new'},
66 66 :controller => 'boards', :action => 'new', :project_id => 'world_domination'
67 67 )
68 68 assert_recognizes(
69 69 {:controller => 'boards', :action => 'new', :project_id => 'world_domination'},
70 70 {:method => :post, :path => '/projects/world_domination/boards'}
71 71 )
72 72 end
73 73
74 74 def test_post_new
75 75 @request.session[:user_id] = 2
76 76 assert_difference 'Board.count' do
77 77 post :new, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing board creation'}
78 78 end
79 79 assert_redirected_to '/projects/ecookbook/settings/boards'
80 80 end
81 81
82 82 def test_show_routing
83 83 assert_routing(
84 84 {:method => :get, :path => '/projects/world_domination/boards/44'},
85 85 :controller => 'boards', :action => 'show', :id => '44', :project_id => 'world_domination'
86 86 )
87 87 assert_routing(
88 88 {:method => :get, :path => '/projects/world_domination/boards/44.atom'},
89 89 :controller => 'boards', :action => 'show', :id => '44', :project_id => 'world_domination', :format => 'atom'
90 90 )
91 91 end
92 92
93 93 def test_show
94 94 get :show, :project_id => 1, :id => 1
95 95 assert_response :success
96 96 assert_template 'show'
97 97 assert_not_nil assigns(:board)
98 98 assert_not_nil assigns(:project)
99 99 assert_not_nil assigns(:topics)
100 100 end
101 101
102 102 def test_show_atom
103 103 get :show, :project_id => 1, :id => 1, :format => 'atom'
104 104 assert_response :success
105 105 assert_template 'common/feed.atom'
106 106 assert_not_nil assigns(:board)
107 107 assert_not_nil assigns(:project)
108 108 assert_not_nil assigns(:messages)
109 109 end
110 110
111 111 def test_edit_routing
112 112 assert_routing(
113 113 {:method => :get, :path => '/projects/world_domination/boards/44/edit'},
114 114 :controller => 'boards', :action => 'edit', :id => '44', :project_id => 'world_domination'
115 115 )
116 116 assert_recognizes(#TODO: use PUT method to board_path, modify form accordingly
117 117 {:controller => 'boards', :action => 'edit', :id => '44', :project_id => 'world_domination'},
118 118 {:method => :post, :path => '/projects/world_domination/boards/44/edit'}
119 119 )
120 120 end
121 121
122 122 def test_post_edit
123 123 @request.session[:user_id] = 2
124 124 assert_no_difference 'Board.count' do
125 125 post :edit, :project_id => 1, :id => 2, :board => { :name => 'Testing', :description => 'Testing board update'}
126 126 end
127 127 assert_redirected_to '/projects/ecookbook/settings/boards'
128 128 assert_equal 'Testing', Board.find(2).name
129 129 end
130 130
131 131 def test_destroy_routing
132 132 assert_routing(#TODO: use DELETE method to board_path, modify form accoringly
133 133 {:method => :post, :path => '/projects/world_domination/boards/44/destroy'},
134 134 :controller => 'boards', :action => 'destroy', :id => '44', :project_id => 'world_domination'
135 135 )
136 136 end
137 137
138 138 def test_post_destroy
139 139 @request.session[:user_id] = 2
140 140 assert_difference 'Board.count', -1 do
141 141 post :destroy, :project_id => 1, :id => 2
142 142 end
143 143 assert_redirected_to '/projects/ecookbook/settings/boards'
144 144 assert_nil Board.find_by_id(2)
145 145 end
146 146 end
@@ -1,121 +1,121
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'documents_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class DocumentsController; def rescue_action(e) raise e end; end
23 23
24 24 class DocumentsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :enabled_modules, :documents, :enumerations
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :documents, :enumerations
26 26
27 27 def setup
28 28 @controller = DocumentsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_index_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/projects/567/documents'},
37 37 :controller => 'documents', :action => 'index', :project_id => '567'
38 38 )
39 39 end
40 40
41 41 def test_index
42 42 # Sets a default category
43 43 e = Enumeration.find_by_name('Technical documentation')
44 44 e.update_attributes(:is_default => true)
45 45
46 46 get :index, :project_id => 'ecookbook'
47 47 assert_response :success
48 48 assert_template 'index'
49 49 assert_not_nil assigns(:grouped)
50 50
51 51 # Default category selected in the new document form
52 52 assert_tag :select, :attributes => {:name => 'document[category_id]'},
53 53 :child => {:tag => 'option', :attributes => {:selected => 'selected'},
54 54 :content => 'Technical documentation'}
55 55 end
56 56
57 57 def test_new_routing
58 58 assert_routing(
59 59 {:method => :get, :path => '/projects/567/documents/new'},
60 60 :controller => 'documents', :action => 'new', :project_id => '567'
61 61 )
62 62 assert_recognizes(
63 63 {:controller => 'documents', :action => 'new', :project_id => '567'},
64 64 {:method => :post, :path => '/projects/567/documents'}
65 65 )
66 66 end
67 67
68 68 def test_new_with_one_attachment
69 69 ActionMailer::Base.deliveries.clear
70 70 Setting.notified_events << 'document_added'
71 71 @request.session[:user_id] = 2
72 72 set_tmp_attachments_directory
73 73
74 74 post :new, :project_id => 'ecookbook',
75 75 :document => { :title => 'DocumentsControllerTest#test_post_new',
76 76 :description => 'This is a new document',
77 77 :category_id => 2},
78 78 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
79 79
80 80 assert_redirected_to 'projects/ecookbook/documents'
81 81
82 82 document = Document.find_by_title('DocumentsControllerTest#test_post_new')
83 83 assert_not_nil document
84 84 assert_equal Enumeration.find(2), document.category
85 85 assert_equal 1, document.attachments.size
86 86 assert_equal 'testfile.txt', document.attachments.first.filename
87 87 assert_equal 1, ActionMailer::Base.deliveries.size
88 88 end
89 89
90 90 def test_edit_routing
91 91 assert_routing(
92 92 {:method => :get, :path => '/documents/22/edit'},
93 93 :controller => 'documents', :action => 'edit', :id => '22'
94 94 )
95 95 assert_recognizes(#TODO: should be using PUT on document URI
96 96 {:controller => 'documents', :action => 'edit', :id => '567'},
97 97 {:method => :post, :path => '/documents/567/edit'}
98 98 )
99 99 end
100 100
101 101 def test_show_routing
102 102 assert_routing(
103 103 {:method => :get, :path => '/documents/22'},
104 104 :controller => 'documents', :action => 'show', :id => '22'
105 105 )
106 106 end
107 107
108 108 def test_destroy_routing
109 109 assert_recognizes(#TODO: should be using DELETE on document URI
110 110 {:controller => 'documents', :action => 'destroy', :id => '567'},
111 111 {:method => :post, :path => '/documents/567/destroy'}
112 112 )
113 113 end
114 114
115 115 def test_destroy
116 116 @request.session[:user_id] = 2
117 117 post :destroy, :id => 1
118 118 assert_redirected_to 'projects/ecookbook/documents'
119 119 assert_nil Document.find_by_id(1)
120 120 end
121 121 end
@@ -1,78 +1,78
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issue_categories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssueCategoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssueCategoriesControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :enabled_modules, :issue_categories
25 fixtures :projects, :users, :members, :member_roles, :roles, :enabled_modules, :issue_categories
26 26
27 27 def setup
28 28 @controller = IssueCategoriesController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 @request.session[:user_id] = 2
33 33 end
34 34
35 35 def test_post_edit
36 36 assert_no_difference 'IssueCategory.count' do
37 37 post :edit, :id => 2, :category => { :name => 'Testing' }
38 38 end
39 39 assert_redirected_to '/projects/ecookbook/settings/categories'
40 40 assert_equal 'Testing', IssueCategory.find(2).name
41 41 end
42 42
43 43 def test_edit_not_found
44 44 post :edit, :id => 97, :category => { :name => 'Testing' }
45 45 assert_response 404
46 46 end
47 47
48 48 def test_destroy_category_not_in_use
49 49 post :destroy, :id => 2
50 50 assert_redirected_to '/projects/ecookbook/settings/categories'
51 51 assert_nil IssueCategory.find_by_id(2)
52 52 end
53 53
54 54 def test_destroy_category_in_use
55 55 post :destroy, :id => 1
56 56 assert_response :success
57 57 assert_template 'destroy'
58 58 assert_not_nil IssueCategory.find_by_id(1)
59 59 end
60 60
61 61 def test_destroy_category_in_use_with_reassignment
62 62 issue = Issue.find(:first, :conditions => {:category_id => 1})
63 63 post :destroy, :id => 1, :todo => 'reassign', :reassign_to_id => 2
64 64 assert_redirected_to '/projects/ecookbook/settings/categories'
65 65 assert_nil IssueCategory.find_by_id(1)
66 66 # check that the issue was reassign
67 67 assert_equal 2, issue.reload.category_id
68 68 end
69 69
70 70 def test_destroy_category_in_use_without_reassignment
71 71 issue = Issue.find(:first, :conditions => {:category_id => 1})
72 72 post :destroy, :id => 1, :todo => 'nullify'
73 73 assert_redirected_to '/projects/ecookbook/settings/categories'
74 74 assert_nil IssueCategory.find_by_id(1)
75 75 # check that the issue category was nullified
76 76 assert_nil issue.reload.category_id
77 77 end
78 78 end
@@ -1,58 +1,59
1 1 require File.dirname(__FILE__) + '/../test_helper'
2 2 require 'issue_relations_controller'
3 3
4 4 # Re-raise errors caught by the controller.
5 5 class IssueRelationsController; def rescue_action(e) raise e end; end
6 6
7 7
8 8 class IssueRelationsControllerTest < Test::Unit::TestCase
9 9 fixtures :projects,
10 10 :users,
11 11 :roles,
12 12 :members,
13 :member_roles,
13 14 :issues,
14 15 :issue_statuses,
15 16 :enabled_modules,
16 17 :enumerations,
17 18 :trackers
18 19
19 20 def setup
20 21 @controller = IssueRelationsController.new
21 22 @request = ActionController::TestRequest.new
22 23 @response = ActionController::TestResponse.new
23 24 User.current = nil
24 25 end
25 26
26 27 def test_new_routing
27 28 assert_routing(
28 29 {:method => :post, :path => '/issues/1/relations'},
29 30 {:controller => 'issue_relations', :action => 'new', :issue_id => '1'}
30 31 )
31 32 end
32 33
33 34 def test_destroy_routing
34 35 assert_recognizes( #TODO: use DELETE on issue URI
35 36 {:controller => 'issue_relations', :action => 'destroy', :issue_id => '1', :id => '23'},
36 37 {:method => :post, :path => '/issues/1/relations/23/destroy'}
37 38 )
38 39 end
39 40
40 41 def test_new
41 42 assert_difference 'IssueRelation.count' do
42 43 @request.session[:user_id] = 3
43 44 post :new, :issue_id => 1,
44 45 :relation => {:issue_to_id => '2', :relation_type => 'relates', :delay => ''}
45 46 end
46 47 end
47 48
48 49 def test_should_create_relations_with_visible_issues_only
49 50 Setting.cross_project_issue_relations = '1'
50 51 assert_nil Issue.visible(User.find(3)).find_by_id(4)
51 52
52 53 assert_no_difference 'IssueRelation.count' do
53 54 @request.session[:user_id] = 3
54 55 post :new, :issue_id => 1,
55 56 :relation => {:issue_to_id => '4', :relation_type => 'relates', :delay => ''}
56 57 end
57 58 end
58 59 end
@@ -1,1061 +1,1062
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 :member_roles,
29 30 :issues,
30 31 :issue_statuses,
31 32 :versions,
32 33 :trackers,
33 34 :projects_trackers,
34 35 :issue_categories,
35 36 :enabled_modules,
36 37 :enumerations,
37 38 :attachments,
38 39 :workflows,
39 40 :custom_fields,
40 41 :custom_values,
41 42 :custom_fields_trackers,
42 43 :time_entries,
43 44 :journals,
44 45 :journal_details
45 46
46 47 def setup
47 48 @controller = IssuesController.new
48 49 @request = ActionController::TestRequest.new
49 50 @response = ActionController::TestResponse.new
50 51 User.current = nil
51 52 end
52 53
53 54 def test_index_routing
54 55 assert_routing(
55 56 {:method => :get, :path => '/issues'},
56 57 :controller => 'issues', :action => 'index'
57 58 )
58 59 end
59 60
60 61 def test_index
61 62 Setting.default_language = 'en'
62 63
63 64 get :index
64 65 assert_response :success
65 66 assert_template 'index.rhtml'
66 67 assert_not_nil assigns(:issues)
67 68 assert_nil assigns(:project)
68 69 assert_tag :tag => 'a', :content => /Can't print recipes/
69 70 assert_tag :tag => 'a', :content => /Subproject issue/
70 71 # private projects hidden
71 72 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
72 73 assert_no_tag :tag => 'a', :content => /Issue on project 2/
73 74 # project column
74 75 assert_tag :tag => 'th', :content => /Project/
75 76 end
76 77
77 78 def test_index_should_not_list_issues_when_module_disabled
78 79 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
79 80 get :index
80 81 assert_response :success
81 82 assert_template 'index.rhtml'
82 83 assert_not_nil assigns(:issues)
83 84 assert_nil assigns(:project)
84 85 assert_no_tag :tag => 'a', :content => /Can't print recipes/
85 86 assert_tag :tag => 'a', :content => /Subproject issue/
86 87 end
87 88
88 89 def test_index_with_project_routing
89 90 assert_routing(
90 91 {:method => :get, :path => '/projects/23/issues'},
91 92 :controller => 'issues', :action => 'index', :project_id => '23'
92 93 )
93 94 end
94 95
95 96 def test_index_should_not_list_issues_when_module_disabled
96 97 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
97 98 get :index
98 99 assert_response :success
99 100 assert_template 'index.rhtml'
100 101 assert_not_nil assigns(:issues)
101 102 assert_nil assigns(:project)
102 103 assert_no_tag :tag => 'a', :content => /Can't print recipes/
103 104 assert_tag :tag => 'a', :content => /Subproject issue/
104 105 end
105 106
106 107 def test_index_with_project_routing
107 108 assert_routing(
108 109 {:method => :get, :path => 'projects/23/issues'},
109 110 :controller => 'issues', :action => 'index', :project_id => '23'
110 111 )
111 112 end
112 113
113 114 def test_index_with_project
114 115 Setting.display_subprojects_issues = 0
115 116 get :index, :project_id => 1
116 117 assert_response :success
117 118 assert_template 'index.rhtml'
118 119 assert_not_nil assigns(:issues)
119 120 assert_tag :tag => 'a', :content => /Can't print recipes/
120 121 assert_no_tag :tag => 'a', :content => /Subproject issue/
121 122 end
122 123
123 124 def test_index_with_project_and_subprojects
124 125 Setting.display_subprojects_issues = 1
125 126 get :index, :project_id => 1
126 127 assert_response :success
127 128 assert_template 'index.rhtml'
128 129 assert_not_nil assigns(:issues)
129 130 assert_tag :tag => 'a', :content => /Can't print recipes/
130 131 assert_tag :tag => 'a', :content => /Subproject issue/
131 132 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
132 133 end
133 134
134 135 def test_index_with_project_and_subprojects_should_show_private_subprojects
135 136 @request.session[:user_id] = 2
136 137 Setting.display_subprojects_issues = 1
137 138 get :index, :project_id => 1
138 139 assert_response :success
139 140 assert_template 'index.rhtml'
140 141 assert_not_nil assigns(:issues)
141 142 assert_tag :tag => 'a', :content => /Can't print recipes/
142 143 assert_tag :tag => 'a', :content => /Subproject issue/
143 144 assert_tag :tag => 'a', :content => /Issue of a private subproject/
144 145 end
145 146
146 147 def test_index_with_project_routing_formatted
147 148 assert_routing(
148 149 {:method => :get, :path => 'projects/23/issues.pdf'},
149 150 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
150 151 )
151 152 assert_routing(
152 153 {:method => :get, :path => 'projects/23/issues.atom'},
153 154 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
154 155 )
155 156 end
156 157
157 158 def test_index_with_project_and_filter
158 159 get :index, :project_id => 1, :set_filter => 1
159 160 assert_response :success
160 161 assert_template 'index.rhtml'
161 162 assert_not_nil assigns(:issues)
162 163 end
163 164
164 165 def test_index_with_query
165 166 get :index, :project_id => 1, :query_id => 5
166 167 assert_response :success
167 168 assert_template 'index.rhtml'
168 169 assert_not_nil assigns(:issues)
169 170 assert_nil assigns(:issue_count_by_group)
170 171 end
171 172
172 173 def test_index_with_grouped_query
173 174 get :index, :project_id => 1, :query_id => 6
174 175 assert_response :success
175 176 assert_template 'index.rhtml'
176 177 assert_not_nil assigns(:issues)
177 178 assert_not_nil assigns(:issue_count_by_group)
178 179 end
179 180
180 181 def test_index_csv_with_project
181 182 get :index, :format => 'csv'
182 183 assert_response :success
183 184 assert_not_nil assigns(:issues)
184 185 assert_equal 'text/csv', @response.content_type
185 186
186 187 get :index, :project_id => 1, :format => 'csv'
187 188 assert_response :success
188 189 assert_not_nil assigns(:issues)
189 190 assert_equal 'text/csv', @response.content_type
190 191 end
191 192
192 193 def test_index_formatted
193 194 assert_routing(
194 195 {:method => :get, :path => 'issues.pdf'},
195 196 :controller => 'issues', :action => 'index', :format => 'pdf'
196 197 )
197 198 assert_routing(
198 199 {:method => :get, :path => 'issues.atom'},
199 200 :controller => 'issues', :action => 'index', :format => 'atom'
200 201 )
201 202 end
202 203
203 204 def test_index_pdf
204 205 get :index, :format => 'pdf'
205 206 assert_response :success
206 207 assert_not_nil assigns(:issues)
207 208 assert_equal 'application/pdf', @response.content_type
208 209
209 210 get :index, :project_id => 1, :format => 'pdf'
210 211 assert_response :success
211 212 assert_not_nil assigns(:issues)
212 213 assert_equal 'application/pdf', @response.content_type
213 214
214 215 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
215 216 assert_response :success
216 217 assert_not_nil assigns(:issues)
217 218 assert_equal 'application/pdf', @response.content_type
218 219 end
219 220
220 221 def test_index_sort
221 222 get :index, :sort => 'tracker,id:desc'
222 223 assert_response :success
223 224
224 225 sort_params = @request.session['issues_index_sort']
225 226 assert sort_params.is_a?(String)
226 227 assert_equal 'tracker,id:desc', sort_params
227 228
228 229 issues = assigns(:issues)
229 230 assert_not_nil issues
230 231 assert !issues.empty?
231 232 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
232 233 end
233 234
234 235 def test_gantt
235 236 get :gantt, :project_id => 1
236 237 assert_response :success
237 238 assert_template 'gantt.rhtml'
238 239 assert_not_nil assigns(:gantt)
239 240 events = assigns(:gantt).events
240 241 assert_not_nil events
241 242 # Issue with start and due dates
242 243 i = Issue.find(1)
243 244 assert_not_nil i.due_date
244 245 assert events.include?(Issue.find(1))
245 246 # Issue with without due date but targeted to a version with date
246 247 i = Issue.find(2)
247 248 assert_nil i.due_date
248 249 assert events.include?(i)
249 250 end
250 251
251 252 def test_cross_project_gantt
252 253 get :gantt
253 254 assert_response :success
254 255 assert_template 'gantt.rhtml'
255 256 assert_not_nil assigns(:gantt)
256 257 events = assigns(:gantt).events
257 258 assert_not_nil events
258 259 end
259 260
260 261 def test_gantt_export_to_pdf
261 262 get :gantt, :project_id => 1, :format => 'pdf'
262 263 assert_response :success
263 264 assert_equal 'application/pdf', @response.content_type
264 265 assert @response.body.starts_with?('%PDF')
265 266 assert_not_nil assigns(:gantt)
266 267 end
267 268
268 269 def test_cross_project_gantt_export_to_pdf
269 270 get :gantt, :format => 'pdf'
270 271 assert_response :success
271 272 assert_equal 'application/pdf', @response.content_type
272 273 assert @response.body.starts_with?('%PDF')
273 274 assert_not_nil assigns(:gantt)
274 275 end
275 276
276 277 if Object.const_defined?(:Magick)
277 278 def test_gantt_image
278 279 get :gantt, :project_id => 1, :format => 'png'
279 280 assert_response :success
280 281 assert_equal 'image/png', @response.content_type
281 282 end
282 283 else
283 284 puts "RMagick not installed. Skipping tests !!!"
284 285 end
285 286
286 287 def test_calendar
287 288 get :calendar, :project_id => 1
288 289 assert_response :success
289 290 assert_template 'calendar'
290 291 assert_not_nil assigns(:calendar)
291 292 end
292 293
293 294 def test_cross_project_calendar
294 295 get :calendar
295 296 assert_response :success
296 297 assert_template 'calendar'
297 298 assert_not_nil assigns(:calendar)
298 299 end
299 300
300 301 def test_changes
301 302 get :changes, :project_id => 1
302 303 assert_response :success
303 304 assert_not_nil assigns(:journals)
304 305 assert_equal 'application/atom+xml', @response.content_type
305 306 end
306 307
307 308 def test_show_routing
308 309 assert_routing(
309 310 {:method => :get, :path => '/issues/64'},
310 311 :controller => 'issues', :action => 'show', :id => '64'
311 312 )
312 313 end
313 314
314 315 def test_show_routing_formatted
315 316 assert_routing(
316 317 {:method => :get, :path => '/issues/2332.pdf'},
317 318 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
318 319 )
319 320 assert_routing(
320 321 {:method => :get, :path => '/issues/23123.atom'},
321 322 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
322 323 )
323 324 end
324 325
325 326 def test_show_by_anonymous
326 327 get :show, :id => 1
327 328 assert_response :success
328 329 assert_template 'show.rhtml'
329 330 assert_not_nil assigns(:issue)
330 331 assert_equal Issue.find(1), assigns(:issue)
331 332
332 333 # anonymous role is allowed to add a note
333 334 assert_tag :tag => 'form',
334 335 :descendant => { :tag => 'fieldset',
335 336 :child => { :tag => 'legend',
336 337 :content => /Notes/ } }
337 338 end
338 339
339 340 def test_show_by_manager
340 341 @request.session[:user_id] = 2
341 342 get :show, :id => 1
342 343 assert_response :success
343 344
344 345 assert_tag :tag => 'form',
345 346 :descendant => { :tag => 'fieldset',
346 347 :child => { :tag => 'legend',
347 348 :content => /Change properties/ } },
348 349 :descendant => { :tag => 'fieldset',
349 350 :child => { :tag => 'legend',
350 351 :content => /Log time/ } },
351 352 :descendant => { :tag => 'fieldset',
352 353 :child => { :tag => 'legend',
353 354 :content => /Notes/ } }
354 355 end
355 356
356 357 def test_show_should_not_disclose_relations_to_invisible_issues
357 358 Setting.cross_project_issue_relations = '1'
358 359 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
359 360 # Relation to a private project issue
360 361 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
361 362
362 363 get :show, :id => 1
363 364 assert_response :success
364 365
365 366 assert_tag :div, :attributes => { :id => 'relations' },
366 367 :descendant => { :tag => 'a', :content => /#2$/ }
367 368 assert_no_tag :div, :attributes => { :id => 'relations' },
368 369 :descendant => { :tag => 'a', :content => /#4$/ }
369 370 end
370 371
371 372 def test_new_routing
372 373 assert_routing(
373 374 {:method => :get, :path => '/projects/1/issues/new'},
374 375 :controller => 'issues', :action => 'new', :project_id => '1'
375 376 )
376 377 assert_recognizes(
377 378 {:controller => 'issues', :action => 'new', :project_id => '1'},
378 379 {:method => :post, :path => '/projects/1/issues'}
379 380 )
380 381 end
381 382
382 383 def test_show_export_to_pdf
383 384 get :show, :id => 3, :format => 'pdf'
384 385 assert_response :success
385 386 assert_equal 'application/pdf', @response.content_type
386 387 assert @response.body.starts_with?('%PDF')
387 388 assert_not_nil assigns(:issue)
388 389 end
389 390
390 391 def test_get_new
391 392 @request.session[:user_id] = 2
392 393 get :new, :project_id => 1, :tracker_id => 1
393 394 assert_response :success
394 395 assert_template 'new'
395 396
396 397 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
397 398 :value => 'Default string' }
398 399 end
399 400
400 401 def test_get_new_without_tracker_id
401 402 @request.session[:user_id] = 2
402 403 get :new, :project_id => 1
403 404 assert_response :success
404 405 assert_template 'new'
405 406
406 407 issue = assigns(:issue)
407 408 assert_not_nil issue
408 409 assert_equal Project.find(1).trackers.first, issue.tracker
409 410 end
410 411
411 412 def test_get_new_with_no_default_status_should_display_an_error
412 413 @request.session[:user_id] = 2
413 414 IssueStatus.delete_all
414 415
415 416 get :new, :project_id => 1
416 417 assert_response 500
417 418 assert_not_nil flash[:error]
418 419 assert_tag :tag => 'div', :attributes => { :class => /error/ },
419 420 :content => /No default issue/
420 421 end
421 422
422 423 def test_get_new_with_no_tracker_should_display_an_error
423 424 @request.session[:user_id] = 2
424 425 Tracker.delete_all
425 426
426 427 get :new, :project_id => 1
427 428 assert_response 500
428 429 assert_not_nil flash[:error]
429 430 assert_tag :tag => 'div', :attributes => { :class => /error/ },
430 431 :content => /No tracker/
431 432 end
432 433
433 434 def test_update_new_form
434 435 @request.session[:user_id] = 2
435 436 xhr :post, :new, :project_id => 1,
436 437 :issue => {:tracker_id => 2,
437 438 :subject => 'This is the test_new issue',
438 439 :description => 'This is the description',
439 440 :priority_id => 5}
440 441 assert_response :success
441 442 assert_template 'new'
442 443 end
443 444
444 445 def test_post_new
445 446 @request.session[:user_id] = 2
446 447 post :new, :project_id => 1,
447 448 :issue => {:tracker_id => 3,
448 449 :subject => 'This is the test_new issue',
449 450 :description => 'This is the description',
450 451 :priority_id => 5,
451 452 :estimated_hours => '',
452 453 :custom_field_values => {'2' => 'Value for field 2'}}
453 454 assert_redirected_to :action => 'show'
454 455
455 456 issue = Issue.find_by_subject('This is the test_new issue')
456 457 assert_not_nil issue
457 458 assert_equal 2, issue.author_id
458 459 assert_equal 3, issue.tracker_id
459 460 assert_nil issue.estimated_hours
460 461 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
461 462 assert_not_nil v
462 463 assert_equal 'Value for field 2', v.value
463 464 end
464 465
465 466 def test_post_new_and_continue
466 467 @request.session[:user_id] = 2
467 468 post :new, :project_id => 1,
468 469 :issue => {:tracker_id => 3,
469 470 :subject => 'This is first issue',
470 471 :priority_id => 5},
471 472 :continue => ''
472 473 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
473 474 end
474 475
475 476 def test_post_new_without_custom_fields_param
476 477 @request.session[:user_id] = 2
477 478 post :new, :project_id => 1,
478 479 :issue => {:tracker_id => 1,
479 480 :subject => 'This is the test_new issue',
480 481 :description => 'This is the description',
481 482 :priority_id => 5}
482 483 assert_redirected_to :action => 'show'
483 484 end
484 485
485 486 def test_post_new_with_required_custom_field_and_without_custom_fields_param
486 487 field = IssueCustomField.find_by_name('Database')
487 488 field.update_attribute(:is_required, true)
488 489
489 490 @request.session[:user_id] = 2
490 491 post :new, :project_id => 1,
491 492 :issue => {:tracker_id => 1,
492 493 :subject => 'This is the test_new issue',
493 494 :description => 'This is the description',
494 495 :priority_id => 5}
495 496 assert_response :success
496 497 assert_template 'new'
497 498 issue = assigns(:issue)
498 499 assert_not_nil issue
499 500 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
500 501 end
501 502
502 503 def test_post_new_with_watchers
503 504 @request.session[:user_id] = 2
504 505 ActionMailer::Base.deliveries.clear
505 506
506 507 assert_difference 'Watcher.count', 2 do
507 508 post :new, :project_id => 1,
508 509 :issue => {:tracker_id => 1,
509 510 :subject => 'This is a new issue with watchers',
510 511 :description => 'This is the description',
511 512 :priority_id => 5,
512 513 :watcher_user_ids => ['2', '3']}
513 514 end
514 515 issue = Issue.find_by_subject('This is a new issue with watchers')
515 516 assert_not_nil issue
516 517 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
517 518
518 519 # Watchers added
519 520 assert_equal [2, 3], issue.watcher_user_ids.sort
520 521 assert issue.watched_by?(User.find(3))
521 522 # Watchers notified
522 523 mail = ActionMailer::Base.deliveries.last
523 524 assert_kind_of TMail::Mail, mail
524 525 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
525 526 end
526 527
527 528 def test_post_new_should_send_a_notification
528 529 ActionMailer::Base.deliveries.clear
529 530 @request.session[:user_id] = 2
530 531 post :new, :project_id => 1,
531 532 :issue => {:tracker_id => 3,
532 533 :subject => 'This is the test_new issue',
533 534 :description => 'This is the description',
534 535 :priority_id => 5,
535 536 :estimated_hours => '',
536 537 :custom_field_values => {'2' => 'Value for field 2'}}
537 538 assert_redirected_to :action => 'show'
538 539
539 540 assert_equal 1, ActionMailer::Base.deliveries.size
540 541 end
541 542
542 543 def test_post_should_preserve_fields_values_on_validation_failure
543 544 @request.session[:user_id] = 2
544 545 post :new, :project_id => 1,
545 546 :issue => {:tracker_id => 1,
546 547 # empty subject
547 548 :subject => '',
548 549 :description => 'This is a description',
549 550 :priority_id => 6,
550 551 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
551 552 assert_response :success
552 553 assert_template 'new'
553 554
554 555 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
555 556 :content => 'This is a description'
556 557 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
557 558 :child => { :tag => 'option', :attributes => { :selected => 'selected',
558 559 :value => '6' },
559 560 :content => 'High' }
560 561 # Custom fields
561 562 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
562 563 :child => { :tag => 'option', :attributes => { :selected => 'selected',
563 564 :value => 'Oracle' },
564 565 :content => 'Oracle' }
565 566 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
566 567 :value => 'Value for field 2'}
567 568 end
568 569
569 570 def test_copy_routing
570 571 assert_routing(
571 572 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
572 573 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
573 574 )
574 575 end
575 576
576 577 def test_copy_issue
577 578 @request.session[:user_id] = 2
578 579 get :new, :project_id => 1, :copy_from => 1
579 580 assert_template 'new'
580 581 assert_not_nil assigns(:issue)
581 582 orig = Issue.find(1)
582 583 assert_equal orig.subject, assigns(:issue).subject
583 584 end
584 585
585 586 def test_edit_routing
586 587 assert_routing(
587 588 {:method => :get, :path => '/issues/1/edit'},
588 589 :controller => 'issues', :action => 'edit', :id => '1'
589 590 )
590 591 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
591 592 {:controller => 'issues', :action => 'edit', :id => '1'},
592 593 {:method => :post, :path => '/issues/1/edit'}
593 594 )
594 595 end
595 596
596 597 def test_get_edit
597 598 @request.session[:user_id] = 2
598 599 get :edit, :id => 1
599 600 assert_response :success
600 601 assert_template 'edit'
601 602 assert_not_nil assigns(:issue)
602 603 assert_equal Issue.find(1), assigns(:issue)
603 604 end
604 605
605 606 def test_get_edit_with_params
606 607 @request.session[:user_id] = 2
607 608 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
608 609 assert_response :success
609 610 assert_template 'edit'
610 611
611 612 issue = assigns(:issue)
612 613 assert_not_nil issue
613 614
614 615 assert_equal 5, issue.status_id
615 616 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
616 617 :child => { :tag => 'option',
617 618 :content => 'Closed',
618 619 :attributes => { :selected => 'selected' } }
619 620
620 621 assert_equal 7, issue.priority_id
621 622 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
622 623 :child => { :tag => 'option',
623 624 :content => 'Urgent',
624 625 :attributes => { :selected => 'selected' } }
625 626 end
626 627
627 628 def test_reply_routing
628 629 assert_routing(
629 630 {:method => :post, :path => '/issues/1/quoted'},
630 631 :controller => 'issues', :action => 'reply', :id => '1'
631 632 )
632 633 end
633 634
634 635 def test_reply_to_issue
635 636 @request.session[:user_id] = 2
636 637 get :reply, :id => 1
637 638 assert_response :success
638 639 assert_select_rjs :show, "update"
639 640 end
640 641
641 642 def test_reply_to_note
642 643 @request.session[:user_id] = 2
643 644 get :reply, :id => 1, :journal_id => 2
644 645 assert_response :success
645 646 assert_select_rjs :show, "update"
646 647 end
647 648
648 649 def test_post_edit_without_custom_fields_param
649 650 @request.session[:user_id] = 2
650 651 ActionMailer::Base.deliveries.clear
651 652
652 653 issue = Issue.find(1)
653 654 assert_equal '125', issue.custom_value_for(2).value
654 655 old_subject = issue.subject
655 656 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
656 657
657 658 assert_difference('Journal.count') do
658 659 assert_difference('JournalDetail.count', 2) do
659 660 post :edit, :id => 1, :issue => {:subject => new_subject,
660 661 :priority_id => '6',
661 662 :category_id => '1' # no change
662 663 }
663 664 end
664 665 end
665 666 assert_redirected_to :action => 'show', :id => '1'
666 667 issue.reload
667 668 assert_equal new_subject, issue.subject
668 669 # Make sure custom fields were not cleared
669 670 assert_equal '125', issue.custom_value_for(2).value
670 671
671 672 mail = ActionMailer::Base.deliveries.last
672 673 assert_kind_of TMail::Mail, mail
673 674 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
674 675 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
675 676 end
676 677
677 678 def test_post_edit_with_custom_field_change
678 679 @request.session[:user_id] = 2
679 680 issue = Issue.find(1)
680 681 assert_equal '125', issue.custom_value_for(2).value
681 682
682 683 assert_difference('Journal.count') do
683 684 assert_difference('JournalDetail.count', 3) do
684 685 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
685 686 :priority_id => '6',
686 687 :category_id => '1', # no change
687 688 :custom_field_values => { '2' => 'New custom value' }
688 689 }
689 690 end
690 691 end
691 692 assert_redirected_to :action => 'show', :id => '1'
692 693 issue.reload
693 694 assert_equal 'New custom value', issue.custom_value_for(2).value
694 695
695 696 mail = ActionMailer::Base.deliveries.last
696 697 assert_kind_of TMail::Mail, mail
697 698 assert mail.body.include?("Searchable field changed from 125 to New custom value")
698 699 end
699 700
700 701 def test_post_edit_with_status_and_assignee_change
701 702 issue = Issue.find(1)
702 703 assert_equal 1, issue.status_id
703 704 @request.session[:user_id] = 2
704 705 assert_difference('TimeEntry.count', 0) do
705 706 post :edit,
706 707 :id => 1,
707 708 :issue => { :status_id => 2, :assigned_to_id => 3 },
708 709 :notes => 'Assigned to dlopper',
709 710 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.activities.first }
710 711 end
711 712 assert_redirected_to :action => 'show', :id => '1'
712 713 issue.reload
713 714 assert_equal 2, issue.status_id
714 715 j = issue.journals.find(:first, :order => 'id DESC')
715 716 assert_equal 'Assigned to dlopper', j.notes
716 717 assert_equal 2, j.details.size
717 718
718 719 mail = ActionMailer::Base.deliveries.last
719 720 assert mail.body.include?("Status changed from New to Assigned")
720 721 # subject should contain the new status
721 722 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
722 723 end
723 724
724 725 def test_post_edit_with_note_only
725 726 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
726 727 # anonymous user
727 728 post :edit,
728 729 :id => 1,
729 730 :notes => notes
730 731 assert_redirected_to :action => 'show', :id => '1'
731 732 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
732 733 assert_equal notes, j.notes
733 734 assert_equal 0, j.details.size
734 735 assert_equal User.anonymous, j.user
735 736
736 737 mail = ActionMailer::Base.deliveries.last
737 738 assert mail.body.include?(notes)
738 739 end
739 740
740 741 def test_post_edit_with_note_and_spent_time
741 742 @request.session[:user_id] = 2
742 743 spent_hours_before = Issue.find(1).spent_hours
743 744 assert_difference('TimeEntry.count') do
744 745 post :edit,
745 746 :id => 1,
746 747 :notes => '2.5 hours added',
747 748 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.activities.first }
748 749 end
749 750 assert_redirected_to :action => 'show', :id => '1'
750 751
751 752 issue = Issue.find(1)
752 753
753 754 j = issue.journals.find(:first, :order => 'id DESC')
754 755 assert_equal '2.5 hours added', j.notes
755 756 assert_equal 0, j.details.size
756 757
757 758 t = issue.time_entries.find(:first, :order => 'id DESC')
758 759 assert_not_nil t
759 760 assert_equal 2.5, t.hours
760 761 assert_equal spent_hours_before + 2.5, issue.spent_hours
761 762 end
762 763
763 764 def test_post_edit_with_attachment_only
764 765 set_tmp_attachments_directory
765 766
766 767 # Delete all fixtured journals, a race condition can occur causing the wrong
767 768 # journal to get fetched in the next find.
768 769 Journal.delete_all
769 770
770 771 # anonymous user
771 772 post :edit,
772 773 :id => 1,
773 774 :notes => '',
774 775 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
775 776 assert_redirected_to :action => 'show', :id => '1'
776 777 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
777 778 assert j.notes.blank?
778 779 assert_equal 1, j.details.size
779 780 assert_equal 'testfile.txt', j.details.first.value
780 781 assert_equal User.anonymous, j.user
781 782
782 783 mail = ActionMailer::Base.deliveries.last
783 784 assert mail.body.include?('testfile.txt')
784 785 end
785 786
786 787 def test_post_edit_with_no_change
787 788 issue = Issue.find(1)
788 789 issue.journals.clear
789 790 ActionMailer::Base.deliveries.clear
790 791
791 792 post :edit,
792 793 :id => 1,
793 794 :notes => ''
794 795 assert_redirected_to :action => 'show', :id => '1'
795 796
796 797 issue.reload
797 798 assert issue.journals.empty?
798 799 # No email should be sent
799 800 assert ActionMailer::Base.deliveries.empty?
800 801 end
801 802
802 803 def test_post_edit_should_send_a_notification
803 804 @request.session[:user_id] = 2
804 805 ActionMailer::Base.deliveries.clear
805 806 issue = Issue.find(1)
806 807 old_subject = issue.subject
807 808 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
808 809
809 810 post :edit, :id => 1, :issue => {:subject => new_subject,
810 811 :priority_id => '6',
811 812 :category_id => '1' # no change
812 813 }
813 814 assert_equal 1, ActionMailer::Base.deliveries.size
814 815 end
815 816
816 817 def test_post_edit_with_invalid_spent_time
817 818 @request.session[:user_id] = 2
818 819 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
819 820
820 821 assert_no_difference('Journal.count') do
821 822 post :edit,
822 823 :id => 1,
823 824 :notes => notes,
824 825 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
825 826 end
826 827 assert_response :success
827 828 assert_template 'edit'
828 829
829 830 assert_tag :textarea, :attributes => { :name => 'notes' },
830 831 :content => notes
831 832 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
832 833 end
833 834
834 835 def test_bulk_edit
835 836 @request.session[:user_id] = 2
836 837 # update issues priority
837 838 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
838 839 :assigned_to_id => '',
839 840 :custom_field_values => {'2' => ''},
840 841 :notes => 'Bulk editing'
841 842 assert_response 302
842 843 # check that the issues were updated
843 844 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
844 845
845 846 issue = Issue.find(1)
846 847 journal = issue.journals.find(:first, :order => 'created_on DESC')
847 848 assert_equal '125', issue.custom_value_for(2).value
848 849 assert_equal 'Bulk editing', journal.notes
849 850 assert_equal 1, journal.details.size
850 851 end
851 852
852 853 def test_bullk_edit_should_send_a_notification
853 854 @request.session[:user_id] = 2
854 855 ActionMailer::Base.deliveries.clear
855 856 post(:bulk_edit,
856 857 {
857 858 :ids => [1, 2],
858 859 :priority_id => 7,
859 860 :assigned_to_id => '',
860 861 :custom_field_values => {'2' => ''},
861 862 :notes => 'Bulk editing'
862 863 })
863 864
864 865 assert_response 302
865 866 assert_equal 2, ActionMailer::Base.deliveries.size
866 867 end
867 868
868 869 def test_bulk_edit_custom_field
869 870 @request.session[:user_id] = 2
870 871 # update issues priority
871 872 post :bulk_edit, :ids => [1, 2], :priority_id => '',
872 873 :assigned_to_id => '',
873 874 :custom_field_values => {'2' => '777'},
874 875 :notes => 'Bulk editing custom field'
875 876 assert_response 302
876 877
877 878 issue = Issue.find(1)
878 879 journal = issue.journals.find(:first, :order => 'created_on DESC')
879 880 assert_equal '777', issue.custom_value_for(2).value
880 881 assert_equal 1, journal.details.size
881 882 assert_equal '125', journal.details.first.old_value
882 883 assert_equal '777', journal.details.first.value
883 884 end
884 885
885 886 def test_bulk_unassign
886 887 assert_not_nil Issue.find(2).assigned_to
887 888 @request.session[:user_id] = 2
888 889 # unassign issues
889 890 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
890 891 assert_response 302
891 892 # check that the issues were updated
892 893 assert_nil Issue.find(2).assigned_to
893 894 end
894 895
895 896 def test_move_routing
896 897 assert_routing(
897 898 {:method => :get, :path => '/issues/1/move'},
898 899 :controller => 'issues', :action => 'move', :id => '1'
899 900 )
900 901 assert_recognizes(
901 902 {:controller => 'issues', :action => 'move', :id => '1'},
902 903 {:method => :post, :path => '/issues/1/move'}
903 904 )
904 905 end
905 906
906 907 def test_move_one_issue_to_another_project
907 908 @request.session[:user_id] = 1
908 909 post :move, :id => 1, :new_project_id => 2
909 910 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
910 911 assert_equal 2, Issue.find(1).project_id
911 912 end
912 913
913 914 def test_bulk_move_to_another_project
914 915 @request.session[:user_id] = 1
915 916 post :move, :ids => [1, 2], :new_project_id => 2
916 917 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
917 918 # Issues moved to project 2
918 919 assert_equal 2, Issue.find(1).project_id
919 920 assert_equal 2, Issue.find(2).project_id
920 921 # No tracker change
921 922 assert_equal 1, Issue.find(1).tracker_id
922 923 assert_equal 2, Issue.find(2).tracker_id
923 924 end
924 925
925 926 def test_bulk_move_to_another_tracker
926 927 @request.session[:user_id] = 1
927 928 post :move, :ids => [1, 2], :new_tracker_id => 2
928 929 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
929 930 assert_equal 2, Issue.find(1).tracker_id
930 931 assert_equal 2, Issue.find(2).tracker_id
931 932 end
932 933
933 934 def test_bulk_copy_to_another_project
934 935 @request.session[:user_id] = 1
935 936 assert_difference 'Issue.count', 2 do
936 937 assert_no_difference 'Project.find(1).issues.count' do
937 938 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
938 939 end
939 940 end
940 941 assert_redirected_to 'projects/ecookbook/issues'
941 942 end
942 943
943 944 def test_context_menu_one_issue
944 945 @request.session[:user_id] = 2
945 946 get :context_menu, :ids => [1]
946 947 assert_response :success
947 948 assert_template 'context_menu'
948 949 assert_tag :tag => 'a', :content => 'Edit',
949 950 :attributes => { :href => '/issues/1/edit',
950 951 :class => 'icon-edit' }
951 952 assert_tag :tag => 'a', :content => 'Closed',
952 953 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
953 954 :class => '' }
954 955 assert_tag :tag => 'a', :content => 'Immediate',
955 956 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
956 957 :class => '' }
957 958 assert_tag :tag => 'a', :content => 'Dave Lopper',
958 959 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
959 960 :class => '' }
960 961 assert_tag :tag => 'a', :content => 'Copy',
961 962 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
962 963 :class => 'icon-copy' }
963 964 assert_tag :tag => 'a', :content => 'Move',
964 965 :attributes => { :href => '/issues/move?ids%5B%5D=1',
965 966 :class => 'icon-move' }
966 967 assert_tag :tag => 'a', :content => 'Delete',
967 968 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
968 969 :class => 'icon-del' }
969 970 end
970 971
971 972 def test_context_menu_one_issue_by_anonymous
972 973 get :context_menu, :ids => [1]
973 974 assert_response :success
974 975 assert_template 'context_menu'
975 976 assert_tag :tag => 'a', :content => 'Delete',
976 977 :attributes => { :href => '#',
977 978 :class => 'icon-del disabled' }
978 979 end
979 980
980 981 def test_context_menu_multiple_issues_of_same_project
981 982 @request.session[:user_id] = 2
982 983 get :context_menu, :ids => [1, 2]
983 984 assert_response :success
984 985 assert_template 'context_menu'
985 986 assert_tag :tag => 'a', :content => 'Edit',
986 987 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
987 988 :class => 'icon-edit' }
988 989 assert_tag :tag => 'a', :content => 'Immediate',
989 990 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
990 991 :class => '' }
991 992 assert_tag :tag => 'a', :content => 'Dave Lopper',
992 993 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
993 994 :class => '' }
994 995 assert_tag :tag => 'a', :content => 'Move',
995 996 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
996 997 :class => 'icon-move' }
997 998 assert_tag :tag => 'a', :content => 'Delete',
998 999 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
999 1000 :class => 'icon-del' }
1000 1001 end
1001 1002
1002 1003 def test_context_menu_multiple_issues_of_different_project
1003 1004 @request.session[:user_id] = 2
1004 1005 get :context_menu, :ids => [1, 2, 4]
1005 1006 assert_response :success
1006 1007 assert_template 'context_menu'
1007 1008 assert_tag :tag => 'a', :content => 'Delete',
1008 1009 :attributes => { :href => '#',
1009 1010 :class => 'icon-del disabled' }
1010 1011 end
1011 1012
1012 1013 def test_destroy_routing
1013 1014 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1014 1015 {:controller => 'issues', :action => 'destroy', :id => '1'},
1015 1016 {:method => :post, :path => '/issues/1/destroy'}
1016 1017 )
1017 1018 end
1018 1019
1019 1020 def test_destroy_issue_with_no_time_entries
1020 1021 assert_nil TimeEntry.find_by_issue_id(2)
1021 1022 @request.session[:user_id] = 2
1022 1023 post :destroy, :id => 2
1023 1024 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1024 1025 assert_nil Issue.find_by_id(2)
1025 1026 end
1026 1027
1027 1028 def test_destroy_issues_with_time_entries
1028 1029 @request.session[:user_id] = 2
1029 1030 post :destroy, :ids => [1, 3]
1030 1031 assert_response :success
1031 1032 assert_template 'destroy'
1032 1033 assert_not_nil assigns(:hours)
1033 1034 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1034 1035 end
1035 1036
1036 1037 def test_destroy_issues_and_destroy_time_entries
1037 1038 @request.session[:user_id] = 2
1038 1039 post :destroy, :ids => [1, 3], :todo => 'destroy'
1039 1040 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1040 1041 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1041 1042 assert_nil TimeEntry.find_by_id([1, 2])
1042 1043 end
1043 1044
1044 1045 def test_destroy_issues_and_assign_time_entries_to_project
1045 1046 @request.session[:user_id] = 2
1046 1047 post :destroy, :ids => [1, 3], :todo => 'nullify'
1047 1048 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1048 1049 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1049 1050 assert_nil TimeEntry.find(1).issue_id
1050 1051 assert_nil TimeEntry.find(2).issue_id
1051 1052 end
1052 1053
1053 1054 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1054 1055 @request.session[:user_id] = 2
1055 1056 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1056 1057 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1057 1058 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1058 1059 assert_equal 2, TimeEntry.find(1).issue_id
1059 1060 assert_equal 2, TimeEntry.find(2).issue_id
1060 1061 end
1061 1062 end
@@ -1,59 +1,59
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'journals_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class JournalsController; def rescue_action(e) raise e end; end
23 23
24 24 class JournalsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :issues, :journals, :journal_details, :enabled_modules
25 fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules
26 26
27 27 def setup
28 28 @controller = JournalsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_get_edit
35 35 @request.session[:user_id] = 1
36 36 xhr :get, :edit, :id => 2
37 37 assert_response :success
38 38 assert_select_rjs :insert, :after, 'journal-2-notes' do
39 39 assert_select 'form[id=journal-2-form]'
40 40 assert_select 'textarea'
41 41 end
42 42 end
43 43
44 44 def test_post_edit
45 45 @request.session[:user_id] = 1
46 46 xhr :post, :edit, :id => 2, :notes => 'Updated notes'
47 47 assert_response :success
48 48 assert_select_rjs :replace, 'journal-2-notes'
49 49 assert_equal 'Updated notes', Journal.find(2).notes
50 50 end
51 51
52 52 def test_post_edit_with_empty_notes
53 53 @request.session[:user_id] = 1
54 54 xhr :post, :edit, :id => 2, :notes => ''
55 55 assert_response :success
56 56 assert_select_rjs :remove, 'change-2'
57 57 assert_nil Journal.find_by_id(2)
58 58 end
59 59 end
@@ -1,53 +1,53
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'mail_handler_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class MailHandlerController; def rescue_action(e) raise e end; end
23 23
24 24 class MailHandlerControllerTest < Test::Unit::TestCase
25 fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :issue_statuses, :trackers, :enumerations
25 fixtures :users, :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :issue_statuses, :trackers, :enumerations
26 26
27 27 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
28 28
29 29 def setup
30 30 @controller = MailHandlerController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 User.current = nil
34 34 end
35 35
36 36 def test_should_create_issue
37 37 # Enable API and set a key
38 38 Setting.mail_handler_api_enabled = 1
39 39 Setting.mail_handler_api_key = 'secret'
40 40
41 41 post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
42 42 assert_response 201
43 43 end
44 44
45 45 def test_should_not_allow
46 46 # Disable API
47 47 Setting.mail_handler_api_enabled = 0
48 48 Setting.mail_handler_api_key = 'secret'
49 49
50 50 post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
51 51 assert_response 403
52 52 end
53 53 end
@@ -1,89 +1,89
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'members_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class MembersController; def rescue_action(e) raise e end; end
23 23
24 24
25 25 class MembersControllerTest < Test::Unit::TestCase
26 fixtures :projects, :members, :roles, :users
26 fixtures :projects, :members, :member_roles, :roles, :users
27 27
28 28 def setup
29 29 @controller = MembersController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 User.current = nil
33 33 @request.session[:user_id] = 2
34 34 end
35 35
36 36 def test_members_routing
37 37 assert_routing(
38 38 {:method => :post, :path => 'projects/5234/members/new'},
39 39 :controller => 'members', :action => 'new', :id => '5234'
40 40 )
41 41 end
42 42
43 43 def test_create
44 44 assert_difference 'Member.count' do
45 post :new, :id => 1, :member => {:role_id => 1, :user_id => 7}
45 post :new, :id => 1, :member => {:role_ids => [1], :user_id => 7}
46 46 end
47 47 assert_redirected_to '/projects/ecookbook/settings/members'
48 48 assert User.find(7).member_of?(Project.find(1))
49 49 end
50 50
51 51 def test_create_by_user_login
52 52 assert_difference 'Member.count' do
53 post :new, :id => 1, :member => {:role_id => 1, :user_login => 'someone'}
53 post :new, :id => 1, :member => {:role_ids => [1], :user_login => 'someone'}
54 54 end
55 55 assert_redirected_to '/projects/ecookbook/settings/members'
56 56 assert User.find(7).member_of?(Project.find(1))
57 57 end
58 58
59 59 def test_create_multiple
60 60 assert_difference 'Member.count', 3 do
61 post :new, :id => 1, :member => {:role_id => 1, :user_ids => [7, 8, 9]}
61 post :new, :id => 1, :member => {:role_ids => [1], :user_ids => [7, 8, 9]}
62 62 end
63 63 assert_redirected_to '/projects/ecookbook/settings/members'
64 64 assert User.find(7).member_of?(Project.find(1))
65 65 end
66 66
67 67 def test_edit
68 68 assert_no_difference 'Member.count' do
69 post :edit, :id => 2, :member => {:role_id => 1, :user_id => 3}
69 post :edit, :id => 2, :member => {:role_ids => [1], :user_id => 3}
70 70 end
71 71 assert_redirected_to '/projects/ecookbook/settings/members'
72 72 end
73 73
74 74 def test_destroy
75 75 assert_difference 'Member.count', -1 do
76 76 post :destroy, :id => 2
77 77 end
78 78 assert_redirected_to '/projects/ecookbook/settings/members'
79 79 assert !User.find(3).member_of?(Project.find(1))
80 80 end
81 81
82 82 def test_autocomplete_for_member_login
83 83 get :autocomplete_for_member_login, :id => 1, :user => 'mis'
84 84 assert_response :success
85 85 assert_template 'autocomplete_for_member_login'
86 86
87 87 assert_tag :ul, :child => {:tag => 'li', :content => /miscuser8/}
88 88 end
89 89 end
@@ -1,170 +1,170
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'messages_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class MessagesController; def rescue_action(e) raise e end; end
23 23
24 24 class MessagesControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :boards, :messages, :enabled_modules
25 fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
26 26
27 27 def setup
28 28 @controller = MessagesController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_show_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/boards/22/topics/2'},
37 37 :controller => 'messages', :action => 'show', :id => '2', :board_id => '22'
38 38 )
39 39 end
40 40
41 41 def test_show
42 42 get :show, :board_id => 1, :id => 1
43 43 assert_response :success
44 44 assert_template 'show'
45 45 assert_not_nil assigns(:board)
46 46 assert_not_nil assigns(:project)
47 47 assert_not_nil assigns(:topic)
48 48 end
49 49
50 50 def test_show_with_reply_permission
51 51 @request.session[:user_id] = 2
52 52 get :show, :board_id => 1, :id => 1
53 53 assert_response :success
54 54 assert_template 'show'
55 55 assert_tag :div, :attributes => { :id => 'reply' },
56 56 :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } }
57 57 end
58 58
59 59 def test_show_message_not_found
60 60 get :show, :board_id => 1, :id => 99999
61 61 assert_response 404
62 62 end
63 63
64 64 def test_new_routing
65 65 assert_routing(
66 66 {:method => :get, :path => '/boards/lala/topics/new'},
67 67 :controller => 'messages', :action => 'new', :board_id => 'lala'
68 68 )
69 69 assert_recognizes(#TODO: POST to collection, need to adjust form accordingly
70 70 {:controller => 'messages', :action => 'new', :board_id => 'lala'},
71 71 {:method => :post, :path => '/boards/lala/topics/new'}
72 72 )
73 73 end
74 74
75 75 def test_get_new
76 76 @request.session[:user_id] = 2
77 77 get :new, :board_id => 1
78 78 assert_response :success
79 79 assert_template 'new'
80 80 end
81 81
82 82 def test_post_new
83 83 @request.session[:user_id] = 2
84 84 ActionMailer::Base.deliveries.clear
85 85 Setting.notified_events = ['message_posted']
86 86
87 87 post :new, :board_id => 1,
88 88 :message => { :subject => 'Test created message',
89 89 :content => 'Message body'}
90 90 message = Message.find_by_subject('Test created message')
91 91 assert_not_nil message
92 92 assert_redirected_to "boards/1/topics/#{message.to_param}"
93 93 assert_equal 'Message body', message.content
94 94 assert_equal 2, message.author_id
95 95 assert_equal 1, message.board_id
96 96
97 97 mail = ActionMailer::Base.deliveries.last
98 98 assert_kind_of TMail::Mail, mail
99 99 assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject
100 100 assert mail.body.include?('Message body')
101 101 # author
102 102 assert mail.bcc.include?('jsmith@somenet.foo')
103 103 # project member
104 104 assert mail.bcc.include?('dlopper@somenet.foo')
105 105 end
106 106
107 107 def test_edit_routing
108 108 assert_routing(
109 109 {:method => :get, :path => '/boards/lala/topics/22/edit'},
110 110 :controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'
111 111 )
112 112 assert_recognizes( #TODO: use PUT to topic_path, modify form accordingly
113 113 {:controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'},
114 114 {:method => :post, :path => '/boards/lala/topics/22/edit'}
115 115 )
116 116 end
117 117
118 118 def test_get_edit
119 119 @request.session[:user_id] = 2
120 120 get :edit, :board_id => 1, :id => 1
121 121 assert_response :success
122 122 assert_template 'edit'
123 123 end
124 124
125 125 def test_post_edit
126 126 @request.session[:user_id] = 2
127 127 post :edit, :board_id => 1, :id => 1,
128 128 :message => { :subject => 'New subject',
129 129 :content => 'New body'}
130 130 assert_redirected_to 'boards/1/topics/1'
131 131 message = Message.find(1)
132 132 assert_equal 'New subject', message.subject
133 133 assert_equal 'New body', message.content
134 134 end
135 135
136 136 def test_reply_routing
137 137 assert_recognizes(
138 138 {:controller => 'messages', :action => 'reply', :board_id => '22', :id => '555'},
139 139 {:method => :post, :path => '/boards/22/topics/555/replies'}
140 140 )
141 141 end
142 142
143 143 def test_reply
144 144 @request.session[:user_id] = 2
145 145 post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' }
146 146 assert_redirected_to 'boards/1/topics/1'
147 147 assert Message.find_by_subject('Test reply')
148 148 end
149 149
150 150 def test_destroy_routing
151 151 assert_recognizes(#TODO: use DELETE to topic_path, adjust form accordingly
152 152 {:controller => 'messages', :action => 'destroy', :board_id => '22', :id => '555'},
153 153 {:method => :post, :path => '/boards/22/topics/555/destroy'}
154 154 )
155 155 end
156 156
157 157 def test_destroy_topic
158 158 @request.session[:user_id] = 2
159 159 post :destroy, :board_id => 1, :id => 1
160 160 assert_redirected_to 'projects/ecookbook/boards/1'
161 161 assert_nil Message.find_by_id(1)
162 162 end
163 163
164 164 def test_quote
165 165 @request.session[:user_id] = 2
166 166 xhr :get, :quote, :board_id => 1, :id => 3
167 167 assert_response :success
168 168 assert_select_rjs :show, 'reply'
169 169 end
170 170 end
@@ -1,215 +1,215
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'news_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class NewsController; def rescue_action(e) raise e end; end
23 23
24 24 class NewsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :enabled_modules, :news, :comments
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments
26 26
27 27 def setup
28 28 @controller = NewsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_index_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/news'},
37 37 :controller => 'news', :action => 'index'
38 38 )
39 39 end
40 40
41 41 def test_index_routing_formatted
42 42 assert_routing(
43 43 {:method => :get, :path => '/news.atom'},
44 44 :controller => 'news', :action => 'index', :format => 'atom'
45 45 )
46 46 end
47 47
48 48 def test_index
49 49 get :index
50 50 assert_response :success
51 51 assert_template 'index'
52 52 assert_not_nil assigns(:newss)
53 53 assert_nil assigns(:project)
54 54 end
55 55
56 56 def test_index_with_project_routing
57 57 assert_routing(
58 58 {:method => :get, :path => '/projects/567/news'},
59 59 :controller => 'news', :action => 'index', :project_id => '567'
60 60 )
61 61 end
62 62
63 63 def test_index_with_project_routing_formatted
64 64 assert_routing(
65 65 {:method => :get, :path => '/projects/567/news.atom'},
66 66 :controller => 'news', :action => 'index', :project_id => '567', :format => 'atom'
67 67 )
68 68 end
69 69
70 70 def test_index_with_project
71 71 get :index, :project_id => 1
72 72 assert_response :success
73 73 assert_template 'index'
74 74 assert_not_nil assigns(:newss)
75 75 end
76 76
77 77 def test_show_routing
78 78 assert_routing(
79 79 {:method => :get, :path => '/news/2'},
80 80 :controller => 'news', :action => 'show', :id => '2'
81 81 )
82 82 end
83 83
84 84 def test_show
85 85 get :show, :id => 1
86 86 assert_response :success
87 87 assert_template 'show'
88 88 assert_tag :tag => 'h2', :content => /eCookbook first release/
89 89 end
90 90
91 91 def test_show_not_found
92 92 get :show, :id => 999
93 93 assert_response 404
94 94 end
95 95
96 96 def test_new_routing
97 97 assert_routing(
98 98 {:method => :get, :path => '/projects/567/news/new'},
99 99 :controller => 'news', :action => 'new', :project_id => '567'
100 100 )
101 101 assert_recognizes(
102 102 {:controller => 'news', :action => 'new', :project_id => '567'},
103 103 {:method => :post, :path => '/projects/567/news'}
104 104 )
105 105 end
106 106
107 107 def test_get_new
108 108 @request.session[:user_id] = 2
109 109 get :new, :project_id => 1
110 110 assert_response :success
111 111 assert_template 'new'
112 112 end
113 113
114 114 def test_post_new
115 115 ActionMailer::Base.deliveries.clear
116 116 Setting.notified_events << 'news_added'
117 117
118 118 @request.session[:user_id] = 2
119 119 post :new, :project_id => 1, :news => { :title => 'NewsControllerTest',
120 120 :description => 'This is the description',
121 121 :summary => '' }
122 122 assert_redirected_to 'projects/ecookbook/news'
123 123
124 124 news = News.find_by_title('NewsControllerTest')
125 125 assert_not_nil news
126 126 assert_equal 'This is the description', news.description
127 127 assert_equal User.find(2), news.author
128 128 assert_equal Project.find(1), news.project
129 129 assert_equal 1, ActionMailer::Base.deliveries.size
130 130 end
131 131
132 132 def test_edit_routing
133 133 assert_routing(
134 134 {:method => :get, :path => '/news/234'},
135 135 :controller => 'news', :action => 'show', :id => '234'
136 136 )
137 137 assert_recognizes(#TODO: PUT to news URI instead, need to modify form
138 138 {:controller => 'news', :action => 'edit', :id => '567'},
139 139 {:method => :post, :path => '/news/567/edit'}
140 140 )
141 141 end
142 142
143 143 def test_get_edit
144 144 @request.session[:user_id] = 2
145 145 get :edit, :id => 1
146 146 assert_response :success
147 147 assert_template 'edit'
148 148 end
149 149
150 150 def test_post_edit
151 151 @request.session[:user_id] = 2
152 152 post :edit, :id => 1, :news => { :description => 'Description changed by test_post_edit' }
153 153 assert_redirected_to 'news/1'
154 154 news = News.find(1)
155 155 assert_equal 'Description changed by test_post_edit', news.description
156 156 end
157 157
158 158 def test_post_new_with_validation_failure
159 159 @request.session[:user_id] = 2
160 160 post :new, :project_id => 1, :news => { :title => '',
161 161 :description => 'This is the description',
162 162 :summary => '' }
163 163 assert_response :success
164 164 assert_template 'new'
165 165 assert_not_nil assigns(:news)
166 166 assert assigns(:news).new_record?
167 167 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' },
168 168 :content => /1 error/
169 169 end
170 170
171 171 def test_add_comment
172 172 @request.session[:user_id] = 2
173 173 post :add_comment, :id => 1, :comment => { :comments => 'This is a NewsControllerTest comment' }
174 174 assert_redirected_to 'news/1'
175 175
176 176 comment = News.find(1).comments.find(:first, :order => 'created_on DESC')
177 177 assert_not_nil comment
178 178 assert_equal 'This is a NewsControllerTest comment', comment.comments
179 179 assert_equal User.find(2), comment.author
180 180 end
181 181
182 182 def test_destroy_comment
183 183 comments_count = News.find(1).comments.size
184 184 @request.session[:user_id] = 2
185 185 post :destroy_comment, :id => 1, :comment_id => 2
186 186 assert_redirected_to 'news/1'
187 187 assert_nil Comment.find_by_id(2)
188 188 assert_equal comments_count - 1, News.find(1).comments.size
189 189 end
190 190
191 191 def test_destroy_routing
192 192 assert_recognizes(#TODO: should use DELETE to news URI, need to change form
193 193 {:controller => 'news', :action => 'destroy', :id => '567'},
194 194 {:method => :post, :path => '/news/567/destroy'}
195 195 )
196 196 end
197 197
198 198 def test_destroy
199 199 @request.session[:user_id] = 2
200 200 post :destroy, :id => 1
201 201 assert_redirected_to 'projects/ecookbook/news'
202 202 assert_nil News.find_by_id(1)
203 203 end
204 204
205 205 def test_preview
206 206 get :preview, :project_id => 1,
207 207 :news => {:title => '',
208 208 :description => 'News description',
209 209 :summary => ''}
210 210 assert_response :success
211 211 assert_template 'common/_preview'
212 212 assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' },
213 213 :content => /News description/
214 214 end
215 215 end
@@ -1,517 +1,517
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'projects_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class ProjectsController; def rescue_action(e) raise e end; end
23 23
24 24 class ProjectsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 27 :attachments
28 28
29 29 def setup
30 30 @controller = ProjectsController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 @request.session[:user_id] = nil
34 34 Setting.default_language = 'en'
35 35 end
36 36
37 37 def test_index_routing
38 38 assert_routing(
39 39 {:method => :get, :path => '/projects'},
40 40 :controller => 'projects', :action => 'index'
41 41 )
42 42 end
43 43
44 44 def test_index
45 45 get :index
46 46 assert_response :success
47 47 assert_template 'index'
48 48 assert_not_nil assigns(:projects)
49 49
50 50 assert_tag :ul, :child => {:tag => 'li',
51 51 :descendant => {:tag => 'a', :content => 'eCookbook'},
52 52 :child => { :tag => 'ul',
53 53 :descendant => { :tag => 'a',
54 54 :content => 'Child of private child'
55 55 }
56 56 }
57 57 }
58 58
59 59 assert_no_tag :a, :content => /Private child of eCookbook/
60 60 end
61 61
62 62 def test_index_atom_routing
63 63 assert_routing(
64 64 {:method => :get, :path => '/projects.atom'},
65 65 :controller => 'projects', :action => 'index', :format => 'atom'
66 66 )
67 67 end
68 68
69 69 def test_index_atom
70 70 get :index, :format => 'atom'
71 71 assert_response :success
72 72 assert_template 'common/feed.atom.rxml'
73 73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
74 74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
75 75 end
76 76
77 77 def test_add_routing
78 78 assert_routing(
79 79 {:method => :get, :path => '/projects/new'},
80 80 :controller => 'projects', :action => 'add'
81 81 )
82 82 assert_recognizes(
83 83 {:controller => 'projects', :action => 'add'},
84 84 {:method => :post, :path => '/projects/new'}
85 85 )
86 86 assert_recognizes(
87 87 {:controller => 'projects', :action => 'add'},
88 88 {:method => :post, :path => '/projects'}
89 89 )
90 90 end
91 91
92 92 def test_show_routing
93 93 assert_routing(
94 94 {:method => :get, :path => '/projects/test'},
95 95 :controller => 'projects', :action => 'show', :id => 'test'
96 96 )
97 97 end
98 98
99 99 def test_show_by_id
100 100 get :show, :id => 1
101 101 assert_response :success
102 102 assert_template 'show'
103 103 assert_not_nil assigns(:project)
104 104 end
105 105
106 106 def test_show_by_identifier
107 107 get :show, :id => 'ecookbook'
108 108 assert_response :success
109 109 assert_template 'show'
110 110 assert_not_nil assigns(:project)
111 111 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
112 112 end
113 113
114 114 def test_private_subprojects_hidden
115 115 get :show, :id => 'ecookbook'
116 116 assert_response :success
117 117 assert_template 'show'
118 118 assert_no_tag :tag => 'a', :content => /Private child/
119 119 end
120 120
121 121 def test_private_subprojects_visible
122 122 @request.session[:user_id] = 2 # manager who is a member of the private subproject
123 123 get :show, :id => 'ecookbook'
124 124 assert_response :success
125 125 assert_template 'show'
126 126 assert_tag :tag => 'a', :content => /Private child/
127 127 end
128 128
129 129 def test_settings_routing
130 130 assert_routing(
131 131 {:method => :get, :path => '/projects/4223/settings'},
132 132 :controller => 'projects', :action => 'settings', :id => '4223'
133 133 )
134 134 assert_routing(
135 135 {:method => :get, :path => '/projects/4223/settings/members'},
136 136 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
137 137 )
138 138 end
139 139
140 140 def test_settings
141 141 @request.session[:user_id] = 2 # manager
142 142 get :settings, :id => 1
143 143 assert_response :success
144 144 assert_template 'settings'
145 145 end
146 146
147 147 def test_edit
148 148 @request.session[:user_id] = 2 # manager
149 149 post :edit, :id => 1, :project => {:name => 'Test changed name',
150 150 :issue_custom_field_ids => ['']}
151 151 assert_redirected_to 'projects/ecookbook/settings'
152 152 project = Project.find(1)
153 153 assert_equal 'Test changed name', project.name
154 154 end
155 155
156 156 def test_add_version_routing
157 157 assert_routing(
158 158 {:method => :get, :path => 'projects/64/versions/new'},
159 159 :controller => 'projects', :action => 'add_version', :id => '64'
160 160 )
161 161 assert_routing(
162 162 #TODO: use PUT
163 163 {:method => :post, :path => 'projects/64/versions/new'},
164 164 :controller => 'projects', :action => 'add_version', :id => '64'
165 165 )
166 166 end
167 167
168 168 def test_add_issue_category_routing
169 169 assert_routing(
170 170 {:method => :get, :path => 'projects/test/categories/new'},
171 171 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
172 172 )
173 173 assert_routing(
174 174 #TODO: use PUT and update form
175 175 {:method => :post, :path => 'projects/64/categories/new'},
176 176 :controller => 'projects', :action => 'add_issue_category', :id => '64'
177 177 )
178 178 end
179 179
180 180 def test_destroy_routing
181 181 assert_routing(
182 182 {:method => :get, :path => '/projects/567/destroy'},
183 183 :controller => 'projects', :action => 'destroy', :id => '567'
184 184 )
185 185 assert_routing(
186 186 #TODO: use DELETE and update form
187 187 {:method => :post, :path => 'projects/64/destroy'},
188 188 :controller => 'projects', :action => 'destroy', :id => '64'
189 189 )
190 190 end
191 191
192 192 def test_get_destroy
193 193 @request.session[:user_id] = 1 # admin
194 194 get :destroy, :id => 1
195 195 assert_response :success
196 196 assert_template 'destroy'
197 197 assert_not_nil Project.find_by_id(1)
198 198 end
199 199
200 200 def test_post_destroy
201 201 @request.session[:user_id] = 1 # admin
202 202 post :destroy, :id => 1, :confirm => 1
203 203 assert_redirected_to 'admin/projects'
204 204 assert_nil Project.find_by_id(1)
205 205 end
206 206
207 207 def test_add_file
208 208 set_tmp_attachments_directory
209 209 @request.session[:user_id] = 2
210 210 Setting.notified_events = ['file_added']
211 211 ActionMailer::Base.deliveries.clear
212 212
213 213 assert_difference 'Attachment.count' do
214 214 post :add_file, :id => 1, :version_id => '',
215 215 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
216 216 end
217 217 assert_redirected_to 'projects/ecookbook/files'
218 218 a = Attachment.find(:first, :order => 'created_on DESC')
219 219 assert_equal 'testfile.txt', a.filename
220 220 assert_equal Project.find(1), a.container
221 221
222 222 mail = ActionMailer::Base.deliveries.last
223 223 assert_kind_of TMail::Mail, mail
224 224 assert_equal "[eCookbook] New file", mail.subject
225 225 assert mail.body.include?('testfile.txt')
226 226 end
227 227
228 228 def test_add_file_routing
229 229 assert_routing(
230 230 {:method => :get, :path => '/projects/33/files/new'},
231 231 :controller => 'projects', :action => 'add_file', :id => '33'
232 232 )
233 233 assert_routing(
234 234 {:method => :post, :path => '/projects/33/files/new'},
235 235 :controller => 'projects', :action => 'add_file', :id => '33'
236 236 )
237 237 end
238 238
239 239 def test_add_version_file
240 240 set_tmp_attachments_directory
241 241 @request.session[:user_id] = 2
242 242 Setting.notified_events = ['file_added']
243 243
244 244 assert_difference 'Attachment.count' do
245 245 post :add_file, :id => 1, :version_id => '2',
246 246 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
247 247 end
248 248 assert_redirected_to 'projects/ecookbook/files'
249 249 a = Attachment.find(:first, :order => 'created_on DESC')
250 250 assert_equal 'testfile.txt', a.filename
251 251 assert_equal Version.find(2), a.container
252 252 end
253 253
254 254 def test_list_files
255 255 get :list_files, :id => 1
256 256 assert_response :success
257 257 assert_template 'list_files'
258 258 assert_not_nil assigns(:containers)
259 259
260 260 # file attached to the project
261 261 assert_tag :a, :content => 'project_file.zip',
262 262 :attributes => { :href => '/attachments/download/8/project_file.zip' }
263 263
264 264 # file attached to a project's version
265 265 assert_tag :a, :content => 'version_file.zip',
266 266 :attributes => { :href => '/attachments/download/9/version_file.zip' }
267 267 end
268 268
269 269 def test_list_files_routing
270 270 assert_routing(
271 271 {:method => :get, :path => '/projects/33/files'},
272 272 :controller => 'projects', :action => 'list_files', :id => '33'
273 273 )
274 274 end
275 275
276 276 def test_changelog_routing
277 277 assert_routing(
278 278 {:method => :get, :path => '/projects/44/changelog'},
279 279 :controller => 'projects', :action => 'changelog', :id => '44'
280 280 )
281 281 end
282 282
283 283 def test_changelog
284 284 get :changelog, :id => 1
285 285 assert_response :success
286 286 assert_template 'changelog'
287 287 assert_not_nil assigns(:versions)
288 288 end
289 289
290 290 def test_roadmap_routing
291 291 assert_routing(
292 292 {:method => :get, :path => 'projects/33/roadmap'},
293 293 :controller => 'projects', :action => 'roadmap', :id => '33'
294 294 )
295 295 end
296 296
297 297 def test_roadmap
298 298 get :roadmap, :id => 1
299 299 assert_response :success
300 300 assert_template 'roadmap'
301 301 assert_not_nil assigns(:versions)
302 302 # Version with no date set appears
303 303 assert assigns(:versions).include?(Version.find(3))
304 304 # Completed version doesn't appear
305 305 assert !assigns(:versions).include?(Version.find(1))
306 306 end
307 307
308 308 def test_roadmap_with_completed_versions
309 309 get :roadmap, :id => 1, :completed => 1
310 310 assert_response :success
311 311 assert_template 'roadmap'
312 312 assert_not_nil assigns(:versions)
313 313 # Version with no date set appears
314 314 assert assigns(:versions).include?(Version.find(3))
315 315 # Completed version appears
316 316 assert assigns(:versions).include?(Version.find(1))
317 317 end
318 318
319 319 def test_project_activity_routing
320 320 assert_routing(
321 321 {:method => :get, :path => '/projects/1/activity'},
322 322 :controller => 'projects', :action => 'activity', :id => '1'
323 323 )
324 324 end
325 325
326 326 def test_project_activity_atom_routing
327 327 assert_routing(
328 328 {:method => :get, :path => '/projects/1/activity.atom'},
329 329 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
330 330 )
331 331 end
332 332
333 333 def test_project_activity
334 334 get :activity, :id => 1, :with_subprojects => 0
335 335 assert_response :success
336 336 assert_template 'activity'
337 337 assert_not_nil assigns(:events_by_day)
338 338
339 339 assert_tag :tag => "h3",
340 340 :content => /#{2.days.ago.to_date.day}/,
341 341 :sibling => { :tag => "dl",
342 342 :child => { :tag => "dt",
343 343 :attributes => { :class => /issue-edit/ },
344 344 :child => { :tag => "a",
345 345 :content => /(#{IssueStatus.find(2).name})/,
346 346 }
347 347 }
348 348 }
349 349 end
350 350
351 351 def test_previous_project_activity
352 352 get :activity, :id => 1, :from => 3.days.ago.to_date
353 353 assert_response :success
354 354 assert_template 'activity'
355 355 assert_not_nil assigns(:events_by_day)
356 356
357 357 assert_tag :tag => "h3",
358 358 :content => /#{3.day.ago.to_date.day}/,
359 359 :sibling => { :tag => "dl",
360 360 :child => { :tag => "dt",
361 361 :attributes => { :class => /issue/ },
362 362 :child => { :tag => "a",
363 363 :content => /#{Issue.find(1).subject}/,
364 364 }
365 365 }
366 366 }
367 367 end
368 368
369 369 def test_global_activity_routing
370 370 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
371 371 end
372 372
373 373 def test_global_activity
374 374 get :activity
375 375 assert_response :success
376 376 assert_template 'activity'
377 377 assert_not_nil assigns(:events_by_day)
378 378
379 379 assert_tag :tag => "h3",
380 380 :content => /#{5.day.ago.to_date.day}/,
381 381 :sibling => { :tag => "dl",
382 382 :child => { :tag => "dt",
383 383 :attributes => { :class => /issue/ },
384 384 :child => { :tag => "a",
385 385 :content => /#{Issue.find(5).subject}/,
386 386 }
387 387 }
388 388 }
389 389 end
390 390
391 391 def test_user_activity
392 392 get :activity, :user_id => 2
393 393 assert_response :success
394 394 assert_template 'activity'
395 395 assert_not_nil assigns(:events_by_day)
396 396
397 397 assert_tag :tag => "h3",
398 398 :content => /#{3.day.ago.to_date.day}/,
399 399 :sibling => { :tag => "dl",
400 400 :child => { :tag => "dt",
401 401 :attributes => { :class => /issue/ },
402 402 :child => { :tag => "a",
403 403 :content => /#{Issue.find(1).subject}/,
404 404 }
405 405 }
406 406 }
407 407 end
408 408
409 409 def test_global_activity_atom_routing
410 410 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
411 411 end
412 412
413 413 def test_activity_atom_feed
414 414 get :activity, :format => 'atom'
415 415 assert_response :success
416 416 assert_template 'common/feed.atom.rxml'
417 417 end
418 418
419 419 def test_archive_routing
420 420 assert_routing(
421 421 #TODO: use PUT to project path and modify form
422 422 {:method => :post, :path => 'projects/64/archive'},
423 423 :controller => 'projects', :action => 'archive', :id => '64'
424 424 )
425 425 end
426 426
427 427 def test_archive
428 428 @request.session[:user_id] = 1 # admin
429 429 post :archive, :id => 1
430 430 assert_redirected_to 'admin/projects'
431 431 assert !Project.find(1).active?
432 432 end
433 433
434 434 def test_unarchive_routing
435 435 assert_routing(
436 436 #TODO: use PUT to project path and modify form
437 437 {:method => :post, :path => '/projects/567/unarchive'},
438 438 :controller => 'projects', :action => 'unarchive', :id => '567'
439 439 )
440 440 end
441 441
442 442 def test_unarchive
443 443 @request.session[:user_id] = 1 # admin
444 444 Project.find(1).archive
445 445 post :unarchive, :id => 1
446 446 assert_redirected_to 'admin/projects'
447 447 assert Project.find(1).active?
448 448 end
449 449
450 450 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
451 451 CustomField.delete_all
452 452 parent = nil
453 453 6.times do |i|
454 454 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
455 455 p.set_parent!(parent)
456 456 get :show, :id => p
457 457 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
458 458 :children => { :count => [i, 3].min,
459 459 :only => { :tag => 'a' } }
460 460
461 461 parent = p
462 462 end
463 463 end
464 464
465 465 def test_copy_with_project
466 466 @request.session[:user_id] = 1 # admin
467 467 get :copy, :id => 1
468 468 assert_response :success
469 469 assert_template 'copy'
470 470 assert assigns(:project)
471 471 assert_equal Project.find(1).description, assigns(:project).description
472 472 assert_nil assigns(:project).id
473 473 end
474 474
475 475 def test_copy_without_project
476 476 @request.session[:user_id] = 1 # admin
477 477 get :copy
478 478 assert_response :redirect
479 479 assert_redirected_to :controller => 'admin', :action => 'projects'
480 480 end
481 481
482 482 def test_jump_should_redirect_to_active_tab
483 483 get :show, :id => 1, :jump => 'issues'
484 484 assert_redirected_to 'projects/ecookbook/issues'
485 485 end
486 486
487 487 def test_jump_should_not_redirect_to_inactive_tab
488 488 get :show, :id => 3, :jump => 'documents'
489 489 assert_response :success
490 490 assert_template 'show'
491 491 end
492 492
493 493 def test_jump_should_not_redirect_to_unknown_tab
494 494 get :show, :id => 3, :jump => 'foobar'
495 495 assert_response :success
496 496 assert_template 'show'
497 497 end
498 498
499 499 # A hook that is manually registered later
500 500 class ProjectBasedTemplate < Redmine::Hook::ViewListener
501 501 def view_layouts_base_html_head(context)
502 502 # Adds a project stylesheet
503 503 stylesheet_link_tag(context[:project].identifier) if context[:project]
504 504 end
505 505 end
506 506 # Don't use this hook now
507 507 Redmine::Hook.clear_listeners
508 508
509 509 def test_hook_response
510 510 Redmine::Hook.add_listener(ProjectBasedTemplate)
511 511 get :show, :id => 1
512 512 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
513 513 :parent => {:tag => 'head'}
514 514
515 515 Redmine::Hook.clear_listeners
516 516 end
517 517 end
@@ -1,240 +1,240
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'queries_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class QueriesController; def rescue_action(e) raise e end; end
23 23
24 24 class QueriesControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
25 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
26 26
27 27 def setup
28 28 @controller = QueriesController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_get_new_project_query
35 35 @request.session[:user_id] = 2
36 36 get :new, :project_id => 1
37 37 assert_response :success
38 38 assert_template 'new'
39 39 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
40 40 :name => 'query[is_public]',
41 41 :checked => nil }
42 42 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
43 43 :name => 'query_is_for_all',
44 44 :checked => nil,
45 45 :disabled => nil }
46 46 end
47 47
48 48 def test_get_new_global_query
49 49 @request.session[:user_id] = 2
50 50 get :new
51 51 assert_response :success
52 52 assert_template 'new'
53 53 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
54 54 :name => 'query[is_public]' }
55 55 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
56 56 :name => 'query_is_for_all',
57 57 :checked => 'checked',
58 58 :disabled => nil }
59 59 end
60 60
61 61 def test_new_project_public_query
62 62 @request.session[:user_id] = 2
63 63 post :new,
64 64 :project_id => 'ecookbook',
65 65 :confirm => '1',
66 66 :default_columns => '1',
67 67 :fields => ["status_id", "assigned_to_id"],
68 68 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
69 69 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
70 70 :query => {"name" => "test_new_project_public_query", "is_public" => "1"}
71 71
72 72 q = Query.find_by_name('test_new_project_public_query')
73 73 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
74 74 assert q.is_public?
75 75 assert q.has_default_columns?
76 76 assert q.valid?
77 77 end
78 78
79 79 def test_new_project_private_query
80 80 @request.session[:user_id] = 3
81 81 post :new,
82 82 :project_id => 'ecookbook',
83 83 :confirm => '1',
84 84 :default_columns => '1',
85 85 :fields => ["status_id", "assigned_to_id"],
86 86 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
87 87 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
88 88 :query => {"name" => "test_new_project_private_query", "is_public" => "1"}
89 89
90 90 q = Query.find_by_name('test_new_project_private_query')
91 91 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
92 92 assert !q.is_public?
93 93 assert q.has_default_columns?
94 94 assert q.valid?
95 95 end
96 96
97 97 def test_new_global_private_query_with_custom_columns
98 98 @request.session[:user_id] = 3
99 99 post :new,
100 100 :confirm => '1',
101 101 :fields => ["status_id", "assigned_to_id"],
102 102 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
103 103 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
104 104 :query => {"name" => "test_new_global_private_query", "is_public" => "1", "column_names" => ["", "tracker", "subject", "priority", "category"]}
105 105
106 106 q = Query.find_by_name('test_new_global_private_query')
107 107 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
108 108 assert !q.is_public?
109 109 assert !q.has_default_columns?
110 110 assert_equal [:tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
111 111 assert q.valid?
112 112 end
113 113
114 114 def test_new_with_sort
115 115 @request.session[:user_id] = 1
116 116 post :new,
117 117 :confirm => '1',
118 118 :default_columns => '1',
119 119 :operators => {"status_id" => "o"},
120 120 :values => {"status_id" => ["1"]},
121 121 :query => {:name => "test_new_with_sort",
122 122 :is_public => "1",
123 123 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
124 124
125 125 query = Query.find_by_name("test_new_with_sort")
126 126 assert_not_nil query
127 127 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
128 128 end
129 129
130 130 def test_get_edit_global_public_query
131 131 @request.session[:user_id] = 1
132 132 get :edit, :id => 4
133 133 assert_response :success
134 134 assert_template 'edit'
135 135 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
136 136 :name => 'query[is_public]',
137 137 :checked => 'checked' }
138 138 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
139 139 :name => 'query_is_for_all',
140 140 :checked => 'checked',
141 141 :disabled => 'disabled' }
142 142 end
143 143
144 144 def test_edit_global_public_query
145 145 @request.session[:user_id] = 1
146 146 post :edit,
147 147 :id => 4,
148 148 :confirm => '1',
149 149 :default_columns => '1',
150 150 :fields => ["status_id", "assigned_to_id"],
151 151 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
152 152 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
153 153 :query => {"name" => "test_edit_global_public_query", "is_public" => "1"}
154 154
155 155 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
156 156 q = Query.find_by_name('test_edit_global_public_query')
157 157 assert q.is_public?
158 158 assert q.has_default_columns?
159 159 assert q.valid?
160 160 end
161 161
162 162 def test_get_edit_global_private_query
163 163 @request.session[:user_id] = 3
164 164 get :edit, :id => 3
165 165 assert_response :success
166 166 assert_template 'edit'
167 167 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
168 168 :name => 'query[is_public]' }
169 169 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
170 170 :name => 'query_is_for_all',
171 171 :checked => 'checked',
172 172 :disabled => 'disabled' }
173 173 end
174 174
175 175 def test_edit_global_private_query
176 176 @request.session[:user_id] = 3
177 177 post :edit,
178 178 :id => 3,
179 179 :confirm => '1',
180 180 :default_columns => '1',
181 181 :fields => ["status_id", "assigned_to_id"],
182 182 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
183 183 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
184 184 :query => {"name" => "test_edit_global_private_query", "is_public" => "1"}
185 185
186 186 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
187 187 q = Query.find_by_name('test_edit_global_private_query')
188 188 assert !q.is_public?
189 189 assert q.has_default_columns?
190 190 assert q.valid?
191 191 end
192 192
193 193 def test_get_edit_project_private_query
194 194 @request.session[:user_id] = 3
195 195 get :edit, :id => 2
196 196 assert_response :success
197 197 assert_template 'edit'
198 198 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
199 199 :name => 'query[is_public]' }
200 200 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
201 201 :name => 'query_is_for_all',
202 202 :checked => nil,
203 203 :disabled => nil }
204 204 end
205 205
206 206 def test_get_edit_project_public_query
207 207 @request.session[:user_id] = 2
208 208 get :edit, :id => 1
209 209 assert_response :success
210 210 assert_template 'edit'
211 211 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
212 212 :name => 'query[is_public]',
213 213 :checked => 'checked'
214 214 }
215 215 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
216 216 :name => 'query_is_for_all',
217 217 :checked => nil,
218 218 :disabled => 'disabled' }
219 219 end
220 220
221 221 def test_get_edit_sort_criteria
222 222 @request.session[:user_id] = 1
223 223 get :edit, :id => 5
224 224 assert_response :success
225 225 assert_template 'edit'
226 226 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
227 227 :child => { :tag => 'option', :attributes => { :value => 'priority',
228 228 :selected => 'selected' } }
229 229 assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' },
230 230 :child => { :tag => 'option', :attributes => { :value => 'desc',
231 231 :selected => 'selected' } }
232 232 end
233 233
234 234 def test_destroy
235 235 @request.session[:user_id] = 2
236 236 post :destroy, :id => 1
237 237 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
238 238 assert_nil Query.find_by_id(1)
239 239 end
240 240 end
@@ -1,137 +1,137
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesBazaarControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
25 fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
26 26
27 27 # No '..' in the repository path
28 28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository'
29 29
30 30 def setup
31 31 @controller = RepositoriesController.new
32 32 @request = ActionController::TestRequest.new
33 33 @response = ActionController::TestResponse.new
34 34 User.current = nil
35 35 Repository::Bazaar.create(:project => Project.find(3), :url => REPOSITORY_PATH)
36 36 end
37 37
38 38 if File.directory?(REPOSITORY_PATH)
39 39 def test_show
40 40 get :show, :id => 3
41 41 assert_response :success
42 42 assert_template 'show'
43 43 assert_not_nil assigns(:entries)
44 44 assert_not_nil assigns(:changesets)
45 45 end
46 46
47 47 def test_browse_root
48 48 get :browse, :id => 3
49 49 assert_response :success
50 50 assert_template 'browse'
51 51 assert_not_nil assigns(:entries)
52 52 assert_equal 2, assigns(:entries).size
53 53 assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'}
54 54 assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'}
55 55 end
56 56
57 57 def test_browse_directory
58 58 get :browse, :id => 3, :path => ['directory']
59 59 assert_response :success
60 60 assert_template 'browse'
61 61 assert_not_nil assigns(:entries)
62 62 assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name)
63 63 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
64 64 assert_not_nil entry
65 65 assert_equal 'file', entry.kind
66 66 assert_equal 'directory/edit.png', entry.path
67 67 end
68 68
69 69 def test_browse_at_given_revision
70 70 get :browse, :id => 3, :path => [], :rev => 3
71 71 assert_response :success
72 72 assert_template 'browse'
73 73 assert_not_nil assigns(:entries)
74 74 assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'], assigns(:entries).collect(&:name)
75 75 end
76 76
77 77 def test_changes
78 78 get :changes, :id => 3, :path => ['doc-mkdir.txt']
79 79 assert_response :success
80 80 assert_template 'changes'
81 81 assert_tag :tag => 'h2', :content => 'doc-mkdir.txt'
82 82 end
83 83
84 84 def test_entry_show
85 85 get :entry, :id => 3, :path => ['directory', 'doc-ls.txt']
86 86 assert_response :success
87 87 assert_template 'entry'
88 88 # Line 19
89 89 assert_tag :tag => 'th',
90 90 :content => /29/,
91 91 :attributes => { :class => /line-num/ },
92 92 :sibling => { :tag => 'td', :content => /Show help message/ }
93 93 end
94 94
95 95 def test_entry_download
96 96 get :entry, :id => 3, :path => ['directory', 'doc-ls.txt'], :format => 'raw'
97 97 assert_response :success
98 98 # File content
99 99 assert @response.body.include?('Show help message')
100 100 end
101 101
102 102 def test_directory_entry
103 103 get :entry, :id => 3, :path => ['directory']
104 104 assert_response :success
105 105 assert_template 'browse'
106 106 assert_not_nil assigns(:entry)
107 107 assert_equal 'directory', assigns(:entry).name
108 108 end
109 109
110 110 def test_diff
111 111 # Full diff of changeset 3
112 112 get :diff, :id => 3, :rev => 3
113 113 assert_response :success
114 114 assert_template 'diff'
115 115 # Line 22 removed
116 116 assert_tag :tag => 'th',
117 117 :content => /2/,
118 118 :sibling => { :tag => 'td',
119 119 :attributes => { :class => /diff_in/ },
120 120 :content => /Main purpose/ }
121 121 end
122 122
123 123 def test_annotate
124 124 get :annotate, :id => 3, :path => ['doc-mkdir.txt']
125 125 assert_response :success
126 126 assert_template 'annotate'
127 127 # Line 2, revision 3
128 128 assert_tag :tag => 'th', :content => /2/,
129 129 :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /3/ } },
130 130 :sibling => { :tag => 'td', :content => /jsmith/ },
131 131 :sibling => { :tag => 'td', :content => /Main purpose/ }
132 132 end
133 133 else
134 134 puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!"
135 135 def test_fake; assert true end
136 136 end
137 137 end
@@ -1,207 +1,207
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
25 fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
26 26
27 27 def setup
28 28 @controller = RepositoriesController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_show_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/projects/redmine/repository'},
37 37 :controller => 'repositories', :action => 'show', :id => 'redmine'
38 38 )
39 39 end
40 40
41 41 def test_edit_routing
42 42 assert_routing(
43 43 {:method => :get, :path => '/projects/world_domination/repository/edit'},
44 44 :controller => 'repositories', :action => 'edit', :id => 'world_domination'
45 45 )
46 46 assert_routing(
47 47 {:method => :post, :path => '/projects/world_domination/repository/edit'},
48 48 :controller => 'repositories', :action => 'edit', :id => 'world_domination'
49 49 )
50 50 end
51 51
52 52 def test_revisions_routing
53 53 assert_routing(
54 54 {:method => :get, :path => '/projects/redmine/repository/revisions'},
55 55 :controller => 'repositories', :action => 'revisions', :id => 'redmine'
56 56 )
57 57 end
58 58
59 59 def test_revisions_atom_routing
60 60 assert_routing(
61 61 {:method => :get, :path => '/projects/redmine/repository/revisions.atom'},
62 62 :controller => 'repositories', :action => 'revisions', :id => 'redmine', :format => 'atom'
63 63 )
64 64 end
65 65
66 66 def test_revisions
67 67 get :revisions, :id => 1
68 68 assert_response :success
69 69 assert_template 'revisions'
70 70 assert_not_nil assigns(:changesets)
71 71 end
72 72
73 73 def test_revision_routing
74 74 assert_routing(
75 75 {:method => :get, :path => '/projects/restmine/repository/revisions/2457'},
76 76 :controller => 'repositories', :action => 'revision', :id => 'restmine', :rev => '2457'
77 77 )
78 78 end
79 79
80 80 def test_revision_with_before_nil_and_afer_normal
81 81 get :revision, {:id => 1, :rev => 1}
82 82 assert_response :success
83 83 assert_template 'revision'
84 84 assert_no_tag :tag => "div", :attributes => { :class => "contextual" },
85 85 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/0'}
86 86 }
87 87 assert_tag :tag => "div", :attributes => { :class => "contextual" },
88 88 :child => { :tag => "a", :attributes => { :href => '/projects/ecookbook/repository/revisions/2'}
89 89 }
90 90 end
91 91
92 92 def test_diff_routing
93 93 assert_routing(
94 94 {:method => :get, :path => '/projects/restmine/repository/revisions/2457/diff'},
95 95 :controller => 'repositories', :action => 'diff', :id => 'restmine', :rev => '2457'
96 96 )
97 97 end
98 98
99 99 def test_unified_diff_routing
100 100 assert_routing(
101 101 {:method => :get, :path => '/projects/restmine/repository/revisions/2457/diff.diff'},
102 102 :controller => 'repositories', :action => 'diff', :id => 'restmine', :rev => '2457', :format => 'diff'
103 103 )
104 104 end
105 105
106 106 def test_diff_path_routing
107 107 assert_routing(
108 108 {:method => :get, :path => '/projects/restmine/repository/diff/path/to/file.c'},
109 109 :controller => 'repositories', :action => 'diff', :id => 'restmine', :path => %w[path to file.c]
110 110 )
111 111 end
112 112
113 113 def test_diff_path_routing_with_revision
114 114 assert_routing(
115 115 {:method => :get, :path => '/projects/restmine/repository/revisions/2/diff/path/to/file.c'},
116 116 :controller => 'repositories', :action => 'diff', :id => 'restmine', :path => %w[path to file.c], :rev => '2'
117 117 )
118 118 end
119 119
120 120 def test_browse_routing
121 121 assert_routing(
122 122 {:method => :get, :path => '/projects/restmine/repository/browse/path/to/dir'},
123 123 :controller => 'repositories', :action => 'browse', :id => 'restmine', :path => %w[path to dir]
124 124 )
125 125 end
126 126
127 127 def test_entry_routing
128 128 assert_routing(
129 129 {:method => :get, :path => '/projects/restmine/repository/entry/path/to/file.c'},
130 130 :controller => 'repositories', :action => 'entry', :id => 'restmine', :path => %w[path to file.c]
131 131 )
132 132 end
133 133
134 134 def test_entry_routing_with_revision
135 135 assert_routing(
136 136 {:method => :get, :path => '/projects/restmine/repository/revisions/2/entry/path/to/file.c'},
137 137 :controller => 'repositories', :action => 'entry', :id => 'restmine', :path => %w[path to file.c], :rev => '2'
138 138 )
139 139 end
140 140
141 141 def test_annotate_routing
142 142 assert_routing(
143 143 {:method => :get, :path => '/projects/restmine/repository/annotate/path/to/file.c'},
144 144 :controller => 'repositories', :action => 'annotate', :id => 'restmine', :path => %w[path to file.c]
145 145 )
146 146 end
147 147
148 148 def test_changesrouting
149 149 assert_routing(
150 150 {:method => :get, :path => '/projects/restmine/repository/changes/path/to/file.c'},
151 151 :controller => 'repositories', :action => 'changes', :id => 'restmine', :path => %w[path to file.c]
152 152 )
153 153 end
154 154
155 155 def test_statistics_routing
156 156 assert_routing(
157 157 {:method => :get, :path => '/projects/restmine/repository/statistics'},
158 158 :controller => 'repositories', :action => 'stats', :id => 'restmine'
159 159 )
160 160 end
161 161
162 162 def test_graph_commits_per_month
163 163 get :graph, :id => 1, :graph => 'commits_per_month'
164 164 assert_response :success
165 165 assert_equal 'image/svg+xml', @response.content_type
166 166 end
167 167
168 168 def test_graph_commits_per_author
169 169 get :graph, :id => 1, :graph => 'commits_per_author'
170 170 assert_response :success
171 171 assert_equal 'image/svg+xml', @response.content_type
172 172 end
173 173
174 174 def test_committers
175 175 @request.session[:user_id] = 2
176 176 # add a commit with an unknown user
177 177 Changeset.create!(:repository => Project.find(1).repository, :committer => 'foo', :committed_on => Time.now, :revision => 100, :comments => 'Committed by foo.')
178 178
179 179 get :committers, :id => 1
180 180 assert_response :success
181 181 assert_template 'committers'
182 182
183 183 assert_tag :td, :content => 'dlopper',
184 184 :sibling => { :tag => 'td',
185 185 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} },
186 186 :child => { :tag => 'option', :content => 'Dave Lopper',
187 187 :attributes => { :value => '3', :selected => 'selected' }}}}
188 188 assert_tag :td, :content => 'foo',
189 189 :sibling => { :tag => 'td',
190 190 :child => { :tag => 'select', :attributes => { :name => %r{^committers\[\d+\]\[\]$} }}}
191 191 assert_no_tag :td, :content => 'foo',
192 192 :sibling => { :tag => 'td',
193 193 :descendant => { :tag => 'option', :attributes => { :selected => 'selected' }}}
194 194 end
195 195
196 196 def test_map_committers
197 197 @request.session[:user_id] = 2
198 198 # add a commit with an unknown user
199 199 c = Changeset.create!(:repository => Project.find(1).repository, :committer => 'foo', :committed_on => Time.now, :revision => 100, :comments => 'Committed by foo.')
200 200
201 201 assert_no_difference "Changeset.count(:conditions => 'user_id = 3')" do
202 202 post :committers, :id => 1, :committers => { '0' => ['foo', '2'], '1' => ['dlopper', '3']}
203 203 assert_redirected_to 'projects/ecookbook/repository/committers'
204 204 assert_equal User.find(2), c.reload.user
205 205 end
206 206 end
207 207 end
@@ -1,103 +1,103
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesDarcsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
25 fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
26 26
27 27 # No '..' in the repository path
28 28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
29 29
30 30 def setup
31 31 @controller = RepositoriesController.new
32 32 @request = ActionController::TestRequest.new
33 33 @response = ActionController::TestResponse.new
34 34 User.current = nil
35 35 Repository::Darcs.create(:project => Project.find(3), :url => REPOSITORY_PATH)
36 36 end
37 37
38 38 if File.directory?(REPOSITORY_PATH)
39 39 def test_show
40 40 get :show, :id => 3
41 41 assert_response :success
42 42 assert_template 'show'
43 43 assert_not_nil assigns(:entries)
44 44 assert_not_nil assigns(:changesets)
45 45 end
46 46
47 47 def test_browse_root
48 48 get :browse, :id => 3
49 49 assert_response :success
50 50 assert_template 'browse'
51 51 assert_not_nil assigns(:entries)
52 52 assert_equal 3, assigns(:entries).size
53 53 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
54 54 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
55 55 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
56 56 end
57 57
58 58 def test_browse_directory
59 59 get :browse, :id => 3, :path => ['images']
60 60 assert_response :success
61 61 assert_template 'browse'
62 62 assert_not_nil assigns(:entries)
63 63 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
64 64 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
65 65 assert_not_nil entry
66 66 assert_equal 'file', entry.kind
67 67 assert_equal 'images/edit.png', entry.path
68 68 end
69 69
70 70 def test_browse_at_given_revision
71 71 Project.find(3).repository.fetch_changesets
72 72 get :browse, :id => 3, :path => ['images'], :rev => 1
73 73 assert_response :success
74 74 assert_template 'browse'
75 75 assert_not_nil assigns(:entries)
76 76 assert_equal ['delete.png'], assigns(:entries).collect(&:name)
77 77 end
78 78
79 79 def test_changes
80 80 get :changes, :id => 3, :path => ['images', 'edit.png']
81 81 assert_response :success
82 82 assert_template 'changes'
83 83 assert_tag :tag => 'h2', :content => 'edit.png'
84 84 end
85 85
86 86 def test_diff
87 87 Project.find(3).repository.fetch_changesets
88 88 # Full diff of changeset 5
89 89 get :diff, :id => 3, :rev => 5
90 90 assert_response :success
91 91 assert_template 'diff'
92 92 # Line 22 removed
93 93 assert_tag :tag => 'th',
94 94 :content => /22/,
95 95 :sibling => { :tag => 'td',
96 96 :attributes => { :class => /diff_out/ },
97 97 :content => /def remove/ }
98 98 end
99 99 else
100 100 puts "Darcs test repository NOT FOUND. Skipping functional tests !!!"
101 101 def test_fake; assert true end
102 102 end
103 103 end
@@ -1,146 +1,146
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesGitControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
25 fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
26 26
27 27 # No '..' in the repository path
28 28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
29 29 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
30 30
31 31 def setup
32 32 @controller = RepositoriesController.new
33 33 @request = ActionController::TestRequest.new
34 34 @response = ActionController::TestResponse.new
35 35 User.current = nil
36 36 Repository::Git.create(:project => Project.find(3), :url => REPOSITORY_PATH)
37 37 end
38 38
39 39 if File.directory?(REPOSITORY_PATH)
40 40 def test_show
41 41 get :show, :id => 3
42 42 assert_response :success
43 43 assert_template 'show'
44 44 assert_not_nil assigns(:entries)
45 45 assert_not_nil assigns(:changesets)
46 46 end
47 47
48 48 def test_browse_root
49 49 get :browse, :id => 3
50 50 assert_response :success
51 51 assert_template 'browse'
52 52 assert_not_nil assigns(:entries)
53 53 assert_equal 3, assigns(:entries).size
54 54 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
55 55 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
56 56 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
57 57 end
58 58
59 59 def test_browse_directory
60 60 get :browse, :id => 3, :path => ['images']
61 61 assert_response :success
62 62 assert_template 'browse'
63 63 assert_not_nil assigns(:entries)
64 64 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
65 65 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
66 66 assert_not_nil entry
67 67 assert_equal 'file', entry.kind
68 68 assert_equal 'images/edit.png', entry.path
69 69 end
70 70
71 71 def test_browse_at_given_revision
72 72 get :browse, :id => 3, :path => ['images'], :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518'
73 73 assert_response :success
74 74 assert_template 'browse'
75 75 assert_not_nil assigns(:entries)
76 76 assert_equal ['delete.png'], assigns(:entries).collect(&:name)
77 77 end
78 78
79 79 def test_changes
80 80 get :changes, :id => 3, :path => ['images', 'edit.png']
81 81 assert_response :success
82 82 assert_template 'changes'
83 83 assert_tag :tag => 'h2', :content => 'edit.png'
84 84 end
85 85
86 86 def test_entry_show
87 87 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb']
88 88 assert_response :success
89 89 assert_template 'entry'
90 90 # Line 19
91 91 assert_tag :tag => 'th',
92 92 :content => /10/,
93 93 :attributes => { :class => /line-num/ },
94 94 :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
95 95 end
96 96
97 97 def test_entry_download
98 98 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
99 99 assert_response :success
100 100 # File content
101 101 assert @response.body.include?('WITHOUT ANY WARRANTY')
102 102 end
103 103
104 104 def test_directory_entry
105 105 get :entry, :id => 3, :path => ['sources']
106 106 assert_response :success
107 107 assert_template 'browse'
108 108 assert_not_nil assigns(:entry)
109 109 assert_equal 'sources', assigns(:entry).name
110 110 end
111 111
112 112 def test_diff
113 113 # Full diff of changeset 2f9c0091
114 114 get :diff, :id => 3, :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
115 115 assert_response :success
116 116 assert_template 'diff'
117 117 # Line 22 removed
118 118 assert_tag :tag => 'th',
119 119 :content => /22/,
120 120 :sibling => { :tag => 'td',
121 121 :attributes => { :class => /diff_out/ },
122 122 :content => /def remove/ }
123 123 end
124 124
125 125 def test_annotate
126 126 get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
127 127 assert_response :success
128 128 assert_template 'annotate'
129 129 # Line 23, changeset 2f9c0091
130 130 assert_tag :tag => 'th', :content => /23/,
131 131 :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /2f9c0091/ } },
132 132 :sibling => { :tag => 'td', :content => /jsmith/ },
133 133 :sibling => { :tag => 'td', :content => /watcher =/ }
134 134 end
135 135
136 136 def test_annotate_binary_file
137 137 get :annotate, :id => 3, :path => ['images', 'delete.png']
138 138 assert_response 500
139 139 assert_tag :tag => 'div', :attributes => { :class => /error/ },
140 140 :content => /can not be annotated/
141 141 end
142 142 else
143 143 puts "Git test repository NOT FOUND. Skipping functional tests !!!"
144 144 def test_fake; assert true end
145 145 end
146 146 end
@@ -1,138 +1,138
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesMercurialControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
25 fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
26 26
27 27 # No '..' in the repository path
28 28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
29 29
30 30 def setup
31 31 @controller = RepositoriesController.new
32 32 @request = ActionController::TestRequest.new
33 33 @response = ActionController::TestResponse.new
34 34 User.current = nil
35 35 Repository::Mercurial.create(:project => Project.find(3), :url => REPOSITORY_PATH)
36 36 end
37 37
38 38 if File.directory?(REPOSITORY_PATH)
39 39 def test_show
40 40 get :show, :id => 3
41 41 assert_response :success
42 42 assert_template 'show'
43 43 assert_not_nil assigns(:entries)
44 44 assert_not_nil assigns(:changesets)
45 45 end
46 46
47 47 def test_browse_root
48 48 get :browse, :id => 3
49 49 assert_response :success
50 50 assert_template 'browse'
51 51 assert_not_nil assigns(:entries)
52 52 assert_equal 3, assigns(:entries).size
53 53 assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
54 54 assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
55 55 assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
56 56 end
57 57
58 58 def test_browse_directory
59 59 get :browse, :id => 3, :path => ['images']
60 60 assert_response :success
61 61 assert_template 'browse'
62 62 assert_not_nil assigns(:entries)
63 63 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
64 64 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
65 65 assert_not_nil entry
66 66 assert_equal 'file', entry.kind
67 67 assert_equal 'images/edit.png', entry.path
68 68 end
69 69
70 70 def test_browse_at_given_revision
71 71 get :browse, :id => 3, :path => ['images'], :rev => 0
72 72 assert_response :success
73 73 assert_template 'browse'
74 74 assert_not_nil assigns(:entries)
75 75 assert_equal ['delete.png'], assigns(:entries).collect(&:name)
76 76 end
77 77
78 78 def test_changes
79 79 get :changes, :id => 3, :path => ['images', 'edit.png']
80 80 assert_response :success
81 81 assert_template 'changes'
82 82 assert_tag :tag => 'h2', :content => 'edit.png'
83 83 end
84 84
85 85 def test_entry_show
86 86 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb']
87 87 assert_response :success
88 88 assert_template 'entry'
89 89 # Line 19
90 90 assert_tag :tag => 'th',
91 91 :content => /10/,
92 92 :attributes => { :class => /line-num/ },
93 93 :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
94 94 end
95 95
96 96 def test_entry_download
97 97 get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
98 98 assert_response :success
99 99 # File content
100 100 assert @response.body.include?('WITHOUT ANY WARRANTY')
101 101 end
102 102
103 103 def test_directory_entry
104 104 get :entry, :id => 3, :path => ['sources']
105 105 assert_response :success
106 106 assert_template 'browse'
107 107 assert_not_nil assigns(:entry)
108 108 assert_equal 'sources', assigns(:entry).name
109 109 end
110 110
111 111 def test_diff
112 112 # Full diff of changeset 4
113 113 get :diff, :id => 3, :rev => 4
114 114 assert_response :success
115 115 assert_template 'diff'
116 116 # Line 22 removed
117 117 assert_tag :tag => 'th',
118 118 :content => /22/,
119 119 :sibling => { :tag => 'td',
120 120 :attributes => { :class => /diff_out/ },
121 121 :content => /def remove/ }
122 122 end
123 123
124 124 def test_annotate
125 125 get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
126 126 assert_response :success
127 127 assert_template 'annotate'
128 128 # Line 23, revision 4
129 129 assert_tag :tag => 'th', :content => /23/,
130 130 :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /4/ } },
131 131 :sibling => { :tag => 'td', :content => /jsmith/ },
132 132 :sibling => { :tag => 'td', :content => /watcher =/ }
133 133 end
134 134 else
135 135 puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!"
136 136 def test_fake; assert true end
137 137 end
138 138 end
@@ -1,194 +1,194
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesSubversionControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :enabled_modules,
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
26 26 :repositories, :issues, :issue_statuses, :changesets, :changes,
27 27 :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
28 28
29 29 # No '..' in the repository path for svn
30 30 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/subversion_repository'
31 31
32 32 def setup
33 33 @controller = RepositoriesController.new
34 34 @request = ActionController::TestRequest.new
35 35 @response = ActionController::TestResponse.new
36 36 Setting.default_language = 'en'
37 37 User.current = nil
38 38 end
39 39
40 40 if File.directory?(REPOSITORY_PATH)
41 41 def test_show
42 42 get :show, :id => 1
43 43 assert_response :success
44 44 assert_template 'show'
45 45 assert_not_nil assigns(:entries)
46 46 assert_not_nil assigns(:changesets)
47 47 end
48 48
49 49 def test_browse_root
50 50 get :browse, :id => 1
51 51 assert_response :success
52 52 assert_template 'browse'
53 53 assert_not_nil assigns(:entries)
54 54 entry = assigns(:entries).detect {|e| e.name == 'subversion_test'}
55 55 assert_equal 'dir', entry.kind
56 56 end
57 57
58 58 def test_browse_directory
59 59 get :browse, :id => 1, :path => ['subversion_test']
60 60 assert_response :success
61 61 assert_template 'browse'
62 62 assert_not_nil assigns(:entries)
63 63 assert_equal ['folder', '.project', 'helloworld.c', 'textfile.txt'], assigns(:entries).collect(&:name)
64 64 entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'}
65 65 assert_equal 'file', entry.kind
66 66 assert_equal 'subversion_test/helloworld.c', entry.path
67 67 assert_tag :a, :content => 'helloworld.c', :attributes => { :class => /text\-x\-c/ }
68 68 end
69 69
70 70 def test_browse_at_given_revision
71 71 get :browse, :id => 1, :path => ['subversion_test'], :rev => 4
72 72 assert_response :success
73 73 assert_template 'browse'
74 74 assert_not_nil assigns(:entries)
75 75 assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name)
76 76 end
77 77
78 78 def test_changes
79 79 get :changes, :id => 1, :path => ['subversion_test', 'folder', 'helloworld.rb' ]
80 80 assert_response :success
81 81 assert_template 'changes'
82 82 # svn properties displayed with svn >= 1.5 only
83 83 if Redmine::Scm::Adapters::SubversionAdapter.client_version_above?([1, 5, 0])
84 84 assert_not_nil assigns(:properties)
85 85 assert_equal 'native', assigns(:properties)['svn:eol-style']
86 86 assert_tag :ul,
87 87 :child => { :tag => 'li',
88 88 :child => { :tag => 'b', :content => 'svn:eol-style' },
89 89 :child => { :tag => 'span', :content => 'native' } }
90 90 end
91 91 end
92 92
93 93 def test_entry
94 94 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
95 95 assert_response :success
96 96 assert_template 'entry'
97 97 end
98 98
99 99 def test_entry_should_send_if_too_big
100 100 # no files in the test repo is larger than 1KB...
101 101 with_settings :file_max_size_displayed => 0 do
102 102 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
103 103 assert_response :success
104 104 assert_template ''
105 105 assert_equal 'attachment; filename="helloworld.c"', @response.headers['Content-Disposition']
106 106 end
107 107 end
108 108
109 109 def test_entry_at_given_revision
110 110 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.rb'], :rev => 2
111 111 assert_response :success
112 112 assert_template 'entry'
113 113 # this line was removed in r3 and file was moved in r6
114 114 assert_tag :tag => 'td', :attributes => { :class => /line-code/},
115 115 :content => /Here's the code/
116 116 end
117 117
118 118 def test_entry_not_found
119 119 get :entry, :id => 1, :path => ['subversion_test', 'zzz.c']
120 120 assert_tag :tag => 'div', :attributes => { :class => /error/ },
121 121 :content => /The entry or revision was not found in the repository/
122 122 end
123 123
124 124 def test_entry_download
125 125 get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'], :format => 'raw'
126 126 assert_response :success
127 127 assert_template ''
128 128 assert_equal 'attachment; filename="helloworld.c"', @response.headers['Content-Disposition']
129 129 end
130 130
131 131 def test_directory_entry
132 132 get :entry, :id => 1, :path => ['subversion_test', 'folder']
133 133 assert_response :success
134 134 assert_template 'browse'
135 135 assert_not_nil assigns(:entry)
136 136 assert_equal 'folder', assigns(:entry).name
137 137 end
138 138
139 139 def test_revision
140 140 get :revision, :id => 1, :rev => 2
141 141 assert_response :success
142 142 assert_template 'revision'
143 143 assert_tag :tag => 'ul',
144 144 :child => { :tag => 'li',
145 145 # link to the entry at rev 2
146 146 :child => { :tag => 'a',
147 147 :attributes => {:href => '/projects/ecookbook/repository/revisions/2/entry/test/some/path/in/the/repo'},
148 148 :content => 'repo',
149 149 # link to partial diff
150 150 :sibling => { :tag => 'a',
151 151 :attributes => { :href => '/projects/ecookbook/repository/revisions/2/diff/test/some/path/in/the/repo' }
152 152 }
153 153 }
154 154 }
155 155 end
156 156
157 157 def test_revision_with_repository_pointing_to_a_subdirectory
158 158 r = Project.find(1).repository
159 159 # Changes repository url to a subdirectory
160 160 r.update_attribute :url, (r.url + '/test/some')
161 161
162 162 get :revision, :id => 1, :rev => 2
163 163 assert_response :success
164 164 assert_template 'revision'
165 165 assert_tag :tag => 'ul',
166 166 :child => { :tag => 'li',
167 167 # link to the entry at rev 2
168 168 :child => { :tag => 'a',
169 169 :attributes => {:href => '/projects/ecookbook/repository/revisions/2/entry/path/in/the/repo'},
170 170 :content => 'repo',
171 171 # link to partial diff
172 172 :sibling => { :tag => 'a',
173 173 :attributes => { :href => '/projects/ecookbook/repository/revisions/2/diff/path/in/the/repo' }
174 174 }
175 175 }
176 176 }
177 177 end
178 178
179 179 def test_diff
180 180 get :diff, :id => 1, :rev => 3
181 181 assert_response :success
182 182 assert_template 'diff'
183 183 end
184 184
185 185 def test_annotate
186 186 get :annotate, :id => 1, :path => ['subversion_test', 'helloworld.c']
187 187 assert_response :success
188 188 assert_template 'annotate'
189 189 end
190 190 else
191 191 puts "Subversion test repository NOT FOUND. Skipping functional tests !!!"
192 192 def test_fake; assert true end
193 193 end
194 194 end
@@ -1,180 +1,180
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'roles_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RolesController; def rescue_action(e) raise e end; end
23 23
24 24 class RolesControllerTest < Test::Unit::TestCase
25 fixtures :roles, :users, :members, :workflows
25 fixtures :roles, :users, :members, :member_roles, :workflows
26 26
27 27 def setup
28 28 @controller = RolesController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 @request.session[:user_id] = 1 # admin
33 33 end
34 34
35 35 def test_get_index
36 36 get :index
37 37 assert_response :success
38 38 assert_template 'list'
39 39
40 40 assert_not_nil assigns(:roles)
41 41 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
42 42
43 43 assert_tag :tag => 'a', :attributes => { :href => '/roles/edit/1' },
44 44 :content => 'Manager'
45 45 end
46 46
47 47 def test_get_new
48 48 get :new
49 49 assert_response :success
50 50 assert_template 'new'
51 51 end
52 52
53 53 def test_post_new_with_validaton_failure
54 54 post :new, :role => {:name => '',
55 55 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
56 56 :assignable => '0'}
57 57
58 58 assert_response :success
59 59 assert_template 'new'
60 60 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' }
61 61 end
62 62
63 63 def test_post_new_without_workflow_copy
64 64 post :new, :role => {:name => 'RoleWithoutWorkflowCopy',
65 65 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
66 66 :assignable => '0'}
67 67
68 assert_redirected_to 'roles/list'
68 assert_redirected_to 'roles'
69 69 role = Role.find_by_name('RoleWithoutWorkflowCopy')
70 70 assert_not_nil role
71 71 assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
72 72 assert !role.assignable?
73 73 end
74 74
75 75 def test_post_new_with_workflow_copy
76 76 post :new, :role => {:name => 'RoleWithWorkflowCopy',
77 77 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
78 78 :assignable => '0'},
79 79 :copy_workflow_from => '1'
80 80
81 assert_redirected_to 'roles/list'
81 assert_redirected_to 'roles'
82 82 role = Role.find_by_name('RoleWithWorkflowCopy')
83 83 assert_not_nil role
84 84 assert_equal Role.find(1).workflows.size, role.workflows.size
85 85 end
86 86
87 87 def test_get_edit
88 88 get :edit, :id => 1
89 89 assert_response :success
90 90 assert_template 'edit'
91 91 assert_equal Role.find(1), assigns(:role)
92 92 end
93 93
94 94 def test_post_edit
95 95 post :edit, :id => 1,
96 96 :role => {:name => 'Manager',
97 97 :permissions => ['edit_project', ''],
98 98 :assignable => '0'}
99 99
100 assert_redirected_to 'roles/list'
100 assert_redirected_to 'roles'
101 101 role = Role.find(1)
102 102 assert_equal [:edit_project], role.permissions
103 103 end
104 104
105 105 def test_destroy
106 106 r = Role.new(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
107 107 assert r.save
108 108
109 109 post :destroy, :id => r
110 assert_redirected_to 'roles/list'
110 assert_redirected_to 'roles'
111 111 assert_nil Role.find_by_id(r.id)
112 112 end
113 113
114 114 def test_destroy_role_in_use
115 115 post :destroy, :id => 1
116 116 assert_redirected_to 'roles'
117 117 assert flash[:error] == 'This role is in use and can not be deleted.'
118 118 assert_not_nil Role.find_by_id(1)
119 119 end
120 120
121 121 def test_get_report
122 122 get :report
123 123 assert_response :success
124 124 assert_template 'report'
125 125
126 126 assert_not_nil assigns(:roles)
127 127 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
128 128
129 129 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
130 130 :name => 'permissions[3][]',
131 131 :value => 'add_issues',
132 132 :checked => 'checked' }
133 133
134 134 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
135 135 :name => 'permissions[3][]',
136 136 :value => 'delete_issues',
137 137 :checked => nil }
138 138 end
139 139
140 140 def test_post_report
141 141 post :report, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
142 assert_redirected_to 'roles/list'
142 assert_redirected_to 'roles'
143 143
144 144 assert_equal [:edit_issues], Role.find(1).permissions
145 145 assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
146 146 assert Role.find(2).permissions.empty?
147 147 end
148 148
149 149 def test_clear_all_permissions
150 150 post :report, :permissions => { '0' => '' }
151 assert_redirected_to 'roles/list'
151 assert_redirected_to 'roles'
152 152 assert Role.find(1).permissions.empty?
153 153 end
154 154
155 155 def test_move_highest
156 156 post :edit, :id => 3, :role => {:move_to => 'highest'}
157 assert_redirected_to 'roles/list'
157 assert_redirected_to 'roles'
158 158 assert_equal 1, Role.find(3).position
159 159 end
160 160
161 161 def test_move_higher
162 162 position = Role.find(3).position
163 163 post :edit, :id => 3, :role => {:move_to => 'higher'}
164 assert_redirected_to 'roles/list'
164 assert_redirected_to 'roles'
165 165 assert_equal position - 1, Role.find(3).position
166 166 end
167 167
168 168 def test_move_lower
169 169 position = Role.find(2).position
170 170 post :edit, :id => 2, :role => {:move_to => 'lower'}
171 assert_redirected_to 'roles/list'
171 assert_redirected_to 'roles'
172 172 assert_equal position + 1, Role.find(2).position
173 173 end
174 174
175 175 def test_move_lowest
176 176 post :edit, :id => 2, :role => {:move_to => 'lowest'}
177 assert_redirected_to 'roles/list'
177 assert_redirected_to 'roles'
178 178 assert_equal Role.count, Role.find(2).position
179 179 end
180 180 end
@@ -1,141 +1,141
1 1 require File.dirname(__FILE__) + '/../test_helper'
2 2 require 'search_controller'
3 3
4 4 # Re-raise errors caught by the controller.
5 5 class SearchController; def rescue_action(e) raise e end; end
6 6
7 7 class SearchControllerTest < Test::Unit::TestCase
8 fixtures :projects, :enabled_modules, :roles, :users,
8 fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles,
9 9 :issues, :trackers, :issue_statuses,
10 10 :custom_fields, :custom_values,
11 11 :repositories, :changesets
12 12
13 13 def setup
14 14 @controller = SearchController.new
15 15 @request = ActionController::TestRequest.new
16 16 @response = ActionController::TestResponse.new
17 17 User.current = nil
18 18 end
19 19
20 20 def test_search_for_projects
21 21 get :index
22 22 assert_response :success
23 23 assert_template 'index'
24 24
25 25 get :index, :q => "cook"
26 26 assert_response :success
27 27 assert_template 'index'
28 28 assert assigns(:results).include?(Project.find(1))
29 29 end
30 30
31 31 def test_search_all_projects
32 32 get :index, :q => 'recipe subproject commit', :submit => 'Search'
33 33 assert_response :success
34 34 assert_template 'index'
35 35
36 36 assert assigns(:results).include?(Issue.find(2))
37 37 assert assigns(:results).include?(Issue.find(5))
38 38 assert assigns(:results).include?(Changeset.find(101))
39 39 assert_tag :dt, :attributes => { :class => /issue/ },
40 40 :child => { :tag => 'a', :content => /Add ingredients categories/ },
41 41 :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
42 42
43 43 assert assigns(:results_by_type).is_a?(Hash)
44 44 assert_equal 4, assigns(:results_by_type)['changesets']
45 45 assert_tag :a, :content => 'Changesets (4)'
46 46 end
47 47
48 48 def test_search_issues
49 49 get :index, :q => 'issue', :issues => 1
50 50 assert_response :success
51 51 assert_template 'index'
52 52
53 53 assert assigns(:results).include?(Issue.find(8))
54 54 assert assigns(:results).include?(Issue.find(5))
55 55 assert_tag :dt, :attributes => { :class => /issue closed/ },
56 56 :child => { :tag => 'a', :content => /Closed/ }
57 57 end
58 58
59 59 def test_search_project_and_subprojects
60 60 get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :submit => 'Search'
61 61 assert_response :success
62 62 assert_template 'index'
63 63 assert assigns(:results).include?(Issue.find(1))
64 64 assert assigns(:results).include?(Issue.find(5))
65 65 end
66 66
67 67 def test_search_without_searchable_custom_fields
68 68 CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
69 69
70 70 get :index, :id => 1
71 71 assert_response :success
72 72 assert_template 'index'
73 73 assert_not_nil assigns(:project)
74 74
75 75 get :index, :id => 1, :q => "can"
76 76 assert_response :success
77 77 assert_template 'index'
78 78 end
79 79
80 80 def test_search_with_searchable_custom_fields
81 81 get :index, :id => 1, :q => "stringforcustomfield"
82 82 assert_response :success
83 83 results = assigns(:results)
84 84 assert_not_nil results
85 85 assert_equal 1, results.size
86 86 assert results.include?(Issue.find(3))
87 87 end
88 88
89 89 def test_search_all_words
90 90 # 'all words' is on by default
91 91 get :index, :id => 1, :q => 'recipe updating saving'
92 92 results = assigns(:results)
93 93 assert_not_nil results
94 94 assert_equal 1, results.size
95 95 assert results.include?(Issue.find(3))
96 96 end
97 97
98 98 def test_search_one_of_the_words
99 99 get :index, :id => 1, :q => 'recipe updating saving', :submit => 'Search'
100 100 results = assigns(:results)
101 101 assert_not_nil results
102 102 assert_equal 3, results.size
103 103 assert results.include?(Issue.find(3))
104 104 end
105 105
106 106 def test_search_titles_only_without_result
107 107 get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1', :titles_only => '1', :submit => 'Search'
108 108 results = assigns(:results)
109 109 assert_not_nil results
110 110 assert_equal 0, results.size
111 111 end
112 112
113 113 def test_search_titles_only
114 114 get :index, :id => 1, :q => 'recipe', :titles_only => '1', :submit => 'Search'
115 115 results = assigns(:results)
116 116 assert_not_nil results
117 117 assert_equal 2, results.size
118 118 end
119 119
120 120 def test_search_with_invalid_project_id
121 121 get :index, :id => 195, :q => 'recipe'
122 122 assert_response 404
123 123 assert_nil assigns(:results)
124 124 end
125 125
126 126 def test_quick_jump_to_issue
127 127 # issue of a public project
128 128 get :index, :q => "3"
129 129 assert_redirected_to 'issues/3'
130 130
131 131 # issue of a private project
132 132 get :index, :q => "4"
133 133 assert_response :success
134 134 assert_template 'index'
135 135 end
136 136
137 137 def test_tokens_with_quotes
138 138 get :index, :id => 1, :q => '"good bye" hello "bye bye"'
139 139 assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens)
140 140 end
141 141 end
@@ -1,396 +1,396
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'timelog_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class TimelogController; def rescue_action(e) raise e end; end
23 23
24 24 class TimelogControllerTest < Test::Unit::TestCase
25 fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
25 fixtures :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
26 26
27 27 def setup
28 28 @controller = TimelogController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 end
32 32
33 33 def test_edit_routing
34 34 assert_routing(
35 35 {:method => :get, :path => '/issues/567/time_entries/new'},
36 36 :controller => 'timelog', :action => 'edit', :issue_id => '567'
37 37 )
38 38 assert_routing(
39 39 {:method => :get, :path => '/projects/ecookbook/time_entries/new'},
40 40 :controller => 'timelog', :action => 'edit', :project_id => 'ecookbook'
41 41 )
42 42 assert_routing(
43 43 {:method => :get, :path => '/projects/ecookbook/issues/567/time_entries/new'},
44 44 :controller => 'timelog', :action => 'edit', :project_id => 'ecookbook', :issue_id => '567'
45 45 )
46 46
47 47 #TODO: change new form to POST to issue_time_entries_path instead of to edit action
48 48 #TODO: change edit form to PUT to time_entry_path
49 49 assert_routing(
50 50 {:method => :get, :path => '/time_entries/22/edit'},
51 51 :controller => 'timelog', :action => 'edit', :id => '22'
52 52 )
53 53 end
54 54
55 55 def test_get_edit
56 56 @request.session[:user_id] = 3
57 57 get :edit, :project_id => 1
58 58 assert_response :success
59 59 assert_template 'edit'
60 60 # Default activity selected
61 61 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
62 62 :content => 'Development'
63 63 end
64 64
65 65 def test_get_edit_existing_time
66 66 @request.session[:user_id] = 2
67 67 get :edit, :id => 2, :project_id => nil
68 68 assert_response :success
69 69 assert_template 'edit'
70 70 # Default activity selected
71 71 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/timelog/edit/2' }
72 72 end
73 73
74 74 def test_post_edit
75 75 # TODO: should POST to issues’ time log instead of project. change form
76 76 # and routing
77 77 @request.session[:user_id] = 3
78 78 post :edit, :project_id => 1,
79 79 :time_entry => {:comments => 'Some work on TimelogControllerTest',
80 80 # Not the default activity
81 81 :activity_id => '11',
82 82 :spent_on => '2008-03-14',
83 83 :issue_id => '1',
84 84 :hours => '7.3'}
85 85 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
86 86
87 87 i = Issue.find(1)
88 88 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
89 89 assert_not_nil t
90 90 assert_equal 11, t.activity_id
91 91 assert_equal 7.3, t.hours
92 92 assert_equal 3, t.user_id
93 93 assert_equal i, t.issue
94 94 assert_equal i.project, t.project
95 95 end
96 96
97 97 def test_update
98 98 entry = TimeEntry.find(1)
99 99 assert_equal 1, entry.issue_id
100 100 assert_equal 2, entry.user_id
101 101
102 102 @request.session[:user_id] = 1
103 103 post :edit, :id => 1,
104 104 :time_entry => {:issue_id => '2',
105 105 :hours => '8'}
106 106 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
107 107 entry.reload
108 108
109 109 assert_equal 8, entry.hours
110 110 assert_equal 2, entry.issue_id
111 111 assert_equal 2, entry.user_id
112 112 end
113 113
114 114 def test_destroy_routing
115 115 #TODO: use DELETE to time_entry_path
116 116 assert_routing(
117 117 {:method => :post, :path => '/time_entries/55/destroy'},
118 118 :controller => 'timelog', :action => 'destroy', :id => '55'
119 119 )
120 120 end
121 121
122 122 def test_destroy
123 123 @request.session[:user_id] = 2
124 124 post :destroy, :id => 1
125 125 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
126 126 assert_nil TimeEntry.find_by_id(1)
127 127 end
128 128
129 129 def test_report_routing
130 130 assert_routing(
131 131 {:method => :get, :path => '/projects/567/time_entries/report'},
132 132 :controller => 'timelog', :action => 'report', :project_id => '567'
133 133 )
134 134 assert_routing(
135 135 {:method => :get, :path => '/projects/567/time_entries/report.csv'},
136 136 :controller => 'timelog', :action => 'report', :project_id => '567', :format => 'csv'
137 137 )
138 138 end
139 139
140 140 def test_report_no_criteria
141 141 get :report, :project_id => 1
142 142 assert_response :success
143 143 assert_template 'report'
144 144 end
145 145
146 146 def test_report_routing_for_all_projects
147 147 assert_routing(
148 148 {:method => :get, :path => '/time_entries/report'},
149 149 :controller => 'timelog', :action => 'report'
150 150 )
151 151 end
152 152
153 153 def test_report_all_projects
154 154 get :report
155 155 assert_response :success
156 156 assert_template 'report'
157 157 end
158 158
159 159 def test_report_all_projects_denied
160 160 r = Role.anonymous
161 161 r.permissions.delete(:view_time_entries)
162 162 r.permissions_will_change!
163 163 r.save
164 164 get :report
165 165 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
166 166 end
167 167
168 168 def test_report_all_projects_one_criteria
169 169 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
170 170 assert_response :success
171 171 assert_template 'report'
172 172 assert_not_nil assigns(:total_hours)
173 173 assert_equal "8.65", "%.2f" % assigns(:total_hours)
174 174 end
175 175
176 176 def test_report_all_time
177 177 get :report, :project_id => 1, :criterias => ['project', 'issue']
178 178 assert_response :success
179 179 assert_template 'report'
180 180 assert_not_nil assigns(:total_hours)
181 181 assert_equal "162.90", "%.2f" % assigns(:total_hours)
182 182 end
183 183
184 184 def test_report_all_time_by_day
185 185 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
186 186 assert_response :success
187 187 assert_template 'report'
188 188 assert_not_nil assigns(:total_hours)
189 189 assert_equal "162.90", "%.2f" % assigns(:total_hours)
190 190 assert_tag :tag => 'th', :content => '2007-03-12'
191 191 end
192 192
193 193 def test_report_one_criteria
194 194 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
195 195 assert_response :success
196 196 assert_template 'report'
197 197 assert_not_nil assigns(:total_hours)
198 198 assert_equal "8.65", "%.2f" % assigns(:total_hours)
199 199 end
200 200
201 201 def test_report_two_criterias
202 202 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
203 203 assert_response :success
204 204 assert_template 'report'
205 205 assert_not_nil assigns(:total_hours)
206 206 assert_equal "162.90", "%.2f" % assigns(:total_hours)
207 207 end
208 208
209 209 def test_report_custom_field_criteria
210 210 get :report, :project_id => 1, :criterias => ['project', 'cf_1']
211 211 assert_response :success
212 212 assert_template 'report'
213 213 assert_not_nil assigns(:total_hours)
214 214 assert_not_nil assigns(:criterias)
215 215 assert_equal 2, assigns(:criterias).size
216 216 assert_equal "162.90", "%.2f" % assigns(:total_hours)
217 217 # Custom field column
218 218 assert_tag :tag => 'th', :content => 'Database'
219 219 # Custom field row
220 220 assert_tag :tag => 'td', :content => 'MySQL',
221 221 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
222 222 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
223 223 :content => '1' }}
224 224 end
225 225
226 226 def test_report_one_criteria_no_result
227 227 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
228 228 assert_response :success
229 229 assert_template 'report'
230 230 assert_not_nil assigns(:total_hours)
231 231 assert_equal "0.00", "%.2f" % assigns(:total_hours)
232 232 end
233 233
234 234 def test_report_all_projects_csv_export
235 235 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
236 236 assert_response :success
237 237 assert_equal 'text/csv', @response.content_type
238 238 lines = @response.body.chomp.split("\n")
239 239 # Headers
240 240 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
241 241 # Total row
242 242 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
243 243 end
244 244
245 245 def test_report_csv_export
246 246 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
247 247 assert_response :success
248 248 assert_equal 'text/csv', @response.content_type
249 249 lines = @response.body.chomp.split("\n")
250 250 # Headers
251 251 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
252 252 # Total row
253 253 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
254 254 end
255 255
256 256 def test_details_all_projects
257 257 get :details
258 258 assert_response :success
259 259 assert_template 'details'
260 260 assert_not_nil assigns(:total_hours)
261 261 assert_equal "162.90", "%.2f" % assigns(:total_hours)
262 262 end
263 263
264 264 def test_project_details_routing
265 265 assert_routing(
266 266 {:method => :get, :path => '/projects/567/time_entries'},
267 267 :controller => 'timelog', :action => 'details', :project_id => '567'
268 268 )
269 269 end
270 270
271 271 def test_details_at_project_level
272 272 get :details, :project_id => 1
273 273 assert_response :success
274 274 assert_template 'details'
275 275 assert_not_nil assigns(:entries)
276 276 assert_equal 4, assigns(:entries).size
277 277 # project and subproject
278 278 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
279 279 assert_not_nil assigns(:total_hours)
280 280 assert_equal "162.90", "%.2f" % assigns(:total_hours)
281 281 # display all time by default
282 282 assert_equal '2007-03-11'.to_date, assigns(:from)
283 283 assert_equal '2007-04-22'.to_date, assigns(:to)
284 284 end
285 285
286 286 def test_details_at_project_level_with_date_range
287 287 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
288 288 assert_response :success
289 289 assert_template 'details'
290 290 assert_not_nil assigns(:entries)
291 291 assert_equal 3, assigns(:entries).size
292 292 assert_not_nil assigns(:total_hours)
293 293 assert_equal "12.90", "%.2f" % assigns(:total_hours)
294 294 assert_equal '2007-03-20'.to_date, assigns(:from)
295 295 assert_equal '2007-04-30'.to_date, assigns(:to)
296 296 end
297 297
298 298 def test_details_at_project_level_with_period
299 299 get :details, :project_id => 1, :period => '7_days'
300 300 assert_response :success
301 301 assert_template 'details'
302 302 assert_not_nil assigns(:entries)
303 303 assert_not_nil assigns(:total_hours)
304 304 assert_equal Date.today - 7, assigns(:from)
305 305 assert_equal Date.today, assigns(:to)
306 306 end
307 307
308 308 def test_issue_details_routing
309 309 assert_routing(
310 310 {:method => :get, :path => 'time_entries'},
311 311 :controller => 'timelog', :action => 'details'
312 312 )
313 313 assert_routing(
314 314 {:method => :get, :path => '/issues/234/time_entries'},
315 315 :controller => 'timelog', :action => 'details', :issue_id => '234'
316 316 )
317 317 # TODO: issue detail page shouldnt link to project_issue_time_entries_path but to normal issues one
318 318 # doesnt seem to have effect on resulting page so controller can be left untouched
319 319 assert_routing(
320 320 {:method => :get, :path => '/projects/ecookbook/issues/123/time_entries'},
321 321 :controller => 'timelog', :action => 'details', :project_id => 'ecookbook', :issue_id => '123'
322 322 )
323 323 end
324 324
325 325 def test_details_at_issue_level
326 326 get :details, :issue_id => 1
327 327 assert_response :success
328 328 assert_template 'details'
329 329 assert_not_nil assigns(:entries)
330 330 assert_equal 2, assigns(:entries).size
331 331 assert_not_nil assigns(:total_hours)
332 332 assert_equal 154.25, assigns(:total_hours)
333 333 # display all time by default
334 334 assert_equal '2007-03-11'.to_date, assigns(:from)
335 335 assert_equal '2007-04-22'.to_date, assigns(:to)
336 336 end
337 337
338 338 def test_details_formatted_routing
339 339 assert_routing(
340 340 {:method => :get, :path => 'time_entries.atom'},
341 341 :controller => 'timelog', :action => 'details', :format => 'atom'
342 342 )
343 343 assert_routing(
344 344 {:method => :get, :path => 'time_entries.csv'},
345 345 :controller => 'timelog', :action => 'details', :format => 'csv'
346 346 )
347 347 end
348 348
349 349 def test_details_for_project_formatted_routing
350 350 assert_routing(
351 351 {:method => :get, :path => '/projects/567/time_entries.atom'},
352 352 :controller => 'timelog', :action => 'details', :format => 'atom', :project_id => '567'
353 353 )
354 354 assert_routing(
355 355 {:method => :get, :path => '/projects/567/time_entries.csv'},
356 356 :controller => 'timelog', :action => 'details', :format => 'csv', :project_id => '567'
357 357 )
358 358 end
359 359
360 360 def test_details_for_issue_formatted_routing
361 361 assert_routing(
362 362 {:method => :get, :path => '/projects/ecookbook/issues/123/time_entries.atom'},
363 363 :controller => 'timelog', :action => 'details', :project_id => 'ecookbook', :issue_id => '123', :format => 'atom'
364 364 )
365 365 assert_routing(
366 366 {:method => :get, :path => '/projects/ecookbook/issues/123/time_entries.csv'},
367 367 :controller => 'timelog', :action => 'details', :project_id => 'ecookbook', :issue_id => '123', :format => 'csv'
368 368 )
369 369 end
370 370
371 371 def test_details_atom_feed
372 372 get :details, :project_id => 1, :format => 'atom'
373 373 assert_response :success
374 374 assert_equal 'application/atom+xml', @response.content_type
375 375 assert_not_nil assigns(:items)
376 376 assert assigns(:items).first.is_a?(TimeEntry)
377 377 end
378 378
379 379 def test_details_all_projects_csv_export
380 380 Setting.date_format = '%m/%d/%Y'
381 381 get :details, :format => 'csv'
382 382 assert_response :success
383 383 assert_equal 'text/csv', @response.content_type
384 384 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
385 385 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
386 386 end
387 387
388 388 def test_details_csv_export
389 389 Setting.date_format = '%m/%d/%Y'
390 390 get :details, :project_id => 1, :format => 'csv'
391 391 assert_response :success
392 392 assert_equal 'text/csv', @response.content_type
393 393 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
394 394 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
395 395 end
396 396 end
@@ -1,160 +1,160
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'users_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class UsersController; def rescue_action(e) raise e end; end
23 23
24 24 class UsersControllerTest < Test::Unit::TestCase
25 25 include Redmine::I18n
26 26
27 fixtures :users, :projects, :members
27 fixtures :users, :projects, :members, :member_roles, :roles
28 28
29 29 def setup
30 30 @controller = UsersController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 User.current = nil
34 34 @request.session[:user_id] = 1 # admin
35 35 end
36 36
37 37 def test_index_routing
38 38 #TODO: unify with list
39 39 assert_generates(
40 40 '/users',
41 41 :controller => 'users', :action => 'index'
42 42 )
43 43 end
44 44
45 45 def test_index
46 46 get :index
47 47 assert_response :success
48 48 assert_template 'list'
49 49 end
50 50
51 51 def test_list_routing
52 52 #TODO: rename action to index
53 53 assert_routing(
54 54 {:method => :get, :path => '/users'},
55 55 :controller => 'users', :action => 'list'
56 56 )
57 57 end
58 58
59 59 def test_list
60 60 get :list
61 61 assert_response :success
62 62 assert_template 'list'
63 63 assert_not_nil assigns(:users)
64 64 # active users only
65 65 assert_nil assigns(:users).detect {|u| !u.active?}
66 66 end
67 67
68 68 def test_list_with_name_filter
69 69 get :list, :name => 'john'
70 70 assert_response :success
71 71 assert_template 'list'
72 72 users = assigns(:users)
73 73 assert_not_nil users
74 74 assert_equal 1, users.size
75 75 assert_equal 'John', users.first.firstname
76 76 end
77 77
78 78 def test_add_routing
79 79 assert_routing(
80 80 {:method => :get, :path => '/users/new'},
81 81 :controller => 'users', :action => 'add'
82 82 )
83 83 assert_recognizes(
84 84 #TODO: remove this and replace with POST to collection, need to modify form
85 85 {:controller => 'users', :action => 'add'},
86 86 {:method => :post, :path => '/users/new'}
87 87 )
88 88 assert_recognizes(
89 89 {:controller => 'users', :action => 'add'},
90 90 {:method => :post, :path => '/users'}
91 91 )
92 92 end
93 93
94 94 def test_edit_routing
95 95 assert_routing(
96 96 {:method => :get, :path => '/users/444/edit'},
97 97 :controller => 'users', :action => 'edit', :id => '444'
98 98 )
99 99 assert_routing(
100 100 {:method => :get, :path => '/users/222/edit/membership'},
101 101 :controller => 'users', :action => 'edit', :id => '222', :tab => 'membership'
102 102 )
103 103 assert_recognizes(
104 104 #TODO: use PUT on user_path, modify form
105 105 {:controller => 'users', :action => 'edit', :id => '444'},
106 106 {:method => :post, :path => '/users/444/edit'}
107 107 )
108 108 end
109 109
110 110 def test_add_membership_routing
111 111 assert_routing(
112 112 {:method => :post, :path => '/users/123/memberships'},
113 113 :controller => 'users', :action => 'edit_membership', :id => '123'
114 114 )
115 115 end
116 116
117 117 def test_edit_membership_routing
118 118 assert_routing(
119 119 {:method => :post, :path => '/users/123/memberships/55'},
120 120 :controller => 'users', :action => 'edit_membership', :id => '123', :membership_id => '55'
121 121 )
122 122 end
123 123
124 124 def test_edit_membership
125 125 post :edit_membership, :id => 2, :membership_id => 1,
126 :membership => { :role_id => 2}
126 :membership => { :role_ids => [2]}
127 127 assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships'
128 assert_equal 2, Member.find(1).role_id
128 assert_equal [2], Member.find(1).role_ids
129 129 end
130 130
131 131 def test_edit_with_activation_should_send_a_notification
132 132 u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr')
133 133 u.login = 'foo'
134 134 u.status = User::STATUS_REGISTERED
135 135 u.save!
136 136 ActionMailer::Base.deliveries.clear
137 137 Setting.bcc_recipients = '1'
138 138
139 139 post :edit, :id => u.id, :user => {:status => User::STATUS_ACTIVE}
140 140 assert u.reload.active?
141 141 mail = ActionMailer::Base.deliveries.last
142 142 assert_not_nil mail
143 143 assert_equal ['foo.bar@somenet.foo'], mail.bcc
144 144 assert mail.body.include?(ll('fr', :notice_account_activated))
145 145 end
146 146
147 147 def test_destroy_membership
148 148 assert_routing(
149 149 #TODO: use DELETE method on user_membership_path, modify form
150 150 {:method => :post, :path => '/users/567/memberships/12/destroy'},
151 151 :controller => 'users', :action => 'destroy_membership', :id => '567', :membership_id => '12'
152 152 )
153 153 end
154 154
155 155 def test_destroy_membership
156 156 post :destroy_membership, :id => 2, :membership_id => 1
157 157 assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships'
158 158 assert_nil Member.find_by_id(1)
159 159 end
160 160 end
@@ -1,73 +1,73
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'versions_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class VersionsController; def rescue_action(e) raise e end; end
23 23
24 24 class VersionsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :versions, :issues, :users, :roles, :members, :enabled_modules
25 fixtures :projects, :versions, :issues, :users, :roles, :members, :member_roles, :enabled_modules
26 26
27 27 def setup
28 28 @controller = VersionsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_show
35 35 get :show, :id => 2
36 36 assert_response :success
37 37 assert_template 'show'
38 38 assert_not_nil assigns(:version)
39 39
40 40 assert_tag :tag => 'h2', :content => /1.0/
41 41 end
42 42
43 43 def test_get_edit
44 44 @request.session[:user_id] = 2
45 45 get :edit, :id => 2
46 46 assert_response :success
47 47 assert_template 'edit'
48 48 end
49 49
50 50 def test_post_edit
51 51 @request.session[:user_id] = 2
52 52 post :edit, :id => 2,
53 53 :version => { :name => 'New version name',
54 54 :effective_date => Date.today.strftime("%Y-%m-%d")}
55 55 assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
56 56 version = Version.find(2)
57 57 assert_equal 'New version name', version.name
58 58 assert_equal Date.today, version.effective_date
59 59 end
60 60
61 61 def test_destroy
62 62 @request.session[:user_id] = 2
63 63 post :destroy, :id => 3
64 64 assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook'
65 65 assert_nil Version.find_by_id(3)
66 66 end
67 67
68 68 def test_issue_status_by
69 69 xhr :get, :status_by, :id => 2
70 70 assert_response :success
71 71 assert_template '_issue_counts'
72 72 end
73 73 end
@@ -1,70 +1,70
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'watchers_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WatchersController; def rescue_action(e) raise e end; end
23 23
24 24 class WatchersControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :enabled_modules,
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
26 26 :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers
27 27
28 28 def setup
29 29 @controller = WatchersController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 User.current = nil
33 33 end
34 34
35 35 def test_get_watch_should_be_invalid
36 36 @request.session[:user_id] = 3
37 37 get :watch, :object_type => 'issue', :object_id => '1'
38 38 assert_response 405
39 39 end
40 40
41 41 def test_watch
42 42 @request.session[:user_id] = 3
43 43 assert_difference('Watcher.count') do
44 44 xhr :post, :watch, :object_type => 'issue', :object_id => '1'
45 45 assert_response :success
46 46 assert_select_rjs :replace_html, 'watcher'
47 47 end
48 48 assert Issue.find(1).watched_by?(User.find(3))
49 49 end
50 50
51 51 def test_unwatch
52 52 @request.session[:user_id] = 3
53 53 assert_difference('Watcher.count', -1) do
54 54 xhr :post, :unwatch, :object_type => 'issue', :object_id => '2'
55 55 assert_response :success
56 56 assert_select_rjs :replace_html, 'watcher'
57 57 end
58 58 assert !Issue.find(1).watched_by?(User.find(3))
59 59 end
60 60
61 61 def test_new_watcher
62 62 @request.session[:user_id] = 2
63 63 assert_difference('Watcher.count') do
64 64 xhr :post, :new, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'}
65 65 assert_response :success
66 66 assert_select_rjs :replace_html, 'watchers'
67 67 end
68 68 assert Issue.find(2).watched_by?(User.find(4))
69 69 end
70 70 end
@@ -1,397 +1,397
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'wiki_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WikiController; def rescue_action(e) raise e end; end
23 23
24 24 class WikiControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
26 26
27 27 def setup
28 28 @controller = WikiController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_index_routing
35 35 assert_routing(
36 36 {:method => :get, :path => '/projects/567/wiki'},
37 37 :controller => 'wiki', :action => 'index', :id => '567'
38 38 )
39 39 assert_routing(
40 40 {:method => :get, :path => '/projects/567/wiki/lalala'},
41 41 :controller => 'wiki', :action => 'index', :id => '567', :page => 'lalala'
42 42 )
43 43 assert_generates(
44 44 '/projects/567/wiki',
45 45 :controller => 'wiki', :action => 'index', :id => '567', :page => nil
46 46 )
47 47 end
48 48
49 49 def test_show_start_page
50 50 get :index, :id => 'ecookbook'
51 51 assert_response :success
52 52 assert_template 'show'
53 53 assert_tag :tag => 'h1', :content => /CookBook documentation/
54 54
55 55 # child_pages macro
56 56 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
57 57 :child => { :tag => 'li',
58 58 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
59 59 :content => 'Page with an inline image' } }
60 60 end
61 61
62 62 def test_show_page_with_name
63 63 get :index, :id => 1, :page => 'Another_page'
64 64 assert_response :success
65 65 assert_template 'show'
66 66 assert_tag :tag => 'h1', :content => /Another page/
67 67 # Included page with an inline image
68 68 assert_tag :tag => 'p', :content => /This is an inline image/
69 69 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
70 70 :alt => 'This is a logo' }
71 71 end
72 72
73 73 def test_show_unexistent_page_without_edit_right
74 74 get :index, :id => 1, :page => 'Unexistent page'
75 75 assert_response 404
76 76 end
77 77
78 78 def test_show_unexistent_page_with_edit_right
79 79 @request.session[:user_id] = 2
80 80 get :index, :id => 1, :page => 'Unexistent page'
81 81 assert_response :success
82 82 assert_template 'edit'
83 83 end
84 84
85 85 def test_edit_routing
86 86 assert_routing(
87 87 {:method => :get, :path => '/projects/567/wiki/my_page/edit'},
88 88 :controller => 'wiki', :action => 'edit', :id => '567', :page => 'my_page'
89 89 )
90 90 assert_recognizes(#TODO: use PUT to page path, adjust forms accordingly
91 91 {:controller => 'wiki', :action => 'edit', :id => '567', :page => 'my_page'},
92 92 {:method => :post, :path => '/projects/567/wiki/my_page/edit'}
93 93 )
94 94 end
95 95
96 96 def test_create_page
97 97 @request.session[:user_id] = 2
98 98 post :edit, :id => 1,
99 99 :page => 'New page',
100 100 :content => {:comments => 'Created the page',
101 101 :text => "h1. New page\n\nThis is a new page",
102 102 :version => 0}
103 103 assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'New_page'
104 104 page = Project.find(1).wiki.find_page('New page')
105 105 assert !page.new_record?
106 106 assert_not_nil page.content
107 107 assert_equal 'Created the page', page.content.comments
108 108 end
109 109
110 110 def test_preview_routing
111 111 assert_routing(
112 112 {:method => :post, :path => '/projects/567/wiki/CookBook_documentation/preview'},
113 113 :controller => 'wiki', :action => 'preview', :id => '567', :page => 'CookBook_documentation'
114 114 )
115 115 end
116 116
117 117 def test_preview
118 118 @request.session[:user_id] = 2
119 119 xhr :post, :preview, :id => 1, :page => 'CookBook_documentation',
120 120 :content => { :comments => '',
121 121 :text => 'this is a *previewed text*',
122 122 :version => 3 }
123 123 assert_response :success
124 124 assert_template 'common/_preview'
125 125 assert_tag :tag => 'strong', :content => /previewed text/
126 126 end
127 127
128 128 def test_preview_new_page
129 129 @request.session[:user_id] = 2
130 130 xhr :post, :preview, :id => 1, :page => 'New page',
131 131 :content => { :text => 'h1. New page',
132 132 :comments => '',
133 133 :version => 0 }
134 134 assert_response :success
135 135 assert_template 'common/_preview'
136 136 assert_tag :tag => 'h1', :content => /New page/
137 137 end
138 138
139 139 def test_history_routing
140 140 assert_routing(
141 141 {:method => :get, :path => '/projects/1/wiki/CookBook_documentation/history'},
142 142 :controller => 'wiki', :action => 'history', :id => '1', :page => 'CookBook_documentation'
143 143 )
144 144 end
145 145
146 146 def test_history
147 147 get :history, :id => 1, :page => 'CookBook_documentation'
148 148 assert_response :success
149 149 assert_template 'history'
150 150 assert_not_nil assigns(:versions)
151 151 assert_equal 3, assigns(:versions).size
152 152 assert_select "input[type=submit][name=commit]"
153 153 end
154 154
155 155 def test_history_with_one_version
156 156 get :history, :id => 1, :page => 'Another_page'
157 157 assert_response :success
158 158 assert_template 'history'
159 159 assert_not_nil assigns(:versions)
160 160 assert_equal 1, assigns(:versions).size
161 161 assert_select "input[type=submit][name=commit]", false
162 162 end
163 163
164 164 def test_diff_routing
165 165 assert_routing(
166 166 {:method => :get, :path => '/projects/1/wiki/CookBook_documentation/diff/2/vs/1'},
167 167 :controller => 'wiki', :action => 'diff', :id => '1', :page => 'CookBook_documentation', :version => '2', :version_from => '1'
168 168 )
169 169 end
170 170
171 171 def test_diff
172 172 get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1
173 173 assert_response :success
174 174 assert_template 'diff'
175 175 assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
176 176 :content => /updated/
177 177 end
178 178
179 179 def test_annotate_routing
180 180 assert_routing(
181 181 {:method => :get, :path => '/projects/1/wiki/CookBook_documentation/annotate/2'},
182 182 :controller => 'wiki', :action => 'annotate', :id => '1', :page => 'CookBook_documentation', :version => '2'
183 183 )
184 184 end
185 185
186 186 def test_annotate
187 187 get :annotate, :id => 1, :page => 'CookBook_documentation', :version => 2
188 188 assert_response :success
189 189 assert_template 'annotate'
190 190 # Line 1
191 191 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1' },
192 192 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/ },
193 193 :child => { :tag => 'td', :content => /h1\. CookBook documentation/ }
194 194 # Line 2
195 195 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '2' },
196 196 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
197 197 :child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
198 198 end
199 199
200 200 def test_rename_routing
201 201 assert_routing(
202 202 {:method => :get, :path => '/projects/22/wiki/ladida/rename'},
203 203 :controller => 'wiki', :action => 'rename', :id => '22', :page => 'ladida'
204 204 )
205 205 assert_recognizes(
206 206 #TODO: should be moved into a update action and use a PUT to the page URI
207 207 {:controller => 'wiki', :action => 'rename', :id => '22', :page => 'ladida'},
208 208 {:method => :post, :path => '/projects/22/wiki/ladida/rename'}
209 209 )
210 210 end
211 211
212 212 def test_rename_with_redirect
213 213 @request.session[:user_id] = 2
214 214 post :rename, :id => 1, :page => 'Another_page',
215 215 :wiki_page => { :title => 'Another renamed page',
216 216 :redirect_existing_links => 1 }
217 217 assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'Another_renamed_page'
218 218 wiki = Project.find(1).wiki
219 219 # Check redirects
220 220 assert_not_nil wiki.find_page('Another page')
221 221 assert_nil wiki.find_page('Another page', :with_redirect => false)
222 222 end
223 223
224 224 def test_rename_without_redirect
225 225 @request.session[:user_id] = 2
226 226 post :rename, :id => 1, :page => 'Another_page',
227 227 :wiki_page => { :title => 'Another renamed page',
228 228 :redirect_existing_links => "0" }
229 229 assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'Another_renamed_page'
230 230 wiki = Project.find(1).wiki
231 231 # Check that there's no redirects
232 232 assert_nil wiki.find_page('Another page')
233 233 end
234 234
235 235 def test_destroy_routing
236 236 assert_recognizes(
237 237 #TODO: should use DELETE on page URI
238 238 {:controller => 'wiki', :action => 'destroy', :id => '22', :page => 'ladida'},
239 239 {:method => :post, :path => 'projects/22/wiki/ladida/destroy'}
240 240 )
241 241 end
242 242
243 243 def test_destroy_child
244 244 @request.session[:user_id] = 2
245 245 post :destroy, :id => 1, :page => 'Child_1'
246 246 assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
247 247 end
248 248
249 249 def test_destroy_parent
250 250 @request.session[:user_id] = 2
251 251 assert_no_difference('WikiPage.count') do
252 252 post :destroy, :id => 1, :page => 'Another_page'
253 253 end
254 254 assert_response :success
255 255 assert_template 'destroy'
256 256 end
257 257
258 258 def test_destroy_parent_with_nullify
259 259 @request.session[:user_id] = 2
260 260 assert_difference('WikiPage.count', -1) do
261 261 post :destroy, :id => 1, :page => 'Another_page', :todo => 'nullify'
262 262 end
263 263 assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
264 264 assert_nil WikiPage.find_by_id(2)
265 265 end
266 266
267 267 def test_destroy_parent_with_cascade
268 268 @request.session[:user_id] = 2
269 269 assert_difference('WikiPage.count', -3) do
270 270 post :destroy, :id => 1, :page => 'Another_page', :todo => 'destroy'
271 271 end
272 272 assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
273 273 assert_nil WikiPage.find_by_id(2)
274 274 assert_nil WikiPage.find_by_id(5)
275 275 end
276 276
277 277 def test_destroy_parent_with_reassign
278 278 @request.session[:user_id] = 2
279 279 assert_difference('WikiPage.count', -1) do
280 280 post :destroy, :id => 1, :page => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
281 281 end
282 282 assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
283 283 assert_nil WikiPage.find_by_id(2)
284 284 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
285 285 end
286 286
287 287 def test_special_routing
288 288 assert_routing(
289 289 {:method => :get, :path => '/projects/567/wiki/page_index'},
290 290 :controller => 'wiki', :action => 'special', :id => '567', :page => 'page_index'
291 291 )
292 292 assert_routing(
293 293 {:method => :get, :path => '/projects/567/wiki/Page_Index'},
294 294 :controller => 'wiki', :action => 'special', :id => '567', :page => 'Page_Index'
295 295 )
296 296 assert_routing(
297 297 {:method => :get, :path => '/projects/567/wiki/date_index'},
298 298 :controller => 'wiki', :action => 'special', :id => '567', :page => 'date_index'
299 299 )
300 300 assert_routing(
301 301 {:method => :get, :path => '/projects/567/wiki/export'},
302 302 :controller => 'wiki', :action => 'special', :id => '567', :page => 'export'
303 303 )
304 304 end
305 305
306 306 def test_page_index
307 307 get :special, :id => 'ecookbook', :page => 'Page_index'
308 308 assert_response :success
309 309 assert_template 'special_page_index'
310 310 pages = assigns(:pages)
311 311 assert_not_nil pages
312 312 assert_equal Project.find(1).wiki.pages.size, pages.size
313 313
314 314 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
315 315 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
316 316 :content => 'CookBook documentation' },
317 317 :child => { :tag => 'ul',
318 318 :child => { :tag => 'li',
319 319 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
320 320 :content => 'Page with an inline image' } } } },
321 321 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
322 322 :content => 'Another page' } }
323 323 end
324 324
325 325 def test_not_found
326 326 get :index, :id => 999
327 327 assert_response 404
328 328 end
329 329
330 330 def test_protect_routing
331 331 assert_routing(
332 332 {:method => :post, :path => 'projects/22/wiki/ladida/protect'},
333 333 {:controller => 'wiki', :action => 'protect', :id => '22', :page => 'ladida'}
334 334 )
335 335 end
336 336
337 337 def test_protect_page
338 338 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
339 339 assert !page.protected?
340 340 @request.session[:user_id] = 2
341 341 post :protect, :id => 1, :page => page.title, :protected => '1'
342 342 assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'Another_page'
343 343 assert page.reload.protected?
344 344 end
345 345
346 346 def test_unprotect_page
347 347 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
348 348 assert page.protected?
349 349 @request.session[:user_id] = 2
350 350 post :protect, :id => 1, :page => page.title, :protected => '0'
351 351 assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'CookBook_documentation'
352 352 assert !page.reload.protected?
353 353 end
354 354
355 355 def test_show_page_with_edit_link
356 356 @request.session[:user_id] = 2
357 357 get :index, :id => 1
358 358 assert_response :success
359 359 assert_template 'show'
360 360 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
361 361 end
362 362
363 363 def test_show_page_without_edit_link
364 364 @request.session[:user_id] = 4
365 365 get :index, :id => 1
366 366 assert_response :success
367 367 assert_template 'show'
368 368 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
369 369 end
370 370
371 371 def test_edit_unprotected_page
372 372 # Non members can edit unprotected wiki pages
373 373 @request.session[:user_id] = 4
374 374 get :edit, :id => 1, :page => 'Another_page'
375 375 assert_response :success
376 376 assert_template 'edit'
377 377 end
378 378
379 379 def test_edit_protected_page_by_nonmember
380 380 # Non members can't edit protected wiki pages
381 381 @request.session[:user_id] = 4
382 382 get :edit, :id => 1, :page => 'CookBook_documentation'
383 383 assert_response 403
384 384 end
385 385
386 386 def test_edit_protected_page_by_member
387 387 @request.session[:user_id] = 2
388 388 get :edit, :id => 1, :page => 'CookBook_documentation'
389 389 assert_response :success
390 390 assert_template 'edit'
391 391 end
392 392
393 393 def test_history_of_non_existing_page_should_return_404
394 394 get :history, :id => 1, :page => 'Unknown_page'
395 395 assert_response 404
396 396 end
397 397 end
@@ -1,75 +1,75
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'wikis_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WikisController; def rescue_action(e) raise e end; end
23 23
24 24 class WikisControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis
26 26
27 27 def setup
28 28 @controller = WikisController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_edit_routing
35 35 assert_routing(
36 36 #TODO: use PUT
37 37 {:method => :post, :path => 'projects/ladida/wiki'},
38 38 :controller => 'wikis', :action => 'edit', :id => 'ladida'
39 39 )
40 40 end
41 41
42 42 def test_create
43 43 @request.session[:user_id] = 1
44 44 assert_nil Project.find(3).wiki
45 45 post :edit, :id => 3, :wiki => { :start_page => 'Start page' }
46 46 assert_response :success
47 47 wiki = Project.find(3).wiki
48 48 assert_not_nil wiki
49 49 assert_equal 'Start page', wiki.start_page
50 50 end
51 51
52 52 def test_destroy_routing
53 53 assert_routing(
54 54 {:method => :get, :path => 'projects/ladida/wiki/destroy'},
55 55 :controller => 'wikis', :action => 'destroy', :id => 'ladida'
56 56 )
57 57 assert_recognizes( #TODO: use DELETE and update form
58 58 {:controller => 'wikis', :action => 'destroy', :id => 'ladida'},
59 59 {:method => :post, :path => 'projects/ladida/wiki/destroy'}
60 60 )
61 61 end
62 62
63 63 def test_destroy
64 64 @request.session[:user_id] = 1
65 65 post :destroy, :id => 1, :confirm => 1
66 66 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'ecookbook', :tab => 'wiki'
67 67 assert_nil Project.find(1).wiki
68 68 end
69 69
70 70 def test_not_found
71 71 @request.session[:user_id] = 1
72 72 post :destroy, :id => 999, :confirm => 1
73 73 assert_response 404
74 74 end
75 75 end
@@ -1,92 +1,92
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ActivityTest < Test::Unit::TestCase
21 fixtures :projects, :versions, :attachments, :users, :roles, :members, :issues, :journals, :journal_details,
21 fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
23 23
24 24 def setup
25 25 @project = Project.find(1)
26 26 end
27 27
28 28 def test_activity_without_subprojects
29 29 events = find_events(User.anonymous, :project => @project)
30 30 assert_not_nil events
31 31
32 32 assert events.include?(Issue.find(1))
33 33 assert !events.include?(Issue.find(4))
34 34 # subproject issue
35 35 assert !events.include?(Issue.find(5))
36 36 end
37 37
38 38 def test_activity_with_subprojects
39 39 events = find_events(User.anonymous, :project => @project, :with_subprojects => 1)
40 40 assert_not_nil events
41 41
42 42 assert events.include?(Issue.find(1))
43 43 # subproject issue
44 44 assert events.include?(Issue.find(5))
45 45 end
46 46
47 47 def test_global_activity_anonymous
48 48 events = find_events(User.anonymous)
49 49 assert_not_nil events
50 50
51 51 assert events.include?(Issue.find(1))
52 52 assert events.include?(Message.find(5))
53 53 # Issue of a private project
54 54 assert !events.include?(Issue.find(4))
55 55 end
56 56
57 57 def test_global_activity_logged_user
58 58 events = find_events(User.find(2)) # manager
59 59 assert_not_nil events
60 60
61 61 assert events.include?(Issue.find(1))
62 62 # Issue of a private project the user belongs to
63 63 assert events.include?(Issue.find(4))
64 64 end
65 65
66 66 def test_user_activity
67 67 user = User.find(2)
68 68 events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10)
69 69
70 70 assert(events.size > 0)
71 71 assert(events.size <= 10)
72 72 assert_nil(events.detect {|e| e.event_author != user})
73 73 end
74 74
75 75 def test_files_activity
76 76 f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
77 77 f.scope = ['files']
78 78 events = f.events
79 79
80 80 assert_kind_of Array, events
81 81 assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1))
82 82 assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1))
83 83 assert_equal [Attachment], events.collect(&:class).uniq
84 84 assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort
85 85 end
86 86
87 87 private
88 88
89 89 def find_events(user, options={})
90 90 Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
91 91 end
92 92 end
@@ -1,75 +1,75
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ChangesetTest < Test::Unit::TestCase
21 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :trackers
21 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
22 22
23 23 def setup
24 24 end
25 25
26 26 def test_ref_keywords_any
27 27 ActionMailer::Base.deliveries.clear
28 28 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
29 29 Setting.commit_fix_done_ratio = '90'
30 30 Setting.commit_ref_keywords = '*'
31 31 Setting.commit_fix_keywords = 'fixes , closes'
32 32
33 33 c = Changeset.new(:repository => Project.find(1).repository,
34 34 :committed_on => Time.now,
35 35 :comments => 'New commit (#2). Fixes #1')
36 36 c.scan_comment_for_issue_ids
37 37
38 38 assert_equal [1, 2], c.issue_ids.sort
39 39 fixed = Issue.find(1)
40 40 assert fixed.closed?
41 41 assert_equal 90, fixed.done_ratio
42 42 assert_equal 1, ActionMailer::Base.deliveries.size
43 43 end
44 44
45 45 def test_ref_keywords_any_line_start
46 46 Setting.commit_ref_keywords = '*'
47 47
48 48 c = Changeset.new(:repository => Project.find(1).repository,
49 49 :committed_on => Time.now,
50 50 :comments => '#1 is the reason of this commit')
51 51 c.scan_comment_for_issue_ids
52 52
53 53 assert_equal [1], c.issue_ids.sort
54 54 end
55 55
56 56 def test_previous
57 57 changeset = Changeset.find_by_revision('3')
58 58 assert_equal Changeset.find_by_revision('2'), changeset.previous
59 59 end
60 60
61 61 def test_previous_nil
62 62 changeset = Changeset.find_by_revision('1')
63 63 assert_nil changeset.previous
64 64 end
65 65
66 66 def test_next
67 67 changeset = Changeset.find_by_revision('2')
68 68 assert_equal Changeset.find_by_revision('3'), changeset.next
69 69 end
70 70
71 71 def test_next_nil
72 72 changeset = Changeset.find_by_revision('4')
73 73 assert_nil changeset.next
74 74 end
75 75 end
@@ -1,271 +1,275
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class IssueTest < Test::Unit::TestCase
21 fixtures :projects, :users, :members,
21 fixtures :projects, :users, :members, :member_roles,
22 22 :trackers, :projects_trackers,
23 23 :issue_statuses, :issue_categories,
24 24 :enumerations,
25 25 :issues,
26 26 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 27 :time_entries
28 28
29 29 def test_create
30 30 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 31 assert issue.save
32 32 issue.reload
33 33 assert_equal 1.5, issue.estimated_hours
34 34 end
35 35
36 36 def test_create_minimal
37 37 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create')
38 38 assert issue.save
39 39 assert issue.description.nil?
40 40 end
41 41
42 42 def test_create_with_required_custom_field
43 43 field = IssueCustomField.find_by_name('Database')
44 44 field.update_attribute(:is_required, true)
45 45
46 46 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 47 assert issue.available_custom_fields.include?(field)
48 48 # No value for the custom field
49 49 assert !issue.save
50 50 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
51 51 # Blank value
52 52 issue.custom_field_values = { field.id => '' }
53 53 assert !issue.save
54 54 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
55 55 # Invalid value
56 56 issue.custom_field_values = { field.id => 'SQLServer' }
57 57 assert !issue.save
58 58 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
59 59 # Valid value
60 60 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 61 assert issue.save
62 62 issue.reload
63 63 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 64 end
65 65
66 66 def test_errors_full_messages_should_include_custom_fields_errors
67 67 field = IssueCustomField.find_by_name('Database')
68 68
69 69 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
70 70 assert issue.available_custom_fields.include?(field)
71 71 # Invalid value
72 72 issue.custom_field_values = { field.id => 'SQLServer' }
73 73
74 74 assert !issue.valid?
75 75 assert_equal 1, issue.errors.full_messages.size
76 76 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
77 77 end
78 78
79 79 def test_update_issue_with_required_custom_field
80 80 field = IssueCustomField.find_by_name('Database')
81 81 field.update_attribute(:is_required, true)
82 82
83 83 issue = Issue.find(1)
84 84 assert_nil issue.custom_value_for(field)
85 85 assert issue.available_custom_fields.include?(field)
86 86 # No change to custom values, issue can be saved
87 87 assert issue.save
88 88 # Blank value
89 89 issue.custom_field_values = { field.id => '' }
90 90 assert !issue.save
91 91 # Valid value
92 92 issue.custom_field_values = { field.id => 'PostgreSQL' }
93 93 assert issue.save
94 94 issue.reload
95 95 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
96 96 end
97 97
98 98 def test_should_not_update_attributes_if_custom_fields_validation_fails
99 99 issue = Issue.find(1)
100 100 field = IssueCustomField.find_by_name('Database')
101 101 assert issue.available_custom_fields.include?(field)
102 102
103 103 issue.custom_field_values = { field.id => 'Invalid' }
104 104 issue.subject = 'Should be not be saved'
105 105 assert !issue.save
106 106
107 107 issue.reload
108 108 assert_equal "Can't print recipes", issue.subject
109 109 end
110 110
111 111 def test_should_not_recreate_custom_values_objects_on_update
112 112 field = IssueCustomField.find_by_name('Database')
113 113
114 114 issue = Issue.find(1)
115 115 issue.custom_field_values = { field.id => 'PostgreSQL' }
116 116 assert issue.save
117 117 custom_value = issue.custom_value_for(field)
118 118 issue.reload
119 119 issue.custom_field_values = { field.id => 'MySQL' }
120 120 assert issue.save
121 121 issue.reload
122 122 assert_equal custom_value.id, issue.custom_value_for(field).id
123 123 end
124 124
125 125 def test_category_based_assignment
126 126 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
127 127 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
128 128 end
129 129
130 130 def test_copy
131 131 issue = Issue.new.copy_from(1)
132 132 assert issue.save
133 133 issue.reload
134 134 orig = Issue.find(1)
135 135 assert_equal orig.subject, issue.subject
136 136 assert_equal orig.tracker, issue.tracker
137 137 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
138 138 end
139 139
140 140 def test_should_close_duplicates
141 141 # Create 3 issues
142 142 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
143 143 assert issue1.save
144 144 issue2 = issue1.clone
145 145 assert issue2.save
146 146 issue3 = issue1.clone
147 147 assert issue3.save
148 148
149 149 # 2 is a dupe of 1
150 150 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
151 151 # And 3 is a dupe of 2
152 152 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
153 153 # And 3 is a dupe of 1 (circular duplicates)
154 154 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
155 155
156 156 assert issue1.reload.duplicates.include?(issue2)
157 157
158 158 # Closing issue 1
159 159 issue1.init_journal(User.find(:first), "Closing issue1")
160 160 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
161 161 assert issue1.save
162 162 # 2 and 3 should be also closed
163 163 assert issue2.reload.closed?
164 164 assert issue3.reload.closed?
165 165 end
166 166
167 167 def test_should_not_close_duplicated_issue
168 168 # Create 3 issues
169 169 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'Duplicates test', :description => 'Duplicates test')
170 170 assert issue1.save
171 171 issue2 = issue1.clone
172 172 assert issue2.save
173 173
174 174 # 2 is a dupe of 1
175 175 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
176 176 # 2 is a dup of 1 but 1 is not a duplicate of 2
177 177 assert !issue2.reload.duplicates.include?(issue1)
178 178
179 179 # Closing issue 2
180 180 issue2.init_journal(User.find(:first), "Closing issue2")
181 181 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
182 182 assert issue2.save
183 183 # 1 should not be also closed
184 184 assert !issue1.reload.closed?
185 185 end
186 186
187 187 def test_move_to_another_project_with_same_category
188 188 issue = Issue.find(1)
189 189 assert issue.move_to(Project.find(2))
190 190 issue.reload
191 191 assert_equal 2, issue.project_id
192 192 # Category changes
193 193 assert_equal 4, issue.category_id
194 194 # Make sure time entries were move to the target project
195 195 assert_equal 2, issue.time_entries.first.project_id
196 196 end
197 197
198 198 def test_move_to_another_project_without_same_category
199 199 issue = Issue.find(2)
200 200 assert issue.move_to(Project.find(2))
201 201 issue.reload
202 202 assert_equal 2, issue.project_id
203 203 # Category cleared
204 204 assert_nil issue.category_id
205 205 end
206 206
207 207 def test_copy_to_the_same_project
208 208 issue = Issue.find(1)
209 209 copy = nil
210 210 assert_difference 'Issue.count' do
211 211 copy = issue.move_to(issue.project, nil, :copy => true)
212 212 end
213 213 assert_kind_of Issue, copy
214 214 assert_equal issue.project, copy.project
215 215 assert_equal "125", copy.custom_value_for(2).value
216 216 end
217 217
218 218 def test_copy_to_another_project_and_tracker
219 219 issue = Issue.find(1)
220 220 copy = nil
221 221 assert_difference 'Issue.count' do
222 222 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
223 223 end
224 224 assert_kind_of Issue, copy
225 225 assert_equal Project.find(3), copy.project
226 226 assert_equal Tracker.find(2), copy.tracker
227 227 # Custom field #2 is not associated with target tracker
228 228 assert_nil copy.custom_value_for(2)
229 229 end
230 230
231 231 def test_issue_destroy
232 232 Issue.find(1).destroy
233 233 assert_nil Issue.find_by_id(1)
234 234 assert_nil TimeEntry.find_by_issue_id(1)
235 235 end
236 236
237 237 def test_overdue
238 238 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
239 239 assert !Issue.new(:due_date => Date.today).overdue?
240 240 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
241 241 assert !Issue.new(:due_date => nil).overdue?
242 242 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
243 243 end
244 244
245 def test_assignable_users
246 assert_kind_of User, Issue.find(1).assignable_users.first
247 end
248
245 249 def test_create_should_send_email_notification
246 250 ActionMailer::Base.deliveries.clear
247 251 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :estimated_hours => '1:30')
248 252
249 253 assert issue.save
250 254 assert_equal 1, ActionMailer::Base.deliveries.size
251 255 end
252 256
253 257 def test_stale_issue_should_not_send_email_notification
254 258 ActionMailer::Base.deliveries.clear
255 259 issue = Issue.find(1)
256 260 stale = Issue.find(1)
257 261
258 262 issue.init_journal(User.find(1))
259 263 issue.subject = 'Subjet update'
260 264 assert issue.save
261 265 assert_equal 1, ActionMailer::Base.deliveries.size
262 266 ActionMailer::Base.deliveries.clear
263 267
264 268 stale.init_journal(User.find(1))
265 269 stale.subject = 'Another subjet update'
266 270 assert_raise ActiveRecord::StaleObjectError do
267 271 stale.save
268 272 end
269 273 assert ActionMailer::Base.deliveries.empty?
270 274 end
271 275 end
@@ -1,205 +1,206
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class MailHandlerTest < Test::Unit::TestCase
21 21 fixtures :users, :projects,
22 22 :enabled_modules,
23 23 :roles,
24 24 :members,
25 :member_roles,
25 26 :issues,
26 27 :issue_statuses,
27 28 :workflows,
28 29 :trackers,
29 30 :projects_trackers,
30 31 :enumerations,
31 32 :issue_categories,
32 33 :custom_fields,
33 34 :custom_fields_trackers,
34 35 :boards,
35 36 :messages
36 37
37 38 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
38 39
39 40 def setup
40 41 ActionMailer::Base.deliveries.clear
41 42 end
42 43
43 44 def test_add_issue
44 45 # This email contains: 'Project: onlinestore'
45 46 issue = submit_email('ticket_on_given_project.eml')
46 47 assert issue.is_a?(Issue)
47 48 assert !issue.new_record?
48 49 issue.reload
49 50 assert_equal 'New ticket on a given project', issue.subject
50 51 assert_equal User.find_by_login('jsmith'), issue.author
51 52 assert_equal Project.find(2), issue.project
52 53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
53 54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
54 55 # keywords should be removed from the email body
55 56 assert !issue.description.match(/^Project:/i)
56 57 assert !issue.description.match(/^Status:/i)
57 58 end
58 59
59 60 def test_add_issue_with_status
60 61 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
61 62 issue = submit_email('ticket_on_given_project.eml')
62 63 assert issue.is_a?(Issue)
63 64 assert !issue.new_record?
64 65 issue.reload
65 66 assert_equal Project.find(2), issue.project
66 67 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
67 68 end
68 69
69 70 def test_add_issue_with_attributes_override
70 71 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
71 72 assert issue.is_a?(Issue)
72 73 assert !issue.new_record?
73 74 issue.reload
74 75 assert_equal 'New ticket on a given project', issue.subject
75 76 assert_equal User.find_by_login('jsmith'), issue.author
76 77 assert_equal Project.find(2), issue.project
77 78 assert_equal 'Feature request', issue.tracker.to_s
78 79 assert_equal 'Stock management', issue.category.to_s
79 80 assert_equal 'Urgent', issue.priority.to_s
80 81 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
81 82 end
82 83
83 84 def test_add_issue_with_partial_attributes_override
84 85 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
85 86 assert issue.is_a?(Issue)
86 87 assert !issue.new_record?
87 88 issue.reload
88 89 assert_equal 'New ticket on a given project', issue.subject
89 90 assert_equal User.find_by_login('jsmith'), issue.author
90 91 assert_equal Project.find(2), issue.project
91 92 assert_equal 'Feature request', issue.tracker.to_s
92 93 assert_nil issue.category
93 94 assert_equal 'High', issue.priority.to_s
94 95 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
95 96 end
96 97
97 98 def test_add_issue_with_attachment_to_specific_project
98 99 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
99 100 assert issue.is_a?(Issue)
100 101 assert !issue.new_record?
101 102 issue.reload
102 103 assert_equal 'Ticket created by email with attachment', issue.subject
103 104 assert_equal User.find_by_login('jsmith'), issue.author
104 105 assert_equal Project.find(2), issue.project
105 106 assert_equal 'This is a new ticket with attachments', issue.description
106 107 # Attachment properties
107 108 assert_equal 1, issue.attachments.size
108 109 assert_equal 'Paella.jpg', issue.attachments.first.filename
109 110 assert_equal 'image/jpeg', issue.attachments.first.content_type
110 111 assert_equal 10790, issue.attachments.first.filesize
111 112 end
112 113
113 114 def test_add_issue_with_custom_fields
114 115 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
115 116 assert issue.is_a?(Issue)
116 117 assert !issue.new_record?
117 118 issue.reload
118 119 assert_equal 'New ticket with custom field values', issue.subject
119 120 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
120 121 assert !issue.description.match(/^searchable field:/i)
121 122 end
122 123
123 124 def test_add_issue_with_cc
124 125 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
125 126 assert issue.is_a?(Issue)
126 127 assert !issue.new_record?
127 128 issue.reload
128 129 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
129 130 assert_equal 1, issue.watchers.size
130 131 end
131 132
132 133 def test_add_issue_without_from_header
133 134 Role.anonymous.add_permission!(:add_issues)
134 135 assert_equal false, submit_email('ticket_without_from_header.eml')
135 136 end
136 137
137 138 def test_add_issue_should_send_email_notification
138 139 ActionMailer::Base.deliveries.clear
139 140 # This email contains: 'Project: onlinestore'
140 141 issue = submit_email('ticket_on_given_project.eml')
141 142 assert issue.is_a?(Issue)
142 143 assert_equal 1, ActionMailer::Base.deliveries.size
143 144 end
144 145
145 146 def test_add_issue_note
146 147 journal = submit_email('ticket_reply.eml')
147 148 assert journal.is_a?(Journal)
148 149 assert_equal User.find_by_login('jsmith'), journal.user
149 150 assert_equal Issue.find(2), journal.journalized
150 151 assert_match /This is reply/, journal.notes
151 152 end
152 153
153 154 def test_add_issue_note_with_status_change
154 155 # This email contains: 'Status: Resolved'
155 156 journal = submit_email('ticket_reply_with_status.eml')
156 157 assert journal.is_a?(Journal)
157 158 issue = Issue.find(journal.issue.id)
158 159 assert_equal User.find_by_login('jsmith'), journal.user
159 160 assert_equal Issue.find(2), journal.journalized
160 161 assert_match /This is reply/, journal.notes
161 162 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
162 163 end
163 164
164 165 def test_add_issue_note_should_send_email_notification
165 166 ActionMailer::Base.deliveries.clear
166 167 journal = submit_email('ticket_reply.eml')
167 168 assert journal.is_a?(Journal)
168 169 assert_equal 1, ActionMailer::Base.deliveries.size
169 170 end
170 171
171 172 def test_reply_to_a_message
172 173 m = submit_email('message_reply.eml')
173 174 assert m.is_a?(Message)
174 175 assert !m.new_record?
175 176 m.reload
176 177 assert_equal 'Reply via email', m.subject
177 178 # The email replies to message #2 which is part of the thread of message #1
178 179 assert_equal Message.find(1), m.parent
179 180 end
180 181
181 182 def test_reply_to_a_message_by_subject
182 183 m = submit_email('message_reply_by_subject.eml')
183 184 assert m.is_a?(Message)
184 185 assert !m.new_record?
185 186 m.reload
186 187 assert_equal 'Reply to the first post', m.subject
187 188 assert_equal Message.find(1), m.parent
188 189 end
189 190
190 191 def test_should_strip_tags_of_html_only_emails
191 192 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
192 193 assert issue.is_a?(Issue)
193 194 assert !issue.new_record?
194 195 issue.reload
195 196 assert_equal 'HTML email', issue.subject
196 197 assert_equal 'This is a html-only email.', issue.description
197 198 end
198 199
199 200 private
200 201
201 202 def submit_email(filename, options={})
202 203 raw = IO.read(File.join(FIXTURES_PATH, filename))
203 204 MailHandler.receive(raw, options)
204 205 end
205 206 end
@@ -1,241 +1,241
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class MailerTest < Test::Unit::TestCase
21 21 include Redmine::I18n
22 fixtures :projects, :issues, :users, :members, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
22 fixtures :projects, :issues, :users, :members, :member_roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
23 23
24 24 def test_generated_links_in_emails
25 25 ActionMailer::Base.deliveries.clear
26 26 Setting.host_name = 'mydomain.foo'
27 27 Setting.protocol = 'https'
28 28
29 29 journal = Journal.find(2)
30 30 assert Mailer.deliver_issue_edit(journal)
31 31
32 32 mail = ActionMailer::Base.deliveries.last
33 33 assert_kind_of TMail::Mail, mail
34 34 # link to the main ticket
35 35 assert mail.body.include?('<a href="https://mydomain.foo/issues/1">Bug #1: Can\'t print recipes</a>')
36 36
37 37 # link to a referenced ticket
38 38 assert mail.body.include?('<a href="https://mydomain.foo/issues/2" class="issue" title="Add ingredients categories (Assigned)">#2</a>')
39 39 # link to a changeset
40 40 assert mail.body.include?('<a href="https://mydomain.foo/projects/ecookbook/repository/revisions/2" class="changeset" title="This commit fixes #1, #2 and references #1 &amp; #3">r2</a>')
41 41 end
42 42
43 43 def test_generated_links_with_prefix
44 44 relative_url_root = Redmine::Utils.relative_url_root
45 45 ActionMailer::Base.deliveries.clear
46 46 Setting.host_name = 'mydomain.foo/rdm'
47 47 Setting.protocol = 'http'
48 48 Redmine::Utils.relative_url_root = '/rdm'
49 49
50 50 journal = Journal.find(2)
51 51 assert Mailer.deliver_issue_edit(journal)
52 52
53 53 mail = ActionMailer::Base.deliveries.last
54 54 assert_kind_of TMail::Mail, mail
55 55 # link to the main ticket
56 56 assert mail.body.include?('<a href="http://mydomain.foo/rdm/issues/1">Bug #1: Can\'t print recipes</a>')
57 57
58 58 # link to a referenced ticket
59 59 assert mail.body.include?('<a href="http://mydomain.foo/rdm/issues/2" class="issue" title="Add ingredients categories (Assigned)">#2</a>')
60 60 # link to a changeset
61 61 assert mail.body.include?('<a href="http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2" class="changeset" title="This commit fixes #1, #2 and references #1 &amp; #3">r2</a>')
62 62 ensure
63 63 # restore it
64 64 Redmine::Utils.relative_url_root = relative_url_root
65 65 end
66 66
67 67 def test_generated_links_with_prefix_and_no_relative_url_root
68 68 relative_url_root = Redmine::Utils.relative_url_root
69 69 ActionMailer::Base.deliveries.clear
70 70 Setting.host_name = 'mydomain.foo/rdm'
71 71 Setting.protocol = 'http'
72 72 Redmine::Utils.relative_url_root = nil
73 73
74 74 journal = Journal.find(2)
75 75 assert Mailer.deliver_issue_edit(journal)
76 76
77 77 mail = ActionMailer::Base.deliveries.last
78 78 assert_kind_of TMail::Mail, mail
79 79 # link to the main ticket
80 80 assert mail.body.include?('<a href="http://mydomain.foo/rdm/issues/1">Bug #1: Can\'t print recipes</a>')
81 81
82 82 # link to a referenced ticket
83 83 assert mail.body.include?('<a href="http://mydomain.foo/rdm/issues/2" class="issue" title="Add ingredients categories (Assigned)">#2</a>')
84 84 # link to a changeset
85 85 assert mail.body.include?('<a href="http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2" class="changeset" title="This commit fixes #1, #2 and references #1 &amp; #3">r2</a>')
86 86 ensure
87 87 # restore it
88 88 Redmine::Utils.relative_url_root = relative_url_root
89 89 end
90 90
91 91 def test_email_headers
92 92 ActionMailer::Base.deliveries.clear
93 93 issue = Issue.find(1)
94 94 Mailer.deliver_issue_add(issue)
95 95 mail = ActionMailer::Base.deliveries.last
96 96 assert_not_nil mail
97 97 assert_equal 'bulk', mail.header_string('Precedence')
98 98 assert_equal 'auto-generated', mail.header_string('Auto-Submitted')
99 99 end
100 100
101 101 def test_plain_text_mail
102 102 Setting.plain_text_mail = 1
103 103 journal = Journal.find(2)
104 104 Mailer.deliver_issue_edit(journal)
105 105 mail = ActionMailer::Base.deliveries.last
106 106 assert !mail.body.include?('<a href="https://mydomain.foo/issues/1">Bug #1: Can\'t print recipes</a>')
107 107 end
108 108
109 109 def test_issue_add_message_id
110 110 ActionMailer::Base.deliveries.clear
111 111 issue = Issue.find(1)
112 112 Mailer.deliver_issue_add(issue)
113 113 mail = ActionMailer::Base.deliveries.last
114 114 assert_not_nil mail
115 115 assert_equal Mailer.message_id_for(issue), mail.message_id
116 116 assert_nil mail.references
117 117 end
118 118
119 119 def test_issue_edit_message_id
120 120 ActionMailer::Base.deliveries.clear
121 121 journal = Journal.find(1)
122 122 Mailer.deliver_issue_edit(journal)
123 123 mail = ActionMailer::Base.deliveries.last
124 124 assert_not_nil mail
125 125 assert_equal Mailer.message_id_for(journal), mail.message_id
126 126 assert_equal Mailer.message_id_for(journal.issue), mail.references.to_s
127 127 end
128 128
129 129 def test_message_posted_message_id
130 130 ActionMailer::Base.deliveries.clear
131 131 message = Message.find(1)
132 132 Mailer.deliver_message_posted(message, message.author.mail)
133 133 mail = ActionMailer::Base.deliveries.last
134 134 assert_not_nil mail
135 135 assert_equal Mailer.message_id_for(message), mail.message_id
136 136 assert_nil mail.references
137 137 end
138 138
139 139 def test_reply_posted_message_id
140 140 ActionMailer::Base.deliveries.clear
141 141 message = Message.find(3)
142 142 Mailer.deliver_message_posted(message, message.author.mail)
143 143 mail = ActionMailer::Base.deliveries.last
144 144 assert_not_nil mail
145 145 assert_equal Mailer.message_id_for(message), mail.message_id
146 146 assert_equal Mailer.message_id_for(message.parent), mail.references.to_s
147 147 end
148 148
149 149 # test mailer methods for each language
150 150 def test_issue_add
151 151 issue = Issue.find(1)
152 152 valid_languages.each do |lang|
153 153 Setting.default_language = lang.to_s
154 154 assert Mailer.deliver_issue_add(issue)
155 155 end
156 156 end
157 157
158 158 def test_issue_edit
159 159 journal = Journal.find(1)
160 160 valid_languages.each do |lang|
161 161 Setting.default_language = lang.to_s
162 162 assert Mailer.deliver_issue_edit(journal)
163 163 end
164 164 end
165 165
166 166 def test_document_added
167 167 document = Document.find(1)
168 168 valid_languages.each do |lang|
169 169 Setting.default_language = lang.to_s
170 170 assert Mailer.deliver_document_added(document)
171 171 end
172 172 end
173 173
174 174 def test_attachments_added
175 175 attachements = [ Attachment.find_by_container_type('Document') ]
176 176 valid_languages.each do |lang|
177 177 Setting.default_language = lang.to_s
178 178 assert Mailer.deliver_attachments_added(attachements)
179 179 end
180 180 end
181 181
182 182 def test_news_added
183 183 news = News.find(:first)
184 184 valid_languages.each do |lang|
185 185 Setting.default_language = lang.to_s
186 186 assert Mailer.deliver_news_added(news)
187 187 end
188 188 end
189 189
190 190 def test_message_posted
191 191 message = Message.find(:first)
192 192 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
193 193 recipients = recipients.compact.uniq
194 194 valid_languages.each do |lang|
195 195 Setting.default_language = lang.to_s
196 196 assert Mailer.deliver_message_posted(message, recipients)
197 197 end
198 198 end
199 199
200 200 def test_account_information
201 201 user = User.find(:first)
202 202 valid_languages.each do |lang|
203 203 user.update_attribute :language, lang.to_s
204 204 user.reload
205 205 assert Mailer.deliver_account_information(user, 'pAsswORd')
206 206 end
207 207 end
208 208
209 209 def test_lost_password
210 210 token = Token.find(2)
211 211 valid_languages.each do |lang|
212 212 token.user.update_attribute :language, lang.to_s
213 213 token.reload
214 214 assert Mailer.deliver_lost_password(token)
215 215 end
216 216 end
217 217
218 218 def test_register
219 219 token = Token.find(1)
220 220 Setting.host_name = 'redmine.foo'
221 221 Setting.protocol = 'https'
222 222
223 223 valid_languages.each do |lang|
224 224 token.user.update_attribute :language, lang.to_s
225 225 token.reload
226 226 ActionMailer::Base.deliveries.clear
227 227 assert Mailer.deliver_register(token)
228 228 mail = ActionMailer::Base.deliveries.last
229 229 assert mail.body.include?("https://redmine.foo/account/activate?token=#{token.value}")
230 230 end
231 231 end
232 232
233 233 def test_reminders
234 234 ActionMailer::Base.deliveries.clear
235 235 Mailer.reminders(:days => 42)
236 236 assert_equal 1, ActionMailer::Base.deliveries.size
237 237 mail = ActionMailer::Base.deliveries.last
238 238 assert mail.bcc.include?('dlopper@somenet.foo')
239 239 assert mail.body.include?('Bug #3: Error 281 when updating a recipe')
240 240 end
241 241 end
@@ -1,51 +1,71
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class MemberTest < Test::Unit::TestCase
21 fixtures :users, :projects, :roles, :members
21 fixtures :users, :projects, :roles, :members, :member_roles
22 22
23 23 def setup
24 24 @jsmith = Member.find(1)
25 25 end
26 26
27 27 def test_create
28 member = Member.new(:project_id => 1, :user_id => 4, :role_id => 1)
29 assert member.save
28 member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])
29 assert member.save
30 member.reload
31
32 assert_equal 2, member.roles.size
33 assert_equal Role.find(1), member.roles.sort.first
30 34 end
31 35
32 36 def test_update
33 37 assert_equal "eCookbook", @jsmith.project.name
34 assert_equal "Manager", @jsmith.role.name
38 assert_equal "Manager", @jsmith.roles.first.name
35 39 assert_equal "jsmith", @jsmith.user.login
36 40
37 @jsmith.role = Role.find(2)
41 @jsmith.mail_notification = !@jsmith.mail_notification
38 42 assert @jsmith.save
39 43 end
44
45 def test_update_roles
46 assert_equal 1, @jsmith.roles.size
47 @jsmith.role_ids = [1, 2]
48 assert @jsmith.save
49 assert_equal 2, @jsmith.reload.roles.size
50 end
40 51
41 52 def test_validate
42 member = Member.new(:project_id => 1, :user_id => 2, :role_id =>2)
43 # same use can't have more than one role for a project
53 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2])
54 # same use can't have more than one membership for a project
55 assert !member.save
56
57 member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [])
58 # must have one role at least
44 59 assert !member.save
45 60 end
46 61
47 def test_destroy
48 @jsmith.destroy
62 def test_destroy
63 assert_difference 'Member.count', -1 do
64 assert_difference 'MemberRole.count', -1 do
65 @jsmith.destroy
66 end
67 end
68
49 69 assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) }
50 70 end
51 71 end
@@ -1,131 +1,131
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class MessageTest < Test::Unit::TestCase
21 fixtures :projects, :roles, :members, :boards, :messages, :users, :watchers
21 fixtures :projects, :roles, :members, :member_roles, :boards, :messages, :users, :watchers
22 22
23 23 def setup
24 24 @board = Board.find(1)
25 25 @user = User.find(1)
26 26 end
27 27
28 28 def test_create
29 29 topics_count = @board.topics_count
30 30 messages_count = @board.messages_count
31 31
32 32 message = Message.new(:board => @board, :subject => 'Test message', :content => 'Test message content', :author => @user)
33 33 assert message.save
34 34 @board.reload
35 35 # topics count incremented
36 36 assert_equal topics_count+1, @board[:topics_count]
37 37 # messages count incremented
38 38 assert_equal messages_count+1, @board[:messages_count]
39 39 assert_equal message, @board.last_message
40 40 # author should be watching the message
41 41 assert message.watched_by?(@user)
42 42 end
43 43
44 44 def test_reply
45 45 topics_count = @board.topics_count
46 46 messages_count = @board.messages_count
47 47 @message = Message.find(1)
48 48 replies_count = @message.replies_count
49 49
50 50 reply_author = User.find(2)
51 51 reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', :parent => @message, :author => reply_author)
52 52 assert reply.save
53 53 @board.reload
54 54 # same topics count
55 55 assert_equal topics_count, @board[:topics_count]
56 56 # messages count incremented
57 57 assert_equal messages_count+1, @board[:messages_count]
58 58 assert_equal reply, @board.last_message
59 59 @message.reload
60 60 # replies count incremented
61 61 assert_equal replies_count+1, @message[:replies_count]
62 62 assert_equal reply, @message.last_reply
63 63 # author should be watching the message
64 64 assert @message.watched_by?(reply_author)
65 65 end
66 66
67 67 def test_moving_message_should_update_counters
68 68 @message = Message.find(1)
69 69 assert_no_difference 'Message.count' do
70 70 # Previous board
71 71 assert_difference 'Board.find(1).topics_count', -1 do
72 72 assert_difference 'Board.find(1).messages_count', -(1 + @message.replies_count) do
73 73 # New board
74 74 assert_difference 'Board.find(2).topics_count' do
75 75 assert_difference 'Board.find(2).messages_count', (1 + @message.replies_count) do
76 76 @message.update_attributes(:board_id => 2)
77 77 end
78 78 end
79 79 end
80 80 end
81 81 end
82 82 end
83 83
84 84 def test_destroy_topic
85 85 message = Message.find(1)
86 86 board = message.board
87 87 topics_count, messages_count = board.topics_count, board.messages_count
88 88
89 89 assert_difference('Watcher.count', -1) do
90 90 assert message.destroy
91 91 end
92 92 board.reload
93 93
94 94 # Replies deleted
95 95 assert Message.find_all_by_parent_id(1).empty?
96 96 # Checks counters
97 97 assert_equal topics_count - 1, board.topics_count
98 98 assert_equal messages_count - 3, board.messages_count
99 99 # Watchers removed
100 100 end
101 101
102 102 def test_destroy_reply
103 103 message = Message.find(5)
104 104 board = message.board
105 105 topics_count, messages_count = board.topics_count, board.messages_count
106 106 assert message.destroy
107 107 board.reload
108 108
109 109 # Checks counters
110 110 assert_equal topics_count, board.topics_count
111 111 assert_equal messages_count - 1, board.messages_count
112 112 end
113 113
114 114 def test_editable_by
115 115 message = Message.find(6)
116 116 author = message.author
117 117 assert message.editable_by?(author)
118 118
119 author.role_for_project(message.project).remove_permission!(:edit_own_messages)
119 author.roles_for_project(message.project).first.remove_permission!(:edit_own_messages)
120 120 assert !message.reload.editable_by?(author.reload)
121 121 end
122 122
123 123 def test_destroyable_by
124 124 message = Message.find(6)
125 125 author = message.author
126 126 assert message.destroyable_by?(author)
127 127
128 author.role_for_project(message.project).remove_permission!(:delete_own_messages)
128 author.roles_for_project(message.project).first.remove_permission!(:delete_own_messages)
129 129 assert !message.reload.destroyable_by?(author.reload)
130 130 end
131 131 end
@@ -1,77 +1,77
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class NewsTest < Test::Unit::TestCase
21 fixtures :projects, :users, :roles, :members, :enabled_modules, :news
21 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news
22 22
23 23 def valid_news
24 24 { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) }
25 25 end
26 26
27 27
28 28 def setup
29 29 end
30 30
31 31 def test_create_should_send_email_notification
32 32 ActionMailer::Base.deliveries.clear
33 33 Setting.notified_events << 'news_added'
34 34 news = Project.find(:first).news.new(valid_news)
35 35
36 36 assert news.save
37 37 assert_equal 1, ActionMailer::Base.deliveries.size
38 38 end
39 39
40 40 def test_should_include_news_for_projects_with_news_enabled
41 41 project = projects(:projects_001)
42 42 assert project.enabled_modules.any?{ |em| em.name == 'news' }
43 43
44 44 # News.latest should return news from projects_001
45 45 assert News.latest.any? { |news| news.project == project }
46 46 end
47 47
48 48 def test_should_not_include_news_for_projects_with_news_disabled
49 49 # The projects_002 (OnlineStore) doesn't have the news module enabled, use that project for this test
50 50 project = projects(:projects_002)
51 51 assert ! project.enabled_modules.any?{ |em| em.name == 'news' }
52 52
53 53 # Add a piece of news to the project
54 54 news = project.news.create(valid_news)
55 55
56 56 # News.latest should not return that new piece of news
57 57 assert News.latest.include?(news) == false
58 58 end
59 59
60 60 def test_should_only_include_news_from_projects_visibly_to_the_user
61 61 # users_001 has no memberships so can only get news from public project
62 62 assert News.latest(users(:users_001)).all? { |news| news.project.is_public? }
63 63 end
64 64
65 65 def test_should_limit_the_amount_of_returned_news
66 66 # Make sure we have a bunch of news stories
67 67 10.times { projects(:projects_001).news.create(valid_news) }
68 68 assert_equal 2, News.latest(users(:users_002), 2).size
69 69 assert_equal 6, News.latest(users(:users_002), 6).size
70 70 end
71 71
72 72 def test_should_return_5_news_stories_by_default
73 73 # Make sure we have a bunch of news stories
74 74 10.times { projects(:projects_001).news.create(valid_news) }
75 75 assert_equal 5, News.latest(users(:users_004)).size
76 76 end
77 77 end
@@ -1,320 +1,320
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ProjectTest < Test::Unit::TestCase
21 21 fixtures :projects, :enabled_modules,
22 22 :issues, :issue_statuses, :journals, :journal_details,
23 :users, :members, :roles, :projects_trackers, :trackers, :boards,
24 :queries
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 :queries
25 25
26 26 def setup
27 27 @ecookbook = Project.find(1)
28 28 @ecookbook_sub1 = Project.find(3)
29 29 end
30 30
31 31 def test_truth
32 32 assert_kind_of Project, @ecookbook
33 33 assert_equal "eCookbook", @ecookbook.name
34 34 end
35 35
36 36 def test_update
37 37 assert_equal "eCookbook", @ecookbook.name
38 38 @ecookbook.name = "eCook"
39 39 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
40 40 @ecookbook.reload
41 41 assert_equal "eCook", @ecookbook.name
42 42 end
43 43
44 44 def test_validate
45 45 @ecookbook.name = ""
46 46 assert !@ecookbook.save
47 47 assert_equal 1, @ecookbook.errors.count
48 48 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
49 49 end
50 50
51 51 def test_archive
52 52 user = @ecookbook.members.first.user
53 53 @ecookbook.archive
54 54 @ecookbook.reload
55 55
56 56 assert !@ecookbook.active?
57 57 assert !user.projects.include?(@ecookbook)
58 58 # Subproject are also archived
59 59 assert !@ecookbook.children.empty?
60 60 assert @ecookbook.descendants.active.empty?
61 61 end
62 62
63 63 def test_unarchive
64 64 user = @ecookbook.members.first.user
65 65 @ecookbook.archive
66 66 # A subproject of an archived project can not be unarchived
67 67 assert !@ecookbook_sub1.unarchive
68 68
69 69 # Unarchive project
70 70 assert @ecookbook.unarchive
71 71 @ecookbook.reload
72 72 assert @ecookbook.active?
73 73 assert user.projects.include?(@ecookbook)
74 74 # Subproject can now be unarchived
75 75 @ecookbook_sub1.reload
76 76 assert @ecookbook_sub1.unarchive
77 77 end
78 78
79 79 def test_destroy
80 80 # 2 active members
81 81 assert_equal 2, @ecookbook.members.size
82 82 # and 1 is locked
83 83 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
84 84 # some boards
85 85 assert @ecookbook.boards.any?
86 86
87 87 @ecookbook.destroy
88 88 # make sure that the project non longer exists
89 89 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
90 90 # make sure related data was removed
91 91 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
92 92 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
93 93 end
94 94
95 95 def test_move_an_orphan_project_to_a_root_project
96 96 sub = Project.find(2)
97 97 sub.set_parent! @ecookbook
98 98 assert_equal @ecookbook.id, sub.parent.id
99 99 @ecookbook.reload
100 100 assert_equal 4, @ecookbook.children.size
101 101 end
102 102
103 103 def test_move_an_orphan_project_to_a_subproject
104 104 sub = Project.find(2)
105 105 assert sub.set_parent!(@ecookbook_sub1)
106 106 end
107 107
108 108 def test_move_a_root_project_to_a_project
109 109 sub = @ecookbook
110 110 assert sub.set_parent!(Project.find(2))
111 111 end
112 112
113 113 def test_should_not_move_a_project_to_its_children
114 114 sub = @ecookbook
115 115 assert !(sub.set_parent!(Project.find(3)))
116 116 end
117 117
118 118 def test_set_parent_should_add_roots_in_alphabetical_order
119 119 ProjectCustomField.delete_all
120 120 Project.delete_all
121 121 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
122 122 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
123 123 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
124 124 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
125 125
126 126 assert_equal 4, Project.count
127 127 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
128 128 end
129 129
130 130 def test_set_parent_should_add_children_in_alphabetical_order
131 131 ProjectCustomField.delete_all
132 132 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
133 133 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
134 134 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
135 135 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
136 136 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
137 137
138 138 parent.reload
139 139 assert_equal 4, parent.children.size
140 140 assert_equal parent.children.sort_by(&:name), parent.children
141 141 end
142 142
143 143 def test_rebuild_should_sort_children_alphabetically
144 144 ProjectCustomField.delete_all
145 145 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
146 146 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
147 147 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
148 148 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
149 149 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
150 150
151 151 Project.update_all("lft = NULL, rgt = NULL")
152 152 Project.rebuild!
153 153
154 154 parent.reload
155 155 assert_equal 4, parent.children.size
156 156 assert_equal parent.children.sort_by(&:name), parent.children
157 157 end
158 158
159 159 def test_parent
160 160 p = Project.find(6).parent
161 161 assert p.is_a?(Project)
162 162 assert_equal 5, p.id
163 163 end
164 164
165 165 def test_ancestors
166 166 a = Project.find(6).ancestors
167 167 assert a.first.is_a?(Project)
168 168 assert_equal [1, 5], a.collect(&:id)
169 169 end
170 170
171 171 def test_root
172 172 r = Project.find(6).root
173 173 assert r.is_a?(Project)
174 174 assert_equal 1, r.id
175 175 end
176 176
177 177 def test_children
178 178 c = Project.find(1).children
179 179 assert c.first.is_a?(Project)
180 180 assert_equal [5, 3, 4], c.collect(&:id)
181 181 end
182 182
183 183 def test_descendants
184 184 d = Project.find(1).descendants
185 185 assert d.first.is_a?(Project)
186 186 assert_equal [5, 6, 3, 4], d.collect(&:id)
187 187 end
188 188
189 189 def test_rolled_up_trackers
190 190 parent = Project.find(1)
191 191 parent.trackers = Tracker.find([1,2])
192 192 child = parent.children.find(3)
193 193
194 194 assert_equal [1, 2], parent.tracker_ids
195 195 assert_equal [2, 3], child.trackers.collect(&:id)
196 196
197 197 assert_kind_of Tracker, parent.rolled_up_trackers.first
198 198 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
199 199
200 200 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
201 201 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
202 202 end
203 203
204 204 def test_rolled_up_trackers_should_ignore_archived_subprojects
205 205 parent = Project.find(1)
206 206 parent.trackers = Tracker.find([1,2])
207 207 child = parent.children.find(3)
208 208 child.trackers = Tracker.find([1,3])
209 209 parent.children.each(&:archive)
210 210
211 211 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
212 212 end
213 213
214 214 def test_next_identifier
215 215 ProjectCustomField.delete_all
216 216 Project.create!(:name => 'last', :identifier => 'p2008040')
217 217 assert_equal 'p2008041', Project.next_identifier
218 218 end
219 219
220 220 def test_next_identifier_first_project
221 221 Project.delete_all
222 222 assert_nil Project.next_identifier
223 223 end
224 224
225 225
226 226 def test_enabled_module_names_should_not_recreate_enabled_modules
227 227 project = Project.find(1)
228 228 # Remove one module
229 229 modules = project.enabled_modules.slice(0..-2)
230 230 assert modules.any?
231 231 assert_difference 'EnabledModule.count', -1 do
232 232 project.enabled_module_names = modules.collect(&:name)
233 233 end
234 234 project.reload
235 235 # Ids should be preserved
236 236 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
237 237 end
238 238
239 239 def test_copy_from_existing_project
240 240 source_project = Project.find(1)
241 241 copied_project = Project.copy_from(1)
242 242
243 243 assert copied_project
244 244 # Cleared attributes
245 245 assert copied_project.id.blank?
246 246 assert copied_project.name.blank?
247 247 assert copied_project.identifier.blank?
248 248
249 249 # Duplicated attributes
250 250 assert_equal source_project.description, copied_project.description
251 251 assert_equal source_project.enabled_modules, copied_project.enabled_modules
252 252 assert_equal source_project.trackers, copied_project.trackers
253 253
254 254 # Default attributes
255 255 assert_equal 1, copied_project.status
256 256 end
257 257
258 258 # Context: Project#copy
259 259 def test_copy_should_copy_issues
260 260 # Setup
261 261 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
262 262 source_project = Project.find(2)
263 263 Project.destroy_all :identifier => "copy-test"
264 264 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
265 265 project.trackers = source_project.trackers
266 266 assert project.valid?
267 267
268 268 assert project.issues.empty?
269 269 assert project.copy(source_project)
270 270
271 271 # Tests
272 272 assert_equal source_project.issues.size, project.issues.size
273 273 project.issues.each do |issue|
274 274 assert issue.valid?
275 275 assert ! issue.assigned_to.blank?
276 276 assert_equal project, issue.project
277 277 end
278 278 end
279 279
280 280 def test_copy_should_copy_members
281 281 # Setup
282 282 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
283 283 source_project = Project.find(2)
284 284 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
285 285 project.trackers = source_project.trackers
286 286 project.enabled_modules = source_project.enabled_modules
287 287 assert project.valid?
288 288
289 289 assert project.members.empty?
290 290 assert project.copy(source_project)
291 291
292 292 # Tests
293 293 assert_equal source_project.members.size, project.members.size
294 294 project.members.each do |member|
295 295 assert member
296 296 assert_equal project, member.project
297 297 end
298 298 end
299 299
300 300 def test_copy_should_copy_project_level_queries
301 301 # Setup
302 302 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
303 303 source_project = Project.find(2)
304 304 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
305 305 project.trackers = source_project.trackers
306 306 project.enabled_modules = source_project.enabled_modules
307 307 assert project.valid?
308 308
309 309 assert project.queries.empty?
310 310 assert project.copy(source_project)
311 311
312 312 # Tests
313 313 assert_equal source_project.queries.size, project.queries.size
314 314 project.queries.each do |query|
315 315 assert query
316 316 assert_equal project, query.project
317 317 end
318 318 end
319 319
320 320 end
@@ -1,299 +1,299
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class QueryTest < Test::Unit::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22 22
23 23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 24 query = Query.new(:project => nil, :name => '_')
25 25 assert query.available_filters.has_key?('cf_1')
26 26 assert !query.available_filters.has_key?('cf_3')
27 27 end
28 28
29 29 def find_issues_with_query(query)
30 30 Issue.find :all,
31 31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
32 32 :conditions => query.statement
33 33 end
34 34
35 35 def test_query_with_multiple_custom_fields
36 36 query = Query.find(1)
37 37 assert query.valid?
38 38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
39 39 issues = find_issues_with_query(query)
40 40 assert_equal 1, issues.length
41 41 assert_equal Issue.find(3), issues.first
42 42 end
43 43
44 44 def test_operator_none
45 45 query = Query.new(:project => Project.find(1), :name => '_')
46 46 query.add_filter('fixed_version_id', '!*', [''])
47 47 query.add_filter('cf_1', '!*', [''])
48 48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
49 49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
50 50 find_issues_with_query(query)
51 51 end
52 52
53 53 def test_operator_none_for_integer
54 54 query = Query.new(:project => Project.find(1), :name => '_')
55 55 query.add_filter('estimated_hours', '!*', [''])
56 56 issues = find_issues_with_query(query)
57 57 assert !issues.empty?
58 58 assert issues.all? {|i| !i.estimated_hours}
59 59 end
60 60
61 61 def test_operator_all
62 62 query = Query.new(:project => Project.find(1), :name => '_')
63 63 query.add_filter('fixed_version_id', '*', [''])
64 64 query.add_filter('cf_1', '*', [''])
65 65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
66 66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
67 67 find_issues_with_query(query)
68 68 end
69 69
70 70 def test_operator_greater_than
71 71 query = Query.new(:project => Project.find(1), :name => '_')
72 72 query.add_filter('done_ratio', '>=', ['40'])
73 73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
74 74 find_issues_with_query(query)
75 75 end
76 76
77 77 def test_operator_in_more_than
78 78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
79 79 query = Query.new(:project => Project.find(1), :name => '_')
80 80 query.add_filter('due_date', '>t+', ['15'])
81 81 issues = find_issues_with_query(query)
82 82 assert !issues.empty?
83 83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
84 84 end
85 85
86 86 def test_operator_in_less_than
87 87 query = Query.new(:project => Project.find(1), :name => '_')
88 88 query.add_filter('due_date', '<t+', ['15'])
89 89 issues = find_issues_with_query(query)
90 90 assert !issues.empty?
91 91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
92 92 end
93 93
94 94 def test_operator_less_than_ago
95 95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
96 96 query = Query.new(:project => Project.find(1), :name => '_')
97 97 query.add_filter('due_date', '>t-', ['3'])
98 98 issues = find_issues_with_query(query)
99 99 assert !issues.empty?
100 100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
101 101 end
102 102
103 103 def test_operator_more_than_ago
104 104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
105 105 query = Query.new(:project => Project.find(1), :name => '_')
106 106 query.add_filter('due_date', '<t-', ['10'])
107 107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
108 108 issues = find_issues_with_query(query)
109 109 assert !issues.empty?
110 110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
111 111 end
112 112
113 113 def test_operator_in
114 114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
115 115 query = Query.new(:project => Project.find(1), :name => '_')
116 116 query.add_filter('due_date', 't+', ['2'])
117 117 issues = find_issues_with_query(query)
118 118 assert !issues.empty?
119 119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
120 120 end
121 121
122 122 def test_operator_ago
123 123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
124 124 query = Query.new(:project => Project.find(1), :name => '_')
125 125 query.add_filter('due_date', 't-', ['3'])
126 126 issues = find_issues_with_query(query)
127 127 assert !issues.empty?
128 128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
129 129 end
130 130
131 131 def test_operator_today
132 132 query = Query.new(:project => Project.find(1), :name => '_')
133 133 query.add_filter('due_date', 't', [''])
134 134 issues = find_issues_with_query(query)
135 135 assert !issues.empty?
136 136 issues.each {|issue| assert_equal Date.today, issue.due_date}
137 137 end
138 138
139 139 def test_operator_this_week_on_date
140 140 query = Query.new(:project => Project.find(1), :name => '_')
141 141 query.add_filter('due_date', 'w', [''])
142 142 find_issues_with_query(query)
143 143 end
144 144
145 145 def test_operator_this_week_on_datetime
146 146 query = Query.new(:project => Project.find(1), :name => '_')
147 147 query.add_filter('created_on', 'w', [''])
148 148 find_issues_with_query(query)
149 149 end
150 150
151 151 def test_operator_contains
152 152 query = Query.new(:project => Project.find(1), :name => '_')
153 153 query.add_filter('subject', '~', ['string'])
154 154 assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'")
155 155 find_issues_with_query(query)
156 156 end
157 157
158 158 def test_operator_does_not_contains
159 159 query = Query.new(:project => Project.find(1), :name => '_')
160 160 query.add_filter('subject', '!~', ['string'])
161 161 assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'")
162 162 find_issues_with_query(query)
163 163 end
164 164
165 165 def test_filter_watched_issues
166 166 User.current = User.find(1)
167 167 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
168 168 result = find_issues_with_query(query)
169 169 assert_not_nil result
170 170 assert !result.empty?
171 171 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
172 172 User.current = nil
173 173 end
174 174
175 175 def test_filter_unwatched_issues
176 176 User.current = User.find(1)
177 177 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
178 178 result = find_issues_with_query(query)
179 179 assert_not_nil result
180 180 assert !result.empty?
181 181 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
182 182 User.current = nil
183 183 end
184 184
185 185 def test_default_columns
186 186 q = Query.new
187 187 assert !q.columns.empty?
188 188 end
189 189
190 190 def test_set_column_names
191 191 q = Query.new
192 192 q.column_names = ['tracker', :subject, '', 'unknonw_column']
193 193 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
194 194 c = q.columns.first
195 195 assert q.has_column?(c)
196 196 end
197 197
198 198 def test_default_sort
199 199 q = Query.new
200 200 assert_equal [], q.sort_criteria
201 201 end
202 202
203 203 def test_set_sort_criteria_with_hash
204 204 q = Query.new
205 205 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
206 206 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
207 207 end
208 208
209 209 def test_set_sort_criteria_with_array
210 210 q = Query.new
211 211 q.sort_criteria = [['priority', 'desc'], 'tracker']
212 212 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
213 213 end
214 214
215 215 def test_create_query_with_sort
216 216 q = Query.new(:name => 'Sorted')
217 217 q.sort_criteria = [['priority', 'desc'], 'tracker']
218 218 assert q.save
219 219 q.reload
220 220 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
221 221 end
222 222
223 223 def test_sort_by_string_custom_field_asc
224 224 q = Query.new
225 225 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
226 226 assert c
227 227 assert c.sortable
228 228 issues = Issue.find :all,
229 229 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
230 230 :conditions => q.statement,
231 231 :order => "#{c.sortable} ASC"
232 232 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
233 233 assert !values.empty?
234 234 assert_equal values.sort, values
235 235 end
236 236
237 237 def test_sort_by_string_custom_field_desc
238 238 q = Query.new
239 239 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
240 240 assert c
241 241 assert c.sortable
242 242 issues = Issue.find :all,
243 243 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
244 244 :conditions => q.statement,
245 245 :order => "#{c.sortable} DESC"
246 246 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
247 247 assert !values.empty?
248 248 assert_equal values.sort.reverse, values
249 249 end
250 250
251 251 def test_sort_by_float_custom_field_asc
252 252 q = Query.new
253 253 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
254 254 assert c
255 255 assert c.sortable
256 256 issues = Issue.find :all,
257 257 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
258 258 :conditions => q.statement,
259 259 :order => "#{c.sortable} ASC"
260 260 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
261 261 assert !values.empty?
262 262 assert_equal values.sort, values
263 263 end
264 264
265 265 def test_label_for
266 266 q = Query.new
267 267 assert_equal 'assigned_to', q.label_for('assigned_to_id')
268 268 end
269 269
270 270 def test_editable_by
271 271 admin = User.find(1)
272 272 manager = User.find(2)
273 273 developer = User.find(3)
274 274
275 275 # Public query on project 1
276 276 q = Query.find(1)
277 277 assert q.editable_by?(admin)
278 278 assert q.editable_by?(manager)
279 279 assert !q.editable_by?(developer)
280 280
281 281 # Private query on project 1
282 282 q = Query.find(2)
283 283 assert q.editable_by?(admin)
284 284 assert !q.editable_by?(manager)
285 285 assert q.editable_by?(developer)
286 286
287 287 # Private query for all projects
288 288 q = Query.find(3)
289 289 assert q.editable_by?(admin)
290 290 assert !q.editable_by?(manager)
291 291 assert q.editable_by?(developer)
292 292
293 293 # Public query for all projects
294 294 q = Query.find(4)
295 295 assert q.editable_by?(admin)
296 296 assert !q.editable_by?(manager)
297 297 assert !q.editable_by?(developer)
298 298 end
299 299 end
@@ -1,143 +1,144
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class SearchTest < Test::Unit::TestCase
21 21 fixtures :users,
22 :members,
22 :members,
23 :member_roles,
23 24 :projects,
24 25 :roles,
25 26 :enabled_modules,
26 27 :issues,
27 28 :trackers,
28 29 :journals,
29 30 :journal_details,
30 31 :repositories,
31 32 :changesets
32 33
33 34 def setup
34 35 @project = Project.find(1)
35 36 @issue_keyword = '%unable to print recipes%'
36 37 @issue = Issue.find(1)
37 38 @changeset_keyword = '%very first commit%'
38 39 @changeset = Changeset.find(100)
39 40 end
40 41
41 42 def test_search_by_anonymous
42 43 User.current = nil
43 44
44 45 r = Issue.search(@issue_keyword).first
45 46 assert r.include?(@issue)
46 47 r = Changeset.search(@changeset_keyword).first
47 48 assert r.include?(@changeset)
48 49
49 50 # Removes the :view_changesets permission from Anonymous role
50 51 remove_permission Role.anonymous, :view_changesets
51 52
52 53 r = Issue.search(@issue_keyword).first
53 54 assert r.include?(@issue)
54 55 r = Changeset.search(@changeset_keyword).first
55 56 assert !r.include?(@changeset)
56 57
57 58 # Make the project private
58 59 @project.update_attribute :is_public, false
59 60 r = Issue.search(@issue_keyword).first
60 61 assert !r.include?(@issue)
61 62 r = Changeset.search(@changeset_keyword).first
62 63 assert !r.include?(@changeset)
63 64 end
64 65
65 66 def test_search_by_user
66 67 User.current = User.find_by_login('rhill')
67 68 assert User.current.memberships.empty?
68 69
69 70 r = Issue.search(@issue_keyword).first
70 71 assert r.include?(@issue)
71 72 r = Changeset.search(@changeset_keyword).first
72 73 assert r.include?(@changeset)
73 74
74 75 # Removes the :view_changesets permission from Non member role
75 76 remove_permission Role.non_member, :view_changesets
76 77
77 78 r = Issue.search(@issue_keyword).first
78 79 assert r.include?(@issue)
79 80 r = Changeset.search(@changeset_keyword).first
80 81 assert !r.include?(@changeset)
81 82
82 83 # Make the project private
83 84 @project.update_attribute :is_public, false
84 85 r = Issue.search(@issue_keyword).first
85 86 assert !r.include?(@issue)
86 87 r = Changeset.search(@changeset_keyword).first
87 88 assert !r.include?(@changeset)
88 89 end
89 90
90 91 def test_search_by_allowed_member
91 92 User.current = User.find_by_login('jsmith')
92 93 assert User.current.projects.include?(@project)
93 94
94 95 r = Issue.search(@issue_keyword).first
95 96 assert r.include?(@issue)
96 97 r = Changeset.search(@changeset_keyword).first
97 98 assert r.include?(@changeset)
98 99
99 100 # Make the project private
100 101 @project.update_attribute :is_public, false
101 102 r = Issue.search(@issue_keyword).first
102 103 assert r.include?(@issue)
103 104 r = Changeset.search(@changeset_keyword).first
104 105 assert r.include?(@changeset)
105 106 end
106 107
107 108 def test_search_by_unallowed_member
108 109 # Removes the :view_changesets permission from user's and non member role
109 110 remove_permission Role.find(1), :view_changesets
110 111 remove_permission Role.non_member, :view_changesets
111 112
112 113 User.current = User.find_by_login('jsmith')
113 114 assert User.current.projects.include?(@project)
114 115
115 116 r = Issue.search(@issue_keyword).first
116 117 assert r.include?(@issue)
117 118 r = Changeset.search(@changeset_keyword).first
118 119 assert !r.include?(@changeset)
119 120
120 121 # Make the project private
121 122 @project.update_attribute :is_public, false
122 123 r = Issue.search(@issue_keyword).first
123 124 assert r.include?(@issue)
124 125 r = Changeset.search(@changeset_keyword).first
125 126 assert !r.include?(@changeset)
126 127 end
127 128
128 129 def test_search_issue_with_multiple_hits_in_journals
129 130 i = Issue.find(1)
130 131 assert_equal 2, i.journals.count(:all, :conditions => "notes LIKE '%notes%'")
131 132
132 133 r = Issue.search('%notes%').first
133 134 assert_equal 1, r.size
134 135 assert_equal i, r.first
135 136 end
136 137
137 138 private
138 139
139 140 def remove_permission(role, permission)
140 141 role.permissions = role.permissions - [ permission ]
141 142 role.save
142 143 end
143 144 end
@@ -1,223 +1,223
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class UserTest < Test::Unit::TestCase
21 fixtures :users, :members, :projects
21 fixtures :users, :members, :projects, :roles, :member_roles
22 22
23 23 def setup
24 24 @admin = User.find(1)
25 25 @jsmith = User.find(2)
26 26 @dlopper = User.find(3)
27 27 end
28 28
29 29 def test_truth
30 30 assert_kind_of User, @jsmith
31 31 end
32 32
33 33 def test_create
34 34 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
35 35
36 36 user.login = "jsmith"
37 37 user.password, user.password_confirmation = "password", "password"
38 38 # login uniqueness
39 39 assert !user.save
40 40 assert_equal 1, user.errors.count
41 41
42 42 user.login = "newuser"
43 43 user.password, user.password_confirmation = "passwd", "password"
44 44 # password confirmation
45 45 assert !user.save
46 46 assert_equal 1, user.errors.count
47 47
48 48 user.password, user.password_confirmation = "password", "password"
49 49 assert user.save
50 50 end
51 51
52 52 def test_mail_uniqueness_should_not_be_case_sensitive
53 53 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
54 54 u.login = 'newuser1'
55 55 u.password, u.password_confirmation = "password", "password"
56 56 assert u.save
57 57
58 58 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
59 59 u.login = 'newuser2'
60 60 u.password, u.password_confirmation = "password", "password"
61 61 assert !u.save
62 62 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
63 63 end
64 64
65 65 def test_update
66 66 assert_equal "admin", @admin.login
67 67 @admin.login = "john"
68 68 assert @admin.save, @admin.errors.full_messages.join("; ")
69 69 @admin.reload
70 70 assert_equal "john", @admin.login
71 71 end
72 72
73 73 def test_destroy
74 74 User.find(2).destroy
75 75 assert_nil User.find_by_id(2)
76 76 assert Member.find_all_by_user_id(2).empty?
77 77 end
78 78
79 79 def test_validate
80 80 @admin.login = ""
81 81 assert !@admin.save
82 82 assert_equal 1, @admin.errors.count
83 83 end
84 84
85 85 def test_password
86 86 user = User.try_to_login("admin", "admin")
87 87 assert_kind_of User, user
88 88 assert_equal "admin", user.login
89 89 user.password = "hello"
90 90 assert user.save
91 91
92 92 user = User.try_to_login("admin", "hello")
93 93 assert_kind_of User, user
94 94 assert_equal "admin", user.login
95 95 assert_equal User.hash_password("hello"), user.hashed_password
96 96 end
97 97
98 98 def test_name_format
99 99 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
100 100 Setting.user_format = :firstname_lastname
101 101 assert_equal 'John Smith', @jsmith.reload.name
102 102 Setting.user_format = :username
103 103 assert_equal 'jsmith', @jsmith.reload.name
104 104 end
105 105
106 106 def test_lock
107 107 user = User.try_to_login("jsmith", "jsmith")
108 108 assert_equal @jsmith, user
109 109
110 110 @jsmith.status = User::STATUS_LOCKED
111 111 assert @jsmith.save
112 112
113 113 user = User.try_to_login("jsmith", "jsmith")
114 114 assert_equal nil, user
115 115 end
116 116
117 117 def test_create_anonymous
118 118 AnonymousUser.delete_all
119 119 anon = User.anonymous
120 120 assert !anon.new_record?
121 121 assert_kind_of AnonymousUser, anon
122 122 end
123 123
124 124 def test_rss_key
125 125 assert_nil @jsmith.rss_token
126 126 key = @jsmith.rss_key
127 127 assert_equal 40, key.length
128 128
129 129 @jsmith.reload
130 130 assert_equal key, @jsmith.rss_key
131 131 end
132 132
133 def test_role_for_project
133 def test_roles_for_project
134 134 # user with a role
135 role = @jsmith.role_for_project(Project.find(1))
136 assert_kind_of Role, role
137 assert_equal "Manager", role.name
135 roles = @jsmith.roles_for_project(Project.find(1))
136 assert_kind_of Role, roles.first
137 assert_equal "Manager", roles.first.name
138 138
139 139 # user with no role
140 assert !@dlopper.role_for_project(Project.find(2)).member?
140 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
141 141 end
142 142
143 143 def test_mail_notification_all
144 144 @jsmith.mail_notification = true
145 145 @jsmith.notified_project_ids = []
146 146 @jsmith.save
147 147 @jsmith.reload
148 148 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
149 149 end
150 150
151 151 def test_mail_notification_selected
152 152 @jsmith.mail_notification = false
153 153 @jsmith.notified_project_ids = [1]
154 154 @jsmith.save
155 155 @jsmith.reload
156 156 assert Project.find(1).recipients.include?(@jsmith.mail)
157 157 end
158 158
159 159 def test_mail_notification_none
160 160 @jsmith.mail_notification = false
161 161 @jsmith.notified_project_ids = []
162 162 @jsmith.save
163 163 @jsmith.reload
164 164 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
165 165 end
166 166
167 167 def test_comments_sorting_preference
168 168 assert !@jsmith.wants_comments_in_reverse_order?
169 169 @jsmith.pref.comments_sorting = 'asc'
170 170 assert !@jsmith.wants_comments_in_reverse_order?
171 171 @jsmith.pref.comments_sorting = 'desc'
172 172 assert @jsmith.wants_comments_in_reverse_order?
173 173 end
174 174
175 175 def test_find_by_mail_should_be_case_insensitive
176 176 u = User.find_by_mail('JSmith@somenet.foo')
177 177 assert_not_nil u
178 178 assert_equal 'jsmith@somenet.foo', u.mail
179 179 end
180 180
181 181 def test_random_password
182 182 u = User.new
183 183 u.random_password
184 184 assert !u.password.blank?
185 185 assert !u.password_confirmation.blank?
186 186 end
187 187
188 188 if Object.const_defined?(:OpenID)
189 189
190 190 def test_setting_identity_url
191 191 normalized_open_id_url = 'http://example.com/'
192 192 u = User.new( :identity_url => 'http://example.com/' )
193 193 assert_equal normalized_open_id_url, u.identity_url
194 194 end
195 195
196 196 def test_setting_identity_url_without_trailing_slash
197 197 normalized_open_id_url = 'http://example.com/'
198 198 u = User.new( :identity_url => 'http://example.com' )
199 199 assert_equal normalized_open_id_url, u.identity_url
200 200 end
201 201
202 202 def test_setting_identity_url_without_protocol
203 203 normalized_open_id_url = 'http://example.com/'
204 204 u = User.new( :identity_url => 'example.com' )
205 205 assert_equal normalized_open_id_url, u.identity_url
206 206 end
207 207
208 208 def test_setting_blank_identity_url
209 209 u = User.new( :identity_url => 'example.com' )
210 210 u.identity_url = ''
211 211 assert u.identity_url.blank?
212 212 end
213 213
214 214 def test_setting_invalid_identity_url
215 215 u = User.new( :identity_url => 'this is not an openid url' )
216 216 assert u.identity_url.blank?
217 217 end
218 218
219 219 else
220 220 puts "Skipping openid tests."
221 221 end
222 222
223 223 end
General Comments 0
You need to be logged in to leave comments. Login now