##// END OF EJS Templates
Files module: makes version field non required (#1053)....
Jean-Philippe Lang -
r2115:66ff4cb7de55
parent child
Show More
@@ -1,285 +1,289
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :activity, :only => :activity
21 21 menu_item :roadmap, :only => :roadmap
22 22 menu_item :files, :only => [:list_files, :add_file]
23 23 menu_item :settings, :only => :settings
24 24 menu_item :issues, :only => [:changelog]
25 25
26 26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
27 27 before_filter :find_optional_project, :only => :activity
28 28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
29 29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 30 accept_key_auth :activity
31 31
32 32 helper :sort
33 33 include SortHelper
34 34 helper :custom_fields
35 35 include CustomFieldsHelper
36 36 helper :ifpdf
37 37 include IfpdfHelper
38 38 helper :issues
39 39 helper IssuesHelper
40 40 helper :queries
41 41 include QueriesHelper
42 42 helper :repositories
43 43 include RepositoriesHelper
44 44 include ProjectsHelper
45 45
46 46 # Lists visible projects
47 47 def index
48 48 projects = Project.find :all,
49 49 :conditions => Project.visible_by(User.current),
50 50 :include => :parent
51 51 respond_to do |format|
52 52 format.html {
53 53 @project_tree = projects.group_by {|p| p.parent || p}
54 54 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
55 55 }
56 56 format.atom {
57 57 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
58 58 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 59 }
60 60 end
61 61 end
62 62
63 63 # Add a new project
64 64 def add
65 65 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 66 @trackers = Tracker.all
67 67 @root_projects = Project.find(:all,
68 68 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
69 69 :order => 'name')
70 70 @project = Project.new(params[:project])
71 71 if request.get?
72 72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 73 @project.trackers = Tracker.all
74 74 @project.is_public = Setting.default_projects_public?
75 75 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
76 76 else
77 77 @project.enabled_module_names = params[:enabled_modules]
78 78 if @project.save
79 79 flash[:notice] = l(:notice_successful_create)
80 80 redirect_to :controller => 'admin', :action => 'projects'
81 81 end
82 82 end
83 83 end
84 84
85 85 # Show @project
86 86 def show
87 87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
88 88 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
89 89 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
90 90 @trackers = @project.rolled_up_trackers
91 91
92 92 cond = @project.project_condition(Setting.display_subprojects_issues?)
93 93 Issue.visible_by(User.current) do
94 94 @open_issues_by_tracker = Issue.count(:group => :tracker,
95 95 :include => [:project, :status, :tracker],
96 96 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
97 97 @total_issues_by_tracker = Issue.count(:group => :tracker,
98 98 :include => [:project, :status, :tracker],
99 99 :conditions => cond)
100 100 end
101 101 TimeEntry.visible_by(User.current) do
102 102 @total_hours = TimeEntry.sum(:hours,
103 103 :include => :project,
104 104 :conditions => cond).to_f
105 105 end
106 106 @key = User.current.rss_key
107 107 end
108 108
109 109 def settings
110 110 @root_projects = Project.find(:all,
111 111 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
112 112 :order => 'name')
113 113 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
114 114 @issue_category ||= IssueCategory.new
115 115 @member ||= @project.members.new
116 116 @trackers = Tracker.all
117 117 @repository ||= @project.repository
118 118 @wiki ||= @project.wiki
119 119 end
120 120
121 121 # Edit @project
122 122 def edit
123 123 if request.post?
124 124 @project.attributes = params[:project]
125 125 if @project.save
126 126 flash[:notice] = l(:notice_successful_update)
127 127 redirect_to :action => 'settings', :id => @project
128 128 else
129 129 settings
130 130 render :action => 'settings'
131 131 end
132 132 end
133 133 end
134 134
135 135 def modules
136 136 @project.enabled_module_names = params[:enabled_modules]
137 137 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
138 138 end
139 139
140 140 def archive
141 141 @project.archive if request.post? && @project.active?
142 142 redirect_to :controller => 'admin', :action => 'projects'
143 143 end
144 144
145 145 def unarchive
146 146 @project.unarchive if request.post? && !@project.active?
147 147 redirect_to :controller => 'admin', :action => 'projects'
148 148 end
149 149
150 150 # Delete @project
151 151 def destroy
152 152 @project_to_destroy = @project
153 153 if request.post? and params[:confirm]
154 154 @project_to_destroy.destroy
155 155 redirect_to :controller => 'admin', :action => 'projects'
156 156 end
157 157 # hide project in layout
158 158 @project = nil
159 159 end
160 160
161 161 # Add a new issue category to @project
162 162 def add_issue_category
163 163 @category = @project.issue_categories.build(params[:category])
164 164 if request.post? and @category.save
165 165 respond_to do |format|
166 166 format.html do
167 167 flash[:notice] = l(:notice_successful_create)
168 168 redirect_to :action => 'settings', :tab => 'categories', :id => @project
169 169 end
170 170 format.js do
171 171 # IE doesn't support the replace_html rjs method for select box options
172 172 render(:update) {|page| page.replace "issue_category_id",
173 173 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
174 174 }
175 175 end
176 176 end
177 177 end
178 178 end
179 179
180 180 # Add a new version to @project
181 181 def add_version
182 182 @version = @project.versions.build(params[:version])
183 183 if request.post? and @version.save
184 184 flash[:notice] = l(:notice_successful_create)
185 185 redirect_to :action => 'settings', :tab => 'versions', :id => @project
186 186 end
187 187 end
188 188
189 189 def add_file
190 190 if request.post?
191 @version = @project.versions.find_by_id(params[:version_id])
192 attachments = attach_files(@version, params[:attachments])
193 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
191 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
192 attachments = attach_files(container, params[:attachments])
193 if !attachments.empty? && Setting.notified_events.include?('file_added')
194 Mailer.deliver_attachments_added(attachments)
195 end
194 196 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
197 return
195 198 end
196 199 @versions = @project.versions.sort
197 200 end
198 201
199 202 def list_files
200 203 sort_init "#{Attachment.table_name}.filename", "asc"
201 204 sort_update
202 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
205 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
206 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
203 207 render :layout => !request.xhr?
204 208 end
205 209
206 210 # Show changelog for @project
207 211 def changelog
208 212 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
209 213 retrieve_selected_tracker_ids(@trackers)
210 214 @versions = @project.versions.sort
211 215 end
212 216
213 217 def roadmap
214 218 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
215 219 retrieve_selected_tracker_ids(@trackers)
216 220 @versions = @project.versions.sort
217 221 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
218 222 end
219 223
220 224 def activity
221 225 @days = Setting.activity_days_default.to_i
222 226
223 227 if params[:from]
224 228 begin; @date_to = params[:from].to_date + 1; rescue; end
225 229 end
226 230
227 231 @date_to ||= Date.today + 1
228 232 @date_from = @date_to - @days
229 233 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
230 234 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
231 235
232 236 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
233 237 :with_subprojects => @with_subprojects,
234 238 :author => @author)
235 239 @activity.scope_select {|t| !params["show_#{t}"].nil?}
236 240 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
237 241
238 242 events = @activity.events(@date_from, @date_to)
239 243
240 244 respond_to do |format|
241 245 format.html {
242 246 @events_by_day = events.group_by(&:event_date)
243 247 render :layout => false if request.xhr?
244 248 }
245 249 format.atom {
246 250 title = l(:label_activity)
247 251 if @author
248 252 title = @author.name
249 253 elsif @activity.scope.size == 1
250 254 title = l("label_#{@activity.scope.first.singularize}_plural")
251 255 end
252 256 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
253 257 }
254 258 end
255 259
256 260 rescue ActiveRecord::RecordNotFound
257 261 render_404
258 262 end
259 263
260 264 private
261 265 # Find project of id params[:id]
262 266 # if not found, redirect to project list
263 267 # Used as a before_filter
264 268 def find_project
265 269 @project = Project.find(params[:id])
266 270 rescue ActiveRecord::RecordNotFound
267 271 render_404
268 272 end
269 273
270 274 def find_optional_project
271 275 return true unless params[:id]
272 276 @project = Project.find(params[:id])
273 277 authorize
274 278 rescue ActiveRecord::RecordNotFound
275 279 render_404
276 280 end
277 281
278 282 def retrieve_selected_tracker_ids(selectable_trackers)
279 283 if ids = params[:tracker_ids]
280 284 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
281 285 else
282 286 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
283 287 end
284 288 end
285 289 end
@@ -1,247 +1,250
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 Mailer < ActionMailer::Base
19 19 helper :application
20 20 helper :issues
21 21 helper :custom_fields
22 22
23 23 include ActionController::UrlWriter
24 24
25 25 def issue_add(issue)
26 26 redmine_headers 'Project' => issue.project.identifier,
27 27 'Issue-Id' => issue.id,
28 28 'Issue-Author' => issue.author.login
29 29 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
30 30 recipients issue.recipients
31 31 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
32 32 body :issue => issue,
33 33 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
34 34 end
35 35
36 36 def issue_edit(journal)
37 37 issue = journal.journalized
38 38 redmine_headers 'Project' => issue.project.identifier,
39 39 'Issue-Id' => issue.id,
40 40 'Issue-Author' => issue.author.login
41 41 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
42 42 recipients issue.recipients
43 43 # Watchers in cc
44 44 cc(issue.watcher_recipients - @recipients)
45 45 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
46 46 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
47 47 s << issue.subject
48 48 subject s
49 49 body :issue => issue,
50 50 :journal => journal,
51 51 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
52 52 end
53 53
54 54 def reminder(user, issues, days)
55 55 set_language_if_valid user.language
56 56 recipients user.mail
57 57 subject l(:mail_subject_reminder, issues.size)
58 58 body :issues => issues,
59 59 :days => days,
60 60 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc')
61 61 end
62 62
63 63 def document_added(document)
64 64 redmine_headers 'Project' => document.project.identifier
65 65 recipients document.project.recipients
66 66 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
67 67 body :document => document,
68 68 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
69 69 end
70 70
71 71 def attachments_added(attachments)
72 72 container = attachments.first.container
73 73 added_to = ''
74 74 added_to_url = ''
75 75 case container.class.name
76 when 'Project'
77 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
78 added_to = "#{l(:label_project)}: #{container}"
76 79 when 'Version'
77 80 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
78 81 added_to = "#{l(:label_version)}: #{container.name}"
79 82 when 'Document'
80 83 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
81 84 added_to = "#{l(:label_document)}: #{container.title}"
82 85 end
83 86 redmine_headers 'Project' => container.project.identifier
84 87 recipients container.project.recipients
85 88 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
86 89 body :attachments => attachments,
87 90 :added_to => added_to,
88 91 :added_to_url => added_to_url
89 92 end
90 93
91 94 def news_added(news)
92 95 redmine_headers 'Project' => news.project.identifier
93 96 recipients news.project.recipients
94 97 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
95 98 body :news => news,
96 99 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
97 100 end
98 101
99 102 def message_posted(message, recipients)
100 103 redmine_headers 'Project' => message.project.identifier,
101 104 'Topic-Id' => (message.parent_id || message.id)
102 105 recipients(recipients)
103 106 subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
104 107 body :message => message,
105 108 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
106 109 end
107 110
108 111 def account_information(user, password)
109 112 set_language_if_valid user.language
110 113 recipients user.mail
111 114 subject l(:mail_subject_register, Setting.app_title)
112 115 body :user => user,
113 116 :password => password,
114 117 :login_url => url_for(:controller => 'account', :action => 'login')
115 118 end
116 119
117 120 def account_activation_request(user)
118 121 # Send the email to all active administrators
119 122 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
120 123 subject l(:mail_subject_account_activation_request, Setting.app_title)
121 124 body :user => user,
122 125 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
123 126 end
124 127
125 128 def lost_password(token)
126 129 set_language_if_valid(token.user.language)
127 130 recipients token.user.mail
128 131 subject l(:mail_subject_lost_password, Setting.app_title)
129 132 body :token => token,
130 133 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
131 134 end
132 135
133 136 def register(token)
134 137 set_language_if_valid(token.user.language)
135 138 recipients token.user.mail
136 139 subject l(:mail_subject_register, Setting.app_title)
137 140 body :token => token,
138 141 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
139 142 end
140 143
141 144 def test(user)
142 145 set_language_if_valid(user.language)
143 146 recipients user.mail
144 147 subject 'Redmine test'
145 148 body :url => url_for(:controller => 'welcome')
146 149 end
147 150
148 151 # Overrides default deliver! method to prevent from sending an email
149 152 # with no recipient, cc or bcc
150 153 def deliver!(mail = @mail)
151 154 return false if (recipients.nil? || recipients.empty?) &&
152 155 (cc.nil? || cc.empty?) &&
153 156 (bcc.nil? || bcc.empty?)
154 157 super
155 158 end
156 159
157 160 # Sends reminders to issue assignees
158 161 # Available options:
159 162 # * :days => how many days in the future to remind about (defaults to 7)
160 163 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
161 164 # * :project => id or identifier of project to process (defaults to all projects)
162 165 def self.reminders(options={})
163 166 days = options[:days] || 7
164 167 project = options[:project] ? Project.find(options[:project]) : nil
165 168 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
166 169
167 170 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
168 171 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
169 172 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
170 173 s << "#{Issue.table_name}.project_id = #{project.id}" if project
171 174 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
172 175
173 176 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
174 177 :conditions => s.conditions
175 178 ).group_by(&:assigned_to)
176 179 issues_by_assignee.each do |assignee, issues|
177 180 deliver_reminder(assignee, issues, days) unless assignee.nil?
178 181 end
179 182 end
180 183
181 184 private
182 185 def initialize_defaults(method_name)
183 186 super
184 187 set_language_if_valid Setting.default_language
185 188 from Setting.mail_from
186 189
187 190 # URL options
188 191 h = Setting.host_name
189 192 h = h.to_s.gsub(%r{\/.*$}, '') unless ActionController::AbstractRequest.relative_url_root.blank?
190 193 default_url_options[:host] = h
191 194 default_url_options[:protocol] = Setting.protocol
192 195
193 196 # Common headers
194 197 headers 'X-Mailer' => 'Redmine',
195 198 'X-Redmine-Host' => Setting.host_name,
196 199 'X-Redmine-Site' => Setting.app_title
197 200 end
198 201
199 202 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
200 203 def redmine_headers(h)
201 204 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
202 205 end
203 206
204 207 # Overrides the create_mail method
205 208 def create_mail
206 209 # Removes the current user from the recipients and cc
207 210 # if he doesn't want to receive notifications about what he does
208 211 if User.current.pref[:no_self_notified]
209 212 recipients.delete(User.current.mail) if recipients
210 213 cc.delete(User.current.mail) if cc
211 214 end
212 215 # Blind carbon copy recipients
213 216 if Setting.bcc_recipients?
214 217 bcc([recipients, cc].flatten.compact.uniq)
215 218 recipients []
216 219 cc []
217 220 end
218 221 super
219 222 end
220 223
221 224 # Renders a message with the corresponding layout
222 225 def render_message(method_name, body)
223 226 layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
224 227 body[:content_for_layout] = render(:file => method_name, :body => body)
225 228 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
226 229 end
227 230
228 231 # for the case of plain text only
229 232 def body(*params)
230 233 value = super(*params)
231 234 if Setting.plain_text_mail?
232 235 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
233 236 unless String === @body or templates.empty?
234 237 template = File.basename(templates.first)
235 238 @body[:content_for_layout] = render(:file => template, :body => @body)
236 239 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
237 240 return @body
238 241 end
239 242 end
240 243 return value
241 244 end
242 245
243 246 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
244 247 def self.controller_path
245 248 ''
246 249 end unless respond_to?('controller_path')
247 250 end
@@ -1,274 +1,276
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_tree :order => "name", :counter_cache => true
47 acts_as_attachable :view_permission => :view_files,
48 :delete_permission => :manage_files
47 49
48 50 acts_as_customizable
49 51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
50 52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
51 53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
52 54 :author => nil
53 55
54 56 attr_protected :status, :enabled_module_names
55 57
56 58 validates_presence_of :name, :identifier
57 59 validates_uniqueness_of :name, :identifier
58 60 validates_associated :repository, :wiki
59 61 validates_length_of :name, :maximum => 30
60 62 validates_length_of :homepage, :maximum => 255
61 63 validates_length_of :identifier, :in => 3..20
62 64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
63 65
64 66 before_destroy :delete_all_members
65 67
66 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] } }
67 69
68 70 def identifier=(identifier)
69 71 super unless identifier_frozen?
70 72 end
71 73
72 74 def identifier_frozen?
73 75 errors[:identifier].nil? && !(new_record? || identifier.blank?)
74 76 end
75 77
76 78 def issues_with_subprojects(include_subprojects=false)
77 79 conditions = nil
78 80 if include_subprojects
79 81 ids = [id] + child_ids
80 82 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
81 83 end
82 84 conditions ||= ["#{Project.table_name}.id = ?", id]
83 85 # Quick and dirty fix for Rails 2 compatibility
84 86 Issue.send(:with_scope, :find => { :conditions => conditions }) do
85 87 Version.send(:with_scope, :find => { :conditions => conditions }) do
86 88 yield
87 89 end
88 90 end
89 91 end
90 92
91 93 # returns latest created projects
92 94 # non public projects will be returned only if user is a member of those
93 95 def self.latest(user=nil, count=5)
94 96 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 97 end
96 98
97 99 def self.visible_by(user=nil)
98 100 user ||= User.current
99 101 if user && user.admin?
100 102 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
101 103 elsif user && user.memberships.any?
102 104 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(',')}))"
103 105 else
104 106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
105 107 end
106 108 end
107 109
108 110 def self.allowed_to_condition(user, permission, options={})
109 111 statements = []
110 112 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
111 113 if perm = Redmine::AccessControl.permission(permission)
112 114 unless perm.project_module.nil?
113 115 # If the permission belongs to a project module, make sure the module is enabled
114 116 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)"
115 117 end
116 118 end
117 119 if options[:project]
118 120 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
119 121 project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
120 122 base_statement = "(#{project_statement}) AND (#{base_statement})"
121 123 end
122 124 if user.admin?
123 125 # no restriction
124 126 else
125 127 statements << "1=0"
126 128 if user.logged?
127 129 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
128 130 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
129 131 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
130 132 elsif Role.anonymous.allowed_to?(permission)
131 133 # anonymous user allowed on public project
132 134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
133 135 else
134 136 # anonymous user is not authorized
135 137 end
136 138 end
137 139 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
138 140 end
139 141
140 142 def project_condition(with_subprojects)
141 143 cond = "#{Project.table_name}.id = #{id}"
142 144 cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
143 145 cond
144 146 end
145 147
146 148 def self.find(*args)
147 149 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
148 150 project = find_by_identifier(*args)
149 151 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
150 152 project
151 153 else
152 154 super
153 155 end
154 156 end
155 157
156 158 def to_param
157 159 # id is used for projects with a numeric identifier (compatibility)
158 160 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
159 161 end
160 162
161 163 def active?
162 164 self.status == STATUS_ACTIVE
163 165 end
164 166
165 167 def archive
166 168 # Archive subprojects if any
167 169 children.each do |subproject|
168 170 subproject.archive
169 171 end
170 172 update_attribute :status, STATUS_ARCHIVED
171 173 end
172 174
173 175 def unarchive
174 176 return false if parent && !parent.active?
175 177 update_attribute :status, STATUS_ACTIVE
176 178 end
177 179
178 180 def active_children
179 181 children.select {|child| child.active?}
180 182 end
181 183
182 184 # Returns an array of the trackers used by the project and its sub projects
183 185 def rolled_up_trackers
184 186 @rolled_up_trackers ||=
185 187 Tracker.find(:all, :include => :projects,
186 188 :select => "DISTINCT #{Tracker.table_name}.*",
187 189 :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
188 190 :order => "#{Tracker.table_name}.position")
189 191 end
190 192
191 193 # Deletes all project's members
192 194 def delete_all_members
193 195 Member.delete_all(['project_id = ?', id])
194 196 end
195 197
196 198 # Users issues can be assigned to
197 199 def assignable_users
198 200 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
199 201 end
200 202
201 203 # Returns the mail adresses of users that should be always notified on project events
202 204 def recipients
203 205 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
204 206 end
205 207
206 208 # Returns an array of all custom fields enabled for project issues
207 209 # (explictly associated custom fields and custom fields enabled for all projects)
208 210 def all_issue_custom_fields
209 211 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
210 212 end
211 213
212 214 def project
213 215 self
214 216 end
215 217
216 218 def <=>(project)
217 219 name.downcase <=> project.name.downcase
218 220 end
219 221
220 222 def to_s
221 223 name
222 224 end
223 225
224 226 # Returns a short description of the projects (first lines)
225 227 def short_description(length = 255)
226 228 description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
227 229 end
228 230
229 231 def allows_to?(action)
230 232 if action.is_a? Hash
231 233 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
232 234 else
233 235 allowed_permissions.include? action
234 236 end
235 237 end
236 238
237 239 def module_enabled?(module_name)
238 240 module_name = module_name.to_s
239 241 enabled_modules.detect {|m| m.name == module_name}
240 242 end
241 243
242 244 def enabled_module_names=(module_names)
243 245 enabled_modules.clear
244 246 module_names = [] unless module_names && module_names.is_a?(Array)
245 247 module_names.each do |name|
246 248 enabled_modules << EnabledModule.new(:name => name.to_s)
247 249 end
248 250 end
249 251
250 252 # Returns an auto-generated project identifier based on the last identifier used
251 253 def self.next_identifier
252 254 p = Project.find(:first, :order => 'created_on DESC')
253 255 p.nil? ? nil : p.identifier.to_s.succ
254 256 end
255 257
256 258 protected
257 259 def validate
258 260 errors.add(parent_id, " must be a root project") if parent and parent.parent
259 261 errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
260 262 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
261 263 end
262 264
263 265 private
264 266 def allowed_permissions
265 267 @allowed_permissions ||= begin
266 268 module_names = enabled_modules.collect {|m| m.name}
267 269 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
268 270 end
269 271 end
270 272
271 273 def allowed_actions
272 274 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
273 275 end
274 276 end
@@ -1,13 +1,16
1 1 <h2><%=l(:label_attachment_new)%></h2>
2 2
3 3 <%= error_messages_for 'attachment' %>
4 4 <div class="box">
5 5 <% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %>
6 6
7 <p><label for="version_id"><%=l(:field_version)%> <span class="required">*</span></label>
8 <%= select_tag "version_id", options_from_collection_for_select(@versions, "id", "name") %></p>
7 <% if @versions.any? %>
8 <p><label for="version_id"><%=l(:field_version)%></label>
9 <%= select_tag "version_id", content_tag('option', '') +
10 options_from_collection_for_select(@versions, "id", "name") %></p>
11 <% end %>
9 12
10 13 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
11 14 </div>
12 15 <%= submit_tag l(:button_add) %>
13 <% end %> No newline at end of file
16 <% end %>
@@ -1,45 +1,42
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_attachment_plural)%></h2>
6 6
7 7 <% delete_allowed = User.current.allowed_to?(:manage_files, @project) %>
8 8
9 9 <table class="list">
10 10 <thead><tr>
11 <th><%=l(:field_version)%></th>
12 11 <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
13 12 <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
14 13 <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
15 14 <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
16 15 <th>MD5</th>
17 <% if delete_allowed %><th></th><% end %>
16 <th></th>
18 17 </tr></thead>
19 18 <tbody>
20 <% for version in @versions %>
21 <% unless version.attachments.empty? %>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
23 <% for file in version.attachments %>
19 <% @containers.each do |container| %>
20 <% next if container.attachments.empty? -%>
21 <% if container.is_a?(Version) -%>
22 <tr><th colspan="6" align="left"><span class="icon icon-package"><b><%=h container %></b></span></th></tr>
23 <% end -%>
24 <% container.attachments.each do |file| %>
24 25 <tr class="<%= cycle("odd", "even") %>">
25 <td></td>
26 26 <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
27 27 <td align="center"><%= format_time(file.created_on) %></td>
28 28 <td align="center"><%= number_to_human_size(file.filesize) %></td>
29 29 <td align="center"><%= file.downloads %></td>
30 30 <td align="center"><small><%= file.digest %></small></td>
31 <% if delete_allowed %>
32 31 <td align="center">
33 <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file},
34 :confirm => l(:text_are_you_sure), :method => :post %>
32 <%= link_to(image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file},
33 :confirm => l(:text_are_you_sure), :method => :post) if delete_allowed %>
35 34 </td>
36 <% end %>
37 35 </tr>
38 36 <% end
39 37 reset_cycle %>
40 <% end %>
41 38 <% end %>
42 39 </tbody>
43 40 </table>
44 41
45 42 <% html_title(l(:label_attachment_plural)) -%>
@@ -1,88 +1,112
1 1 ---
2 2 attachments_001:
3 3 created_on: 2006-07-19 21:07:27 +02:00
4 4 downloads: 0
5 5 content_type: text/plain
6 6 disk_filename: 060719210727_error281.txt
7 7 container_id: 3
8 8 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
9 9 id: 1
10 10 container_type: Issue
11 11 filesize: 28
12 12 filename: error281.txt
13 13 author_id: 2
14 14 attachments_002:
15 15 created_on: 2006-07-19 21:07:27 +02:00
16 16 downloads: 0
17 17 content_type: text/plain
18 18 disk_filename: 060719210727_document.txt
19 19 container_id: 1
20 20 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
21 21 id: 2
22 22 container_type: Document
23 23 filesize: 28
24 24 filename: document.txt
25 25 author_id: 2
26 26 attachments_003:
27 27 created_on: 2006-07-19 21:07:27 +02:00
28 28 downloads: 0
29 29 content_type: image/gif
30 30 disk_filename: 060719210727_logo.gif
31 31 container_id: 4
32 32 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
33 33 id: 3
34 34 container_type: WikiPage
35 35 filesize: 280
36 36 filename: logo.gif
37 37 description: This is a logo
38 38 author_id: 2
39 39 attachments_004:
40 40 created_on: 2006-07-19 21:07:27 +02:00
41 41 container_type: Issue
42 42 container_id: 3
43 43 downloads: 0
44 44 disk_filename: 060719210727_source.rb
45 45 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
46 46 id: 4
47 47 filesize: 153
48 48 filename: source.rb
49 49 author_id: 2
50 50 description: This is a Ruby source file
51 51 content_type: application/x-ruby
52 52 attachments_005:
53 53 created_on: 2006-07-19 21:07:27 +02:00
54 54 container_type: Issue
55 55 container_id: 3
56 56 downloads: 0
57 57 disk_filename: 060719210727_changeset.diff
58 58 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
59 59 id: 5
60 60 filesize: 687
61 61 filename: changeset.diff
62 62 author_id: 2
63 63 content_type: text/x-diff
64 64 attachments_006:
65 65 created_on: 2006-07-19 21:07:27 +02:00
66 66 container_type: Issue
67 67 container_id: 3
68 68 downloads: 0
69 69 disk_filename: 060719210727_archive.zip
70 70 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
71 71 id: 6
72 72 filesize: 157
73 73 filename: archive.zip
74 74 author_id: 2
75 75 content_type: application/octet-stream
76 76 attachments_007:
77 77 created_on: 2006-07-19 21:07:27 +02:00
78 78 container_type: Issue
79 79 container_id: 4
80 80 downloads: 0
81 81 disk_filename: 060719210727_archive.zip
82 82 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
83 83 id: 7
84 84 filesize: 157
85 85 filename: archive.zip
86 86 author_id: 1
87 87 content_type: application/octet-stream
88 attachments_008:
89 created_on: 2006-07-19 21:07:27 +02:00
90 container_type: Project
91 container_id: 1
92 downloads: 0
93 disk_filename: 060719210727_project_file.zip
94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
95 id: 8
96 filesize: 320
97 filename: project_file.zip
98 author_id: 2
99 content_type: application/octet-stream
100 attachments_009:
101 created_on: 2006-07-19 21:07:27 +02:00
102 container_type: Version
103 container_id: 1
104 downloads: 0
105 disk_filename: 060719210727_version_file.zip
106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
107 id: 9
108 filesize: 452
109 filename: version_file.zip
110 author_id: 2
111 content_type: application/octet-stream
88 112 No newline at end of file
@@ -1,108 +1,125
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 26 fixtures :users, :projects, :roles, :members, :enabled_modules, :issues, :attachments
27 27
28 28 def setup
29 29 @controller = AttachmentsController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
33 33 User.current = nil
34 34 end
35 35
36 36 def test_routing
37 37 assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
38 38 assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
39 39 assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
40 40 assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
41 41 end
42 42
43 43 def test_recognizes
44 44 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
45 45 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
46 46 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
47 47 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
48 48 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
49 49 end
50 50
51 51 def test_show_diff
52 52 get :show, :id => 5
53 53 assert_response :success
54 54 assert_template 'diff'
55 55 end
56 56
57 57 def test_show_text_file
58 58 get :show, :id => 4
59 59 assert_response :success
60 60 assert_template 'file'
61 61 end
62 62
63 63 def test_show_other
64 64 get :show, :id => 6
65 65 assert_response :success
66 66 assert_equal 'application/octet-stream', @response.content_type
67 67 end
68 68
69 69 def test_download_text_file
70 70 get :download, :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_anonymous_on_private_private
76 76 get :download, :id => 7
77 77 assert_redirected_to 'account/login'
78 78 end
79 79
80 80 def test_destroy_issue_attachment
81 81 issue = Issue.find(3)
82 82 @request.session[:user_id] = 2
83 83
84 84 assert_difference 'issue.attachments.count', -1 do
85 85 post :destroy, :id => 1
86 86 end
87 87 # no referrer
88 88 assert_redirected_to 'projects/show/ecookbook'
89 89 assert_nil Attachment.find_by_id(1)
90 90 j = issue.journals.find(:first, :order => 'created_on DESC')
91 91 assert_equal 'attachment', j.details.first.property
92 92 assert_equal '1', j.details.first.prop_key
93 93 assert_equal 'error281.txt', j.details.first.old_value
94 94 end
95 95
96 96 def test_destroy_wiki_page_attachment
97 97 @request.session[:user_id] = 2
98 98 assert_difference 'Attachment.count', -1 do
99 99 post :destroy, :id => 3
100 assert_response 302
101 end
102 end
103
104 def test_destroy_project_attachment
105 @request.session[:user_id] = 2
106 assert_difference 'Attachment.count', -1 do
107 post :destroy, :id => 8
108 assert_response 302
109 end
110 end
111
112 def test_destroy_version_attachment
113 @request.session[:user_id] = 2
114 assert_difference 'Attachment.count', -1 do
115 post :destroy, :id => 9
116 assert_response 302
100 117 end
101 118 end
102 119
103 120 def test_destroy_without_permission
104 121 post :destroy, :id => 3
105 122 assert_redirected_to '/login'
106 123 assert Attachment.find_by_id(3)
107 124 end
108 125 end
@@ -1,295 +1,340
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'projects_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class ProjectsController; def rescue_action(e) raise e end; end
23 23
24 24 class ProjectsControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments
27 28
28 29 def setup
29 30 @controller = ProjectsController.new
30 31 @request = ActionController::TestRequest.new
31 32 @response = ActionController::TestResponse.new
32 33 @request.session[:user_id] = nil
33 34 Setting.default_language = 'en'
34 35 end
35 36
36 37 def test_index
37 38 get :index
38 39 assert_response :success
39 40 assert_template 'index'
40 41 assert_not_nil assigns(:project_tree)
41 42 # Root project as hash key
42 43 assert assigns(:project_tree).keys.include?(Project.find(1))
43 44 # Subproject in corresponding value
44 45 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
45 46 end
46 47
47 48 def test_index_atom
48 49 get :index, :format => 'atom'
49 50 assert_response :success
50 51 assert_template 'common/feed.atom.rxml'
51 52 assert_select 'feed>title', :text => 'Redmine: Latest projects'
52 53 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
53 54 end
54 55
55 56 def test_show_by_id
56 57 get :show, :id => 1
57 58 assert_response :success
58 59 assert_template 'show'
59 60 assert_not_nil assigns(:project)
60 61 end
61 62
62 63 def test_show_by_identifier
63 64 get :show, :id => 'ecookbook'
64 65 assert_response :success
65 66 assert_template 'show'
66 67 assert_not_nil assigns(:project)
67 68 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
68 69 end
69 70
70 71 def test_private_subprojects_hidden
71 72 get :show, :id => 'ecookbook'
72 73 assert_response :success
73 74 assert_template 'show'
74 75 assert_no_tag :tag => 'a', :content => /Private child/
75 76 end
76 77
77 78 def test_private_subprojects_visible
78 79 @request.session[:user_id] = 2 # manager who is a member of the private subproject
79 80 get :show, :id => 'ecookbook'
80 81 assert_response :success
81 82 assert_template 'show'
82 83 assert_tag :tag => 'a', :content => /Private child/
83 84 end
84 85
85 86 def test_settings
86 87 @request.session[:user_id] = 2 # manager
87 88 get :settings, :id => 1
88 89 assert_response :success
89 90 assert_template 'settings'
90 91 end
91 92
92 93 def test_edit
93 94 @request.session[:user_id] = 2 # manager
94 95 post :edit, :id => 1, :project => {:name => 'Test changed name',
95 96 :issue_custom_field_ids => ['']}
96 97 assert_redirected_to 'projects/settings/ecookbook'
97 98 project = Project.find(1)
98 99 assert_equal 'Test changed name', project.name
99 100 end
100 101
101 102 def test_get_destroy
102 103 @request.session[:user_id] = 1 # admin
103 104 get :destroy, :id => 1
104 105 assert_response :success
105 106 assert_template 'destroy'
106 107 assert_not_nil Project.find_by_id(1)
107 108 end
108 109
109 110 def test_post_destroy
110 111 @request.session[:user_id] = 1 # admin
111 112 post :destroy, :id => 1, :confirm => 1
112 113 assert_redirected_to 'admin/projects'
113 114 assert_nil Project.find_by_id(1)
114 115 end
116
117 def test_add_file
118 set_tmp_attachments_directory
119 @request.session[:user_id] = 2
120 Setting.notified_events << 'file_added'
121 ActionMailer::Base.deliveries.clear
122
123 assert_difference 'Attachment.count' do
124 post :add_file, :id => 1, :version_id => '',
125 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
126 end
127 assert_redirected_to 'projects/list_files/ecookbook'
128 a = Attachment.find(:first, :order => 'created_on DESC')
129 assert_equal 'testfile.txt', a.filename
130 assert_equal Project.find(1), a.container
131
132 mail = ActionMailer::Base.deliveries.last
133 assert_kind_of TMail::Mail, mail
134 assert_equal "[eCookbook] New file", mail.subject
135 assert mail.body.include?('testfile.txt')
136 end
137
138 def test_add_version_file
139 set_tmp_attachments_directory
140 @request.session[:user_id] = 2
141 Setting.notified_events << 'file_added'
142
143 assert_difference 'Attachment.count' do
144 post :add_file, :id => 1, :version_id => '2',
145 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
146 end
147 assert_redirected_to 'projects/list_files/ecookbook'
148 a = Attachment.find(:first, :order => 'created_on DESC')
149 assert_equal 'testfile.txt', a.filename
150 assert_equal Version.find(2), a.container
151 end
115 152
116 153 def test_list_files
117 154 get :list_files, :id => 1
118 155 assert_response :success
119 156 assert_template 'list_files'
120 assert_not_nil assigns(:versions)
157 assert_not_nil assigns(:containers)
158
159 # file attached to the project
160 assert_tag :a, :content => 'project_file.zip',
161 :attributes => { :href => '/attachments/download/8/project_file.zip' }
162
163 # file attached to a project's version
164 assert_tag :a, :content => 'version_file.zip',
165 :attributes => { :href => '/attachments/download/9/version_file.zip' }
121 166 end
122 167
123 168 def test_changelog
124 169 get :changelog, :id => 1
125 170 assert_response :success
126 171 assert_template 'changelog'
127 172 assert_not_nil assigns(:versions)
128 173 end
129 174
130 175 def test_roadmap
131 176 get :roadmap, :id => 1
132 177 assert_response :success
133 178 assert_template 'roadmap'
134 179 assert_not_nil assigns(:versions)
135 180 # Version with no date set appears
136 181 assert assigns(:versions).include?(Version.find(3))
137 182 # Completed version doesn't appear
138 183 assert !assigns(:versions).include?(Version.find(1))
139 184 end
140 185
141 186 def test_roadmap_with_completed_versions
142 187 get :roadmap, :id => 1, :completed => 1
143 188 assert_response :success
144 189 assert_template 'roadmap'
145 190 assert_not_nil assigns(:versions)
146 191 # Version with no date set appears
147 192 assert assigns(:versions).include?(Version.find(3))
148 193 # Completed version appears
149 194 assert assigns(:versions).include?(Version.find(1))
150 195 end
151 196
152 197 def test_project_activity
153 198 get :activity, :id => 1, :with_subprojects => 0
154 199 assert_response :success
155 200 assert_template 'activity'
156 201 assert_not_nil assigns(:events_by_day)
157 202
158 203 assert_tag :tag => "h3",
159 204 :content => /#{2.days.ago.to_date.day}/,
160 205 :sibling => { :tag => "dl",
161 206 :child => { :tag => "dt",
162 207 :attributes => { :class => /issue-edit/ },
163 208 :child => { :tag => "a",
164 209 :content => /(#{IssueStatus.find(2).name})/,
165 210 }
166 211 }
167 212 }
168 213 end
169 214
170 215 def test_previous_project_activity
171 216 get :activity, :id => 1, :from => 3.days.ago.to_date
172 217 assert_response :success
173 218 assert_template 'activity'
174 219 assert_not_nil assigns(:events_by_day)
175 220
176 221 assert_tag :tag => "h3",
177 222 :content => /#{3.day.ago.to_date.day}/,
178 223 :sibling => { :tag => "dl",
179 224 :child => { :tag => "dt",
180 225 :attributes => { :class => /issue/ },
181 226 :child => { :tag => "a",
182 227 :content => /#{Issue.find(1).subject}/,
183 228 }
184 229 }
185 230 }
186 231 end
187 232
188 233 def test_global_activity
189 234 get :activity
190 235 assert_response :success
191 236 assert_template 'activity'
192 237 assert_not_nil assigns(:events_by_day)
193 238
194 239 assert_tag :tag => "h3",
195 240 :content => /#{5.day.ago.to_date.day}/,
196 241 :sibling => { :tag => "dl",
197 242 :child => { :tag => "dt",
198 243 :attributes => { :class => /issue/ },
199 244 :child => { :tag => "a",
200 245 :content => /#{Issue.find(5).subject}/,
201 246 }
202 247 }
203 248 }
204 249 end
205 250
206 251 def test_user_activity
207 252 get :activity, :user_id => 2
208 253 assert_response :success
209 254 assert_template 'activity'
210 255 assert_not_nil assigns(:events_by_day)
211 256
212 257 assert_tag :tag => "h3",
213 258 :content => /#{3.day.ago.to_date.day}/,
214 259 :sibling => { :tag => "dl",
215 260 :child => { :tag => "dt",
216 261 :attributes => { :class => /issue/ },
217 262 :child => { :tag => "a",
218 263 :content => /#{Issue.find(1).subject}/,
219 264 }
220 265 }
221 266 }
222 267 end
223 268
224 269 def test_activity_atom_feed
225 270 get :activity, :format => 'atom'
226 271 assert_response :success
227 272 assert_template 'common/feed.atom.rxml'
228 273 end
229 274
230 275 def test_archive
231 276 @request.session[:user_id] = 1 # admin
232 277 post :archive, :id => 1
233 278 assert_redirected_to 'admin/projects'
234 279 assert !Project.find(1).active?
235 280 end
236 281
237 282 def test_unarchive
238 283 @request.session[:user_id] = 1 # admin
239 284 Project.find(1).archive
240 285 post :unarchive, :id => 1
241 286 assert_redirected_to 'admin/projects'
242 287 assert Project.find(1).active?
243 288 end
244 289
245 290 def test_project_menu
246 291 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
247 292 Redmine::MenuManager.map :project_menu do |menu|
248 293 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
249 294 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
250 295 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
251 296 end
252 297
253 298 get :show, :id => 1
254 299 assert_tag :div, :attributes => { :id => 'main-menu' },
255 300 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
256 301 :attributes => { :class => 'foo' } } }
257 302
258 303 assert_tag :div, :attributes => { :id => 'main-menu' },
259 304 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
260 305 :attributes => { :class => 'bar' } },
261 306 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
262 307
263 308 assert_tag :div, :attributes => { :id => 'main-menu' },
264 309 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
265 310 :attributes => { :class => 'hello' } },
266 311 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
267 312
268 313 # Remove the menu items
269 314 Redmine::MenuManager.map :project_menu do |menu|
270 315 menu.delete :foo
271 316 menu.delete :bar
272 317 menu.delete :hello
273 318 end
274 319 end
275 320 end
276 321
277 322 # A hook that is manually registered later
278 323 class ProjectBasedTemplate < Redmine::Hook::ViewListener
279 324 def view_layouts_base_html_head(context)
280 325 # Adds a project stylesheet
281 326 stylesheet_link_tag(context[:project].identifier) if context[:project]
282 327 end
283 328 end
284 329 # Don't use this hook now
285 330 Redmine::Hook.clear_listeners
286 331
287 332 def test_hook_response
288 333 Redmine::Hook.add_listener(ProjectBasedTemplate)
289 334 get :show, :id => 1
290 335 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
291 336 :parent => {:tag => 'head'}
292 337
293 338 Redmine::Hook.clear_listeners
294 339 end
295 340 end
General Comments 0
You need to be logged in to leave comments. Login now