##// END OF EJS Templates
Display latest user's activity on account/show view....
Jean-Philippe Lang -
r2064:fce4615f10ad
parent child
Show More
@@ -1,190 +1,194
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AccountController < ApplicationController
19 19 helper :custom_fields
20 20 include CustomFieldsHelper
21 21
22 22 # prevents login action to be filtered by check_if_login_required application scope filter
23 23 skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
24 24
25 25 # Show user's account
26 26 def show
27 27 @user = User.find_active(params[:id])
28 28 @custom_values = @user.custom_values
29 29
30 30 # show only public projects and private projects that the logged in user is also a member of
31 31 @memberships = @user.memberships.select do |membership|
32 32 membership.project.is_public? || (User.current.member_of?(membership.project))
33 33 end
34
35 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
36 @events_by_day = events.group_by(&:event_date)
37
34 38 rescue ActiveRecord::RecordNotFound
35 39 render_404
36 40 end
37 41
38 42 # Login request and validation
39 43 def login
40 44 if request.get?
41 45 # Logout user
42 46 self.logged_user = nil
43 47 else
44 48 # Authenticate user
45 49 user = User.try_to_login(params[:username], params[:password])
46 50 if user.nil?
47 51 # Invalid credentials
48 52 flash.now[:error] = l(:notice_account_invalid_creditentials)
49 53 elsif user.new_record?
50 54 # Onthefly creation failed, display the registration form to fill/fix attributes
51 55 @user = user
52 56 session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
53 57 render :action => 'register'
54 58 else
55 59 # Valid user
56 60 self.logged_user = user
57 61 # generate a key and set cookie if autologin
58 62 if params[:autologin] && Setting.autologin?
59 63 token = Token.create(:user => user, :action => 'autologin')
60 64 cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
61 65 end
62 66 redirect_back_or_default :controller => 'my', :action => 'page'
63 67 end
64 68 end
65 69 end
66 70
67 71 # Log out current user and redirect to welcome page
68 72 def logout
69 73 cookies.delete :autologin
70 74 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
71 75 self.logged_user = nil
72 76 redirect_to home_url
73 77 end
74 78
75 79 # Enable user to choose a new password
76 80 def lost_password
77 81 redirect_to(home_url) && return unless Setting.lost_password?
78 82 if params[:token]
79 83 @token = Token.find_by_action_and_value("recovery", params[:token])
80 84 redirect_to(home_url) && return unless @token and !@token.expired?
81 85 @user = @token.user
82 86 if request.post?
83 87 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
84 88 if @user.save
85 89 @token.destroy
86 90 flash[:notice] = l(:notice_account_password_updated)
87 91 redirect_to :action => 'login'
88 92 return
89 93 end
90 94 end
91 95 render :template => "account/password_recovery"
92 96 return
93 97 else
94 98 if request.post?
95 99 user = User.find_by_mail(params[:mail])
96 100 # user not found in db
97 101 flash.now[:error] = l(:notice_account_unknown_email) and return unless user
98 102 # user uses an external authentification
99 103 flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
100 104 # create a new token for password recovery
101 105 token = Token.new(:user => user, :action => "recovery")
102 106 if token.save
103 107 Mailer.deliver_lost_password(token)
104 108 flash[:notice] = l(:notice_account_lost_email_sent)
105 109 redirect_to :action => 'login'
106 110 return
107 111 end
108 112 end
109 113 end
110 114 end
111 115
112 116 # User self-registration
113 117 def register
114 118 redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
115 119 if request.get?
116 120 session[:auth_source_registration] = nil
117 121 @user = User.new(:language => Setting.default_language)
118 122 else
119 123 @user = User.new(params[:user])
120 124 @user.admin = false
121 125 @user.status = User::STATUS_REGISTERED
122 126 if session[:auth_source_registration]
123 127 @user.status = User::STATUS_ACTIVE
124 128 @user.login = session[:auth_source_registration][:login]
125 129 @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
126 130 if @user.save
127 131 session[:auth_source_registration] = nil
128 132 self.logged_user = @user
129 133 flash[:notice] = l(:notice_account_activated)
130 134 redirect_to :controller => 'my', :action => 'account'
131 135 end
132 136 else
133 137 @user.login = params[:user][:login]
134 138 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
135 139 case Setting.self_registration
136 140 when '1'
137 141 # Email activation
138 142 token = Token.new(:user => @user, :action => "register")
139 143 if @user.save and token.save
140 144 Mailer.deliver_register(token)
141 145 flash[:notice] = l(:notice_account_register_done)
142 146 redirect_to :action => 'login'
143 147 end
144 148 when '3'
145 149 # Automatic activation
146 150 @user.status = User::STATUS_ACTIVE
147 151 if @user.save
148 152 self.logged_user = @user
149 153 flash[:notice] = l(:notice_account_activated)
150 154 redirect_to :controller => 'my', :action => 'account'
151 155 end
152 156 else
153 157 # Manual activation by the administrator
154 158 if @user.save
155 159 # Sends an email to the administrators
156 160 Mailer.deliver_account_activation_request(@user)
157 161 flash[:notice] = l(:notice_account_pending)
158 162 redirect_to :action => 'login'
159 163 end
160 164 end
161 165 end
162 166 end
163 167 end
164 168
165 169 # Token based account activation
166 170 def activate
167 171 redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
168 172 token = Token.find_by_action_and_value('register', params[:token])
169 173 redirect_to(home_url) && return unless token and !token.expired?
170 174 user = token.user
171 175 redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
172 176 user.status = User::STATUS_ACTIVE
173 177 if user.save
174 178 token.destroy
175 179 flash[:notice] = l(:notice_account_activated)
176 180 end
177 181 redirect_to :action => 'login'
178 182 end
179 183
180 184 private
181 185 def logged_user=(user)
182 186 if user && user.is_a?(User)
183 187 User.current = user
184 188 session[:user_id] = user.id
185 189 else
186 190 User.current = User.anonymous
187 191 session[:user_id] = nil
188 192 end
189 193 end
190 194 end
@@ -1,605 +1,617
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
22 22 module ApplicationHelper
23 23 include Redmine::WikiFormatting::Macros::Definitions
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 def current_role
30 30 @current_role ||= User.current.role_for_project(@project)
31 31 end
32 32
33 33 # Return true if user is authorized for controller/action, otherwise false
34 34 def authorize_for(controller, action)
35 35 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 36 end
37 37
38 38 # Display a link if user is authorized
39 39 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 40 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 41 end
42 42
43 43 # Display a link to remote if user is authorized
44 44 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 45 url = options[:url] || {}
46 46 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 47 end
48 48
49 49 # Display a link to user's account page
50 50 def link_to_user(user)
51 51 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
52 52 end
53 53
54 54 def link_to_issue(issue, options={})
55 55 options[:class] ||= ''
56 56 options[:class] << ' issue'
57 57 options[:class] << ' closed' if issue.closed?
58 58 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
59 59 end
60 60
61 61 # Generates a link to an attachment.
62 62 # Options:
63 63 # * :text - Link text (default to attachment filename)
64 64 # * :download - Force download (default: false)
65 65 def link_to_attachment(attachment, options={})
66 66 text = options.delete(:text) || attachment.filename
67 67 action = options.delete(:download) ? 'download' : 'show'
68 68
69 69 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
70 70 end
71 71
72 72 def toggle_link(name, id, options={})
73 73 onclick = "Element.toggle('#{id}'); "
74 74 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
75 75 onclick << "return false;"
76 76 link_to(name, "#", :onclick => onclick)
77 77 end
78 78
79 79 def image_to_function(name, function, html_options = {})
80 80 html_options.symbolize_keys!
81 81 tag(:input, html_options.merge({
82 82 :type => "image", :src => image_path(name),
83 83 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
84 84 }))
85 85 end
86 86
87 87 def prompt_to_remote(name, text, param, url, html_options = {})
88 88 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
89 89 link_to name, {}, html_options
90 90 end
91 91
92 92 def format_date(date)
93 93 return nil unless date
94 94 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
95 95 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
96 96 date.strftime(@date_format)
97 97 end
98 98
99 99 def format_time(time, include_date = true)
100 100 return nil unless time
101 101 time = time.to_time if time.is_a?(String)
102 102 zone = User.current.time_zone
103 103 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
104 104 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
105 105 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
106 106 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
107 107 end
108
109 def format_activity_title(text)
110 h(truncate_single_line(text, 100))
111 end
112
113 def format_activity_day(date)
114 date == Date.today ? l(:label_today).titleize : format_date(date)
115 end
116
117 def format_activity_description(text)
118 h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
119 end
108 120
109 121 def distance_of_date_in_words(from_date, to_date = 0)
110 122 from_date = from_date.to_date if from_date.respond_to?(:to_date)
111 123 to_date = to_date.to_date if to_date.respond_to?(:to_date)
112 124 distance_in_days = (to_date - from_date).abs
113 125 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
114 126 end
115 127
116 128 def due_date_distance_in_words(date)
117 129 if date
118 130 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
119 131 end
120 132 end
121 133
122 134 def render_page_hierarchy(pages, node=nil)
123 135 content = ''
124 136 if pages[node]
125 137 content << "<ul class=\"pages-hierarchy\">\n"
126 138 pages[node].each do |page|
127 139 content << "<li>"
128 140 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
129 141 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
130 142 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
131 143 content << "</li>\n"
132 144 end
133 145 content << "</ul>\n"
134 146 end
135 147 content
136 148 end
137 149
138 150 # Truncates and returns the string as a single line
139 151 def truncate_single_line(string, *args)
140 152 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
141 153 end
142 154
143 155 def html_hours(text)
144 156 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
145 157 end
146 158
147 159 def authoring(created, author)
148 160 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
149 161 link_to(distance_of_time_in_words(Time.now, created),
150 162 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
151 163 :title => format_time(created))
152 164 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
153 165 l(:label_added_time_by, author_tag, time_tag)
154 166 end
155 167
156 168 def l_or_humanize(s, options={})
157 169 k = "#{options[:prefix]}#{s}".to_sym
158 170 l_has_string?(k) ? l(k) : s.to_s.humanize
159 171 end
160 172
161 173 def day_name(day)
162 174 l(:general_day_names).split(',')[day-1]
163 175 end
164 176
165 177 def month_name(month)
166 178 l(:actionview_datehelper_select_month_names).split(',')[month-1]
167 179 end
168 180
169 181 def syntax_highlight(name, content)
170 182 type = CodeRay::FileType[name]
171 183 type ? CodeRay.scan(content, type).html : h(content)
172 184 end
173 185
174 186 def to_path_param(path)
175 187 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
176 188 end
177 189
178 190 def pagination_links_full(paginator, count=nil, options={})
179 191 page_param = options.delete(:page_param) || :page
180 192 url_param = params.dup
181 193 # don't reuse params if filters are present
182 194 url_param.clear if url_param.has_key?(:set_filter)
183 195
184 196 html = ''
185 197 html << link_to_remote(('&#171; ' + l(:label_previous)),
186 198 {:update => 'content',
187 199 :url => url_param.merge(page_param => paginator.current.previous),
188 200 :complete => 'window.scrollTo(0,0)'},
189 201 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
190 202
191 203 html << (pagination_links_each(paginator, options) do |n|
192 204 link_to_remote(n.to_s,
193 205 {:url => {:params => url_param.merge(page_param => n)},
194 206 :update => 'content',
195 207 :complete => 'window.scrollTo(0,0)'},
196 208 {:href => url_for(:params => url_param.merge(page_param => n))})
197 209 end || '')
198 210
199 211 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
200 212 {:update => 'content',
201 213 :url => url_param.merge(page_param => paginator.current.next),
202 214 :complete => 'window.scrollTo(0,0)'},
203 215 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
204 216
205 217 unless count.nil?
206 218 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
207 219 end
208 220
209 221 html
210 222 end
211 223
212 224 def per_page_links(selected=nil)
213 225 url_param = params.dup
214 226 url_param.clear if url_param.has_key?(:set_filter)
215 227
216 228 links = Setting.per_page_options_array.collect do |n|
217 229 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
218 230 {:href => url_for(url_param.merge(:per_page => n))})
219 231 end
220 232 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
221 233 end
222 234
223 235 def breadcrumb(*args)
224 236 elements = args.flatten
225 237 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
226 238 end
227 239
228 240 def html_title(*args)
229 241 if args.empty?
230 242 title = []
231 243 title << @project.name if @project
232 244 title += @html_title if @html_title
233 245 title << Setting.app_title
234 246 title.compact.join(' - ')
235 247 else
236 248 @html_title ||= []
237 249 @html_title += args
238 250 end
239 251 end
240 252
241 253 def accesskey(s)
242 254 Redmine::AccessKeys.key_for s
243 255 end
244 256
245 257 # Formats text according to system settings.
246 258 # 2 ways to call this method:
247 259 # * with a String: textilizable(text, options)
248 260 # * with an object and one of its attribute: textilizable(issue, :description, options)
249 261 def textilizable(*args)
250 262 options = args.last.is_a?(Hash) ? args.pop : {}
251 263 case args.size
252 264 when 1
253 265 obj = options[:object]
254 266 text = args.shift
255 267 when 2
256 268 obj = args.shift
257 269 text = obj.send(args.shift).to_s
258 270 else
259 271 raise ArgumentError, 'invalid arguments to textilizable'
260 272 end
261 273 return '' if text.blank?
262 274
263 275 only_path = options.delete(:only_path) == false ? false : true
264 276
265 277 # when using an image link, try to use an attachment, if possible
266 278 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
267 279
268 280 if attachments
269 281 attachments = attachments.sort_by(&:created_on).reverse
270 282 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
271 283 style = $1
272 284 filename = $6
273 285 rf = Regexp.new(Regexp.escape(filename), Regexp::IGNORECASE)
274 286 # search for the picture in attachments
275 287 if found = attachments.detect { |att| att.filename =~ rf }
276 288 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
277 289 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
278 290 alt = desc.blank? ? nil : "(#{desc})"
279 291 "!#{style}#{image_url}#{alt}!"
280 292 else
281 293 "!#{style}#{filename}!"
282 294 end
283 295 end
284 296 end
285 297
286 298 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
287 299
288 300 # different methods for formatting wiki links
289 301 case options[:wiki_links]
290 302 when :local
291 303 # used for local links to html files
292 304 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
293 305 when :anchor
294 306 # used for single-file wiki export
295 307 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
296 308 else
297 309 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
298 310 end
299 311
300 312 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
301 313
302 314 # Wiki links
303 315 #
304 316 # Examples:
305 317 # [[mypage]]
306 318 # [[mypage|mytext]]
307 319 # wiki links can refer other project wikis, using project name or identifier:
308 320 # [[project:]] -> wiki starting page
309 321 # [[project:|mytext]]
310 322 # [[project:mypage]]
311 323 # [[project:mypage|mytext]]
312 324 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
313 325 link_project = project
314 326 esc, all, page, title = $1, $2, $3, $5
315 327 if esc.nil?
316 328 if page =~ /^([^\:]+)\:(.*)$/
317 329 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
318 330 page = $2
319 331 title ||= $1 if page.blank?
320 332 end
321 333
322 334 if link_project && link_project.wiki
323 335 # extract anchor
324 336 anchor = nil
325 337 if page =~ /^(.+?)\#(.+)$/
326 338 page, anchor = $1, $2
327 339 end
328 340 # check if page exists
329 341 wiki_page = link_project.wiki.find_page(page)
330 342 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
331 343 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
332 344 else
333 345 # project or wiki doesn't exist
334 346 title || page
335 347 end
336 348 else
337 349 all
338 350 end
339 351 end
340 352
341 353 # Redmine links
342 354 #
343 355 # Examples:
344 356 # Issues:
345 357 # #52 -> Link to issue #52
346 358 # Changesets:
347 359 # r52 -> Link to revision 52
348 360 # commit:a85130f -> Link to scmid starting with a85130f
349 361 # Documents:
350 362 # document#17 -> Link to document with id 17
351 363 # document:Greetings -> Link to the document with title "Greetings"
352 364 # document:"Some document" -> Link to the document with title "Some document"
353 365 # Versions:
354 366 # version#3 -> Link to version with id 3
355 367 # version:1.0.0 -> Link to version named "1.0.0"
356 368 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
357 369 # Attachments:
358 370 # attachment:file.zip -> Link to the attachment of the current object named file.zip
359 371 # Source files:
360 372 # source:some/file -> Link to the file located at /some/file in the project's repository
361 373 # source:some/file@52 -> Link to the file's revision 52
362 374 # source:some/file#L120 -> Link to line 120 of the file
363 375 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
364 376 # export:some/file -> Force the download of the file
365 377 # Forum messages:
366 378 # message#1218 -> Link to message with id 1218
367 379 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
368 380 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
369 381 link = nil
370 382 if esc.nil?
371 383 if prefix.nil? && sep == 'r'
372 384 if project && (changeset = project.changesets.find_by_revision(oid))
373 385 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
374 386 :class => 'changeset',
375 387 :title => truncate_single_line(changeset.comments, 100))
376 388 end
377 389 elsif sep == '#'
378 390 oid = oid.to_i
379 391 case prefix
380 392 when nil
381 393 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
382 394 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
383 395 :class => (issue.closed? ? 'issue closed' : 'issue'),
384 396 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
385 397 link = content_tag('del', link) if issue.closed?
386 398 end
387 399 when 'document'
388 400 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
389 401 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
390 402 :class => 'document'
391 403 end
392 404 when 'version'
393 405 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
394 406 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
395 407 :class => 'version'
396 408 end
397 409 when 'message'
398 410 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
399 411 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
400 412 :controller => 'messages',
401 413 :action => 'show',
402 414 :board_id => message.board,
403 415 :id => message.root,
404 416 :anchor => (message.parent ? "message-#{message.id}" : nil)},
405 417 :class => 'message'
406 418 end
407 419 end
408 420 elsif sep == ':'
409 421 # removes the double quotes if any
410 422 name = oid.gsub(%r{^"(.*)"$}, "\\1")
411 423 case prefix
412 424 when 'document'
413 425 if project && document = project.documents.find_by_title(name)
414 426 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
415 427 :class => 'document'
416 428 end
417 429 when 'version'
418 430 if project && version = project.versions.find_by_name(name)
419 431 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
420 432 :class => 'version'
421 433 end
422 434 when 'commit'
423 435 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
424 436 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
425 437 :class => 'changeset',
426 438 :title => truncate_single_line(changeset.comments, 100)
427 439 end
428 440 when 'source', 'export'
429 441 if project && project.repository
430 442 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
431 443 path, rev, anchor = $1, $3, $5
432 444 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
433 445 :path => to_path_param(path),
434 446 :rev => rev,
435 447 :anchor => anchor,
436 448 :format => (prefix == 'export' ? 'raw' : nil)},
437 449 :class => (prefix == 'export' ? 'source download' : 'source')
438 450 end
439 451 when 'attachment'
440 452 if attachments && attachment = attachments.detect {|a| a.filename == name }
441 453 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
442 454 :class => 'attachment'
443 455 end
444 456 end
445 457 end
446 458 end
447 459 leading + (link || "#{prefix}#{sep}#{oid}")
448 460 end
449 461
450 462 text
451 463 end
452 464
453 465 # Same as Rails' simple_format helper without using paragraphs
454 466 def simple_format_without_paragraph(text)
455 467 text.to_s.
456 468 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
457 469 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
458 470 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
459 471 end
460 472
461 473 def error_messages_for(object_name, options = {})
462 474 options = options.symbolize_keys
463 475 object = instance_variable_get("@#{object_name}")
464 476 if object && !object.errors.empty?
465 477 # build full_messages here with controller current language
466 478 full_messages = []
467 479 object.errors.each do |attr, msg|
468 480 next if msg.nil?
469 481 msg = msg.first if msg.is_a? Array
470 482 if attr == "base"
471 483 full_messages << l(msg)
472 484 else
473 485 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
474 486 end
475 487 end
476 488 # retrieve custom values error messages
477 489 if object.errors[:custom_values]
478 490 object.custom_values.each do |v|
479 491 v.errors.each do |attr, msg|
480 492 next if msg.nil?
481 493 msg = msg.first if msg.is_a? Array
482 494 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
483 495 end
484 496 end
485 497 end
486 498 content_tag("div",
487 499 content_tag(
488 500 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
489 501 ) +
490 502 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
491 503 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
492 504 )
493 505 else
494 506 ""
495 507 end
496 508 end
497 509
498 510 def lang_options_for_select(blank=true)
499 511 (blank ? [["(auto)", ""]] : []) +
500 512 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
501 513 end
502 514
503 515 def label_tag_for(name, option_tags = nil, options = {})
504 516 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
505 517 content_tag("label", label_text)
506 518 end
507 519
508 520 def labelled_tabular_form_for(name, object, options, &proc)
509 521 options[:html] ||= {}
510 522 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
511 523 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
512 524 end
513 525
514 526 def back_url_hidden_field_tag
515 527 back_url = params[:back_url] || request.env['HTTP_REFERER']
516 528 hidden_field_tag('back_url', back_url) unless back_url.blank?
517 529 end
518 530
519 531 def check_all_links(form_name)
520 532 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
521 533 " | " +
522 534 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
523 535 end
524 536
525 537 def progress_bar(pcts, options={})
526 538 pcts = [pcts, pcts] unless pcts.is_a?(Array)
527 539 pcts[1] = pcts[1] - pcts[0]
528 540 pcts << (100 - pcts[1] - pcts[0])
529 541 width = options[:width] || '100px;'
530 542 legend = options[:legend] || ''
531 543 content_tag('table',
532 544 content_tag('tr',
533 545 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
534 546 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
535 547 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
536 548 ), :class => 'progress', :style => "width: #{width};") +
537 549 content_tag('p', legend, :class => 'pourcent')
538 550 end
539 551
540 552 def context_menu_link(name, url, options={})
541 553 options[:class] ||= ''
542 554 if options.delete(:selected)
543 555 options[:class] << ' icon-checked disabled'
544 556 options[:disabled] = true
545 557 end
546 558 if options.delete(:disabled)
547 559 options.delete(:method)
548 560 options.delete(:confirm)
549 561 options.delete(:onclick)
550 562 options[:class] << ' disabled'
551 563 url = '#'
552 564 end
553 565 link_to name, url, options
554 566 end
555 567
556 568 def calendar_for(field_id)
557 569 include_calendar_headers_tags
558 570 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
559 571 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
560 572 end
561 573
562 574 def include_calendar_headers_tags
563 575 unless @calendar_headers_tags_included
564 576 @calendar_headers_tags_included = true
565 577 content_for :header_tags do
566 578 javascript_include_tag('calendar/calendar') +
567 579 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
568 580 javascript_include_tag('calendar/calendar-setup') +
569 581 stylesheet_link_tag('calendar')
570 582 end
571 583 end
572 584 end
573 585
574 586 def content_for(name, content = nil, &block)
575 587 @has_content ||= {}
576 588 @has_content[name] = true
577 589 super(name, content, &block)
578 590 end
579 591
580 592 def has_content?(name)
581 593 (@has_content && @has_content[name]) || false
582 594 end
583 595
584 596 # Returns the avatar image tag for the given +user+ if avatars are enabled
585 597 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
586 598 def avatar(user, options = { })
587 599 if Setting.gravatar_enabled?
588 600 email = nil
589 601 if user.respond_to?(:mail)
590 602 email = user.mail
591 603 elsif user.to_s =~ %r{<(.+?)>}
592 604 email = $1
593 605 end
594 606 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
595 607 end
596 608 end
597 609
598 610 private
599 611
600 612 def wiki_helper
601 613 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
602 614 extend helper
603 615 return self
604 616 end
605 617 end
@@ -1,48 +1,36
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module ProjectsHelper
19 19 def link_to_version(version, options = {})
20 20 return '' unless version && version.is_a?(Version)
21 21 link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
22 22 end
23 23
24 def format_activity_title(text)
25 h(truncate_single_line(text, 100))
26 end
27
28 def format_activity_day(date)
29 date == Date.today ? l(:label_today).titleize : format_date(date)
30 end
31
32 def format_activity_description(text)
33 h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
34 end
35
36 24 def project_settings_tabs
37 25 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
38 26 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
39 27 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
40 28 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
41 29 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
42 30 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
43 31 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
44 32 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
45 33 ]
46 34 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
47 35 end
48 36 end
@@ -1,134 +1,136
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/md5"
19 19
20 20 class Attachment < ActiveRecord::Base
21 21 belongs_to :container, :polymorphic => true
22 22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 23
24 24 validates_presence_of :container, :filename, :author
25 25 validates_length_of :filename, :maximum => 255
26 26 validates_length_of :disk_filename, :maximum => 255
27 27
28 28 acts_as_event :title => :filename,
29 29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 30
31 31 acts_as_activity_provider :type => 'files',
32 32 :permission => :view_files,
33 :author_key => :author_id,
33 34 :find_options => {:select => "#{Attachment.table_name}.*",
34 35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
35 36 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
36 37
37 38 acts_as_activity_provider :type => 'documents',
38 39 :permission => :view_documents,
40 :author_key => :author_id,
39 41 :find_options => {:select => "#{Attachment.table_name}.*",
40 42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
41 43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
42 44
43 45 cattr_accessor :storage_path
44 46 @@storage_path = "#{RAILS_ROOT}/files"
45 47
46 48 def validate
47 49 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
48 50 end
49 51
50 52 def file=(incoming_file)
51 53 unless incoming_file.nil?
52 54 @temp_file = incoming_file
53 55 if @temp_file.size > 0
54 56 self.filename = sanitize_filename(@temp_file.original_filename)
55 57 self.disk_filename = Attachment.disk_filename(filename)
56 58 self.content_type = @temp_file.content_type.to_s.chomp
57 59 self.filesize = @temp_file.size
58 60 end
59 61 end
60 62 end
61 63
62 64 def file
63 65 nil
64 66 end
65 67
66 68 # Copy temp file to its final location
67 69 def before_save
68 70 if @temp_file && (@temp_file.size > 0)
69 71 logger.debug("saving '#{self.diskfile}'")
70 72 File.open(diskfile, "wb") do |f|
71 73 f.write(@temp_file.read)
72 74 end
73 75 self.digest = Digest::MD5.hexdigest(File.read(diskfile))
74 76 end
75 77 # Don't save the content type if it's longer than the authorized length
76 78 if self.content_type && self.content_type.length > 255
77 79 self.content_type = nil
78 80 end
79 81 end
80 82
81 83 # Deletes file on the disk
82 84 def after_destroy
83 85 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
84 86 end
85 87
86 88 # Returns file's location on disk
87 89 def diskfile
88 90 "#{@@storage_path}/#{self.disk_filename}"
89 91 end
90 92
91 93 def increment_download
92 94 increment!(:downloads)
93 95 end
94 96
95 97 def project
96 98 container.project
97 99 end
98 100
99 101 def image?
100 102 self.filename =~ /\.(jpe?g|gif|png)$/i
101 103 end
102 104
103 105 def is_text?
104 106 Redmine::MimeType.is_type?('text', filename)
105 107 end
106 108
107 109 def is_diff?
108 110 self.filename =~ /\.(patch|diff)$/i
109 111 end
110 112
111 113 private
112 114 def sanitize_filename(value)
113 115 # get only the filename, not the whole path
114 116 just_filename = value.gsub(/^.*(\\|\/)/, '')
115 117 # NOTE: File.basename doesn't work right with Windows paths on Unix
116 118 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
117 119
118 120 # Finally, replace all non alphanumeric, hyphens or periods with underscore
119 121 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
120 122 end
121 123
122 124 # Returns an ASCII or hashed filename
123 125 def self.disk_filename(filename)
124 126 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
125 127 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
126 128 df << filename
127 129 else
128 130 df << Digest::MD5.hexdigest(filename)
129 131 # keep the extension if any
130 132 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
131 133 end
132 134 df
133 135 end
134 136 end
@@ -1,154 +1,155
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 'iconv'
19 19
20 20 class Changeset < ActiveRecord::Base
21 21 belongs_to :repository
22 22 belongs_to :user
23 23 has_many :changes, :dependent => :delete_all
24 24 has_and_belongs_to_many :issues
25 25
26 26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
27 27 :description => :comments,
28 28 :datetime => :committed_on,
29 29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
30 30
31 31 acts_as_searchable :columns => 'comments',
32 32 :include => {:repository => :project},
33 33 :project_key => "#{Repository.table_name}.project_id",
34 34 :date_column => 'committed_on'
35 35
36 36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 :author_key => :user_id,
37 38 :find_options => {:include => {:repository => :project}}
38 39
39 40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 41 validates_uniqueness_of :revision, :scope => :repository_id
41 42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42 43
43 44 def revision=(r)
44 45 write_attribute :revision, (r.nil? ? nil : r.to_s)
45 46 end
46 47
47 48 def comments=(comment)
48 49 write_attribute(:comments, Changeset.normalize_comments(comment))
49 50 end
50 51
51 52 def committed_on=(date)
52 53 self.commit_date = date
53 54 super
54 55 end
55 56
56 57 def project
57 58 repository.project
58 59 end
59 60
60 61 def author
61 62 user || committer.to_s.split('<').first
62 63 end
63 64
64 65 def before_create
65 66 self.user = repository.find_committer_user(committer)
66 67 end
67 68
68 69 def after_create
69 70 scan_comment_for_issue_ids
70 71 end
71 72 require 'pp'
72 73
73 74 def scan_comment_for_issue_ids
74 75 return if comments.blank?
75 76 # keywords used to reference issues
76 77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
77 78 # keywords used to fix issues
78 79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
79 80 # status and optional done ratio applied
80 81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
81 82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
82 83
83 84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
84 85 return if kw_regexp.blank?
85 86
86 87 referenced_issues = []
87 88
88 89 if ref_keywords.delete('*')
89 90 # find any issue ID in the comments
90 91 target_issue_ids = []
91 92 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
92 93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
93 94 end
94 95
95 96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
96 97 action = match[0]
97 98 target_issue_ids = match[1].scan(/\d+/)
98 99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
99 100 if fix_status && fix_keywords.include?(action.downcase)
100 101 # update status of issues
101 102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
102 103 target_issues.each do |issue|
103 104 # the issue may have been updated by the closure of another one (eg. duplicate)
104 105 issue.reload
105 106 # don't change the status is the issue is closed
106 107 next if issue.status.is_closed?
107 108 csettext = "r#{self.revision}"
108 109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
109 110 csettext = "commit:\"#{self.scmid}\""
110 111 end
111 112 journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
112 113 issue.status = fix_status
113 114 issue.done_ratio = done_ratio if done_ratio
114 115 issue.save
115 116 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
116 117 end
117 118 end
118 119 referenced_issues += target_issues
119 120 end
120 121
121 122 self.issues = referenced_issues.uniq
122 123 end
123 124
124 125 # Returns the previous changeset
125 126 def previous
126 127 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
127 128 end
128 129
129 130 # Returns the next changeset
130 131 def next
131 132 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
132 133 end
133 134
134 135 # Strips and reencodes a commit log before insertion into the database
135 136 def self.normalize_comments(str)
136 137 to_utf8(str.to_s.strip)
137 138 end
138 139
139 140 private
140 141
141 142
142 143 def self.to_utf8(str)
143 144 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
144 145 encoding = Setting.commit_logs_encoding.to_s.strip
145 146 unless encoding.blank? || encoding == 'UTF-8'
146 147 begin
147 148 return Iconv.conv('UTF-8', encoding, str)
148 149 rescue Iconv::Failure
149 150 # do nothing here
150 151 end
151 152 end
152 153 str
153 154 end
154 155 end
@@ -1,263 +1,264
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 :attachments, :as => :container, :dependent => :destroy
30 30 has_many :time_entries, :dependent => :delete_all
31 31 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 32
33 33 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 34 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 35
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
45 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}
45 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 :author_key => :author_id
46 47
47 48 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
48 49 validates_length_of :subject, :maximum => 255
49 50 validates_inclusion_of :done_ratio, :in => 0..100
50 51 validates_numericality_of :estimated_hours, :allow_nil => true
51 52
52 53 def after_initialize
53 54 if new_record?
54 55 # set default values for new records only
55 56 self.status ||= IssueStatus.default
56 57 self.priority ||= Enumeration.default('IPRI')
57 58 end
58 59 end
59 60
60 61 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
61 62 def available_custom_fields
62 63 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
63 64 end
64 65
65 66 def copy_from(arg)
66 67 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
67 68 self.attributes = issue.attributes.dup
68 69 self.custom_values = issue.custom_values.collect {|v| v.clone}
69 70 self
70 71 end
71 72
72 73 # Move an issue to a new project and tracker
73 74 def move_to(new_project, new_tracker = nil)
74 75 transaction do
75 76 if new_project && project_id != new_project.id
76 77 # delete issue relations
77 78 unless Setting.cross_project_issue_relations?
78 79 self.relations_from.clear
79 80 self.relations_to.clear
80 81 end
81 82 # issue is moved to another project
82 83 # reassign to the category with same name if any
83 84 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
84 85 self.category = new_category
85 86 self.fixed_version = nil
86 87 self.project = new_project
87 88 end
88 89 if new_tracker
89 90 self.tracker = new_tracker
90 91 end
91 92 if save
92 93 # Manually update project_id on related time entries
93 94 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
94 95 else
95 96 rollback_db_transaction
96 97 return false
97 98 end
98 99 end
99 100 return true
100 101 end
101 102
102 103 def priority_id=(pid)
103 104 self.priority = nil
104 105 write_attribute(:priority_id, pid)
105 106 end
106 107
107 108 def estimated_hours=(h)
108 109 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
109 110 end
110 111
111 112 def validate
112 113 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
113 114 errors.add :due_date, :activerecord_error_not_a_date
114 115 end
115 116
116 117 if self.due_date and self.start_date and self.due_date < self.start_date
117 118 errors.add :due_date, :activerecord_error_greater_than_start_date
118 119 end
119 120
120 121 if start_date && soonest_start && start_date < soonest_start
121 122 errors.add :start_date, :activerecord_error_invalid
122 123 end
123 124 end
124 125
125 126 def validate_on_create
126 127 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
127 128 end
128 129
129 130 def before_create
130 131 # default assignment based on category
131 132 if assigned_to.nil? && category && category.assigned_to
132 133 self.assigned_to = category.assigned_to
133 134 end
134 135 end
135 136
136 137 def before_save
137 138 if @current_journal
138 139 # attributes changes
139 140 (Issue.column_names - %w(id description)).each {|c|
140 141 @current_journal.details << JournalDetail.new(:property => 'attr',
141 142 :prop_key => c,
142 143 :old_value => @issue_before_change.send(c),
143 144 :value => send(c)) unless send(c)==@issue_before_change.send(c)
144 145 }
145 146 # custom fields changes
146 147 custom_values.each {|c|
147 148 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
148 149 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
149 150 @current_journal.details << JournalDetail.new(:property => 'cf',
150 151 :prop_key => c.custom_field_id,
151 152 :old_value => @custom_values_before_change[c.custom_field_id],
152 153 :value => c.value)
153 154 }
154 155 @current_journal.save
155 156 end
156 157 # Save the issue even if the journal is not saved (because empty)
157 158 true
158 159 end
159 160
160 161 def after_save
161 162 # Reload is needed in order to get the right status
162 163 reload
163 164
164 165 # Update start/due dates of following issues
165 166 relations_from.each(&:set_issue_to_dates)
166 167
167 168 # Close duplicates if the issue was closed
168 169 if @issue_before_change && !@issue_before_change.closed? && self.closed?
169 170 duplicates.each do |duplicate|
170 171 # Reload is need in case the duplicate was updated by a previous duplicate
171 172 duplicate.reload
172 173 # Don't re-close it if it's already closed
173 174 next if duplicate.closed?
174 175 # Same user and notes
175 176 duplicate.init_journal(@current_journal.user, @current_journal.notes)
176 177 duplicate.update_attribute :status, self.status
177 178 end
178 179 end
179 180 end
180 181
181 182 def init_journal(user, notes = "")
182 183 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
183 184 @issue_before_change = self.clone
184 185 @issue_before_change.status = self.status
185 186 @custom_values_before_change = {}
186 187 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
187 188 # Make sure updated_on is updated when adding a note.
188 189 updated_on_will_change!
189 190 @current_journal
190 191 end
191 192
192 193 # Return true if the issue is closed, otherwise false
193 194 def closed?
194 195 self.status.is_closed?
195 196 end
196 197
197 198 # Users the issue can be assigned to
198 199 def assignable_users
199 200 project.assignable_users
200 201 end
201 202
202 203 # Returns an array of status that user is able to apply
203 204 def new_statuses_allowed_to(user)
204 205 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
205 206 statuses << status unless statuses.empty?
206 207 statuses.uniq.sort
207 208 end
208 209
209 210 # Returns the mail adresses of users that should be notified for the issue
210 211 def recipients
211 212 recipients = project.recipients
212 213 # Author and assignee are always notified unless they have been locked
213 214 recipients << author.mail if author && author.active?
214 215 recipients << assigned_to.mail if assigned_to && assigned_to.active?
215 216 recipients.compact.uniq
216 217 end
217 218
218 219 def spent_hours
219 220 @spent_hours ||= time_entries.sum(:hours) || 0
220 221 end
221 222
222 223 def relations
223 224 (relations_from + relations_to).sort
224 225 end
225 226
226 227 def all_dependent_issues
227 228 dependencies = []
228 229 relations_from.each do |relation|
229 230 dependencies << relation.issue_to
230 231 dependencies += relation.issue_to.all_dependent_issues
231 232 end
232 233 dependencies
233 234 end
234 235
235 236 # Returns an array of issues that duplicate this one
236 237 def duplicates
237 238 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
238 239 end
239 240
240 241 # Returns the due date or the target due date if any
241 242 # Used on gantt chart
242 243 def due_before
243 244 due_date || (fixed_version ? fixed_version.effective_date : nil)
244 245 end
245 246
246 247 def duration
247 248 (start_date && due_date) ? due_date - start_date : 0
248 249 end
249 250
250 251 def soonest_start
251 252 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
252 253 end
253 254
254 255 def self.visible_by(usr)
255 256 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
256 257 yield
257 258 end
258 259 end
259 260
260 261 def to_s
261 262 "#{tracker} ##{id}: #{subject}"
262 263 end
263 264 end
@@ -1,67 +1,68
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 Journal < ActiveRecord::Base
19 19 belongs_to :journalized, :polymorphic => true
20 20 # added as a quick fix to allow eager loading of the polymorphic association
21 21 # since always associated to an issue, for now
22 22 belongs_to :issue, :foreign_key => :journalized_id
23 23
24 24 belongs_to :user
25 25 has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
26 26 attr_accessor :indice
27 27
28 28 acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
29 29 :description => :notes,
30 30 :author => :user,
31 31 :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
32 32 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
33 33
34 34 acts_as_activity_provider :type => 'issues',
35 35 :permission => :view_issues,
36 :author_key => :user_id,
36 37 :find_options => {:include => [{:issue => :project}, :details, :user],
37 38 :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
38 39 " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
39 40
40 41 def save
41 42 # Do not save an empty journal
42 43 (details.empty? && notes.blank?) ? false : super
43 44 end
44 45
45 46 # Returns the new status if the journal contains a status change, otherwise nil
46 47 def new_status
47 48 c = details.detect {|detail| detail.prop_key == 'status_id'}
48 49 (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
49 50 end
50 51
51 52 def new_value_for(prop)
52 53 c = details.detect {|detail| detail.prop_key == prop}
53 54 c ? c.value : nil
54 55 end
55 56
56 57 def editable_by?(usr)
57 58 usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
58 59 end
59 60
60 61 def project
61 62 journalized.respond_to?(:project) ? journalized.project : nil
62 63 end
63 64
64 65 def attachments
65 66 journalized.respond_to?(:attachments) ? journalized.attachments : nil
66 67 end
67 68 end
@@ -1,88 +1,89
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 Message < ActiveRecord::Base
19 19 belongs_to :board
20 20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 22 has_many :attachments, :as => :container, :dependent => :destroy
23 23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24 24
25 25 acts_as_searchable :columns => ['subject', 'content'],
26 26 :include => {:board, :project},
27 27 :project_key => 'project_id',
28 28 :date_column => "#{table_name}.created_on"
29 29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 30 :description => :content,
31 31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
32 32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
33 33 {:id => o.parent_id, :anchor => "message-#{o.id}"})}
34 34
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
36 :author_key => :author_id
36 37 acts_as_watchable
37 38
38 39 attr_protected :locked, :sticky
39 40 validates_presence_of :subject, :content
40 41 validates_length_of :subject, :maximum => 255
41 42
42 43 after_create :add_author_as_watcher
43 44
44 45 def validate_on_create
45 46 # Can not reply to a locked topic
46 47 errors.add_to_base 'Topic is locked' if root.locked? && self != root
47 48 end
48 49
49 50 def after_create
50 51 board.update_attribute(:last_message_id, self.id)
51 52 board.increment! :messages_count
52 53 if parent
53 54 parent.reload.update_attribute(:last_reply_id, self.id)
54 55 else
55 56 board.increment! :topics_count
56 57 end
57 58 end
58 59
59 60 def after_destroy
60 61 # The following line is required so that the previous counter
61 62 # updates (due to children removal) are not overwritten
62 63 board.reload
63 64 board.decrement! :messages_count
64 65 board.decrement! :topics_count unless parent
65 66 end
66 67
67 68 def sticky?
68 69 sticky == 1
69 70 end
70 71
71 72 def project
72 73 board.project
73 74 end
74 75
75 76 def editable_by?(usr)
76 77 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
77 78 end
78 79
79 80 def destroyable_by?(usr)
80 81 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
81 82 end
82 83
83 84 private
84 85
85 86 def add_author_as_watcher
86 87 Watcher.create(:watchable => self.root, :user => author)
87 88 end
88 89 end
@@ -1,35 +1,36
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 News < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 21 has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
22 22
23 23 validates_presence_of :title, :description
24 24 validates_length_of :title, :maximum => 60
25 25 validates_length_of :summary, :maximum => 255
26 26
27 27 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
28 28 acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
29 acts_as_activity_provider :find_options => {:include => [:project, :author]}
29 acts_as_activity_provider :find_options => {:include => [:project, :author]},
30 :author_key => :author_id
30 31
31 32 # returns latest news for projects visible by user
32 33 def self.latest(user = User.current, count = 5)
33 34 find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
34 35 end
35 36 end
@@ -1,89 +1,90
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 'zlib'
19 19
20 20 class WikiContent < ActiveRecord::Base
21 21 set_locking_column :version
22 22 belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
23 23 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 24 validates_presence_of :text
25 25
26 26 acts_as_versioned
27 27 class Version
28 28 belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
29 29 belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
30 30 attr_protected :data
31 31
32 32 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
33 33 :description => :comments,
34 34 :datetime => :updated_on,
35 35 :type => 'wiki-page',
36 36 :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
37 37
38 38 acts_as_activity_provider :type => 'wiki_edits',
39 39 :timestamp => "#{WikiContent.versioned_table_name}.updated_on",
40 :author_key => "#{WikiContent.versioned_table_name}.author_id",
40 41 :permission => :view_wiki_edits,
41 42 :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
42 43 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
43 44 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
44 45 "#{WikiContent.versioned_table_name}.id",
45 46 :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
46 47 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
47 48 "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
48 49
49 50 def text=(plain)
50 51 case Setting.wiki_compression
51 52 when 'gzip'
52 53 begin
53 54 self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
54 55 self.compression = 'gzip'
55 56 rescue
56 57 self.data = plain
57 58 self.compression = ''
58 59 end
59 60 else
60 61 self.data = plain
61 62 self.compression = ''
62 63 end
63 64 plain
64 65 end
65 66
66 67 def text
67 68 @text ||= case compression
68 69 when 'gzip'
69 70 Zlib::Inflate.inflate(data)
70 71 else
71 72 # uncompressed data
72 73 data
73 74 end
74 75 end
75 76
76 77 def project
77 78 page.project
78 79 end
79 80
80 81 # Returns the previous version or nil
81 82 def previous
82 83 @previous ||= WikiContent::Version.find(:first,
83 84 :order => 'version DESC',
84 85 :include => :author,
85 86 :conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
86 87 end
87 88 end
88 89
89 90 end
@@ -1,32 +1,57
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 <div class="splitcontentleft">
7 8 <p>
8 9 <%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %>
9 10 <ul>
10 11 <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
11 12 <% for custom_value in @custom_values %>
12 13 <% if !custom_value.value.empty? %>
13 14 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
14 15 <% end %>
15 16 <% end %>
16 17 </ul>
17 18 </p>
18 19
19 20 <% unless @memberships.empty? %>
20 21 <h3><%=l(:label_project_plural)%></h3>
21 22 <ul>
22 23 <% for membership in @memberships %>
23 24 <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %>
24 25 (<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)</li>
25 26 <% end %>
26 27 </ul>
27 28 <% end %>
29 </div>
30
31 <div class="splitcontentright">
28 32
33 <% unless @events_by_day.empty? %>
29 34 <h3><%=l(:label_activity)%></h3>
35
30 36 <p>
31 37 <%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
32 38 </p>
39
40 <div id="activity">
41 <% @events_by_day.keys.sort.reverse.each do |day| %>
42 <h4><%= format_activity_day(day) %></h4>
43 <dl>
44 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
45 <dt class="<%= e.event_type %>">
46 <span class="time"><%= format_time(e.event_datetime, false) %></span>
47 <%= content_tag('span', h(e.project), :class => 'project') %>
48 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
49 <dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd>
50 <% end -%>
51 </dl>
52 <% end -%>
53 </div>
54 <% end %>
55 </div>
56
57 <% html_title @user.name %>
@@ -1,79 +1,85
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 module Redmine
19 19 module Activity
20 20 # Class used to retrieve activity events
21 21 class Fetcher
22 22 attr_reader :user, :project, :scope
23 23
24 24 # Needs to be unloaded in development mode
25 25 @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } }
26 26
27 27 def initialize(user, options={})
28 options.assert_valid_keys(:project, :with_subprojects)
28 options.assert_valid_keys(:project, :with_subprojects, :author)
29 29 @user = user
30 30 @project = options[:project]
31 31 @options = options
32 32
33 33 @scope = event_types
34 34 end
35 35
36 36 # Returns an array of available event types
37 37 def event_types
38 38 return @event_types unless @event_types.nil?
39 39
40 40 @event_types = Redmine::Activity.available_event_types
41 41 @event_types = @event_types.select {|o| @user.allowed_to?("view_#{o}".to_sym, @project)} if @project
42 42 @event_types
43 43 end
44 44
45 45 # Yields to filter the activity scope
46 46 def scope_select(&block)
47 47 @scope = @scope.select {|t| yield t }
48 48 end
49 49
50 50 # Sets the scope
51 51 def scope=(s)
52 52 @scope = s & event_types
53 53 end
54 54
55 55 # Resets the scope to the default scope
56 56 def default_scope!
57 57 @scope = Redmine::Activity.default_event_types
58 58 end
59 59
60 60 # Returns an array of events for the given date range
61 def events(from, to)
61 def events(from = nil, to = nil, options={})
62 62 e = []
63 @options[:limit] = options[:limit]
63 64
64 65 @scope.each do |event_type|
65 66 constantized_providers(event_type).each do |provider|
66 67 e += provider.find_events(event_type, @user, from, to, @options)
67 68 end
68 69 end
70
71 if options[:limit]
72 e.sort! {|a,b| b.event_date <=> a.event_date}
73 e = e.slice(0, options[:limit])
74 end
69 75 e
70 76 end
71 77
72 78 private
73 79
74 80 def constantized_providers(event_type)
75 81 @@constantized_providers[event_type]
76 82 end
77 83 end
78 84 end
79 85 end
@@ -1,71 +1,80
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 21 fixtures :projects, :versions, :users, :roles, :members, :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 def test_user_activity
67 user = User.find(2)
68 events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10)
69
70 assert(events.size > 0)
71 assert(events.size <= 10)
72 assert_nil(events.detect {|e| e.event_author != user})
73 end
74
66 75 private
67 76
68 77 def find_events(user, options={})
69 78 Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
70 79 end
71 80 end
@@ -1,68 +1,80
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 module Redmine
19 19 module Acts
20 20 module ActivityProvider
21 21 def self.included(base)
22 22 base.extend ClassMethods
23 23 end
24 24
25 25 module ClassMethods
26 26 def acts_as_activity_provider(options = {})
27 27 unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods)
28 28 cattr_accessor :activity_provider_options
29 29 send :include, Redmine::Acts::ActivityProvider::InstanceMethods
30 30 end
31 31
32 options.assert_valid_keys(:type, :permission, :timestamp, :find_options)
32 options.assert_valid_keys(:type, :permission, :timestamp, :author_key, :find_options)
33 33 self.activity_provider_options ||= {}
34 34
35 35 # One model can provide different event types
36 36 # We store these options in activity_provider_options hash
37 37 event_type = options.delete(:type) || self.name.underscore.pluralize
38 38
39 39 options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless options.has_key?(:permission)
40 40 options[:timestamp] ||= "#{table_name}.created_on"
41 41 options[:find_options] ||= {}
42 options[:author_key] = "#{table_name}.#{options[:author_key]}" if options[:author_key].is_a?(Symbol)
42 43 self.activity_provider_options[event_type] = options
43 44 end
44 45 end
45 46
46 47 module InstanceMethods
47 48 def self.included(base)
48 49 base.extend ClassMethods
49 50 end
50 51
51 52 module ClassMethods
52 53 # Returns events of type event_type visible by user that occured between from and to
53 54 def find_events(event_type, user, from, to, options)
54 55 provider_options = activity_provider_options[event_type]
55 56 raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
56 57
57 cond = ARCondition.new(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
58 scope_options = {}
59 cond = ARCondition.new
60 if from && to
61 cond.add(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
62 end
63 if options[:author]
64 return [] if provider_options[:author_key].nil?
65 cond.add(["#{provider_options[:author_key]} = ?", options[:author].id])
66 end
58 67 cond.add(Project.allowed_to_condition(user, provider_options[:permission], options)) if provider_options[:permission]
68 scope_options[:conditions] = cond.conditions
69 scope_options[:order] = "#{provider_options[:timestamp]} DESC"
70 scope_options[:limit] = options[:limit]
59 71
60 with_scope(:find => { :conditions => cond.conditions }) do
72 with_scope(:find => scope_options) do
61 73 find(:all, provider_options[:find_options])
62 74 end
63 75 end
64 76 end
65 77 end
66 78 end
67 79 end
68 80 end
General Comments 0
You need to be logged in to leave comments. Login now