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