##// END OF EJS Templates
Merged Rails 2.2 branch. Redmine now requires Rails 2.2.2....
Jean-Philippe Lang -
r2430:fe28193e4eb9
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,234 +1,241
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 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class ApplicationController < ActionController::Base
21 class ApplicationController < ActionController::Base
22 include Redmine::I18n
23
24 # In case the cookie store secret changes
25 rescue_from CGI::Session::CookieStore::TamperedWithCookie do |exception|
26 render :text => 'Your session was invalid and has been reset. Please, reload this page.', :status => 500
27 end
28
22 layout 'base'
29 layout 'base'
23
30
24 before_filter :user_setup, :check_if_login_required, :set_localization
31 before_filter :user_setup, :check_if_login_required, :set_localization
25 filter_parameter_logging :password
32 filter_parameter_logging :password
26
33
27 include Redmine::MenuManager::MenuController
34 include Redmine::MenuManager::MenuController
28 helper Redmine::MenuManager::MenuHelper
35 helper Redmine::MenuManager::MenuHelper
29
36
30 REDMINE_SUPPORTED_SCM.each do |scm|
37 REDMINE_SUPPORTED_SCM.each do |scm|
31 require_dependency "repository/#{scm.underscore}"
38 require_dependency "repository/#{scm.underscore}"
32 end
39 end
33
40
34 def current_role
41 def current_role
35 @current_role ||= User.current.role_for_project(@project)
42 @current_role ||= User.current.role_for_project(@project)
36 end
43 end
37
44
38 def user_setup
45 def user_setup
39 # Check the settings cache for each request
46 # Check the settings cache for each request
40 Setting.check_cache
47 Setting.check_cache
41 # Find the current user
48 # Find the current user
42 User.current = find_current_user
49 User.current = find_current_user
43 end
50 end
44
51
45 # Returns the current user or nil if no user is logged in
52 # Returns the current user or nil if no user is logged in
46 def find_current_user
53 def find_current_user
47 if session[:user_id]
54 if session[:user_id]
48 # existing session
55 # existing session
49 (User.active.find(session[:user_id]) rescue nil)
56 (User.active.find(session[:user_id]) rescue nil)
50 elsif cookies[:autologin] && Setting.autologin?
57 elsif cookies[:autologin] && Setting.autologin?
51 # auto-login feature
58 # auto-login feature
52 User.find_by_autologin_key(cookies[:autologin])
59 User.find_by_autologin_key(cookies[:autologin])
53 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
60 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
54 # RSS key authentication
61 # RSS key authentication
55 User.find_by_rss_key(params[:key])
62 User.find_by_rss_key(params[:key])
56 end
63 end
57 end
64 end
58
65
59 # check if login is globally required to access the application
66 # check if login is globally required to access the application
60 def check_if_login_required
67 def check_if_login_required
61 # no check needed if user is already logged in
68 # no check needed if user is already logged in
62 return true if User.current.logged?
69 return true if User.current.logged?
63 require_login if Setting.login_required?
70 require_login if Setting.login_required?
64 end
71 end
65
72
66 def set_localization
73 def set_localization
67 User.current.language = nil unless User.current.logged?
74 lang = nil
68 lang = begin
75 if User.current.logged?
69 if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
76 lang = find_language(User.current.language)
70 User.current.language
77 end
71 elsif request.env['HTTP_ACCEPT_LANGUAGE']
78 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
72 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
79 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
73 if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
80 if !accept_lang.blank?
74 User.current.language = accept_lang
81 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
75 end
76 end
82 end
77 rescue
83 end
78 nil
84 lang ||= Setting.default_language
79 end || Setting.default_language
85 set_language_if_valid(lang)
80 set_language_if_valid(lang)
81 end
86 end
82
87
83 def require_login
88 def require_login
84 if !User.current.logged?
89 if !User.current.logged?
85 redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
90 redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
86 return false
91 return false
87 end
92 end
88 true
93 true
89 end
94 end
90
95
91 def require_admin
96 def require_admin
92 return unless require_login
97 return unless require_login
93 if !User.current.admin?
98 if !User.current.admin?
94 render_403
99 render_403
95 return false
100 return false
96 end
101 end
97 true
102 true
98 end
103 end
99
104
100 def deny_access
105 def deny_access
101 User.current.logged? ? render_403 : require_login
106 User.current.logged? ? render_403 : require_login
102 end
107 end
103
108
104 # Authorize the user for the requested action
109 # Authorize the user for the requested action
105 def authorize(ctrl = params[:controller], action = params[:action])
110 def authorize(ctrl = params[:controller], action = params[:action])
106 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
111 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
107 allowed ? true : deny_access
112 allowed ? true : deny_access
108 end
113 end
109
114
110 # make sure that the user is a member of the project (or admin) if project is private
115 # make sure that the user is a member of the project (or admin) if project is private
111 # used as a before_filter for actions that do not require any particular permission on the project
116 # used as a before_filter for actions that do not require any particular permission on the project
112 def check_project_privacy
117 def check_project_privacy
113 if @project && @project.active?
118 if @project && @project.active?
114 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
119 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
115 true
120 true
116 else
121 else
117 User.current.logged? ? render_403 : require_login
122 User.current.logged? ? render_403 : require_login
118 end
123 end
119 else
124 else
120 @project = nil
125 @project = nil
121 render_404
126 render_404
122 false
127 false
123 end
128 end
124 end
129 end
125
130
126 def redirect_back_or_default(default)
131 def redirect_back_or_default(default)
127 back_url = CGI.unescape(params[:back_url].to_s)
132 back_url = CGI.unescape(params[:back_url].to_s)
128 if !back_url.blank?
133 if !back_url.blank?
129 begin
134 begin
130 uri = URI.parse(back_url)
135 uri = URI.parse(back_url)
131 # do not redirect user to another host or to the login or register page
136 # do not redirect user to another host or to the login or register page
132 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
137 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
133 redirect_to(back_url) and return
138 redirect_to(back_url) and return
134 end
139 end
135 rescue URI::InvalidURIError
140 rescue URI::InvalidURIError
136 # redirect to default
141 # redirect to default
137 end
142 end
138 end
143 end
139 redirect_to default
144 redirect_to default
140 end
145 end
141
146
142 def render_403
147 def render_403
143 @project = nil
148 @project = nil
144 render :template => "common/403", :layout => !request.xhr?, :status => 403
149 render :template => "common/403", :layout => !request.xhr?, :status => 403
145 return false
150 return false
146 end
151 end
147
152
148 def render_404
153 def render_404
149 render :template => "common/404", :layout => !request.xhr?, :status => 404
154 render :template => "common/404", :layout => !request.xhr?, :status => 404
150 return false
155 return false
151 end
156 end
152
157
153 def render_error(msg)
158 def render_error(msg)
154 flash.now[:error] = msg
159 flash.now[:error] = msg
155 render :nothing => true, :layout => !request.xhr?, :status => 500
160 render :text => '', :layout => !request.xhr?, :status => 500
156 end
161 end
157
162
158 def render_feed(items, options={})
163 def render_feed(items, options={})
159 @items = items || []
164 @items = items || []
160 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
165 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
161 @items = @items.slice(0, Setting.feeds_limit.to_i)
166 @items = @items.slice(0, Setting.feeds_limit.to_i)
162 @title = options[:title] || Setting.app_title
167 @title = options[:title] || Setting.app_title
163 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
168 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
164 end
169 end
165
170
166 def self.accept_key_auth(*actions)
171 def self.accept_key_auth(*actions)
167 actions = actions.flatten.map(&:to_s)
172 actions = actions.flatten.map(&:to_s)
168 write_inheritable_attribute('accept_key_auth_actions', actions)
173 write_inheritable_attribute('accept_key_auth_actions', actions)
169 end
174 end
170
175
171 def accept_key_auth_actions
176 def accept_key_auth_actions
172 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
177 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
173 end
178 end
174
179
175 # TODO: move to model
180 # TODO: move to model
176 def attach_files(obj, attachments)
181 def attach_files(obj, attachments)
177 attached = []
182 attached = []
178 unsaved = []
183 unsaved = []
179 if attachments && attachments.is_a?(Hash)
184 if attachments && attachments.is_a?(Hash)
180 attachments.each_value do |attachment|
185 attachments.each_value do |attachment|
181 file = attachment['file']
186 file = attachment['file']
182 next unless file && file.size > 0
187 next unless file && file.size > 0
183 a = Attachment.create(:container => obj,
188 a = Attachment.create(:container => obj,
184 :file => file,
189 :file => file,
185 :description => attachment['description'].to_s.strip,
190 :description => attachment['description'].to_s.strip,
186 :author => User.current)
191 :author => User.current)
187 a.new_record? ? (unsaved << a) : (attached << a)
192 a.new_record? ? (unsaved << a) : (attached << a)
188 end
193 end
189 if unsaved.any?
194 if unsaved.any?
190 flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
195 flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
191 end
196 end
192 end
197 end
193 attached
198 attached
194 end
199 end
195
200
196 # Returns the number of objects that should be displayed
201 # Returns the number of objects that should be displayed
197 # on the paginated list
202 # on the paginated list
198 def per_page_option
203 def per_page_option
199 per_page = nil
204 per_page = nil
200 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
205 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
201 per_page = params[:per_page].to_s.to_i
206 per_page = params[:per_page].to_s.to_i
202 session[:per_page] = per_page
207 session[:per_page] = per_page
203 elsif session[:per_page]
208 elsif session[:per_page]
204 per_page = session[:per_page]
209 per_page = session[:per_page]
205 else
210 else
206 per_page = Setting.per_page_options_array.first || 25
211 per_page = Setting.per_page_options_array.first || 25
207 end
212 end
208 per_page
213 per_page
209 end
214 end
210
215
211 # qvalues http header parser
216 # qvalues http header parser
212 # code taken from webrick
217 # code taken from webrick
213 def parse_qvalues(value)
218 def parse_qvalues(value)
214 tmp = []
219 tmp = []
215 if value
220 if value
216 parts = value.split(/,\s*/)
221 parts = value.split(/,\s*/)
217 parts.each {|part|
222 parts.each {|part|
218 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
223 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
219 val = m[1]
224 val = m[1]
220 q = (m[2] or 1).to_f
225 q = (m[2] or 1).to_f
221 tmp.push([val, q])
226 tmp.push([val, q])
222 end
227 end
223 }
228 }
224 tmp = tmp.sort_by{|val, q| -q}
229 tmp = tmp.sort_by{|val, q| -q}
225 tmp.collect!{|val, q| val}
230 tmp.collect!{|val, q| val}
226 end
231 end
227 return tmp
232 return tmp
233 rescue
234 nil
228 end
235 end
229
236
230 # Returns a string that can be used as filename value in Content-Disposition header
237 # Returns a string that can be used as filename value in Content-Disposition header
231 def filename_for_content_disposition(name)
238 def filename_for_content_disposition(name)
232 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
239 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
233 end
240 end
234 end
241 end
@@ -1,492 +1,496
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 IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20
20
21 before_filter :find_issue, :only => [:show, :edit, :reply]
21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 accept_key_auth :index, :changes
26 accept_key_auth :index, :changes
27
27
28 helper :journals
28 helper :journals
29 helper :projects
29 helper :projects
30 include ProjectsHelper
30 include ProjectsHelper
31 helper :custom_fields
31 helper :custom_fields
32 include CustomFieldsHelper
32 include CustomFieldsHelper
33 helper :issue_relations
33 helper :issue_relations
34 include IssueRelationsHelper
34 include IssueRelationsHelper
35 helper :watchers
35 helper :watchers
36 include WatchersHelper
36 include WatchersHelper
37 helper :attachments
37 helper :attachments
38 include AttachmentsHelper
38 include AttachmentsHelper
39 helper :queries
39 helper :queries
40 helper :sort
40 helper :sort
41 include SortHelper
41 include SortHelper
42 include IssuesHelper
42 include IssuesHelper
43 helper :timelog
43 helper :timelog
44 include Redmine::Export::PDF
44 include Redmine::Export::PDF
45
45
46 def index
46 def index
47 retrieve_query
47 retrieve_query
48 sort_init 'id', 'desc'
48 sort_init 'id', 'desc'
49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50
50
51 if @query.valid?
51 if @query.valid?
52 limit = per_page_option
52 limit = per_page_option
53 respond_to do |format|
53 respond_to do |format|
54 format.html { }
54 format.html { }
55 format.atom { }
55 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
58 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
61 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
63 :conditions => @query.statement,
64 :limit => limit,
64 :limit => limit,
65 :offset => @issue_pages.current.offset
65 :offset => @issue_pages.current.offset
66 respond_to do |format|
66 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
70 format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
71 end
72 else
72 else
73 # Send html if the query is not valid
73 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
75 end
76 rescue ActiveRecord::RecordNotFound
76 rescue ActiveRecord::RecordNotFound
77 render_404
77 render_404
78 end
78 end
79
79
80 def changes
80 def changes
81 retrieve_query
81 retrieve_query
82 sort_init 'id', 'desc'
82 sort_init 'id', 'desc'
83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
83 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
84
84
85 if @query.valid?
85 if @query.valid?
86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
87 :conditions => @query.statement,
87 :conditions => @query.statement,
88 :limit => 25,
88 :limit => 25,
89 :order => "#{Journal.table_name}.created_on DESC"
89 :order => "#{Journal.table_name}.created_on DESC"
90 end
90 end
91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
92 render :layout => false, :content_type => 'application/atom+xml'
92 render :layout => false, :content_type => 'application/atom+xml'
93 rescue ActiveRecord::RecordNotFound
93 rescue ActiveRecord::RecordNotFound
94 render_404
94 render_404
95 end
95 end
96
96
97 def show
97 def show
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
103 @priorities = Enumeration.priorities
103 @priorities = Enumeration.priorities
104 @time_entry = TimeEntry.new
104 @time_entry = TimeEntry.new
105 respond_to do |format|
105 respond_to do |format|
106 format.html { render :template => 'issues/show.rhtml' }
106 format.html { render :template => 'issues/show.rhtml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
109 end
109 end
110 end
110 end
111
111
112 # Add a new issue
112 # Add a new issue
113 # The new issue will be created from an existing one if copy_from parameter is given
113 # The new issue will be created from an existing one if copy_from parameter is given
114 def new
114 def new
115 @issue = Issue.new
115 @issue = Issue.new
116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 @issue.copy_from(params[:copy_from]) if params[:copy_from]
117 @issue.project = @project
117 @issue.project = @project
118 # Tracker must be set before custom field values
118 # Tracker must be set before custom field values
119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
120 if @issue.tracker.nil?
120 if @issue.tracker.nil?
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
122 render :nothing => true, :layout => true
122 render :nothing => true, :layout => true
123 return
123 return
124 end
124 end
125 if params[:issue].is_a?(Hash)
125 if params[:issue].is_a?(Hash)
126 @issue.attributes = params[:issue]
126 @issue.attributes = params[:issue]
127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
127 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
128 end
128 end
129 @issue.author = User.current
129 @issue.author = User.current
130
130
131 default_status = IssueStatus.default
131 default_status = IssueStatus.default
132 unless default_status
132 unless default_status
133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
133 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
134 render :nothing => true, :layout => true
134 render :nothing => true, :layout => true
135 return
135 return
136 end
136 end
137 @issue.status = default_status
137 @issue.status = default_status
138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
138 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
139
139
140 if request.get? || request.xhr?
140 if request.get? || request.xhr?
141 @issue.start_date ||= Date.today
141 @issue.start_date ||= Date.today
142 else
142 else
143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
143 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
144 # Check that the user is allowed to apply the requested status
144 # Check that the user is allowed to apply the requested status
145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
145 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
146 if @issue.save
146 if @issue.save
147 attach_files(@issue, params[:attachments])
147 attach_files(@issue, params[:attachments])
148 flash[:notice] = l(:notice_successful_create)
148 flash[:notice] = l(:notice_successful_create)
149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
149 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
150 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
150 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
151 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
151 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
152 { :action => 'show', :id => @issue })
152 { :action => 'show', :id => @issue })
153 return
153 return
154 end
154 end
155 end
155 end
156 @priorities = Enumeration.priorities
156 @priorities = Enumeration.priorities
157 render :layout => !request.xhr?
157 render :layout => !request.xhr?
158 end
158 end
159
159
160 # Attributes that can be updated on workflow transition (without :edit permission)
160 # Attributes that can be updated on workflow transition (without :edit permission)
161 # TODO: make it configurable (at least per role)
161 # TODO: make it configurable (at least per role)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
162 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
163
163
164 def edit
164 def edit
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
165 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
166 @priorities = Enumeration.priorities
166 @priorities = Enumeration.priorities
167 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
167 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
168 @time_entry = TimeEntry.new
168 @time_entry = TimeEntry.new
169
169
170 @notes = params[:notes]
170 @notes = params[:notes]
171 journal = @issue.init_journal(User.current, @notes)
171 journal = @issue.init_journal(User.current, @notes)
172 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
172 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
173 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
173 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
174 attrs = params[:issue].dup
174 attrs = params[:issue].dup
175 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
175 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
176 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
176 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
177 @issue.attributes = attrs
177 @issue.attributes = attrs
178 end
178 end
179
179
180 if request.post?
180 if request.post?
181 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
181 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
182 @time_entry.attributes = params[:time_entry]
182 @time_entry.attributes = params[:time_entry]
183 attachments = attach_files(@issue, params[:attachments])
183 attachments = attach_files(@issue, params[:attachments])
184 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
184 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
185
185
186 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
186 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
187
187
188 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
188 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
189 # Log spend time
189 # Log spend time
190 if User.current.allowed_to?(:log_time, @project)
190 if User.current.allowed_to?(:log_time, @project)
191 @time_entry.save
191 @time_entry.save
192 end
192 end
193 if !journal.new_record?
193 if !journal.new_record?
194 # Only send notification if something was actually changed
194 # Only send notification if something was actually changed
195 flash[:notice] = l(:notice_successful_update)
195 flash[:notice] = l(:notice_successful_update)
196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
196 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
197 end
197 end
198 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
198 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
199 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
199 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
200 end
200 end
201 end
201 end
202 rescue ActiveRecord::StaleObjectError
202 rescue ActiveRecord::StaleObjectError
203 # Optimistic locking exception
203 # Optimistic locking exception
204 flash.now[:error] = l(:notice_locking_conflict)
204 flash.now[:error] = l(:notice_locking_conflict)
205 end
205 end
206
206
207 def reply
207 def reply
208 journal = Journal.find(params[:journal_id]) if params[:journal_id]
208 journal = Journal.find(params[:journal_id]) if params[:journal_id]
209 if journal
209 if journal
210 user = journal.user
210 user = journal.user
211 text = journal.notes
211 text = journal.notes
212 else
212 else
213 user = @issue.author
213 user = @issue.author
214 text = @issue.description
214 text = @issue.description
215 end
215 end
216 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
216 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
217 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
217 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
218 render(:update) { |page|
218 render(:update) { |page|
219 page.<< "$('notes').value = \"#{content}\";"
219 page.<< "$('notes').value = \"#{content}\";"
220 page.show 'update'
220 page.show 'update'
221 page << "Form.Element.focus('notes');"
221 page << "Form.Element.focus('notes');"
222 page << "Element.scrollTo('update');"
222 page << "Element.scrollTo('update');"
223 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
223 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
224 }
224 }
225 end
225 end
226
226
227 # Bulk edit a set of issues
227 # Bulk edit a set of issues
228 def bulk_edit
228 def bulk_edit
229 if request.post?
229 if request.post?
230 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
230 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
231 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
231 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
232 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
232 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
233 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
233 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
234 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
234 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
235 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
235 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
236
236
237 unsaved_issue_ids = []
237 unsaved_issue_ids = []
238 @issues.each do |issue|
238 @issues.each do |issue|
239 journal = issue.init_journal(User.current, params[:notes])
239 journal = issue.init_journal(User.current, params[:notes])
240 issue.priority = priority if priority
240 issue.priority = priority if priority
241 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
241 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
242 issue.category = category if category || params[:category_id] == 'none'
242 issue.category = category if category || params[:category_id] == 'none'
243 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
243 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
244 issue.start_date = params[:start_date] unless params[:start_date].blank?
244 issue.start_date = params[:start_date] unless params[:start_date].blank?
245 issue.due_date = params[:due_date] unless params[:due_date].blank?
245 issue.due_date = params[:due_date] unless params[:due_date].blank?
246 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
246 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
247 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
247 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
248 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
248 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
249 # Don't save any change to the issue if the user is not authorized to apply the requested status
249 # Don't save any change to the issue if the user is not authorized to apply the requested status
250 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
250 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
251 # Send notification for each issue (if changed)
251 # Send notification for each issue (if changed)
252 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
252 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
253 else
253 else
254 # Keep unsaved issue ids to display them in flash error
254 # Keep unsaved issue ids to display them in flash error
255 unsaved_issue_ids << issue.id
255 unsaved_issue_ids << issue.id
256 end
256 end
257 end
257 end
258 if unsaved_issue_ids.empty?
258 if unsaved_issue_ids.empty?
259 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
259 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
260 else
260 else
261 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
261 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
262 :total => @issues.size,
263 :ids => '#' + unsaved_issue_ids.join(', #'))
262 end
264 end
263 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
265 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
264 return
266 return
265 end
267 end
266 # Find potential statuses the user could be allowed to switch issues to
268 # Find potential statuses the user could be allowed to switch issues to
267 @available_statuses = Workflow.find(:all, :include => :new_status,
269 @available_statuses = Workflow.find(:all, :include => :new_status,
268 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
270 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
269 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
271 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
270 end
272 end
271
273
272 def move
274 def move
273 @allowed_projects = []
275 @allowed_projects = []
274 # find projects to which the user is allowed to move the issue
276 # find projects to which the user is allowed to move the issue
275 if User.current.admin?
277 if User.current.admin?
276 # admin is allowed to move issues to any active (visible) project
278 # admin is allowed to move issues to any active (visible) project
277 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
279 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
278 else
280 else
279 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
281 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
280 end
282 end
281 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
283 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
282 @target_project ||= @project
284 @target_project ||= @project
283 @trackers = @target_project.trackers
285 @trackers = @target_project.trackers
284 if request.post?
286 if request.post?
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
287 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
286 unsaved_issue_ids = []
288 unsaved_issue_ids = []
287 @issues.each do |issue|
289 @issues.each do |issue|
288 issue.init_journal(User.current)
290 issue.init_journal(User.current)
289 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
291 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
290 end
292 end
291 if unsaved_issue_ids.empty?
293 if unsaved_issue_ids.empty?
292 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
294 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
293 else
295 else
294 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
296 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
297 :total => @issues.size,
298 :ids => '#' + unsaved_issue_ids.join(', #'))
295 end
299 end
296 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
300 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
297 return
301 return
298 end
302 end
299 render :layout => false if request.xhr?
303 render :layout => false if request.xhr?
300 end
304 end
301
305
302 def destroy
306 def destroy
303 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
307 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
304 if @hours > 0
308 if @hours > 0
305 case params[:todo]
309 case params[:todo]
306 when 'destroy'
310 when 'destroy'
307 # nothing to do
311 # nothing to do
308 when 'nullify'
312 when 'nullify'
309 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
313 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
310 when 'reassign'
314 when 'reassign'
311 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
315 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
312 if reassign_to.nil?
316 if reassign_to.nil?
313 flash.now[:error] = l(:error_issue_not_found_in_project)
317 flash.now[:error] = l(:error_issue_not_found_in_project)
314 return
318 return
315 else
319 else
316 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
320 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
317 end
321 end
318 else
322 else
319 # display the destroy form
323 # display the destroy form
320 return
324 return
321 end
325 end
322 end
326 end
323 @issues.each(&:destroy)
327 @issues.each(&:destroy)
324 redirect_to :action => 'index', :project_id => @project
328 redirect_to :action => 'index', :project_id => @project
325 end
329 end
326
330
327 def gantt
331 def gantt
328 @gantt = Redmine::Helpers::Gantt.new(params)
332 @gantt = Redmine::Helpers::Gantt.new(params)
329 retrieve_query
333 retrieve_query
330 if @query.valid?
334 if @query.valid?
331 events = []
335 events = []
332 # Issues that have start and due dates
336 # Issues that have start and due dates
333 events += Issue.find(:all,
337 events += Issue.find(:all,
334 :order => "start_date, due_date",
338 :order => "start_date, due_date",
335 :include => [:tracker, :status, :assigned_to, :priority, :project],
339 :include => [:tracker, :status, :assigned_to, :priority, :project],
336 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
340 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
337 )
341 )
338 # Issues that don't have a due date but that are assigned to a version with a date
342 # Issues that don't have a due date but that are assigned to a version with a date
339 events += Issue.find(:all,
343 events += Issue.find(:all,
340 :order => "start_date, effective_date",
344 :order => "start_date, effective_date",
341 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
345 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
342 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
346 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
343 )
347 )
344 # Versions
348 # Versions
345 events += Version.find(:all, :include => :project,
349 events += Version.find(:all, :include => :project,
346 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
350 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
347
351
348 @gantt.events = events
352 @gantt.events = events
349 end
353 end
350
354
351 respond_to do |format|
355 respond_to do |format|
352 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
356 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
353 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
357 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
354 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
358 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
355 end
359 end
356 end
360 end
357
361
358 def calendar
362 def calendar
359 if params[:year] and params[:year].to_i > 1900
363 if params[:year] and params[:year].to_i > 1900
360 @year = params[:year].to_i
364 @year = params[:year].to_i
361 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
365 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
362 @month = params[:month].to_i
366 @month = params[:month].to_i
363 end
367 end
364 end
368 end
365 @year ||= Date.today.year
369 @year ||= Date.today.year
366 @month ||= Date.today.month
370 @month ||= Date.today.month
367
371
368 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
372 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
369 retrieve_query
373 retrieve_query
370 if @query.valid?
374 if @query.valid?
371 events = []
375 events = []
372 events += Issue.find(:all,
376 events += Issue.find(:all,
373 :include => [:tracker, :status, :assigned_to, :priority, :project],
377 :include => [:tracker, :status, :assigned_to, :priority, :project],
374 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
378 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
375 )
379 )
376 events += Version.find(:all, :include => :project,
380 events += Version.find(:all, :include => :project,
377 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
381 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
378
382
379 @calendar.events = events
383 @calendar.events = events
380 end
384 end
381
385
382 render :layout => false if request.xhr?
386 render :layout => false if request.xhr?
383 end
387 end
384
388
385 def context_menu
389 def context_menu
386 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
390 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
387 if (@issues.size == 1)
391 if (@issues.size == 1)
388 @issue = @issues.first
392 @issue = @issues.first
389 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
393 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
390 end
394 end
391 projects = @issues.collect(&:project).compact.uniq
395 projects = @issues.collect(&:project).compact.uniq
392 @project = projects.first if projects.size == 1
396 @project = projects.first if projects.size == 1
393
397
394 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
398 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
395 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
399 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
396 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
400 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
397 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
401 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
398 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
402 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
399 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
403 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
400 }
404 }
401 if @project
405 if @project
402 @assignables = @project.assignable_users
406 @assignables = @project.assignable_users
403 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
407 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
404 end
408 end
405
409
406 @priorities = Enumeration.priorities.reverse
410 @priorities = Enumeration.priorities.reverse
407 @statuses = IssueStatus.find(:all, :order => 'position')
411 @statuses = IssueStatus.find(:all, :order => 'position')
408 @back = request.env['HTTP_REFERER']
412 @back = request.env['HTTP_REFERER']
409
413
410 render :layout => false
414 render :layout => false
411 end
415 end
412
416
413 def update_form
417 def update_form
414 @issue = Issue.new(params[:issue])
418 @issue = Issue.new(params[:issue])
415 render :action => :new, :layout => false
419 render :action => :new, :layout => false
416 end
420 end
417
421
418 def preview
422 def preview
419 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
423 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
420 @attachements = @issue.attachments if @issue
424 @attachements = @issue.attachments if @issue
421 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
425 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
422 render :partial => 'common/preview'
426 render :partial => 'common/preview'
423 end
427 end
424
428
425 private
429 private
426 def find_issue
430 def find_issue
427 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
431 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
428 @project = @issue.project
432 @project = @issue.project
429 rescue ActiveRecord::RecordNotFound
433 rescue ActiveRecord::RecordNotFound
430 render_404
434 render_404
431 end
435 end
432
436
433 # Filter for bulk operations
437 # Filter for bulk operations
434 def find_issues
438 def find_issues
435 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
439 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
436 raise ActiveRecord::RecordNotFound if @issues.empty?
440 raise ActiveRecord::RecordNotFound if @issues.empty?
437 projects = @issues.collect(&:project).compact.uniq
441 projects = @issues.collect(&:project).compact.uniq
438 if projects.size == 1
442 if projects.size == 1
439 @project = projects.first
443 @project = projects.first
440 else
444 else
441 # TODO: let users bulk edit/move/destroy issues from different projects
445 # TODO: let users bulk edit/move/destroy issues from different projects
442 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
446 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
443 end
447 end
444 rescue ActiveRecord::RecordNotFound
448 rescue ActiveRecord::RecordNotFound
445 render_404
449 render_404
446 end
450 end
447
451
448 def find_project
452 def find_project
449 @project = Project.find(params[:project_id])
453 @project = Project.find(params[:project_id])
450 rescue ActiveRecord::RecordNotFound
454 rescue ActiveRecord::RecordNotFound
451 render_404
455 render_404
452 end
456 end
453
457
454 def find_optional_project
458 def find_optional_project
455 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
459 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
456 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
460 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
457 allowed ? true : deny_access
461 allowed ? true : deny_access
458 rescue ActiveRecord::RecordNotFound
462 rescue ActiveRecord::RecordNotFound
459 render_404
463 render_404
460 end
464 end
461
465
462 # Retrieve query from session or build a new query
466 # Retrieve query from session or build a new query
463 def retrieve_query
467 def retrieve_query
464 if !params[:query_id].blank?
468 if !params[:query_id].blank?
465 cond = "project_id IS NULL"
469 cond = "project_id IS NULL"
466 cond << " OR project_id = #{@project.id}" if @project
470 cond << " OR project_id = #{@project.id}" if @project
467 @query = Query.find(params[:query_id], :conditions => cond)
471 @query = Query.find(params[:query_id], :conditions => cond)
468 @query.project = @project
472 @query.project = @project
469 session[:query] = {:id => @query.id, :project_id => @query.project_id}
473 session[:query] = {:id => @query.id, :project_id => @query.project_id}
470 else
474 else
471 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
475 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
472 # Give it a name, required to be valid
476 # Give it a name, required to be valid
473 @query = Query.new(:name => "_")
477 @query = Query.new(:name => "_")
474 @query.project = @project
478 @query.project = @project
475 if params[:fields] and params[:fields].is_a? Array
479 if params[:fields] and params[:fields].is_a? Array
476 params[:fields].each do |field|
480 params[:fields].each do |field|
477 @query.add_filter(field, params[:operators][field], params[:values][field])
481 @query.add_filter(field, params[:operators][field], params[:values][field])
478 end
482 end
479 else
483 else
480 @query.available_filters.keys.each do |field|
484 @query.available_filters.keys.each do |field|
481 @query.add_short_filter(field, params[field]) if params[field]
485 @query.add_short_filter(field, params[field]) if params[field]
482 end
486 end
483 end
487 end
484 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
488 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
485 else
489 else
486 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
490 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
487 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
491 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
488 @query.project = @project
492 @query.project = @project
489 end
493 end
490 end
494 end
491 end
495 end
492 end
496 end
@@ -1,328 +1,327
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 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21
21
22 class ChangesetNotFound < Exception; end
22 class ChangesetNotFound < Exception; end
23 class InvalidRevisionParam < Exception; end
23 class InvalidRevisionParam < Exception; end
24
24
25 class RepositoriesController < ApplicationController
25 class RepositoriesController < ApplicationController
26 menu_item :repository
26 menu_item :repository
27 before_filter :find_repository, :except => :edit
27 before_filter :find_repository, :except => :edit
28 before_filter :find_project, :only => :edit
28 before_filter :find_project, :only => :edit
29 before_filter :authorize
29 before_filter :authorize
30 accept_key_auth :revisions
30 accept_key_auth :revisions
31
31
32 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
32 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
33
33
34 def edit
34 def edit
35 @repository = @project.repository
35 @repository = @project.repository
36 if !@repository
36 if !@repository
37 @repository = Repository.factory(params[:repository_scm])
37 @repository = Repository.factory(params[:repository_scm])
38 @repository.project = @project if @repository
38 @repository.project = @project if @repository
39 end
39 end
40 if request.post? && @repository
40 if request.post? && @repository
41 @repository.attributes = params[:repository]
41 @repository.attributes = params[:repository]
42 @repository.save
42 @repository.save
43 end
43 end
44 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
44 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
45 end
45 end
46
46
47 def committers
47 def committers
48 @committers = @repository.committers
48 @committers = @repository.committers
49 @users = @project.users
49 @users = @project.users
50 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
50 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
51 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
51 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
52 @users.compact!
52 @users.compact!
53 @users.sort!
53 @users.sort!
54 if request.post? && params[:committers].is_a?(Hash)
54 if request.post? && params[:committers].is_a?(Hash)
55 # Build a hash with repository usernames as keys and corresponding user ids as values
55 # Build a hash with repository usernames as keys and corresponding user ids as values
56 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
56 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
57 flash[:notice] = l(:notice_successful_update)
57 flash[:notice] = l(:notice_successful_update)
58 redirect_to :action => 'committers', :id => @project
58 redirect_to :action => 'committers', :id => @project
59 end
59 end
60 end
60 end
61
61
62 def destroy
62 def destroy
63 @repository.destroy
63 @repository.destroy
64 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
64 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
65 end
65 end
66
66
67 def show
67 def show
68 # check if new revisions have been committed in the repository
68 # check if new revisions have been committed in the repository
69 @repository.fetch_changesets if Setting.autofetch_changesets?
69 @repository.fetch_changesets if Setting.autofetch_changesets?
70 # root entries
70 # root entries
71 @entries = @repository.entries('', @rev)
71 @entries = @repository.entries('', @rev)
72 # latest changesets
72 # latest changesets
73 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
73 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
74 show_error_not_found unless @entries || @changesets.any?
74 show_error_not_found unless @entries || @changesets.any?
75 end
75 end
76
76
77 def browse
77 def browse
78 @entries = @repository.entries(@path, @rev)
78 @entries = @repository.entries(@path, @rev)
79 if request.xhr?
79 if request.xhr?
80 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
80 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
81 else
81 else
82 show_error_not_found and return unless @entries
82 show_error_not_found and return unless @entries
83 @properties = @repository.properties(@path, @rev)
83 @properties = @repository.properties(@path, @rev)
84 render :action => 'browse'
84 render :action => 'browse'
85 end
85 end
86 end
86 end
87
87
88 def changes
88 def changes
89 @entry = @repository.entry(@path, @rev)
89 @entry = @repository.entry(@path, @rev)
90 show_error_not_found and return unless @entry
90 show_error_not_found and return unless @entry
91 @changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
91 @changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
92 @properties = @repository.properties(@path, @rev)
92 @properties = @repository.properties(@path, @rev)
93 end
93 end
94
94
95 def revisions
95 def revisions
96 @changeset_count = @repository.changesets.count
96 @changeset_count = @repository.changesets.count
97 @changeset_pages = Paginator.new self, @changeset_count,
97 @changeset_pages = Paginator.new self, @changeset_count,
98 per_page_option,
98 per_page_option,
99 params['page']
99 params['page']
100 @changesets = @repository.changesets.find(:all,
100 @changesets = @repository.changesets.find(:all,
101 :limit => @changeset_pages.items_per_page,
101 :limit => @changeset_pages.items_per_page,
102 :offset => @changeset_pages.current.offset,
102 :offset => @changeset_pages.current.offset,
103 :include => :user)
103 :include => :user)
104
104
105 respond_to do |format|
105 respond_to do |format|
106 format.html { render :layout => false if request.xhr? }
106 format.html { render :layout => false if request.xhr? }
107 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
107 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
108 end
108 end
109 end
109 end
110
110
111 def entry
111 def entry
112 @entry = @repository.entry(@path, @rev)
112 @entry = @repository.entry(@path, @rev)
113 show_error_not_found and return unless @entry
113 show_error_not_found and return unless @entry
114
114
115 # If the entry is a dir, show the browser
115 # If the entry is a dir, show the browser
116 browse and return if @entry.is_dir?
116 browse and return if @entry.is_dir?
117
117
118 @content = @repository.cat(@path, @rev)
118 @content = @repository.cat(@path, @rev)
119 show_error_not_found and return unless @content
119 show_error_not_found and return unless @content
120 if 'raw' == params[:format] || @content.is_binary_data?
120 if 'raw' == params[:format] || @content.is_binary_data?
121 # Force the download if it's a binary file
121 # Force the download if it's a binary file
122 send_data @content, :filename => @path.split('/').last
122 send_data @content, :filename => @path.split('/').last
123 else
123 else
124 # Prevent empty lines when displaying a file with Windows style eol
124 # Prevent empty lines when displaying a file with Windows style eol
125 @content.gsub!("\r\n", "\n")
125 @content.gsub!("\r\n", "\n")
126 end
126 end
127 end
127 end
128
128
129 def annotate
129 def annotate
130 @entry = @repository.entry(@path, @rev)
130 @entry = @repository.entry(@path, @rev)
131 show_error_not_found and return unless @entry
131 show_error_not_found and return unless @entry
132
132
133 @annotate = @repository.scm.annotate(@path, @rev)
133 @annotate = @repository.scm.annotate(@path, @rev)
134 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
134 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
135 end
135 end
136
136
137 def revision
137 def revision
138 @changeset = @repository.changesets.find_by_revision(@rev)
138 @changeset = @repository.changesets.find_by_revision(@rev)
139 raise ChangesetNotFound unless @changeset
139 raise ChangesetNotFound unless @changeset
140
140
141 respond_to do |format|
141 respond_to do |format|
142 format.html
142 format.html
143 format.js {render :layout => false}
143 format.js {render :layout => false}
144 end
144 end
145 rescue ChangesetNotFound
145 rescue ChangesetNotFound
146 show_error_not_found
146 show_error_not_found
147 end
147 end
148
148
149 def diff
149 def diff
150 if params[:format] == 'diff'
150 if params[:format] == 'diff'
151 @diff = @repository.diff(@path, @rev, @rev_to)
151 @diff = @repository.diff(@path, @rev, @rev_to)
152 show_error_not_found and return unless @diff
152 show_error_not_found and return unless @diff
153 filename = "changeset_r#{@rev}"
153 filename = "changeset_r#{@rev}"
154 filename << "_r#{@rev_to}" if @rev_to
154 filename << "_r#{@rev_to}" if @rev_to
155 send_data @diff.join, :filename => "#{filename}.diff",
155 send_data @diff.join, :filename => "#{filename}.diff",
156 :type => 'text/x-patch',
156 :type => 'text/x-patch',
157 :disposition => 'attachment'
157 :disposition => 'attachment'
158 else
158 else
159 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
159 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
160 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
160 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
161
161
162 # Save diff type as user preference
162 # Save diff type as user preference
163 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
163 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
164 User.current.pref[:diff_type] = @diff_type
164 User.current.pref[:diff_type] = @diff_type
165 User.current.preference.save
165 User.current.preference.save
166 end
166 end
167
167
168 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
168 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
169 unless read_fragment(@cache_key)
169 unless read_fragment(@cache_key)
170 @diff = @repository.diff(@path, @rev, @rev_to)
170 @diff = @repository.diff(@path, @rev, @rev_to)
171 show_error_not_found unless @diff
171 show_error_not_found unless @diff
172 end
172 end
173 end
173 end
174 end
174 end
175
175
176 def stats
176 def stats
177 end
177 end
178
178
179 def graph
179 def graph
180 data = nil
180 data = nil
181 case params[:graph]
181 case params[:graph]
182 when "commits_per_month"
182 when "commits_per_month"
183 data = graph_commits_per_month(@repository)
183 data = graph_commits_per_month(@repository)
184 when "commits_per_author"
184 when "commits_per_author"
185 data = graph_commits_per_author(@repository)
185 data = graph_commits_per_author(@repository)
186 end
186 end
187 if data
187 if data
188 headers["Content-Type"] = "image/svg+xml"
188 headers["Content-Type"] = "image/svg+xml"
189 send_data(data, :type => "image/svg+xml", :disposition => "inline")
189 send_data(data, :type => "image/svg+xml", :disposition => "inline")
190 else
190 else
191 render_404
191 render_404
192 end
192 end
193 end
193 end
194
194
195 private
195 private
196 def find_project
196 def find_project
197 @project = Project.find(params[:id])
197 @project = Project.find(params[:id])
198 rescue ActiveRecord::RecordNotFound
198 rescue ActiveRecord::RecordNotFound
199 render_404
199 render_404
200 end
200 end
201
201
202 REV_PARAM_RE = %r{^[a-f0-9]*$}
202 REV_PARAM_RE = %r{^[a-f0-9]*$}
203
203
204 def find_repository
204 def find_repository
205 @project = Project.find(params[:id])
205 @project = Project.find(params[:id])
206 @repository = @project.repository
206 @repository = @project.repository
207 render_404 and return false unless @repository
207 render_404 and return false unless @repository
208 @path = params[:path].join('/') unless params[:path].nil?
208 @path = params[:path].join('/') unless params[:path].nil?
209 @path ||= ''
209 @path ||= ''
210 @rev = params[:rev]
210 @rev = params[:rev]
211 @rev_to = params[:rev_to]
211 @rev_to = params[:rev_to]
212 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
212 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
213 rescue ActiveRecord::RecordNotFound
213 rescue ActiveRecord::RecordNotFound
214 render_404
214 render_404
215 rescue InvalidRevisionParam
215 rescue InvalidRevisionParam
216 show_error_not_found
216 show_error_not_found
217 end
217 end
218
218
219 def show_error_not_found
219 def show_error_not_found
220 render_error l(:error_scm_not_found)
220 render_error l(:error_scm_not_found)
221 end
221 end
222
222
223 # Handler for Redmine::Scm::Adapters::CommandFailed exception
223 # Handler for Redmine::Scm::Adapters::CommandFailed exception
224 def show_error_command_failed(exception)
224 def show_error_command_failed(exception)
225 render_error l(:error_scm_command_failed, exception.message)
225 render_error l(:error_scm_command_failed, exception.message)
226 end
226 end
227
227
228 def graph_commits_per_month(repository)
228 def graph_commits_per_month(repository)
229 @date_to = Date.today
229 @date_to = Date.today
230 @date_from = @date_to << 11
230 @date_from = @date_to << 11
231 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
231 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
232 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
232 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
233 commits_by_month = [0] * 12
233 commits_by_month = [0] * 12
234 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
234 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
235
235
236 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
236 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
237 changes_by_month = [0] * 12
237 changes_by_month = [0] * 12
238 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
238 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
239
239
240 fields = []
240 fields = []
241 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
241 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
242 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
243
242
244 graph = SVG::Graph::Bar.new(
243 graph = SVG::Graph::Bar.new(
245 :height => 300,
244 :height => 300,
246 :width => 800,
245 :width => 800,
247 :fields => fields.reverse,
246 :fields => fields.reverse,
248 :stack => :side,
247 :stack => :side,
249 :scale_integers => true,
248 :scale_integers => true,
250 :step_x_labels => 2,
249 :step_x_labels => 2,
251 :show_data_values => false,
250 :show_data_values => false,
252 :graph_title => l(:label_commits_per_month),
251 :graph_title => l(:label_commits_per_month),
253 :show_graph_title => true
252 :show_graph_title => true
254 )
253 )
255
254
256 graph.add_data(
255 graph.add_data(
257 :data => commits_by_month[0..11].reverse,
256 :data => commits_by_month[0..11].reverse,
258 :title => l(:label_revision_plural)
257 :title => l(:label_revision_plural)
259 )
258 )
260
259
261 graph.add_data(
260 graph.add_data(
262 :data => changes_by_month[0..11].reverse,
261 :data => changes_by_month[0..11].reverse,
263 :title => l(:label_change_plural)
262 :title => l(:label_change_plural)
264 )
263 )
265
264
266 graph.burn
265 graph.burn
267 end
266 end
268
267
269 def graph_commits_per_author(repository)
268 def graph_commits_per_author(repository)
270 commits_by_author = repository.changesets.count(:all, :group => :committer)
269 commits_by_author = repository.changesets.count(:all, :group => :committer)
271 commits_by_author.sort! {|x, y| x.last <=> y.last}
270 commits_by_author.sort! {|x, y| x.last <=> y.last}
272
271
273 changes_by_author = repository.changes.count(:all, :group => :committer)
272 changes_by_author = repository.changes.count(:all, :group => :committer)
274 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
273 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
275
274
276 fields = commits_by_author.collect {|r| r.first}
275 fields = commits_by_author.collect {|r| r.first}
277 commits_data = commits_by_author.collect {|r| r.last}
276 commits_data = commits_by_author.collect {|r| r.last}
278 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
277 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
279
278
280 fields = fields + [""]*(10 - fields.length) if fields.length<10
279 fields = fields + [""]*(10 - fields.length) if fields.length<10
281 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
280 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
282 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
281 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
283
282
284 # Remove email adress in usernames
283 # Remove email adress in usernames
285 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
284 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
286
285
287 graph = SVG::Graph::BarHorizontal.new(
286 graph = SVG::Graph::BarHorizontal.new(
288 :height => 400,
287 :height => 400,
289 :width => 800,
288 :width => 800,
290 :fields => fields,
289 :fields => fields,
291 :stack => :side,
290 :stack => :side,
292 :scale_integers => true,
291 :scale_integers => true,
293 :show_data_values => false,
292 :show_data_values => false,
294 :rotate_y_labels => false,
293 :rotate_y_labels => false,
295 :graph_title => l(:label_commits_per_author),
294 :graph_title => l(:label_commits_per_author),
296 :show_graph_title => true
295 :show_graph_title => true
297 )
296 )
298
297
299 graph.add_data(
298 graph.add_data(
300 :data => commits_data,
299 :data => commits_data,
301 :title => l(:label_revision_plural)
300 :title => l(:label_revision_plural)
302 )
301 )
303
302
304 graph.add_data(
303 graph.add_data(
305 :data => changes_data,
304 :data => changes_data,
306 :title => l(:label_change_plural)
305 :title => l(:label_change_plural)
307 )
306 )
308
307
309 graph.burn
308 graph.burn
310 end
309 end
311
310
312 end
311 end
313
312
314 class Date
313 class Date
315 def months_ago(date = Date.today)
314 def months_ago(date = Date.today)
316 (date.year - self.year)*12 + (date.month - self.month)
315 (date.year - self.year)*12 + (date.month - self.month)
317 end
316 end
318
317
319 def weeks_ago(date = Date.today)
318 def weeks_ago(date = Date.today)
320 (date.year - self.year)*52 + (date.cweek - self.cweek)
319 (date.year - self.year)*52 + (date.cweek - self.cweek)
321 end
320 end
322 end
321 end
323
322
324 class String
323 class String
325 def with_leading_slash
324 def with_leading_slash
326 starts_with?('/') ? self : "/#{self}"
325 starts_with?('/') ? self : "/#{self}"
327 end
326 end
328 end
327 end
@@ -1,721 +1,648
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 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
26
27
27 extend Forwardable
28 extend Forwardable
28 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29
30
30 def current_role
31 def current_role
31 @current_role ||= User.current.role_for_project(@project)
32 @current_role ||= User.current.role_for_project(@project)
32 end
33 end
33
34
34 # Return true if user is authorized for controller/action, otherwise false
35 # Return true if user is authorized for controller/action, otherwise false
35 def authorize_for(controller, action)
36 def authorize_for(controller, action)
36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 end
38 end
38
39
39 # Display a link if user is authorized
40 # Display a link if user is authorized
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
43 end
43
44
44 # Display a link to remote if user is authorized
45 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
47 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
49 end
49
50
50 # Display a link to user's account page
51 # Display a link to user's account page
51 def link_to_user(user, options={})
52 def link_to_user(user, options={})
52 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
53 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
53 end
54 end
54
55
55 def link_to_issue(issue, options={})
56 def link_to_issue(issue, options={})
56 options[:class] ||= ''
57 options[:class] ||= ''
57 options[:class] << ' issue'
58 options[:class] << ' issue'
58 options[:class] << ' closed' if issue.closed?
59 options[:class] << ' closed' if issue.closed?
59 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
60 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
60 end
61 end
61
62
62 # Generates a link to an attachment.
63 # Generates a link to an attachment.
63 # Options:
64 # Options:
64 # * :text - Link text (default to attachment filename)
65 # * :text - Link text (default to attachment filename)
65 # * :download - Force download (default: false)
66 # * :download - Force download (default: false)
66 def link_to_attachment(attachment, options={})
67 def link_to_attachment(attachment, options={})
67 text = options.delete(:text) || attachment.filename
68 text = options.delete(:text) || attachment.filename
68 action = options.delete(:download) ? 'download' : 'show'
69 action = options.delete(:download) ? 'download' : 'show'
69
70
70 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
71 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
71 end
72 end
72
73
73 def toggle_link(name, id, options={})
74 def toggle_link(name, id, options={})
74 onclick = "Element.toggle('#{id}'); "
75 onclick = "Element.toggle('#{id}'); "
75 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
76 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
76 onclick << "return false;"
77 onclick << "return false;"
77 link_to(name, "#", :onclick => onclick)
78 link_to(name, "#", :onclick => onclick)
78 end
79 end
79
80
80 def image_to_function(name, function, html_options = {})
81 def image_to_function(name, function, html_options = {})
81 html_options.symbolize_keys!
82 html_options.symbolize_keys!
82 tag(:input, html_options.merge({
83 tag(:input, html_options.merge({
83 :type => "image", :src => image_path(name),
84 :type => "image", :src => image_path(name),
84 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
85 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
85 }))
86 }))
86 end
87 end
87
88
88 def prompt_to_remote(name, text, param, url, html_options = {})
89 def prompt_to_remote(name, text, param, url, html_options = {})
89 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
90 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
90 link_to name, {}, html_options
91 link_to name, {}, html_options
91 end
92 end
92
93 def format_date(date)
94 return nil unless date
95 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
97 date.strftime(@date_format)
98 end
99
100 def format_time(time, include_date = true)
101 return nil unless time
102 time = time.to_time if time.is_a?(String)
103 zone = User.current.time_zone
104 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
105 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
106 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
107 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
108 end
109
93
110 def format_activity_title(text)
94 def format_activity_title(text)
111 h(truncate_single_line(text, 100))
95 h(truncate_single_line(text, :length => 100))
112 end
96 end
113
97
114 def format_activity_day(date)
98 def format_activity_day(date)
115 date == Date.today ? l(:label_today).titleize : format_date(date)
99 date == Date.today ? l(:label_today).titleize : format_date(date)
116 end
100 end
117
101
118 def format_activity_description(text)
102 def format_activity_description(text)
119 h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
103 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
120 end
121
122 def distance_of_date_in_words(from_date, to_date = 0)
123 from_date = from_date.to_date if from_date.respond_to?(:to_date)
124 to_date = to_date.to_date if to_date.respond_to?(:to_date)
125 distance_in_days = (to_date - from_date).abs
126 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
127 end
104 end
128
105
129 def due_date_distance_in_words(date)
106 def due_date_distance_in_words(date)
130 if date
107 if date
131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
108 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
132 end
109 end
133 end
110 end
134
111
135 def render_page_hierarchy(pages, node=nil)
112 def render_page_hierarchy(pages, node=nil)
136 content = ''
113 content = ''
137 if pages[node]
114 if pages[node]
138 content << "<ul class=\"pages-hierarchy\">\n"
115 content << "<ul class=\"pages-hierarchy\">\n"
139 pages[node].each do |page|
116 pages[node].each do |page|
140 content << "<li>"
117 content << "<li>"
141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
118 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
119 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
120 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
144 content << "</li>\n"
121 content << "</li>\n"
145 end
122 end
146 content << "</ul>\n"
123 content << "</ul>\n"
147 end
124 end
148 content
125 content
149 end
126 end
150
127
151 # Renders flash messages
128 # Renders flash messages
152 def render_flash_messages
129 def render_flash_messages
153 s = ''
130 s = ''
154 flash.each do |k,v|
131 flash.each do |k,v|
155 s << content_tag('div', v, :class => "flash #{k}")
132 s << content_tag('div', v, :class => "flash #{k}")
156 end
133 end
157 s
134 s
158 end
135 end
159
136
160 # Renders the project quick-jump box
137 # Renders the project quick-jump box
161 def render_project_jump_box
138 def render_project_jump_box
162 # Retrieve them now to avoid a COUNT query
139 # Retrieve them now to avoid a COUNT query
163 projects = User.current.projects.all
140 projects = User.current.projects.all
164 if projects.any?
141 if projects.any?
165 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
142 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
166 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
143 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
167 '<option disabled="disabled">---</option>'
144 '<option disabled="disabled">---</option>'
168 s << project_tree_options_for_select(projects) do |p|
145 s << project_tree_options_for_select(projects) do |p|
169 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
146 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
170 end
147 end
171 s << '</select>'
148 s << '</select>'
172 s
149 s
173 end
150 end
174 end
151 end
175
152
176 def project_tree_options_for_select(projects, options = {})
153 def project_tree_options_for_select(projects, options = {})
177 s = ''
154 s = ''
178 project_tree(projects) do |project, level|
155 project_tree(projects) do |project, level|
179 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
156 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
180 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
157 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
181 tag_options.merge!(yield(project)) if block_given?
158 tag_options.merge!(yield(project)) if block_given?
182 s << content_tag('option', name_prefix + h(project), tag_options)
159 s << content_tag('option', name_prefix + h(project), tag_options)
183 end
160 end
184 s
161 s
185 end
162 end
186
163
187 # Yields the given block for each project with its level in the tree
164 # Yields the given block for each project with its level in the tree
188 def project_tree(projects, &block)
165 def project_tree(projects, &block)
189 ancestors = []
166 ancestors = []
190 projects.sort_by(&:lft).each do |project|
167 projects.sort_by(&:lft).each do |project|
191 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
168 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
192 ancestors.pop
169 ancestors.pop
193 end
170 end
194 yield project, ancestors.size
171 yield project, ancestors.size
195 ancestors << project
172 ancestors << project
196 end
173 end
197 end
174 end
198
175
199 def project_nested_ul(projects, &block)
176 def project_nested_ul(projects, &block)
200 s = ''
177 s = ''
201 if projects.any?
178 if projects.any?
202 ancestors = []
179 ancestors = []
203 projects.sort_by(&:lft).each do |project|
180 projects.sort_by(&:lft).each do |project|
204 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
181 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
205 s << "<ul>\n"
182 s << "<ul>\n"
206 else
183 else
207 ancestors.pop
184 ancestors.pop
208 s << "</li>"
185 s << "</li>"
209 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
186 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
210 ancestors.pop
187 ancestors.pop
211 s << "</ul></li>\n"
188 s << "</ul></li>\n"
212 end
189 end
213 end
190 end
214 s << "<li>"
191 s << "<li>"
215 s << yield(project).to_s
192 s << yield(project).to_s
216 ancestors << project
193 ancestors << project
217 end
194 end
218 s << ("</li></ul>\n" * ancestors.size)
195 s << ("</li></ul>\n" * ancestors.size)
219 end
196 end
220 s
197 s
221 end
198 end
222
199
223 # Truncates and returns the string as a single line
200 # Truncates and returns the string as a single line
224 def truncate_single_line(string, *args)
201 def truncate_single_line(string, *args)
225 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
202 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
226 end
203 end
227
204
228 def html_hours(text)
205 def html_hours(text)
229 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
206 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
230 end
207 end
231
208
232 def authoring(created, author, options={})
209 def authoring(created, author, options={})
233 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
210 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
234 link_to(distance_of_time_in_words(Time.now, created),
211 link_to(distance_of_time_in_words(Time.now, created),
235 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
212 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
236 :title => format_time(created))
213 :title => format_time(created))
237 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
214 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
238 l(options[:label] || :label_added_time_by, author_tag, time_tag)
215 l(options[:label] || :label_added_time_by, :author => author_tag, :age => time_tag)
239 end
240
241 def l_or_humanize(s, options={})
242 k = "#{options[:prefix]}#{s}".to_sym
243 l_has_string?(k) ? l(k) : s.to_s.humanize
244 end
245
246 def day_name(day)
247 l(:general_day_names).split(',')[day-1]
248 end
249
250 def month_name(month)
251 l(:actionview_datehelper_select_month_names).split(',')[month-1]
252 end
216 end
253
217
254 def syntax_highlight(name, content)
218 def syntax_highlight(name, content)
255 type = CodeRay::FileType[name]
219 type = CodeRay::FileType[name]
256 type ? CodeRay.scan(content, type).html : h(content)
220 type ? CodeRay.scan(content, type).html : h(content)
257 end
221 end
258
222
259 def to_path_param(path)
223 def to_path_param(path)
260 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
224 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
261 end
225 end
262
226
263 def pagination_links_full(paginator, count=nil, options={})
227 def pagination_links_full(paginator, count=nil, options={})
264 page_param = options.delete(:page_param) || :page
228 page_param = options.delete(:page_param) || :page
265 url_param = params.dup
229 url_param = params.dup
266 # don't reuse query params if filters are present
230 # don't reuse query params if filters are present
267 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
231 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
268
232
269 html = ''
233 html = ''
270 if paginator.current.previous
234 if paginator.current.previous
271 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
235 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
272 end
236 end
273
237
274 html << (pagination_links_each(paginator, options) do |n|
238 html << (pagination_links_each(paginator, options) do |n|
275 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
239 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
276 end || '')
240 end || '')
277
241
278 if paginator.current.next
242 if paginator.current.next
279 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
243 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
280 end
244 end
281
245
282 unless count.nil?
246 unless count.nil?
283 html << [
247 html << [
284 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
248 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
285 per_page_links(paginator.items_per_page)
249 per_page_links(paginator.items_per_page)
286 ].compact.join(' | ')
250 ].compact.join(' | ')
287 end
251 end
288
252
289 html
253 html
290 end
254 end
291
255
292 def per_page_links(selected=nil)
256 def per_page_links(selected=nil)
293 url_param = params.dup
257 url_param = params.dup
294 url_param.clear if url_param.has_key?(:set_filter)
258 url_param.clear if url_param.has_key?(:set_filter)
295
259
296 links = Setting.per_page_options_array.collect do |n|
260 links = Setting.per_page_options_array.collect do |n|
297 n == selected ? n : link_to_remote(n, {:update => "content",
261 n == selected ? n : link_to_remote(n, {:update => "content",
298 :url => params.dup.merge(:per_page => n),
262 :url => params.dup.merge(:per_page => n),
299 :method => :get},
263 :method => :get},
300 {:href => url_for(url_param.merge(:per_page => n))})
264 {:href => url_for(url_param.merge(:per_page => n))})
301 end
265 end
302 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
266 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
303 end
267 end
304
268
305 def breadcrumb(*args)
269 def breadcrumb(*args)
306 elements = args.flatten
270 elements = args.flatten
307 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
271 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
308 end
272 end
309
273
310 def other_formats_links(&block)
274 def other_formats_links(&block)
311 concat('<p class="other-formats">' + l(:label_export_to), block.binding)
275 concat('<p class="other-formats">' + l(:label_export_to))
312 yield Redmine::Views::OtherFormatsBuilder.new(self)
276 yield Redmine::Views::OtherFormatsBuilder.new(self)
313 concat('</p>', block.binding)
277 concat('</p>')
314 end
278 end
315
279
316 def page_header_title
280 def page_header_title
317 if @project.nil? || @project.new_record?
281 if @project.nil? || @project.new_record?
318 h(Setting.app_title)
282 h(Setting.app_title)
319 else
283 else
320 b = []
284 b = []
321 ancestors = (@project.root? ? [] : @project.ancestors.visible)
285 ancestors = (@project.root? ? [] : @project.ancestors.visible)
322 if ancestors.any?
286 if ancestors.any?
323 root = ancestors.shift
287 root = ancestors.shift
324 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
288 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
325 if ancestors.size > 2
289 if ancestors.size > 2
326 b << '&#8230;'
290 b << '&#8230;'
327 ancestors = ancestors[-2, 2]
291 ancestors = ancestors[-2, 2]
328 end
292 end
329 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
293 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
330 end
294 end
331 b << h(@project)
295 b << h(@project)
332 b.join(' &#187; ')
296 b.join(' &#187; ')
333 end
297 end
334 end
298 end
335
299
336 def html_title(*args)
300 def html_title(*args)
337 if args.empty?
301 if args.empty?
338 title = []
302 title = []
339 title << @project.name if @project
303 title << @project.name if @project
340 title += @html_title if @html_title
304 title += @html_title if @html_title
341 title << Setting.app_title
305 title << Setting.app_title
342 title.compact.join(' - ')
306 title.compact.join(' - ')
343 else
307 else
344 @html_title ||= []
308 @html_title ||= []
345 @html_title += args
309 @html_title += args
346 end
310 end
347 end
311 end
348
312
349 def accesskey(s)
313 def accesskey(s)
350 Redmine::AccessKeys.key_for s
314 Redmine::AccessKeys.key_for s
351 end
315 end
352
316
353 # Formats text according to system settings.
317 # Formats text according to system settings.
354 # 2 ways to call this method:
318 # 2 ways to call this method:
355 # * with a String: textilizable(text, options)
319 # * with a String: textilizable(text, options)
356 # * with an object and one of its attribute: textilizable(issue, :description, options)
320 # * with an object and one of its attribute: textilizable(issue, :description, options)
357 def textilizable(*args)
321 def textilizable(*args)
358 options = args.last.is_a?(Hash) ? args.pop : {}
322 options = args.last.is_a?(Hash) ? args.pop : {}
359 case args.size
323 case args.size
360 when 1
324 when 1
361 obj = options[:object]
325 obj = options[:object]
362 text = args.shift
326 text = args.shift
363 when 2
327 when 2
364 obj = args.shift
328 obj = args.shift
365 text = obj.send(args.shift).to_s
329 text = obj.send(args.shift).to_s
366 else
330 else
367 raise ArgumentError, 'invalid arguments to textilizable'
331 raise ArgumentError, 'invalid arguments to textilizable'
368 end
332 end
369 return '' if text.blank?
333 return '' if text.blank?
370
334
371 only_path = options.delete(:only_path) == false ? false : true
335 only_path = options.delete(:only_path) == false ? false : true
372
336
373 # when using an image link, try to use an attachment, if possible
337 # when using an image link, try to use an attachment, if possible
374 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
338 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
375
339
376 if attachments
340 if attachments
377 attachments = attachments.sort_by(&:created_on).reverse
341 attachments = attachments.sort_by(&:created_on).reverse
378 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
342 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
379 style = $1
343 style = $1
380 filename = $6.downcase
344 filename = $6.downcase
381 # search for the picture in attachments
345 # search for the picture in attachments
382 if found = attachments.detect { |att| att.filename.downcase == filename }
346 if found = attachments.detect { |att| att.filename.downcase == filename }
383 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
347 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
384 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
348 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
385 alt = desc.blank? ? nil : "(#{desc})"
349 alt = desc.blank? ? nil : "(#{desc})"
386 "!#{style}#{image_url}#{alt}!"
350 "!#{style}#{image_url}#{alt}!"
387 else
351 else
388 m
352 m
389 end
353 end
390 end
354 end
391 end
355 end
392
356
393 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
357 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
394
358
395 # different methods for formatting wiki links
359 # different methods for formatting wiki links
396 case options[:wiki_links]
360 case options[:wiki_links]
397 when :local
361 when :local
398 # used for local links to html files
362 # used for local links to html files
399 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
363 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
400 when :anchor
364 when :anchor
401 # used for single-file wiki export
365 # used for single-file wiki export
402 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
366 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
403 else
367 else
404 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
368 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
405 end
369 end
406
370
407 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
371 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
408
372
409 # Wiki links
373 # Wiki links
410 #
374 #
411 # Examples:
375 # Examples:
412 # [[mypage]]
376 # [[mypage]]
413 # [[mypage|mytext]]
377 # [[mypage|mytext]]
414 # wiki links can refer other project wikis, using project name or identifier:
378 # wiki links can refer other project wikis, using project name or identifier:
415 # [[project:]] -> wiki starting page
379 # [[project:]] -> wiki starting page
416 # [[project:|mytext]]
380 # [[project:|mytext]]
417 # [[project:mypage]]
381 # [[project:mypage]]
418 # [[project:mypage|mytext]]
382 # [[project:mypage|mytext]]
419 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
383 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
420 link_project = project
384 link_project = project
421 esc, all, page, title = $1, $2, $3, $5
385 esc, all, page, title = $1, $2, $3, $5
422 if esc.nil?
386 if esc.nil?
423 if page =~ /^([^\:]+)\:(.*)$/
387 if page =~ /^([^\:]+)\:(.*)$/
424 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
388 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
425 page = $2
389 page = $2
426 title ||= $1 if page.blank?
390 title ||= $1 if page.blank?
427 end
391 end
428
392
429 if link_project && link_project.wiki
393 if link_project && link_project.wiki
430 # extract anchor
394 # extract anchor
431 anchor = nil
395 anchor = nil
432 if page =~ /^(.+?)\#(.+)$/
396 if page =~ /^(.+?)\#(.+)$/
433 page, anchor = $1, $2
397 page, anchor = $1, $2
434 end
398 end
435 # check if page exists
399 # check if page exists
436 wiki_page = link_project.wiki.find_page(page)
400 wiki_page = link_project.wiki.find_page(page)
437 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
401 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
438 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
402 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
439 else
403 else
440 # project or wiki doesn't exist
404 # project or wiki doesn't exist
441 all
405 all
442 end
406 end
443 else
407 else
444 all
408 all
445 end
409 end
446 end
410 end
447
411
448 # Redmine links
412 # Redmine links
449 #
413 #
450 # Examples:
414 # Examples:
451 # Issues:
415 # Issues:
452 # #52 -> Link to issue #52
416 # #52 -> Link to issue #52
453 # Changesets:
417 # Changesets:
454 # r52 -> Link to revision 52
418 # r52 -> Link to revision 52
455 # commit:a85130f -> Link to scmid starting with a85130f
419 # commit:a85130f -> Link to scmid starting with a85130f
456 # Documents:
420 # Documents:
457 # document#17 -> Link to document with id 17
421 # document#17 -> Link to document with id 17
458 # document:Greetings -> Link to the document with title "Greetings"
422 # document:Greetings -> Link to the document with title "Greetings"
459 # document:"Some document" -> Link to the document with title "Some document"
423 # document:"Some document" -> Link to the document with title "Some document"
460 # Versions:
424 # Versions:
461 # version#3 -> Link to version with id 3
425 # version#3 -> Link to version with id 3
462 # version:1.0.0 -> Link to version named "1.0.0"
426 # version:1.0.0 -> Link to version named "1.0.0"
463 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
427 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
464 # Attachments:
428 # Attachments:
465 # attachment:file.zip -> Link to the attachment of the current object named file.zip
429 # attachment:file.zip -> Link to the attachment of the current object named file.zip
466 # Source files:
430 # Source files:
467 # source:some/file -> Link to the file located at /some/file in the project's repository
431 # source:some/file -> Link to the file located at /some/file in the project's repository
468 # source:some/file@52 -> Link to the file's revision 52
432 # source:some/file@52 -> Link to the file's revision 52
469 # source:some/file#L120 -> Link to line 120 of the file
433 # source:some/file#L120 -> Link to line 120 of the file
470 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
434 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
471 # export:some/file -> Force the download of the file
435 # export:some/file -> Force the download of the file
472 # Forum messages:
436 # Forum messages:
473 # message#1218 -> Link to message with id 1218
437 # message#1218 -> Link to message with id 1218
474 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
438 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
475 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
439 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
476 link = nil
440 link = nil
477 if esc.nil?
441 if esc.nil?
478 if prefix.nil? && sep == 'r'
442 if prefix.nil? && sep == 'r'
479 if project && (changeset = project.changesets.find_by_revision(oid))
443 if project && (changeset = project.changesets.find_by_revision(oid))
480 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
444 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
481 :class => 'changeset',
445 :class => 'changeset',
482 :title => truncate_single_line(changeset.comments, 100))
446 :title => truncate_single_line(changeset.comments, :length => 100))
483 end
447 end
484 elsif sep == '#'
448 elsif sep == '#'
485 oid = oid.to_i
449 oid = oid.to_i
486 case prefix
450 case prefix
487 when nil
451 when nil
488 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
452 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
489 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
453 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
490 :class => (issue.closed? ? 'issue closed' : 'issue'),
454 :class => (issue.closed? ? 'issue closed' : 'issue'),
491 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
455 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
492 link = content_tag('del', link) if issue.closed?
456 link = content_tag('del', link) if issue.closed?
493 end
457 end
494 when 'document'
458 when 'document'
495 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
459 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
496 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
460 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
497 :class => 'document'
461 :class => 'document'
498 end
462 end
499 when 'version'
463 when 'version'
500 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
464 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
501 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
465 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
502 :class => 'version'
466 :class => 'version'
503 end
467 end
504 when 'message'
468 when 'message'
505 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
469 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
506 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
470 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
507 :controller => 'messages',
471 :controller => 'messages',
508 :action => 'show',
472 :action => 'show',
509 :board_id => message.board,
473 :board_id => message.board,
510 :id => message.root,
474 :id => message.root,
511 :anchor => (message.parent ? "message-#{message.id}" : nil)},
475 :anchor => (message.parent ? "message-#{message.id}" : nil)},
512 :class => 'message'
476 :class => 'message'
513 end
477 end
514 end
478 end
515 elsif sep == ':'
479 elsif sep == ':'
516 # removes the double quotes if any
480 # removes the double quotes if any
517 name = oid.gsub(%r{^"(.*)"$}, "\\1")
481 name = oid.gsub(%r{^"(.*)"$}, "\\1")
518 case prefix
482 case prefix
519 when 'document'
483 when 'document'
520 if project && document = project.documents.find_by_title(name)
484 if project && document = project.documents.find_by_title(name)
521 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
485 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
522 :class => 'document'
486 :class => 'document'
523 end
487 end
524 when 'version'
488 when 'version'
525 if project && version = project.versions.find_by_name(name)
489 if project && version = project.versions.find_by_name(name)
526 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
490 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
527 :class => 'version'
491 :class => 'version'
528 end
492 end
529 when 'commit'
493 when 'commit'
530 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
494 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
531 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
495 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
532 :class => 'changeset',
496 :class => 'changeset',
533 :title => truncate_single_line(changeset.comments, 100)
497 :title => truncate_single_line(changeset.comments, :length => 100)
534 end
498 end
535 when 'source', 'export'
499 when 'source', 'export'
536 if project && project.repository
500 if project && project.repository
537 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
501 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
538 path, rev, anchor = $1, $3, $5
502 path, rev, anchor = $1, $3, $5
539 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
503 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
540 :path => to_path_param(path),
504 :path => to_path_param(path),
541 :rev => rev,
505 :rev => rev,
542 :anchor => anchor,
506 :anchor => anchor,
543 :format => (prefix == 'export' ? 'raw' : nil)},
507 :format => (prefix == 'export' ? 'raw' : nil)},
544 :class => (prefix == 'export' ? 'source download' : 'source')
508 :class => (prefix == 'export' ? 'source download' : 'source')
545 end
509 end
546 when 'attachment'
510 when 'attachment'
547 if attachments && attachment = attachments.detect {|a| a.filename == name }
511 if attachments && attachment = attachments.detect {|a| a.filename == name }
548 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
512 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
549 :class => 'attachment'
513 :class => 'attachment'
550 end
514 end
551 end
515 end
552 end
516 end
553 end
517 end
554 leading + (link || "#{prefix}#{sep}#{oid}")
518 leading + (link || "#{prefix}#{sep}#{oid}")
555 end
519 end
556
520
557 text
521 text
558 end
522 end
559
523
560 # Same as Rails' simple_format helper without using paragraphs
524 # Same as Rails' simple_format helper without using paragraphs
561 def simple_format_without_paragraph(text)
525 def simple_format_without_paragraph(text)
562 text.to_s.
526 text.to_s.
563 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
527 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
564 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
528 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
565 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
529 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
566 end
530 end
567
531
568 def error_messages_for(object_name, options = {})
569 options = options.symbolize_keys
570 object = instance_variable_get("@#{object_name}")
571 if object && !object.errors.empty?
572 # build full_messages here with controller current language
573 full_messages = []
574 object.errors.each do |attr, msg|
575 next if msg.nil?
576 msg = [msg] unless msg.is_a?(Array)
577 if attr == "base"
578 full_messages << l(*msg)
579 else
580 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(*msg) unless attr == "custom_values"
581 end
582 end
583 # retrieve custom values error messages
584 if object.errors[:custom_values]
585 object.custom_values.each do |v|
586 v.errors.each do |attr, msg|
587 next if msg.nil?
588 msg = [msg] unless msg.is_a?(Array)
589 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(*msg)
590 end
591 end
592 end
593 content_tag("div",
594 content_tag(
595 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
596 ) +
597 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
598 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
599 )
600 else
601 ""
602 end
603 end
604
605 def lang_options_for_select(blank=true)
532 def lang_options_for_select(blank=true)
606 (blank ? [["(auto)", ""]] : []) +
533 (blank ? [["(auto)", ""]] : []) +
607 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
534 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
608 end
535 end
609
536
610 def label_tag_for(name, option_tags = nil, options = {})
537 def label_tag_for(name, option_tags = nil, options = {})
611 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
538 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
612 content_tag("label", label_text)
539 content_tag("label", label_text)
613 end
540 end
614
541
615 def labelled_tabular_form_for(name, object, options, &proc)
542 def labelled_tabular_form_for(name, object, options, &proc)
616 options[:html] ||= {}
543 options[:html] ||= {}
617 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
544 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
618 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
545 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
619 end
546 end
620
547
621 def back_url_hidden_field_tag
548 def back_url_hidden_field_tag
622 back_url = params[:back_url] || request.env['HTTP_REFERER']
549 back_url = params[:back_url] || request.env['HTTP_REFERER']
623 back_url = CGI.unescape(back_url.to_s)
550 back_url = CGI.unescape(back_url.to_s)
624 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
551 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
625 end
552 end
626
553
627 def check_all_links(form_name)
554 def check_all_links(form_name)
628 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
555 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
629 " | " +
556 " | " +
630 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
557 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
631 end
558 end
632
559
633 def progress_bar(pcts, options={})
560 def progress_bar(pcts, options={})
634 pcts = [pcts, pcts] unless pcts.is_a?(Array)
561 pcts = [pcts, pcts] unless pcts.is_a?(Array)
635 pcts[1] = pcts[1] - pcts[0]
562 pcts[1] = pcts[1] - pcts[0]
636 pcts << (100 - pcts[1] - pcts[0])
563 pcts << (100 - pcts[1] - pcts[0])
637 width = options[:width] || '100px;'
564 width = options[:width] || '100px;'
638 legend = options[:legend] || ''
565 legend = options[:legend] || ''
639 content_tag('table',
566 content_tag('table',
640 content_tag('tr',
567 content_tag('tr',
641 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
568 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
642 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
569 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
643 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
570 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
644 ), :class => 'progress', :style => "width: #{width};") +
571 ), :class => 'progress', :style => "width: #{width};") +
645 content_tag('p', legend, :class => 'pourcent')
572 content_tag('p', legend, :class => 'pourcent')
646 end
573 end
647
574
648 def context_menu_link(name, url, options={})
575 def context_menu_link(name, url, options={})
649 options[:class] ||= ''
576 options[:class] ||= ''
650 if options.delete(:selected)
577 if options.delete(:selected)
651 options[:class] << ' icon-checked disabled'
578 options[:class] << ' icon-checked disabled'
652 options[:disabled] = true
579 options[:disabled] = true
653 end
580 end
654 if options.delete(:disabled)
581 if options.delete(:disabled)
655 options.delete(:method)
582 options.delete(:method)
656 options.delete(:confirm)
583 options.delete(:confirm)
657 options.delete(:onclick)
584 options.delete(:onclick)
658 options[:class] << ' disabled'
585 options[:class] << ' disabled'
659 url = '#'
586 url = '#'
660 end
587 end
661 link_to name, url, options
588 link_to name, url, options
662 end
589 end
663
590
664 def calendar_for(field_id)
591 def calendar_for(field_id)
665 include_calendar_headers_tags
592 include_calendar_headers_tags
666 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
593 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
667 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
594 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
668 end
595 end
669
596
670 def include_calendar_headers_tags
597 def include_calendar_headers_tags
671 unless @calendar_headers_tags_included
598 unless @calendar_headers_tags_included
672 @calendar_headers_tags_included = true
599 @calendar_headers_tags_included = true
673 content_for :header_tags do
600 content_for :header_tags do
674 javascript_include_tag('calendar/calendar') +
601 javascript_include_tag('calendar/calendar') +
675 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
602 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
676 javascript_include_tag('calendar/calendar-setup') +
603 javascript_include_tag('calendar/calendar-setup') +
677 stylesheet_link_tag('calendar')
604 stylesheet_link_tag('calendar')
678 end
605 end
679 end
606 end
680 end
607 end
681
608
682 def content_for(name, content = nil, &block)
609 def content_for(name, content = nil, &block)
683 @has_content ||= {}
610 @has_content ||= {}
684 @has_content[name] = true
611 @has_content[name] = true
685 super(name, content, &block)
612 super(name, content, &block)
686 end
613 end
687
614
688 def has_content?(name)
615 def has_content?(name)
689 (@has_content && @has_content[name]) || false
616 (@has_content && @has_content[name]) || false
690 end
617 end
691
618
692 # Returns the avatar image tag for the given +user+ if avatars are enabled
619 # Returns the avatar image tag for the given +user+ if avatars are enabled
693 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
620 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
694 def avatar(user, options = { })
621 def avatar(user, options = { })
695 if Setting.gravatar_enabled?
622 if Setting.gravatar_enabled?
696 email = nil
623 email = nil
697 if user.respond_to?(:mail)
624 if user.respond_to?(:mail)
698 email = user.mail
625 email = user.mail
699 elsif user.to_s =~ %r{<(.+?)>}
626 elsif user.to_s =~ %r{<(.+?)>}
700 email = $1
627 email = $1
701 end
628 end
702 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
629 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
703 end
630 end
704 end
631 end
705
632
706 private
633 private
707
634
708 def wiki_helper
635 def wiki_helper
709 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
636 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
710 extend helper
637 extend helper
711 return self
638 return self
712 end
639 end
713
640
714 def link_to_remote_content_update(text, url_params)
641 def link_to_remote_content_update(text, url_params)
715 link_to_remote(text,
642 link_to_remote(text,
716 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
643 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
717 {:href => url_for(:params => url_params)}
644 {:href => url_for(:params => url_params)}
718 )
645 )
719 end
646 end
720
647
721 end
648 end
@@ -1,196 +1,196
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 require 'csv'
18 require 'csv'
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_issue_tooltip(issue)
23 def render_issue_tooltip(issue)
24 @cached_label_start_date ||= l(:field_start_date)
24 @cached_label_start_date ||= l(:field_start_date)
25 @cached_label_due_date ||= l(:field_due_date)
25 @cached_label_due_date ||= l(:field_due_date)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
27 @cached_label_priority ||= l(:field_priority)
27 @cached_label_priority ||= l(:field_priority)
28
28
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
34 end
34 end
35
35
36 # Returns a string of css classes that apply to the given issue
36 # Returns a string of css classes that apply to the given issue
37 def css_issue_classes(issue)
37 def css_issue_classes(issue)
38 s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
38 s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
39 s << ' closed' if issue.closed?
39 s << ' closed' if issue.closed?
40 s << ' overdue' if issue.overdue?
40 s << ' overdue' if issue.overdue?
41 s
41 s
42 end
42 end
43
43
44 def sidebar_queries
44 def sidebar_queries
45 unless @sidebar_queries
45 unless @sidebar_queries
46 # User can see public queries and his own queries
46 # User can see public queries and his own queries
47 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
47 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
48 # Project specific queries and global queries
48 # Project specific queries and global queries
49 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
49 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
50 @sidebar_queries = Query.find(:all,
50 @sidebar_queries = Query.find(:all,
51 :order => "name ASC",
51 :order => "name ASC",
52 :conditions => visible.conditions)
52 :conditions => visible.conditions)
53 end
53 end
54 @sidebar_queries
54 @sidebar_queries
55 end
55 end
56
56
57 def show_detail(detail, no_html=false)
57 def show_detail(detail, no_html=false)
58 case detail.property
58 case detail.property
59 when 'attr'
59 when 'attr'
60 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
60 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
61 case detail.prop_key
61 case detail.prop_key
62 when 'due_date', 'start_date'
62 when 'due_date', 'start_date'
63 value = format_date(detail.value.to_date) if detail.value
63 value = format_date(detail.value.to_date) if detail.value
64 old_value = format_date(detail.old_value.to_date) if detail.old_value
64 old_value = format_date(detail.old_value.to_date) if detail.old_value
65 when 'project_id'
65 when 'project_id'
66 p = Project.find_by_id(detail.value) and value = p.name if detail.value
66 p = Project.find_by_id(detail.value) and value = p.name if detail.value
67 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
67 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
68 when 'status_id'
68 when 'status_id'
69 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
69 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
70 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
70 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
71 when 'tracker_id'
71 when 'tracker_id'
72 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
72 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
73 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
73 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
74 when 'assigned_to_id'
74 when 'assigned_to_id'
75 u = User.find_by_id(detail.value) and value = u.name if detail.value
75 u = User.find_by_id(detail.value) and value = u.name if detail.value
76 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
76 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
77 when 'priority_id'
77 when 'priority_id'
78 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
78 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
79 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
79 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
80 when 'category_id'
80 when 'category_id'
81 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
81 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
82 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
82 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
83 when 'fixed_version_id'
83 when 'fixed_version_id'
84 v = Version.find_by_id(detail.value) and value = v.name if detail.value
84 v = Version.find_by_id(detail.value) and value = v.name if detail.value
85 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
85 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
86 when 'estimated_hours'
86 when 'estimated_hours'
87 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
87 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
88 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
88 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
89 end
89 end
90 when 'cf'
90 when 'cf'
91 custom_field = CustomField.find_by_id(detail.prop_key)
91 custom_field = CustomField.find_by_id(detail.prop_key)
92 if custom_field
92 if custom_field
93 label = custom_field.name
93 label = custom_field.name
94 value = format_value(detail.value, custom_field.field_format) if detail.value
94 value = format_value(detail.value, custom_field.field_format) if detail.value
95 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
95 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
96 end
96 end
97 when 'attachment'
97 when 'attachment'
98 label = l(:label_attachment)
98 label = l(:label_attachment)
99 end
99 end
100 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
100 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
101
101
102 label ||= detail.prop_key
102 label ||= detail.prop_key
103 value ||= detail.value
103 value ||= detail.value
104 old_value ||= detail.old_value
104 old_value ||= detail.old_value
105
105
106 unless no_html
106 unless no_html
107 label = content_tag('strong', label)
107 label = content_tag('strong', label)
108 old_value = content_tag("i", h(old_value)) if detail.old_value
108 old_value = content_tag("i", h(old_value)) if detail.old_value
109 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
109 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
110 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
110 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
111 # Link to the attachment if it has not been removed
111 # Link to the attachment if it has not been removed
112 value = link_to_attachment(a)
112 value = link_to_attachment(a)
113 else
113 else
114 value = content_tag("i", h(value)) if value
114 value = content_tag("i", h(value)) if value
115 end
115 end
116 end
116 end
117
117
118 if !detail.value.blank?
118 if !detail.value.blank?
119 case detail.property
119 case detail.property
120 when 'attr', 'cf'
120 when 'attr', 'cf'
121 if !detail.old_value.blank?
121 if !detail.old_value.blank?
122 label + " " + l(:text_journal_changed, old_value, value)
122 label + " " + l(:text_journal_changed, :old => old_value, :new => value)
123 else
123 else
124 label + " " + l(:text_journal_set_to, value)
124 label + " " + l(:text_journal_set_to, value)
125 end
125 end
126 when 'attachment'
126 when 'attachment'
127 "#{label} #{value} #{l(:label_added)}"
127 "#{label} #{value} #{l(:label_added)}"
128 end
128 end
129 else
129 else
130 case detail.property
130 case detail.property
131 when 'attr', 'cf'
131 when 'attr', 'cf'
132 label + " " + l(:text_journal_deleted) + " (#{old_value})"
132 label + " " + l(:text_journal_deleted) + " (#{old_value})"
133 when 'attachment'
133 when 'attachment'
134 "#{label} #{old_value} #{l(:label_deleted)}"
134 "#{label} #{old_value} #{l(:label_deleted)}"
135 end
135 end
136 end
136 end
137 end
137 end
138
138
139 def issues_to_csv(issues, project = nil)
139 def issues_to_csv(issues, project = nil)
140 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
140 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
141 decimal_separator = l(:general_csv_decimal_separator)
141 decimal_separator = l(:general_csv_decimal_separator)
142 export = StringIO.new
142 export = StringIO.new
143 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
143 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
144 # csv header fields
144 # csv header fields
145 headers = [ "#",
145 headers = [ "#",
146 l(:field_status),
146 l(:field_status),
147 l(:field_project),
147 l(:field_project),
148 l(:field_tracker),
148 l(:field_tracker),
149 l(:field_priority),
149 l(:field_priority),
150 l(:field_subject),
150 l(:field_subject),
151 l(:field_assigned_to),
151 l(:field_assigned_to),
152 l(:field_category),
152 l(:field_category),
153 l(:field_fixed_version),
153 l(:field_fixed_version),
154 l(:field_author),
154 l(:field_author),
155 l(:field_start_date),
155 l(:field_start_date),
156 l(:field_due_date),
156 l(:field_due_date),
157 l(:field_done_ratio),
157 l(:field_done_ratio),
158 l(:field_estimated_hours),
158 l(:field_estimated_hours),
159 l(:field_created_on),
159 l(:field_created_on),
160 l(:field_updated_on)
160 l(:field_updated_on)
161 ]
161 ]
162 # Export project custom fields if project is given
162 # Export project custom fields if project is given
163 # otherwise export custom fields marked as "For all projects"
163 # otherwise export custom fields marked as "For all projects"
164 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
164 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
165 custom_fields.each {|f| headers << f.name}
165 custom_fields.each {|f| headers << f.name}
166 # Description in the last column
166 # Description in the last column
167 headers << l(:field_description)
167 headers << l(:field_description)
168 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
168 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
169 # csv lines
169 # csv lines
170 issues.each do |issue|
170 issues.each do |issue|
171 fields = [issue.id,
171 fields = [issue.id,
172 issue.status.name,
172 issue.status.name,
173 issue.project.name,
173 issue.project.name,
174 issue.tracker.name,
174 issue.tracker.name,
175 issue.priority.name,
175 issue.priority.name,
176 issue.subject,
176 issue.subject,
177 issue.assigned_to,
177 issue.assigned_to,
178 issue.category,
178 issue.category,
179 issue.fixed_version,
179 issue.fixed_version,
180 issue.author.name,
180 issue.author.name,
181 format_date(issue.start_date),
181 format_date(issue.start_date),
182 format_date(issue.due_date),
182 format_date(issue.due_date),
183 issue.done_ratio,
183 issue.done_ratio,
184 issue.estimated_hours.to_s.gsub('.', decimal_separator),
184 issue.estimated_hours.to_s.gsub('.', decimal_separator),
185 format_time(issue.created_on),
185 format_time(issue.created_on),
186 format_time(issue.updated_on)
186 format_time(issue.updated_on)
187 ]
187 ]
188 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
188 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
189 fields << issue.description
189 fields << issue.description
190 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
190 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
191 end
191 end
192 end
192 end
193 export.rewind
193 export.rewind
194 export
194 export
195 end
195 end
196 end
196 end
@@ -1,28 +1,28
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 module MessagesHelper
18 module MessagesHelper
19
19
20 def link_to_message(message)
20 def link_to_message(message)
21 return '' unless message
21 return '' unless message
22 link_to h(truncate(message.subject, 60)), :controller => 'messages',
22 link_to h(truncate(message.subject, :length => 60)), :controller => 'messages',
23 :action => 'show',
23 :action => 'show',
24 :board_id => message.board_id,
24 :board_id => message.board_id,
25 :id => message.root,
25 :id => message.root,
26 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
26 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
27 end
27 end
28 end
28 end
@@ -1,30 +1,30
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 module SettingsHelper
18 module SettingsHelper
19 def administration_settings_tabs
19 def administration_settings_tabs
20 tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
20 tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
21 {:name => 'display', :partial => 'settings/display', :label => :label_display},
21 {:name => 'display', :partial => 'settings/display', :label => :label_display},
22 {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
22 {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
23 {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
23 {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
24 {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
24 {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
25 {:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
25 {:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
26 {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)},
26 {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
27 {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
27 {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
28 ]
28 ]
29 end
29 end
30 end
30 end
@@ -1,169 +1,169
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.short_comments.blank? ? '' : (': ' + o.short_comments))},
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 :description => :long_comments,
27 :description => :long_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 :author_key => :user_id,
38 :find_options => {:include => {:repository => :project}}
38 :find_options => {:include => {:repository => :project}}
39
39
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 validates_uniqueness_of :revision, :scope => :repository_id
41 validates_uniqueness_of :revision, :scope => :repository_id
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43
43
44 def revision=(r)
44 def revision=(r)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
46 end
46 end
47
47
48 def comments=(comment)
48 def comments=(comment)
49 write_attribute(:comments, Changeset.normalize_comments(comment))
49 write_attribute(:comments, Changeset.normalize_comments(comment))
50 end
50 end
51
51
52 def committed_on=(date)
52 def committed_on=(date)
53 self.commit_date = date
53 self.commit_date = date
54 super
54 super
55 end
55 end
56
56
57 def project
57 def project
58 repository.project
58 repository.project
59 end
59 end
60
60
61 def author
61 def author
62 user || committer.to_s.split('<').first
62 user || committer.to_s.split('<').first
63 end
63 end
64
64
65 def before_create
65 def before_create
66 self.user = repository.find_committer_user(committer)
66 self.user = repository.find_committer_user(committer)
67 end
67 end
68
68
69 def after_create
69 def after_create
70 scan_comment_for_issue_ids
70 scan_comment_for_issue_ids
71 end
71 end
72 require 'pp'
72 require 'pp'
73
73
74 def scan_comment_for_issue_ids
74 def scan_comment_for_issue_ids
75 return if comments.blank?
75 return if comments.blank?
76 # keywords used to reference issues
76 # keywords used to reference issues
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
78 # keywords used to fix issues
78 # keywords used to fix issues
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
80 # status and optional done ratio applied
80 # status and optional done ratio applied
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
82 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
83
83
84 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("|")
85 return if kw_regexp.blank?
85 return if kw_regexp.blank?
86
86
87 referenced_issues = []
87 referenced_issues = []
88
88
89 if ref_keywords.delete('*')
89 if ref_keywords.delete('*')
90 # find any issue ID in the comments
90 # find any issue ID in the comments
91 target_issue_ids = []
91 target_issue_ids = []
92 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] }
93 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)
94 end
94 end
95
95
96 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|
97 action = match[0]
97 action = match[0]
98 target_issue_ids = match[1].scan(/\d+/)
98 target_issue_ids = match[1].scan(/\d+/)
99 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)
100 if fix_status && fix_keywords.include?(action.downcase)
100 if fix_status && fix_keywords.include?(action.downcase)
101 # update status of issues
101 # update status of issues
102 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?
103 target_issues.each do |issue|
103 target_issues.each do |issue|
104 # 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)
105 issue.reload
105 issue.reload
106 # don't change the status is the issue is closed
106 # don't change the status is the issue is closed
107 next if issue.status.is_closed?
107 next if issue.status.is_closed?
108 csettext = "r#{self.revision}"
108 csettext = "r#{self.revision}"
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
110 csettext = "commit:\"#{self.scmid}\""
110 csettext = "commit:\"#{self.scmid}\""
111 end
111 end
112 journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
112 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
113 issue.status = fix_status
113 issue.status = fix_status
114 issue.done_ratio = done_ratio if done_ratio
114 issue.done_ratio = done_ratio if done_ratio
115 issue.save
115 issue.save
116 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')
117 end
117 end
118 end
118 end
119 referenced_issues += target_issues
119 referenced_issues += target_issues
120 end
120 end
121
121
122 self.issues = referenced_issues.uniq
122 self.issues = referenced_issues.uniq
123 end
123 end
124
124
125 def short_comments
125 def short_comments
126 @short_comments || split_comments.first
126 @short_comments || split_comments.first
127 end
127 end
128
128
129 def long_comments
129 def long_comments
130 @long_comments || split_comments.last
130 @long_comments || split_comments.last
131 end
131 end
132
132
133 # Returns the previous changeset
133 # Returns the previous changeset
134 def previous
134 def previous
135 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
135 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
136 end
136 end
137
137
138 # Returns the next changeset
138 # Returns the next changeset
139 def next
139 def next
140 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
140 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
141 end
141 end
142
142
143 # Strips and reencodes a commit log before insertion into the database
143 # Strips and reencodes a commit log before insertion into the database
144 def self.normalize_comments(str)
144 def self.normalize_comments(str)
145 to_utf8(str.to_s.strip)
145 to_utf8(str.to_s.strip)
146 end
146 end
147
147
148 private
148 private
149
149
150 def split_comments
150 def split_comments
151 comments =~ /\A(.+?)\r?\n(.*)$/m
151 comments =~ /\A(.+?)\r?\n(.*)$/m
152 @short_comments = $1 || comments
152 @short_comments = $1 || comments
153 @long_comments = $2.to_s.strip
153 @long_comments = $2.to_s.strip
154 return @short_comments, @long_comments
154 return @short_comments, @long_comments
155 end
155 end
156
156
157 def self.to_utf8(str)
157 def self.to_utf8(str)
158 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
158 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
159 encoding = Setting.commit_logs_encoding.to_s.strip
159 encoding = Setting.commit_logs_encoding.to_s.strip
160 unless encoding.blank? || encoding == 'UTF-8'
160 unless encoding.blank? || encoding == 'UTF-8'
161 begin
161 begin
162 return Iconv.conv('UTF-8', encoding, str)
162 return Iconv.conv('UTF-8', encoding, str)
163 rescue Iconv::Failure
163 rescue Iconv::Failure
164 # do nothing here
164 # do nothing here
165 end
165 end
166 end
166 end
167 str
167 str
168 end
168 end
169 end
169 end
@@ -1,111 +1,111
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 CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 has_many :custom_values, :dependent => :delete_all
19 has_many :custom_values, :dependent => :delete_all
20 acts_as_list :scope => 'type = \'#{self.class}\''
20 acts_as_list :scope => 'type = \'#{self.class}\''
21 serialize :possible_values
21 serialize :possible_values
22
22
23 FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
23 FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
24 "text" => { :name => :label_text, :order => 2 },
24 "text" => { :name => :label_text, :order => 2 },
25 "int" => { :name => :label_integer, :order => 3 },
25 "int" => { :name => :label_integer, :order => 3 },
26 "float" => { :name => :label_float, :order => 4 },
26 "float" => { :name => :label_float, :order => 4 },
27 "list" => { :name => :label_list, :order => 5 },
27 "list" => { :name => :label_list, :order => 5 },
28 "date" => { :name => :label_date, :order => 6 },
28 "date" => { :name => :label_date, :order => 6 },
29 "bool" => { :name => :label_boolean, :order => 7 }
29 "bool" => { :name => :label_boolean, :order => 7 }
30 }.freeze
30 }.freeze
31
31
32 validates_presence_of :name, :field_format
32 validates_presence_of :name, :field_format
33 validates_uniqueness_of :name, :scope => :type
33 validates_uniqueness_of :name, :scope => :type
34 validates_length_of :name, :maximum => 30
34 validates_length_of :name, :maximum => 30
35 validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
35 validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
36 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
36 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
37
37
38 def initialize(attributes = nil)
38 def initialize(attributes = nil)
39 super
39 super
40 self.possible_values ||= []
40 self.possible_values ||= []
41 end
41 end
42
42
43 def before_validation
43 def before_validation
44 # make sure these fields are not searchable
44 # make sure these fields are not searchable
45 self.searchable = false if %w(int float date bool).include?(field_format)
45 self.searchable = false if %w(int float date bool).include?(field_format)
46 true
46 true
47 end
47 end
48
48
49 def validate
49 def validate
50 if self.field_format == "list"
50 if self.field_format == "list"
51 errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
51 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
52 errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
52 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
53 end
53 end
54
54
55 # validate default value
55 # validate default value
56 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
56 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
57 v.custom_field.is_required = false
57 v.custom_field.is_required = false
58 errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
58 errors.add(:default_value, :invalid) unless v.valid?
59 end
59 end
60
60
61 # Makes possible_values accept a multiline string
61 # Makes possible_values accept a multiline string
62 def possible_values=(arg)
62 def possible_values=(arg)
63 if arg.is_a?(Array)
63 if arg.is_a?(Array)
64 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
64 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
65 else
65 else
66 self.possible_values = arg.to_s.split(/[\n\r]+/)
66 self.possible_values = arg.to_s.split(/[\n\r]+/)
67 end
67 end
68 end
68 end
69
69
70 # Returns a ORDER BY clause that can used to sort customized
70 # Returns a ORDER BY clause that can used to sort customized
71 # objects by their value of the custom field.
71 # objects by their value of the custom field.
72 # Returns false, if the custom field can not be used for sorting.
72 # Returns false, if the custom field can not be used for sorting.
73 def order_statement
73 def order_statement
74 case field_format
74 case field_format
75 when 'string', 'text', 'list', 'date', 'bool'
75 when 'string', 'text', 'list', 'date', 'bool'
76 # COALESCE is here to make sure that blank and NULL values are sorted equally
76 # COALESCE is here to make sure that blank and NULL values are sorted equally
77 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
77 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
78 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
78 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
79 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
79 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
80 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
80 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
81 when 'int', 'float'
81 when 'int', 'float'
82 # Make the database cast values into numeric
82 # Make the database cast values into numeric
83 # Postgresql will raise an error if a value can not be casted!
83 # Postgresql will raise an error if a value can not be casted!
84 # CustomValue validations should ensure that it doesn't occur
84 # CustomValue validations should ensure that it doesn't occur
85 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
85 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
86 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
86 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
87 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
87 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
88 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
88 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
89 else
89 else
90 nil
90 nil
91 end
91 end
92 end
92 end
93
93
94 def <=>(field)
94 def <=>(field)
95 position <=> field.position
95 position <=> field.position
96 end
96 end
97
97
98 def self.customized_class
98 def self.customized_class
99 self.name =~ /^(.+)CustomField$/
99 self.name =~ /^(.+)CustomField$/
100 begin; $1.constantize; rescue nil; end
100 begin; $1.constantize; rescue nil; end
101 end
101 end
102
102
103 # to move in project_custom_field
103 # to move in project_custom_field
104 def self.for_all
104 def self.for_all
105 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
105 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
106 end
106 end
107
107
108 def type_name
108 def type_name
109 nil
109 nil
110 end
110 end
111 end
111 end
@@ -1,67 +1,67
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 CustomValue < ActiveRecord::Base
18 class CustomValue < ActiveRecord::Base
19 belongs_to :custom_field
19 belongs_to :custom_field
20 belongs_to :customized, :polymorphic => true
20 belongs_to :customized, :polymorphic => true
21
21
22 def after_initialize
22 def after_initialize
23 if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
23 if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
24 self.value ||= custom_field.default_value
24 self.value ||= custom_field.default_value
25 end
25 end
26 end
26 end
27
27
28 # Returns true if the boolean custom value is true
28 # Returns true if the boolean custom value is true
29 def true?
29 def true?
30 self.value == '1'
30 self.value == '1'
31 end
31 end
32
32
33 def editable?
33 def editable?
34 custom_field.editable?
34 custom_field.editable?
35 end
35 end
36
36
37 def required?
37 def required?
38 custom_field.is_required?
38 custom_field.is_required?
39 end
39 end
40
40
41 def to_s
41 def to_s
42 value.to_s
42 value.to_s
43 end
43 end
44
44
45 protected
45 protected
46 def validate
46 def validate
47 if value.blank?
47 if value.blank?
48 errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank?
48 errors.add(:value, :blank) if custom_field.is_required? and value.blank?
49 else
49 else
50 errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
50 errors.add(:value, :invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
51 errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
51 errors.add(:value, :too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
52 errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
52 errors.add(:value, :too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
53
53
54 # Format specific validations
54 # Format specific validations
55 case custom_field.field_format
55 case custom_field.field_format
56 when 'int'
56 when 'int'
57 errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/
57 errors.add(:value, :not_a_number) unless value =~ /^[+-]?\d+$/
58 when 'float'
58 when 'float'
59 begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
59 begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end
60 when 'date'
60 when 'date'
61 errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
61 errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
62 when 'list'
62 when 'list'
63 errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value)
63 errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value)
64 end
64 end
65 end
65 end
66 end
66 end
67 end
67 end
@@ -1,292 +1,292
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 :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
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 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 validates_length_of :subject, :maximum => 255
50 validates_length_of :subject, :maximum => 255
51 validates_inclusion_of :done_ratio, :in => 0..100
51 validates_inclusion_of :done_ratio, :in => 0..100
52 validates_numericality_of :estimated_hours, :allow_nil => true
52 validates_numericality_of :estimated_hours, :allow_nil => true
53
53
54 named_scope :visible, lambda {|*args| { :include => :project,
54 named_scope :visible, lambda {|*args| { :include => :project,
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56
56
57 # Returns true if usr or current user is allowed to view the issue
57 # Returns true if usr or current user is allowed to view the issue
58 def visible?(usr=nil)
58 def visible?(usr=nil)
59 (usr || User.current).allowed_to?(:view_issues, self.project)
59 (usr || User.current).allowed_to?(:view_issues, self.project)
60 end
60 end
61
61
62 def after_initialize
62 def after_initialize
63 if new_record?
63 if new_record?
64 # set default values for new records only
64 # set default values for new records only
65 self.status ||= IssueStatus.default
65 self.status ||= IssueStatus.default
66 self.priority ||= Enumeration.priorities.default
66 self.priority ||= Enumeration.priorities.default
67 end
67 end
68 end
68 end
69
69
70 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
70 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
71 def available_custom_fields
71 def available_custom_fields
72 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
72 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
73 end
73 end
74
74
75 def copy_from(arg)
75 def copy_from(arg)
76 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
76 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
77 self.attributes = issue.attributes.dup
77 self.attributes = issue.attributes.dup
78 self.custom_values = issue.custom_values.collect {|v| v.clone}
78 self.custom_values = issue.custom_values.collect {|v| v.clone}
79 self
79 self
80 end
80 end
81
81
82 # Moves/copies an issue to a new project and tracker
82 # Moves/copies an issue to a new project and tracker
83 # Returns the moved/copied issue on success, false on failure
83 # Returns the moved/copied issue on success, false on failure
84 def move_to(new_project, new_tracker = nil, options = {})
84 def move_to(new_project, new_tracker = nil, options = {})
85 options ||= {}
85 options ||= {}
86 issue = options[:copy] ? self.clone : self
86 issue = options[:copy] ? self.clone : self
87 transaction do
87 transaction do
88 if new_project && issue.project_id != new_project.id
88 if new_project && issue.project_id != new_project.id
89 # delete issue relations
89 # delete issue relations
90 unless Setting.cross_project_issue_relations?
90 unless Setting.cross_project_issue_relations?
91 issue.relations_from.clear
91 issue.relations_from.clear
92 issue.relations_to.clear
92 issue.relations_to.clear
93 end
93 end
94 # issue is moved to another project
94 # issue is moved to another project
95 # reassign to the category with same name if any
95 # reassign to the category with same name if any
96 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
96 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
97 issue.category = new_category
97 issue.category = new_category
98 issue.fixed_version = nil
98 issue.fixed_version = nil
99 issue.project = new_project
99 issue.project = new_project
100 end
100 end
101 if new_tracker
101 if new_tracker
102 issue.tracker = new_tracker
102 issue.tracker = new_tracker
103 end
103 end
104 if options[:copy]
104 if options[:copy]
105 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
105 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
106 issue.status = self.status
106 issue.status = self.status
107 end
107 end
108 if issue.save
108 if issue.save
109 unless options[:copy]
109 unless options[:copy]
110 # Manually update project_id on related time entries
110 # Manually update project_id on related time entries
111 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
111 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
112 end
112 end
113 else
113 else
114 Issue.connection.rollback_db_transaction
114 Issue.connection.rollback_db_transaction
115 return false
115 return false
116 end
116 end
117 end
117 end
118 return issue
118 return issue
119 end
119 end
120
120
121 def priority_id=(pid)
121 def priority_id=(pid)
122 self.priority = nil
122 self.priority = nil
123 write_attribute(:priority_id, pid)
123 write_attribute(:priority_id, pid)
124 end
124 end
125
125
126 def estimated_hours=(h)
126 def estimated_hours=(h)
127 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
127 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
128 end
128 end
129
129
130 def validate
130 def validate
131 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
131 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
132 errors.add :due_date, :activerecord_error_not_a_date
132 errors.add :due_date, :not_a_date
133 end
133 end
134
134
135 if self.due_date and self.start_date and self.due_date < self.start_date
135 if self.due_date and self.start_date and self.due_date < self.start_date
136 errors.add :due_date, :activerecord_error_greater_than_start_date
136 errors.add :due_date, :greater_than_start_date
137 end
137 end
138
138
139 if start_date && soonest_start && start_date < soonest_start
139 if start_date && soonest_start && start_date < soonest_start
140 errors.add :start_date, :activerecord_error_invalid
140 errors.add :start_date, :invalid
141 end
141 end
142 end
142 end
143
143
144 def validate_on_create
144 def validate_on_create
145 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
145 errors.add :tracker_id, :invalid unless project.trackers.include?(tracker)
146 end
146 end
147
147
148 def before_create
148 def before_create
149 # default assignment based on category
149 # default assignment based on category
150 if assigned_to.nil? && category && category.assigned_to
150 if assigned_to.nil? && category && category.assigned_to
151 self.assigned_to = category.assigned_to
151 self.assigned_to = category.assigned_to
152 end
152 end
153 end
153 end
154
154
155 def before_save
155 def before_save
156 if @current_journal
156 if @current_journal
157 # attributes changes
157 # attributes changes
158 (Issue.column_names - %w(id description)).each {|c|
158 (Issue.column_names - %w(id description)).each {|c|
159 @current_journal.details << JournalDetail.new(:property => 'attr',
159 @current_journal.details << JournalDetail.new(:property => 'attr',
160 :prop_key => c,
160 :prop_key => c,
161 :old_value => @issue_before_change.send(c),
161 :old_value => @issue_before_change.send(c),
162 :value => send(c)) unless send(c)==@issue_before_change.send(c)
162 :value => send(c)) unless send(c)==@issue_before_change.send(c)
163 }
163 }
164 # custom fields changes
164 # custom fields changes
165 custom_values.each {|c|
165 custom_values.each {|c|
166 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
166 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
167 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
167 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
168 @current_journal.details << JournalDetail.new(:property => 'cf',
168 @current_journal.details << JournalDetail.new(:property => 'cf',
169 :prop_key => c.custom_field_id,
169 :prop_key => c.custom_field_id,
170 :old_value => @custom_values_before_change[c.custom_field_id],
170 :old_value => @custom_values_before_change[c.custom_field_id],
171 :value => c.value)
171 :value => c.value)
172 }
172 }
173 @current_journal.save
173 @current_journal.save
174 end
174 end
175 # Save the issue even if the journal is not saved (because empty)
175 # Save the issue even if the journal is not saved (because empty)
176 true
176 true
177 end
177 end
178
178
179 def after_save
179 def after_save
180 # Reload is needed in order to get the right status
180 # Reload is needed in order to get the right status
181 reload
181 reload
182
182
183 # Update start/due dates of following issues
183 # Update start/due dates of following issues
184 relations_from.each(&:set_issue_to_dates)
184 relations_from.each(&:set_issue_to_dates)
185
185
186 # Close duplicates if the issue was closed
186 # Close duplicates if the issue was closed
187 if @issue_before_change && !@issue_before_change.closed? && self.closed?
187 if @issue_before_change && !@issue_before_change.closed? && self.closed?
188 duplicates.each do |duplicate|
188 duplicates.each do |duplicate|
189 # Reload is need in case the duplicate was updated by a previous duplicate
189 # Reload is need in case the duplicate was updated by a previous duplicate
190 duplicate.reload
190 duplicate.reload
191 # Don't re-close it if it's already closed
191 # Don't re-close it if it's already closed
192 next if duplicate.closed?
192 next if duplicate.closed?
193 # Same user and notes
193 # Same user and notes
194 duplicate.init_journal(@current_journal.user, @current_journal.notes)
194 duplicate.init_journal(@current_journal.user, @current_journal.notes)
195 duplicate.update_attribute :status, self.status
195 duplicate.update_attribute :status, self.status
196 end
196 end
197 end
197 end
198 end
198 end
199
199
200 def init_journal(user, notes = "")
200 def init_journal(user, notes = "")
201 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
201 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
202 @issue_before_change = self.clone
202 @issue_before_change = self.clone
203 @issue_before_change.status = self.status
203 @issue_before_change.status = self.status
204 @custom_values_before_change = {}
204 @custom_values_before_change = {}
205 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
205 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
206 # Make sure updated_on is updated when adding a note.
206 # Make sure updated_on is updated when adding a note.
207 updated_on_will_change!
207 updated_on_will_change!
208 @current_journal
208 @current_journal
209 end
209 end
210
210
211 # Return true if the issue is closed, otherwise false
211 # Return true if the issue is closed, otherwise false
212 def closed?
212 def closed?
213 self.status.is_closed?
213 self.status.is_closed?
214 end
214 end
215
215
216 # Returns true if the issue is overdue
216 # Returns true if the issue is overdue
217 def overdue?
217 def overdue?
218 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
218 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
219 end
219 end
220
220
221 # Users the issue can be assigned to
221 # Users the issue can be assigned to
222 def assignable_users
222 def assignable_users
223 project.assignable_users
223 project.assignable_users
224 end
224 end
225
225
226 # Returns an array of status that user is able to apply
226 # Returns an array of status that user is able to apply
227 def new_statuses_allowed_to(user)
227 def new_statuses_allowed_to(user)
228 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
228 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
229 statuses << status unless statuses.empty?
229 statuses << status unless statuses.empty?
230 statuses.uniq.sort
230 statuses.uniq.sort
231 end
231 end
232
232
233 # Returns the mail adresses of users that should be notified for the issue
233 # Returns the mail adresses of users that should be notified for the issue
234 def recipients
234 def recipients
235 recipients = project.recipients
235 recipients = project.recipients
236 # Author and assignee are always notified unless they have been locked
236 # Author and assignee are always notified unless they have been locked
237 recipients << author.mail if author && author.active?
237 recipients << author.mail if author && author.active?
238 recipients << assigned_to.mail if assigned_to && assigned_to.active?
238 recipients << assigned_to.mail if assigned_to && assigned_to.active?
239 recipients.compact.uniq
239 recipients.compact.uniq
240 end
240 end
241
241
242 def spent_hours
242 def spent_hours
243 @spent_hours ||= time_entries.sum(:hours) || 0
243 @spent_hours ||= time_entries.sum(:hours) || 0
244 end
244 end
245
245
246 def relations
246 def relations
247 (relations_from + relations_to).sort
247 (relations_from + relations_to).sort
248 end
248 end
249
249
250 def all_dependent_issues
250 def all_dependent_issues
251 dependencies = []
251 dependencies = []
252 relations_from.each do |relation|
252 relations_from.each do |relation|
253 dependencies << relation.issue_to
253 dependencies << relation.issue_to
254 dependencies += relation.issue_to.all_dependent_issues
254 dependencies += relation.issue_to.all_dependent_issues
255 end
255 end
256 dependencies
256 dependencies
257 end
257 end
258
258
259 # Returns an array of issues that duplicate this one
259 # Returns an array of issues that duplicate this one
260 def duplicates
260 def duplicates
261 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
261 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
262 end
262 end
263
263
264 # Returns the due date or the target due date if any
264 # Returns the due date or the target due date if any
265 # Used on gantt chart
265 # Used on gantt chart
266 def due_before
266 def due_before
267 due_date || (fixed_version ? fixed_version.effective_date : nil)
267 due_date || (fixed_version ? fixed_version.effective_date : nil)
268 end
268 end
269
269
270 def duration
270 def duration
271 (start_date && due_date) ? due_date - start_date : 0
271 (start_date && due_date) ? due_date - start_date : 0
272 end
272 end
273
273
274 def soonest_start
274 def soonest_start
275 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
275 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
276 end
276 end
277
277
278 def to_s
278 def to_s
279 "#{tracker} ##{id}: #{subject}"
279 "#{tracker} ##{id}: #{subject}"
280 end
280 end
281
281
282 private
282 private
283
283
284 # Callback on attachment deletion
284 # Callback on attachment deletion
285 def attachment_removed(obj)
285 def attachment_removed(obj)
286 journal = init_journal(User.current)
286 journal = init_journal(User.current)
287 journal.details << JournalDetail.new(:property => 'attachment',
287 journal.details << JournalDetail.new(:property => 'attachment',
288 :prop_key => obj.id,
288 :prop_key => obj.id,
289 :old_value => obj.filename)
289 :old_value => obj.filename)
290 journal.save
290 journal.save
291 end
291 end
292 end
292 end
@@ -1,81 +1,81
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 IssueRelation < ActiveRecord::Base
18 class IssueRelation < ActiveRecord::Base
19 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
19 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
20 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
20 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
21
21
22 TYPE_RELATES = "relates"
22 TYPE_RELATES = "relates"
23 TYPE_DUPLICATES = "duplicates"
23 TYPE_DUPLICATES = "duplicates"
24 TYPE_BLOCKS = "blocks"
24 TYPE_BLOCKS = "blocks"
25 TYPE_PRECEDES = "precedes"
25 TYPE_PRECEDES = "precedes"
26
26
27 TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
27 TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
28 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
28 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
29 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
29 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
30 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
30 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
31 }.freeze
31 }.freeze
32
32
33 validates_presence_of :issue_from, :issue_to, :relation_type
33 validates_presence_of :issue_from, :issue_to, :relation_type
34 validates_inclusion_of :relation_type, :in => TYPES.keys
34 validates_inclusion_of :relation_type, :in => TYPES.keys
35 validates_numericality_of :delay, :allow_nil => true
35 validates_numericality_of :delay, :allow_nil => true
36 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
36 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
37
37
38 attr_protected :issue_from_id, :issue_to_id
38 attr_protected :issue_from_id, :issue_to_id
39
39
40 def validate
40 def validate
41 if issue_from && issue_to
41 if issue_from && issue_to
42 errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
42 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
43 errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
43 errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
44 errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
44 errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
45 end
45 end
46 end
46 end
47
47
48 def other_issue(issue)
48 def other_issue(issue)
49 (self.issue_from_id == issue.id) ? issue_to : issue_from
49 (self.issue_from_id == issue.id) ? issue_to : issue_from
50 end
50 end
51
51
52 def label_for(issue)
52 def label_for(issue)
53 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
53 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
54 end
54 end
55
55
56 def before_save
56 def before_save
57 if TYPE_PRECEDES == relation_type
57 if TYPE_PRECEDES == relation_type
58 self.delay ||= 0
58 self.delay ||= 0
59 else
59 else
60 self.delay = nil
60 self.delay = nil
61 end
61 end
62 set_issue_to_dates
62 set_issue_to_dates
63 end
63 end
64
64
65 def set_issue_to_dates
65 def set_issue_to_dates
66 soonest_start = self.successor_soonest_start
66 soonest_start = self.successor_soonest_start
67 if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
67 if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
68 issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
68 issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
69 issue_to.save
69 issue_to.save
70 end
70 end
71 end
71 end
72
72
73 def successor_soonest_start
73 def successor_soonest_start
74 return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
74 return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
75 (issue_from.due_date || issue_from.start_date) + 1 + delay
75 (issue_from.due_date || issue_from.start_date) + 1 + delay
76 end
76 end
77
77
78 def <=>(relation)
78 def <=>(relation)
79 TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
79 TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
80 end
80 end
81 end
81 end
@@ -1,239 +1,244
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 MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20
20
21 class UnauthorizedAction < StandardError; end
21 class UnauthorizedAction < StandardError; end
22 class MissingInformation < StandardError; end
22 class MissingInformation < StandardError; end
23
23
24 attr_reader :email, :user
24 attr_reader :email, :user
25
25
26 def self.receive(email, options={})
26 def self.receive(email, options={})
27 @@handler_options = options.dup
27 @@handler_options = options.dup
28
28
29 @@handler_options[:issue] ||= {}
29 @@handler_options[:issue] ||= {}
30
30
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] ||= []
32 @@handler_options[:allow_override] ||= []
33 # Project needs to be overridable if not specified
33 # Project needs to be overridable if not specified
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 # Status overridable by default
35 # Status overridable by default
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 super email
37 super email
38 end
38 end
39
39
40 # Processes incoming emails
40 # Processes incoming emails
41 def receive(email)
41 def receive(email)
42 @email = email
42 @email = email
43 @user = User.active.find_by_mail(email.from.first.to_s.strip)
43 @user = User.active.find_by_mail(email.from.first.to_s.strip)
44 unless @user
44 unless @user
45 # Unknown user => the email is ignored
45 # Unknown user => the email is ignored
46 # TODO: ability to create the user's account
46 # TODO: ability to create the user's account
47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
48 return false
48 return false
49 end
49 end
50 User.current = @user
50 User.current = @user
51 dispatch
51 dispatch
52 end
52 end
53
53
54 private
54 private
55
55
56 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
56 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
57 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
57 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
58 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
58 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
59
59
60 def dispatch
60 def dispatch
61 headers = [email.in_reply_to, email.references].flatten.compact
61 headers = [email.in_reply_to, email.references].flatten.compact
62 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
62 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
63 klass, object_id = $1, $2.to_i
63 klass, object_id = $1, $2.to_i
64 method_name = "receive_#{klass}_reply"
64 method_name = "receive_#{klass}_reply"
65 if self.class.private_instance_methods.include?(method_name)
65 if self.class.private_instance_methods.include?(method_name)
66 send method_name, object_id
66 send method_name, object_id
67 else
67 else
68 # ignoring it
68 # ignoring it
69 end
69 end
70 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
70 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
71 receive_issue_reply(m[1].to_i)
71 receive_issue_reply(m[1].to_i)
72 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
72 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
73 receive_message_reply(m[1].to_i)
73 receive_message_reply(m[1].to_i)
74 else
74 else
75 receive_issue
75 receive_issue
76 end
76 end
77 rescue ActiveRecord::RecordInvalid => e
77 rescue ActiveRecord::RecordInvalid => e
78 # TODO: send a email to the user
78 # TODO: send a email to the user
79 logger.error e.message if logger
79 logger.error e.message if logger
80 false
80 false
81 rescue MissingInformation => e
81 rescue MissingInformation => e
82 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
82 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
83 false
83 false
84 rescue UnauthorizedAction => e
84 rescue UnauthorizedAction => e
85 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
85 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
86 false
86 false
87 end
87 end
88
88
89 # Creates a new issue
89 # Creates a new issue
90 def receive_issue
90 def receive_issue
91 project = target_project
91 project = target_project
92 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
92 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
93 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
93 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
94 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
94 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
95 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
95 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
96
96
97 # check permission
97 # check permission
98 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
98 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
99 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
99 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
100 # check workflow
100 # check workflow
101 if status && issue.new_statuses_allowed_to(user).include?(status)
101 if status && issue.new_statuses_allowed_to(user).include?(status)
102 issue.status = status
102 issue.status = status
103 end
103 end
104 issue.subject = email.subject.chomp.toutf8
104 issue.subject = email.subject.chomp.toutf8
105 issue.description = plain_text_body
105 issue.description = plain_text_body
106 # custom fields
106 # custom fields
107 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
107 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
108 if value = get_keyword(c.name, :override => true)
108 if value = get_keyword(c.name, :override => true)
109 h[c.id] = value
109 h[c.id] = value
110 end
110 end
111 h
111 h
112 end
112 end
113 issue.save!
113 issue.save!
114 add_attachments(issue)
114 add_attachments(issue)
115 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
115 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
116 # add To and Cc as watchers
116 # add To and Cc as watchers
117 add_watchers(issue)
117 add_watchers(issue)
118 # send notification after adding watchers so that they can reply to Redmine
118 # send notification after adding watchers so that they can reply to Redmine
119 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
119 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
120 issue
120 issue
121 end
121 end
122
122
123 def target_project
123 def target_project
124 # TODO: other ways to specify project:
124 # TODO: other ways to specify project:
125 # * parse the email To field
125 # * parse the email To field
126 # * specific project (eg. Setting.mail_handler_target_project)
126 # * specific project (eg. Setting.mail_handler_target_project)
127 target = Project.find_by_identifier(get_keyword(:project))
127 target = Project.find_by_identifier(get_keyword(:project))
128 raise MissingInformation.new('Unable to determine target project') if target.nil?
128 raise MissingInformation.new('Unable to determine target project') if target.nil?
129 target
129 target
130 end
130 end
131
131
132 # Adds a note to an existing issue
132 # Adds a note to an existing issue
133 def receive_issue_reply(issue_id)
133 def receive_issue_reply(issue_id)
134 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
134 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
135
135
136 issue = Issue.find_by_id(issue_id)
136 issue = Issue.find_by_id(issue_id)
137 return unless issue
137 return unless issue
138 # check permission
138 # check permission
139 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
139 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
140 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
140 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
141
141
142 # add the note
142 # add the note
143 journal = issue.init_journal(user, plain_text_body)
143 journal = issue.init_journal(user, plain_text_body)
144 add_attachments(issue)
144 add_attachments(issue)
145 # check workflow
145 # check workflow
146 if status && issue.new_statuses_allowed_to(user).include?(status)
146 if status && issue.new_statuses_allowed_to(user).include?(status)
147 issue.status = status
147 issue.status = status
148 end
148 end
149 issue.save!
149 issue.save!
150 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
150 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
151 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
151 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
152 journal
152 journal
153 end
153 end
154
154
155 # Reply will be added to the issue
155 # Reply will be added to the issue
156 def receive_journal_reply(journal_id)
156 def receive_journal_reply(journal_id)
157 journal = Journal.find_by_id(journal_id)
157 journal = Journal.find_by_id(journal_id)
158 if journal && journal.journalized_type == 'Issue'
158 if journal && journal.journalized_type == 'Issue'
159 receive_issue_reply(journal.journalized_id)
159 receive_issue_reply(journal.journalized_id)
160 end
160 end
161 end
161 end
162
162
163 # Receives a reply to a forum message
163 # Receives a reply to a forum message
164 def receive_message_reply(message_id)
164 def receive_message_reply(message_id)
165 message = Message.find_by_id(message_id)
165 message = Message.find_by_id(message_id)
166 if message
166 if message
167 message = message.root
167 message = message.root
168 if user.allowed_to?(:add_messages, message.project) && !message.locked?
168 if user.allowed_to?(:add_messages, message.project) && !message.locked?
169 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
169 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
170 :content => plain_text_body)
170 :content => plain_text_body)
171 reply.author = user
171 reply.author = user
172 reply.board = message.board
172 reply.board = message.board
173 message.children << reply
173 message.children << reply
174 add_attachments(reply)
174 add_attachments(reply)
175 reply
175 reply
176 else
176 else
177 raise UnauthorizedAction
177 raise UnauthorizedAction
178 end
178 end
179 end
179 end
180 end
180 end
181
181
182 def add_attachments(obj)
182 def add_attachments(obj)
183 if email.has_attachments?
183 if email.has_attachments?
184 email.attachments.each do |attachment|
184 email.attachments.each do |attachment|
185 Attachment.create(:container => obj,
185 Attachment.create(:container => obj,
186 :file => attachment,
186 :file => attachment,
187 :author => user,
187 :author => user,
188 :content_type => attachment.content_type)
188 :content_type => attachment.content_type)
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 # Adds To and Cc as watchers of the given object if the sender has the
193 # Adds To and Cc as watchers of the given object if the sender has the
194 # appropriate permission
194 # appropriate permission
195 def add_watchers(obj)
195 def add_watchers(obj)
196 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
196 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
197 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
197 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
198 unless addresses.empty?
198 unless addresses.empty?
199 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
199 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
200 watchers.each {|w| obj.add_watcher(w)}
200 watchers.each {|w| obj.add_watcher(w)}
201 end
201 end
202 end
202 end
203 end
203 end
204
204
205 def get_keyword(attr, options={})
205 def get_keyword(attr, options={})
206 @keywords ||= {}
206 @keywords ||= {}
207 if @keywords.has_key?(attr)
207 if @keywords.has_key?(attr)
208 @keywords[attr]
208 @keywords[attr]
209 else
209 else
210 @keywords[attr] = begin
210 @keywords[attr] = begin
211 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}:[ \t]*(.+)\s*$/i, '')
211 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}:[ \t]*(.+)\s*$/i, '')
212 $1.strip
212 $1.strip
213 elsif !@@handler_options[:issue][attr].blank?
213 elsif !@@handler_options[:issue][attr].blank?
214 @@handler_options[:issue][attr]
214 @@handler_options[:issue][attr]
215 end
215 end
216 end
216 end
217 end
217 end
218 end
218 end
219
219
220 # Returns the text/plain part of the email
220 # Returns the text/plain part of the email
221 # If not found (eg. HTML-only email), returns the body with tags removed
221 # If not found (eg. HTML-only email), returns the body with tags removed
222 def plain_text_body
222 def plain_text_body
223 return @plain_text_body unless @plain_text_body.nil?
223 return @plain_text_body unless @plain_text_body.nil?
224 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
224 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
225 if parts.empty?
225 if parts.empty?
226 parts << @email
226 parts << @email
227 end
227 end
228 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
228 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
229 if plain_text_part.nil?
229 if plain_text_part.nil?
230 # no text/plain part found, assuming html-only email
230 # no text/plain part found, assuming html-only email
231 # strip html tags and remove doctype directive
231 # strip html tags and remove doctype directive
232 @plain_text_body = strip_tags(@email.body.to_s)
232 @plain_text_body = strip_tags(@email.body.to_s)
233 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
233 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
234 else
234 else
235 @plain_text_body = plain_text_part.body.to_s
235 @plain_text_body = plain_text_part.body.to_s
236 end
236 end
237 @plain_text_body.strip!
237 @plain_text_body.strip!
238 end
238 end
239
240
241 def self.full_sanitizer
242 @full_sanitizer ||= HTML::FullSanitizer.new
243 end
239 end
244 end
@@ -1,306 +1,307
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 Mailer < ActionMailer::Base
18 class Mailer < ActionMailer::Base
19 helper :application
19 helper :application
20 helper :issues
20 helper :issues
21 helper :custom_fields
21 helper :custom_fields
22
22
23 include ActionController::UrlWriter
23 include ActionController::UrlWriter
24 include Redmine::I18n
24
25
25 def issue_add(issue)
26 def issue_add(issue)
26 redmine_headers 'Project' => issue.project.identifier,
27 redmine_headers 'Project' => issue.project.identifier,
27 'Issue-Id' => issue.id,
28 'Issue-Id' => issue.id,
28 'Issue-Author' => issue.author.login
29 'Issue-Author' => issue.author.login
29 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
30 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
30 message_id issue
31 message_id issue
31 recipients issue.recipients
32 recipients issue.recipients
32 cc(issue.watcher_recipients - @recipients)
33 cc(issue.watcher_recipients - @recipients)
33 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
34 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
34 body :issue => issue,
35 body :issue => issue,
35 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
36 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
36 end
37 end
37
38
38 def issue_edit(journal)
39 def issue_edit(journal)
39 issue = journal.journalized
40 issue = journal.journalized
40 redmine_headers 'Project' => issue.project.identifier,
41 redmine_headers 'Project' => issue.project.identifier,
41 'Issue-Id' => issue.id,
42 'Issue-Id' => issue.id,
42 'Issue-Author' => issue.author.login
43 'Issue-Author' => issue.author.login
43 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
44 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
44 message_id journal
45 message_id journal
45 references issue
46 references issue
46 @author = journal.user
47 @author = journal.user
47 recipients issue.recipients
48 recipients issue.recipients
48 # Watchers in cc
49 # Watchers in cc
49 cc(issue.watcher_recipients - @recipients)
50 cc(issue.watcher_recipients - @recipients)
50 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
51 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
51 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
52 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
52 s << issue.subject
53 s << issue.subject
53 subject s
54 subject s
54 body :issue => issue,
55 body :issue => issue,
55 :journal => journal,
56 :journal => journal,
56 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
57 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
57 end
58 end
58
59
59 def reminder(user, issues, days)
60 def reminder(user, issues, days)
60 set_language_if_valid user.language
61 set_language_if_valid user.language
61 recipients user.mail
62 recipients user.mail
62 subject l(:mail_subject_reminder, issues.size)
63 subject l(:mail_subject_reminder, issues.size)
63 body :issues => issues,
64 body :issues => issues,
64 :days => days,
65 :days => days,
65 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
66 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
66 end
67 end
67
68
68 def document_added(document)
69 def document_added(document)
69 redmine_headers 'Project' => document.project.identifier
70 redmine_headers 'Project' => document.project.identifier
70 recipients document.project.recipients
71 recipients document.project.recipients
71 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
72 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
72 body :document => document,
73 body :document => document,
73 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
74 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
74 end
75 end
75
76
76 def attachments_added(attachments)
77 def attachments_added(attachments)
77 container = attachments.first.container
78 container = attachments.first.container
78 added_to = ''
79 added_to = ''
79 added_to_url = ''
80 added_to_url = ''
80 case container.class.name
81 case container.class.name
81 when 'Project'
82 when 'Project'
82 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
83 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
83 added_to = "#{l(:label_project)}: #{container}"
84 added_to = "#{l(:label_project)}: #{container}"
84 when 'Version'
85 when 'Version'
85 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
86 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
86 added_to = "#{l(:label_version)}: #{container.name}"
87 added_to = "#{l(:label_version)}: #{container.name}"
87 when 'Document'
88 when 'Document'
88 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
89 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
89 added_to = "#{l(:label_document)}: #{container.title}"
90 added_to = "#{l(:label_document)}: #{container.title}"
90 end
91 end
91 redmine_headers 'Project' => container.project.identifier
92 redmine_headers 'Project' => container.project.identifier
92 recipients container.project.recipients
93 recipients container.project.recipients
93 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
94 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
94 body :attachments => attachments,
95 body :attachments => attachments,
95 :added_to => added_to,
96 :added_to => added_to,
96 :added_to_url => added_to_url
97 :added_to_url => added_to_url
97 end
98 end
98
99
99 def news_added(news)
100 def news_added(news)
100 redmine_headers 'Project' => news.project.identifier
101 redmine_headers 'Project' => news.project.identifier
101 message_id news
102 message_id news
102 recipients news.project.recipients
103 recipients news.project.recipients
103 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
104 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
104 body :news => news,
105 body :news => news,
105 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
106 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
106 end
107 end
107
108
108 def message_posted(message, recipients)
109 def message_posted(message, recipients)
109 redmine_headers 'Project' => message.project.identifier,
110 redmine_headers 'Project' => message.project.identifier,
110 'Topic-Id' => (message.parent_id || message.id)
111 'Topic-Id' => (message.parent_id || message.id)
111 message_id message
112 message_id message
112 references message.parent unless message.parent.nil?
113 references message.parent unless message.parent.nil?
113 recipients(recipients)
114 recipients(recipients)
114 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
115 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
115 body :message => message,
116 body :message => message,
116 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
117 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
117 end
118 end
118
119
119 def account_information(user, password)
120 def account_information(user, password)
120 set_language_if_valid user.language
121 set_language_if_valid user.language
121 recipients user.mail
122 recipients user.mail
122 subject l(:mail_subject_register, Setting.app_title)
123 subject l(:mail_subject_register, Setting.app_title)
123 body :user => user,
124 body :user => user,
124 :password => password,
125 :password => password,
125 :login_url => url_for(:controller => 'account', :action => 'login')
126 :login_url => url_for(:controller => 'account', :action => 'login')
126 end
127 end
127
128
128 def account_activation_request(user)
129 def account_activation_request(user)
129 # Send the email to all active administrators
130 # Send the email to all active administrators
130 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
131 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
131 subject l(:mail_subject_account_activation_request, Setting.app_title)
132 subject l(:mail_subject_account_activation_request, Setting.app_title)
132 body :user => user,
133 body :user => user,
133 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
134 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
134 end
135 end
135
136
136 # A registered user's account was activated by an administrator
137 # A registered user's account was activated by an administrator
137 def account_activated(user)
138 def account_activated(user)
138 set_language_if_valid user.language
139 set_language_if_valid user.language
139 recipients user.mail
140 recipients user.mail
140 subject l(:mail_subject_register, Setting.app_title)
141 subject l(:mail_subject_register, Setting.app_title)
141 body :user => user,
142 body :user => user,
142 :login_url => url_for(:controller => 'account', :action => 'login')
143 :login_url => url_for(:controller => 'account', :action => 'login')
143 end
144 end
144
145
145 def lost_password(token)
146 def lost_password(token)
146 set_language_if_valid(token.user.language)
147 set_language_if_valid(token.user.language)
147 recipients token.user.mail
148 recipients token.user.mail
148 subject l(:mail_subject_lost_password, Setting.app_title)
149 subject l(:mail_subject_lost_password, Setting.app_title)
149 body :token => token,
150 body :token => token,
150 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
151 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
151 end
152 end
152
153
153 def register(token)
154 def register(token)
154 set_language_if_valid(token.user.language)
155 set_language_if_valid(token.user.language)
155 recipients token.user.mail
156 recipients token.user.mail
156 subject l(:mail_subject_register, Setting.app_title)
157 subject l(:mail_subject_register, Setting.app_title)
157 body :token => token,
158 body :token => token,
158 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
159 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
159 end
160 end
160
161
161 def test(user)
162 def test(user)
162 set_language_if_valid(user.language)
163 set_language_if_valid(user.language)
163 recipients user.mail
164 recipients user.mail
164 subject 'Redmine test'
165 subject 'Redmine test'
165 body :url => url_for(:controller => 'welcome')
166 body :url => url_for(:controller => 'welcome')
166 end
167 end
167
168
168 # Overrides default deliver! method to prevent from sending an email
169 # Overrides default deliver! method to prevent from sending an email
169 # with no recipient, cc or bcc
170 # with no recipient, cc or bcc
170 def deliver!(mail = @mail)
171 def deliver!(mail = @mail)
171 return false if (recipients.nil? || recipients.empty?) &&
172 return false if (recipients.nil? || recipients.empty?) &&
172 (cc.nil? || cc.empty?) &&
173 (cc.nil? || cc.empty?) &&
173 (bcc.nil? || bcc.empty?)
174 (bcc.nil? || bcc.empty?)
174
175
175 # Set Message-Id and References
176 # Set Message-Id and References
176 if @message_id_object
177 if @message_id_object
177 mail.message_id = self.class.message_id_for(@message_id_object)
178 mail.message_id = self.class.message_id_for(@message_id_object)
178 end
179 end
179 if @references_objects
180 if @references_objects
180 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
181 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
181 end
182 end
182 super(mail)
183 super(mail)
183 end
184 end
184
185
185 # Sends reminders to issue assignees
186 # Sends reminders to issue assignees
186 # Available options:
187 # Available options:
187 # * :days => how many days in the future to remind about (defaults to 7)
188 # * :days => how many days in the future to remind about (defaults to 7)
188 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
189 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
189 # * :project => id or identifier of project to process (defaults to all projects)
190 # * :project => id or identifier of project to process (defaults to all projects)
190 def self.reminders(options={})
191 def self.reminders(options={})
191 days = options[:days] || 7
192 days = options[:days] || 7
192 project = options[:project] ? Project.find(options[:project]) : nil
193 project = options[:project] ? Project.find(options[:project]) : nil
193 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
194 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
194
195
195 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
196 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
196 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
197 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
197 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
198 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
198 s << "#{Issue.table_name}.project_id = #{project.id}" if project
199 s << "#{Issue.table_name}.project_id = #{project.id}" if project
199 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
200 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
200
201
201 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
202 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
202 :conditions => s.conditions
203 :conditions => s.conditions
203 ).group_by(&:assigned_to)
204 ).group_by(&:assigned_to)
204 issues_by_assignee.each do |assignee, issues|
205 issues_by_assignee.each do |assignee, issues|
205 deliver_reminder(assignee, issues, days) unless assignee.nil?
206 deliver_reminder(assignee, issues, days) unless assignee.nil?
206 end
207 end
207 end
208 end
208
209
209 private
210 private
210 def initialize_defaults(method_name)
211 def initialize_defaults(method_name)
211 super
212 super
212 set_language_if_valid Setting.default_language
213 set_language_if_valid Setting.default_language
213 from Setting.mail_from
214 from Setting.mail_from
214
215
215 # URL options
216 # URL options
216 h = Setting.host_name
217 h = Setting.host_name
217 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
218 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
218 default_url_options[:host] = h
219 default_url_options[:host] = h
219 default_url_options[:protocol] = Setting.protocol
220 default_url_options[:protocol] = Setting.protocol
220
221
221 # Common headers
222 # Common headers
222 headers 'X-Mailer' => 'Redmine',
223 headers 'X-Mailer' => 'Redmine',
223 'X-Redmine-Host' => Setting.host_name,
224 'X-Redmine-Host' => Setting.host_name,
224 'X-Redmine-Site' => Setting.app_title
225 'X-Redmine-Site' => Setting.app_title
225 end
226 end
226
227
227 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
228 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
228 def redmine_headers(h)
229 def redmine_headers(h)
229 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
230 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
230 end
231 end
231
232
232 # Overrides the create_mail method
233 # Overrides the create_mail method
233 def create_mail
234 def create_mail
234 # Removes the current user from the recipients and cc
235 # Removes the current user from the recipients and cc
235 # if he doesn't want to receive notifications about what he does
236 # if he doesn't want to receive notifications about what he does
236 @author ||= User.current
237 @author ||= User.current
237 if @author.pref[:no_self_notified]
238 if @author.pref[:no_self_notified]
238 recipients.delete(@author.mail) if recipients
239 recipients.delete(@author.mail) if recipients
239 cc.delete(@author.mail) if cc
240 cc.delete(@author.mail) if cc
240 end
241 end
241 # Blind carbon copy recipients
242 # Blind carbon copy recipients
242 if Setting.bcc_recipients?
243 if Setting.bcc_recipients?
243 bcc([recipients, cc].flatten.compact.uniq)
244 bcc([recipients, cc].flatten.compact.uniq)
244 recipients []
245 recipients []
245 cc []
246 cc []
246 end
247 end
247 super
248 super
248 end
249 end
249
250
250 # Renders a message with the corresponding layout
251 # Renders a message with the corresponding layout
251 def render_message(method_name, body)
252 def render_message(method_name, body)
252 layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
253 layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
253 body[:content_for_layout] = render(:file => method_name, :body => body)
254 body[:content_for_layout] = render(:file => method_name, :body => body)
254 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
255 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
255 end
256 end
256
257
257 # for the case of plain text only
258 # for the case of plain text only
258 def body(*params)
259 def body(*params)
259 value = super(*params)
260 value = super(*params)
260 if Setting.plain_text_mail?
261 if Setting.plain_text_mail?
261 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
262 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
262 unless String === @body or templates.empty?
263 unless String === @body or templates.empty?
263 template = File.basename(templates.first)
264 template = File.basename(templates.first)
264 @body[:content_for_layout] = render(:file => template, :body => @body)
265 @body[:content_for_layout] = render(:file => template, :body => @body)
265 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
266 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
266 return @body
267 return @body
267 end
268 end
268 end
269 end
269 return value
270 return value
270 end
271 end
271
272
272 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
273 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
273 def self.controller_path
274 def self.controller_path
274 ''
275 ''
275 end unless respond_to?('controller_path')
276 end unless respond_to?('controller_path')
276
277
277 # Returns a predictable Message-Id for the given object
278 # Returns a predictable Message-Id for the given object
278 def self.message_id_for(object)
279 def self.message_id_for(object)
279 # id + timestamp should reduce the odds of a collision
280 # id + timestamp should reduce the odds of a collision
280 # as far as we don't send multiple emails for the same object
281 # as far as we don't send multiple emails for the same object
281 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
282 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
282 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
283 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
283 host = "#{::Socket.gethostname}.redmine" if host.empty?
284 host = "#{::Socket.gethostname}.redmine" if host.empty?
284 "<#{hash}@#{host}>"
285 "<#{hash}@#{host}>"
285 end
286 end
286
287
287 private
288 private
288
289
289 def message_id(object)
290 def message_id(object)
290 @message_id_object = object
291 @message_id_object = object
291 end
292 end
292
293
293 def references(object)
294 def references(object)
294 @references_objects ||= []
295 @references_objects ||= []
295 @references_objects << object
296 @references_objects << object
296 end
297 end
297 end
298 end
298
299
299 # Patch TMail so that message_id is not overwritten
300 # Patch TMail so that message_id is not overwritten
300 module TMail
301 module TMail
301 class Mail
302 class Mail
302 def add_message_id( fqdn = nil )
303 def add_message_id( fqdn = nil )
303 self.message_id ||= ::TMail::new_message_id(fqdn)
304 self.message_id ||= ::TMail::new_message_id(fqdn)
304 end
305 end
305 end
306 end
306 end
307 end
@@ -1,42 +1,42
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 Member < ActiveRecord::Base
18 class Member < ActiveRecord::Base
19 belongs_to :user
19 belongs_to :user
20 belongs_to :role
20 belongs_to :role
21 belongs_to :project
21 belongs_to :project
22
22
23 validates_presence_of :role, :user, :project
23 validates_presence_of :role, :user, :project
24 validates_uniqueness_of :user_id, :scope => :project_id
24 validates_uniqueness_of :user_id, :scope => :project_id
25
25
26 def validate
26 def validate
27 errors.add :role_id, :activerecord_error_invalid if role && !role.member?
27 errors.add :role_id, :invalid if role && !role.member?
28 end
28 end
29
29
30 def name
30 def name
31 self.user.name
31 self.user.name
32 end
32 end
33
33
34 def <=>(member)
34 def <=>(member)
35 role == member.role ? (user <=> member.user) : (role <=> member.role)
35 role == member.role ? (user <=> member.user) : (role <=> member.role)
36 end
36 end
37
37
38 def before_destroy
38 def before_destroy
39 # remove category based auto assignments for this member
39 # remove category based auto assignments for this member
40 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
40 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
41 end
41 end
42 end
42 end
@@ -1,323 +1,323
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 Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :users, :through => :members
24 has_many :users, :through => :members
25 has_many :enabled_modules, :dependent => :delete_all
25 has_many :enabled_modules, :dependent => :delete_all
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 has_many :issue_changes, :through => :issues, :source => :journals
28 has_many :issue_changes, :through => :issues, :source => :journals
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
32 has_many :documents, :dependent => :destroy
32 has_many :documents, :dependent => :destroy
33 has_many :news, :dependent => :delete_all, :include => :author
33 has_many :news, :dependent => :delete_all, :include => :author
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 has_one :repository, :dependent => :destroy
36 has_one :repository, :dependent => :destroy
37 has_many :changesets, :through => :repository
37 has_many :changesets, :through => :repository
38 has_one :wiki, :dependent => :destroy
38 has_one :wiki, :dependent => :destroy
39 # Custom field for the project issues
39 # Custom field for the project issues
40 has_and_belongs_to_many :issue_custom_fields,
40 has_and_belongs_to_many :issue_custom_fields,
41 :class_name => 'IssueCustomField',
41 :class_name => 'IssueCustomField',
42 :order => "#{CustomField.table_name}.position",
42 :order => "#{CustomField.table_name}.position",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
45
45
46 acts_as_nested_set :order => 'name', :dependent => :destroy
46 acts_as_nested_set :order => 'name', :dependent => :destroy
47 acts_as_attachable :view_permission => :view_files,
47 acts_as_attachable :view_permission => :view_files,
48 :delete_permission => :manage_files
48 :delete_permission => :manage_files
49
49
50 acts_as_customizable
50 acts_as_customizable
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
54 :author => nil
54 :author => nil
55
55
56 attr_protected :status, :enabled_module_names
56 attr_protected :status, :enabled_module_names
57
57
58 validates_presence_of :name, :identifier
58 validates_presence_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
60 validates_associated :repository, :wiki
60 validates_associated :repository, :wiki
61 validates_length_of :name, :maximum => 30
61 validates_length_of :name, :maximum => 30
62 validates_length_of :homepage, :maximum => 255
62 validates_length_of :homepage, :maximum => 255
63 validates_length_of :identifier, :in => 2..20
63 validates_length_of :identifier, :in => 2..20
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
65
65
66 before_destroy :delete_all_members
66 before_destroy :delete_all_members
67
67
68 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
68 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
70 named_scope :public, { :conditions => { :is_public => true } }
70 named_scope :public, { :conditions => { :is_public => true } }
71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
72
72
73 def identifier=(identifier)
73 def identifier=(identifier)
74 super unless identifier_frozen?
74 super unless identifier_frozen?
75 end
75 end
76
76
77 def identifier_frozen?
77 def identifier_frozen?
78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
79 end
79 end
80
80
81 def issues_with_subprojects(include_subprojects=false)
81 def issues_with_subprojects(include_subprojects=false)
82 conditions = nil
82 conditions = nil
83 if include_subprojects
83 if include_subprojects
84 ids = [id] + descendants.collect(&:id)
84 ids = [id] + descendants.collect(&:id)
85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
86 end
86 end
87 conditions ||= ["#{Project.table_name}.id = ?", id]
87 conditions ||= ["#{Project.table_name}.id = ?", id]
88 # Quick and dirty fix for Rails 2 compatibility
88 # Quick and dirty fix for Rails 2 compatibility
89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
90 Version.send(:with_scope, :find => { :conditions => conditions }) do
90 Version.send(:with_scope, :find => { :conditions => conditions }) do
91 yield
91 yield
92 end
92 end
93 end
93 end
94 end
94 end
95
95
96 # returns latest created projects
96 # returns latest created projects
97 # non public projects will be returned only if user is a member of those
97 # non public projects will be returned only if user is a member of those
98 def self.latest(user=nil, count=5)
98 def self.latest(user=nil, count=5)
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
100 end
100 end
101
101
102 def self.visible_by(user=nil)
102 def self.visible_by(user=nil)
103 user ||= User.current
103 user ||= User.current
104 if user && user.admin?
104 if user && user.admin?
105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 elsif user && user.memberships.any?
106 elsif user && user.memberships.any?
107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
108 else
108 else
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 end
110 end
111 end
111 end
112
112
113 def self.allowed_to_condition(user, permission, options={})
113 def self.allowed_to_condition(user, permission, options={})
114 statements = []
114 statements = []
115 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
115 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 if perm = Redmine::AccessControl.permission(permission)
116 if perm = Redmine::AccessControl.permission(permission)
117 unless perm.project_module.nil?
117 unless perm.project_module.nil?
118 # If the permission belongs to a project module, make sure the module is enabled
118 # If the permission belongs to a project module, make sure the module is enabled
119 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
119 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
120 end
120 end
121 end
121 end
122 if options[:project]
122 if options[:project]
123 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
123 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
124 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 base_statement = "(#{project_statement}) AND (#{base_statement})"
125 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 end
126 end
127 if user.admin?
127 if user.admin?
128 # no restriction
128 # no restriction
129 else
129 else
130 statements << "1=0"
130 statements << "1=0"
131 if user.logged?
131 if user.logged?
132 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
132 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
133 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
133 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
134 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
134 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
135 elsif Role.anonymous.allowed_to?(permission)
135 elsif Role.anonymous.allowed_to?(permission)
136 # anonymous user allowed on public project
136 # anonymous user allowed on public project
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
138 else
138 else
139 # anonymous user is not authorized
139 # anonymous user is not authorized
140 end
140 end
141 end
141 end
142 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
142 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
143 end
143 end
144
144
145 def project_condition(with_subprojects)
145 def project_condition(with_subprojects)
146 cond = "#{Project.table_name}.id = #{id}"
146 cond = "#{Project.table_name}.id = #{id}"
147 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
147 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
148 cond
148 cond
149 end
149 end
150
150
151 def self.find(*args)
151 def self.find(*args)
152 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
152 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
153 project = find_by_identifier(*args)
153 project = find_by_identifier(*args)
154 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
154 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
155 project
155 project
156 else
156 else
157 super
157 super
158 end
158 end
159 end
159 end
160
160
161 def to_param
161 def to_param
162 # id is used for projects with a numeric identifier (compatibility)
162 # id is used for projects with a numeric identifier (compatibility)
163 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
163 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
164 end
164 end
165
165
166 def active?
166 def active?
167 self.status == STATUS_ACTIVE
167 self.status == STATUS_ACTIVE
168 end
168 end
169
169
170 # Archives the project and its descendants recursively
170 # Archives the project and its descendants recursively
171 def archive
171 def archive
172 # Archive subprojects if any
172 # Archive subprojects if any
173 children.each do |subproject|
173 children.each do |subproject|
174 subproject.archive
174 subproject.archive
175 end
175 end
176 update_attribute :status, STATUS_ARCHIVED
176 update_attribute :status, STATUS_ARCHIVED
177 end
177 end
178
178
179 # Unarchives the project
179 # Unarchives the project
180 # All its ancestors must be active
180 # All its ancestors must be active
181 def unarchive
181 def unarchive
182 return false if ancestors.detect {|a| !a.active?}
182 return false if ancestors.detect {|a| !a.active?}
183 update_attribute :status, STATUS_ACTIVE
183 update_attribute :status, STATUS_ACTIVE
184 end
184 end
185
185
186 # Returns an array of projects the project can be moved to
186 # Returns an array of projects the project can be moved to
187 def possible_parents
187 def possible_parents
188 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
188 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
189 end
189 end
190
190
191 # Sets the parent of the project
191 # Sets the parent of the project
192 # Argument can be either a Project, a String, a Fixnum or nil
192 # Argument can be either a Project, a String, a Fixnum or nil
193 def set_parent!(p)
193 def set_parent!(p)
194 unless p.nil? || p.is_a?(Project)
194 unless p.nil? || p.is_a?(Project)
195 if p.to_s.blank?
195 if p.to_s.blank?
196 p = nil
196 p = nil
197 else
197 else
198 p = Project.find_by_id(p)
198 p = Project.find_by_id(p)
199 return false unless p
199 return false unless p
200 end
200 end
201 end
201 end
202 if p == parent && !p.nil?
202 if p == parent && !p.nil?
203 # Nothing to do
203 # Nothing to do
204 true
204 true
205 elsif p.nil? || (p.active? && move_possible?(p))
205 elsif p.nil? || (p.active? && move_possible?(p))
206 # Insert the project so that target's children or root projects stay alphabetically sorted
206 # Insert the project so that target's children or root projects stay alphabetically sorted
207 sibs = (p.nil? ? self.class.roots : p.children)
207 sibs = (p.nil? ? self.class.roots : p.children)
208 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
208 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
209 if to_be_inserted_before
209 if to_be_inserted_before
210 move_to_left_of(to_be_inserted_before)
210 move_to_left_of(to_be_inserted_before)
211 elsif p.nil?
211 elsif p.nil?
212 if sibs.empty?
212 if sibs.empty?
213 # move_to_root adds the project in first (ie. left) position
213 # move_to_root adds the project in first (ie. left) position
214 move_to_root
214 move_to_root
215 else
215 else
216 move_to_right_of(sibs.last) unless self == sibs.last
216 move_to_right_of(sibs.last) unless self == sibs.last
217 end
217 end
218 else
218 else
219 # move_to_child_of adds the project in last (ie.right) position
219 # move_to_child_of adds the project in last (ie.right) position
220 move_to_child_of(p)
220 move_to_child_of(p)
221 end
221 end
222 true
222 true
223 else
223 else
224 # Can not move to the given target
224 # Can not move to the given target
225 false
225 false
226 end
226 end
227 end
227 end
228
228
229 # Returns an array of the trackers used by the project and its active sub projects
229 # Returns an array of the trackers used by the project and its active sub projects
230 def rolled_up_trackers
230 def rolled_up_trackers
231 @rolled_up_trackers ||=
231 @rolled_up_trackers ||=
232 Tracker.find(:all, :include => :projects,
232 Tracker.find(:all, :include => :projects,
233 :select => "DISTINCT #{Tracker.table_name}.*",
233 :select => "DISTINCT #{Tracker.table_name}.*",
234 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
234 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
235 :order => "#{Tracker.table_name}.position")
235 :order => "#{Tracker.table_name}.position")
236 end
236 end
237
237
238 # Deletes all project's members
238 # Deletes all project's members
239 def delete_all_members
239 def delete_all_members
240 Member.delete_all(['project_id = ?', id])
240 Member.delete_all(['project_id = ?', id])
241 end
241 end
242
242
243 # Users issues can be assigned to
243 # Users issues can be assigned to
244 def assignable_users
244 def assignable_users
245 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
245 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
246 end
246 end
247
247
248 # Returns the mail adresses of users that should be always notified on project events
248 # Returns the mail adresses of users that should be always notified on project events
249 def recipients
249 def recipients
250 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
250 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
251 end
251 end
252
252
253 # Returns an array of all custom fields enabled for project issues
253 # Returns an array of all custom fields enabled for project issues
254 # (explictly associated custom fields and custom fields enabled for all projects)
254 # (explictly associated custom fields and custom fields enabled for all projects)
255 def all_issue_custom_fields
255 def all_issue_custom_fields
256 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
256 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
257 end
257 end
258
258
259 def project
259 def project
260 self
260 self
261 end
261 end
262
262
263 def <=>(project)
263 def <=>(project)
264 name.downcase <=> project.name.downcase
264 name.downcase <=> project.name.downcase
265 end
265 end
266
266
267 def to_s
267 def to_s
268 name
268 name
269 end
269 end
270
270
271 # Returns a short description of the projects (first lines)
271 # Returns a short description of the projects (first lines)
272 def short_description(length = 255)
272 def short_description(length = 255)
273 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
273 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
274 end
274 end
275
275
276 def allows_to?(action)
276 def allows_to?(action)
277 if action.is_a? Hash
277 if action.is_a? Hash
278 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
278 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
279 else
279 else
280 allowed_permissions.include? action
280 allowed_permissions.include? action
281 end
281 end
282 end
282 end
283
283
284 def module_enabled?(module_name)
284 def module_enabled?(module_name)
285 module_name = module_name.to_s
285 module_name = module_name.to_s
286 enabled_modules.detect {|m| m.name == module_name}
286 enabled_modules.detect {|m| m.name == module_name}
287 end
287 end
288
288
289 def enabled_module_names=(module_names)
289 def enabled_module_names=(module_names)
290 if module_names && module_names.is_a?(Array)
290 if module_names && module_names.is_a?(Array)
291 module_names = module_names.collect(&:to_s)
291 module_names = module_names.collect(&:to_s)
292 # remove disabled modules
292 # remove disabled modules
293 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
293 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
294 # add new modules
294 # add new modules
295 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
295 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
296 else
296 else
297 enabled_modules.clear
297 enabled_modules.clear
298 end
298 end
299 end
299 end
300
300
301 # Returns an auto-generated project identifier based on the last identifier used
301 # Returns an auto-generated project identifier based on the last identifier used
302 def self.next_identifier
302 def self.next_identifier
303 p = Project.find(:first, :order => 'created_on DESC')
303 p = Project.find(:first, :order => 'created_on DESC')
304 p.nil? ? nil : p.identifier.to_s.succ
304 p.nil? ? nil : p.identifier.to_s.succ
305 end
305 end
306
306
307 protected
307 protected
308 def validate
308 def validate
309 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
309 errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
310 end
310 end
311
311
312 private
312 private
313 def allowed_permissions
313 def allowed_permissions
314 @allowed_permissions ||= begin
314 @allowed_permissions ||= begin
315 module_names = enabled_modules.collect {|m| m.name}
315 module_names = enabled_modules.collect {|m| m.name}
316 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
316 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
317 end
317 end
318 end
318 end
319
319
320 def allowed_actions
320 def allowed_actions
321 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
321 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
322 end
322 end
323 end
323 end
@@ -1,413 +1,411
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 QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :default_order
19 attr_accessor :name, :sortable, :default_order
20 include GLoc
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.default_order = options[:default_order]
25 self.default_order = options[:default_order]
26 end
26 end
27
27
28 def caption
28 def caption
29 set_language_if_valid(User.current.language)
30 l("field_#{name}")
29 l("field_#{name}")
31 end
30 end
32 end
31 end
33
32
34 class QueryCustomFieldColumn < QueryColumn
33 class QueryCustomFieldColumn < QueryColumn
35
34
36 def initialize(custom_field)
35 def initialize(custom_field)
37 self.name = "cf_#{custom_field.id}".to_sym
36 self.name = "cf_#{custom_field.id}".to_sym
38 self.sortable = custom_field.order_statement || false
37 self.sortable = custom_field.order_statement || false
39 @cf = custom_field
38 @cf = custom_field
40 end
39 end
41
40
42 def caption
41 def caption
43 @cf.name
42 @cf.name
44 end
43 end
45
44
46 def custom_field
45 def custom_field
47 @cf
46 @cf
48 end
47 end
49 end
48 end
50
49
51 class Query < ActiveRecord::Base
50 class Query < ActiveRecord::Base
52 belongs_to :project
51 belongs_to :project
53 belongs_to :user
52 belongs_to :user
54 serialize :filters
53 serialize :filters
55 serialize :column_names
54 serialize :column_names
56
55
57 attr_protected :project_id, :user_id
56 attr_protected :project_id, :user_id
58
57
59 validates_presence_of :name, :on => :save
58 validates_presence_of :name, :on => :save
60 validates_length_of :name, :maximum => 255
59 validates_length_of :name, :maximum => 255
61
60
62 @@operators = { "=" => :label_equals,
61 @@operators = { "=" => :label_equals,
63 "!" => :label_not_equals,
62 "!" => :label_not_equals,
64 "o" => :label_open_issues,
63 "o" => :label_open_issues,
65 "c" => :label_closed_issues,
64 "c" => :label_closed_issues,
66 "!*" => :label_none,
65 "!*" => :label_none,
67 "*" => :label_all,
66 "*" => :label_all,
68 ">=" => '>=',
67 ">=" => '>=',
69 "<=" => '<=',
68 "<=" => '<=',
70 "<t+" => :label_in_less_than,
69 "<t+" => :label_in_less_than,
71 ">t+" => :label_in_more_than,
70 ">t+" => :label_in_more_than,
72 "t+" => :label_in,
71 "t+" => :label_in,
73 "t" => :label_today,
72 "t" => :label_today,
74 "w" => :label_this_week,
73 "w" => :label_this_week,
75 ">t-" => :label_less_than_ago,
74 ">t-" => :label_less_than_ago,
76 "<t-" => :label_more_than_ago,
75 "<t-" => :label_more_than_ago,
77 "t-" => :label_ago,
76 "t-" => :label_ago,
78 "~" => :label_contains,
77 "~" => :label_contains,
79 "!~" => :label_not_contains }
78 "!~" => :label_not_contains }
80
79
81 cattr_reader :operators
80 cattr_reader :operators
82
81
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
82 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
83 :list_status => [ "o", "=", "!", "c", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
84 :list_optional => [ "=", "!", "!*", "*" ],
86 :list_subprojects => [ "*", "!*", "=" ],
85 :list_subprojects => [ "*", "!*", "=" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
86 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
87 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 :string => [ "=", "~", "!", "!~" ],
88 :string => [ "=", "~", "!", "!~" ],
90 :text => [ "~", "!~" ],
89 :text => [ "~", "!~" ],
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
90 :integer => [ "=", ">=", "<=", "!*", "*" ] }
92
91
93 cattr_reader :operators_by_filter_type
92 cattr_reader :operators_by_filter_type
94
93
95 @@available_columns = [
94 @@available_columns = [
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
95 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
96 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
97 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
98 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 QueryColumn.new(:author),
99 QueryColumn.new(:author),
101 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname"]),
100 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname"]),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
101 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
102 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'),
103 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
104 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
105 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
106 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
107 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
108 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 ]
109 ]
111 cattr_reader :available_columns
110 cattr_reader :available_columns
112
111
113 def initialize(attributes = nil)
112 def initialize(attributes = nil)
114 super attributes
113 super attributes
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
114 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 set_language_if_valid(User.current.language)
117 end
115 end
118
116
119 def after_initialize
117 def after_initialize
120 # Store the fact that project is nil (used in #editable_by?)
118 # Store the fact that project is nil (used in #editable_by?)
121 @is_for_all = project.nil?
119 @is_for_all = project.nil?
122 end
120 end
123
121
124 def validate
122 def validate
125 filters.each_key do |field|
123 filters.each_key do |field|
126 errors.add label_for(field), :activerecord_error_blank unless
124 errors.add label_for(field), :blank unless
127 # filter requires one or more values
125 # filter requires one or more values
128 (values_for(field) and !values_for(field).first.blank?) or
126 (values_for(field) and !values_for(field).first.blank?) or
129 # filter doesn't require any value
127 # filter doesn't require any value
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
128 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
131 end if filters
129 end if filters
132 end
130 end
133
131
134 def editable_by?(user)
132 def editable_by?(user)
135 return false unless user
133 return false unless user
136 # Admin can edit them all and regular users can edit their private queries
134 # Admin can edit them all and regular users can edit their private queries
137 return true if user.admin? || (!is_public && self.user_id == user.id)
135 return true if user.admin? || (!is_public && self.user_id == user.id)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
136 # Members can not edit public queries that are for all project (only admin is allowed to)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
137 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
140 end
138 end
141
139
142 def available_filters
140 def available_filters
143 return @available_filters if @available_filters
141 return @available_filters if @available_filters
144
142
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
143 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
146
144
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
145 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
146 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
147 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
150 "subject" => { :type => :text, :order => 8 },
148 "subject" => { :type => :text, :order => 8 },
151 "created_on" => { :type => :date_past, :order => 9 },
149 "created_on" => { :type => :date_past, :order => 9 },
152 "updated_on" => { :type => :date_past, :order => 10 },
150 "updated_on" => { :type => :date_past, :order => 10 },
153 "start_date" => { :type => :date, :order => 11 },
151 "start_date" => { :type => :date, :order => 11 },
154 "due_date" => { :type => :date, :order => 12 },
152 "due_date" => { :type => :date, :order => 12 },
155 "estimated_hours" => { :type => :integer, :order => 13 },
153 "estimated_hours" => { :type => :integer, :order => 13 },
156 "done_ratio" => { :type => :integer, :order => 14 }}
154 "done_ratio" => { :type => :integer, :order => 14 }}
157
155
158 user_values = []
156 user_values = []
159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
157 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
160 if project
158 if project
161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
159 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
162 else
160 else
163 # members of the user's projects
161 # members of the user's projects
164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
162 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
165 end
163 end
166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
164 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
165 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
168
166
169 if User.current.logged?
167 if User.current.logged?
170 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
168 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
171 end
169 end
172
170
173 if project
171 if project
174 # project specific filters
172 # project specific filters
175 unless @project.issue_categories.empty?
173 unless @project.issue_categories.empty?
176 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
174 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
177 end
175 end
178 unless @project.versions.empty?
176 unless @project.versions.empty?
179 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
177 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
180 end
178 end
181 unless @project.descendants.active.empty?
179 unless @project.descendants.active.empty?
182 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
180 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
183 end
181 end
184 add_custom_fields_filters(@project.all_issue_custom_fields)
182 add_custom_fields_filters(@project.all_issue_custom_fields)
185 else
183 else
186 # global filters for cross project issue list
184 # global filters for cross project issue list
187 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
185 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
188 end
186 end
189 @available_filters
187 @available_filters
190 end
188 end
191
189
192 def add_filter(field, operator, values)
190 def add_filter(field, operator, values)
193 # values must be an array
191 # values must be an array
194 return unless values and values.is_a? Array # and !values.first.empty?
192 return unless values and values.is_a? Array # and !values.first.empty?
195 # check if field is defined as an available filter
193 # check if field is defined as an available filter
196 if available_filters.has_key? field
194 if available_filters.has_key? field
197 filter_options = available_filters[field]
195 filter_options = available_filters[field]
198 # check if operator is allowed for that filter
196 # check if operator is allowed for that filter
199 #if @@operators_by_filter_type[filter_options[:type]].include? operator
197 #if @@operators_by_filter_type[filter_options[:type]].include? operator
200 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
198 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
201 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
199 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
202 #end
200 #end
203 filters[field] = {:operator => operator, :values => values }
201 filters[field] = {:operator => operator, :values => values }
204 end
202 end
205 end
203 end
206
204
207 def add_short_filter(field, expression)
205 def add_short_filter(field, expression)
208 return unless expression
206 return unless expression
209 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
207 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
210 add_filter field, (parms[0] || "="), [parms[1] || ""]
208 add_filter field, (parms[0] || "="), [parms[1] || ""]
211 end
209 end
212
210
213 def has_filter?(field)
211 def has_filter?(field)
214 filters and filters[field]
212 filters and filters[field]
215 end
213 end
216
214
217 def operator_for(field)
215 def operator_for(field)
218 has_filter?(field) ? filters[field][:operator] : nil
216 has_filter?(field) ? filters[field][:operator] : nil
219 end
217 end
220
218
221 def values_for(field)
219 def values_for(field)
222 has_filter?(field) ? filters[field][:values] : nil
220 has_filter?(field) ? filters[field][:values] : nil
223 end
221 end
224
222
225 def label_for(field)
223 def label_for(field)
226 label = available_filters[field][:name] if available_filters.has_key?(field)
224 label = available_filters[field][:name] if available_filters.has_key?(field)
227 label ||= field.gsub(/\_id$/, "")
225 label ||= field.gsub(/\_id$/, "")
228 end
226 end
229
227
230 def available_columns
228 def available_columns
231 return @available_columns if @available_columns
229 return @available_columns if @available_columns
232 @available_columns = Query.available_columns
230 @available_columns = Query.available_columns
233 @available_columns += (project ?
231 @available_columns += (project ?
234 project.all_issue_custom_fields :
232 project.all_issue_custom_fields :
235 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
233 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
236 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
234 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
237 end
235 end
238
236
239 def columns
237 def columns
240 if has_default_columns?
238 if has_default_columns?
241 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
239 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
242 else
240 else
243 # preserve the column_names order
241 # preserve the column_names order
244 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
242 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
245 end
243 end
246 end
244 end
247
245
248 def column_names=(names)
246 def column_names=(names)
249 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
247 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
250 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
248 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
251 write_attribute(:column_names, names)
249 write_attribute(:column_names, names)
252 end
250 end
253
251
254 def has_column?(column)
252 def has_column?(column)
255 column_names && column_names.include?(column.name)
253 column_names && column_names.include?(column.name)
256 end
254 end
257
255
258 def has_default_columns?
256 def has_default_columns?
259 column_names.nil? || column_names.empty?
257 column_names.nil? || column_names.empty?
260 end
258 end
261
259
262 def project_statement
260 def project_statement
263 project_clauses = []
261 project_clauses = []
264 if project && !@project.descendants.active.empty?
262 if project && !@project.descendants.active.empty?
265 ids = [project.id]
263 ids = [project.id]
266 if has_filter?("subproject_id")
264 if has_filter?("subproject_id")
267 case operator_for("subproject_id")
265 case operator_for("subproject_id")
268 when '='
266 when '='
269 # include the selected subprojects
267 # include the selected subprojects
270 ids += values_for("subproject_id").each(&:to_i)
268 ids += values_for("subproject_id").each(&:to_i)
271 when '!*'
269 when '!*'
272 # main project only
270 # main project only
273 else
271 else
274 # all subprojects
272 # all subprojects
275 ids += project.descendants.collect(&:id)
273 ids += project.descendants.collect(&:id)
276 end
274 end
277 elsif Setting.display_subprojects_issues?
275 elsif Setting.display_subprojects_issues?
278 ids += project.descendants.collect(&:id)
276 ids += project.descendants.collect(&:id)
279 end
277 end
280 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
278 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
281 elsif project
279 elsif project
282 project_clauses << "#{Project.table_name}.id = %d" % project.id
280 project_clauses << "#{Project.table_name}.id = %d" % project.id
283 end
281 end
284 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
282 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
285 project_clauses.join(' AND ')
283 project_clauses.join(' AND ')
286 end
284 end
287
285
288 def statement
286 def statement
289 # filters clauses
287 # filters clauses
290 filters_clauses = []
288 filters_clauses = []
291 filters.each_key do |field|
289 filters.each_key do |field|
292 next if field == "subproject_id"
290 next if field == "subproject_id"
293 v = values_for(field).clone
291 v = values_for(field).clone
294 next unless v and !v.empty?
292 next unless v and !v.empty?
295 operator = operator_for(field)
293 operator = operator_for(field)
296
294
297 # "me" value subsitution
295 # "me" value subsitution
298 if %w(assigned_to_id author_id watcher_id).include?(field)
296 if %w(assigned_to_id author_id watcher_id).include?(field)
299 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
297 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
300 end
298 end
301
299
302 sql = ''
300 sql = ''
303 if field =~ /^cf_(\d+)$/
301 if field =~ /^cf_(\d+)$/
304 # custom field
302 # custom field
305 db_table = CustomValue.table_name
303 db_table = CustomValue.table_name
306 db_field = 'value'
304 db_field = 'value'
307 is_custom_filter = true
305 is_custom_filter = true
308 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
306 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
309 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
307 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
310 elsif field == 'watcher_id'
308 elsif field == 'watcher_id'
311 db_table = Watcher.table_name
309 db_table = Watcher.table_name
312 db_field = 'user_id'
310 db_field = 'user_id'
313 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
311 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
314 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
312 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
315 else
313 else
316 # regular field
314 # regular field
317 db_table = Issue.table_name
315 db_table = Issue.table_name
318 db_field = field
316 db_field = field
319 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
317 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
320 end
318 end
321 filters_clauses << sql
319 filters_clauses << sql
322
320
323 end if filters and valid?
321 end if filters and valid?
324
322
325 (filters_clauses << project_statement).join(' AND ')
323 (filters_clauses << project_statement).join(' AND ')
326 end
324 end
327
325
328 private
326 private
329
327
330 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
328 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
331 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
329 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
332 sql = ''
330 sql = ''
333 case operator
331 case operator
334 when "="
332 when "="
335 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
333 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
336 when "!"
334 when "!"
337 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
335 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
338 when "!*"
336 when "!*"
339 sql = "#{db_table}.#{db_field} IS NULL"
337 sql = "#{db_table}.#{db_field} IS NULL"
340 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
338 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
341 when "*"
339 when "*"
342 sql = "#{db_table}.#{db_field} IS NOT NULL"
340 sql = "#{db_table}.#{db_field} IS NOT NULL"
343 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
341 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
344 when ">="
342 when ">="
345 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
343 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
346 when "<="
344 when "<="
347 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
345 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
348 when "o"
346 when "o"
349 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
347 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
350 when "c"
348 when "c"
351 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
349 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
352 when ">t-"
350 when ">t-"
353 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
351 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
354 when "<t-"
352 when "<t-"
355 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
353 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
356 when "t-"
354 when "t-"
357 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
355 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
358 when ">t+"
356 when ">t+"
359 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
357 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
360 when "<t+"
358 when "<t+"
361 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
359 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
362 when "t+"
360 when "t+"
363 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
361 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
364 when "t"
362 when "t"
365 sql = date_range_clause(db_table, db_field, 0, 0)
363 sql = date_range_clause(db_table, db_field, 0, 0)
366 when "w"
364 when "w"
367 from = l(:general_first_day_of_week) == '7' ?
365 from = l(:general_first_day_of_week) == '7' ?
368 # week starts on sunday
366 # week starts on sunday
369 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
367 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
370 # week starts on monday (Rails default)
368 # week starts on monday (Rails default)
371 Time.now.at_beginning_of_week
369 Time.now.at_beginning_of_week
372 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
370 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
373 when "~"
371 when "~"
374 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
372 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
375 when "!~"
373 when "!~"
376 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
374 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
377 end
375 end
378
376
379 return sql
377 return sql
380 end
378 end
381
379
382 def add_custom_fields_filters(custom_fields)
380 def add_custom_fields_filters(custom_fields)
383 @available_filters ||= {}
381 @available_filters ||= {}
384
382
385 custom_fields.select(&:is_filter?).each do |field|
383 custom_fields.select(&:is_filter?).each do |field|
386 case field.field_format
384 case field.field_format
387 when "text"
385 when "text"
388 options = { :type => :text, :order => 20 }
386 options = { :type => :text, :order => 20 }
389 when "list"
387 when "list"
390 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
388 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
391 when "date"
389 when "date"
392 options = { :type => :date, :order => 20 }
390 options = { :type => :date, :order => 20 }
393 when "bool"
391 when "bool"
394 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
392 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
395 else
393 else
396 options = { :type => :string, :order => 20 }
394 options = { :type => :string, :order => 20 }
397 end
395 end
398 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
396 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
399 end
397 end
400 end
398 end
401
399
402 # Returns a SQL clause for a date or datetime field.
400 # Returns a SQL clause for a date or datetime field.
403 def date_range_clause(table, field, from, to)
401 def date_range_clause(table, field, from, to)
404 s = []
402 s = []
405 if from
403 if from
406 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
404 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
407 end
405 end
408 if to
406 if to
409 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
407 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
410 end
408 end
411 s.join(' AND ')
409 s.join(' AND ')
412 end
410 end
413 end
411 end
@@ -1,180 +1,180
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 Repository < ActiveRecord::Base
18 class Repository < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
20 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 has_many :changes, :through => :changesets
21 has_many :changes, :through => :changesets
22
22
23 # Raw SQL to delete changesets and changes in the database
23 # Raw SQL to delete changesets and changes in the database
24 # has_many :changesets, :dependent => :destroy is too slow for big repositories
24 # has_many :changesets, :dependent => :destroy is too slow for big repositories
25 before_destroy :clear_changesets
25 before_destroy :clear_changesets
26
26
27 # Checks if the SCM is enabled when creating a repository
27 # Checks if the SCM is enabled when creating a repository
28 validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
28 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
29
29
30 # Removes leading and trailing whitespace
30 # Removes leading and trailing whitespace
31 def url=(arg)
31 def url=(arg)
32 write_attribute(:url, arg ? arg.to_s.strip : nil)
32 write_attribute(:url, arg ? arg.to_s.strip : nil)
33 end
33 end
34
34
35 # Removes leading and trailing whitespace
35 # Removes leading and trailing whitespace
36 def root_url=(arg)
36 def root_url=(arg)
37 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
37 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
38 end
38 end
39
39
40 def scm
40 def scm
41 @scm ||= self.scm_adapter.new url, root_url, login, password
41 @scm ||= self.scm_adapter.new url, root_url, login, password
42 update_attribute(:root_url, @scm.root_url) if root_url.blank?
42 update_attribute(:root_url, @scm.root_url) if root_url.blank?
43 @scm
43 @scm
44 end
44 end
45
45
46 def scm_name
46 def scm_name
47 self.class.scm_name
47 self.class.scm_name
48 end
48 end
49
49
50 def supports_cat?
50 def supports_cat?
51 scm.supports_cat?
51 scm.supports_cat?
52 end
52 end
53
53
54 def supports_annotate?
54 def supports_annotate?
55 scm.supports_annotate?
55 scm.supports_annotate?
56 end
56 end
57
57
58 def entry(path=nil, identifier=nil)
58 def entry(path=nil, identifier=nil)
59 scm.entry(path, identifier)
59 scm.entry(path, identifier)
60 end
60 end
61
61
62 def entries(path=nil, identifier=nil)
62 def entries(path=nil, identifier=nil)
63 scm.entries(path, identifier)
63 scm.entries(path, identifier)
64 end
64 end
65
65
66 def properties(path, identifier=nil)
66 def properties(path, identifier=nil)
67 scm.properties(path, identifier)
67 scm.properties(path, identifier)
68 end
68 end
69
69
70 def cat(path, identifier=nil)
70 def cat(path, identifier=nil)
71 scm.cat(path, identifier)
71 scm.cat(path, identifier)
72 end
72 end
73
73
74 def diff(path, rev, rev_to)
74 def diff(path, rev, rev_to)
75 scm.diff(path, rev, rev_to)
75 scm.diff(path, rev, rev_to)
76 end
76 end
77
77
78 # Default behaviour: we search in cached changesets
78 # Default behaviour: we search in cached changesets
79 def changesets_for_path(path, options={})
79 def changesets_for_path(path, options={})
80 path = "/#{path}" unless path.starts_with?('/')
80 path = "/#{path}" unless path.starts_with?('/')
81 Change.find(:all, :include => {:changeset => :user},
81 Change.find(:all, :include => {:changeset => :user},
82 :conditions => ["repository_id = ? AND path = ?", id, path],
82 :conditions => ["repository_id = ? AND path = ?", id, path],
83 :order => "committed_on DESC, #{Changeset.table_name}.id DESC",
83 :order => "committed_on DESC, #{Changeset.table_name}.id DESC",
84 :limit => options[:limit]).collect(&:changeset)
84 :limit => options[:limit]).collect(&:changeset)
85 end
85 end
86
86
87 # Returns a path relative to the url of the repository
87 # Returns a path relative to the url of the repository
88 def relative_path(path)
88 def relative_path(path)
89 path
89 path
90 end
90 end
91
91
92 def latest_changeset
92 def latest_changeset
93 @latest_changeset ||= changesets.find(:first)
93 @latest_changeset ||= changesets.find(:first)
94 end
94 end
95
95
96 def scan_changesets_for_issue_ids
96 def scan_changesets_for_issue_ids
97 self.changesets.each(&:scan_comment_for_issue_ids)
97 self.changesets.each(&:scan_comment_for_issue_ids)
98 end
98 end
99
99
100 # Returns an array of committers usernames and associated user_id
100 # Returns an array of committers usernames and associated user_id
101 def committers
101 def committers
102 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
102 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
103 end
103 end
104
104
105 # Maps committers username to a user ids
105 # Maps committers username to a user ids
106 def committer_ids=(h)
106 def committer_ids=(h)
107 if h.is_a?(Hash)
107 if h.is_a?(Hash)
108 committers.each do |committer, user_id|
108 committers.each do |committer, user_id|
109 new_user_id = h[committer]
109 new_user_id = h[committer]
110 if new_user_id && (new_user_id.to_i != user_id.to_i)
110 if new_user_id && (new_user_id.to_i != user_id.to_i)
111 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
111 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
112 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
112 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
113 end
113 end
114 end
114 end
115 @committers = nil
115 @committers = nil
116 true
116 true
117 else
117 else
118 false
118 false
119 end
119 end
120 end
120 end
121
121
122 # Returns the Redmine User corresponding to the given +committer+
122 # Returns the Redmine User corresponding to the given +committer+
123 # It will return nil if the committer is not yet mapped and if no User
123 # It will return nil if the committer is not yet mapped and if no User
124 # with the same username or email was found
124 # with the same username or email was found
125 def find_committer_user(committer)
125 def find_committer_user(committer)
126 if committer
126 if committer
127 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
127 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
128 if c && c.user
128 if c && c.user
129 c.user
129 c.user
130 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
130 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
131 username, email = $1.strip, $3
131 username, email = $1.strip, $3
132 u = User.find_by_login(username)
132 u = User.find_by_login(username)
133 u ||= User.find_by_mail(email) unless email.blank?
133 u ||= User.find_by_mail(email) unless email.blank?
134 u
134 u
135 end
135 end
136 end
136 end
137 end
137 end
138
138
139 # fetch new changesets for all repositories
139 # fetch new changesets for all repositories
140 # can be called periodically by an external script
140 # can be called periodically by an external script
141 # eg. ruby script/runner "Repository.fetch_changesets"
141 # eg. ruby script/runner "Repository.fetch_changesets"
142 def self.fetch_changesets
142 def self.fetch_changesets
143 find(:all).each(&:fetch_changesets)
143 find(:all).each(&:fetch_changesets)
144 end
144 end
145
145
146 # scan changeset comments to find related and fixed issues for all repositories
146 # scan changeset comments to find related and fixed issues for all repositories
147 def self.scan_changesets_for_issue_ids
147 def self.scan_changesets_for_issue_ids
148 find(:all).each(&:scan_changesets_for_issue_ids)
148 find(:all).each(&:scan_changesets_for_issue_ids)
149 end
149 end
150
150
151 def self.scm_name
151 def self.scm_name
152 'Abstract'
152 'Abstract'
153 end
153 end
154
154
155 def self.available_scm
155 def self.available_scm
156 subclasses.collect {|klass| [klass.scm_name, klass.name]}
156 subclasses.collect {|klass| [klass.scm_name, klass.name]}
157 end
157 end
158
158
159 def self.factory(klass_name, *args)
159 def self.factory(klass_name, *args)
160 klass = "Repository::#{klass_name}".constantize
160 klass = "Repository::#{klass_name}".constantize
161 klass.new(*args)
161 klass.new(*args)
162 rescue
162 rescue
163 nil
163 nil
164 end
164 end
165
165
166 private
166 private
167
167
168 def before_save
168 def before_save
169 # Strips url and root_url
169 # Strips url and root_url
170 url.strip!
170 url.strip!
171 root_url.strip!
171 root_url.strip!
172 true
172 true
173 end
173 end
174
174
175 def clear_changesets
175 def clear_changesets
176 connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
176 connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
177 connection.delete("DELETE FROM changesets_issues WHERE changesets_issues.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
177 connection.delete("DELETE FROM changesets_issues WHERE changesets_issues.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
178 connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}")
178 connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}")
179 end
179 end
180 end
180 end
@@ -1,79 +1,79
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 TimeEntry < ActiveRecord::Base
18 class TimeEntry < ActiveRecord::Base
19 # could have used polymorphic association
19 # could have used polymorphic association
20 # project association here allows easy loading of time entries at project level with one database trip
20 # project association here allows easy loading of time entries at project level with one database trip
21 belongs_to :project
21 belongs_to :project
22 belongs_to :issue
22 belongs_to :issue
23 belongs_to :user
23 belongs_to :user
24 belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
24 belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
25
25
26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27
27
28 acts_as_customizable
28 acts_as_customizable
29 acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"},
29 acts_as_event :title => Proc.new {|o| "#{o.user}: #{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
31 :author => :user,
31 :author => :user,
32 :description => :comments
32 :description => :comments
33
33
34 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
34 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
35 validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
35 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
36 validates_length_of :comments, :maximum => 255, :allow_nil => true
36 validates_length_of :comments, :maximum => 255, :allow_nil => true
37
37
38 def after_initialize
38 def after_initialize
39 if new_record? && self.activity.nil?
39 if new_record? && self.activity.nil?
40 if default_activity = Enumeration.activities.default
40 if default_activity = Enumeration.activities.default
41 self.activity_id = default_activity.id
41 self.activity_id = default_activity.id
42 end
42 end
43 end
43 end
44 end
44 end
45
45
46 def before_validation
46 def before_validation
47 self.project = issue.project if issue && project.nil?
47 self.project = issue.project if issue && project.nil?
48 end
48 end
49
49
50 def validate
50 def validate
51 errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
51 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
52 errors.add :project_id, :activerecord_error_invalid if project.nil?
52 errors.add :project_id, :invalid if project.nil?
53 errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
53 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
54 end
54 end
55
55
56 def hours=(h)
56 def hours=(h)
57 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
57 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
58 end
58 end
59
59
60 # tyear, tmonth, tweek assigned where setting spent_on attributes
60 # tyear, tmonth, tweek assigned where setting spent_on attributes
61 # these attributes make time aggregations easier
61 # these attributes make time aggregations easier
62 def spent_on=(date)
62 def spent_on=(date)
63 super
63 super
64 self.tyear = spent_on ? spent_on.year : nil
64 self.tyear = spent_on ? spent_on.year : nil
65 self.tmonth = spent_on ? spent_on.month : nil
65 self.tmonth = spent_on ? spent_on.month : nil
66 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
66 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
67 end
67 end
68
68
69 # Returns true if the time entry can be edited by usr, otherwise false
69 # Returns true if the time entry can be edited by usr, otherwise false
70 def editable_by?(usr)
70 def editable_by?(usr)
71 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
71 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
72 end
72 end
73
73
74 def self.visible_by(usr)
74 def self.visible_by(usr)
75 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
75 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
76 yield
76 yield
77 end
77 end
78 end
78 end
79 end
79 end
@@ -1,143 +1,143
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 Version < ActiveRecord::Base
18 class Version < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 belongs_to :project
20 belongs_to :project
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
22 acts_as_attachable :view_permission => :view_files,
22 acts_as_attachable :view_permission => :view_files,
23 :delete_permission => :manage_files
23 :delete_permission => :manage_files
24
24
25 validates_presence_of :name
25 validates_presence_of :name
26 validates_uniqueness_of :name, :scope => [:project_id]
26 validates_uniqueness_of :name, :scope => [:project_id]
27 validates_length_of :name, :maximum => 60
27 validates_length_of :name, :maximum => 60
28 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
28 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
29
29
30 def start_date
30 def start_date
31 effective_date
31 effective_date
32 end
32 end
33
33
34 def due_date
34 def due_date
35 effective_date
35 effective_date
36 end
36 end
37
37
38 # Returns the total estimated time for this version
38 # Returns the total estimated time for this version
39 def estimated_hours
39 def estimated_hours
40 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
40 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
41 end
41 end
42
42
43 # Returns the total reported time for this version
43 # Returns the total reported time for this version
44 def spent_hours
44 def spent_hours
45 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
45 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
46 end
46 end
47
47
48 # Returns true if the version is completed: due date reached and no open issues
48 # Returns true if the version is completed: due date reached and no open issues
49 def completed?
49 def completed?
50 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
50 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
51 end
51 end
52
52
53 def completed_pourcent
53 def completed_pourcent
54 if issues_count == 0
54 if issues_count == 0
55 0
55 0
56 elsif open_issues_count == 0
56 elsif open_issues_count == 0
57 100
57 100
58 else
58 else
59 issues_progress(false) + issues_progress(true)
59 issues_progress(false) + issues_progress(true)
60 end
60 end
61 end
61 end
62
62
63 def closed_pourcent
63 def closed_pourcent
64 if issues_count == 0
64 if issues_count == 0
65 0
65 0
66 else
66 else
67 issues_progress(false)
67 issues_progress(false)
68 end
68 end
69 end
69 end
70
70
71 # Returns true if the version is overdue: due date reached and some open issues
71 # Returns true if the version is overdue: due date reached and some open issues
72 def overdue?
72 def overdue?
73 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
73 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
74 end
74 end
75
75
76 # Returns assigned issues count
76 # Returns assigned issues count
77 def issues_count
77 def issues_count
78 @issue_count ||= fixed_issues.count
78 @issue_count ||= fixed_issues.count
79 end
79 end
80
80
81 def open_issues_count
81 def open_issues_count
82 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
82 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
83 end
83 end
84
84
85 def closed_issues_count
85 def closed_issues_count
86 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
86 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
87 end
87 end
88
88
89 def wiki_page
89 def wiki_page
90 if project.wiki && !wiki_page_title.blank?
90 if project.wiki && !wiki_page_title.blank?
91 @wiki_page ||= project.wiki.find_page(wiki_page_title)
91 @wiki_page ||= project.wiki.find_page(wiki_page_title)
92 end
92 end
93 @wiki_page
93 @wiki_page
94 end
94 end
95
95
96 def to_s; name end
96 def to_s; name end
97
97
98 # Versions are sorted by effective_date and name
98 # Versions are sorted by effective_date and name
99 # Those with no effective_date are at the end, sorted by name
99 # Those with no effective_date are at the end, sorted by name
100 def <=>(version)
100 def <=>(version)
101 if self.effective_date
101 if self.effective_date
102 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
102 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
103 else
103 else
104 version.effective_date ? 1 : (self.name <=> version.name)
104 version.effective_date ? 1 : (self.name <=> version.name)
105 end
105 end
106 end
106 end
107
107
108 private
108 private
109 def check_integrity
109 def check_integrity
110 raise "Can't delete version" if self.fixed_issues.find(:first)
110 raise "Can't delete version" if self.fixed_issues.find(:first)
111 end
111 end
112
112
113 # Returns the average estimated time of assigned issues
113 # Returns the average estimated time of assigned issues
114 # or 1 if no issue has an estimated time
114 # or 1 if no issue has an estimated time
115 # Used to weigth unestimated issues in progress calculation
115 # Used to weigth unestimated issues in progress calculation
116 def estimated_average
116 def estimated_average
117 if @estimated_average.nil?
117 if @estimated_average.nil?
118 average = fixed_issues.average(:estimated_hours).to_f
118 average = fixed_issues.average(:estimated_hours).to_f
119 if average == 0
119 if average == 0
120 average = 1
120 average = 1
121 end
121 end
122 @estimated_average = average
122 @estimated_average = average
123 end
123 end
124 @estimated_average
124 @estimated_average
125 end
125 end
126
126
127 # Returns the total progress of open or closed issues
127 # Returns the total progress of open or closed issues
128 def issues_progress(open)
128 def issues_progress(open)
129 @issues_progress ||= {}
129 @issues_progress ||= {}
130 @issues_progress[open] ||= begin
130 @issues_progress[open] ||= begin
131 progress = 0
131 progress = 0
132 if issues_count > 0
132 if issues_count > 0
133 ratio = open ? 'done_ratio' : 100
133 ratio = open ? 'done_ratio' : 100
134 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
134 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
135 :include => :status,
135 :include => :status,
136 :conditions => ["is_closed = ?", !open]).to_f
136 :conditions => ["is_closed = ?", !open]).to_f
137
137
138 progress = done / (estimated_average * issues_count)
138 progress = done / (estimated_average * issues_count)
139 end
139 end
140 progress
140 progress
141 end
141 end
142 end
142 end
143 end
143 end
@@ -1,30 +1,30
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 Watcher < ActiveRecord::Base
18 class Watcher < ActiveRecord::Base
19 belongs_to :watchable, :polymorphic => true
19 belongs_to :watchable, :polymorphic => true
20 belongs_to :user
20 belongs_to :user
21
21
22 validates_presence_of :user
22 validates_presence_of :user
23 validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
23 validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
24
24
25 protected
25 protected
26
26
27 def validate
27 def validate
28 errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active?
28 errors.add :user_id, :invalid unless user.nil? || user.active?
29 end
29 end
30 end
30 end
@@ -1,188 +1,188
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 'diff'
18 require 'diff'
19 require 'enumerator'
19 require 'enumerator'
20
20
21 class WikiPage < ActiveRecord::Base
21 class WikiPage < ActiveRecord::Base
22 belongs_to :wiki
22 belongs_to :wiki
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
25 acts_as_tree :order => 'title'
25 acts_as_tree :order => 'title'
26
26
27 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
27 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
28 :description => :text,
28 :description => :text,
29 :datetime => :created_on,
29 :datetime => :created_on,
30 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
30 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
31
31
32 acts_as_searchable :columns => ['title', 'text'],
32 acts_as_searchable :columns => ['title', 'text'],
33 :include => [{:wiki => :project}, :content],
33 :include => [{:wiki => :project}, :content],
34 :project_key => "#{Wiki.table_name}.project_id"
34 :project_key => "#{Wiki.table_name}.project_id"
35
35
36 attr_accessor :redirect_existing_links
36 attr_accessor :redirect_existing_links
37
37
38 validates_presence_of :title
38 validates_presence_of :title
39 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
39 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
40 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
40 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
41 validates_associated :content
41 validates_associated :content
42
42
43 def title=(value)
43 def title=(value)
44 value = Wiki.titleize(value)
44 value = Wiki.titleize(value)
45 @previous_title = read_attribute(:title) if @previous_title.blank?
45 @previous_title = read_attribute(:title) if @previous_title.blank?
46 write_attribute(:title, value)
46 write_attribute(:title, value)
47 end
47 end
48
48
49 def before_save
49 def before_save
50 self.title = Wiki.titleize(title)
50 self.title = Wiki.titleize(title)
51 # Manage redirects if the title has changed
51 # Manage redirects if the title has changed
52 if !@previous_title.blank? && (@previous_title != title) && !new_record?
52 if !@previous_title.blank? && (@previous_title != title) && !new_record?
53 # Update redirects that point to the old title
53 # Update redirects that point to the old title
54 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
54 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
55 r.redirects_to = title
55 r.redirects_to = title
56 r.title == r.redirects_to ? r.destroy : r.save
56 r.title == r.redirects_to ? r.destroy : r.save
57 end
57 end
58 # Remove redirects for the new title
58 # Remove redirects for the new title
59 wiki.redirects.find_all_by_title(title).each(&:destroy)
59 wiki.redirects.find_all_by_title(title).each(&:destroy)
60 # Create a redirect to the new title
60 # Create a redirect to the new title
61 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
61 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
62 @previous_title = nil
62 @previous_title = nil
63 end
63 end
64 end
64 end
65
65
66 def before_destroy
66 def before_destroy
67 # Remove redirects to this page
67 # Remove redirects to this page
68 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
68 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
69 end
69 end
70
70
71 def pretty_title
71 def pretty_title
72 WikiPage.pretty_title(title)
72 WikiPage.pretty_title(title)
73 end
73 end
74
74
75 def content_for_version(version=nil)
75 def content_for_version(version=nil)
76 result = content.versions.find_by_version(version.to_i) if version
76 result = content.versions.find_by_version(version.to_i) if version
77 result ||= content
77 result ||= content
78 result
78 result
79 end
79 end
80
80
81 def diff(version_to=nil, version_from=nil)
81 def diff(version_to=nil, version_from=nil)
82 version_to = version_to ? version_to.to_i : self.content.version
82 version_to = version_to ? version_to.to_i : self.content.version
83 version_from = version_from ? version_from.to_i : version_to - 1
83 version_from = version_from ? version_from.to_i : version_to - 1
84 version_to, version_from = version_from, version_to unless version_from < version_to
84 version_to, version_from = version_from, version_to unless version_from < version_to
85
85
86 content_to = content.versions.find_by_version(version_to)
86 content_to = content.versions.find_by_version(version_to)
87 content_from = content.versions.find_by_version(version_from)
87 content_from = content.versions.find_by_version(version_from)
88
88
89 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
89 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
90 end
90 end
91
91
92 def annotate(version=nil)
92 def annotate(version=nil)
93 version = version ? version.to_i : self.content.version
93 version = version ? version.to_i : self.content.version
94 c = content.versions.find_by_version(version)
94 c = content.versions.find_by_version(version)
95 c ? WikiAnnotate.new(c) : nil
95 c ? WikiAnnotate.new(c) : nil
96 end
96 end
97
97
98 def self.pretty_title(str)
98 def self.pretty_title(str)
99 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
99 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
100 end
100 end
101
101
102 def project
102 def project
103 wiki.project
103 wiki.project
104 end
104 end
105
105
106 def text
106 def text
107 content.text if content
107 content.text if content
108 end
108 end
109
109
110 # Returns true if usr is allowed to edit the page, otherwise false
110 # Returns true if usr is allowed to edit the page, otherwise false
111 def editable_by?(usr)
111 def editable_by?(usr)
112 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
112 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
113 end
113 end
114
114
115 def attachments_deletable?(usr=User.current)
115 def attachments_deletable?(usr=User.current)
116 editable_by?(usr) && super(usr)
116 editable_by?(usr) && super(usr)
117 end
117 end
118
118
119 def parent_title
119 def parent_title
120 @parent_title || (self.parent && self.parent.pretty_title)
120 @parent_title || (self.parent && self.parent.pretty_title)
121 end
121 end
122
122
123 def parent_title=(t)
123 def parent_title=(t)
124 @parent_title = t
124 @parent_title = t
125 parent_page = t.blank? ? nil : self.wiki.find_page(t)
125 parent_page = t.blank? ? nil : self.wiki.find_page(t)
126 self.parent = parent_page
126 self.parent = parent_page
127 end
127 end
128
128
129 protected
129 protected
130
130
131 def validate
131 def validate
132 errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil?
132 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
133 errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
133 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
134 errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id)
134 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
135 end
135 end
136 end
136 end
137
137
138 class WikiDiff
138 class WikiDiff
139 attr_reader :diff, :words, :content_to, :content_from
139 attr_reader :diff, :words, :content_to, :content_from
140
140
141 def initialize(content_to, content_from)
141 def initialize(content_to, content_from)
142 @content_to = content_to
142 @content_to = content_to
143 @content_from = content_from
143 @content_from = content_from
144 @words = content_to.text.split(/(\s+)/)
144 @words = content_to.text.split(/(\s+)/)
145 @words = @words.select {|word| word != ' '}
145 @words = @words.select {|word| word != ' '}
146 words_from = content_from.text.split(/(\s+)/)
146 words_from = content_from.text.split(/(\s+)/)
147 words_from = words_from.select {|word| word != ' '}
147 words_from = words_from.select {|word| word != ' '}
148 @diff = words_from.diff @words
148 @diff = words_from.diff @words
149 end
149 end
150 end
150 end
151
151
152 class WikiAnnotate
152 class WikiAnnotate
153 attr_reader :lines, :content
153 attr_reader :lines, :content
154
154
155 def initialize(content)
155 def initialize(content)
156 @content = content
156 @content = content
157 current = content
157 current = content
158 current_lines = current.text.split(/\r?\n/)
158 current_lines = current.text.split(/\r?\n/)
159 @lines = current_lines.collect {|t| [nil, nil, t]}
159 @lines = current_lines.collect {|t| [nil, nil, t]}
160 positions = []
160 positions = []
161 current_lines.size.times {|i| positions << i}
161 current_lines.size.times {|i| positions << i}
162 while (current.previous)
162 while (current.previous)
163 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
163 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
164 d.each_slice(3) do |s|
164 d.each_slice(3) do |s|
165 sign, line = s[0], s[1]
165 sign, line = s[0], s[1]
166 if sign == '+' && positions[line] && positions[line] != -1
166 if sign == '+' && positions[line] && positions[line] != -1
167 if @lines[positions[line]][0].nil?
167 if @lines[positions[line]][0].nil?
168 @lines[positions[line]][0] = current.version
168 @lines[positions[line]][0] = current.version
169 @lines[positions[line]][1] = current.author
169 @lines[positions[line]][1] = current.author
170 end
170 end
171 end
171 end
172 end
172 end
173 d.each_slice(3) do |s|
173 d.each_slice(3) do |s|
174 sign, line = s[0], s[1]
174 sign, line = s[0], s[1]
175 if sign == '-'
175 if sign == '-'
176 positions.insert(line, -1)
176 positions.insert(line, -1)
177 else
177 else
178 positions[line] = nil
178 positions[line] = nil
179 end
179 end
180 end
180 end
181 positions.compact!
181 positions.compact!
182 # Stop if every line is annotated
182 # Stop if every line is annotated
183 break unless @lines.detect { |line| line[0].nil? }
183 break unless @lines.detect { |line| line[0].nil? }
184 current = current.previous
184 current = current.previous
185 end
185 end
186 @lines.each { |line| line[0] ||= current.version }
186 @lines.each { |line| line[0] ||= current.version }
187 end
187 end
188 end
188 end
@@ -1,39 +1,39
1 <table class="cal">
1 <table class="cal">
2 <thead>
2 <thead>
3 <tr><td></td><% 7.times do |i| %><th><%= day_name( (calendar.first_wday+i)%7 ) %></th><% end %></tr>
3 <tr><td></td><% 7.times do |i| %><th><%= day_name( (calendar.first_wday+i)%7 ) %></th><% end %></tr>
4 </thead>
4 </thead>
5 <tbody>
5 <tbody>
6 <tr>
6 <tr>
7 <% day = calendar.startdt
7 <% day = calendar.startdt
8 while day <= calendar.enddt %>
8 while day <= calendar.enddt %>
9 <%= "<th>#{day.cweek}</th>" if day.cwday == calendar.first_wday %>
9 <%= "<th>#{day.cweek}</th>" if day.cwday == calendar.first_wday %>
10 <td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %>">
10 <td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %>">
11 <p class="day-num"><%= day.day %></p>
11 <p class="day-num"><%= day.day %></p>
12 <% calendar.events_on(day).each do |i| %>
12 <% calendar.events_on(day).each do |i| %>
13 <% if i.is_a? Issue %>
13 <% if i.is_a? Issue %>
14 <div class="tooltip">
14 <div class="tooltip">
15 <%= if day == i.start_date && day == i.due_date
15 <%= if day == i.start_date && day == i.due_date
16 image_tag('arrow_bw.png')
16 image_tag('arrow_bw.png')
17 elsif day == i.start_date
17 elsif day == i.start_date
18 image_tag('arrow_from.png')
18 image_tag('arrow_from.png')
19 elsif day == i.due_date
19 elsif day == i.due_date
20 image_tag('arrow_to.png')
20 image_tag('arrow_to.png')
21 end %>
21 end %>
22 <%= h("#{i.project} -") unless @project && @project == i.project %>
22 <%= h("#{i.project} -") unless @project && @project == i.project %>
23 <%= link_to_issue i %>: <%= h(truncate(i.subject, 30)) %>
23 <%= link_to_issue i %>: <%= h(truncate(i.subject, :length => 30)) %>
24 <span class="tip"><%= render_issue_tooltip i %></span>
24 <span class="tip"><%= render_issue_tooltip i %></span>
25 </div>
25 </div>
26 <% else %>
26 <% else %>
27 <span class="icon icon-package">
27 <span class="icon icon-package">
28 <%= h("#{i.project} -") unless @project && @project == i.project %>
28 <%= h("#{i.project} -") unless @project && @project == i.project %>
29 <%= link_to_version i%>
29 <%= link_to_version i%>
30 </span>
30 </span>
31 <% end %>
31 <% end %>
32 <% end %>
32 <% end %>
33 </td>
33 </td>
34 <%= '</tr><tr>' if day.cwday==calendar.last_wday and day!=calendar.enddt %>
34 <%= '</tr><tr>' if day.cwday==calendar.last_wday and day!=calendar.enddt %>
35 <% day = day + 1
35 <% day = day + 1
36 end %>
36 end %>
37 </tr>
37 </tr>
38 </tbody>
38 </tbody>
39 </table>
39 </table>
@@ -1,31 +1,31
1 xml.instruct!
1 xml.instruct!
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3 xml.title truncate_single_line(@title, 100)
3 xml.title truncate_single_line(@title, :length => 100)
4 xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false}))
4 xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false}))
5 xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false)
5 xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false)
6 xml.id url_for(:controller => 'welcome', :only_path => false)
6 xml.id url_for(:controller => 'welcome', :only_path => false)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
8 xml.author { xml.name "#{Setting.app_title}" }
8 xml.author { xml.name "#{Setting.app_title}" }
9 xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
9 xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
10 @items.each do |item|
10 @items.each do |item|
11 xml.entry do
11 xml.entry do
12 url = url_for(item.event_url(:only_path => false))
12 url = url_for(item.event_url(:only_path => false))
13 if @project
13 if @project
14 xml.title truncate_single_line(item.event_title, 100)
14 xml.title truncate_single_line(item.event_title, :length => 100)
15 else
15 else
16 xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100)
16 xml.title truncate_single_line("#{item.project} - #{item.event_title}", :length => 100)
17 end
17 end
18 xml.link "rel" => "alternate", "href" => url
18 xml.link "rel" => "alternate", "href" => url
19 xml.id url
19 xml.id url
20 xml.updated item.event_datetime.xmlschema
20 xml.updated item.event_datetime.xmlschema
21 author = item.event_author if item.respond_to?(:event_author)
21 author = item.event_author if item.respond_to?(:event_author)
22 xml.author do
22 xml.author do
23 xml.name(author)
23 xml.name(author)
24 xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank?
24 xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank?
25 end if author
25 end if author
26 xml.content "type" => "html" do
26 xml.content "type" => "html" do
27 xml.text! textilizable(item.event_description)
27 xml.text! textilizable(item.event_description)
28 end
28 end
29 end
29 end
30 end
30 end
31 end
31 end
@@ -1,58 +1,58
1 <h2><%=l(:label_custom_field_plural)%></h2>
1 <h2><%=l(:label_custom_field_plural)%></h2>
2
2
3 <% selected_tab = params[:tab] ? params[:tab].to_s : custom_fields_tabs.first[:name] %>
3 <% selected_tab = params[:tab] ? params[:tab].to_s : custom_fields_tabs.first[:name] %>
4
4
5 <div class="tabs">
5 <div class="tabs">
6 <ul>
6 <ul>
7 <% custom_fields_tabs.each do |tab| -%>
7 <% custom_fields_tabs.each do |tab| -%>
8 <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
8 <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
9 :id => "tab-#{tab[:name]}",
9 :id => "tab-#{tab[:name]}",
10 :class => (tab[:name] != selected_tab ? nil : 'selected'),
10 :class => (tab[:name] != selected_tab ? nil : 'selected'),
11 :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
11 :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
12 <% end -%>
12 <% end -%>
13 </ul>
13 </ul>
14 </div>
14 </div>
15
15
16 <% custom_fields_tabs.each do |tab| %>
16 <% custom_fields_tabs.each do |tab| %>
17 <div id="tab-content-<%= tab[:name] %>" class="tab-content" style="<%= tab[:name] != selected_tab ? 'display:none' : nil %>">
17 <div id="tab-content-<%= tab[:name] %>" class="tab-content" style="<%= tab[:name] != selected_tab ? 'display:none' : nil %>">
18 <table class="list">
18 <table class="list">
19 <thead><tr>
19 <thead><tr>
20 <th width="30%"><%=l(:field_name)%></th>
20 <th width="30%"><%=l(:field_name)%></th>
21 <th><%=l(:field_field_format)%></th>
21 <th><%=l(:field_field_format)%></th>
22 <th><%=l(:field_is_required)%></th>
22 <th><%=l(:field_is_required)%></th>
23 <% if tab[:name] == 'IssueCustomField' %>
23 <% if tab[:name] == 'IssueCustomField' %>
24 <th><%=l(:field_is_for_all)%></th>
24 <th><%=l(:field_is_for_all)%></th>
25 <th><%=l(:label_used_by)%></th>
25 <th><%=l(:label_used_by)%></th>
26 <% end %>
26 <% end %>
27 <th><%=l(:button_sort)%></th>
27 <th><%=l(:button_sort)%></th>
28 <th width="10%"></th>
28 <th width="10%"></th>
29 </tr></thead>
29 </tr></thead>
30 <tbody>
30 <tbody>
31 <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%>
31 <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%>
32 <tr class="<%= cycle("odd", "even") %>">
32 <tr class="<%= cycle("odd", "even") %>">
33 <td><%= link_to custom_field.name, :action => 'edit', :id => custom_field %></td>
33 <td><%= link_to custom_field.name, :action => 'edit', :id => custom_field %></td>
34 <td align="center"><%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %></td>
34 <td align="center"><%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %></td>
35 <td align="center"><%= image_tag 'true.png' if custom_field.is_required? %></td>
35 <td align="center"><%= image_tag 'true.png' if custom_field.is_required? %></td>
36 <% if tab[:name] == 'IssueCustomField' %>
36 <% if tab[:name] == 'IssueCustomField' %>
37 <td align="center"><%= image_tag 'true.png' if custom_field.is_for_all? %></td>
37 <td align="center"><%= image_tag 'true.png' if custom_field.is_for_all? %></td>
38 <td align="center"><%= custom_field.projects.count.to_s + ' ' + lwr(:label_project, custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td>
38 <td align="center"><%= l(:label_x_projects, :count => custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td>
39 <% end %>
39 <% end %>
40 <td align="center" style="width:15%;">
40 <td align="center" style="width:15%;">
41 <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => custom_field, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
41 <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => custom_field, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
42 <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => custom_field, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
42 <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => custom_field, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
43 <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => custom_field, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
43 <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => custom_field, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
44 <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => custom_field, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
44 <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => custom_field, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
45 </td>
45 </td>
46 <td align="center">
46 <td align="center">
47 <%= button_to l(:button_delete), { :action => 'destroy', :id => custom_field }, :confirm => l(:text_are_you_sure), :class => "button-small" %>
47 <%= button_to l(:button_delete), { :action => 'destroy', :id => custom_field }, :confirm => l(:text_are_you_sure), :class => "button-small" %>
48 </td>
48 </td>
49 </tr>
49 </tr>
50 <% end; reset_cycle %>
50 <% end; reset_cycle %>
51 </tbody>
51 </tbody>
52 </table>
52 </table>
53
53
54 <p><%= link_to l(:label_custom_field_new), {:action => 'new', :type => tab[:name]}, :class => 'icon icon-add' %></p>
54 <p><%= link_to l(:label_custom_field_new), {:action => 'new', :type => tab[:name]}, :class => 'icon icon-add' %></p>
55 </div>
55 </div>
56 <% end %>
56 <% end %>
57
57
58 <% html_title(l(:label_custom_field_plural)) -%>
58 <% html_title(l(:label_custom_field_plural)) -%>
@@ -1,3 +1,3
1 <p><%= link_to h(document.title), :controller => 'documents', :action => 'show', :id => document %><br />
1 <p><%= link_to h(document.title), :controller => 'documents', :action => 'show', :id => document %><br />
2 <% unless document.description.blank? %><%=h(truncate(document.description, 250)) %><br /><% end %>
2 <% unless document.description.blank? %><%=h(truncate(document.description, :length => 250)) %><br /><% end %>
3 <em><%= format_time(document.created_on) %></em></p> No newline at end of file
3 <em><%= format_time(document.created_on) %></em></p>
@@ -1,32 +1,32
1 <div class="contextual">
1 <div class="contextual">
2 <% if authorize_for('issue_relations', 'new') %>
2 <% if authorize_for('issue_relations', 'new') %>
3 <%= toggle_link l(:button_add), 'new-relation-form'%>
3 <%= toggle_link l(:button_add), 'new-relation-form'%>
4 <% end %>
4 <% end %>
5 </div>
5 </div>
6
6
7 <p><strong><%=l(:label_related_issues)%></strong></p>
7 <p><strong><%=l(:label_related_issues)%></strong></p>
8
8
9 <% if @issue.relations.any? %>
9 <% if @issue.relations.any? %>
10 <table style="width:100%">
10 <table style="width:100%">
11 <% @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation| %>
11 <% @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation| %>
12 <tr>
12 <tr>
13 <td><%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %>
13 <td><%= l(relation.label_for(@issue)) %> <%= "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)})" if relation.delay && relation.delay != 0 %>
14 <%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %> <%= link_to_issue relation.other_issue(@issue) %></td>
14 <%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %> <%= link_to_issue relation.other_issue(@issue) %></td>
15 <td><%=h relation.other_issue(@issue).subject %></td>
15 <td><%=h relation.other_issue(@issue).subject %></td>
16 <td><%= relation.other_issue(@issue).status.name %></td>
16 <td><%= relation.other_issue(@issue).status.name %></td>
17 <td><%= format_date(relation.other_issue(@issue).start_date) %></td>
17 <td><%= format_date(relation.other_issue(@issue).start_date) %></td>
18 <td><%= format_date(relation.other_issue(@issue).due_date) %></td>
18 <td><%= format_date(relation.other_issue(@issue).due_date) %></td>
19 <td><%= link_to_remote(image_tag('delete.png'), { :url => {:controller => 'issue_relations', :action => 'destroy', :issue_id => @issue, :id => relation},
19 <td><%= link_to_remote(image_tag('delete.png'), { :url => {:controller => 'issue_relations', :action => 'destroy', :issue_id => @issue, :id => relation},
20 :method => :post
20 :method => :post
21 }, :title => l(:label_relation_delete)) if authorize_for('issue_relations', 'destroy') %></td>
21 }, :title => l(:label_relation_delete)) if authorize_for('issue_relations', 'destroy') %></td>
22 </tr>
22 </tr>
23 <% end %>
23 <% end %>
24 </table>
24 </table>
25 <% end %>
25 <% end %>
26
26
27 <% remote_form_for(:relation, @relation,
27 <% remote_form_for(:relation, @relation,
28 :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue},
28 :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue},
29 :method => :post,
29 :method => :post,
30 :html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %>
30 :html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %>
31 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
31 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
32 <% end %>
32 <% end %>
@@ -1,125 +1,125
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time-add' %>
4 <%= watcher_tag(@issue, User.current) %>
4 <%= watcher_tag(@issue, User.current) %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
7 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
7 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
8 </div>
8 </div>
9
9
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
11
11
12 <div class="<%= css_issue_classes(@issue) %>">
12 <div class="<%= css_issue_classes(@issue) %>">
13 <%= avatar(@issue.author, :size => "64") %>
13 <%= avatar(@issue.author, :size => "64") %>
14 <h3><%=h @issue.subject %></h3>
14 <h3><%=h @issue.subject %></h3>
15 <p class="author">
15 <p class="author">
16 <%= authoring @issue.created_on, @issue.author %>.
16 <%= authoring @issue.created_on, @issue.author %>.
17 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
17 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
18 </p>
18 </p>
19
19
20 <table width="100%">
20 <table width="100%">
21 <tr>
21 <tr>
22 <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status"><%= @issue.status.name %></td>
22 <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status"><%= @issue.status.name %></td>
23 <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
23 <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
24 </tr>
24 </tr>
25 <tr>
25 <tr>
26 <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority"><%= @issue.priority.name %></td>
26 <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority"><%= @issue.priority.name %></td>
27 <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
27 <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
28 </tr>
28 </tr>
29 <tr>
29 <tr>
30 <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
30 <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
31 <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
31 <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
32 </tr>
32 </tr>
33 <tr>
33 <tr>
34 <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
34 <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
35 <% if User.current.allowed_to?(:view_time_entries, @project) %>
35 <% if User.current.allowed_to?(:view_time_entries, @project) %>
36 <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
36 <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
37 <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>
37 <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %></td>
38 <% end %>
38 <% end %>
39 </tr>
39 </tr>
40 <tr>
40 <tr>
41 <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
41 <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
42 <% if @issue.estimated_hours %>
42 <% if @issue.estimated_hours %>
43 <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
43 <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= l_hours(@issue.estimated_hours) %></td>
44 <% end %>
44 <% end %>
45 </tr>
45 </tr>
46 <tr>
46 <tr>
47 <% n = 0 -%>
47 <% n = 0 -%>
48 <% @issue.custom_values.each do |value| -%>
48 <% @issue.custom_values.each do |value| -%>
49 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
49 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
50 <% n = n + 1
50 <% n = n + 1
51 if (n > 1)
51 if (n > 1)
52 n = 0 %>
52 n = 0 %>
53 </tr><tr>
53 </tr><tr>
54 <%end
54 <%end
55 end %>
55 end %>
56 </tr>
56 </tr>
57 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
57 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
58 </table>
58 </table>
59 <hr />
59 <hr />
60
60
61 <div class="contextual">
61 <div class="contextual">
62 <%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
62 <%= link_to_remote_if_authorized(l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment') unless @issue.description.blank? %>
63 </div>
63 </div>
64
64
65 <p><strong><%=l(:field_description)%></strong></p>
65 <p><strong><%=l(:field_description)%></strong></p>
66 <div class="wiki">
66 <div class="wiki">
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
68 </div>
68 </div>
69
69
70 <%= link_to_attachments @issue %>
70 <%= link_to_attachments @issue %>
71
71
72 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
72 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
73 <hr />
73 <hr />
74 <div id="relations">
74 <div id="relations">
75 <%= render :partial => 'relations' %>
75 <%= render :partial => 'relations' %>
76 </div>
76 </div>
77 <% end %>
77 <% end %>
78
78
79 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
79 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
80 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
80 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
81 <hr />
81 <hr />
82 <div id="watchers">
82 <div id="watchers">
83 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
83 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
84 </div>
84 </div>
85 <% end %>
85 <% end %>
86
86
87 </div>
87 </div>
88
88
89 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
89 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
90 <div id="issue-changesets">
90 <div id="issue-changesets">
91 <h3><%=l(:label_associated_revisions)%></h3>
91 <h3><%=l(:label_associated_revisions)%></h3>
92 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
92 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
93 </div>
93 </div>
94 <% end %>
94 <% end %>
95
95
96 <% if @journals.any? %>
96 <% if @journals.any? %>
97 <div id="history">
97 <div id="history">
98 <h3><%=l(:label_history)%></h3>
98 <h3><%=l(:label_history)%></h3>
99 <%= render :partial => 'history', :locals => { :journals => @journals } %>
99 <%= render :partial => 'history', :locals => { :journals => @journals } %>
100 </div>
100 </div>
101 <% end %>
101 <% end %>
102 <div style="clear: both;"></div>
102 <div style="clear: both;"></div>
103
103
104 <% if authorize_for('issues', 'edit') %>
104 <% if authorize_for('issues', 'edit') %>
105 <div id="update" style="display:none;">
105 <div id="update" style="display:none;">
106 <h3><%= l(:button_update) %></h3>
106 <h3><%= l(:button_update) %></h3>
107 <%= render :partial => 'edit' %>
107 <%= render :partial => 'edit' %>
108 </div>
108 </div>
109 <% end %>
109 <% end %>
110
110
111 <% other_formats_links do |f| %>
111 <% other_formats_links do |f| %>
112 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
112 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
113 <%= f.link_to 'PDF' %>
113 <%= f.link_to 'PDF' %>
114 <% end %>
114 <% end %>
115
115
116 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
116 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
117
117
118 <% content_for :sidebar do %>
118 <% content_for :sidebar do %>
119 <%= render :partial => 'issues/sidebar' %>
119 <%= render :partial => 'issues/sidebar' %>
120 <% end %>
120 <% end %>
121
121
122 <% content_for :header_tags do %>
122 <% content_for :header_tags do %>
123 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
123 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
124 <%= stylesheet_link_tag 'scm' %>
124 <%= stylesheet_link_tag 'scm' %>
125 <% end %>
125 <% end %>
@@ -1,3 +1,3
1 <%= l(:text_issue_added, "##{@issue.id}", @issue.author) %>
1 <%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %>
2 <hr />
2 <hr />
3 <%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %>
3 <%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %>
@@ -1,4 +1,4
1 <%= l(:text_issue_added, "##{@issue.id}", @issue.author) %>
1 <%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %>
2
2
3 ----------------------------------------
3 ----------------------------------------
4 <%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %>
4 <%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %>
@@ -1,11 +1,11
1 <%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %>
1 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
2
2
3 <ul>
3 <ul>
4 <% for detail in @journal.details %>
4 <% for detail in @journal.details %>
5 <li><%= show_detail(detail, true) %></li>
5 <li><%= show_detail(detail, true) %></li>
6 <% end %>
6 <% end %>
7 </ul>
7 </ul>
8
8
9 <%= textilizable(@journal, :notes, :only_path => false) %>
9 <%= textilizable(@journal, :notes, :only_path => false) %>
10 <hr />
10 <hr />
11 <%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %>
11 <%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %>
@@ -1,9 +1,9
1 <%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %>
1 <%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
2
2
3 <% for detail in @journal.details -%>
3 <% for detail in @journal.details -%>
4 <%= show_detail(detail, true) %>
4 <%= show_detail(detail, true) %>
5 <% end -%>
5 <% end -%>
6
6
7 <%= @journal.notes if @journal.notes? %>
7 <%= @journal.notes if @journal.notes? %>
8 ----------------------------------------
8 ----------------------------------------
9 <%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %>
9 <%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %>
@@ -1,9 +1,9
1 <p><%= l(:mail_body_reminder, @issues.size, @days) %></p>
1 <p><%= l(:mail_body_reminder, :count => @issues.size, :days => @days) %></p>
2
2
3 <ul>
3 <ul>
4 <% @issues.each do |issue| -%>
4 <% @issues.each do |issue| -%>
5 <li><%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %></li>
5 <li><%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %></li>
6 <% end -%>
6 <% end -%>
7 </ul>
7 </ul>
8
8
9 <p><%= link_to l(:label_issue_view_all), @issues_url %></p>
9 <p><%= link_to l(:label_issue_view_all), @issues_url %></p>
@@ -1,7 +1,7
1 <%= l(:mail_body_reminder, @issues.size, @days) %>:
1 <%= l(:mail_body_reminder, :count => @issues.size, :days => @days) %>:
2
2
3 <% @issues.each do |issue| -%>
3 <% @issues.each do |issue| -%>
4 * <%= "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %>
4 * <%= "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %>
5 <% end -%>
5 <% end -%>
6
6
7 <%= @issues_url %>
7 <%= @issues_url %>
@@ -1,10 +1,11
1 <h3><%=l(:label_watched_issues)%></h3>
1 <h3><%=l(:label_watched_issues)%> (<%= Issue.visible.count(:include => :watchers,
2 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]) %>)</h3>
2 <% watched_issues = Issue.visible.find(:all,
3 <% watched_issues = Issue.visible.find(:all,
3 :include => [:status, :project, :tracker, :watchers],
4 :include => [:status, :project, :tracker, :watchers],
4 :limit => 10,
5 :limit => 10,
5 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id],
6 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id],
6 :order => "#{Issue.table_name}.updated_on DESC") %>
7 :order => "#{Issue.table_name}.updated_on DESC") %>
7 <%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %>
8 <%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %>
8 <% if watched_issues.length > 0 %>
9 <% if watched_issues.length > 0 %>
9 <p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :watcher_id => 'me' %></p>
10 <p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :watcher_id => 'me' %></p>
10 <% end %>
11 <% end %>
@@ -1,6 +1,6
1 <p><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless @project %>
1 <p><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless @project %>
2 <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
2 <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
3 <%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %>
3 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %>
4 <br />
4 <br />
5 <% unless news.summary.blank? %><span class="summary"><%=h news.summary %></span><br /><% end %>
5 <% unless news.summary.blank? %><span class="summary"><%=h news.summary %></span><br /><% end %>
6 <span class="author"><%= authoring news.created_on, news.author %></span></p>
6 <span class="author"><%= authoring news.created_on, news.author %></span></p>
@@ -1,50 +1,50
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized(l(:label_news_new),
2 <%= link_to_if_authorized(l(:label_news_new),
3 {:controller => 'news', :action => 'new', :project_id => @project},
3 {:controller => 'news', :action => 'new', :project_id => @project},
4 :class => 'icon icon-add',
4 :class => 'icon icon-add',
5 :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project %>
5 :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project %>
6 </div>
6 </div>
7
7
8 <div id="add-news" style="display:none;">
8 <div id="add-news" style="display:none;">
9 <h2><%=l(:label_news_new)%></h2>
9 <h2><%=l(:label_news_new)%></h2>
10 <% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project },
10 <% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project },
11 :html => { :id => 'news-form' } do |f| %>
11 :html => { :id => 'news-form' } do |f| %>
12 <%= render :partial => 'news/form', :locals => { :f => f } %>
12 <%= render :partial => 'news/form', :locals => { :f => f } %>
13 <%= submit_tag l(:button_create) %>
13 <%= submit_tag l(:button_create) %>
14 <%= link_to_remote l(:label_preview),
14 <%= link_to_remote l(:label_preview),
15 { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
15 { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
16 :method => 'post',
16 :method => 'post',
17 :update => 'preview',
17 :update => 'preview',
18 :with => "Form.serialize('news-form')"
18 :with => "Form.serialize('news-form')"
19 }, :accesskey => accesskey(:preview) %> |
19 }, :accesskey => accesskey(:preview) %> |
20 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %>
20 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %>
21 <% end if @project %>
21 <% end if @project %>
22 <div id="preview" class="wiki"></div>
22 <div id="preview" class="wiki"></div>
23 </div>
23 </div>
24
24
25 <h2><%=l(:label_news_plural)%></h2>
25 <h2><%=l(:label_news_plural)%></h2>
26
26
27 <% if @newss.empty? %>
27 <% if @newss.empty? %>
28 <p class="nodata"><%= l(:label_no_data) %></p>
28 <p class="nodata"><%= l(:label_no_data) %></p>
29 <% else %>
29 <% else %>
30 <% @newss.each do |news| %>
30 <% @newss.each do |news| %>
31 <h3><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless news.project == @project %>
31 <h3><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless news.project == @project %>
32 <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
32 <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
33 <%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %></h3>
33 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
34 <p class="author"><%= authoring news.created_on, news.author %></p>
34 <p class="author"><%= authoring news.created_on, news.author %></p>
35 <div class="wiki">
35 <div class="wiki">
36 <%= textilizable(news.description) %>
36 <%= textilizable(news.description) %>
37 </div>
37 </div>
38 <% end %>
38 <% end %>
39 <% end %>
39 <% end %>
40 <p class="pagination"><%= pagination_links_full @news_pages %></p>
40 <p class="pagination"><%= pagination_links_full @news_pages %></p>
41
41
42 <% other_formats_links do |f| %>
42 <% other_formats_links do |f| %>
43 <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
43 <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
44 <% end %>
44 <% end %>
45
45
46 <% content_for :header_tags do %>
46 <% content_for :header_tags do %>
47 <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
47 <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
48 <% end %>
48 <% end %>
49
49
50 <% html_title(l(:label_news_plural)) -%>
50 <% html_title(l(:label_news_plural)) -%>
@@ -1,49 +1,49
1 <%= error_messages_for 'project' %>
1 <%= error_messages_for 'project' %>
2
2
3 <div class="box">
3 <div class="box">
4 <!--[form:project]-->
4 <!--[form:project]-->
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
5 <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
6
6
7 <% if User.current.admin? && !@project.possible_parents.empty? %>
7 <% if User.current.admin? && !@project.possible_parents.empty? %>
8 <p><label><%= l(:field_parent) %></label><%= parent_project_select_tag(@project) %></p>
8 <p><label><%= l(:field_parent) %></label><%= parent_project_select_tag(@project) %></p>
9 <% end %>
9 <% end %>
10
10
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
11 <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
12 <p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
12 <p><%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
13 <% unless @project.identifier_frozen? %>
14 <br /><em><%= l(:text_length_between, 2, 20) %> <%= l(:text_project_identifier_info) %></em>
14 <br /><em><%= l(:text_length_between, :min => 2, :max => 20) %> <%= l(:text_project_identifier_info) %></em>
15 <% end %></p>
15 <% end %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
16 <p><%= f.text_field :homepage, :size => 60 %></p>
17 <p><%= f.check_box :is_public %></p>
17 <p><%= f.check_box :is_public %></p>
18 <%= wikitoolbar_for 'project_description' %>
18 <%= wikitoolbar_for 'project_description' %>
19
19
20 <% @project.custom_field_values.each do |value| %>
20 <% @project.custom_field_values.each do |value| %>
21 <p><%= custom_field_tag_with_label :project, value %></p>
21 <p><%= custom_field_tag_with_label :project, value %></p>
22 <% end %>
22 <% end %>
23 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
23 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
24 </div>
24 </div>
25
25
26 <% unless @trackers.empty? %>
26 <% unless @trackers.empty? %>
27 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
27 <fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
28 <% @trackers.each do |tracker| %>
28 <% @trackers.each do |tracker| %>
29 <label class="floating">
29 <label class="floating">
30 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
30 <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.include?(tracker) %>
31 <%= tracker %>
31 <%= tracker %>
32 </label>
32 </label>
33 <% end %>
33 <% end %>
34 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
34 <%= hidden_field_tag 'project[tracker_ids][]', '' %>
35 </fieldset>
35 </fieldset>
36 <% end %>
36 <% end %>
37
37
38 <% unless @issue_custom_fields.empty? %>
38 <% unless @issue_custom_fields.empty? %>
39 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
39 <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
40 <% @issue_custom_fields.each do |custom_field| %>
40 <% @issue_custom_fields.each do |custom_field| %>
41 <label class="floating">
41 <label class="floating">
42 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
42 <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
43 <%= custom_field.name %>
43 <%= custom_field.name %>
44 </label>
44 </label>
45 <% end %>
45 <% end %>
46 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
46 <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
47 </fieldset>
47 </fieldset>
48 <% end %>
48 <% end %>
49 <!--[eoform:project]-->
49 <!--[eoform:project]-->
@@ -1,17 +1,17
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %>
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
5
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 <label class="floating">
8 <label class="floating">
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
10 <%= (l_has_string?("project_module_#{m}".to_sym) ? l("project_module_#{m}".to_sym) : m.to_s.humanize) %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 </label>
11 </label>
12 <% end %>
12 <% end %>
13 </fieldset>
13 </fieldset>
14
14
15 <%= submit_tag l(:button_save) %>
15 <%= submit_tag l(:button_save) %>
16 <%= javascript_tag "Form.Element.focus('project_name');" %>
16 <%= javascript_tag "Form.Element.focus('project_name');" %>
17 <% end %>
17 <% end %>
@@ -1,17 +1,17
1 <% form_for :project, @project,
1 <% form_for :project, @project,
2 :url => { :action => 'modules', :id => @project },
2 :url => { :action => 'modules', :id => @project },
3 :html => {:id => 'modules-form'} do |f| %>
3 :html => {:id => 'modules-form'} do |f| %>
4
4
5 <div class=box>
5 <div class=box>
6 <strong><%= l(:text_select_project_modules) %></strong>
6 <strong><%= l(:text_select_project_modules) %></strong>
7
7
8 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 <% Redmine::AccessControl.available_project_modules.each do |m| %>
9 <p><label><%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) -%>
9 <p><label><%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) -%>
10 <%= (l_has_string?("project_module_#{m}".to_sym) ? l("project_module_#{m}".to_sym) : m.to_s.humanize) %></label></p>
10 <%= l_or_humanize(m, :prefix => "project_module_") %></label></p>
11 <% end %>
11 <% end %>
12 </div>
12 </div>
13
13
14 <p><%= check_all_links 'modules-form' %></p>
14 <p><%= check_all_links 'modules-form' %></p>
15 <p><%= submit_tag l(:button_save) %></p>
15 <p><%= submit_tag l(:button_save) %></p>
16
16
17 <% end %>
17 <% end %>
@@ -1,78 +1,79
1 <h2><%=l(:label_overview)%></h2>
1 <h2><%=l(:label_overview)%></h2>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <%= textilizable @project.description %>
4 <%= textilizable @project.description %>
5 <ul>
5 <ul>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
6 <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
7 <% if @subprojects.any? %>
7 <% if @subprojects.any? %>
8 <li><%=l(:label_subproject_plural)%>:
8 <li><%=l(:label_subproject_plural)%>:
9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
9 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
10 <% end %>
10 <% end %>
11 <% @project.custom_values.each do |custom_value| %>
11 <% @project.custom_values.each do |custom_value| %>
12 <% if !custom_value.value.empty? %>
12 <% if !custom_value.value.empty? %>
13 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
13 <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
14 <% end %>
14 <% end %>
15 <% end %>
15 <% end %>
16 </ul>
16 </ul>
17
17
18 <% if User.current.allowed_to?(:view_issues, @project) %>
18 <% if User.current.allowed_to?(:view_issues, @project) %>
19 <div class="box">
19 <div class="box">
20 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
20 <h3 class="icon22 icon22-tracker"><%=l(:label_issue_tracking)%></h3>
21 <ul>
21 <ul>
22 <% for tracker in @trackers %>
22 <% for tracker in @trackers %>
23 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
23 <li><%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project,
24 :set_filter => 1,
24 :set_filter => 1,
25 "tracker_id" => tracker.id %>:
25 "tracker_id" => tracker.id %>:
26 <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %>
26 <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
27 <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %></li>
27 :total => @total_issues_by_tracker[tracker].to_i) %>
28 </li>
28 <% end %>
29 <% end %>
29 </ul>
30 </ul>
30 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
31 <p><%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %></p>
31 </div>
32 </div>
32 <% end %>
33 <% end %>
33 </div>
34 </div>
34
35
35 <div class="splitcontentright">
36 <div class="splitcontentright">
36 <% if @members_by_role.any? %>
37 <% if @members_by_role.any? %>
37 <div class="box">
38 <div class="box">
38 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
39 <h3 class="icon22 icon22-users"><%=l(:label_member_plural)%></h3>
39 <p><% @members_by_role.keys.sort.each do |role| %>
40 <p><% @members_by_role.keys.sort.each do |role| %>
40 <%= role.name %>:
41 <%= role.name %>:
41 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
42 <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %>
42 <br />
43 <br />
43 <% end %></p>
44 <% end %></p>
44 </div>
45 </div>
45 <% end %>
46 <% end %>
46
47
47 <% if @news.any? && authorize_for('news', 'index') %>
48 <% if @news.any? && authorize_for('news', 'index') %>
48 <div class="box">
49 <div class="box">
49 <h3><%=l(:label_news_latest)%></h3>
50 <h3><%=l(:label_news_latest)%></h3>
50 <%= render :partial => 'news/news', :collection => @news %>
51 <%= render :partial => 'news/news', :collection => @news %>
51 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
52 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
52 </div>
53 </div>
53 <% end %>
54 <% end %>
54 </div>
55 </div>
55
56
56 <% content_for :sidebar do %>
57 <% content_for :sidebar do %>
57 <% planning_links = []
58 <% planning_links = []
58 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
59 planning_links << link_to_if_authorized(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project)
59 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
60 planning_links << link_to_if_authorized(l(:label_gantt), :controller => 'issues', :action => 'gantt', :project_id => @project)
60 planning_links.compact!
61 planning_links.compact!
61 unless planning_links.empty? %>
62 unless planning_links.empty? %>
62 <h3><%= l(:label_planning) %></h3>
63 <h3><%= l(:label_planning) %></h3>
63 <p><%= planning_links.join(' | ') %></p>
64 <p><%= planning_links.join(' | ') %></p>
64 <% end %>
65 <% end %>
65
66
66 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
67 <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
67 <h3><%= l(:label_spent_time) %></h3>
68 <h3><%= l(:label_spent_time) %></h3>
68 <p><span class="icon icon-time"><%= lwr(:label_f_hour, @total_hours) %></span></p>
69 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
69 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
70 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> |
70 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
71 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
71 <% end %>
72 <% end %>
72 <% end %>
73 <% end %>
73
74
74 <% content_for :header_tags do %>
75 <% content_for :header_tags do %>
75 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
76 <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
76 <% end %>
77 <% end %>
77
78
78 <% html_title(l(:label_overview)) -%>
79 <% html_title(l(:label_overview)) -%>
@@ -1,24 +1,24
1 <% @entries.each do |entry| %>
1 <% @entries.each do |entry| %>
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 depth = params[:depth].to_i %>
3 depth = params[:depth].to_i %>
4 <tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry <%= entry.kind %>">
4 <tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry <%= entry.kind %>">
5 <td style="padding-left: <%=18 * depth%>px;" class="filename">
5 <td style="padding-left: <%=18 * depth%>px;" class="filename">
6 <% if entry.is_dir? %>
6 <% if entry.is_dir? %>
7 <span class="expander" onclick="<%= remote_function :url => {:action => 'browse', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
7 <span class="expander" onclick="<%= remote_function :url => {:action => 'browse', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
8 :update => { :success => tr_id },
8 :update => { :success => tr_id },
9 :position => :after,
9 :position => :after,
10 :success => "scmEntryLoaded('#{tr_id}')",
10 :success => "scmEntryLoaded('#{tr_id}')",
11 :condition => "scmEntryClick('#{tr_id}')"%>">&nbsp</span>
11 :condition => "scmEntryClick('#{tr_id}')"%>">&nbsp</span>
12 <% end %>
12 <% end %>
13 <%= link_to h(entry.name),
13 <%= link_to h(entry.name),
14 {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev},
14 {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev},
15 :class => (entry.is_dir? ? 'icon icon-folder' : 'icon icon-file')%>
15 :class => (entry.is_dir? ? 'icon icon-folder' : 'icon icon-file')%>
16 </td>
16 </td>
17 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
17 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
18 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
18 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
19 <td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
19 <td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
20 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
20 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
21 <td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
21 <td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
22 <td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td>
22 <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
23 </tr>
23 </tr>
24 <% end %>
24 <% end %>
@@ -1,51 +1,51
1 <h2><%= l(:label_search) %></h2>
1 <h2><%= l(:label_search) %></h2>
2
2
3 <div class="box">
3 <div class="box">
4 <% form_tag({}, :method => :get) do %>
4 <% form_tag({}, :method => :get) do %>
5 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
5 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
6 <%= javascript_tag "Field.focus('search-input')" %>
6 <%= javascript_tag "Field.focus('search-input')" %>
7 <%= project_select_tag %>
7 <%= project_select_tag %>
8 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
8 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
9 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
9 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
10 </p>
10 </p>
11 <p>
11 <p>
12 <% @object_types.each do |t| %>
12 <% @object_types.each do |t| %>
13 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
13 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
14 <% end %>
14 <% end %>
15 </p>
15 </p>
16
16
17 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
17 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
18 <% end %>
18 <% end %>
19 </div>
19 </div>
20
20
21 <% if @results %>
21 <% if @results %>
22 <div id="search-results-counts">
22 <div id="search-results-counts">
23 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
23 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
24 </div>
24 </div>
25
25
26 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
26 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
27 <dl id="search-results">
27 <dl id="search-results">
28 <% @results.each do |e| %>
28 <% @results.each do |e| %>
29 <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %></dt>
29 <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), e.event_url %></dt>
30 <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
30 <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
31 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
31 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
32 <% end %>
32 <% end %>
33 </dl>
33 </dl>
34 <% end %>
34 <% end %>
35
35
36 <p><center>
36 <p><center>
37 <% if @pagination_previous_date %>
37 <% if @pagination_previous_date %>
38 <%= link_to_remote ('&#171; ' + l(:label_previous)),
38 <%= link_to_remote ('&#171; ' + l(:label_previous)),
39 {:update => :content,
39 {:update => :content,
40 :url => params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))
40 :url => params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))
41 }, :href => url_for(params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
41 }, :href => url_for(params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
42 <% end %>
42 <% end %>
43 <% if @pagination_next_date %>
43 <% if @pagination_next_date %>
44 <%= link_to_remote (l(:label_next) + ' &#187;'),
44 <%= link_to_remote (l(:label_next) + ' &#187;'),
45 {:update => :content,
45 {:update => :content,
46 :url => params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))
46 :url => params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))
47 }, :href => url_for(params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
47 }, :href => url_for(params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
48 <% end %>
48 <% end %>
49 </center></p>
49 </center></p>
50
50
51 <% html_title(l(:label_search)) -%>
51 <% html_title(l(:label_search)) -%>
@@ -1,30 +1,30
1 <% form_tag({:action => 'edit', :tab => 'authentication'}) do %>
1 <% form_tag({:action => 'edit', :tab => 'authentication'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><label><%= l(:setting_login_required) %></label>
4 <p><label><%= l(:setting_login_required) %></label>
5 <%= check_box_tag 'settings[login_required]', 1, Setting.login_required? %><%= hidden_field_tag 'settings[login_required]', 0 %></p>
5 <%= check_box_tag 'settings[login_required]', 1, Setting.login_required? %><%= hidden_field_tag 'settings[login_required]', 0 %></p>
6
6
7 <p><label><%= l(:setting_autologin) %></label>
7 <p><label><%= l(:setting_autologin) %></label>
8 <%= select_tag 'settings[autologin]', options_for_select( [[l(:label_disabled), "0"]] + [1, 7, 30, 365].collect{|days| [lwr(:actionview_datehelper_time_in_words_day, days), days.to_s]}, Setting.autologin) %></p>
8 <%= select_tag 'settings[autologin]', options_for_select( [[l(:label_disabled), "0"]] + [1, 7, 30, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), days.to_s]}, Setting.autologin) %></p>
9
9
10 <p><label><%= l(:setting_self_registration) %></label>
10 <p><label><%= l(:setting_self_registration) %></label>
11 <%= select_tag 'settings[self_registration]',
11 <%= select_tag 'settings[self_registration]',
12 options_for_select( [[l(:label_disabled), "0"],
12 options_for_select( [[l(:label_disabled), "0"],
13 [l(:label_registration_activation_by_email), "1"],
13 [l(:label_registration_activation_by_email), "1"],
14 [l(:label_registration_manual_activation), "2"],
14 [l(:label_registration_manual_activation), "2"],
15 [l(:label_registration_automatic_activation), "3"]
15 [l(:label_registration_automatic_activation), "3"]
16 ], Setting.self_registration ) %></p>
16 ], Setting.self_registration ) %></p>
17
17
18 <p><label><%= l(:label_password_lost) %></label>
18 <p><label><%= l(:label_password_lost) %></label>
19 <%= check_box_tag 'settings[lost_password]', 1, Setting.lost_password? %><%= hidden_field_tag 'settings[lost_password]', 0 %></p>
19 <%= check_box_tag 'settings[lost_password]', 1, Setting.lost_password? %><%= hidden_field_tag 'settings[lost_password]', 0 %></p>
20
20
21 <p><label><%= l(:setting_openid) %></label>
21 <p><label><%= l(:setting_openid) %></label>
22 <%= check_box_tag 'settings[openid]', 1, Setting.openid?, :disabled => !Object.const_defined?(:OpenID) %><%= hidden_field_tag 'settings[openid]', 0 %></p>
22 <%= check_box_tag 'settings[openid]', 1, Setting.openid?, :disabled => !Object.const_defined?(:OpenID) %><%= hidden_field_tag 'settings[openid]', 0 %></p>
23 </div>
23 </div>
24
24
25 <div style="float:right;">
25 <div style="float:right;">
26 <%= link_to l(:label_ldap_authentication), :controller => 'auth_sources', :action => 'list' %>
26 <%= link_to l(:label_ldap_authentication), :controller => 'auth_sources', :action => 'list' %>
27 </div>
27 </div>
28
28
29 <%= submit_tag l(:button_save) %>
29 <%= submit_tag l(:button_save) %>
30 <% end %>
30 <% end %>
@@ -1,41 +1,41
1 <table class="list time-entries">
1 <table class="list time-entries">
2 <thead>
2 <thead>
3 <tr>
3 <tr>
4 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
4 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
5 <%= sort_header_tag('user', :caption => l(:label_member)) %>
5 <%= sort_header_tag('user', :caption => l(:label_member)) %>
6 <%= sort_header_tag('activity', :caption => l(:label_activity)) %>
6 <%= sort_header_tag('activity', :caption => l(:label_activity)) %>
7 <%= sort_header_tag('project', :caption => l(:label_project)) %>
7 <%= sort_header_tag('project', :caption => l(:label_project)) %>
8 <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
8 <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
9 <th><%= l(:field_comments) %></th>
9 <th><%= l(:field_comments) %></th>
10 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
10 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
11 <th></th>
11 <th></th>
12 </tr>
12 </tr>
13 </thead>
13 </thead>
14 <tbody>
14 <tbody>
15 <% entries.each do |entry| -%>
15 <% entries.each do |entry| -%>
16 <tr class="time-entry <%= cycle("odd", "even") %>">
16 <tr class="time-entry <%= cycle("odd", "even") %>">
17 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
17 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
18 <td class="user"><%=h entry.user %></td>
18 <td class="user"><%=h entry.user %></td>
19 <td class="activity"><%=h entry.activity %></td>
19 <td class="activity"><%=h entry.activity %></td>
20 <td class="project"><%=h entry.project %></td>
20 <td class="project"><%=h entry.project %></td>
21 <td class="subject">
21 <td class="subject">
22 <% if entry.issue -%>
22 <% if entry.issue -%>
23 <%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%>
23 <%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, :length => 50)) -%>
24 <% end -%>
24 <% end -%>
25 </td>
25 </td>
26 <td class="comments"><%=h entry.comments %></td>
26 <td class="comments"><%=h entry.comments %></td>
27 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
27 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
28 <td align="center">
28 <td align="center">
29 <% if entry.editable_by?(User.current) -%>
29 <% if entry.editable_by?(User.current) -%>
30 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
30 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
31 :title => l(:button_edit) %>
31 :title => l(:button_edit) %>
32 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
32 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
33 :confirm => l(:text_are_you_sure),
33 :confirm => l(:text_are_you_sure),
34 :method => :post,
34 :method => :post,
35 :title => l(:button_delete) %>
35 :title => l(:button_delete) %>
36 <% end -%>
36 <% end -%>
37 </td>
37 </td>
38 </tr>
38 </tr>
39 <% end -%>
39 <% end -%>
40 </tbody>
40 </tbody>
41 </table>
41 </table>
@@ -1,35 +1,35
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 </div>
3 </div>
4
4
5 <%= render_timelog_breadcrumb %>
5 <%= render_timelog_breadcrumb %>
6
6
7 <h2><%= l(:label_spent_time) %></h2>
7 <h2><%= l(:label_spent_time) %></h2>
8
8
9 <% form_remote_tag( :url => {}, :html => {:method => :get}, :method => :get, :update => 'content' ) do %>
9 <% form_remote_tag( :url => {}, :html => {:method => :get}, :method => :get, :update => 'content' ) do %>
10 <%# TOOD: remove the project_id and issue_id hidden fields, that information is
10 <%# TOOD: remove the project_id and issue_id hidden fields, that information is
11 already in the URI %>
11 already in the URI %>
12 <%= hidden_field_tag 'project_id', params[:project_id] %>
12 <%= hidden_field_tag 'project_id', params[:project_id] %>
13 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
13 <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
14 <%= render :partial => 'date_range' %>
14 <%= render :partial => 'date_range' %>
15 <% end %>
15 <% end %>
16
16
17 <div class="total-hours">
17 <div class="total-hours">
18 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
18 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
19 </div>
19 </div>
20
20
21 <% unless @entries.empty? %>
21 <% unless @entries.empty? %>
22 <%= render :partial => 'list', :locals => { :entries => @entries }%>
22 <%= render :partial => 'list', :locals => { :entries => @entries }%>
23 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
23 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
24
24
25 <% other_formats_links do |f| %>
25 <% other_formats_links do |f| %>
26 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
26 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
27 <%= f.link_to 'CSV', :url => params %>
27 <%= f.link_to 'CSV', :url => params %>
28 <% end %>
28 <% end %>
29 <% end %>
29 <% end %>
30
30
31 <% html_title l(:label_spent_time), l(:label_details) %>
31 <% html_title l(:label_spent_time), l(:label_details) %>
32
32
33 <% content_for :header_tags do %>
33 <% content_for :header_tags do %>
34 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
34 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
35 <% end %>
35 <% end %>
@@ -1,75 +1,75
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 </div>
3 </div>
4
4
5 <%= render_timelog_breadcrumb %>
5 <%= render_timelog_breadcrumb %>
6
6
7 <h2><%= l(:label_spent_time) %></h2>
7 <h2><%= l(:label_spent_time) %></h2>
8
8
9 <% form_remote_tag(:url => {}, :html => {:method => :get}, :method => :get, :update => 'content') do %>
9 <% form_remote_tag(:url => {}, :html => {:method => :get}, :method => :get, :update => 'content') do %>
10 <% @criterias.each do |criteria| %>
10 <% @criterias.each do |criteria| %>
11 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
11 <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
12 <% end %>
12 <% end %>
13 <%# TODO: get rid of the project_id field, that should already be in the URL %>
13 <%# TODO: get rid of the project_id field, that should already be in the URL %>
14 <%= hidden_field_tag 'project_id', params[:project_id] %>
14 <%= hidden_field_tag 'project_id', params[:project_id] %>
15 <%= render :partial => 'date_range' %>
15 <%= render :partial => 'date_range' %>
16
16
17 <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
17 <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
18 [l(:label_month), 'month'],
18 [l(:label_month), 'month'],
19 [l(:label_week), 'week'],
19 [l(:label_week), 'week'],
20 [l(:label_day_plural).titleize, 'day']], @columns),
20 [l(:label_day_plural).titleize, 'day']], @columns),
21 :onchange => "this.form.onsubmit();" %>
21 :onchange => "this.form.onsubmit();" %>
22
22
23 <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
23 <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l_or_humanize(@available_criterias[k][:label]), k]}),
24 :onchange => "this.form.onsubmit();",
24 :onchange => "this.form.onsubmit();",
25 :style => 'width: 200px',
25 :style => 'width: 200px',
26 :id => nil,
26 :id => nil,
27 :disabled => (@criterias.length >= 3)) %>
27 :disabled => (@criterias.length >= 3)) %>
28 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
28 <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
29 :method => :get,
29 :method => :get,
30 :update => 'content'
30 :update => 'content'
31 }, :class => 'icon icon-reload' %></p>
31 }, :class => 'icon icon-reload' %></p>
32 <% end %>
32 <% end %>
33
33
34 <% unless @criterias.empty? %>
34 <% unless @criterias.empty? %>
35 <div class="total-hours">
35 <div class="total-hours">
36 <p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
36 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
37 </div>
37 </div>
38
38
39 <% unless @hours.empty? %>
39 <% unless @hours.empty? %>
40 <table class="list" id="time-report">
40 <table class="list" id="time-report">
41 <thead>
41 <thead>
42 <tr>
42 <tr>
43 <% @criterias.each do |criteria| %>
43 <% @criterias.each do |criteria| %>
44 <th><%= l(@available_criterias[criteria][:label]) %></th>
44 <th><%= l_or_humanize(@available_criterias[criteria][:label]) %></th>
45 <% end %>
45 <% end %>
46 <% columns_width = (40 / (@periods.length+1)).to_i %>
46 <% columns_width = (40 / (@periods.length+1)).to_i %>
47 <% @periods.each do |period| %>
47 <% @periods.each do |period| %>
48 <th class="period" width="<%= columns_width %>%"><%= period %></th>
48 <th class="period" width="<%= columns_width %>%"><%= period %></th>
49 <% end %>
49 <% end %>
50 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
50 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
51 </tr>
51 </tr>
52 </thead>
52 </thead>
53 <tbody>
53 <tbody>
54 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
54 <%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %>
55 <tr class="total">
55 <tr class="total">
56 <td><%= l(:label_total) %></td>
56 <td><%= l(:label_total) %></td>
57 <%= '<td></td>' * (@criterias.size - 1) %>
57 <%= '<td></td>' * (@criterias.size - 1) %>
58 <% total = 0 -%>
58 <% total = 0 -%>
59 <% @periods.each do |period| -%>
59 <% @periods.each do |period| -%>
60 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
60 <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
61 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
61 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
62 <% end -%>
62 <% end -%>
63 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
63 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
64 </tr>
64 </tr>
65 </tbody>
65 </tbody>
66 </table>
66 </table>
67
67
68 <% other_formats_links do |f| %>
68 <% other_formats_links do |f| %>
69 <%= f.link_to 'CSV', :url => params %>
69 <%= f.link_to 'CSV', :url => params %>
70 <% end %>
70 <% end %>
71 <% end %>
71 <% end %>
72 <% end %>
72 <% end %>
73
73
74 <% html_title l(:label_spent_time), l(:label_report) %>
74 <% html_title l(:label_spent_time), l(:label_report) %>
75
75
@@ -1,22 +1,20
1 <% if version.completed? %>
1 <% if version.completed? %>
2 <p><%= format_date(version.effective_date) %></p>
2 <p><%= format_date(version.effective_date) %></p>
3 <% elsif version.effective_date %>
3 <% elsif version.effective_date %>
4 <p><strong><%= due_date_distance_in_words(version.effective_date) %></strong> (<%= format_date(version.effective_date) %>)</p>
4 <p><strong><%= due_date_distance_in_words(version.effective_date) %></strong> (<%= format_date(version.effective_date) %>)</p>
5 <% end %>
5 <% end %>
6
6
7 <p><%=h version.description %></p>
7 <p><%=h version.description %></p>
8
8
9 <% if version.fixed_issues.count > 0 %>
9 <% if version.fixed_issues.count > 0 %>
10 <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %>
10 <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %>
11 <p class="progress-info">
11 <p class="progress-info">
12 <%= link_to(version.closed_issues_count, :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %>
12 <%= link_to_if(version.closed_issues_count > 0, l(:label_x_closed_issues_abbr, :count => version.closed_issues_count), :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %>
13 <%= lwr(:label_closed_issues, version.closed_issues_count) %>
14 (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%)
13 (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%)
15 &#160;
14 &#160;
16 <%= link_to(version.open_issues_count, :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'o', :fixed_version_id => version, :set_filter => 1) %>
15 <%= link_to_if(version.open_issues_count > 0, l(:label_x_open_issues_abbr, :count => version.open_issues_count), :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'o', :fixed_version_id => version, :set_filter => 1) %>
17 <%= lwr(:label_open_issues, version.open_issues_count)%>
18 (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%)
16 (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%)
19 </p>
17 </p>
20 <% else %>
18 <% else %>
21 <p><em><%= l(:label_roadmap_no_issues) %></em></p>
19 <p><em><%= l(:label_roadmap_no_issues) %></em></p>
22 <% end %>
20 <% end %>
@@ -1,50 +1,50
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %>
2 <%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %>
3 </div>
3 </div>
4
4
5 <h2><%= h(@version.name) %></h2>
5 <h2><%= h(@version.name) %></h2>
6
6
7 <div id="version-summary">
7 <div id="version-summary">
8 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
8 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
9 <fieldset><legend><%= l(:label_time_tracking) %></legend>
9 <fieldset><legend><%= l(:label_time_tracking) %></legend>
10 <table>
10 <table>
11 <tr>
11 <tr>
12 <td width="130px" align="right"><%= l(:field_estimated_hours) %></td>
12 <td width="130px" align="right"><%= l(:field_estimated_hours) %></td>
13 <td width="240px" class="total-hours"width="130px" align="right"><%= html_hours(lwr(:label_f_hour, @version.estimated_hours)) %></td>
13 <td width="240px" class="total-hours"width="130px" align="right"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
14 </tr>
14 </tr>
15 <% if User.current.allowed_to?(:view_time_entries, @project) %>
15 <% if User.current.allowed_to?(:view_time_entries, @project) %>
16 <tr>
16 <tr>
17 <td width="130px" align="right"><%= l(:label_spent_time) %></td>
17 <td width="130px" align="right"><%= l(:label_spent_time) %></td>
18 <td width="240px" class="total-hours"><%= html_hours(lwr(:label_f_hour, @version.spent_hours)) %></td>
18 <td width="240px" class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td>
19 </tr>
19 </tr>
20 <% end %>
20 <% end %>
21 </table>
21 </table>
22 </fieldset>
22 </fieldset>
23 <% end %>
23 <% end %>
24
24
25 <div id="status_by">
25 <div id="status_by">
26 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
26 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
27 </div>
27 </div>
28 </div>
28 </div>
29
29
30 <div id="roadmap">
30 <div id="roadmap">
31 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
31 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
32 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
32 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
33
33
34 <% issues = @version.fixed_issues.find(:all,
34 <% issues = @version.fixed_issues.find(:all,
35 :include => [:status, :tracker],
35 :include => [:status, :tracker],
36 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
36 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
37 <% if issues.size > 0 %>
37 <% if issues.size > 0 %>
38 <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
38 <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
39 <ul>
39 <ul>
40 <% issues.each do |issue| -%>
40 <% issues.each do |issue| -%>
41 <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
41 <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
42 <% end -%>
42 <% end -%>
43 </ul>
43 </ul>
44 </fieldset>
44 </fieldset>
45 <% end %>
45 <% end %>
46 </div>
46 </div>
47
47
48 <%= call_hook :view_versions_show_bottom, :version => @version %>
48 <%= call_hook :view_versions_show_bottom, :version => @version %>
49
49
50 <% html_title @version.name %>
50 <% html_title @version.name %>
@@ -1,109 +1,109
1 # Don't change this file!
1 # Don't change this file!
2 # Configure your app in config/environment.rb and config/environments/*.rb
2 # Configure your app in config/environment.rb and config/environments/*.rb
3
3
4 RAILS_ROOT = File.expand_path("#{File.dirname(__FILE__)}/..") unless defined?(RAILS_ROOT)
4 RAILS_ROOT = File.expand_path("#{File.dirname(__FILE__)}/..") unless defined?(RAILS_ROOT)
5
5
6 module Rails
6 module Rails
7 class << self
7 class << self
8 def boot!
8 def boot!
9 unless booted?
9 unless booted?
10 preinitialize
10 preinitialize
11 pick_boot.run
11 pick_boot.run
12 end
12 end
13 end
13 end
14
14
15 def booted?
15 def booted?
16 defined? Rails::Initializer
16 defined? Rails::Initializer
17 end
17 end
18
18
19 def pick_boot
19 def pick_boot
20 (vendor_rails? ? VendorBoot : GemBoot).new
20 (vendor_rails? ? VendorBoot : GemBoot).new
21 end
21 end
22
22
23 def vendor_rails?
23 def vendor_rails?
24 File.exist?("#{RAILS_ROOT}/vendor/rails")
24 File.exist?("#{RAILS_ROOT}/vendor/rails")
25 end
25 end
26
26
27 def preinitialize
27 def preinitialize
28 load(preinitializer_path) if File.exist?(preinitializer_path)
28 load(preinitializer_path) if File.exist?(preinitializer_path)
29 end
29 end
30
30
31 def preinitializer_path
31 def preinitializer_path
32 "#{RAILS_ROOT}/config/preinitializer.rb"
32 "#{RAILS_ROOT}/config/preinitializer.rb"
33 end
33 end
34 end
34 end
35
35
36 class Boot
36 class Boot
37 def run
37 def run
38 load_initializer
38 load_initializer
39 Rails::Initializer.run(:set_load_path)
39 Rails::Initializer.run(:set_load_path)
40 end
40 end
41 end
41 end
42
42
43 class VendorBoot < Boot
43 class VendorBoot < Boot
44 def load_initializer
44 def load_initializer
45 require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
45 require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
46 Rails::Initializer.run(:install_gem_spec_stubs)
46 Rails::Initializer.run(:install_gem_spec_stubs)
47 end
47 end
48 end
48 end
49
49
50 class GemBoot < Boot
50 class GemBoot < Boot
51 def load_initializer
51 def load_initializer
52 self.class.load_rubygems
52 self.class.load_rubygems
53 load_rails_gem
53 load_rails_gem
54 require 'initializer'
54 require 'initializer'
55 end
55 end
56
56
57 def load_rails_gem
57 def load_rails_gem
58 if version = self.class.gem_version
58 if version = self.class.gem_version
59 gem 'rails', version
59 gem 'rails', version
60 else
60 else
61 gem 'rails'
61 gem 'rails'
62 end
62 end
63 rescue Gem::LoadError => load_error
63 rescue Gem::LoadError => load_error
64 $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
64 $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
65 exit 1
65 exit 1
66 end
66 end
67
67
68 class << self
68 class << self
69 def rubygems_version
69 def rubygems_version
70 Gem::RubyGemsVersion if defined? Gem::RubyGemsVersion
70 Gem::RubyGemsVersion rescue nil
71 end
71 end
72
72
73 def gem_version
73 def gem_version
74 if defined? RAILS_GEM_VERSION
74 if defined? RAILS_GEM_VERSION
75 RAILS_GEM_VERSION
75 RAILS_GEM_VERSION
76 elsif ENV.include?('RAILS_GEM_VERSION')
76 elsif ENV.include?('RAILS_GEM_VERSION')
77 ENV['RAILS_GEM_VERSION']
77 ENV['RAILS_GEM_VERSION']
78 else
78 else
79 parse_gem_version(read_environment_rb)
79 parse_gem_version(read_environment_rb)
80 end
80 end
81 end
81 end
82
82
83 def load_rubygems
83 def load_rubygems
84 require 'rubygems'
84 require 'rubygems'
85
85 min_version = '1.3.1'
86 unless rubygems_version >= '0.9.4'
86 unless rubygems_version >= min_version
87 $stderr.puts %(Rails requires RubyGems >= 0.9.4 (you have #{rubygems_version}). Please `gem update --system` and try again.)
87 $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
88 exit 1
88 exit 1
89 end
89 end
90
90
91 rescue LoadError
91 rescue LoadError
92 $stderr.puts %(Rails requires RubyGems >= 0.9.4. Please install RubyGems and try again: http://rubygems.rubyforge.org)
92 $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
93 exit 1
93 exit 1
94 end
94 end
95
95
96 def parse_gem_version(text)
96 def parse_gem_version(text)
97 $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
97 $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
98 end
98 end
99
99
100 private
100 private
101 def read_environment_rb
101 def read_environment_rb
102 File.read("#{RAILS_ROOT}/config/environment.rb")
102 File.read("#{RAILS_ROOT}/config/environment.rb")
103 end
103 end
104 end
104 end
105 end
105 end
106 end
106 end
107
107
108 # All that for this:
108 # All that for this:
109 Rails.boot!
109 Rails.boot!
@@ -1,57 +1,52
1 # Be sure to restart your web server when you modify this file.
1 # Be sure to restart your web server when you modify this file.
2
2
3 # Uncomment below to force Rails into production mode when
3 # Uncomment below to force Rails into production mode when
4 # you don't control web/app server and can't set it the proper way
4 # you don't control web/app server and can't set it the proper way
5 # ENV['RAILS_ENV'] ||= 'production'
5 # ENV['RAILS_ENV'] ||= 'production'
6
6
7 # Specifies gem version of Rails to use when vendor/rails is not present
7 # Specifies gem version of Rails to use when vendor/rails is not present
8 RAILS_GEM_VERSION = '2.1.2' unless defined? RAILS_GEM_VERSION
8 RAILS_GEM_VERSION = '2.2.2' unless defined? RAILS_GEM_VERSION
9
9
10 # Bootstrap the Rails environment, frameworks, and default configuration
10 # Bootstrap the Rails environment, frameworks, and default configuration
11 require File.join(File.dirname(__FILE__), 'boot')
11 require File.join(File.dirname(__FILE__), 'boot')
12
12
13 # Load Engine plugin if available
13 # Load Engine plugin if available
14 begin
14 begin
15 require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
15 require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
16 rescue LoadError
16 rescue LoadError
17 # Not available
17 # Not available
18 end
18 end
19
19
20 Rails::Initializer.run do |config|
20 Rails::Initializer.run do |config|
21 # Settings in config/environments/* take precedence those specified here
21 # Settings in config/environments/* take precedence those specified here
22
22
23 # Skip frameworks you're not going to use
23 # Skip frameworks you're not going to use
24 # config.frameworks -= [ :action_web_service, :action_mailer ]
24 # config.frameworks -= [ :action_web_service, :action_mailer ]
25
25
26 # Add additional load paths for sweepers
26 # Add additional load paths for sweepers
27 config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )
27 config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )
28
28
29 # Force all environments to use the same logger level
29 # Force all environments to use the same logger level
30 # (by default production uses :info, the others :debug)
30 # (by default production uses :info, the others :debug)
31 # config.log_level = :debug
31 # config.log_level = :debug
32
32
33 # Use the database for sessions instead of the file system
34 # (create the session table with 'rake db:sessions:create')
35 # config.action_controller.session_store = :active_record_store
36 config.action_controller.session_store = :PStore
37
38 # Enable page/fragment caching by setting a file-based store
33 # Enable page/fragment caching by setting a file-based store
39 # (remember to create the caching directory and make it readable to the application)
34 # (remember to create the caching directory and make it readable to the application)
40 # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
35 # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
41
36
42 # Activate observers that should always be running
37 # Activate observers that should always be running
43 # config.active_record.observers = :cacher, :garbage_collector
38 # config.active_record.observers = :cacher, :garbage_collector
44 config.active_record.observers = :message_observer
39 config.active_record.observers = :message_observer
45
40
46 # Make Active Record use UTC-base instead of local time
41 # Make Active Record use UTC-base instead of local time
47 # config.active_record.default_timezone = :utc
42 # config.active_record.default_timezone = :utc
48
43
49 # Use Active Record's schema dumper instead of SQL when creating the test database
44 # Use Active Record's schema dumper instead of SQL when creating the test database
50 # (enables use of different database adapters for development and test environments)
45 # (enables use of different database adapters for development and test environments)
51 # config.active_record.schema_format = :ruby
46 # config.active_record.schema_format = :ruby
52
47
53 # Deliveries are disabled by default. Do NOT modify this section.
48 # Deliveries are disabled by default. Do NOT modify this section.
54 # Define your email configuration in email.yml instead.
49 # Define your email configuration in email.yml instead.
55 # It will automatically turn deliveries on
50 # It will automatically turn deliveries on
56 config.action_mailer.perform_deliveries = false
51 config.action_mailer.perform_deliveries = false
57 end
52 end
@@ -1,17 +1,22
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2
2
3 # The test environment is used exclusively to run your application's
3 # The test environment is used exclusively to run your application's
4 # test suite. You never need to work with it otherwise. Remember that
4 # test suite. You never need to work with it otherwise. Remember that
5 # your test database is "scratch space" for the test suite and is wiped
5 # your test database is "scratch space" for the test suite and is wiped
6 # and recreated between test runs. Don't rely on the data there!
6 # and recreated between test runs. Don't rely on the data there!
7 config.cache_classes = true
7 config.cache_classes = true
8
8
9 # Log error messages when you accidentally call methods on nil.
9 # Log error messages when you accidentally call methods on nil.
10 config.whiny_nils = true
10 config.whiny_nils = true
11
11
12 # Show full error reports and disable caching
12 # Show full error reports and disable caching
13 config.action_controller.consider_all_requests_local = true
13 config.action_controller.consider_all_requests_local = true
14 config.action_controller.perform_caching = false
14 config.action_controller.perform_caching = false
15
15
16 config.action_mailer.perform_deliveries = true
16 config.action_mailer.perform_deliveries = true
17 config.action_mailer.delivery_method = :test
17 config.action_mailer.delivery_method = :test
18
19 config.action_controller.session = {
20 :session_key => "_test_session",
21 :secret => "some secret phrase for the tests."
22 }
@@ -1,17 +1,22
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2
2
3 # The test environment is used exclusively to run your application's
3 # The test environment is used exclusively to run your application's
4 # test suite. You never need to work with it otherwise. Remember that
4 # test suite. You never need to work with it otherwise. Remember that
5 # your test database is "scratch space" for the test suite and is wiped
5 # your test database is "scratch space" for the test suite and is wiped
6 # and recreated between test runs. Don't rely on the data there!
6 # and recreated between test runs. Don't rely on the data there!
7 config.cache_classes = true
7 config.cache_classes = true
8
8
9 # Log error messages when you accidentally call methods on nil.
9 # Log error messages when you accidentally call methods on nil.
10 config.whiny_nils = true
10 config.whiny_nils = true
11
11
12 # Show full error reports and disable caching
12 # Show full error reports and disable caching
13 config.action_controller.consider_all_requests_local = true
13 config.action_controller.consider_all_requests_local = true
14 config.action_controller.perform_caching = false
14 config.action_controller.perform_caching = false
15
15
16 config.action_mailer.perform_deliveries = true
16 config.action_mailer.perform_deliveries = true
17 config.action_mailer.delivery_method = :test
17 config.action_mailer.delivery_method = :test
18
19 config.action_controller.session = {
20 :session_key => "_test_session",
21 :secret => "some secret phrase for the tests."
22 }
@@ -1,17 +1,22
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2
2
3 # The test environment is used exclusively to run your application's
3 # The test environment is used exclusively to run your application's
4 # test suite. You never need to work with it otherwise. Remember that
4 # test suite. You never need to work with it otherwise. Remember that
5 # your test database is "scratch space" for the test suite and is wiped
5 # your test database is "scratch space" for the test suite and is wiped
6 # and recreated between test runs. Don't rely on the data there!
6 # and recreated between test runs. Don't rely on the data there!
7 config.cache_classes = true
7 config.cache_classes = true
8
8
9 # Log error messages when you accidentally call methods on nil.
9 # Log error messages when you accidentally call methods on nil.
10 config.whiny_nils = true
10 config.whiny_nils = true
11
11
12 # Show full error reports and disable caching
12 # Show full error reports and disable caching
13 config.action_controller.consider_all_requests_local = true
13 config.action_controller.consider_all_requests_local = true
14 config.action_controller.perform_caching = false
14 config.action_controller.perform_caching = false
15
15
16 config.action_mailer.perform_deliveries = true
16 config.action_mailer.perform_deliveries = true
17 config.action_mailer.delivery_method = :test
17 config.action_mailer.delivery_method = :test
18
19 config.action_controller.session = {
20 :session_key => "_test_session",
21 :secret => "some secret phrase for the tests."
22 }
@@ -1,31 +1,50
1
1
2 ActiveRecord::Errors.default_error_messages = {
2 require 'activerecord'
3 :inclusion => "activerecord_error_inclusion",
3
4 :exclusion => "activerecord_error_exclusion",
4 module ActiveRecord
5 :invalid => "activerecord_error_invalid",
5 class Base
6 :confirmation => "activerecord_error_confirmation",
6 include Redmine::I18n
7 :accepted => "activerecord_error_accepted",
7
8 :empty => "activerecord_error_empty",
8 # Translate attribute names for validation errors display
9 :blank => "activerecord_error_blank",
9 def self.human_attribute_name(attr)
10 :too_long => "activerecord_error_too_long",
10 l("field_#{attr.to_s.gsub(/_id$/, '')}")
11 :too_short => "activerecord_error_too_short",
11 end
12 :wrong_length => "activerecord_error_wrong_length",
12 end
13 :taken => "activerecord_error_taken",
13 end
14 :not_a_number => "activerecord_error_not_a_number"
14
15 } if ActiveRecord::Errors.respond_to?('default_error_messages=')
15 module ActionView
16 module Helpers
17 module DateHelper
18 # distance_of_time_in_words breaks when difference is greater than 30 years
19 def distance_of_date_in_words(from_date, to_date = 0, options = {})
20 from_date = from_date.to_date if from_date.respond_to?(:to_date)
21 to_date = to_date.to_date if to_date.respond_to?(:to_date)
22 distance_in_days = (to_date - from_date).abs
23
24 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
25 case distance_in_days
26 when 0..60 then locale.t :x_days, :count => distance_in_days
27 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
28 else locale.t :over_x_years, :count => (distance_in_days / 365).round
29 end
30 end
31 end
32 end
33 end
34 end
16
35
17 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
36 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
18
37
19 # Adds :async_smtp and :async_sendmail delivery methods
38 # Adds :async_smtp and :async_sendmail delivery methods
20 # to perform email deliveries asynchronously
39 # to perform email deliveries asynchronously
21 module AsynchronousMailer
40 module AsynchronousMailer
22 %w(smtp sendmail).each do |type|
41 %w(smtp sendmail).each do |type|
23 define_method("perform_delivery_async_#{type}") do |mail|
42 define_method("perform_delivery_async_#{type}") do |mail|
24 Thread.start do
43 Thread.start do
25 send "perform_delivery_#{type}", mail
44 send "perform_delivery_#{type}", mail
26 end
45 end
27 end
46 end
28 end
47 end
29 end
48 end
30
49
31 ActionMailer::Base.send :include, AsynchronousMailer
50 ActionMailer::Base.send :include, AsynchronousMailer
@@ -1,7 +1,3
1 GLoc.set_config :default_language => :en
1 I18n.default_locale = 'en'
2 GLoc.clear_strings
3 GLoc.set_kcode
4 GLoc.load_localized_strings
5 GLoc.set_config(:raise_string_not_found_errors => false)
6
2
7 require 'redmine'
3 require 'redmine'
This diff has been collapsed as it changes many lines, (1478 lines changed) Show them Hide them
@@ -1,709 +1,773
1 _gloc_rule_default: '|n| n==1 ? "" : "_plural" '
1 bg:
2 date:
3 formats:
4 # Use the strftime parameters for formats.
5 # When no format has been given, it uses default.
6 # You can provide other formats here if you like!
7 default: "%Y-%m-%d"
8 short: "%b %d"
9 long: "%B %d, %Y"
10
11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
13
14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
17 # Used in date_select and datime_select.
18 order: [ :year, :month, :day ]
2
19
3 actionview_datehelper_select_day_prefix:
20 time:
4 actionview_datehelper_select_month_names: Януари,Февруари,Март,Април,Май,Юни,Юли,Август,Септември,Октомври,Ноември,Декември
21 formats:
5 actionview_datehelper_select_month_names_abbr: Яну,Фев,Мар,Апр,Май,Юни,Юли,Авг,Сеп,Окт,Ное,Дек
22 default: "%a, %d %b %Y %H:%M:%S %z"
6 actionview_datehelper_select_month_prefix:
23 short: "%d %b %H:%M"
7 actionview_datehelper_select_year_prefix:
24 long: "%B %d, %Y %H:%M"
8 actionview_datehelper_time_in_words_day: 1 ден
25 am: "am"
9 actionview_datehelper_time_in_words_day_plural: %d дни
26 pm: "pm"
10 actionview_datehelper_time_in_words_hour_about: около час
27
11 actionview_datehelper_time_in_words_hour_about_plural: около %d часа
28 datetime:
12 actionview_datehelper_time_in_words_hour_about_single: около час
29 distance_in_words:
13 actionview_datehelper_time_in_words_minute: 1 минута
30 half_a_minute: "half a minute"
14 actionview_datehelper_time_in_words_minute_half: половин минута
31 less_than_x_seconds:
15 actionview_datehelper_time_in_words_minute_less_than: по-малко от минута
32 one: "less than 1 second"
16 actionview_datehelper_time_in_words_minute_plural: %d минути
33 other: "less than {{count}} seconds"
17 actionview_datehelper_time_in_words_minute_single: 1 минута
34 x_seconds:
18 actionview_datehelper_time_in_words_second_less_than: по-малко от секунда
35 one: "1 second"
19 actionview_datehelper_time_in_words_second_less_than_plural: по-малко от %d секунди
36 other: "{{count}} seconds"
20 actionview_instancetag_blank_option: Изберете
37 less_than_x_minutes:
38 one: "less than a minute"
39 other: "less than {{count}} minutes"
40 x_minutes:
41 one: "1 minute"
42 other: "{{count}} minutes"
43 about_x_hours:
44 one: "about 1 hour"
45 other: "about {{count}} hours"
46 x_days:
47 one: "1 day"
48 other: "{{count}} days"
49 about_x_months:
50 one: "about 1 month"
51 other: "about {{count}} months"
52 x_months:
53 one: "1 month"
54 other: "{{count}} months"
55 about_x_years:
56 one: "about 1 year"
57 other: "about {{count}} years"
58 over_x_years:
59 one: "over 1 year"
60 other: "over {{count}} years"
61
62 # Used in array.to_sentence.
63 support:
64 array:
65 sentence_connector: "and"
66 skip_last_comma: false
67
68 activerecord:
69 errors:
70 messages:
71 inclusion: "не съществува в списъка"
72 exclusion: запазено"
73 invalid: невалидно"
74 confirmation: "липсва одобрение"
75 accepted: "трябва да се приеме"
76 empty: "не може да е празно"
77 blank: "не може да е празно"
78 too_long: прекалено дълго"
79 too_short: прекалено късо"
80 wrong_length: с грешна дължина"
81 taken: "вече съществува"
82 not_a_number: "не е число"
83 not_a_date: невалидна дата"
84 greater_than: "must be greater than {{count}}"
85 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
86 equal_to: "must be equal to {{count}}"
87 less_than: "must be less than {{count}}"
88 less_than_or_equal_to: "must be less than or equal to {{count}}"
89 odd: "must be odd"
90 even: "must be even"
91 greater_than_start_date: "трябва да е след началната дата"
92 not_same_project: "не е от същия проект"
93 circular_dependency: "Тази релация ще доведе до безкрайна зависимост"
21
94
22 activerecord_error_inclusion: не съществува в списъка
95 actionview_instancetag_blank_option: Изберете
23 activerecord_error_exclusion: е запазено
96
24 activerecord_error_invalid: е невалидно
97 general_text_No: 'Не'
25 activerecord_error_confirmation: липсва одобрение
98 general_text_Yes: 'Да'
26 activerecord_error_accepted: трябва да се приеме
99 general_text_no: 'не'
27 activerecord_error_empty: не може да е празно
100 general_text_yes: 'да'
28 activerecord_error_blank: не може да е празно
101 general_lang_name: 'Bulgarian'
29 activerecord_error_too_long: е прекалено дълго
102 general_csv_separator: ','
30 activerecord_error_too_short: е прекалено късо
103 general_csv_decimal_separator: '.'
31 activerecord_error_wrong_length: е с грешна дължина
104 general_csv_encoding: UTF-8
32 activerecord_error_taken: вече съществува
105 general_pdf_encoding: UTF-8
33 activerecord_error_not_a_number: не е число
106 general_first_day_of_week: '1'
34 activerecord_error_not_a_date: е невалидна дата
107
35 activerecord_error_greater_than_start_date: трябва да е след началната дата
108 notice_account_updated: Профилът е обновен успешно.
36 activerecord_error_not_same_project: не е от същия проект
109 notice_account_invalid_creditentials: Невалиден потребител или парола.
37 activerecord_error_circular_dependency: Тази релация ще доведе до безкрайна зависимост
110 notice_account_password_updated: Паролата е успешно променена.
38
111 notice_account_wrong_password: Грешна парола
39 general_fmt_age: %d yr
112 notice_account_register_done: Профилът е създаден успешно.
40 general_fmt_age_plural: %d yrs
113 notice_account_unknown_email: Непознат e-mail.
41 general_fmt_date: %%d.%%m.%%Y
114 notice_can_t_change_password: Този профил е с външен метод за оторизация. Невъзможна смяна на паролата.
42 general_fmt_datetime: %%d.%%m.%%Y %%H:%%M
115 notice_account_lost_email_sent: Изпратен ви е e-mail с инструкции за избор на нова парола.
43 general_fmt_datetime_short: %%b %%d, %%H:%%M
116 notice_account_activated: Профилът ви е активиран. Вече може да влезете в системата.
44 general_fmt_time: %%H:%%M
117 notice_successful_create: Успешно създаване.
45 general_text_No: 'Не'
118 notice_successful_update: Успешно обновяване.
46 general_text_Yes: 'Да'
119 notice_successful_delete: Успешно изтриване.
47 general_text_no: 'не'
120 notice_successful_connection: Успешно свързване.
48 general_text_yes: 'да'
121 notice_file_not_found: Несъществуваща или преместена страница.
49 general_lang_name: 'Bulgarian'
122 notice_locking_conflict: Друг потребител променя тези данни в момента.
50 general_csv_separator: ','
123 notice_not_authorized: Нямате право на достъп до тази страница.
51 general_csv_decimal_separator: '.'
124 notice_email_sent: "Изпратен e-mail на {{value}}"
52 general_csv_encoding: UTF-8
125 notice_email_error: "Грешка при изпращане на e-mail ({{value}})"
53 general_pdf_encoding: UTF-8
126 notice_feeds_access_key_reseted: Вашия ключ за RSS достъп беше променен.
54 general_day_names: Понеделник,Вторник,Сряда,Четвъртък,Петък,Събота,Неделя
127
55 general_first_day_of_week: '1'
128 error_scm_not_found: Несъществуващ обект в хранилището.
56
129 error_scm_command_failed: "Грешка при опит за комуникация с хранилище: {{value}}"
57 notice_account_updated: Профилът е обновен успешно.
130
58 notice_account_invalid_creditentials: Невалиден потребител или парола.
131 mail_subject_lost_password: "Вашата парола ({{value}})"
59 notice_account_password_updated: Паролата е успешно променена.
132 mail_body_lost_password: 'За да смените паролата си, използвайте следния линк:'
60 notice_account_wrong_password: Грешна парола
133 mail_subject_register: "Активация на профил ({{value}})"
61 notice_account_register_done: Профилът е създаден успешно.
134 mail_body_register: 'За да активирате профила си използвайте следния линк:'
62 notice_account_unknown_email: Непознат e-mail.
135
63 notice_can_t_change_password: Този профил е с външен метод за оторизация. Невъзможна смяна на паролата.
136 gui_validation_error: 1 грешка
64 notice_account_lost_email_sent: Изпратен ви е e-mail с инструкции за избор на нова парола.
137 gui_validation_error_plural: "{{count}} грешки"
65 notice_account_activated: Профилът ви е активиран. Вече може да влезете в системата.
138
66 notice_successful_create: Успешно създаване.
139 field_name: Име
67 notice_successful_update: Успешно обновяване.
140 field_description: Описание
68 notice_successful_delete: Успешно изтриване.
141 field_summary: Групиран изглед
69 notice_successful_connection: Успешно свързване.
142 field_is_required: Задължително
70 notice_file_not_found: Несъществуваща или преместена страница.
143 field_firstname: Име
71 notice_locking_conflict: Друг потребител променя тези данни в момента.
144 field_lastname: Фамилия
72 notice_not_authorized: Нямате право на достъп до тази страница.
145 field_mail: Email
73 notice_email_sent: Изпратен e-mail на %s
146 field_filename: Файл
74 notice_email_error: Грешка при изпращане на e-mail (%s)
147 field_filesize: Големина
75 notice_feeds_access_key_reseted: Вашия ключ за RSS достъп беше променен.
148 field_downloads: Downloads
76
149 field_author: Автор
77 error_scm_not_found: Несъществуващ обект в хранилището.
150 field_created_on: От дата
78 error_scm_command_failed: "Грешка при опит за комуникация с хранилище: %s"
151 field_updated_on: Обновена
79
152 field_field_format: Тип
80 mail_subject_lost_password: Вашата парола (%s)
153 field_is_for_all: За всички проекти
81 mail_body_lost_password: 'За да смените паролата си, използвайте следния линк:'
154 field_possible_values: Възможни стойности
82 mail_subject_register: Активация на профил (%s)
155 field_regexp: Регулярен израз
83 mail_body_register: 'За да активирате профила си използвайте следния линк:'
156 field_min_length: Мин. дължина
84
157 field_max_length: Макс. дължина
85 gui_validation_error: 1 грешка
158 field_value: Стойност
86 gui_validation_error_plural: %d грешки
159 field_category: Категория
87
160 field_title: Заглавие
88 field_name: Име
161 field_project: Проект
89 field_description: Описание
162 field_issue: Задача
90 field_summary: Групиран изглед
163 field_status: Статус
91 field_is_required: Задължително
164 field_notes: Бележка
92 field_firstname: Име
165 field_is_closed: Затворена задача
93 field_lastname: Фамилия
166 field_is_default: Статус по подразбиране
94 field_mail: Email
167 field_tracker: Тракер
95 field_filename: Файл
168 field_subject: Относно
96 field_filesize: Големина
169 field_due_date: Крайна дата
97 field_downloads: Downloads
170 field_assigned_to: Възложена на
98 field_author: Автор
171 field_priority: Приоритет
99 field_created_on: От дата
172 field_fixed_version: Планувана версия
100 field_updated_on: Обновена
173 field_user: Потребител
101 field_field_format: Тип
174 field_role: Роля
102 field_is_for_all: За всички проекти
175 field_homepage: Начална страница
103 field_possible_values: Възможни стойности
176 field_is_public: Публичен
104 field_regexp: Регулярен израз
177 field_parent: Подпроект на
105 field_min_length: Мин. дължина
178 field_is_in_chlog: Да се вижда ли в Изменения
106 field_max_length: Макс. дължина
179 field_is_in_roadmap: Да се вижда ли в Пътна карта
107 field_value: Стойност
180 field_login: Потребител
108 field_category: Категория
181 field_mail_notification: Известия по пощата
109 field_title: Заглавие
182 field_admin: Администратор
110 field_project: Проект
183 field_last_login_on: Последно свързване
111 field_issue: Задача
184 field_language: Език
112 field_status: Статус
185 field_effective_date: Дата
113 field_notes: Бележка
186 field_password: Парола
114 field_is_closed: Затворена задача
187 field_new_password: Нова парола
115 field_is_default: Статус по подразбиране
188 field_password_confirmation: Потвърждение
116 field_tracker: Тракер
189 field_version: Версия
117 field_subject: Относно
190 field_type: Тип
118 field_due_date: Крайна дата
191 field_host: Хост
119 field_assigned_to: Възложена на
192 field_port: Порт
120 field_priority: Приоритет
193 field_account: Профил
121 field_fixed_version: Планувана версия
194 field_base_dn: Base DN
122 field_user: Потребител
195 field_attr_login: Login attribute
123 field_role: Роля
196 field_attr_firstname: Firstname attribute
124 field_homepage: Начална страница
197 field_attr_lastname: Lastname attribute
125 field_is_public: Публичен
198 field_attr_mail: Email attribute
126 field_parent: Подпроект на
199 field_onthefly: Динамично създаване на потребител
127 field_is_in_chlog: Да се вижда ли в Изменения
200 field_start_date: Начална дата
128 field_is_in_roadmap: Да се вижда ли в Пътна карта
201 field_done_ratio: %% Прогрес
129 field_login: Потребител
202 field_auth_source: Начин на оторизация
130 field_mail_notification: Известия по пощата
203 field_hide_mail: Скрий e-mail адреса ми
131 field_admin: Администратор
204 field_comments: Коментар
132 field_last_login_on: Последно свързване
205 field_url: Адрес
133 field_language: Език
206 field_start_page: Начална страница
134 field_effective_date: Дата
207 field_subproject: Подпроект
135 field_password: Парола
208 field_hours: Часове
136 field_new_password: Нова парола
209 field_activity: Дейност
137 field_password_confirmation: Потвърждение
210 field_spent_on: Дата
138 field_version: Версия
211 field_identifier: Идентификатор
139 field_type: Тип
212 field_is_filter: Използва се за филтър
140 field_host: Хост
213 field_issue_to_id: Свързана задача
141 field_port: Порт
214 field_delay: Отместване
142 field_account: Профил
215 field_assignable: Възможно е възлагане на задачи за тази роля
143 field_base_dn: Base DN
216 field_redirect_existing_links: Пренасочване на съществуващи линкове
144 field_attr_login: Login attribute
217 field_estimated_hours: Изчислено време
145 field_attr_firstname: Firstname attribute
218 field_default_value: Стойност по подразбиране
146 field_attr_lastname: Lastname attribute
219
147 field_attr_mail: Email attribute
220 setting_app_title: Заглавие
148 field_onthefly: Динамично създаване на потребител
221 setting_app_subtitle: Описание
149 field_start_date: Начална дата
222 setting_welcome_text: Допълнителен текст
150 field_done_ratio: %% Прогрес
223 setting_default_language: Език по подразбиране
151 field_auth_source: Начин на оторизация
224 setting_login_required: Изискване за вход в системата
152 field_hide_mail: Скрий e-mail адреса ми
225 setting_self_registration: Регистрация от потребители
153 field_comments: Коментар
226 setting_attachment_max_size: Максимална големина на прикачен файл
154 field_url: Адрес
227 setting_issues_export_limit: Лимит за експорт на задачи
155 field_start_page: Начална страница
228 setting_mail_from: E-mail адрес за емисии
156 field_subproject: Подпроект
229 setting_host_name: Хост
157 field_hours: Часове
230 setting_text_formatting: Форматиране на текста
158 field_activity: Дейност
231 setting_wiki_compression: Wiki компресиране на историята
159 field_spent_on: Дата
232 setting_feeds_limit: Лимит на Feeds
160 field_identifier: Идентификатор
233 setting_autofetch_changesets: Автоматично обработване на ревизиите
161 field_is_filter: Използва се за филтър
234 setting_sys_api_enabled: Разрешаване на WS за управление
162 field_issue_to_id: Свързана задача
235 setting_commit_ref_keywords: Отбелязващи ключови думи
163 field_delay: Отместване
236 setting_commit_fix_keywords: Приключващи ключови думи
164 field_assignable: Възможно е възлагане на задачи за тази роля
237 setting_autologin: Автоматичен вход
165 field_redirect_existing_links: Пренасочване на съществуващи линкове
238 setting_date_format: Формат на датата
166 field_estimated_hours: Изчислено време
239 setting_cross_project_issue_relations: Релации на задачи между проекти
167 field_default_value: Стойност по подразбиране
240
168
241 label_user: Потребител
169 setting_app_title: Заглавие
242 label_user_plural: Потребители
170 setting_app_subtitle: Описание
243 label_user_new: Нов потребител
171 setting_welcome_text: Допълнителен текст
244 label_project: Проект
172 setting_default_language: Език по подразбиране
245 label_project_new: Нов проект
173 setting_login_required: Изискване за вход в системата
246 label_project_plural: Проекти
174 setting_self_registration: Регистрация от потребители
247 label_x_projects:
175 setting_attachment_max_size: Максимална големина на прикачен файл
248 zero: no projects
176 setting_issues_export_limit: Лимит за експорт на задачи
249 one: 1 project
177 setting_mail_from: E-mail адрес за емисии
250 other: "{{count}} projects"
178 setting_host_name: Хост
251 label_project_all: Всички проекти
179 setting_text_formatting: Форматиране на текста
252 label_project_latest: Последни проекти
180 setting_wiki_compression: Wiki компресиране на историята
253 label_issue: Задача
181 setting_feeds_limit: Лимит на Feeds
254 label_issue_new: Нова задача
182 setting_autofetch_changesets: Автоматично обработване на ревизиите
255 label_issue_plural: Задачи
183 setting_sys_api_enabled: Разрешаване на WS за управление
256 label_issue_view_all: Всички задачи
184 setting_commit_ref_keywords: Отбелязващи ключови думи
257 label_document: Документ
185 setting_commit_fix_keywords: Приключващи ключови думи
258 label_document_new: Нов документ
186 setting_autologin: Автоматичен вход
259 label_document_plural: Документи
187 setting_date_format: Формат на датата
260 label_role: Роля
188 setting_cross_project_issue_relations: Релации на задачи между проекти
261 label_role_plural: Роли
189
262 label_role_new: Нова роля
190 label_user: Потребител
263 label_role_and_permissions: Роли и права
191 label_user_plural: Потребители
264 label_member: Член
192 label_user_new: Нов потребител
265 label_member_new: Нов член
193 label_project: Проект
266 label_member_plural: Членове
194 label_project_new: Нов проект
267 label_tracker: Тракер
195 label_project_plural: Проекти
268 label_tracker_plural: Тракери
196 label_project_all: Всички проекти
269 label_tracker_new: Нов тракер
197 label_project_latest: Последни проекти
270 label_workflow: Работен процес
198 label_issue: Задача
271 label_issue_status: Статус на задача
199 label_issue_new: Нова задача
272 label_issue_status_plural: Статуси на задачи
200 label_issue_plural: Задачи
273 label_issue_status_new: Нов статус
201 label_issue_view_all: Всички задачи
274 label_issue_category: Категория задача
202 label_document: Документ
275 label_issue_category_plural: Категории задачи
203 label_document_new: Нов документ
276 label_issue_category_new: Нова категория
204 label_document_plural: Документи
277 label_custom_field: Потребителско поле
205 label_role: Роля
278 label_custom_field_plural: Потребителски полета
206 label_role_plural: Роли
279 label_custom_field_new: Ново потребителско поле
207 label_role_new: Нова роля
280 label_enumerations: Списъци
208 label_role_and_permissions: Роли и права
281 label_enumeration_new: Нова стойност
209 label_member: Член
282 label_information: Информация
210 label_member_new: Нов член
283 label_information_plural: Информация
211 label_member_plural: Членове
284 label_please_login: Вход
212 label_tracker: Тракер
285 label_register: Регистрация
213 label_tracker_plural: Тракери
286 label_password_lost: Забравена парола
214 label_tracker_new: Нов тракер
287 label_home: Начало
215 label_workflow: Работен процес
288 label_my_page: Лична страница
216 label_issue_status: Статус на задача
289 label_my_account: Профил
217 label_issue_status_plural: Статуси на задачи
290 label_my_projects: Проекти, в които участвам
218 label_issue_status_new: Нов статус
291 label_administration: Администрация
219 label_issue_category: Категория задача
292 label_login: Вход
220 label_issue_category_plural: Категории задачи
293 label_logout: Изход
221 label_issue_category_new: Нова категория
294 label_help: Помощ
222 label_custom_field: Потребителско поле
295 label_reported_issues: Публикувани задачи
223 label_custom_field_plural: Потребителски полета
296 label_assigned_to_me_issues: Възложени на мен
224 label_custom_field_new: Ново потребителско поле
297 label_last_login: Последно свързване
225 label_enumerations: Списъци
298 label_registered_on: Регистрация
226 label_enumeration_new: Нова стойност
299 label_activity: Дейност
227 label_information: Информация
300 label_new: Нов
228 label_information_plural: Информация
301 label_logged_as: Логнат като
229 label_please_login: Вход
302 label_environment: Среда
230 label_register: Регистрация
303 label_authentication: Оторизация
231 label_password_lost: Забравена парола
304 label_auth_source: Начин на оторозация
232 label_home: Начало
305 label_auth_source_new: Нов начин на оторизация
233 label_my_page: Лична страница
306 label_auth_source_plural: Начини на оторизация
234 label_my_account: Профил
307 label_subproject_plural: Подпроекти
235 label_my_projects: Проекти, в които участвам
308 label_min_max_length: Мин. - Макс. дължина
236 label_administration: Администрация
309 label_list: Списък
237 label_login: Вход
310 label_date: Дата
238 label_logout: Изход
311 label_integer: Целочислен
239 label_help: Помощ
312 label_boolean: Чекбокс
240 label_reported_issues: Публикувани задачи
313 label_string: Текст
241 label_assigned_to_me_issues: Възложени на мен
314 label_text: Дълъг текст
242 label_last_login: Последно свързване
315 label_attribute: Атрибут
243 label_last_updates: Последно обновена
316 label_attribute_plural: Атрибути
244 label_last_updates_plural: %d последно обновени
317 label_download: "{{count}} Download"
245 label_registered_on: Регистрация
318 label_download_plural: "{{count}} Downloads"
246 label_activity: Дейност
319 label_no_data: Няма изходни данни
247 label_new: Нов
320 label_change_status: Промяна на статуса
248 label_logged_as: Логнат като
321 label_history: История
249 label_environment: Среда
322 label_attachment: Файл
250 label_authentication: Оторизация
323 label_attachment_new: Нов файл
251 label_auth_source: Начин на оторозация
324 label_attachment_delete: Изтриване
252 label_auth_source_new: Нов начин на оторизация
325 label_attachment_plural: Файлове
253 label_auth_source_plural: Начини на оторизация
326 label_report: Справка
254 label_subproject_plural: Подпроекти
327 label_report_plural: Справки
255 label_min_max_length: Мин. - Макс. дължина
328 label_news: Новини
256 label_list: Списък
329 label_news_new: Добави
257 label_date: Дата
330 label_news_plural: Новини
258 label_integer: Целочислен
331 label_news_latest: Последни новини
259 label_boolean: Чекбокс
332 label_news_view_all: Виж всички
260 label_string: Текст
333 label_change_log: Изменения
261 label_text: Дълъг текст
334 label_settings: Настройки
262 label_attribute: Атрибут
335 label_overview: Общ изглед
263 label_attribute_plural: Атрибути
336 label_version: Версия
264 label_download: %d Download
337 label_version_new: Нова версия
265 label_download_plural: %d Downloads
338 label_version_plural: Версии
266 label_no_data: Няма изходни данни
339 label_confirmation: Одобрение
267 label_change_status: Промяна на статуса
340 label_export_to: Експорт към
268 label_history: История
341 label_read: Read...
269 label_attachment: Файл
342 label_public_projects: Публични проекти
270 label_attachment_new: Нов файл
343 label_open_issues: отворена
271 label_attachment_delete: Изтриване
344 label_open_issues_plural: отворени
272 label_attachment_plural: Файлове
345 label_closed_issues: затворена
273 label_report: Справка
346 label_closed_issues_plural: затворени
274 label_report_plural: Справки
347 label_x_open_issues_abbr_on_total:
275 label_news: Новини
348 zero: 0 open / {{total}}
276 label_news_new: Добави
349 one: 1 open / {{total}}
277 label_news_plural: Новини
350 other: "{{count}} open / {{total}}"
278 label_news_latest: Последни новини
351 label_x_open_issues_abbr:
279 label_news_view_all: Виж всички
352 zero: 0 open
280 label_change_log: Изменения
353 one: 1 open
281 label_settings: Настройки
354 other: "{{count}} open"
282 label_overview: Общ изглед
355 label_x_closed_issues_abbr:
283 label_version: Версия
356 zero: 0 closed
284 label_version_new: Нова версия
357 one: 1 closed
285 label_version_plural: Версии
358 other: "{{count}} closed"
286 label_confirmation: Одобрение
359 label_total: Общо
287 label_export_to: Експорт към
360 label_permissions: Права
288 label_read: Read...
361 label_current_status: Текущ статус
289 label_public_projects: Публични проекти
362 label_new_statuses_allowed: Позволени статуси
290 label_open_issues: отворена
363 label_all: всички
291 label_open_issues_plural: отворени
364 label_none: никакви
292 label_closed_issues: затворена
365 label_next: Следващ
293 label_closed_issues_plural: затворени
366 label_previous: Предишен
294 label_total: Общо
367 label_used_by: Използва се от
295 label_permissions: Права
368 label_details: Детайли
296 label_current_status: Текущ статус
369 label_add_note: Добавяне на бележка
297 label_new_statuses_allowed: Позволени статуси
370 label_per_page: На страница
298 label_all: всички
371 label_calendar: Календар
299 label_none: никакви
372 label_months_from: месеца от
300 label_next: Следващ
373 label_gantt: Gantt
301 label_previous: Предишен
374 label_internal: Вътрешен
302 label_used_by: Използва се от
375 label_last_changes: "последни {{count}} промени"
303 label_details: Детайли
376 label_change_view_all: Виж всички промени
304 label_add_note: Добавяне на бележка
377 label_personalize_page: Персонализиране
305 label_per_page: На страница
378 label_comment: Коментар
306 label_calendar: Календар
379 label_comment_plural: Коментари
307 label_months_from: месеца от
380 label_x_comments:
308 label_gantt: Gantt
381 zero: no comments
309 label_internal: Вътрешен
382 one: 1 comment
310 label_last_changes: последни %d промени
383 other: "{{count}} comments"
311 label_change_view_all: Виж всички промени
384 label_comment_add: Добавяне на коментар
312 label_personalize_page: Персонализиране
385 label_comment_added: Добавен коментар
313 label_comment: Коментар
386 label_comment_delete: Изтриване на коментари
314 label_comment_plural: Коментари
387 label_query: Потребителска справка
315 label_comment_add: Добавяне на коментар
388 label_query_plural: Потребителски справки
316 label_comment_added: Добавен коментар
389 label_query_new: Нова заявка
317 label_comment_delete: Изтриване на коментари
390 label_filter_add: Добави филтър
318 label_query: Потребителска справка
391 label_filter_plural: Филтри
319 label_query_plural: Потребителски справки
392 label_equals: е
320 label_query_new: Нова заявка
393 label_not_equals: не е
321 label_filter_add: Добави филтър
394 label_in_less_than: след по-малко от
322 label_filter_plural: Филтри
395 label_in_more_than: след повече от
323 label_equals: е
396 label_in: в следващите
324 label_not_equals: не е
397 label_today: днес
325 label_in_less_than: след по-малко от
398 label_this_week: тази седмица
326 label_in_more_than: след повече от
399 label_less_than_ago: преди по-малко от
327 label_in: в следващите
400 label_more_than_ago: преди повече от
328 label_today: днес
401 label_ago: преди
329 label_this_week: тази седмица
402 label_contains: съдържа
330 label_less_than_ago: преди по-малко от
403 label_not_contains: не съдържа
331 label_more_than_ago: преди повече от
404 label_day_plural: дни
332 label_ago: преди
405 label_repository: Хранилище
333 label_contains: съдържа
406 label_browse: Разглеждане
334 label_not_contains: не съдържа
407 label_modification: "{{count}} промяна"
335 label_day_plural: дни
408 label_modification_plural: "{{count}} промени"
336 label_repository: Хранилище
409 label_revision: Ревизия
337 label_browse: Разглеждане
410 label_revision_plural: Ревизии
338 label_modification: %d промяна
411 label_added: добавено
339 label_modification_plural: %d промени
412 label_modified: променено
340 label_revision: Ревизия
413 label_deleted: изтрито
341 label_revision_plural: Ревизии
414 label_latest_revision: Последна ревизия
342 label_added: добавено
415 label_latest_revision_plural: Последни ревизии
343 label_modified: променено
416 label_view_revisions: Виж ревизиите
344 label_deleted: изтрито
417 label_max_size: Максимална големина
345 label_latest_revision: Последна ревизия
418 label_sort_highest: Премести най-горе
346 label_latest_revision_plural: Последни ревизии
419 label_sort_higher: Премести по-горе
347 label_view_revisions: Виж ревизиите
420 label_sort_lower: Премести по-долу
348 label_max_size: Максимална големина
421 label_sort_lowest: Премести най-долу
349 label_on: 'от'
422 label_roadmap: Пътна карта
350 label_sort_highest: Премести най-горе
423 label_roadmap_due_in: "Излиза след {{value}}"
351 label_sort_higher: Премести по-горе
424 label_roadmap_overdue: "{{value}} закъснение"
352 label_sort_lower: Премести по-долу
425 label_roadmap_no_issues: Няма задачи за тази версия
353 label_sort_lowest: Премести най-долу
426 label_search: Търсене
354 label_roadmap: Пътна карта
427 label_result_plural: Pезултати
355 label_roadmap_due_in: Излиза след %s
428 label_all_words: Всички думи
356 label_roadmap_overdue: %s закъснение
429 label_wiki: Wiki
357 label_roadmap_no_issues: Няма задачи за тази версия
430 label_wiki_edit: Wiki редакция
358 label_search: Търсене
431 label_wiki_edit_plural: Wiki редакции
359 label_result_plural: Pезултати
432 label_wiki_page: Wiki page
360 label_all_words: Всички думи
433 label_wiki_page_plural: Wiki pages
361 label_wiki: Wiki
434 label_index_by_title: Индекс
362 label_wiki_edit: Wiki редакция
435 label_index_by_date: Индекс по дата
363 label_wiki_edit_plural: Wiki редакции
436 label_current_version: Текуща версия
364 label_wiki_page: Wiki page
437 label_preview: Преглед
365 label_wiki_page_plural: Wiki pages
438 label_feed_plural: Feeds
366 label_index_by_title: Индекс
439 label_changes_details: Подробни промени
367 label_index_by_date: Индекс по дата
440 label_issue_tracking: Тракинг
368 label_current_version: Текуща версия
441 label_spent_time: Отделено време
369 label_preview: Преглед
442 label_f_hour: "{{value}} час"
370 label_feed_plural: Feeds
443 label_f_hour_plural: "{{value}} часа"
371 label_changes_details: Подробни промени
444 label_time_tracking: Отделяне на време
372 label_issue_tracking: Тракинг
445 label_change_plural: Промени
373 label_spent_time: Отделено време
446 label_statistics: Статистики
374 label_f_hour: %.2f час
447 label_commits_per_month: Ревизии по месеци
375 label_f_hour_plural: %.2f часа
448 label_commits_per_author: Ревизии по автор
376 label_time_tracking: Отделяне на време
449 label_view_diff: Виж разликите
377 label_change_plural: Промени
450 label_diff_inline: хоризонтално
378 label_statistics: Статистики
451 label_diff_side_by_side: вертикално
379 label_commits_per_month: Ревизии по месеци
452 label_options: Опции
380 label_commits_per_author: Ревизии по автор
453 label_copy_workflow_from: Копирай работния процес от
381 label_view_diff: Виж разликите
454 label_permissions_report: Справка за права
382 label_diff_inline: хоризонтално
455 label_watched_issues: Наблюдавани задачи
383 label_diff_side_by_side: вертикално
456 label_related_issues: Свързани задачи
384 label_options: Опции
457 label_applied_status: Промени статуса на
385 label_copy_workflow_from: Копирай работния процес от
458 label_loading: Зареждане...
386 label_permissions_report: Справка за права
459 label_relation_new: Нова релация
387 label_watched_issues: Наблюдавани задачи
460 label_relation_delete: Изтриване на релация
388 label_related_issues: Свързани задачи
461 label_relates_to: свързана със
389 label_applied_status: Промени статуса на
462 label_duplicates: дублира
390 label_loading: Зареждане...
463 label_blocks: блокира
391 label_relation_new: Нова релация
464 label_blocked_by: блокирана от
392 label_relation_delete: Изтриване на релация
465 label_precedes: предшества
393 label_relates_to: свързана със
466 label_follows: изпълнява се след
394 label_duplicates: дублира
467 label_end_to_start: end to start
395 label_blocks: блокира
468 label_end_to_end: end to end
396 label_blocked_by: блокирана от
469 label_start_to_start: start to start
397 label_precedes: предшества
470 label_start_to_end: start to end
398 label_follows: изпълнява се след
471 label_stay_logged_in: Запомни ме
399 label_end_to_start: end to start
472 label_disabled: забранено
400 label_end_to_end: end to end
473 label_show_completed_versions: Показване на реализирани версии
401 label_start_to_start: start to start
474 label_me: аз
402 label_start_to_end: start to end
475 label_board: Форум
403 label_stay_logged_in: Запомни ме
476 label_board_new: Нов форум
404 label_disabled: забранено
477 label_board_plural: Форуми
405 label_show_completed_versions: Показване на реализирани версии
478 label_topic_plural: Теми
406 label_me: аз
479 label_message_plural: Съобщения
407 label_board: Форум
480 label_message_last: Последно съобщение
408 label_board_new: Нов форум
481 label_message_new: Нова тема
409 label_board_plural: Форуми
482 label_reply_plural: Отговори
410 label_topic_plural: Теми
483 label_send_information: Изпращане на информацията до потребителя
411 label_message_plural: Съобщения
484 label_year: Година
412 label_message_last: Последно съобщение
485 label_month: Месец
413 label_message_new: Нова тема
486 label_week: Седмица
414 label_reply_plural: Отговори
487 label_date_from: От
415 label_send_information: Изпращане на информацията до потребителя
488 label_date_to: До
416 label_year: Година
489 label_language_based: В зависимост от езика
417 label_month: Месец
490 label_sort_by: "Сортиране по {{value}}"
418 label_week: Седмица
491 label_send_test_email: Изпращане на тестов e-mail
419 label_date_from: От
492 label_feeds_access_key_created_on: "{{value}} от създаването на RSS ключа"
420 label_date_to: До
493 label_module_plural: Модули
421 label_language_based: В зависимост от езика
494 label_added_time_by: "Публикувана от {{author}} преди {{age}}"
422 label_sort_by: Сортиране по %s
495 label_updated_time: "Обновена преди {{value}}"
423 label_send_test_email: Изпращане на тестов e-mail
496 label_jump_to_a_project: Проект...
424 label_feeds_access_key_created_on: %s от създаването на RSS ключа
497
425 label_module_plural: Модули
498 button_login: Вход
426 label_added_time_by: Публикувана от %s преди %s
499 button_submit: Прикачване
427 label_updated_time: Обновена преди %s
500 button_save: Запис
428 label_jump_to_a_project: Проект...
501 button_check_all: Избор на всички
429
502 button_uncheck_all: Изчистване на всички
430 button_login: Вход
503 button_delete: Изтриване
431 button_submit: Прикачване
504 button_create: Създаване
432 button_save: Запис
505 button_test: Тест
433 button_check_all: Избор на всички
506 button_edit: Редакция
434 button_uncheck_all: Изчистване на всички
507 button_add: Добавяне
435 button_delete: Изтриване
508 button_change: Промяна
436 button_create: Създаване
509 button_apply: Приложи
437 button_test: Тест
510 button_clear: Изчисти
438 button_edit: Редакция
511 button_lock: Заключване
439 button_add: Добавяне
512 button_unlock: Отключване
440 button_change: Промяна
513 button_download: Download
441 button_apply: Приложи
514 button_list: Списък
442 button_clear: Изчисти
515 button_view: Преглед
443 button_lock: Заключване
516 button_move: Преместване
444 button_unlock: Отключване
517 button_back: Назад
445 button_download: Download
518 button_cancel: Отказ
446 button_list: Списък
519 button_activate: Активация
447 button_view: Преглед
520 button_sort: Сортиране
448 button_move: Преместване
521 button_log_time: Отделяне на време
449 button_back: Назад
522 button_rollback: Върни се към тази ревизия
450 button_cancel: Отказ
523 button_watch: Наблюдавай
451 button_activate: Активация
524 button_unwatch: Спри наблюдението
452 button_sort: Сортиране
525 button_reply: Отговор
453 button_log_time: Отделяне на време
526 button_archive: Архивиране
454 button_rollback: Върни се към тази ревизия
527 button_unarchive: Разархивиране
455 button_watch: Наблюдавай
528 button_reset: Генериране наново
456 button_unwatch: Спри наблюдението
529 button_rename: Преименуване
457 button_reply: Отговор
530
458 button_archive: Архивиране
531 status_active: активен
459 button_unarchive: Разархивиране
532 status_registered: регистриран
460 button_reset: Генериране наново
533 status_locked: заключен
461 button_rename: Преименуване
534
462
535 text_select_mail_notifications: Изберете събития за изпращане на e-mail.
463 status_active: активен
536 text_regexp_info: пр. ^[A-Z0-9]+$
464 status_registered: регистриран
537 text_min_max_length_info: 0 - без ограничения
465 status_locked: заключен
538 text_project_destroy_confirmation: Сигурни ли сте, че искате да изтриете проекта и данните в него?
466
539 text_workflow_edit: Изберете роля и тракер за да редактирате работния процес
467 text_select_mail_notifications: Изберете събития за изпращане на e-mail.
540 text_are_you_sure: Сигурни ли сте?
468 text_regexp_info: пр. ^[A-Z0-9]+$
541 text_journal_changed: "промяна от {{old}} на {{new}}"
469 text_min_max_length_info: 0 - без ограничения
542 text_journal_set_to: "установено на {{value}}"
470 text_project_destroy_confirmation: Сигурни ли сте, че искате да изтриете проекта и данните в него?
543 text_journal_deleted: изтрито
471 text_workflow_edit: Изберете роля и тракер за да редактирате работния процес
544 text_tip_task_begin_day: задача започваща този ден
472 text_are_you_sure: Сигурни ли сте?
545 text_tip_task_end_day: задача завършваща този ден
473 text_journal_changed: промяна от %s на %s
546 text_tip_task_begin_end_day: задача започваща и завършваща този ден
474 text_journal_set_to: установено на %s
547 text_project_identifier_info: 'Позволени са малки букви (a-z), цифри и тирета.<br />Невъзможна промяна след запис.'
475 text_journal_deleted: изтрито
548 text_caracters_maximum: "До {{count}} символа."
476 text_tip_task_begin_day: задача започваща този ден
549 text_length_between: "От {{min}} до {{max}} символа."
477 text_tip_task_end_day: задача завършваща този ден
550 text_tracker_no_workflow: Няма дефиниран работен процес за този тракер
478 text_tip_task_begin_end_day: задача започваща и завършваща този ден
551 text_unallowed_characters: Непозволени символи
479 text_project_identifier_info: 'Позволени са малки букви (a-z), цифри и тирета.<br />Невъзможна промяна след запис.'
552 text_comma_separated: Позволено е изброяване (с разделител запетая).
480 text_caracters_maximum: До %d символа.
553 text_issues_ref_in_commit_messages: Отбелязване и приключване на задачи от ревизии
481 text_length_between: От %d до %d символа.
554 text_issue_added: "Публикувана е нова задача с номер {{id}} (от {{author}})."
482 text_tracker_no_workflow: Няма дефиниран работен процес за този тракер
555 text_issue_updated: "Задача {{id}} е обновена (от {{author}})."
483 text_unallowed_characters: Непозволени символи
556 text_wiki_destroy_confirmation: Сигурни ли сте, че искате да изтриете това Wiki и цялото му съдържание?
484 text_comma_separated: Позволено е изброяване (с разделител запетая).
557 text_issue_category_destroy_question: "Има задачи ({{count}}) обвързани с тази категория. Какво ще изберете?"
485 text_issues_ref_in_commit_messages: Отбелязване и приключване на задачи от ревизии
558 text_issue_category_destroy_assignments: Премахване на връзките с категорията
486 text_issue_added: Публикувана е нова задача с номер %s (от %s).
559 text_issue_category_reassign_to: Преобвързване с категория
487 text_issue_updated: Задача %s е обновена (от %s).
560
488 text_wiki_destroy_confirmation: Сигурни ли сте, че искате да изтриете това Wiki и цялото му съдържание?
561 default_role_manager: Мениджър
489 text_issue_category_destroy_question: Има задачи (%d) обвързани с тази категория. Какво ще изберете?
562 default_role_developper: Разработчик
490 text_issue_category_destroy_assignments: Премахване на връзките с категорията
563 default_role_reporter: Публикуващ
491 text_issue_category_reassign_to: Преобвързване с категория
564 default_tracker_bug: Бъг
492
565 default_tracker_feature: Функционалност
493 default_role_manager: Мениджър
566 default_tracker_support: Поддръжка
494 default_role_developper: Разработчик
567 default_issue_status_new: Нова
495 default_role_reporter: Публикуващ
568 default_issue_status_assigned: Възложена
496 default_tracker_bug: Бъг
569 default_issue_status_resolved: Приключена
497 default_tracker_feature: Функционалност
570 default_issue_status_feedback: Обратна връзка
498 default_tracker_support: Поддръжка
571 default_issue_status_closed: Затворена
499 default_issue_status_new: Нова
572 default_issue_status_rejected: Отхвърлена
500 default_issue_status_assigned: Възложена
573 default_doc_category_user: Документация за потребителя
501 default_issue_status_resolved: Приключена
574 default_doc_category_tech: Техническа документация
502 default_issue_status_feedback: Обратна връзка
575 default_priority_low: Нисък
503 default_issue_status_closed: Затворена
576 default_priority_normal: Нормален
504 default_issue_status_rejected: Отхвърлена
577 default_priority_high: Висок
505 default_doc_category_user: Документация за потребителя
578 default_priority_urgent: Спешен
506 default_doc_category_tech: Техническа документация
579 default_priority_immediate: Веднага
507 default_priority_low: Нисък
580 default_activity_design: Дизайн
508 default_priority_normal: Нормален
581 default_activity_development: Разработка
509 default_priority_high: Висок
582
510 default_priority_urgent: Спешен
583 enumeration_issue_priorities: Приоритети на задачи
511 default_priority_immediate: Веднага
584 enumeration_doc_categories: Категории документи
512 default_activity_design: Дизайн
585 enumeration_activities: Дейности (time tracking)
513 default_activity_development: Разработка
586 label_file_plural: Файлове
514
587 label_changeset_plural: Ревизии
515 enumeration_issue_priorities: Приоритети на задачи
588 field_column_names: Колони
516 enumeration_doc_categories: Категории документи
589 label_default_columns: По подразбиране
517 enumeration_activities: Дейности (time tracking)
590 setting_issue_list_default_columns: Показвани колони по подразбиране
518 label_file_plural: Файлове
591 setting_repositories_encodings: Кодови таблици
519 label_changeset_plural: Ревизии
592 notice_no_issue_selected: "Няма избрани задачи."
520 field_column_names: Колони
593 label_bulk_edit_selected_issues: Редактиране на задачи
521 label_default_columns: По подразбиране
594 label_no_change_option: (Без промяна)
522 setting_issue_list_default_columns: Показвани колони по подразбиране
595 notice_failed_to_save_issues: "Неуспешен запис на {{count}} задачи от {{total}} избрани: {{ids}}."
523 setting_repositories_encodings: Кодови таблици
596 label_theme: Тема
524 notice_no_issue_selected: "Няма избрани задачи."
597 label_default: По подразбиране
525 label_bulk_edit_selected_issues: Редактиране на задачи
598 label_search_titles_only: Само в заглавията
526 label_no_change_option: (Без промяна)
599 label_nobody: никой
527 notice_failed_to_save_issues: "Неуспешен запис на %d задачи от %d избрани: %s."
600 button_change_password: Промяна на парола
528 label_theme: Тема
601 text_user_mail_option: "За неизбраните проекти, ще получавате известия само за наблюдавани дейности или в които участвате (т.е. автор или назначени на мен)."
529 label_default: По подразбиране
602 label_user_mail_option_selected: "За всички събития само в избраните проекти..."
530 label_search_titles_only: Само в заглавията
603 label_user_mail_option_all: "За всяко събитие в проектите, в които участвам"
531 label_nobody: никой
604 label_user_mail_option_none: "Само за наблюдавани или в които участвам (автор или назначени на мен)"
532 button_change_password: Промяна на парола
605 setting_emails_footer: Подтекст за e-mail
533 text_user_mail_option: "За неизбраните проекти, ще получавате известия само за наблюдавани дейности или в които участвате (т.е. автор или назначени на мен)."
606 label_float: Дробно
534 label_user_mail_option_selected: "За всички събития само в избраните проекти..."
607 button_copy: Копиране
535 label_user_mail_option_all: "За всяко събитие в проектите, в които участвам"
608 mail_body_account_information_external: "Можете да използвате вашия {{value}} профил за вход."
536 label_user_mail_option_none: "Само за наблюдавани или в които участвам (автор или назначени на мен)"
609 mail_body_account_information: Информацията за профила ви
537 setting_emails_footer: Подтекст за e-mail
610 setting_protocol: Протокол
538 label_float: Дробно
611 label_user_mail_no_self_notified: "Не искам известия за извършени от мен промени"
539 button_copy: Копиране
612 setting_time_format: Формат на часа
540 mail_body_account_information_external: Можете да използвате вашия "%s" профил за вход.
613 label_registration_activation_by_email: активиране на профила по email
541 mail_body_account_information: Информацията за профила ви
614 mail_subject_account_activation_request: "Заявка за активиране на профил в {{value}}"
542 setting_protocol: Протокол
615 mail_body_account_activation_request: "Има новорегистриран потребител ({{value}}), очакващ вашето одобрение:'"
543 label_user_mail_no_self_notified: "Не искам известия за извършени от мен промени"
616 label_registration_automatic_activation: автоматично активиране
544 setting_time_format: Формат на часа
617 label_registration_manual_activation: ръчно активиране
545 label_registration_activation_by_email: активиране на профила по email
618 notice_account_pending: "Профилът Ви е създаден и очаква одобрение от администратор."
546 mail_subject_account_activation_request: Заявка за активиране на профил в %s
619 field_time_zone: Часова зона
547 mail_body_account_activation_request: 'Има новорегистриран потребител (%s), очакващ вашето одобрение:'
620 text_caracters_minimum: "Минимум {{count}} символа."
548 label_registration_automatic_activation: автоматично активиране
621 setting_bcc_recipients: Получатели на скрито копие (bcc)
549 label_registration_manual_activation: ръчно активиране
622 button_annotate: Анотация
550 notice_account_pending: "Профилът Ви е създаден и очаква одобрение от администратор."
623 label_issues_by: "Задачи по {{value}}"
551 field_time_zone: Часова зона
624 field_searchable: С възможност за търсене
552 text_caracters_minimum: Минимум %d символа.
625 label_display_per_page: "На страница по: {{value}}'"
553 setting_bcc_recipients: Получатели на скрито копие (bcc)
626 setting_per_page_options: Опции за страниране
554 button_annotate: Анотация
627 label_age: Възраст
555 label_issues_by: Задачи по %s
628 notice_default_data_loaded: Примерната информацията е успешно заредена.
556 field_searchable: С възможност за търсене
629 text_load_default_configuration: Зареждане на примерна информация
557 label_display_per_page: 'На страница по: %s'
630 text_no_configuration_data: "Все още не са конфигурирани Роли, тракери, статуси на задачи и работен процес.\nСтрого се препоръчва зареждането на примерната информация. Веднъж заредена ще имате възможност да я редактирате."
558 setting_per_page_options: Опции за страниране
631 error_can_t_load_default_data: "Грешка при зареждане на примерната информация: {{value}}"
559 label_age: Възраст
632 button_update: Обновяване
560 notice_default_data_loaded: Примерната информацията е успешно заредена.
633 label_change_properties: Промяна на настройки
561 text_load_default_configuration: Зареждане на примерна информация
634 label_general: Основни
562 text_no_configuration_data: "Все още не са конфигурирани Роли, тракери, статуси на задачи и работен процес.\nСтрого се препоръчва зареждането на примерната информация. Веднъж заредена ще имате възможност да я редактирате."
635 label_repository_plural: Хранилища
563 error_can_t_load_default_data: "Грешка при зареждане на примерната информация: %s"
636 label_associated_revisions: Асоциирани ревизии
564 button_update: Обновяване
637 setting_user_format: Потребителски формат
565 label_change_properties: Промяна на настройки
638 text_status_changed_by_changeset: "Приложено с ревизия {{value}}."
566 label_general: Основни
639 label_more: Още
567 label_repository_plural: Хранилища
640 text_issues_destroy_confirmation: 'Сигурни ли сте, че искате да изтриете избраните задачи?'
568 label_associated_revisions: Асоциирани ревизии
641 label_scm: SCM (Система за контрол на кода)
569 setting_user_format: Потребителски формат
642 text_select_project_modules: 'Изберете активните модули за този проект:'
570 text_status_changed_by_changeset: Приложено с ревизия %s.
643 label_issue_added: Добавена задача
571 label_more: Още
644 label_issue_updated: Обновена задача
572 text_issues_destroy_confirmation: 'Сигурни ли сте, че искате да изтриете избраните задачи?'
645 label_document_added: Добавен документ
573 label_scm: SCM (Система за контрол на кода)
646 label_message_posted: Добавено съобщение
574 text_select_project_modules: 'Изберете активните модули за този проект:'
647 label_file_added: Добавен файл
575 label_issue_added: Добавена задача
648 label_news_added: Добавена новина
576 label_issue_updated: Обновена задача
649 project_module_boards: Форуми
577 label_document_added: Добавен документ
650 project_module_issue_tracking: Тракинг
578 label_message_posted: Добавено съобщение
651 project_module_wiki: Wiki
579 label_file_added: Добавен файл
652 project_module_files: Файлове
580 label_news_added: Добавена новина
653 project_module_documents: Документи
581 project_module_boards: Форуми
654 project_module_repository: Хранилище
582 project_module_issue_tracking: Тракинг
655 project_module_news: Новини
583 project_module_wiki: Wiki
656 project_module_time_tracking: Отделяне на време
584 project_module_files: Файлове
657 text_file_repository_writable: Възможност за писане в хранилището с файлове
585 project_module_documents: Документи
658 text_default_administrator_account_changed: Сменен фабричния администраторски профил
586 project_module_repository: Хранилище
659 text_rmagick_available: Наличен RMagick (по избор)
587 project_module_news: Новини
660 button_configure: Конфигуриране
588 project_module_time_tracking: Отделяне на време
661 label_plugins: Плъгини
589 text_file_repository_writable: Възможност за писане в хранилището с файлове
662 label_ldap_authentication: LDAP оторизация
590 text_default_administrator_account_changed: Сменен фабричния администраторски профил
663 label_downloads_abbr: D/L
591 text_rmagick_available: Наличен RMagick (по избор)
664 label_this_month: текущия месец
592 button_configure: Конфигуриране
665 label_last_n_days: "последните {{count}} дни"
593 label_plugins: Плъгини
666 label_all_time: всички
594 label_ldap_authentication: LDAP оторизация
667 label_this_year: текущата година
595 label_downloads_abbr: D/L
668 label_date_range: Период
596 label_this_month: текущия месец
669 label_last_week: последната седмица
597 label_last_n_days: последните %d дни
670 label_yesterday: вчера
598 label_all_time: всички
671 label_last_month: последния месец
599 label_this_year: текущата година
672 label_add_another_file: Добавяне на друг файл
600 label_date_range: Период
673 label_optional_description: Незадължително описание
601 label_last_week: последната седмица
674 text_destroy_time_entries_question: %.02f часа са отделени на задачите, които искате да изтриете. Какво избирате?
602 label_yesterday: вчера
675 error_issue_not_found_in_project: 'Задачата не е намерена или не принадлежи на този проект'
603 label_last_month: последния месец
676 text_assign_time_entries_to_project: Прехвърляне на отделеното време към проект
604 label_add_another_file: Добавяне на друг файл
677 text_destroy_time_entries: Изтриване на отделеното време
605 label_optional_description: Незадължително описание
678 text_reassign_time_entries: 'Прехвърляне на отделеното време към задача:'
606 text_destroy_time_entries_question: %.02f часа са отделени на задачите, които искате да изтриете. Какво избирате?
679 setting_activity_days_default: Брой дни показвани на таб Дейност
607 error_issue_not_found_in_project: 'Задачата не е намерена или не принадлежи на този проект'
680 label_chronological_order: Хронологичен ред
608 text_assign_time_entries_to_project: Прехвърляне на отделеното време към проект
681 field_comments_sorting: Сортиране на коментарите
609 text_destroy_time_entries: Изтриване на отделеното време
682 label_reverse_chronological_order: Обратен хронологичен ред
610 text_reassign_time_entries: 'Прехвърляне на отделеното време към задача:'
683 label_preferences: Предпочитания
611 setting_activity_days_default: Брой дни показвани на таб Дейност
684 setting_display_subprojects_issues: Показване на подпроектите в проектите по подразбиране
612 label_chronological_order: Хронологичен ред
685 label_overall_activity: Цялостна дейност
613 field_comments_sorting: Сортиране на коментарите
686 setting_default_projects_public: Новите проекти са публични по подразбиране
614 label_reverse_chronological_order: Обратен хронологичен ред
687 error_scm_annotate: "Обектът не съществува или не може да бъде анотиран."
615 label_preferences: Предпочитания
688 label_planning: Планиране
616 setting_display_subprojects_issues: Показване на подпроектите в проектите по подразбиране
689 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted.'"
617 label_overall_activity: Цялостна дейност
690 label_and_its_subprojects: "{{value}} and its subprojects"
618 setting_default_projects_public: Новите проекти са публични по подразбиране
691 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
619 error_scm_annotate: "Обектът не съществува или не може да бъде анотиран."
692 mail_subject_reminder: "{{count}} issue(s) due in the next days"
620 label_planning: Планиране
693 text_user_wrote: "{{value}} wrote:'"
621 text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
694 label_duplicated_by: duplicated by
622 label_and_its_subprojects: %s and its subprojects
695 setting_enabled_scm: Enabled SCM
623 mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
696 text_enumeration_category_reassign_to: 'Reassign them to this value:'
624 mail_subject_reminder: "%d issue(s) due in the next days"
697 text_enumeration_destroy_question: "{{count}} objects are assigned to this value.'"
625 text_user_wrote: '%s wrote:'
698 label_incoming_emails: Incoming emails
626 label_duplicated_by: duplicated by
699 label_generate_key: Generate a key
627 setting_enabled_scm: Enabled SCM
700 setting_mail_handler_api_enabled: Enable WS for incoming emails
628 text_enumeration_category_reassign_to: 'Reassign them to this value:'
701 setting_mail_handler_api_key: API key
629 text_enumeration_destroy_question: '%d objects are assigned to this value.'
702 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
630 label_incoming_emails: Incoming emails
703 field_parent_title: Parent page
631 label_generate_key: Generate a key
704 label_issue_watchers: Watchers
632 setting_mail_handler_api_enabled: Enable WS for incoming emails
705 setting_commit_logs_encoding: Commit messages encoding
633 setting_mail_handler_api_key: API key
706 button_quote: Quote
634 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
707 setting_sequential_project_identifiers: Generate sequential project identifiers
635 field_parent_title: Parent page
708 notice_unable_delete_version: Unable to delete version
636 label_issue_watchers: Watchers
709 label_renamed: renamed
637 setting_commit_logs_encoding: Commit messages encoding
710 label_copied: copied
638 button_quote: Quote
711 setting_plain_text_mail: plain text only (no HTML)
639 setting_sequential_project_identifiers: Generate sequential project identifiers
712 permission_view_files: View files
640 notice_unable_delete_version: Unable to delete version
713 permission_edit_issues: Edit issues
641 label_renamed: renamed
714 permission_edit_own_time_entries: Edit own time logs
642 label_copied: copied
715 permission_manage_public_queries: Manage public queries
643 setting_plain_text_mail: plain text only (no HTML)
716 permission_add_issues: Add issues
644 permission_view_files: View files
717 permission_log_time: Log spent time
645 permission_edit_issues: Edit issues
718 permission_view_changesets: View changesets
646 permission_edit_own_time_entries: Edit own time logs
719 permission_view_time_entries: View spent time
647 permission_manage_public_queries: Manage public queries
720 permission_manage_versions: Manage versions
648 permission_add_issues: Add issues
721 permission_manage_wiki: Manage wiki
649 permission_log_time: Log spent time
722 permission_manage_categories: Manage issue categories
650 permission_view_changesets: View changesets
723 permission_protect_wiki_pages: Protect wiki pages
651 permission_view_time_entries: View spent time
724 permission_comment_news: Comment news
652 permission_manage_versions: Manage versions
725 permission_delete_messages: Delete messages
653 permission_manage_wiki: Manage wiki
726 permission_select_project_modules: Select project modules
654 permission_manage_categories: Manage issue categories
727 permission_manage_documents: Manage documents
655 permission_protect_wiki_pages: Protect wiki pages
728 permission_edit_wiki_pages: Edit wiki pages
656 permission_comment_news: Comment news
729 permission_add_issue_watchers: Add watchers
657 permission_delete_messages: Delete messages
730 permission_view_gantt: View gantt chart
658 permission_select_project_modules: Select project modules
731 permission_move_issues: Move issues
659 permission_manage_documents: Manage documents
732 permission_manage_issue_relations: Manage issue relations
660 permission_edit_wiki_pages: Edit wiki pages
733 permission_delete_wiki_pages: Delete wiki pages
661 permission_add_issue_watchers: Add watchers
734 permission_manage_boards: Manage boards
662 permission_view_gantt: View gantt chart
735 permission_delete_wiki_pages_attachments: Delete attachments
663 permission_move_issues: Move issues
736 permission_view_wiki_edits: View wiki history
664 permission_manage_issue_relations: Manage issue relations
737 permission_add_messages: Post messages
665 permission_delete_wiki_pages: Delete wiki pages
738 permission_view_messages: View messages
666 permission_manage_boards: Manage boards
739 permission_manage_files: Manage files
667 permission_delete_wiki_pages_attachments: Delete attachments
740 permission_edit_issue_notes: Edit notes
668 permission_view_wiki_edits: View wiki history
741 permission_manage_news: Manage news
669 permission_add_messages: Post messages
742 permission_view_calendar: View calendrier
670 permission_view_messages: View messages
743 permission_manage_members: Manage members
671 permission_manage_files: Manage files
744 permission_edit_messages: Edit messages
672 permission_edit_issue_notes: Edit notes
745 permission_delete_issues: Delete issues
673 permission_manage_news: Manage news
746 permission_view_issue_watchers: View watchers list
674 permission_view_calendar: View calendrier
747 permission_manage_repository: Manage repository
675 permission_manage_members: Manage members
748 permission_commit_access: Commit access
676 permission_edit_messages: Edit messages
749 permission_browse_repository: Browse repository
677 permission_delete_issues: Delete issues
750 permission_view_documents: View documents
678 permission_view_issue_watchers: View watchers list
751 permission_edit_project: Edit project
679 permission_manage_repository: Manage repository
752 permission_add_issue_notes: Add notes
680 permission_commit_access: Commit access
753 permission_save_queries: Save queries
681 permission_browse_repository: Browse repository
754 permission_view_wiki_pages: View wiki
682 permission_view_documents: View documents
755 permission_rename_wiki_pages: Rename wiki pages
683 permission_edit_project: Edit project
756 permission_edit_time_entries: Edit time logs
684 permission_add_issue_notes: Add notes
757 permission_edit_own_issue_notes: Edit own notes
685 permission_save_queries: Save queries
758 setting_gravatar_enabled: Use Gravatar user icons
686 permission_view_wiki_pages: View wiki
759 label_example: Example
687 permission_rename_wiki_pages: Rename wiki pages
760 text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
688 permission_edit_time_entries: Edit time logs
761 permission_edit_own_messages: Edit own messages
689 permission_edit_own_issue_notes: Edit own notes
762 permission_delete_own_messages: Delete own messages
690 setting_gravatar_enabled: Use Gravatar user icons
763 label_user_activity: "{{value}}'s activity"
691 label_example: Example
764 label_updated_time_by: "Updated by {{author}} {{age}} ago"
692 text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
765 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
693 permission_edit_own_messages: Edit own messages
766 setting_diff_max_lines_displayed: Max number of diff lines displayed
694 permission_delete_own_messages: Delete own messages
767 text_plugin_assets_writable: Plugin assets directory writable
695 label_user_activity: "%s's activity"
768 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
696 label_updated_time_by: Updated by %s %s ago
769 button_create_and_continue: Create and continue
697 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
770 text_custom_field_possible_values_info: 'One line for each value'
698 setting_diff_max_lines_displayed: Max number of diff lines displayed
771 label_display: Display
699 text_plugin_assets_writable: Plugin assets directory writable
772 field_editable: Editable
700 warning_attachments_not_saved: "%d file(s) could not be saved."
773 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
701 button_create_and_continue: Create and continue
702 text_custom_field_possible_values_info: 'One line for each value'
703 label_display: Display
704 field_editable: Editable
705 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
706 field_identity_url: OpenID URL
707 setting_openid: Allow OpenID login and registration
708 label_login_with_open_id_option: or login with OpenID
709 field_watcher: Watcher
This diff has been collapsed as it changes many lines, (1480 lines changed) Show them Hide them
@@ -1,710 +1,774
1 _gloc_rule_default: '|n| n==1 ? "" : "_plural" '
1 ca:
2 date:
3 formats:
4 # Use the strftime parameters for formats.
5 # When no format has been given, it uses default.
6 # You can provide other formats here if you like!
7 default: "%Y-%m-%d"
8 short: "%b %d"
9 long: "%B %d, %Y"
10
11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
13
14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
17 # Used in date_select and datime_select.
18 order: [ :year, :month, :day ]
2
19
3 actionview_datehelper_select_day_prefix:
20 time:
4 actionview_datehelper_select_month_names: Gener,Febrer,Març,Abril,Maig,Juny,Juliol,Agost,Setembre,Octubre,Novembre,Desembre
21 formats:
5 actionview_datehelper_select_month_names_abbr: Gen,Feb,Mar,Abr,Mai,Jun,Jul,Ago,Set,Oct,Nov,Dec
22 default: "%a, %d %b %Y %H:%M:%S %z"
6 actionview_datehelper_select_month_prefix:
23 short: "%d %b %H:%M"
7 actionview_datehelper_select_year_prefix:
24 long: "%B %d, %Y %H:%M"
8 actionview_datehelper_time_in_words_day: 1 dia
25 am: "am"
9 actionview_datehelper_time_in_words_day_plural: %d dies
26 pm: "pm"
10 actionview_datehelper_time_in_words_hour_about: aproximadament una hora
27
11 actionview_datehelper_time_in_words_hour_about_plural: aproximadament %d hores
28 datetime:
12 actionview_datehelper_time_in_words_hour_about_single: aproximadament una hora
29 distance_in_words:
13 actionview_datehelper_time_in_words_minute: 1 minut
30 half_a_minute: "half a minute"
14 actionview_datehelper_time_in_words_minute_half: mig minut
31 less_than_x_seconds:
15 actionview_datehelper_time_in_words_minute_less_than: "menys d'un minut"
32 one: "less than 1 second"
16 actionview_datehelper_time_in_words_minute_plural: %d minuts
33 other: "less than {{count}} seconds"
17 actionview_datehelper_time_in_words_minute_single: 1 minut
34 x_seconds:
18 actionview_datehelper_time_in_words_second_less_than: "menys d'un segon"
35 one: "1 second"
19 actionview_datehelper_time_in_words_second_less_than_plural: menys de %d segons
36 other: "{{count}} seconds"
20 actionview_instancetag_blank_option: Seleccioneu
37 less_than_x_minutes:
38 one: "less than a minute"
39 other: "less than {{count}} minutes"
40 x_minutes:
41 one: "1 minute"
42 other: "{{count}} minutes"
43 about_x_hours:
44 one: "about 1 hour"
45 other: "about {{count}} hours"
46 x_days:
47 one: "1 day"
48 other: "{{count}} days"
49 about_x_months:
50 one: "about 1 month"
51 other: "about {{count}} months"
52 x_months:
53 one: "1 month"
54 other: "{{count}} months"
55 about_x_years:
56 one: "about 1 year"
57 other: "about {{count}} years"
58 over_x_years:
59 one: "over 1 year"
60 other: "over {{count}} years"
61
62 # Used in array.to_sentence.
63 support:
64 array:
65 sentence_connector: "and"
66 skip_last_comma: false
67
68 activerecord:
69 errors:
70 messages:
71 inclusion: "no està inclòs a la llista"
72 exclusion: "està reservat"
73 invalid: "no és vàlid"
74 confirmation: "la confirmació no coincideix"
75 accepted: "s'ha d'acceptar"
76 empty: "no pot estar buit"
77 blank: "no pot estar en blanc"
78 too_long: "és massa llarg"
79 too_short: "és massa curt"
80 wrong_length: "la longitud és incorrecta"
81 taken: "ja s'està utilitzant"
82 not_a_number: "no és un número"
83 not_a_date: "no és una data vàlida"
84 greater_than: "must be greater than {{count}}"
85 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
86 equal_to: "must be equal to {{count}}"
87 less_than: "must be less than {{count}}"
88 less_than_or_equal_to: "must be less than or equal to {{count}}"
89 odd: "must be odd"
90 even: "must be even"
91 greater_than_start_date: "ha de ser superior que la data inicial"
92 not_same_project: "no pertany al mateix projecte"
93 circular_dependency: "Aquesta relació crearia una dependència circular"
21
94
22 activerecord_error_inclusion: no està inclòs a la llista
95 actionview_instancetag_blank_option: Seleccioneu
23 activerecord_error_exclusion: està reservat
96
24 activerecord_error_invalid: no és vàlid
97 general_text_No: 'No'
25 activerecord_error_confirmation: la confirmació no coincideix
98 general_text_Yes: 'Si'
26 activerecord_error_accepted: "s'ha d'acceptar"
99 general_text_no: 'no'
27 activerecord_error_empty: no pot estar buit
100 general_text_yes: 'si'
28 activerecord_error_blank: no pot estar en blanc
101 general_lang_name: 'Català'
29 activerecord_error_too_long: és massa llarg
102 general_csv_separator: ';'
30 activerecord_error_too_short: és massa curt
103 general_csv_decimal_separator: ','
31 activerecord_error_wrong_length: la longitud és incorrecta
104 general_csv_encoding: ISO-8859-15
32 activerecord_error_taken: "ja s'està utilitzant"
105 general_pdf_encoding: ISO-8859-15
33 activerecord_error_not_a_number: no és un número
106 general_first_day_of_week: '1'
34 activerecord_error_not_a_date: no és una data vàlida
107
35 activerecord_error_greater_than_start_date: ha de ser superior que la data inicial
108 notice_account_updated: "El compte s'ha actualitzat correctament."
36 activerecord_error_not_same_project: no pertany al mateix projecte
109 notice_account_invalid_creditentials: Usuari o contrasenya invàlid
37 activerecord_error_circular_dependency: Aquesta relació crearia una dependència circular
110 notice_account_password_updated: "La contrasenya s'ha modificat correctament."
38
111 notice_account_wrong_password: Contrasenya incorrecta
39 general_fmt_age: %d any
112 notice_account_register_done: "El compte s'ha creat correctament. Per a activar el compte, feu clic en l'enllaç que us han enviat per correu electrònic."
40 general_fmt_age_plural: %d anys
113 notice_account_unknown_email: Usuari desconegut.
41 general_fmt_date: %%d/%%m/%%Y
114 notice_can_t_change_password: "Aquest compte utilitza una font d'autenticació externa. No és possible canviar la contrasenya."
42 general_fmt_datetime: %%d/%%m/%%Y %%H:%%M
115 notice_account_lost_email_sent: "S'ha enviat un correu electrònic amb instruccions per a seleccionar una contrasenya nova."
43 general_fmt_datetime_short: %%d/%%m %%H:%%M
116 notice_account_activated: "El compte s'ha activat. Ara podeu entrar."
44 general_fmt_time: %%H:%%M
117 notice_successful_create: "S'ha creat correctament."
45 general_text_No: 'No'
118 notice_successful_update: "S'ha modificat correctament."
46 general_text_Yes: 'Si'
119 notice_successful_delete: "S'ha suprimit correctament."
47 general_text_no: 'no'
120 notice_successful_connection: "S'ha connectat correctament."
48 general_text_yes: 'si'
121 notice_file_not_found: "La pàgina a la que intenteu accedir no existeix o s'ha suprimit."
49 general_lang_name: 'Català'
122 notice_locking_conflict: Un altre usuari ha actualitzat les dades.
50 general_csv_separator: ';'
123 notice_not_authorized: No teniu permís per a accedir a aquesta pàgina.
51 general_csv_decimal_separator: ','
124 notice_email_sent: "S'ha enviat un correu electrònic a {{value}}"
52 general_csv_encoding: ISO-8859-15
125 notice_email_error: "S'ha produït un error en enviar el correu ({{value}})"
53 general_pdf_encoding: ISO-8859-15
126 notice_feeds_access_key_reseted: "S'ha reiniciat la clau d'accés del RSS."
54 general_day_names: Dilluns,Dimarts,Dimecres,Dijous,Divendres,Dissabte,Diumenge
127 notice_failed_to_save_issues: "No s'han pogut desar %s assumptes de {{count}} seleccionats: {{value}}."
55 general_first_day_of_week: '1'
128 notice_no_issue_selected: "No s'ha seleccionat cap assumpte. Activeu els assumptes que voleu editar."
56
129 notice_account_pending: "S'ha creat el compte i ara està pendent de l'aprovació de l'administrador."
57 notice_account_updated: "El compte s'ha actualitzat correctament."
130 notice_default_data_loaded: "S'ha carregat correctament la configuració predeterminada."
58 notice_account_invalid_creditentials: Usuari o contrasenya invàlid
131 notice_unable_delete_version: "No s'ha pogut suprimir la versió."
59 notice_account_password_updated: "La contrasenya s'ha modificat correctament."
132
60 notice_account_wrong_password: Contrasenya incorrecta
133 error_can_t_load_default_data: "No s'ha pogut carregar la configuració predeterminada: {{value}} "
61 notice_account_register_done: "El compte s'ha creat correctament. Per a activar el compte, feu clic en l'enllaç que us han enviat per correu electrònic."
134 error_scm_not_found: "No s'ha trobat l'entrada o la revisió en el dipòsit."
62 notice_account_unknown_email: Usuari desconegut.
135 error_scm_command_failed: "S'ha produït un error en intentar accedir al dipòsit: {{value}}"
63 notice_can_t_change_password: "Aquest compte utilitza una font d'autenticació externa. No és possible canviar la contrasenya."
136 error_scm_annotate: "L'entrada no existeix o no s'ha pogut anotar."
64 notice_account_lost_email_sent: "S'ha enviat un correu electrònic amb instruccions per a seleccionar una contrasenya nova."
137 error_issue_not_found_in_project: "No s'ha trobat l'assumpte o no pertany a aquest projecte"
65 notice_account_activated: "El compte s'ha activat. Ara podeu entrar."
138
66 notice_successful_create: "S'ha creat correctament."
139 mail_subject_lost_password: "Contrasenya de {{value}}"
67 notice_successful_update: "S'ha modificat correctament."
140 mail_body_lost_password: "Per a canviar la contrasenya, feu clic en l'enllaç següent:"
68 notice_successful_delete: "S'ha suprimit correctament."
141 mail_subject_register: "Activació del compte de {{value}}"
69 notice_successful_connection: "S'ha connectat correctament."
142 mail_body_register: "Per a activar el compte, feu clic en l'enllaç següent:"
70 notice_file_not_found: "La pàgina a la que intenteu accedir no existeix o s'ha suprimit."
143 mail_body_account_information_external: "Podeu utilitzar el compte «{{value}}» per a entrar."
71 notice_locking_conflict: Un altre usuari ha actualitzat les dades.
144 mail_body_account_information: Informació del compte
72 notice_not_authorized: No teniu permís per a accedir a aquesta pàgina.
145 mail_subject_account_activation_request: "Sol·licitud d'activació del compte de {{value}}"
73 notice_email_sent: "S'ha enviat un correu electrònic a %s"
146 mail_body_account_activation_request: "S'ha registrat un usuari nou ({{value}}). El seu compte està pendent d'aprovació:"
74 notice_email_error: "S'ha produït un error en enviar el correu (%s)"
147 mail_subject_reminder: "%d assumptes venceran els següents {{count}} dies"
75 notice_feeds_access_key_reseted: "S'ha reiniciat la clau d'accés del RSS."
148 mail_body_reminder: "{{count}} assumptes que teniu assignades venceran els següents {{days}} dies:"
76 notice_failed_to_save_issues: "No s'han pogut desar %s assumptes de %d seleccionats: %s."
149
77 notice_no_issue_selected: "No s'ha seleccionat cap assumpte. Activeu els assumptes que voleu editar."
150 gui_validation_error: 1 error
78 notice_account_pending: "S'ha creat el compte i ara està pendent de l'aprovació de l'administrador."
151 gui_validation_error_plural: "{{count}} errors"
79 notice_default_data_loaded: "S'ha carregat correctament la configuració predeterminada."
152
80 notice_unable_delete_version: "No s'ha pogut suprimir la versió."
153 field_name: Nom
81
154 field_description: Descripció
82 error_can_t_load_default_data: "No s'ha pogut carregar la configuració predeterminada: %s"
155 field_summary: Resum
83 error_scm_not_found: "No s'ha trobat l'entrada o la revisió en el dipòsit."
156 field_is_required: Necessari
84 error_scm_command_failed: "S'ha produït un error en intentar accedir al dipòsit: %s"
157 field_firstname: Nom
85 error_scm_annotate: "L'entrada no existeix o no s'ha pogut anotar."
158 field_lastname: Cognom
86 error_issue_not_found_in_project: "No s'ha trobat l'assumpte o no pertany a aquest projecte"
159 field_mail: Correu electrònic
87
160 field_filename: Fitxer
88 mail_subject_lost_password: Contrasenya de %s
161 field_filesize: Mida
89 mail_body_lost_password: "Per a canviar la contrasenya, feu clic en l'enllaç següent:"
162 field_downloads: Baixades
90 mail_subject_register: Activació del compte de %s
163 field_author: Autor
91 mail_body_register: "Per a activar el compte, feu clic en l'enllaç següent:"
164 field_created_on: Creat
92 mail_body_account_information_external: Podeu utilitzar el compte «%s» per a entrar.
165 field_updated_on: Actualitzat
93 mail_body_account_information: Informació del compte
166 field_field_format: Format
94 mail_subject_account_activation_request: "Sol·licitud d'activació del compte de %s"
167 field_is_for_all: Per a tots els projectes
95 mail_body_account_activation_request: "S'ha registrat un usuari nou (%s). El seu compte està pendent d'aprovació:"
168 field_possible_values: Valores possibles
96 mail_subject_reminder: "%d assumptes venceran els següents %d dies"
169 field_regexp: Expressió regular
97 mail_body_reminder: "%d assumptes que teniu assignades venceran els següents %d dies:"
170 field_min_length: Longitud mínima
98
171 field_max_length: Longitud màxima
99 gui_validation_error: 1 error
172 field_value: Valor
100 gui_validation_error_plural: %d errors
173 field_category: Categoria
101
174 field_title: Títol
102 field_name: Nom
175 field_project: Projecte
103 field_description: Descripció
176 field_issue: Assumpte
104 field_summary: Resum
177 field_status: Estat
105 field_is_required: Necessari
178 field_notes: Notes
106 field_firstname: Nom
179 field_is_closed: Assumpte tancat
107 field_lastname: Cognom
180 field_is_default: Estat predeterminat
108 field_mail: Correu electrònic
181 field_tracker: Seguidor
109 field_filename: Fitxer
182 field_subject: Tema
110 field_filesize: Mida
183 field_due_date: Data de venciment
111 field_downloads: Baixades
184 field_assigned_to: Assignat a
112 field_author: Autor
185 field_priority: Prioritat
113 field_created_on: Creat
186 field_fixed_version: Versió objectiu
114 field_updated_on: Actualitzat
187 field_user: Usuari
115 field_field_format: Format
188 field_role: Rol
116 field_is_for_all: Per a tots els projectes
189 field_homepage: Pàgina web
117 field_possible_values: Valores possibles
190 field_is_public: Públic
118 field_regexp: Expressió regular
191 field_parent: Subprojecte de
119 field_min_length: Longitud mínima
192 field_is_in_chlog: Assumptes mostrats en el registre de canvis
120 field_max_length: Longitud màxima
193 field_is_in_roadmap: Assumptes mostrats en la planificació
121 field_value: Valor
194 field_login: Entrada
122 field_category: Categoria
195 field_mail_notification: Notificacions per correu electrònic
123 field_title: Títol
196 field_admin: Administrador
124 field_project: Projecte
197 field_last_login_on: Última connexió
125 field_issue: Assumpte
198 field_language: Idioma
126 field_status: Estat
199 field_effective_date: Data
127 field_notes: Notes
200 field_password: Contrasenya
128 field_is_closed: Assumpte tancat
201 field_new_password: Contrasenya nova
129 field_is_default: Estat predeterminat
202 field_password_confirmation: Confirmació
130 field_tracker: Seguidor
203 field_version: Versió
131 field_subject: Tema
204 field_type: Tipus
132 field_due_date: Data de venciment
205 field_host: Ordinador
133 field_assigned_to: Assignat a
206 field_port: Port
134 field_priority: Prioritat
207 field_account: Compte
135 field_fixed_version: Versió objectiu
208 field_base_dn: Base DN
136 field_user: Usuari
209 field_attr_login: "Atribut d'entrada"
137 field_role: Rol
210 field_attr_firstname: Atribut del nom
138 field_homepage: Pàgina web
211 field_attr_lastname: Atribut del cognom
139 field_is_public: Públic
212 field_attr_mail: Atribut del correu electrònic
140 field_parent: Subprojecte de
213 field_onthefly: "Creació de l'usuari «al vol»"
141 field_is_in_chlog: Assumptes mostrats en el registre de canvis
214 field_start_date: Inici
142 field_is_in_roadmap: Assumptes mostrats en la planificació
215 field_done_ratio: %% realitzat
143 field_login: Entrada
216 field_auth_source: "Mode d'autenticació"
144 field_mail_notification: Notificacions per correu electrònic
217 field_hide_mail: "Oculta l'adreça de correu electrònic"
145 field_admin: Administrador
218 field_comments: Comentari
146 field_last_login_on: Última connexió
219 field_url: URL
147 field_language: Idioma
220 field_start_page: Pàgina inicial
148 field_effective_date: Data
221 field_subproject: Subprojecte
149 field_password: Contrasenya
222 field_hours: Hores
150 field_new_password: Contrasenya nova
223 field_activity: Activitat
151 field_password_confirmation: Confirmació
224 field_spent_on: Data
152 field_version: Versió
225 field_identifier: Identificador
153 field_type: Tipus
226 field_is_filter: "S'ha utilitzat com a filtre"
154 field_host: Ordinador
227 field_issue_to_id: Assumpte relacionat
155 field_port: Port
228 field_delay: Retard
156 field_account: Compte
229 field_assignable: Es poden assignar assumptes a aquest rol
157 field_base_dn: Base DN
230 field_redirect_existing_links: Redirigeix els enllaços existents
158 field_attr_login: "Atribut d'entrada"
231 field_estimated_hours: Temps previst
159 field_attr_firstname: Atribut del nom
232 field_column_names: Columnes
160 field_attr_lastname: Atribut del cognom
233 field_time_zone: Zona horària
161 field_attr_mail: Atribut del correu electrònic
234 field_searchable: Es pot cercar
162 field_onthefly: "Creació de l'usuari «al vol»"
235 field_default_value: Valor predeterminat
163 field_start_date: Inici
236 field_comments_sorting: Mostra els comentaris
164 field_done_ratio: %% realitzat
237 field_parent_title: Pàgina pare
165 field_auth_source: "Mode d'autenticació"
238
166 field_hide_mail: "Oculta l'adreça de correu electrònic"
239 setting_app_title: "Títol de l'aplicació"
167 field_comments: Comentari
240 setting_app_subtitle: "Subtítol de l'aplicació"
168 field_url: URL
241 setting_welcome_text: Text de benvinguda
169 field_start_page: Pàgina inicial
242 setting_default_language: Idioma predeterminat
170 field_subproject: Subprojecte
243 setting_login_required: Es necessita autenticació
171 field_hours: Hores
244 setting_self_registration: Registre automàtic
172 field_activity: Activitat
245 setting_attachment_max_size: Mida màxima dels adjunts
173 field_spent_on: Data
246 setting_issues_export_limit: "Límit d'exportació d'assumptes"
174 field_identifier: Identificador
247 setting_mail_from: "Adreça de correu electrònic d'emissió"
175 field_is_filter: "S'ha utilitzat com a filtre"
248 setting_bcc_recipients: Vincula els destinataris de les còpies amb carbó (bcc)
176 field_issue_to_id: Assumpte relacionat
249 setting_host_name: "Nom de l'ordinador"
177 field_delay: Retard
250 setting_text_formatting: Format del text
178 field_assignable: Es poden assignar assumptes a aquest rol
251 setting_wiki_compression: "Comprimeix l'historial del wiki"
179 field_redirect_existing_links: Redirigeix els enllaços existents
252 setting_feeds_limit: Límit de contingut del canal
180 field_estimated_hours: Temps previst
253 setting_default_projects_public: Els projectes nous són públics per defecte
181 field_column_names: Columnes
254 setting_autofetch_changesets: Omple automàticament les publicacions
182 field_time_zone: Zona horària
255 setting_sys_api_enabled: Habilita el WS per a la gestió del dipòsit
183 field_searchable: Es pot cercar
256 setting_commit_ref_keywords: Paraules claus per a la referència
184 field_default_value: Valor predeterminat
257 setting_commit_fix_keywords: Paraules claus per a la correcció
185 field_comments_sorting: Mostra els comentaris
258 setting_autologin: Entrada automàtica
186 field_parent_title: Pàgina pare
259 setting_date_format: Format de la data
187
260 setting_time_format: Format de hora
188 setting_app_title: "Títol de l'aplicació"
261 setting_cross_project_issue_relations: "Permet les relacions d'assumptes entre projectes"
189 setting_app_subtitle: "Subtítol de l'aplicació"
262 setting_issue_list_default_columns: "Columnes mostrades per defecte en la llista d'assumptes"
190 setting_welcome_text: Text de benvinguda
263 setting_repositories_encodings: Codificacions del dipòsit
191 setting_default_language: Idioma predeterminat
264 setting_commit_logs_encoding: Codificació dels missatges publicats
192 setting_login_required: Es necessita autenticació
265 setting_emails_footer: Peu dels correus electrònics
193 setting_self_registration: Registre automàtic
266 setting_protocol: Protocol
194 setting_attachment_max_size: Mida màxima dels adjunts
267 setting_per_page_options: Opcions dels objectes per pàgina
195 setting_issues_export_limit: "Límit d'exportació d'assumptes"
268 setting_user_format: "Format de com mostrar l'usuari"
196 setting_mail_from: "Adreça de correu electrònic d'emissió"
269 setting_activity_days_default: "Dies a mostrar l'activitat del projecte"
197 setting_bcc_recipients: Vincula els destinataris de les còpies amb carbó (bcc)
270 setting_display_subprojects_issues: "Mostra els assumptes d'un subprojecte en el projecte pare per defecte"
198 setting_host_name: "Nom de l'ordinador"
271 setting_enabled_scm: "Habilita l'SCM"
199 setting_text_formatting: Format del text
272 setting_mail_handler_api_enabled: "Habilita el WS per correus electrònics d'entrada"
200 setting_wiki_compression: "Comprimeix l'historial del wiki"
273 setting_mail_handler_api_key: Clau API
201 setting_feeds_limit: Límit de contingut del canal
274 setting_sequential_project_identifiers: Genera identificadors de projecte seqüencials
202 setting_default_projects_public: Els projectes nous són públics per defecte
275
203 setting_autofetch_changesets: Omple automàticament les publicacions
276 project_module_issue_tracking: "Seguidor d'assumptes"
204 setting_sys_api_enabled: Habilita el WS per a la gestió del dipòsit
277 project_module_time_tracking: Seguidor de temps
205 setting_commit_ref_keywords: Paraules claus per a la referència
278 project_module_news: Noticies
206 setting_commit_fix_keywords: Paraules claus per a la correcció
279 project_module_documents: Documents
207 setting_autologin: Entrada automàtica
280 project_module_files: Fitxers
208 setting_date_format: Format de la data
281 project_module_wiki: Wiki
209 setting_time_format: Format de hora
282 project_module_repository: Dipòsit
210 setting_cross_project_issue_relations: "Permet les relacions d'assumptes entre projectes"
283 project_module_boards: Taulers
211 setting_issue_list_default_columns: "Columnes mostrades per defecte en la llista d'assumptes"
284
212 setting_repositories_encodings: Codificacions del dipòsit
285 label_user: Usuari
213 setting_commit_logs_encoding: Codificació dels missatges publicats
286 label_user_plural: Usuaris
214 setting_emails_footer: Peu dels correus electrònics
287 label_user_new: Usuari nou
215 setting_protocol: Protocol
288 label_project: Projecte
216 setting_per_page_options: Opcions dels objectes per pàgina
289 label_project_new: Projecte nou
217 setting_user_format: "Format de com mostrar l'usuari"
290 label_project_plural: Projectes
218 setting_activity_days_default: "Dies a mostrar l'activitat del projecte"
291 label_x_projects:
219 setting_display_subprojects_issues: "Mostra els assumptes d'un subprojecte en el projecte pare per defecte"
292 zero: no projects
220 setting_enabled_scm: "Habilita l'SCM"
293 one: 1 project
221 setting_mail_handler_api_enabled: "Habilita el WS per correus electrònics d'entrada"
294 other: "{{count}} projects"
222 setting_mail_handler_api_key: Clau API
295 label_project_all: Tots els projectes
223 setting_sequential_project_identifiers: Genera identificadors de projecte seqüencials
296 label_project_latest: Els últims projectes
224
297 label_issue: Assumpte
225 project_module_issue_tracking: "Seguidor d'assumptes"
298 label_issue_new: Assumpte nou
226 project_module_time_tracking: Seguidor de temps
299 label_issue_plural: Assumptes
227 project_module_news: Noticies
300 label_issue_view_all: Visualitza tots els assumptes
228 project_module_documents: Documents
301 label_issues_by: "Assumptes per {{value}}"
229 project_module_files: Fitxers
302 label_issue_added: Assumpte afegit
230 project_module_wiki: Wiki
303 label_issue_updated: Assumpte actualitzat
231 project_module_repository: Dipòsit
304 label_document: Document
232 project_module_boards: Taulers
305 label_document_new: Document nou
233
306 label_document_plural: Documents
234 label_user: Usuari
307 label_document_added: Document afegit
235 label_user_plural: Usuaris
308 label_role: Rol
236 label_user_new: Usuari nou
309 label_role_plural: Rols
237 label_project: Projecte
310 label_role_new: Rol nou
238 label_project_new: Projecte nou
311 label_role_and_permissions: Rols i permisos
239 label_project_plural: Projectes
312 label_member: Membre
240 label_project_all: Tots els projectes
313 label_member_new: Membre nou
241 label_project_latest: Els últims projectes
314 label_member_plural: Membres
242 label_issue: Assumpte
315 label_tracker: Seguidor
243 label_issue_new: Assumpte nou
316 label_tracker_plural: Seguidors
244 label_issue_plural: Assumptes
317 label_tracker_new: Seguidor nou
245 label_issue_view_all: Visualitza tots els assumptes
318 label_workflow: Flux de treball
246 label_issues_by: Assumptes per %s
319 label_issue_status: "Estat de l'assumpte"
247 label_issue_added: Assumpte afegit
320 label_issue_status_plural: "Estats de l'assumpte"
248 label_issue_updated: Assumpte actualitzat
321 label_issue_status_new: Estat nou
249 label_document: Document
322 label_issue_category: "Categoria de l'assumpte"
250 label_document_new: Document nou
323 label_issue_category_plural: "Categories de l'assumpte"
251 label_document_plural: Documents
324 label_issue_category_new: Categoria nova
252 label_document_added: Document afegit
325 label_custom_field: Camp personalitzat
253 label_role: Rol
326 label_custom_field_plural: Camps personalitzats
254 label_role_plural: Rols
327 label_custom_field_new: Camp personalitzat nou
255 label_role_new: Rol nou
328 label_enumerations: Enumeracions
256 label_role_and_permissions: Rols i permisos
329 label_enumeration_new: Valor nou
257 label_member: Membre
330 label_information: Informació
258 label_member_new: Membre nou
331 label_information_plural: Informació
259 label_member_plural: Membres
332 label_please_login: Entreu
260 label_tracker: Seguidor
333 label_register: Registre
261 label_tracker_plural: Seguidors
334 label_password_lost: Contrasenya perduda
262 label_tracker_new: Seguidor nou
335 label_home: Inici
263 label_workflow: Flux de treball
336 label_my_page: La meva pàgina
264 label_issue_status: "Estat de l'assumpte"
337 label_my_account: El meu compte
265 label_issue_status_plural: "Estats de l'assumpte"
338 label_my_projects: Els meus projectes
266 label_issue_status_new: Estat nou
339 label_administration: Administració
267 label_issue_category: "Categoria de l'assumpte"
340 label_login: Entra
268 label_issue_category_plural: "Categories de l'assumpte"
341 label_logout: Surt
269 label_issue_category_new: Categoria nova
342 label_help: Ajuda
270 label_custom_field: Camp personalitzat
343 label_reported_issues: Assumptes informats
271 label_custom_field_plural: Camps personalitzats
344 label_assigned_to_me_issues: Assumptes assignats a mi
272 label_custom_field_new: Camp personalitzat nou
345 label_last_login: Última connexió
273 label_enumerations: Enumeracions
346 label_registered_on: Informat el
274 label_enumeration_new: Valor nou
347 label_activity: Activitat
275 label_information: Informació
348 label_overall_activity: Activitat global
276 label_information_plural: Informació
349 label_new: Nou
277 label_please_login: Entreu
350 label_logged_as: Heu entrat com a
278 label_register: Registre
351 label_environment: Entorn
279 label_password_lost: Contrasenya perduda
352 label_authentication: Autenticació
280 label_home: Inici
353 label_auth_source: "Mode d'autenticació"
281 label_my_page: La meva pàgina
354 label_auth_source_new: "Mode d'autenticació nou"
282 label_my_account: El meu compte
355 label_auth_source_plural: "Modes d'autenticació"
283 label_my_projects: Els meus projectes
356 label_subproject_plural: Subprojectes
284 label_administration: Administració
357 label_and_its_subprojects: "{{value}} i els seus subprojectes"
285 label_login: Entra
358 label_min_max_length: Longitud mín - max
286 label_logout: Surt
359 label_list: Llist
287 label_help: Ajuda
360 label_date: Data
288 label_reported_issues: Assumptes informats
361 label_integer: Enter
289 label_assigned_to_me_issues: Assumptes assignats a mi
362 label_float: Flotant
290 label_last_login: Última connexió
363 label_boolean: Booleà
291 label_last_updates: Última actualització
364 label_string: Text
292 label_last_updates_plural: %d última actualització
365 label_text: Text llarg
293 label_registered_on: Informat el
366 label_attribute: Atribut
294 label_activity: Activitat
367 label_attribute_plural: Atributs
295 label_overall_activity: Activitat global
368 label_download: "{{count}} baixada"
296 label_new: Nou
369 label_download_plural: "{{count}} baixades"
297 label_logged_as: Heu entrat com a
370 label_no_data: Sense dades a mostrar
298 label_environment: Entorn
371 label_change_status: "Canvia l'estat"
299 label_authentication: Autenticació
372 label_history: Historial
300 label_auth_source: "Mode d'autenticació"
373 label_attachment: Fitxer
301 label_auth_source_new: "Mode d'autenticació nou"
374 label_attachment_new: Fitxer nou
302 label_auth_source_plural: "Modes d'autenticació"
375 label_attachment_delete: Suprimeix el fitxer
303 label_subproject_plural: Subprojectes
376 label_attachment_plural: Fitxers
304 label_and_its_subprojects: %s i els seus subprojectes
377 label_file_added: Fitxer afegit
305 label_min_max_length: Longitud mín - max
378 label_report: Informe
306 label_list: Llist
379 label_report_plural: Informes
307 label_date: Data
380 label_news: Noticies
308 label_integer: Enter
381 label_news_new: Afegeix noticies
309 label_float: Flotant
382 label_news_plural: Noticies
310 label_boolean: Booleà
383 label_news_latest: Últimes noticies
311 label_string: Text
384 label_news_view_all: Visualitza totes les noticies
312 label_text: Text llarg
385 label_news_added: Noticies afegides
313 label_attribute: Atribut
386 label_change_log: Registre de canvis
314 label_attribute_plural: Atributs
387 label_settings: Paràmetres
315 label_download: %d baixada
388 label_overview: Resum
316 label_download_plural: %d baixades
389 label_version: Versió
317 label_no_data: Sense dades a mostrar
390 label_version_new: Versió nova
318 label_change_status: "Canvia l'estat"
391 label_version_plural: Versions
319 label_history: Historial
392 label_confirmation: Confirmació
320 label_attachment: Fitxer
393 label_export_to: 'També disponible a:'
321 label_attachment_new: Fitxer nou
394 label_read: Llegeix...
322 label_attachment_delete: Suprimeix el fitxer
395 label_public_projects: Projectes públics
323 label_attachment_plural: Fitxers
396 label_open_issues: obert
324 label_file_added: Fitxer afegit
397 label_open_issues_plural: oberts
325 label_report: Informe
398 label_closed_issues: tancat
326 label_report_plural: Informes
399 label_closed_issues_plural: tancats
327 label_news: Noticies
400 label_x_open_issues_abbr_on_total:
328 label_news_new: Afegeix noticies
401 zero: 0 open / {{total}}
329 label_news_plural: Noticies
402 one: 1 open / {{total}}
330 label_news_latest: Últimes noticies
403 other: "{{count}} open / {{total}}"
331 label_news_view_all: Visualitza totes les noticies
404 label_x_open_issues_abbr:
332 label_news_added: Noticies afegides
405 zero: 0 open
333 label_change_log: Registre de canvis
406 one: 1 open
334 label_settings: Paràmetres
407 other: "{{count}} open"
335 label_overview: Resum
408 label_x_closed_issues_abbr:
336 label_version: Versió
409 zero: 0 closed
337 label_version_new: Versió nova
410 one: 1 closed
338 label_version_plural: Versions
411 other: "{{count}} closed"
339 label_confirmation: Confirmació
412 label_total: Total
340 label_export_to: 'També disponible a:'
413 label_permissions: Permisos
341 label_read: Llegeix...
414 label_current_status: Estat actual
342 label_public_projects: Projectes públics
415 label_new_statuses_allowed: Nous estats autoritzats
343 label_open_issues: obert
416 label_all: tots
344 label_open_issues_plural: oberts
417 label_none: cap
345 label_closed_issues: tancat
418 label_nobody: ningú
346 label_closed_issues_plural: tancats
419 label_next: Següent
347 label_total: Total
420 label_previous: Anterior
348 label_permissions: Permisos
421 label_used_by: Utilitzat per
349 label_current_status: Estat actual
422 label_details: Detalls
350 label_new_statuses_allowed: Nous estats autoritzats
423 label_add_note: Afegeix una nota
351 label_all: tots
424 label_per_page: Per pàgina
352 label_none: cap
425 label_calendar: Calendari
353 label_nobody: ningú
426 label_months_from: mesos des de
354 label_next: Següent
427 label_gantt: Gantt
355 label_previous: Anterior
428 label_internal: Intern
356 label_used_by: Utilitzat per
429 label_last_changes: "últims {{count}} canvis"
357 label_details: Detalls
430 label_change_view_all: Visualitza tots els canvis
358 label_add_note: Afegeix una nota
431 label_personalize_page: Personalitza aquesta pàgina
359 label_per_page: Per pàgina
432 label_comment: Comentari
360 label_calendar: Calendari
433 label_comment_plural: Comentaris
361 label_months_from: mesos des de
434 label_x_comments:
362 label_gantt: Gantt
435 zero: no comments
363 label_internal: Intern
436 one: 1 comment
364 label_last_changes: últims %d canvis
437 other: "{{count}} comments"
365 label_change_view_all: Visualitza tots els canvis
438 label_comment_add: Afegeix un comentari
366 label_personalize_page: Personalitza aquesta pàgina
439 label_comment_added: Comentari afegit
367 label_comment: Comentari
440 label_comment_delete: Suprimeix comentaris
368 label_comment_plural: Comentaris
441 label_query: Consulta personalitzada
369 label_comment_add: Afegeix un comentari
442 label_query_plural: Consultes personalitzades
370 label_comment_added: Comentari afegit
443 label_query_new: Consulta nova
371 label_comment_delete: Suprimeix comentaris
444 label_filter_add: Afegeix un filtre
372 label_query: Consulta personalitzada
445 label_filter_plural: Filtres
373 label_query_plural: Consultes personalitzades
446 label_equals: és
374 label_query_new: Consulta nova
447 label_not_equals: no és
375 label_filter_add: Afegeix un filtre
448 label_in_less_than: en menys de
376 label_filter_plural: Filtres
449 label_in_more_than: en més de
377 label_equals: és
450 label_in: en
378 label_not_equals: no és
451 label_today: avui
379 label_in_less_than: en menys de
452 label_all_time: tot el temps
380 label_in_more_than: en més de
453 label_yesterday: ahir
381 label_in: en
454 label_this_week: aquesta setmana
382 label_today: avui
455 label_last_week: "l'última setmana"
383 label_all_time: tot el temps
456 label_last_n_days: "els últims {{count}} dies"
384 label_yesterday: ahir
457 label_this_month: aquest més
385 label_this_week: aquesta setmana
458 label_last_month: "l'últim més"
386 label_last_week: "l'última setmana"
459 label_this_year: aquest any
387 label_last_n_days: els últims %d dies
460 label_date_range: Abast de les dates
388 label_this_month: aquest més
461 label_less_than_ago: fa menys de
389 label_last_month: "l'últim més"
462 label_more_than_ago: fa més de
390 label_this_year: aquest any
463 label_ago: fa
391 label_date_range: Abast de les dates
464 label_contains: conté
392 label_less_than_ago: fa menys de
465 label_not_contains: no conté
393 label_more_than_ago: fa més de
466 label_day_plural: dies
394 label_ago: fa
467 label_repository: Dipòsit
395 label_contains: conté
468 label_repository_plural: Dipòsits
396 label_not_contains: no conté
469 label_browse: Navega
397 label_day_plural: dies
470 label_modification: "{{count}} canvi"
398 label_repository: Dipòsit
471 label_modification_plural: "{{count}} canvis"
399 label_repository_plural: Dipòsits
472 label_revision: Revisió
400 label_browse: Navega
473 label_revision_plural: Revisions
401 label_modification: %d canvi
474 label_associated_revisions: Revisions associades
402 label_modification_plural: %d canvis
475 label_added: afegit
403 label_revision: Revisió
476 label_modified: modificat
404 label_revision_plural: Revisions
477 label_renamed: reanomenat
405 label_associated_revisions: Revisions associades
478 label_copied: copiat
406 label_added: afegit
479 label_deleted: suprimit
407 label_modified: modificat
480 label_latest_revision: Última revisió
408 label_renamed: reanomenat
481 label_latest_revision_plural: Últimes revisions
409 label_copied: copiat
482 label_view_revisions: Visualitza les revisions
410 label_deleted: suprimit
483 label_max_size: Mida màxima
411 label_latest_revision: Última revisió
484 label_sort_highest: Mou a la part superior
412 label_latest_revision_plural: Últimes revisions
485 label_sort_higher: Mou cap amunt
413 label_view_revisions: Visualitza les revisions
486 label_sort_lower: Mou cap avall
414 label_max_size: Mida màxima
487 label_sort_lowest: Mou a la part inferior
415 label_on: 'de'
488 label_roadmap: Planificació
416 label_sort_highest: Mou a la part superior
489 label_roadmap_due_in: "Venç en {{value}}"
417 label_sort_higher: Mou cap amunt
490 label_roadmap_overdue: "{{value}} tard"
418 label_sort_lower: Mou cap avall
491 label_roadmap_no_issues: No hi ha assumptes per a aquesta versió
419 label_sort_lowest: Mou a la part inferior
492 label_search: Cerca
420 label_roadmap: Planificació
493 label_result_plural: Resultats
421 label_roadmap_due_in: Venç en %s
494 label_all_words: Totes les paraules
422 label_roadmap_overdue: %s tard
495 label_wiki: Wiki
423 label_roadmap_no_issues: No hi ha assumptes per a aquesta versió
496 label_wiki_edit: Edició wiki
424 label_search: Cerca
497 label_wiki_edit_plural: Edicions wiki
425 label_result_plural: Resultats
498 label_wiki_page: Pàgina wiki
426 label_all_words: Totes les paraules
499 label_wiki_page_plural: Pàgines wiki
427 label_wiki: Wiki
500 label_index_by_title: Índex per títol
428 label_wiki_edit: Edició wiki
501 label_index_by_date: Índex per data
429 label_wiki_edit_plural: Edicions wiki
502 label_current_version: Versió actual
430 label_wiki_page: Pàgina wiki
503 label_preview: Previsualització
431 label_wiki_page_plural: Pàgines wiki
504 label_feed_plural: Canals
432 label_index_by_title: Índex per títol
505 label_changes_details: Detalls de tots els canvis
433 label_index_by_date: Índex per data
506 label_issue_tracking: "Seguiment d'assumptes"
434 label_current_version: Versió actual
507 label_spent_time: Temps invertit
435 label_preview: Previsualització
508 label_f_hour: "{{value}} hora"
436 label_feed_plural: Canals
509 label_f_hour_plural: "{{value}} hores"
437 label_changes_details: Detalls de tots els canvis
510 label_time_tracking: Temps de seguiment
438 label_issue_tracking: "Seguiment d'assumptes"
511 label_change_plural: Canvis
439 label_spent_time: Temps invertit
512 label_statistics: Estadístiques
440 label_f_hour: %.2f hora
513 label_commits_per_month: Publicacions per mes
441 label_f_hour_plural: %.2f hores
514 label_commits_per_author: Publicacions per autor
442 label_time_tracking: Temps de seguiment
515 label_view_diff: Visualitza les diferències
443 label_change_plural: Canvis
516 label_diff_inline: en línia
444 label_statistics: Estadístiques
517 label_diff_side_by_side: costat per costat
445 label_commits_per_month: Publicacions per mes
518 label_options: Opcions
446 label_commits_per_author: Publicacions per autor
519 label_copy_workflow_from: Copia el flux de treball des de
447 label_view_diff: Visualitza les diferències
520 label_permissions_report: Informe de permisos
448 label_diff_inline: en línia
521 label_watched_issues: Assumptes vigilats
449 label_diff_side_by_side: costat per costat
522 label_related_issues: Assumptes relacionats
450 label_options: Opcions
523 label_applied_status: Estat aplicat
451 label_copy_workflow_from: Copia el flux de treball des de
524 label_loading: "S'està carregant..."
452 label_permissions_report: Informe de permisos
525 label_relation_new: Relació nova
453 label_watched_issues: Assumptes vigilats
526 label_relation_delete: Suprimeix la relació
454 label_related_issues: Assumptes relacionats
527 label_relates_to: relacionat amb
455 label_applied_status: Estat aplicat
528 label_duplicates: duplicats
456 label_loading: "S'està carregant..."
529 label_duplicated_by: duplicat per
457 label_relation_new: Relació nova
530 label_blocks: bloqueja
458 label_relation_delete: Suprimeix la relació
531 label_blocked_by: bloquejats per
459 label_relates_to: relacionat amb
532 label_precedes: anterior a
460 label_duplicates: duplicats
533 label_follows: posterior a
461 label_duplicated_by: duplicat per
534 label_end_to_start: final al començament
462 label_blocks: bloqueja
535 label_end_to_end: final al final
463 label_blocked_by: bloquejats per
536 label_start_to_start: començament al començament
464 label_precedes: anterior a
537 label_start_to_end: començament al final
465 label_follows: posterior a
538 label_stay_logged_in: "Manté l'entrada"
466 label_end_to_start: final al començament
539 label_disabled: inhabilitat
467 label_end_to_end: final al final
540 label_show_completed_versions: Mostra les versions completes
468 label_start_to_start: començament al començament
541 label_me: jo mateix
469 label_start_to_end: començament al final
542 label_board: Fòrum
470 label_stay_logged_in: "Manté l'entrada"
543 label_board_new: Fòrum nou
471 label_disabled: inhabilitat
544 label_board_plural: Fòrums
472 label_show_completed_versions: Mostra les versions completes
545 label_topic_plural: Temes
473 label_me: jo mateix
546 label_message_plural: Missatges
474 label_board: Fòrum
547 label_message_last: Últim missatge
475 label_board_new: Fòrum nou
548 label_message_new: Missatge nou
476 label_board_plural: Fòrums
549 label_message_posted: Missatge afegit
477 label_topic_plural: Temes
550 label_reply_plural: Respostes
478 label_message_plural: Missatges
551 label_send_information: "Envia la informació del compte a l'usuari"
479 label_message_last: Últim missatge
552 label_year: Any
480 label_message_new: Missatge nou
553 label_month: Mes
481 label_message_posted: Missatge afegit
554 label_week: Setmana
482 label_reply_plural: Respostes
555 label_date_from: Des de
483 label_send_information: "Envia la informació del compte a l'usuari"
556 label_date_to: A
484 label_year: Any
557 label_language_based: "Basat en l'idioma de l'usuari"
485 label_month: Mes
558 label_sort_by: "Ordena per {{value}}"
486 label_week: Setmana
559 label_send_test_email: Envia un correu electrònic de prova
487 label_date_from: Des de
560 label_feeds_access_key_created_on: "Clau d'accés del RSS creada fa {{value}}"
488 label_date_to: A
561 label_module_plural: Mòduls
489 label_language_based: "Basat en l'idioma de l'usuari"
562 label_added_time_by: "Afegit per {{author}} fa {{age}}"
490 label_sort_by: Ordena per %s
563 label_updated_time: "Actualitzat fa {{value}}"
491 label_send_test_email: Envia un correu electrònic de prova
564 label_jump_to_a_project: Salta al projecte...
492 label_feeds_access_key_created_on: "Clau d'accés del RSS creada fa %s"
565 label_file_plural: Fitxers
493 label_module_plural: Mòduls
566 label_changeset_plural: Conjunt de canvis
494 label_added_time_by: Afegit per %s fa %s
567 label_default_columns: Columnes predeterminades
495 label_updated_time: Actualitzat fa %s
568 label_no_change_option: (sense canvis)
496 label_jump_to_a_project: Salta al projecte...
569 label_bulk_edit_selected_issues: Edita en bloc els assumptes seleccionats
497 label_file_plural: Fitxers
570 label_theme: Tema
498 label_changeset_plural: Conjunt de canvis
571 label_default: Predeterminat
499 label_default_columns: Columnes predeterminades
572 label_search_titles_only: Cerca només en els títols
500 label_no_change_option: (sense canvis)
573 label_user_mail_option_all: "Per qualsevol esdeveniment en tots els meus projectes"
501 label_bulk_edit_selected_issues: Edita en bloc els assumptes seleccionats
574 label_user_mail_option_selected: "Per qualsevol esdeveniment en els projectes seleccionats..."
502 label_theme: Tema
575 label_user_mail_option_none: "Només per les coses que vigilo o hi estic implicat"
503 label_default: Predeterminat
576 label_user_mail_no_self_notified: "No vull ser notificat pels canvis que faig jo mateix"
504 label_search_titles_only: Cerca només en els títols
577 label_registration_activation_by_email: activació del compte per correu electrònic
505 label_user_mail_option_all: "Per qualsevol esdeveniment en tots els meus projectes"
578 label_registration_manual_activation: activació del compte manual
506 label_user_mail_option_selected: "Per qualsevol esdeveniment en els projectes seleccionats..."
579 label_registration_automatic_activation: activació del compte automàtica
507 label_user_mail_option_none: "Només per les coses que vigilo o hi estic implicat"
580 label_display_per_page: "Per pàgina: {{value}}'"
508 label_user_mail_no_self_notified: "No vull ser notificat pels canvis que faig jo mateix"
581 label_age: Edat
509 label_registration_activation_by_email: activació del compte per correu electrònic
582 label_change_properties: Canvia les propietats
510 label_registration_manual_activation: activació del compte manual
583 label_general: General
511 label_registration_automatic_activation: activació del compte automàtica
584 label_more: Més
512 label_display_per_page: 'Per pàgina: %s'
585 label_scm: SCM
513 label_age: Edat
586 label_plugins: Connectors
514 label_change_properties: Canvia les propietats
587 label_ldap_authentication: Autenticació LDAP
515 label_general: General
588 label_downloads_abbr: Baixades
516 label_more: Més
589 label_optional_description: Descripció opcional
517 label_scm: SCM
590 label_add_another_file: Afegeix un altre fitxer
518 label_plugins: Connectors
591 label_preferences: Preferències
519 label_ldap_authentication: Autenticació LDAP
592 label_chronological_order: En ordre cronològic
520 label_downloads_abbr: Baixades
593 label_reverse_chronological_order: En ordre cronològic invers
521 label_optional_description: Descripció opcional
594 label_planning: Planificació
522 label_add_another_file: Afegeix un altre fitxer
595 label_incoming_emails: "Correu electrònics d'entrada"
523 label_preferences: Preferències
596 label_generate_key: Genera una clau
524 label_chronological_order: En ordre cronològic
597 label_issue_watchers: Vigilants
525 label_reverse_chronological_order: En ordre cronològic invers
598
526 label_planning: Planificació
599 button_login: Entra
527 label_incoming_emails: "Correu electrònics d'entrada"
600 button_submit: Tramet
528 label_generate_key: Genera una clau
601 button_save: Desa
529 label_issue_watchers: Vigilants
602 button_check_all: Activa-ho tot
530
603 button_uncheck_all: Desactiva-ho tot
531 button_login: Entra
604 button_delete: Suprimeix
532 button_submit: Tramet
605 button_create: Crea
533 button_save: Desa
606 button_test: Test
534 button_check_all: Activa-ho tot
607 button_edit: Edit
535 button_uncheck_all: Desactiva-ho tot
608 button_add: Afegeix
536 button_delete: Suprimeix
609 button_change: Canvia
537 button_create: Crea
610 button_apply: Aplica
538 button_test: Test
611 button_clear: Neteja
539 button_edit: Edit
612 button_lock: Bloca
540 button_add: Afegeix
613 button_unlock: Desbloca
541 button_change: Canvia
614 button_download: Baixa
542 button_apply: Aplica
615 button_list: Llista
543 button_clear: Neteja
616 button_view: Visualitza
544 button_lock: Bloca
617 button_move: Mou
545 button_unlock: Desbloca
618 button_back: Enrere
546 button_download: Baixa
619 button_cancel: Cancel·la
547 button_list: Llista
620 button_activate: Activa
548 button_view: Visualitza
621 button_sort: Ordena
549 button_move: Mou
622 button_log_time: "Hora d'entrada"
550 button_back: Enrere
623 button_rollback: Torna a aquesta versió
551 button_cancel: Cancel·la
624 button_watch: Vigila
552 button_activate: Activa
625 button_unwatch: No vigilis
553 button_sort: Ordena
626 button_reply: Resposta
554 button_log_time: "Hora d'entrada"
627 button_archive: Arxiva
555 button_rollback: Torna a aquesta versió
628 button_unarchive: Desarxiva
556 button_watch: Vigila
629 button_reset: Reinicia
557 button_unwatch: No vigilis
630 button_rename: Reanomena
558 button_reply: Resposta
631 button_change_password: Canvia la contrasenya
559 button_archive: Arxiva
632 button_copy: Copia
560 button_unarchive: Desarxiva
633 button_annotate: Anota
561 button_reset: Reinicia
634 button_update: Actualitza
562 button_rename: Reanomena
635 button_configure: Configura
563 button_change_password: Canvia la contrasenya
636 button_quote: Cita
564 button_copy: Copia
637
565 button_annotate: Anota
638 status_active: actiu
566 button_update: Actualitza
639 status_registered: informat
567 button_configure: Configura
640 status_locked: bloquejat
568 button_quote: Cita
641
569
642 text_select_mail_notifications: "Seleccioneu les accions per les quals s'hauria d'enviar una notificació per correu electrònic."
570 status_active: actiu
643 text_regexp_info: ex. ^[A-Z0-9]+$
571 status_registered: informat
644 text_min_max_length_info: 0 significa sense restricció
572 status_locked: bloquejat
645 text_project_destroy_confirmation: Segur que voleu suprimir aquest projecte i les dades relacionades?
573
646 text_subprojects_destroy_warning: "També seran suprimits els seus subprojectes: {{value}}."
574 text_select_mail_notifications: "Seleccioneu les accions per les quals s'hauria d'enviar una notificació per correu electrònic."
647 text_workflow_edit: Seleccioneu un rol i un seguidor per a editar el flux de treball
575 text_regexp_info: ex. ^[A-Z0-9]+$
648 text_are_you_sure: Segur?
576 text_min_max_length_info: 0 significa sense restricció
649 text_journal_changed: "canviat des de {{old}} a {{new}}"
577 text_project_destroy_confirmation: Segur que voleu suprimir aquest projecte i les dades relacionades?
650 text_journal_set_to: "establert a {{value}}"
578 text_subprojects_destroy_warning: "També seran suprimits els seus subprojectes: %s."
651 text_journal_deleted: suprimit
579 text_workflow_edit: Seleccioneu un rol i un seguidor per a editar el flux de treball
652 text_tip_task_begin_day: "tasca que s'inicia aquest dia"
580 text_are_you_sure: Segur?
653 text_tip_task_end_day: tasca que finalitza aquest dia
581 text_journal_changed: canviat des de %s a %s
654 text_tip_task_begin_end_day: "tasca que s'inicia i finalitza aquest dia"
582 text_journal_set_to: establert a %s
655 text_project_identifier_info: "Es permeten lletres en minúscules (a-z), números i guions.<br />Un cop desat, l'identificador no es pot modificar."
583 text_journal_deleted: suprimit
656 text_caracters_maximum: "{{count}} caràcters com a màxim."
584 text_tip_task_begin_day: "tasca que s'inicia aquest dia"
657 text_caracters_minimum: "Com a mínim ha de tenir {{count}} caràcters."
585 text_tip_task_end_day: tasca que finalitza aquest dia
658 text_length_between: "Longitud entre {{min}} i {{max}} caràcters."
586 text_tip_task_begin_end_day: "tasca que s'inicia i finalitza aquest dia"
659 text_tracker_no_workflow: "No s'ha definit cap flux de treball per a aquest seguidor"
587 text_project_identifier_info: "Es permeten lletres en minúscules (a-z), números i guions.<br />Un cop desat, l'identificador no es pot modificar."
660 text_unallowed_characters: Caràcters no permesos
588 text_caracters_maximum: %d caràcters com a màxim.
661 text_comma_separated: Es permeten valors múltiples (separats per una coma).
589 text_caracters_minimum: Com a mínim ha de tenir %d caràcters.
662 text_issues_ref_in_commit_messages: Referència i soluciona els assumptes en els missatges publicats
590 text_length_between: Longitud entre %d i %d caràcters.
663 text_issue_added: "L'assumpte {{id}} ha sigut informat per {{author}}."
591 text_tracker_no_workflow: "No s'ha definit cap flux de treball per a aquest seguidor"
664 text_issue_updated: "L'assumpte {{id}} ha sigut actualitzat per {{author}}."
592 text_unallowed_characters: Caràcters no permesos
665 text_wiki_destroy_confirmation: Segur que voleu suprimir aquest wiki i tots els seus continguts?
593 text_comma_separated: Es permeten valors múltiples (separats per una coma).
666 text_issue_category_destroy_question: "Alguns assumptes ({{count}}) estan assignats a aquesta categoria. Què voleu fer?"
594 text_issues_ref_in_commit_messages: Referència i soluciona els assumptes en els missatges publicats
667 text_issue_category_destroy_assignments: Suprimeix les assignacions de la categoria
595 text_issue_added: "L'assumpte %s ha sigut informat per %s."
668 text_issue_category_reassign_to: Torna a assignar els assumptes a aquesta categoria
596 text_issue_updated: "L'assumpte %s ha sigut actualitzat per %s."
669 text_user_mail_option: "Per als projectes no seleccionats, només rebreu notificacions sobre les coses que vigileu o que hi esteu implicat (ex. assumptes que en sou l'autor o hi esteu assignat)."
597 text_wiki_destroy_confirmation: Segur que voleu suprimir aquest wiki i tots els seus continguts?
670 text_no_configuration_data: "Encara no s'han configurat els rols, seguidors, estats de l'assumpte i flux de treball.\nÉs altament recomanable que carregueu la configuració predeterminada. Podreu modificar-la un cop carregada."
598 text_issue_category_destroy_question: Alguns assumptes (%d) estan assignats a aquesta categoria. Què voleu fer?
671 text_load_default_configuration: Carrega la configuració predeterminada
599 text_issue_category_destroy_assignments: Suprimeix les assignacions de la categoria
672 text_status_changed_by_changeset: "Aplicat en el conjunt de canvis {{value}}."
600 text_issue_category_reassign_to: Torna a assignar els assumptes a aquesta categoria
673 text_issues_destroy_confirmation: "Segur que voleu suprimir els assumptes seleccionats?"
601 text_user_mail_option: "Per als projectes no seleccionats, només rebreu notificacions sobre les coses que vigileu o que hi esteu implicat (ex. assumptes que en sou l'autor o hi esteu assignat)."
674 text_select_project_modules: "Seleccioneu els mòduls a habilitar per a aquest projecte:"
602 text_no_configuration_data: "Encara no s'han configurat els rols, seguidors, estats de l'assumpte i flux de treball.\nÉs altament recomanable que carregueu la configuració predeterminada. Podreu modificar-la un cop carregada."
675 text_default_administrator_account_changed: "S'ha canviat el compte d'administrador predeterminat"
603 text_load_default_configuration: Carrega la configuració predeterminada
676 text_file_repository_writable: Es pot escriure en el dipòsit de fitxers
604 text_status_changed_by_changeset: Aplicat en el conjunt de canvis %s.
677 text_rmagick_available: RMagick disponible (opcional)
605 text_issues_destroy_confirmation: "Segur que voleu suprimir els assumptes seleccionats?"
678 text_destroy_time_entries_question: "S'han informat %.02f hores en els assumptes que aneu a suprimir. Què voleu fer?"
606 text_select_project_modules: "Seleccioneu els mòduls a habilitar per a aquest projecte:"
679 text_destroy_time_entries: Suprimeix les hores informades
607 text_default_administrator_account_changed: "S'ha canviat el compte d'administrador predeterminat"
680 text_assign_time_entries_to_project: Assigna les hores informades al projecte
608 text_file_repository_writable: Es pot escriure en el dipòsit de fitxers
681 text_reassign_time_entries: 'Torna a assignar les hores informades a aquest assumpte:'
609 text_rmagick_available: RMagick disponible (opcional)
682 text_user_wrote: "{{value}} va escriure:'"
610 text_destroy_time_entries_question: "S'han informat %.02f hores en els assumptes que aneu a suprimir. Què voleu fer?"
683 text_enumeration_destroy_question: "{{count}} objectes estan assignats a aquest valor.'"
611 text_destroy_time_entries: Suprimeix les hores informades
684 text_enumeration_category_reassign_to: 'Torna a assignar-los a aquest valor:'
612 text_assign_time_entries_to_project: Assigna les hores informades al projecte
685 text_email_delivery_not_configured: "El lliurament per correu electrònic no està configurat i les notificacions estan inhabilitades.\nConfigureu el servidor SMTP a config/email.yml i reinicieu l'aplicació per habilitar-lo."
613 text_reassign_time_entries: 'Torna a assignar les hores informades a aquest assumpte:'
686
614 text_user_wrote: '%s va escriure:'
687 default_role_manager: Gestor
615 text_enumeration_destroy_question: '%d objectes estan assignats a aquest valor.'
688 default_role_developper: Desenvolupador
616 text_enumeration_category_reassign_to: 'Torna a assignar-los a aquest valor:'
689 default_role_reporter: Informador
617 text_email_delivery_not_configured: "El lliurament per correu electrònic no està configurat i les notificacions estan inhabilitades.\nConfigureu el servidor SMTP a config/email.yml i reinicieu l'aplicació per habilitar-lo."
690 default_tracker_bug: Error
618
691 default_tracker_feature: Característica
619 default_role_manager: Gestor
692 default_tracker_support: Suport
620 default_role_developper: Desenvolupador
693 default_issue_status_new: Nou
621 default_role_reporter: Informador
694 default_issue_status_assigned: Assignat
622 default_tracker_bug: Error
695 default_issue_status_resolved: Resolt
623 default_tracker_feature: Característica
696 default_issue_status_feedback: Comentaris
624 default_tracker_support: Suport
697 default_issue_status_closed: Tancat
625 default_issue_status_new: Nou
698 default_issue_status_rejected: Rebutjat
626 default_issue_status_assigned: Assignat
699 default_doc_category_user: "Documentació d'usuari"
627 default_issue_status_resolved: Resolt
700 default_doc_category_tech: Documentació tècnica
628 default_issue_status_feedback: Comentaris
701 default_priority_low: Baixa
629 default_issue_status_closed: Tancat
702 default_priority_normal: Normal
630 default_issue_status_rejected: Rebutjat
703 default_priority_high: Alta
631 default_doc_category_user: "Documentació d'usuari"
704 default_priority_urgent: Urgent
632 default_doc_category_tech: Documentació tècnica
705 default_priority_immediate: Immediata
633 default_priority_low: Baixa
706 default_activity_design: Disseny
634 default_priority_normal: Normal
707 default_activity_development: Desenvolupament
635 default_priority_high: Alta
708
636 default_priority_urgent: Urgent
709 enumeration_issue_priorities: Prioritat dels assumptes
637 default_priority_immediate: Immediata
710 enumeration_doc_categories: Categories del document
638 default_activity_design: Disseny
711 enumeration_activities: Activitats (seguidor de temps)
639 default_activity_development: Desenvolupament
712 setting_plain_text_mail: plain text only (no HTML)
640
713 permission_view_files: View files
641 enumeration_issue_priorities: Prioritat dels assumptes
714 permission_edit_issues: Edit issues
642 enumeration_doc_categories: Categories del document
715 permission_edit_own_time_entries: Edit own time logs
643 enumeration_activities: Activitats (seguidor de temps)
716 permission_manage_public_queries: Manage public queries
644 setting_plain_text_mail: plain text only (no HTML)
717 permission_add_issues: Add issues
645 permission_view_files: View files
718 permission_log_time: Log spent time
646 permission_edit_issues: Edit issues
719 permission_view_changesets: View changesets
647 permission_edit_own_time_entries: Edit own time logs
720 permission_view_time_entries: View spent time
648 permission_manage_public_queries: Manage public queries
721 permission_manage_versions: Manage versions
649 permission_add_issues: Add issues
722 permission_manage_wiki: Manage wiki
650 permission_log_time: Log spent time
723 permission_manage_categories: Manage issue categories
651 permission_view_changesets: View changesets
724 permission_protect_wiki_pages: Protect wiki pages
652 permission_view_time_entries: View spent time
725 permission_comment_news: Comment news
653 permission_manage_versions: Manage versions
726 permission_delete_messages: Delete messages
654 permission_manage_wiki: Manage wiki
727 permission_select_project_modules: Select project modules
655 permission_manage_categories: Manage issue categories
728 permission_manage_documents: Manage documents
656 permission_protect_wiki_pages: Protect wiki pages
729 permission_edit_wiki_pages: Edit wiki pages
657 permission_comment_news: Comment news
730 permission_add_issue_watchers: Add watchers
658 permission_delete_messages: Delete messages
731 permission_view_gantt: View gantt chart
659 permission_select_project_modules: Select project modules
732 permission_move_issues: Move issues
660 permission_manage_documents: Manage documents
733 permission_manage_issue_relations: Manage issue relations
661 permission_edit_wiki_pages: Edit wiki pages
734 permission_delete_wiki_pages: Delete wiki pages
662 permission_add_issue_watchers: Add watchers
735 permission_manage_boards: Manage boards
663 permission_view_gantt: View gantt chart
736 permission_delete_wiki_pages_attachments: Delete attachments
664 permission_move_issues: Move issues
737 permission_view_wiki_edits: View wiki history
665 permission_manage_issue_relations: Manage issue relations
738 permission_add_messages: Post messages
666 permission_delete_wiki_pages: Delete wiki pages
739 permission_view_messages: View messages
667 permission_manage_boards: Manage boards
740 permission_manage_files: Manage files
668 permission_delete_wiki_pages_attachments: Delete attachments
741 permission_edit_issue_notes: Edit notes
669 permission_view_wiki_edits: View wiki history
742 permission_manage_news: Manage news
670 permission_add_messages: Post messages
743 permission_view_calendar: View calendrier
671 permission_view_messages: View messages
744 permission_manage_members: Manage members
672 permission_manage_files: Manage files
745 permission_edit_messages: Edit messages
673 permission_edit_issue_notes: Edit notes
746 permission_delete_issues: Delete issues
674 permission_manage_news: Manage news
747 permission_view_issue_watchers: View watchers list
675 permission_view_calendar: View calendrier
748 permission_manage_repository: Manage repository
676 permission_manage_members: Manage members
749 permission_commit_access: Commit access
677 permission_edit_messages: Edit messages
750 permission_browse_repository: Browse repository
678 permission_delete_issues: Delete issues
751 permission_view_documents: View documents
679 permission_view_issue_watchers: View watchers list
752 permission_edit_project: Edit project
680 permission_manage_repository: Manage repository
753 permission_add_issue_notes: Add notes
681 permission_commit_access: Commit access
754 permission_save_queries: Save queries
682 permission_browse_repository: Browse repository
755 permission_view_wiki_pages: View wiki
683 permission_view_documents: View documents
756 permission_rename_wiki_pages: Rename wiki pages
684 permission_edit_project: Edit project
757 permission_edit_time_entries: Edit time logs
685 permission_add_issue_notes: Add notes
758 permission_edit_own_issue_notes: Edit own notes
686 permission_save_queries: Save queries
759 setting_gravatar_enabled: Use Gravatar user icons
687 permission_view_wiki_pages: View wiki
760 label_example: Example
688 permission_rename_wiki_pages: Rename wiki pages
761 text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
689 permission_edit_time_entries: Edit time logs
762 permission_edit_own_messages: Edit own messages
690 permission_edit_own_issue_notes: Edit own notes
763 permission_delete_own_messages: Delete own messages
691 setting_gravatar_enabled: Use Gravatar user icons
764 label_user_activity: "{{value}}'s activity"
692 label_example: Example
765 label_updated_time_by: "Updated by {{author}} {{age}} ago"
693 text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
766 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
694 permission_edit_own_messages: Edit own messages
767 setting_diff_max_lines_displayed: Max number of diff lines displayed
695 permission_delete_own_messages: Delete own messages
768 text_plugin_assets_writable: Plugin assets directory writable
696 label_user_activity: "%s's activity"
769 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
697 label_updated_time_by: Updated by %s %s ago
770 button_create_and_continue: Create and continue
698 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
771 text_custom_field_possible_values_info: 'One line for each value'
699 setting_diff_max_lines_displayed: Max number of diff lines displayed
772 label_display: Display
700 text_plugin_assets_writable: Plugin assets directory writable
773 field_editable: Editable
701 warning_attachments_not_saved: "%d file(s) could not be saved."
774 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
702 button_create_and_continue: Create and continue
703 text_custom_field_possible_values_info: 'One line for each value'
704 label_display: Display
705 field_editable: Editable
706 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
707 field_identity_url: OpenID URL
708 setting_openid: Allow OpenID login and registration
709 label_login_with_open_id_option: or login with OpenID
710 field_watcher: Watcher
This diff has been collapsed as it changes many lines, (1488 lines changed) Show them Hide them
@@ -1,714 +1,778
1 # CZ translation by Maxim Krušina | Massimo Filippi, s.r.o. | maxim@mxm.cz
1 cs:
2 # Based on original CZ translation by Jan Kadleček
2 date:
3 formats:
4 # Use the strftime parameters for formats.
5 # When no format has been given, it uses default.
6 # You can provide other formats here if you like!
7 default: "%Y-%m-%d"
8 short: "%b %d"
9 long: "%B %d, %Y"
10
11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
13
14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
17 # Used in date_select and datime_select.
18 order: [ :year, :month, :day ]
3
19
4 _gloc_rule_default: '|n| n==1 ? "" : "_plural" '
20 time:
21 formats:
22 default: "%a, %d %b %Y %H:%M:%S %z"
23 short: "%d %b %H:%M"
24 long: "%B %d, %Y %H:%M"
25 am: "am"
26 pm: "pm"
27
28 datetime:
29 distance_in_words:
30 half_a_minute: "half a minute"
31 less_than_x_seconds:
32 one: "less than 1 second"
33 other: "less than {{count}} seconds"
34 x_seconds:
35 one: "1 second"
36 other: "{{count}} seconds"
37 less_than_x_minutes:
38 one: "less than a minute"
39 other: "less than {{count}} minutes"
40 x_minutes:
41 one: "1 minute"
42 other: "{{count}} minutes"
43 about_x_hours:
44 one: "about 1 hour"
45 other: "about {{count}} hours"
46 x_days:
47 one: "1 day"
48 other: "{{count}} days"
49 about_x_months:
50 one: "about 1 month"
51 other: "about {{count}} months"
52 x_months:
53 one: "1 month"
54 other: "{{count}} months"
55 about_x_years:
56 one: "about 1 year"
57 other: "about {{count}} years"
58 over_x_years:
59 one: "over 1 year"
60 other: "over {{count}} years"
61
62 # Used in array.to_sentence.
63 support:
64 array:
65 sentence_connector: "and"
66 skip_last_comma: false
67
68 activerecord:
69 errors:
70 messages:
71 inclusion: "není zahrnuto v seznamu"
72 exclusion: "je rezervováno"
73 invalid: "je neplatné"
74 confirmation: "se neshoduje s potvrzením"
75 accepted: "musí být akceptováno"
76 empty: "nemůže být prázdný"
77 blank: "nemůže být prázdný"
78 too_long: "je příliš dlouhý"
79 too_short: "je příliš krátký"
80 wrong_length: "má chybnou délku"
81 taken: "je již použito"
82 not_a_number: "není číslo"
83 not_a_date: "není platné datum"
84 greater_than: "must be greater than {{count}}"
85 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
86 equal_to: "must be equal to {{count}}"
87 less_than: "must be less than {{count}}"
88 less_than_or_equal_to: "must be less than or equal to {{count}}"
89 odd: "must be odd"
90 even: "must be even"
91 greater_than_start_date: "musí být větší než počáteční datum"
92 not_same_project: "nepatří stejnému projektu"
93 circular_dependency: "Tento vztah by vytvořil cyklickou závislost"
5
94
6 actionview_datehelper_select_day_prefix:
95 # CZ translation by Maxim Krušina | Massimo Filippi, s.r.o. | maxim@mxm.cz
7 actionview_datehelper_select_month_names: Leden,Únor,Březen,Duben,Květen,Červen,Červenec,Srpen,Září,Říjen,Listopad,Prosinec
96 # Based on original CZ translation by Jan Kadleček
8 actionview_datehelper_select_month_names_abbr: Led,Úno,Bře,Dub,Kvě,Čer,Čvc,Srp,Zář,Říj,Lis,Pro
97
9 actionview_datehelper_select_month_prefix:
98 actionview_instancetag_blank_option: Prosím vyberte
10 actionview_datehelper_select_year_prefix:
99
11 actionview_datehelper_time_in_words_day: 1 den
100 general_text_No: 'Ne'
12 actionview_datehelper_time_in_words_day_plural: %d dny
101 general_text_Yes: 'Ano'
13 actionview_datehelper_time_in_words_hour_about: asi hodinou
102 general_text_no: 'ne'
14 actionview_datehelper_time_in_words_hour_about_plural: asi %d hodinami
103 general_text_yes: 'ano'
15 actionview_datehelper_time_in_words_hour_about_single: asi hodinou
104 general_lang_name: 'Čeština'
16 actionview_datehelper_time_in_words_minute: 1 minutou
105 general_csv_separator: ','
17 actionview_datehelper_time_in_words_minute_half: půl minutou
106 general_csv_decimal_separator: '.'
18 actionview_datehelper_time_in_words_minute_less_than: méně než minutou
107 general_csv_encoding: UTF-8
19 actionview_datehelper_time_in_words_minute_plural: %d minutami
108 general_pdf_encoding: UTF-8
20 actionview_datehelper_time_in_words_minute_single: 1 minutou
109 general_first_day_of_week: '1'
21 actionview_datehelper_time_in_words_second_less_than: méně než sekundou
110
22 actionview_datehelper_time_in_words_second_less_than_plural: méně než %d sekundami
111 notice_account_updated: Účet byl úspěšně změněn.
23 actionview_instancetag_blank_option: Prosím vyberte
112 notice_account_invalid_creditentials: Chybné jméno nebo heslo
24
113 notice_account_password_updated: Heslo bylo úspěšně změněno.
25 activerecord_error_inclusion: není zahrnuto v seznamu
114 notice_account_wrong_password: Chybné heslo
26 activerecord_error_exclusion: je rezervováno
115 notice_account_register_done: Účet byl úspěšně vytvořen. Pro aktivaci účtu klikněte na odkaz v emailu, který vám byl zaslán.
27 activerecord_error_invalid: je neplatné
116 notice_account_unknown_email: Neznámý uživatel.
28 activerecord_error_confirmation: se neshoduje s potvrzením
117 notice_can_t_change_password: Tento účet používá externí autentifikaci. Zde heslo změnit nemůžete.
29 activerecord_error_accepted: musí být akceptováno
118 notice_account_lost_email_sent: Byl vám zaslán email s intrukcemi jak si nastavíte nové heslo.
30 activerecord_error_empty: nemůže být prázdný
119 notice_account_activated: Váš účet byl aktivován. Nyní se můžete přihlásit.
31 activerecord_error_blank: nemůže být prázdný
120 notice_successful_create: Úspěšně vytvořeno.
32 activerecord_error_too_long: je příliš dlouhý
121 notice_successful_update: Úspěšně aktualizováno.
33 activerecord_error_too_short: je příliš krátký
122 notice_successful_delete: Úspěšně odstraněno.
34 activerecord_error_wrong_length: má chybnou délku
123 notice_successful_connection: Úspěšné připojení.
35 activerecord_error_taken: je již použito
124 notice_file_not_found: Stránka na kterou se snažíte zobrazit neexistuje nebo byla smazána.
36 activerecord_error_not_a_number: není číslo
125 notice_locking_conflict: Údaje byly změněny jiným uživatelem.
37 activerecord_error_not_a_date: není platné datum
126 notice_scm_error: Entry and/or revision doesn't exist in the repository.
38 activerecord_error_greater_than_start_date: musí být větší než počáteční datum
127 notice_not_authorized: Nemáte dostatečná práva pro zobrazení této stránky.
39 activerecord_error_not_same_project: nepatří stejnému projektu
128 notice_email_sent: "Na adresu {{value}} byl odeslán email"
40 activerecord_error_circular_dependency: Tento vztah by vytvořil cyklickou závislost
129 notice_email_error: "Při odesílání emailu nastala chyba ({{value}})"
41
130 notice_feeds_access_key_reseted: Váš klíč pro přístup k RSS byl resetován.
42 general_fmt_age: %d rok
131 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
43 general_fmt_age_plural: %d roků
132 notice_no_issue_selected: "Nebyl zvolen žádný úkol. Prosím, zvolte úkoly, které chcete editovat"
44 general_fmt_date: %%m/%%d/%%Y
133 notice_account_pending: "Váš účet byl vytvořen, nyní čeká na schválení administrátorem."
45 general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p
134 notice_default_data_loaded: Výchozí konfigurace úspěšně nahrána.
46 general_fmt_datetime_short: %%b %%d, %%I:%%M %%p
135
47 general_fmt_time: %%I:%%M %%p
136 error_can_t_load_default_data: "Výchozí konfigurace nebyla nahrána: {{value}}"
48 general_text_No: 'Ne'
137 error_scm_not_found: "Položka a/nebo revize neexistují v repository."
49 general_text_Yes: 'Ano'
138 error_scm_command_failed: "Při pokusu o přístup k repository došlo k chybě: {{value}}"
50 general_text_no: 'ne'
139 error_issue_not_found_in_project: 'Úkol nebyl nalezen nebo nepatří k tomuto projektu'
51 general_text_yes: 'ano'
140
52 general_lang_name: 'Čeština'
141 mail_subject_lost_password: "Vaše heslo ({{value}})"
53 general_csv_separator: ','
142 mail_body_lost_password: 'Pro změnu vašeho hesla klikněte na následující odkaz:'
54 general_csv_decimal_separator: '.'
143 mail_subject_register: "Aktivace účtu ({{value}})"
55 general_csv_encoding: UTF-8
144 mail_body_register: 'Pro aktivaci vašeho účtu klikněte na následující odkaz:'
56 general_pdf_encoding: UTF-8
145 mail_body_account_information_external: "Pomocí vašeho účtu {{value}} se můžete přihlásit."
57 general_day_names: Pondělí,Úterý,Středa,Čtvrtek,Pátek,Sobota,Neděle
146 mail_body_account_information: Informace o vašem účtu
58 general_first_day_of_week: '1'
147 mail_subject_account_activation_request: "Aktivace {{value}} účtu"
59
148 mail_body_account_activation_request: "Byl zaregistrován nový uživatel {{value}}. Aktivace jeho účtu závisí na vašem potvrzení."
60 notice_account_updated: Účet byl úspěšně změněn.
149
61 notice_account_invalid_creditentials: Chybné jméno nebo heslo
150 gui_validation_error: 1 chyba
62 notice_account_password_updated: Heslo bylo úspěšně změněno.
151 gui_validation_error_plural: "{{count}} chyb(y)"
63 notice_account_wrong_password: Chybné heslo
152
64 notice_account_register_done: Účet byl úspěšně vytvořen. Pro aktivaci účtu klikněte na odkaz v emailu, který vám byl zaslán.
153 field_name: Název
65 notice_account_unknown_email: Neznámý uživatel.
154 field_description: Popis
66 notice_can_t_change_password: Tento účet používá externí autentifikaci. Zde heslo změnit nemůžete.
155 field_summary: Přehled
67 notice_account_lost_email_sent: Byl vám zaslán email s intrukcemi jak si nastavíte nové heslo.
156 field_is_required: Povinné pole
68 notice_account_activated: Váš účet byl aktivován. Nyní se můžete přihlásit.
157 field_firstname: Jméno
69 notice_successful_create: Úspěšně vytvořeno.
158 field_lastname: Příjmení
70 notice_successful_update: Úspěšně aktualizováno.
159 field_mail: Email
71 notice_successful_delete: Úspěšně odstraněno.
160 field_filename: Soubor
72 notice_successful_connection: Úspěšné připojení.
161 field_filesize: Velikost
73 notice_file_not_found: Stránka na kterou se snažíte zobrazit neexistuje nebo byla smazána.
162 field_downloads: Staženo
74 notice_locking_conflict: Údaje byly změněny jiným uživatelem.
163 field_author: Autor
75 notice_scm_error: Entry and/or revision doesn't exist in the repository.
164 field_created_on: Vytvořeno
76 notice_not_authorized: Nemáte dostatečná práva pro zobrazení této stránky.
165 field_updated_on: Aktualizováno
77 notice_email_sent: Na adresu %s byl odeslán email
166 field_field_format: Formát
78 notice_email_error: Při odesílání emailu nastala chyba (%s)
167 field_is_for_all: Pro všechny projekty
79 notice_feeds_access_key_reseted: Váš klíč pro přístup k RSS byl resetován.
168 field_possible_values: Možné hodnoty
80 notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
169 field_regexp: Regulární výraz
81 notice_no_issue_selected: "Nebyl zvolen žádný úkol. Prosím, zvolte úkoly, které chcete editovat"
170 field_min_length: Minimální délka
82 notice_account_pending: "Váš účet byl vytvořen, nyní čeká na schválení administrátorem."
171 field_max_length: Maximální délka
83 notice_default_data_loaded: Výchozí konfigurace úspěšně nahrána.
172 field_value: Hodnota
84
173 field_category: Kategorie
85 error_can_t_load_default_data: "Výchozí konfigurace nebyla nahrána: %s"
174 field_title: Název
86 error_scm_not_found: "Položka a/nebo revize neexistují v repository."
175 field_project: Projekt
87 error_scm_command_failed: "Při pokusu o přístup k repository došlo k chybě: %s"
176 field_issue: Úkol
88 error_issue_not_found_in_project: 'Úkol nebyl nalezen nebo nepatří k tomuto projektu'
177 field_status: Stav
89
178 field_notes: Poznámka
90 mail_subject_lost_password: Vaše heslo (%s)
179 field_is_closed: Úkol uzavřen
91 mail_body_lost_password: 'Pro změnu vašeho hesla klikněte na následující odkaz:'
180 field_is_default: Výchozí stav
92 mail_subject_register: Aktivace účtu (%s)
181 field_tracker: Fronta
93 mail_body_register: 'Pro aktivaci vašeho účtu klikněte na následující odkaz:'
182 field_subject: Předmět
94 mail_body_account_information_external: Pomocí vašeho účtu "%s" se můžete přihlásit.
183 field_due_date: Uzavřít do
95 mail_body_account_information: Informace o vašem účtu
184 field_assigned_to: Přiřazeno
96 mail_subject_account_activation_request: Aktivace %s účtu
185 field_priority: Priorita
97 mail_body_account_activation_request: Byl zaregistrován nový uživatel "%s". Aktivace jeho účtu závisí na vašem potvrzení.
186 field_fixed_version: Přiřazeno k verzi
98
187 field_user: Uživatel
99 gui_validation_error: 1 chyba
188 field_role: Role
100 gui_validation_error_plural: %d chyb(y)
189 field_homepage: Homepage
101
190 field_is_public: Veřejný
102 field_name: Název
191 field_parent: Nadřazený projekt
103 field_description: Popis
192 field_is_in_chlog: Úkoly zobrazené v změnovém logu
104 field_summary: Přehled
193 field_is_in_roadmap: Úkoly zobrazené v plánu
105 field_is_required: Povinné pole
194 field_login: Přihlášení
106 field_firstname: Jméno
195 field_mail_notification: Emailová oznámení
107 field_lastname: Příjmení
196 field_admin: Administrátor
108 field_mail: Email
197 field_last_login_on: Poslední přihlášení
109 field_filename: Soubor
198 field_language: Jazyk
110 field_filesize: Velikost
199 field_effective_date: Datum
111 field_downloads: Staženo
200 field_password: Heslo
112 field_author: Autor
201 field_new_password: Nové heslo
113 field_created_on: Vytvořeno
202 field_password_confirmation: Potvrzení
114 field_updated_on: Aktualizováno
203 field_version: Verze
115 field_field_format: Formát
204 field_type: Typ
116 field_is_for_all: Pro všechny projekty
205 field_host: Host
117 field_possible_values: Možné hodnoty
206 field_port: Port
118 field_regexp: Regulární výraz
207 field_account: Účet
119 field_min_length: Minimální délka
208 field_base_dn: Base DN
120 field_max_length: Maximální délka
209 field_attr_login: Přihlášení (atribut)
121 field_value: Hodnota
210 field_attr_firstname: Jméno (atribut)
122 field_category: Kategorie
211 field_attr_lastname: Příjemní (atribut)
123 field_title: Název
212 field_attr_mail: Email (atribut)
124 field_project: Projekt
213 field_onthefly: Automatické vytváření uživatelů
125 field_issue: Úkol
214 field_start_date: Začátek
126 field_status: Stav
215 field_done_ratio: %% Hotovo
127 field_notes: Poznámka
216 field_auth_source: Autentifikační mód
128 field_is_closed: Úkol uzavřen
217 field_hide_mail: Nezobrazovat můj email
129 field_is_default: Výchozí stav
218 field_comments: Komentář
130 field_tracker: Fronta
219 field_url: URL
131 field_subject: Předmět
220 field_start_page: Výchozí stránka
132 field_due_date: Uzavřít do
221 field_subproject: Podprojekt
133 field_assigned_to: Přiřazeno
222 field_hours: Hodiny
134 field_priority: Priorita
223 field_activity: Aktivita
135 field_fixed_version: Přiřazeno k verzi
224 field_spent_on: Datum
136 field_user: Uživatel
225 field_identifier: Identifikátor
137 field_role: Role
226 field_is_filter: Použít jako filtr
138 field_homepage: Homepage
227 field_issue_to_id: Související úkol
139 field_is_public: Veřejný
228 field_delay: Zpoždění
140 field_parent: Nadřazený projekt
229 field_assignable: Úkoly mohou být přiřazeny této roli
141 field_is_in_chlog: Úkoly zobrazené v změnovém logu
230 field_redirect_existing_links: Přesměrovat stvávající odkazy
142 field_is_in_roadmap: Úkoly zobrazené v plánu
231 field_estimated_hours: Odhadovaná doba
143 field_login: Přihlášení
232 field_column_names: Sloupce
144 field_mail_notification: Emailová oznámení
233 field_time_zone: Časové pásmo
145 field_admin: Administrátor
234 field_searchable: Umožnit vyhledávání
146 field_last_login_on: Poslední přihlášení
235 field_default_value: Výchozí hodnota
147 field_language: Jazyk
236 field_comments_sorting: Zobrazit komentáře
148 field_effective_date: Datum
237
149 field_password: Heslo
238 setting_app_title: Název aplikace
150 field_new_password: Nové heslo
239 setting_app_subtitle: Podtitulek aplikace
151 field_password_confirmation: Potvrzení
240 setting_welcome_text: Uvítací text
152 field_version: Verze
241 setting_default_language: Výchozí jazyk
153 field_type: Typ
242 setting_login_required: Auten. vyžadována
154 field_host: Host
243 setting_self_registration: Povolena automatická registrace
155 field_port: Port
244 setting_attachment_max_size: Maximální velikost přílohy
156 field_account: Účet
245 setting_issues_export_limit: Limit pro export úkolů
157 field_base_dn: Base DN
246 setting_mail_from: Odesílat emaily z adresy
158 field_attr_login: Přihlášení (atribut)
247 setting_bcc_recipients: Příjemci skryté kopie (bcc)
159 field_attr_firstname: Jméno (atribut)
248 setting_host_name: Host name
160 field_attr_lastname: Příjemní (atribut)
249 setting_text_formatting: Formátování textu
161 field_attr_mail: Email (atribut)
250 setting_wiki_compression: Komperese historie Wiki
162 field_onthefly: Automatické vytváření uživatelů
251 setting_feeds_limit: Feed content limit
163 field_start_date: Začátek
252 setting_default_projects_public: Nové projekty nastavovat jako veřejné
164 field_done_ratio: %% Hotovo
253 setting_autofetch_changesets: Autofetch commits
165 field_auth_source: Autentifikační mód
254 setting_sys_api_enabled: Povolit WS pro správu repozitory
166 field_hide_mail: Nezobrazovat můj email
255 setting_commit_ref_keywords: Klíčová slova pro odkazy
167 field_comments: Komentář
256 setting_commit_fix_keywords: Klíčová slova pro uzavření
168 field_url: URL
257 setting_autologin: Automatické přihlašování
169 field_start_page: Výchozí stránka
258 setting_date_format: Formát data
170 field_subproject: Podprojekt
259 setting_time_format: Formát času
171 field_hours: Hodiny
260 setting_cross_project_issue_relations: Povolit vazby úkolů napříč projekty
172 field_activity: Aktivita
261 setting_issue_list_default_columns: Výchozí sloupce zobrazené v seznamu úkolů
173 field_spent_on: Datum
262 setting_repositories_encodings: Kódování
174 field_identifier: Identifikátor
263 setting_emails_footer: Patička emailů
175 field_is_filter: Použít jako filtr
264 setting_protocol: Protokol
176 field_issue_to_id: Související úkol
265 setting_per_page_options: Povolené počty řádků na stránce
177 field_delay: Zpoždění
266 setting_user_format: Formát zobrazení uživatele
178 field_assignable: Úkoly mohou být přiřazeny této roli
267 setting_activity_days_default: Days displayed on project activity
179 field_redirect_existing_links: Přesměrovat stvávající odkazy
268 setting_display_subprojects_issues: Display subprojects issues on main projects by default
180 field_estimated_hours: Odhadovaná doba
269
181 field_column_names: Sloupce
270 project_module_issue_tracking: Sledování úkolů
182 field_time_zone: Časové pásmo
271 project_module_time_tracking: Sledování času
183 field_searchable: Umožnit vyhledávání
272 project_module_news: Novinky
184 field_default_value: Výchozí hodnota
273 project_module_documents: Dokumenty
185 field_comments_sorting: Zobrazit komentáře
274 project_module_files: Soubory
186
275 project_module_wiki: Wiki
187 setting_app_title: Název aplikace
276 project_module_repository: Repository
188 setting_app_subtitle: Podtitulek aplikace
277 project_module_boards: Diskuse
189 setting_welcome_text: Uvítací text
278
190 setting_default_language: Výchozí jazyk
279 label_user: Uživatel
191 setting_login_required: Auten. vyžadována
280 label_user_plural: Uživatelé
192 setting_self_registration: Povolena automatická registrace
281 label_user_new: Nový uživatel
193 setting_attachment_max_size: Maximální velikost přílohy
282 label_project: Projekt
194 setting_issues_export_limit: Limit pro export úkolů
283 label_project_new: Nový projekt
195 setting_mail_from: Odesílat emaily z adresy
284 label_project_plural: Projekty
196 setting_bcc_recipients: Příjemci skryté kopie (bcc)
285 label_x_projects:
197 setting_host_name: Host name
286 zero: no projects
198 setting_text_formatting: Formátování textu
287 one: 1 project
199 setting_wiki_compression: Komperese historie Wiki
288 other: "{{count}} projects"
200 setting_feeds_limit: Feed content limit
289 label_project_all: Všechny projekty
201 setting_default_projects_public: Nové projekty nastavovat jako veřejné
290 label_project_latest: Poslední projekty
202 setting_autofetch_changesets: Autofetch commits
291 label_issue: Úkol
203 setting_sys_api_enabled: Povolit WS pro správu repozitory
292 label_issue_new: Nový úkol
204 setting_commit_ref_keywords: Klíčová slova pro odkazy
293 label_issue_plural: Úkoly
205 setting_commit_fix_keywords: Klíčová slova pro uzavření
294 label_issue_view_all: Všechny úkoly
206 setting_autologin: Automatické přihlašování
295 label_issues_by: "Úkoly od uživatele {{value}}"
207 setting_date_format: Formát data
296 label_issue_added: Úkol přidán
208 setting_time_format: Formát času
297 label_issue_updated: Úkol aktualizován
209 setting_cross_project_issue_relations: Povolit vazby úkolů napříč projekty
298 label_document: Dokument
210 setting_issue_list_default_columns: Výchozí sloupce zobrazené v seznamu úkolů
299 label_document_new: Nový dokument
211 setting_repositories_encodings: Kódování
300 label_document_plural: Dokumenty
212 setting_emails_footer: Patička emailů
301 label_document_added: Dokument přidán
213 setting_protocol: Protokol
302 label_role: Role
214 setting_per_page_options: Povolené počty řádků na stránce
303 label_role_plural: Role
215 setting_user_format: Formát zobrazení uživatele
304 label_role_new: Nová role
216 setting_activity_days_default: Days displayed on project activity
305 label_role_and_permissions: Role a práva
217 setting_display_subprojects_issues: Display subprojects issues on main projects by default
306 label_member: Člen
218
307 label_member_new: Nový člen
219 project_module_issue_tracking: Sledování úkolů
308 label_member_plural: Členové
220 project_module_time_tracking: Sledování času
309 label_tracker: Fronta
221 project_module_news: Novinky
310 label_tracker_plural: Fronty
222 project_module_documents: Dokumenty
311 label_tracker_new: Nová fronta
223 project_module_files: Soubory
312 label_workflow: Workflow
224 project_module_wiki: Wiki
313 label_issue_status: Stav úkolu
225 project_module_repository: Repository
314 label_issue_status_plural: Stavy úkolů
226 project_module_boards: Diskuse
315 label_issue_status_new: Nový stav
227
316 label_issue_category: Kategorie úkolu
228 label_user: Uživatel
317 label_issue_category_plural: Kategorie úkolů
229 label_user_plural: Uživatelé
318 label_issue_category_new: Nová kategorie
230 label_user_new: Nový uživatel
319 label_custom_field: Uživatelské pole
231 label_project: Projekt
320 label_custom_field_plural: Uživatelská pole
232 label_project_new: Nový projekt
321 label_custom_field_new: Nové uživatelské pole
233 label_project_plural: Projekty
322 label_enumerations: Seznamy
234 label_project_all: Všechny projekty
323 label_enumeration_new: Nová hodnota
235 label_project_latest: Poslední projekty
324 label_information: Informace
236 label_issue: Úkol
325 label_information_plural: Informace
237 label_issue_new: Nový úkol
326 label_please_login: Prosím přihlašte se
238 label_issue_plural: Úkoly
327 label_register: Registrovat
239 label_issue_view_all: Všechny úkoly
328 label_password_lost: Zapomenuté heslo
240 label_issues_by: Úkoly od uživatele %s
329 label_home: Úvodní
241 label_issue_added: Úkol přidán
330 label_my_page: Moje stránka
242 label_issue_updated: Úkol aktualizován
331 label_my_account: Můj účet
243 label_document: Dokument
332 label_my_projects: Moje projekty
244 label_document_new: Nový dokument
333 label_administration: Administrace
245 label_document_plural: Dokumenty
334 label_login: Přihlášení
246 label_document_added: Dokument přidán
335 label_logout: Odhlášení
247 label_role: Role
336 label_help: Nápověda
248 label_role_plural: Role
337 label_reported_issues: Nahlášené úkoly
249 label_role_new: Nová role
338 label_assigned_to_me_issues: Mé úkoly
250 label_role_and_permissions: Role a práva
339 label_last_login: Poslední přihlášení
251 label_member: Člen
340 label_registered_on: Registrován
252 label_member_new: Nový člen
341 label_activity: Aktivita
253 label_member_plural: Členové
342 label_overall_activity: Celková aktivita
254 label_tracker: Fronta
343 label_new: Nový
255 label_tracker_plural: Fronty
344 label_logged_as: Přihlášen jako
256 label_tracker_new: Nová fronta
345 label_environment: Prostředí
257 label_workflow: Workflow
346 label_authentication: Autentifikace
258 label_issue_status: Stav úkolu
347 label_auth_source: Mód autentifikace
259 label_issue_status_plural: Stavy úkolů
348 label_auth_source_new: Nový mód autentifikace
260 label_issue_status_new: Nový stav
349 label_auth_source_plural: Módy autentifikace
261 label_issue_category: Kategorie úkolu
350 label_subproject_plural: Podprojekty
262 label_issue_category_plural: Kategorie úkolů
351 label_min_max_length: Min - Max délka
263 label_issue_category_new: Nová kategorie
352 label_list: Seznam
264 label_custom_field: Uživatelské pole
353 label_date: Datum
265 label_custom_field_plural: Uživatelská pole
354 label_integer: Celé číslo
266 label_custom_field_new: Nové uživatelské pole
355 label_float: Desetiné číslo
267 label_enumerations: Seznamy
356 label_boolean: Ano/Ne
268 label_enumeration_new: Nová hodnota
357 label_string: Text
269 label_information: Informace
358 label_text: Dlouhý text
270 label_information_plural: Informace
359 label_attribute: Atribut
271 label_please_login: Prosím přihlašte se
360 label_attribute_plural: Atributy
272 label_register: Registrovat
361 label_download: "{{count}} Download"
273 label_password_lost: Zapomenuté heslo
362 label_download_plural: "{{count}} Downloads"
274 label_home: Úvodní
363 label_no_data: Žádné položky
275 label_my_page: Moje stránka
364 label_change_status: Změnit stav
276 label_my_account: Můj účet
365 label_history: Historie
277 label_my_projects: Moje projekty
366 label_attachment: Soubor
278 label_administration: Administrace
367 label_attachment_new: Nový soubor
279 label_login: Přihlášení
368 label_attachment_delete: Odstranit soubor
280 label_logout: Odhlášení
369 label_attachment_plural: Soubory
281 label_help: Nápověda
370 label_file_added: Soubor přidán
282 label_reported_issues: Nahlášené úkoly
371 label_report: Přeheled
283 label_assigned_to_me_issues: Mé úkoly
372 label_report_plural: Přehledy
284 label_last_login: Poslední přihlášení
373 label_news: Novinky
285 label_last_updates: Poslední změna
374 label_news_new: Přidat novinku
286 label_last_updates_plural: %d poslední změny
375 label_news_plural: Novinky
287 label_registered_on: Registrován
376 label_news_latest: Poslední novinky
288 label_activity: Aktivita
377 label_news_view_all: Zobrazit všechny novinky
289 label_overall_activity: Celková aktivita
378 label_news_added: Novinka přidána
290 label_new: Nový
379 label_change_log: Protokol změn
291 label_logged_as: Přihlášen jako
380 label_settings: Nastavení
292 label_environment: Prostředí
381 label_overview: Přehled
293 label_authentication: Autentifikace
382 label_version: Verze
294 label_auth_source: Mód autentifikace
383 label_version_new: Nová verze
295 label_auth_source_new: Nový mód autentifikace
384 label_version_plural: Verze
296 label_auth_source_plural: Módy autentifikace
385 label_confirmation: Potvrzení
297 label_subproject_plural: Podprojekty
386 label_export_to: 'Také k dispozici:'
298 label_min_max_length: Min - Max délka
387 label_read: Načítá se...
299 label_list: Seznam
388 label_public_projects: Veřejné projekty
300 label_date: Datum
389 label_open_issues: otevřený
301 label_integer: Celé číslo
390 label_open_issues_plural: otevřené
302 label_float: Desetiné číslo
391 label_closed_issues: uzavřený
303 label_boolean: Ano/Ne
392 label_closed_issues_plural: uzavřené
304 label_string: Text
393 label_x_open_issues_abbr_on_total:
305 label_text: Dlouhý text
394 zero: 0 open / {{total}}
306 label_attribute: Atribut
395 one: 1 open / {{total}}
307 label_attribute_plural: Atributy
396 other: "{{count}} open / {{total}}"
308 label_download: %d Download
397 label_x_open_issues_abbr:
309 label_download_plural: %d Downloads
398 zero: 0 open
310 label_no_data: Žádné položky
399 one: 1 open
311 label_change_status: Změnit stav
400 other: "{{count}} open"
312 label_history: Historie
401 label_x_closed_issues_abbr:
313 label_attachment: Soubor
402 zero: 0 closed
314 label_attachment_new: Nový soubor
403 one: 1 closed
315 label_attachment_delete: Odstranit soubor
404 other: "{{count}} closed"
316 label_attachment_plural: Soubory
405 label_total: Celkem
317 label_file_added: Soubor přidán
406 label_permissions: Práva
318 label_report: Přeheled
407 label_current_status: Aktuální stav
319 label_report_plural: Přehledy
408 label_new_statuses_allowed: Nové povolené stavy
320 label_news: Novinky
409 label_all: vše
321 label_news_new: Přidat novinku
410 label_none: nic
322 label_news_plural: Novinky
411 label_nobody: nikdo
323 label_news_latest: Poslední novinky
412 label_next: Další
324 label_news_view_all: Zobrazit všechny novinky
413 label_previous: Předchozí
325 label_news_added: Novinka přidána
414 label_used_by: Použito
326 label_change_log: Protokol změn
415 label_details: Detaily
327 label_settings: Nastavení
416 label_add_note: Přidat poznámku
328 label_overview: Přehled
417 label_per_page: Na stránku
329 label_version: Verze
418 label_calendar: Kalendář
330 label_version_new: Nová verze
419 label_months_from: měsíců od
331 label_version_plural: Verze
420 label_gantt: Ganttův graf
332 label_confirmation: Potvrzení
421 label_internal: Interní
333 label_export_to: 'Také k dispozici:'
422 label_last_changes: "posledních {{count}} změn"
334 label_read: Načítá se...
423 label_change_view_all: Zobrazit všechny změny
335 label_public_projects: Veřejné projekty
424 label_personalize_page: Přizpůsobit tuto stránku
336 label_open_issues: otevřený
425 label_comment: Komentář
337 label_open_issues_plural: otevřené
426 label_comment_plural: Komentáře
338 label_closed_issues: uzavřený
427 label_x_comments:
339 label_closed_issues_plural: uzavřené
428 zero: no comments
340 label_total: Celkem
429 one: 1 comment
341 label_permissions: Práva
430 other: "{{count}} comments"
342 label_current_status: Aktuální stav
431 label_comment_add: Přidat komentáře
343 label_new_statuses_allowed: Nové povolené stavy
432 label_comment_added: Komentář přidán
344 label_all: vše
433 label_comment_delete: Odstranit komentář
345 label_none: nic
434 label_query: Uživatelský dotaz
346 label_nobody: nikdo
435 label_query_plural: Uživatelské dotazy
347 label_next: Další
436 label_query_new: Nový dotaz
348 label_previous: Předchozí
437 label_filter_add: Přidat filtr
349 label_used_by: Použito
438 label_filter_plural: Filtry
350 label_details: Detaily
439 label_equals: je
351 label_add_note: Přidat poznámku
440 label_not_equals: není
352 label_per_page: Na stránku
441 label_in_less_than: je měší než
353 label_calendar: Kalendář
442 label_in_more_than: je větší než
354 label_months_from: měsíců od
443 label_in: v
355 label_gantt: Ganttův graf
444 label_today: dnes
356 label_internal: Interní
445 label_all_time: vše
357 label_last_changes: posledních %d změn
446 label_yesterday: včera
358 label_change_view_all: Zobrazit všechny změny
447 label_this_week: tento týden
359 label_personalize_page: Přizpůsobit tuto stránku
448 label_last_week: minulý týden
360 label_comment: Komentář
449 label_last_n_days: "posledních {{count}} dnů"
361 label_comment_plural: Komentáře
450 label_this_month: tento měsíc
362 label_comment_add: Přidat komentáře
451 label_last_month: minulý měsíc
363 label_comment_added: Komentář přidán
452 label_this_year: tento rok
364 label_comment_delete: Odstranit komentář
453 label_date_range: Časový rozsah
365 label_query: Uživatelský dotaz
454 label_less_than_ago: před méně jak (dny)
366 label_query_plural: Uživatelské dotazy
455 label_more_than_ago: před více jak (dny)
367 label_query_new: Nový dotaz
456 label_ago: před (dny)
368 label_filter_add: Přidat filtr
457 label_contains: obsahuje
369 label_filter_plural: Filtry
458 label_not_contains: neobsahuje
370 label_equals: je
459 label_day_plural: dny
371 label_not_equals: není
460 label_repository: Repository
372 label_in_less_than: je měší než
461 label_repository_plural: Repository
373 label_in_more_than: je větší než
462 label_browse: Procházet
374 label_in: v
463 label_modification: "{{count}} změna"
375 label_today: dnes
464 label_modification_plural: "{{count}} změn"
376 label_all_time: vše
465 label_revision: Revize
377 label_yesterday: včera
466 label_revision_plural: Revizí
378 label_this_week: tento týden
467 label_associated_revisions: Související verze
379 label_last_week: minulý týden
468 label_added: přidáno
380 label_last_n_days: posledních %d dnů
469 label_modified: změněno
381 label_this_month: tento měsíc
470 label_deleted: odstraněno
382 label_last_month: minulý měsíc
471 label_latest_revision: Poslední revize
383 label_this_year: tento rok
472 label_latest_revision_plural: Poslední revize
384 label_date_range: Časový rozsah
473 label_view_revisions: Zobrazit revize
385 label_less_than_ago: před méně jak (dny)
474 label_max_size: Maximální velikost
386 label_more_than_ago: před více jak (dny)
475 label_sort_highest: Přesunout na začátek
387 label_ago: před (dny)
476 label_sort_higher: Přesunout nahoru
388 label_contains: obsahuje
477 label_sort_lower: Přesunout dolů
389 label_not_contains: neobsahuje
478 label_sort_lowest: Přesunout na konec
390 label_day_plural: dny
479 label_roadmap: Plán
391 label_repository: Repository
480 label_roadmap_due_in: "Zbývá {{value}}"
392 label_repository_plural: Repository
481 label_roadmap_overdue: "{{value}} pozdě"
393 label_browse: Procházet
482 label_roadmap_no_issues: Pro tuto verzi nejsou žádné úkoly
394 label_modification: %d změna
483 label_search: Hledat
395 label_modification_plural: %d změn
484 label_result_plural: Výsledky
396 label_revision: Revize
485 label_all_words: Všechna slova
397 label_revision_plural: Revizí
486 label_wiki: Wiki
398 label_associated_revisions: Související verze
487 label_wiki_edit: Wiki úprava
399 label_added: přidáno
488 label_wiki_edit_plural: Wiki úpravy
400 label_modified: změněno
489 label_wiki_page: Wiki stránka
401 label_deleted: odstraněno
490 label_wiki_page_plural: Wiki stránky
402 label_latest_revision: Poslední revize
491 label_index_by_title: Index dle názvu
403 label_latest_revision_plural: Poslední revize
492 label_index_by_date: Index dle data
404 label_view_revisions: Zobrazit revize
493 label_current_version: Aktuální verze
405 label_max_size: Maximální velikost
494 label_preview: Náhled
406 label_on: 'zapnuto'
495 label_feed_plural: Příspěvky
407 label_sort_highest: Přesunout na začátek
496 label_changes_details: Detail všech změn
408 label_sort_higher: Přesunout nahoru
497 label_issue_tracking: Sledování úkolů
409 label_sort_lower: Přesunout dolů
498 label_spent_time: Strávený čas
410 label_sort_lowest: Přesunout na konec
499 label_f_hour: "{{value}} hodina"
411 label_roadmap: Plán
500 label_f_hour_plural: "{{value}} hodin"
412 label_roadmap_due_in: Zbývá %s
501 label_time_tracking: Sledování času
413 label_roadmap_overdue: %s pozdě
502 label_change_plural: Změny
414 label_roadmap_no_issues: Pro tuto verzi nejsou žádné úkoly
503 label_statistics: Statistiky
415 label_search: Hledat
504 label_commits_per_month: Commitů za měsíc
416 label_result_plural: Výsledky
505 label_commits_per_author: Commitů za autora
417 label_all_words: Všechna slova
506 label_view_diff: Zobrazit rozdíly
418 label_wiki: Wiki
507 label_diff_inline: uvnitř
419 label_wiki_edit: Wiki úprava
508 label_diff_side_by_side: vedle sebe
420 label_wiki_edit_plural: Wiki úpravy
509 label_options: Nastavení
421 label_wiki_page: Wiki stránka
510 label_copy_workflow_from: Kopírovat workflow z
422 label_wiki_page_plural: Wiki stránky
511 label_permissions_report: Přehled práv
423 label_index_by_title: Index dle názvu
512 label_watched_issues: Sledované úkoly
424 label_index_by_date: Index dle data
513 label_related_issues: Související úkoly
425 label_current_version: Aktuální verze
514 label_applied_status: Použitý stav
426 label_preview: Náhled
515 label_loading: Nahrávám...
427 label_feed_plural: Příspěvky
516 label_relation_new: Nová souvislost
428 label_changes_details: Detail všech změn
517 label_relation_delete: Odstranit souvislost
429 label_issue_tracking: Sledování úkolů
518 label_relates_to: související s
430 label_spent_time: Strávený čas
519 label_duplicates: duplicity
431 label_f_hour: %.2f hodina
520 label_blocks: bloků
432 label_f_hour_plural: %.2f hodin
521 label_blocked_by: zablokován
433 label_time_tracking: Sledování času
522 label_precedes: předchází
434 label_change_plural: Změny
523 label_follows: následuje
435 label_statistics: Statistiky
524 label_end_to_start: od konce do začátku
436 label_commits_per_month: Commitů za měsíc
525 label_end_to_end: od konce do konce
437 label_commits_per_author: Commitů za autora
526 label_start_to_start: od začátku do začátku
438 label_view_diff: Zobrazit rozdíly
527 label_start_to_end: od začátku do konce
439 label_diff_inline: uvnitř
528 label_stay_logged_in: Zůstat přihlášený
440 label_diff_side_by_side: vedle sebe
529 label_disabled: zakázán
441 label_options: Nastavení
530 label_show_completed_versions: Ukázat dokončené verze
442 label_copy_workflow_from: Kopírovat workflow z
531 label_me:
443 label_permissions_report: Přehled práv
532 label_board: Fórum
444 label_watched_issues: Sledované úkoly
533 label_board_new: Nové fórum
445 label_related_issues: Související úkoly
534 label_board_plural: Fóra
446 label_applied_status: Použitý stav
535 label_topic_plural: Témata
447 label_loading: Nahrávám...
536 label_message_plural: Zprávy
448 label_relation_new: Nová souvislost
537 label_message_last: Poslední zpráva
449 label_relation_delete: Odstranit souvislost
538 label_message_new: Nová zpráva
450 label_relates_to: související s
539 label_message_posted: Zpráva přidána
451 label_duplicates: duplicity
540 label_reply_plural: Odpovědi
452 label_blocks: bloků
541 label_send_information: Zaslat informace o účtu uživateli
453 label_blocked_by: zablokován
542 label_year: Rok
454 label_precedes: předchází
543 label_month: Měsíc
455 label_follows: následuje
544 label_week: Týden
456 label_end_to_start: od konce do začátku
545 label_date_from: Od
457 label_end_to_end: od konce do konce
546 label_date_to: Do
458 label_start_to_start: od začátku do začátku
547 label_language_based: Podle výchozího jazyku
459 label_start_to_end: od začátku do konce
548 label_sort_by: "Seřadit podle {{value}}"
460 label_stay_logged_in: Zůstat přihlášený
549 label_send_test_email: Poslat testovací email
461 label_disabled: zakázán
550 label_feeds_access_key_created_on: "Přístupový klíč pro RSS byl vytvořen před {{value}}"
462 label_show_completed_versions: Ukázat dokončené verze
551 label_module_plural: Moduly
463 label_me:
552 label_added_time_by: "'Přidáno uživatelem {{author}} před {{age}}'"
464 label_board: Fórum
553 label_updated_time: "Aktualizováno před {{value}}'"
465 label_board_new: Nové fórum
554 label_jump_to_a_project: Zvolit projekt...
466 label_board_plural: Fóra
555 label_file_plural: Soubory
467 label_topic_plural: Témata
556 label_changeset_plural: Changesety
468 label_message_plural: Zprávy
557 label_default_columns: Výchozí sloupce
469 label_message_last: Poslední zpráva
558 label_no_change_option: (beze změny)
470 label_message_new: Nová zpráva
559 label_bulk_edit_selected_issues: Bulk edit selected issues
471 label_message_posted: Zpráva přidána
560 label_theme: Téma
472 label_reply_plural: Odpovědi
561 label_default: Výchozí
473 label_send_information: Zaslat informace o účtu uživateli
562 label_search_titles_only: Vyhledávat pouze v názvech
474 label_year: Rok
563 label_user_mail_option_all: "Pro všechny události všech mých projektů"
475 label_month: Měsíc
564 label_user_mail_option_selected: "Pro všechny události vybraných projektů..."
476 label_week: Týden
565 label_user_mail_option_none: "Pouze pro události které sleduji nebo které se mne týkají"
477 label_date_from: Od
566 label_user_mail_no_self_notified: "Nezasílat informace o mnou vytvořených změnách"
478 label_date_to: Do
567 label_registration_activation_by_email: aktivace účtu emailem
479 label_language_based: Podle výchozího jazyku
568 label_registration_manual_activation: manuální aktivace účtu
480 label_sort_by: Seřadit podle %s
569 label_registration_automatic_activation: automatická aktivace účtu
481 label_send_test_email: Poslat testovací email
570 label_display_per_page: "{{value}} na stránku'"
482 label_feeds_access_key_created_on: Přístupový klíč pro RSS byl vytvořen před %s
571 label_age: Věk
483 label_module_plural: Moduly
572 label_change_properties: Změnit vlastnosti
484 label_added_time_by: 'Přidáno uživatelem %s před %s'
573 label_general: Obecné
485 label_updated_time: 'Aktualizováno před %s'
574 label_more: Více
486 label_jump_to_a_project: Zvolit projekt...
575 label_scm: SCM
487 label_file_plural: Soubory
576 label_plugins: Doplňky
488 label_changeset_plural: Changesety
577 label_ldap_authentication: Autentifikace LDAP
489 label_default_columns: Výchozí sloupce
578 label_downloads_abbr: D/L
490 label_no_change_option: (beze změny)
579 label_optional_description: Volitelný popis
491 label_bulk_edit_selected_issues: Bulk edit selected issues
580 label_add_another_file: Přidat další soubor
492 label_theme: Téma
581 label_preferences: Nastavení
493 label_default: Výchozí
582 label_chronological_order: V chronologickém pořadí
494 label_search_titles_only: Vyhledávat pouze v názvech
583 label_reverse_chronological_order: V obrácaném chronologickém pořadí
495 label_user_mail_option_all: "Pro všechny události všech mých projektů"
584
496 label_user_mail_option_selected: "Pro všechny události vybraných projektů..."
585 button_login: Přihlásit
497 label_user_mail_option_none: "Pouze pro události které sleduji nebo které se mne týkají"
586 button_submit: Potvrdit
498 label_user_mail_no_self_notified: "Nezasílat informace o mnou vytvořených změnách"
587 button_save: Uložit
499 label_registration_activation_by_email: aktivace účtu emailem
588 button_check_all: Zašrtnout vše
500 label_registration_manual_activation: manuální aktivace účtu
589 button_uncheck_all: Odšrtnout vše
501 label_registration_automatic_activation: automatická aktivace účtu
590 button_delete: Odstranit
502 label_display_per_page: '%s na stránku'
591 button_create: Vytvořit
503 label_age: Věk
592 button_test: Test
504 label_change_properties: Změnit vlastnosti
593 button_edit: Upravit
505 label_general: Obecné
594 button_add: Přidat
506 label_more: Více
595 button_change: Změnit
507 label_scm: SCM
596 button_apply: Použít
508 label_plugins: Doplňky
597 button_clear: Smazat
509 label_ldap_authentication: Autentifikace LDAP
598 button_lock: Zamknout
510 label_downloads_abbr: D/L
599 button_unlock: Odemknout
511 label_optional_description: Volitelný popis
600 button_download: Stáhnout
512 label_add_another_file: Přidat další soubor
601 button_list: Vypsat
513 label_preferences: Nastavení
602 button_view: Zobrazit
514 label_chronological_order: V chronologickém pořadí
603 button_move: Přesunout
515 label_reverse_chronological_order: V obrácaném chronologickém pořadí
604 button_back: Zpět
516
605 button_cancel: Storno
517 button_login: Přihlásit
606 button_activate: Aktivovat
518 button_submit: Potvrdit
607 button_sort: Seřadit
519 button_save: Uložit
608 button_log_time: Přidat čas
520 button_check_all: Zašrtnout vše
609 button_rollback: Zpět k této verzi
521 button_uncheck_all: Odšrtnout vše
610 button_watch: Sledovat
522 button_delete: Odstranit
611 button_unwatch: Nesledovat
523 button_create: Vytvořit
612 button_reply: Odpovědět
524 button_test: Test
613 button_archive: Archivovat
525 button_edit: Upravit
614 button_unarchive: Odarchivovat
526 button_add: Přidat
615 button_reset: Reset
527 button_change: Změnit
616 button_rename: Přejmenovat
528 button_apply: Použít
617 button_change_password: Změnit heslo
529 button_clear: Smazat
618 button_copy: Kopírovat
530 button_lock: Zamknout
619 button_annotate: Komentovat
531 button_unlock: Odemknout
620 button_update: Aktualizovat
532 button_download: Stáhnout
621 button_configure: Konfigurovat
533 button_list: Vypsat
622
534 button_view: Zobrazit
623 status_active: aktivní
535 button_move: Přesunout
624 status_registered: registrovaný
536 button_back: Zpět
625 status_locked: uzamčený
537 button_cancel: Storno
626
538 button_activate: Aktivovat
627 text_select_mail_notifications: Vyberte akci při které bude zasláno upozornění emailem.
539 button_sort: Seřadit
628 text_regexp_info: např. ^[A-Z0-9]+$
540 button_log_time: Přidat čas
629 text_min_max_length_info: 0 znamená bez limitu
541 button_rollback: Zpět k této verzi
630 text_project_destroy_confirmation: Jste si jisti, že chcete odstranit tento projekt a všechna související data ?
542 button_watch: Sledovat
631 text_workflow_edit: Vyberte roli a frontu k editaci workflow
543 button_unwatch: Nesledovat
632 text_are_you_sure: Jste si jisti?
544 button_reply: Odpovědět
633 text_journal_changed: "změněno z {{old}} na {{new}}"
545 button_archive: Archivovat
634 text_journal_set_to: "nastaveno na {{value}}"
546 button_unarchive: Odarchivovat
635 text_journal_deleted: odstraněno
547 button_reset: Reset
636 text_tip_task_begin_day: úkol začíná v tento den
548 button_rename: Přejmenovat
637 text_tip_task_end_day: úkol končí v tento den
549 button_change_password: Změnit heslo
638 text_tip_task_begin_end_day: úkol začíná a končí v tento den
550 button_copy: Kopírovat
639 text_project_identifier_info: 'Jsou povolena malá písmena (a-z), čísla a pomlčky.<br />Po uložení již není možné identifikátor změnit.'
551 button_annotate: Komentovat
640 text_caracters_maximum: "{{count}} znaků maximálně."
552 button_update: Aktualizovat
641 text_caracters_minimum: "Musí být alespoň {{count}} znaků dlouhé."
553 button_configure: Konfigurovat
642 text_length_between: "Délka mezi {{min}} a {{max}} znaky."
554
643 text_tracker_no_workflow: Pro tuto frontu není definován žádný workflow
555 status_active: aktivní
644 text_unallowed_characters: Nepovolené znaky
556 status_registered: registrovaný
645 text_comma_separated: Povoleno více hodnot (oddělěné čárkou).
557 status_locked: uzamčený
646 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
558
647 text_issue_added: "Úkol {{id}} byl vytvořen uživatelem {{author}}."
559 text_select_mail_notifications: Vyberte akci při které bude zasláno upozornění emailem.
648 text_issue_updated: "Úkol {{id}} byl aktualizován uživatelem {{author}}."
560 text_regexp_info: např. ^[A-Z0-9]+$
649 text_wiki_destroy_confirmation: Opravdu si přejete odstranit tuto WIKI a celý její obsah?
561 text_min_max_length_info: 0 znamená bez limitu
650 text_issue_category_destroy_question: "Některé úkoly ({{count}}) jsou přiřazeny k této kategorii. Co s nimi chtete udělat?"
562 text_project_destroy_confirmation: Jste si jisti, že chcete odstranit tento projekt a všechna související data ?
651 text_issue_category_destroy_assignments: Zrušit přiřazení ke kategorii
563 text_workflow_edit: Vyberte roli a frontu k editaci workflow
652 text_issue_category_reassign_to: Přiřadit úkoly do této kategorie
564 text_are_you_sure: Jste si jisti?
653 text_user_mail_option: "U projektů, které nebyly vybrány, budete dostávat oznámení pouze o vašich či o sledovaných položkách (např. o položkách jejichž jste autor nebo ke kterým jste přiřazen(a))."
565 text_journal_changed: změněno z %s na %s
654 text_no_configuration_data: "Role, fronty, stavy úkolů ani workflow nebyly zatím nakonfigurovány.\nVelice doporučujeme nahrát výchozí konfiguraci.Po si můžete vše upravit"
566 text_journal_set_to: nastaveno na %s
655 text_load_default_configuration: Nahrát výchozí konfiguraci
567 text_journal_deleted: odstraněno
656 text_status_changed_by_changeset: "Použito v changesetu {{value}}."
568 text_tip_task_begin_day: úkol začíná v tento den
657 text_issues_destroy_confirmation: 'Opravdu si přejete odstranit všechny zvolené úkoly?'
569 text_tip_task_end_day: úkol končí v tento den
658 text_select_project_modules: 'Aktivní moduly v tomto projektu:'
570 text_tip_task_begin_end_day: úkol začíná a končí v tento den
659 text_default_administrator_account_changed: Výchozí nastavení administrátorského účtu změněno
571 text_project_identifier_info: 'Jsou povolena malá písmena (a-z), čísla a pomlčky.<br />Po uložení již není možné identifikátor změnit.'
660 text_file_repository_writable: Povolen zápis do repository
572 text_caracters_maximum: %d znaků maximálně.
661 text_rmagick_available: RMagick k dispozici (volitelné)
573 text_caracters_minimum: Musí být alespoň %d znaků dlouhé.
662 text_destroy_time_entries_question: U úkolů, které chcete odstranit je evidováno %.02f práce. Co chete udělat?
574 text_length_between: Délka mezi %d a %d znaky.
663 text_destroy_time_entries: Odstranit evidované hodiny.
575 text_tracker_no_workflow: Pro tuto frontu není definován žádný workflow
664 text_assign_time_entries_to_project: Přiřadit evidované hodiny projektu
576 text_unallowed_characters: Nepovolené znaky
665 text_reassign_time_entries: 'Přeřadit evidované hodiny k tomuto úkolu:'
577 text_comma_separated: Povoleno více hodnot (oddělěné čárkou).
666
578 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
667 default_role_manager: Manažer
579 text_issue_added: Úkol %s byl vytvořen uživatelem %s.
668 default_role_developper: Vývojář
580 text_issue_updated: Úkol %s byl aktualizován uživatelem %s.
669 default_role_reporter: Reportér
581 text_wiki_destroy_confirmation: Opravdu si přejete odstranit tuto WIKI a celý její obsah?
670 default_tracker_bug: Chyba
582 text_issue_category_destroy_question: Některé úkoly (%d) jsou přiřazeny k této kategorii. Co s nimi chtete udělat?
671 default_tracker_feature: Požadavek
583 text_issue_category_destroy_assignments: Zrušit přiřazení ke kategorii
672 default_tracker_support: Podpora
584 text_issue_category_reassign_to: Přiřadit úkoly do této kategorie
673 default_issue_status_new: Nový
585 text_user_mail_option: "U projektů, které nebyly vybrány, budete dostávat oznámení pouze o vašich či o sledovaných položkách (např. o položkách jejichž jste autor nebo ke kterým jste přiřazen(a))."
674 default_issue_status_assigned: Přiřazený
586 text_no_configuration_data: "Role, fronty, stavy úkolů ani workflow nebyly zatím nakonfigurovány.\nVelice doporučujeme nahrát výchozí konfiguraci.Po si můžete vše upravit"
675 default_issue_status_resolved: Vyřešený
587 text_load_default_configuration: Nahrát výchozí konfiguraci
676 default_issue_status_feedback: Čeká se
588 text_status_changed_by_changeset: Použito v changesetu %s.
677 default_issue_status_closed: Uzavřený
589 text_issues_destroy_confirmation: 'Opravdu si přejete odstranit všechny zvolené úkoly?'
678 default_issue_status_rejected: Odmítnutý
590 text_select_project_modules: 'Aktivní moduly v tomto projektu:'
679 default_doc_category_user: Uživatelská dokumentace
591 text_default_administrator_account_changed: Výchozí nastavení administrátorského účtu změněno
680 default_doc_category_tech: Technická dokumentace
592 text_file_repository_writable: Povolen zápis do repository
681 default_priority_low: Nízká
593 text_rmagick_available: RMagick k dispozici (volitelné)
682 default_priority_normal: Normální
594 text_destroy_time_entries_question: U úkolů, které chcete odstranit je evidováno %.02f práce. Co chete udělat?
683 default_priority_high: Vysoká
595 text_destroy_time_entries: Odstranit evidované hodiny.
684 default_priority_urgent: Urgentní
596 text_assign_time_entries_to_project: Přiřadit evidované hodiny projektu
685 default_priority_immediate: Okamžitá
597 text_reassign_time_entries: 'Přeřadit evidované hodiny k tomuto úkolu:'
686 default_activity_design: Design
598
687 default_activity_development: Vývoj
599 default_role_manager: Manažer
688
600 default_role_developper: Vývojář
689 enumeration_issue_priorities: Priority úkolů
601 default_role_reporter: Reportér
690 enumeration_doc_categories: Kategorie dokumentů
602 default_tracker_bug: Chyba
691 enumeration_activities: Aktivity (sledování času)
603 default_tracker_feature: Požadavek
692 error_scm_annotate: "Položka neexistuje nebo nemůže být komentována."
604 default_tracker_support: Podpora
693 label_planning: Plánování
605 default_issue_status_new: Nový
694 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted.'"
606 default_issue_status_assigned: Přiřazený
695 label_and_its_subprojects: "{{value}} and its subprojects"
607 default_issue_status_resolved: Vyřešený
696 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
608 default_issue_status_feedback: Čeká se
697 mail_subject_reminder: "{{count}} issue(s) due in the next days"
609 default_issue_status_closed: Uzavřený
698 text_user_wrote: "{{value}} wrote:'"
610 default_issue_status_rejected: Odmítnutý
699 label_duplicated_by: duplicated by
611 default_doc_category_user: Uživatelská dokumentace
700 setting_enabled_scm: Enabled SCM
612 default_doc_category_tech: Technická dokumentace
701 text_enumeration_category_reassign_to: 'Reassign them to this value:'
613 default_priority_low: Nízká
702 text_enumeration_destroy_question: "{{count}} objects are assigned to this value.'"
614 default_priority_normal: Normální
703 label_incoming_emails: Incoming emails
615 default_priority_high: Vysoká
704 label_generate_key: Generate a key
616 default_priority_urgent: Urgentní
705 setting_mail_handler_api_enabled: Enable WS for incoming emails
617 default_priority_immediate: Okamžitá
706 setting_mail_handler_api_key: API key
618 default_activity_design: Design
707 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
619 default_activity_development: Vývoj
708 field_parent_title: Parent page
620
709 label_issue_watchers: Watchers
621 enumeration_issue_priorities: Priority úkolů
710 setting_commit_logs_encoding: Commit messages encoding
622 enumeration_doc_categories: Kategorie dokumentů
711 button_quote: Quote
623 enumeration_activities: Aktivity (sledování času)
712 setting_sequential_project_identifiers: Generate sequential project identifiers
624 error_scm_annotate: "Položka neexistuje nebo nemůže být komentována."
713 notice_unable_delete_version: Unable to delete version
625 label_planning: Plánování
714 label_renamed: renamed
626 text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
715 label_copied: copied
627 label_and_its_subprojects: %s and its subprojects
716 setting_plain_text_mail: plain text only (no HTML)
628 mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
717 permission_view_files: View files
629 mail_subject_reminder: "%d issue(s) due in the next days"
718 permission_edit_issues: Edit issues
630 text_user_wrote: '%s wrote:'
719 permission_edit_own_time_entries: Edit own time logs
631 label_duplicated_by: duplicated by
720 permission_manage_public_queries: Manage public queries
632 setting_enabled_scm: Enabled SCM
721 permission_add_issues: Add issues
633 text_enumeration_category_reassign_to: 'Reassign them to this value:'
722 permission_log_time: Log spent time
634 text_enumeration_destroy_question: '%d objects are assigned to this value.'
723 permission_view_changesets: View changesets
635 label_incoming_emails: Incoming emails
724 permission_view_time_entries: View spent time
636 label_generate_key: Generate a key
725 permission_manage_versions: Manage versions
637 setting_mail_handler_api_enabled: Enable WS for incoming emails
726 permission_manage_wiki: Manage wiki
638 setting_mail_handler_api_key: API key
727 permission_manage_categories: Manage issue categories
639 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
728 permission_protect_wiki_pages: Protect wiki pages
640 field_parent_title: Parent page
729 permission_comment_news: Comment news
641 label_issue_watchers: Watchers
730 permission_delete_messages: Delete messages
642 setting_commit_logs_encoding: Commit messages encoding
731 permission_select_project_modules: Select project modules
643 button_quote: Quote
732 permission_manage_documents: Manage documents
644 setting_sequential_project_identifiers: Generate sequential project identifiers
733 permission_edit_wiki_pages: Edit wiki pages
645 notice_unable_delete_version: Unable to delete version
734 permission_add_issue_watchers: Add watchers
646 label_renamed: renamed
735 permission_view_gantt: View gantt chart
647 label_copied: copied
736 permission_move_issues: Move issues
648 setting_plain_text_mail: plain text only (no HTML)
737 permission_manage_issue_relations: Manage issue relations
649 permission_view_files: View files
738 permission_delete_wiki_pages: Delete wiki pages
650 permission_edit_issues: Edit issues
739 permission_manage_boards: Manage boards
651 permission_edit_own_time_entries: Edit own time logs
740 permission_delete_wiki_pages_attachments: Delete attachments
652 permission_manage_public_queries: Manage public queries
741 permission_view_wiki_edits: View wiki history
653 permission_add_issues: Add issues
742 permission_add_messages: Post messages
654 permission_log_time: Log spent time
743 permission_view_messages: View messages
655 permission_view_changesets: View changesets
744 permission_manage_files: Manage files
656 permission_view_time_entries: View spent time
745 permission_edit_issue_notes: Edit notes
657 permission_manage_versions: Manage versions
746 permission_manage_news: Manage news
658 permission_manage_wiki: Manage wiki
747 permission_view_calendar: View calendrier
659 permission_manage_categories: Manage issue categories
748 permission_manage_members: Manage members
660 permission_protect_wiki_pages: Protect wiki pages
749 permission_edit_messages: Edit messages
661 permission_comment_news: Comment news
750 permission_delete_issues: Delete issues
662 permission_delete_messages: Delete messages
751 permission_view_issue_watchers: View watchers list
663 permission_select_project_modules: Select project modules
752 permission_manage_repository: Manage repository
664 permission_manage_documents: Manage documents
753 permission_commit_access: Commit access
665 permission_edit_wiki_pages: Edit wiki pages
754 permission_browse_repository: Browse repository
666 permission_add_issue_watchers: Add watchers
755 permission_view_documents: View documents
667 permission_view_gantt: View gantt chart
756 permission_edit_project: Edit project
668 permission_move_issues: Move issues
757 permission_add_issue_notes: Add notes
669 permission_manage_issue_relations: Manage issue relations
758 permission_save_queries: Save queries
670 permission_delete_wiki_pages: Delete wiki pages
759 permission_view_wiki_pages: View wiki
671 permission_manage_boards: Manage boards
760 permission_rename_wiki_pages: Rename wiki pages
672 permission_delete_wiki_pages_attachments: Delete attachments
761 permission_edit_time_entries: Edit time logs
673 permission_view_wiki_edits: View wiki history
762 permission_edit_own_issue_notes: Edit own notes
674 permission_add_messages: Post messages
763 setting_gravatar_enabled: Use Gravatar user icons
675 permission_view_messages: View messages
764 label_example: Example
676 permission_manage_files: Manage files
765 text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
677 permission_edit_issue_notes: Edit notes
766 permission_edit_own_messages: Edit own messages
678 permission_manage_news: Manage news
767 permission_delete_own_messages: Delete own messages
679 permission_view_calendar: View calendrier
768 label_user_activity: "{{value}}'s activity"
680 permission_manage_members: Manage members
769 label_updated_time_by: "Updated by {{author}} {{age}} ago"
681 permission_edit_messages: Edit messages
770 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
682 permission_delete_issues: Delete issues
771 setting_diff_max_lines_displayed: Max number of diff lines displayed
683 permission_view_issue_watchers: View watchers list
772 text_plugin_assets_writable: Plugin assets directory writable
684 permission_manage_repository: Manage repository
773 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
685 permission_commit_access: Commit access
774 button_create_and_continue: Create and continue
686 permission_browse_repository: Browse repository
775 text_custom_field_possible_values_info: 'One line for each value'
687 permission_view_documents: View documents
776 label_display: Display
688 permission_edit_project: Edit project
777 field_editable: Editable
689 permission_add_issue_notes: Add notes
778 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
690 permission_save_queries: Save queries
691 permission_view_wiki_pages: View wiki
692 permission_rename_wiki_pages: Rename wiki pages
693 permission_edit_time_entries: Edit time logs
694 permission_edit_own_issue_notes: Edit own notes
695 setting_gravatar_enabled: Use Gravatar user icons
696 label_example: Example
697 text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
698 permission_edit_own_messages: Edit own messages
699 permission_delete_own_messages: Delete own messages
700 label_user_activity: "%s's activity"
701 label_updated_time_by: Updated by %s %s ago
702 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
703 setting_diff_max_lines_displayed: Max number of diff lines displayed
704 text_plugin_assets_writable: Plugin assets directory writable
705 warning_attachments_not_saved: "%d file(s) could not be saved."
706 button_create_and_continue: Create and continue
707 text_custom_field_possible_values_info: 'One line for each value'
708 label_display: Display
709 field_editable: Editable
710 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
711 field_identity_url: OpenID URL
712 setting_openid: Allow OpenID login and registration
713 label_login_with_open_id_option: or login with OpenID
714 field_watcher: Watcher
1 NO CONTENT: file renamed from lang/da.yml to config/locales/da.yml
NO CONTENT: file renamed from lang/da.yml to config/locales/da.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/de.yml to config/locales/de.yml
NO CONTENT: file renamed from lang/de.yml to config/locales/de.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/en.yml to config/locales/en.yml
NO CONTENT: file renamed from lang/en.yml to config/locales/en.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/fi.yml to config/locales/fi.yml
NO CONTENT: file renamed from lang/fi.yml to config/locales/fi.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/fr.yml to config/locales/fr.yml
NO CONTENT: file renamed from lang/fr.yml to config/locales/fr.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/he.yml to config/locales/he.yml
NO CONTENT: file renamed from lang/he.yml to config/locales/he.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/hu.yml to config/locales/hu.yml
NO CONTENT: file renamed from lang/hu.yml to config/locales/hu.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/it.yml to config/locales/it.yml
NO CONTENT: file renamed from lang/it.yml to config/locales/it.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/ja.yml to config/locales/ja.yml
NO CONTENT: file renamed from lang/ja.yml to config/locales/ja.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/lt.yml to config/locales/lt.yml
NO CONTENT: file renamed from lang/lt.yml to config/locales/lt.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/nl.yml to config/locales/nl.yml
NO CONTENT: file renamed from lang/nl.yml to config/locales/nl.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/no.yml to config/locales/no.yml
NO CONTENT: file renamed from lang/no.yml to config/locales/no.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/pl.yml to config/locales/pl.yml
NO CONTENT: file renamed from lang/pl.yml to config/locales/pl.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/pt-br.yml to config/locales/pt-BR.yml
NO CONTENT: file renamed from lang/pt-br.yml to config/locales/pt-BR.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/pt.yml to config/locales/pt.yml
NO CONTENT: file renamed from lang/pt.yml to config/locales/pt.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/ro.yml to config/locales/ro.yml
NO CONTENT: file renamed from lang/ro.yml to config/locales/ro.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/ru.yml to config/locales/ru.yml
NO CONTENT: file renamed from lang/ru.yml to config/locales/ru.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/sk.yml to config/locales/sk.yml
NO CONTENT: file renamed from lang/sk.yml to config/locales/sk.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/sl.yml to config/locales/sl.yml
NO CONTENT: file renamed from lang/sl.yml to config/locales/sl.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/sr.yml to config/locales/sr.yml
NO CONTENT: file renamed from lang/sr.yml to config/locales/sr.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/th.yml to config/locales/th.yml
NO CONTENT: file renamed from lang/th.yml to config/locales/th.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/tr.yml to config/locales/tr.yml
NO CONTENT: file renamed from lang/tr.yml to config/locales/tr.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/zh-tw.yml to config/locales/zh-TW.yml
NO CONTENT: file renamed from lang/zh-tw.yml to config/locales/zh-TW.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from lang/zh.yml to config/locales/zh.yml
NO CONTENT: file renamed from lang/zh.yml to config/locales/zh.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from extra/sample_plugin/lang/en.yml to extra/sample_plugin/config/locales/en.yml
NO CONTENT: file renamed from extra/sample_plugin/lang/en.yml to extra/sample_plugin/config/locales/en.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from extra/sample_plugin/lang/fr.yml to extra/sample_plugin/config/locales/fr.yml
NO CONTENT: file renamed from extra/sample_plugin/lang/fr.yml to extra/sample_plugin/config/locales/fr.yml
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from public/javascripts/calendar/lang/calendar-vn.js to public/javascripts/calendar/lang/calendar-vi.js
NO CONTENT: file renamed from public/javascripts/calendar/lang/calendar-vn.js to public/javascripts/calendar/lang/calendar-vi.js
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from public/javascripts/jstoolbar/lang/jstoolbar-vn.js to public/javascripts/jstoolbar/lang/jstoolbar-vi.js
NO CONTENT: file renamed from public/javascripts/jstoolbar/lang/jstoolbar-vn.js to public/javascripts/jstoolbar/lang/jstoolbar-vi.js
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now