##// END OF EJS Templates
Merged rails-3.2 branch....
Jean-Philippe Lang -
r9346:5e57a1a9d947
parent child
Show More

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

@@ -0,0 +1,4
1 # This file is used by Rack-based servers to start the application.
2
3 require ::File.expand_path('../config/environment', __FILE__)
4 run RedmineApp::Application
@@ -0,0 +1,56
1 require File.expand_path('../boot', __FILE__)
2
3 require 'rails/all'
4
5 if defined?(Bundler)
6 # If you precompile assets before deploying to production, use this line
7 Bundler.require(*Rails.groups(:assets => %w(development test)))
8 # If you want your assets lazily compiled in production, use this line
9 # Bundler.require(:default, :assets, Rails.env)
10 end
11
12 module RedmineApp
13 class Application < Rails::Application
14 # Settings in config/environments/* take precedence over those specified here.
15 # Application configuration should go into files in config/initializers
16 # -- all .rb files in that directory are automatically loaded.
17
18 # Custom directories with classes and modules you want to be autoloadable.
19 config.autoload_paths += %W(#{config.root}/lib)
20
21 # Only load the plugins named here, in the order given (default is alphabetical).
22 # :all can be used as a placeholder for all plugins not explicitly named.
23 # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24
25 # Activate observers that should always be running.
26 config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
27
28 config.active_record.store_full_sti_class = true
29
30 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
31 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
32 # config.time_zone = 'Central Time (US & Canada)'
33
34 # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
35 # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
36 # config.i18n.default_locale = :de
37
38 # Configure the default encoding used in templates for Ruby 1.9.
39 config.encoding = "utf-8"
40
41 # Configure sensitive parameters which will be filtered from the log file.
42 config.filter_parameters += [:password]
43
44 # Enable the asset pipeline
45 config.assets.enabled = false
46
47 # Version of your assets, change this if you want to expire all your assets
48 config.assets.version = '1.0'
49
50 config.action_mailer.perform_deliveries = false
51
52 if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
53 instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
54 end
55 end
56 end
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,31 +1,32
1 1 /.project
2 2 /.loadpath
3 3 /config/additional_environment.rb
4 4 /config/configuration.yml
5 5 /config/database.yml
6 6 /config/email.yml
7 7 /config/initializers/session_store.rb
8 /config/initializers/secret_token.rb
8 9 /coverage
9 10 /db/*.db
10 11 /db/*.sqlite3
11 12 /db/schema.rb
12 13 /files/*
13 14 /lib/redmine/scm/adapters/mercurial/redminehelper.pyc
14 15 /lib/redmine/scm/adapters/mercurial/redminehelper.pyo
15 16 /log/*.log*
16 17 /log/mongrel_debug
17 18 /public/dispatch.*
18 19 /public/plugin_assets
19 20 /tmp/*
20 21 /tmp/cache/*
21 22 /tmp/sessions/*
22 23 /tmp/sockets/*
23 24 /tmp/test/*
24 25 /vendor/cache
25 26 /vendor/rails
26 27 *.rbc
27 28
28 29 /.bundle
29 30 /Gemfile.lock
30 31 /Gemfile.local
31 32
@@ -1,36 +1,37
1 1 syntax: glob
2 2
3 3 .project
4 4 .loadpath
5 5 config/additional_environment.rb
6 6 config/configuration.yml
7 7 config/database.yml
8 8 config/email.yml
9 9 config/initializers/session_store.rb
10 config/initializers/secret_token.rb
10 11 coverage
11 12 db/*.db
12 13 db/*.sqlite3
13 14 db/schema.rb
14 15 files/*
15 16 lib/redmine/scm/adapters/mercurial/redminehelper.pyc
16 17 lib/redmine/scm/adapters/mercurial/redminehelper.pyo
17 18 log/*.log*
18 19 log/mongrel_debug
19 20 public/dispatch.*
20 21 public/plugin_assets
21 22 tmp/*
22 23 tmp/cache/*
23 24 tmp/sessions/*
24 25 tmp/sockets/*
25 26 tmp/test/*
26 27 vendor/cache
27 28 vendor/rails
28 29 *.rbc
29 30
30 31 .svn/
31 32 .git/
32 33
33 34 .bundle
34 35 Gemfile.lock
35 36 Gemfile.local
36 37
@@ -1,87 +1,91
1 source :rubygems
1 source 'http://rubygems.org'
2 2
3 gem "rails", "2.3.14"
4 gem "i18n", "~> 0.4.2"
3 gem 'rails', '3.2.3'
4 gem 'prototype-rails', '3.2.1'
5 gem "i18n", "~> 0.6.0"
5 6 gem "coderay", "~> 1.0.6"
6 7 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
7 8 gem "tzinfo", "~> 0.3.31"
9 gem "builder"
8 10
9 11 # Optional gem for LDAP authentication
10 12 group :ldap do
11 13 gem "net-ldap", "~> 0.3.1"
12 14 end
13 15
14 16 # Optional gem for OpenID authentication
15 17 group :openid do
16 18 gem "ruby-openid", "~> 2.1.4", :require => "openid"
19 gem "rack-openid"
17 20 end
18 21
19 22 # Optional gem for exporting the gantt to a PNG file, not supported with jruby
20 23 platforms :mri, :mingw do
21 24 group :rmagick do
22 25 # RMagick 2 supports ruby 1.9
23 26 # RMagick 1 would be fine for ruby 1.8 but Bundler does not support
24 27 # different requirements for the same gem on different platforms
25 28 gem "rmagick", ">= 2.0.0"
26 29 end
27 30 end
28 31
29 32 # Database gems
30 33 platforms :mri, :mingw do
31 34 group :postgresql do
32 35 gem "pg", ">= 0.11.0"
33 36 end
34 37
35 38 group :sqlite do
36 39 gem "sqlite3"
37 40 end
38 41 end
39 42
40 43 platforms :mri_18, :mingw_18 do
41 44 group :mysql do
42 45 gem "mysql"
43 46 end
44 47 end
45 48
46 49 platforms :mri_19, :mingw_19 do
47 50 group :mysql do
48 gem "mysql2", "~> 0.2.7"
51 gem "mysql2", "~> 0.3.11"
49 52 end
50 53 end
51 54
52 55 platforms :jruby do
53 56 gem "jruby-openssl"
54 57
55 58 group :mysql do
56 59 gem "activerecord-jdbcmysql-adapter"
57 60 end
58 61
59 62 group :postgresql do
60 63 gem "activerecord-jdbcpostgresql-adapter"
61 64 end
62 65
63 66 group :sqlite do
64 67 gem "activerecord-jdbcsqlite3-adapter"
65 68 end
66 69 end
67 70
68 71 group :development do
69 72 gem "rdoc", ">= 2.4.2"
70 73 end
71 74
75
72 76 group :test do
73 gem "shoulda", "~> 2.10.3"
77 gem "shoulda"
74 78 gem "mocha"
75 79 end
76 80
77 81 local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
78 82 if File.exists?(local_gemfile)
79 83 puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v`
80 84 instance_eval File.read(local_gemfile)
81 85 end
82 86
83 87 # Load plugins' Gemfiles
84 88 Dir.glob File.expand_path("../vendor/plugins/*/Gemfile", __FILE__) do |file|
85 89 puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
86 90 instance_eval File.read(file)
87 91 end
@@ -1,15 +1,7
1 #!/usr/bin/env rake
1 2 # Add your own tasks in files placed in lib/tasks ending in .rake,
2 # for example lib/tasks/switchtower.rake, and they will automatically be available to Rake.
3 # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 4
4 require(File.join(File.dirname(__FILE__), 'config', 'boot'))
5 require File.expand_path('../config/application', __FILE__)
5 6
6 require 'rake'
7 require 'rake/testtask'
8
9 begin
10 require 'rdoc/task'
11 rescue LoadError
12 # RDoc is not available
13 end
14
15 require 'tasks/rails'
7 RedmineApp::Application.load_tasks
@@ -1,548 +1,532
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25
26 class_attribute :accept_api_auth_actions
27 class_attribute :accept_rss_auth_actions
28 class_attribute :model_object
25 29
26 30 layout 'base'
27 exempt_from_layout 'builder', 'rsb'
28 31
29 32 protect_from_forgery
30 33 def handle_unverified_request
31 34 super
32 35 cookies.delete(:autologin)
33 36 end
34 37 # Remove broken cookie after upgrade from 0.8.x (#4292)
35 38 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
36 39 # TODO: remove it when Rails is fixed
37 40 before_filter :delete_broken_cookies
38 41 def delete_broken_cookies
39 42 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
40 43 cookies.delete '_redmine_session'
41 44 redirect_to home_path
42 45 return false
43 46 end
44 47 end
45 48
46 49 # FIXME: Remove this when all of Rack and Rails have learned how to
47 50 # properly use encodings
48 51 before_filter :params_filter
49 52
50 53 def params_filter
51 54 if RUBY_VERSION >= '1.9' && defined?(Rails) && Rails::VERSION::MAJOR < 3
52 55 self.utf8nize!(params)
53 56 end
54 57 end
55 58
56 59 def utf8nize!(obj)
57 60 if obj.frozen?
58 61 obj
59 62 elsif obj.is_a? String
60 63 obj.respond_to?(:force_encoding) ? obj.force_encoding("UTF-8") : obj
61 64 elsif obj.is_a? Hash
62 65 obj.each {|k, v| obj[k] = self.utf8nize!(v)}
63 66 elsif obj.is_a? Array
64 67 obj.each {|v| self.utf8nize!(v)}
65 68 else
66 69 obj
67 70 end
68 71 end
69 72
70 73 before_filter :user_setup, :check_if_login_required, :set_localization
71 filter_parameter_logging :password
72 74
73 75 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
74 76 rescue_from ::Unauthorized, :with => :deny_access
75 77
76 78 include Redmine::Search::Controller
77 79 include Redmine::MenuManager::MenuController
78 80 helper Redmine::MenuManager::MenuHelper
79 81
80 Redmine::Scm::Base.all.each do |scm|
81 require_dependency "repository/#{scm.underscore}"
82 end
83
84 82 def user_setup
85 83 # Check the settings cache for each request
86 84 Setting.check_cache
87 85 # Find the current user
88 86 User.current = find_current_user
89 87 end
90 88
91 89 # Returns the current user or nil if no user is logged in
92 90 # and starts a session if needed
93 91 def find_current_user
94 92 if session[:user_id]
95 93 # existing session
96 94 (User.active.find(session[:user_id]) rescue nil)
97 95 elsif cookies[:autologin] && Setting.autologin?
98 96 # auto-login feature starts a new session
99 97 user = User.try_to_autologin(cookies[:autologin])
100 98 session[:user_id] = user.id if user
101 99 user
102 100 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
103 101 # RSS key authentication does not start a session
104 102 User.find_by_rss_key(params[:key])
105 103 elsif Setting.rest_api_enabled? && accept_api_auth?
106 104 if (key = api_key_from_request)
107 105 # Use API key
108 106 User.find_by_api_key(key)
109 107 else
110 108 # HTTP Basic, either username/password or API key/random
111 109 authenticate_with_http_basic do |username, password|
112 110 User.try_to_login(username, password) || User.find_by_api_key(username)
113 111 end
114 112 end
115 113 end
116 114 end
117 115
118 116 # Sets the logged in user
119 117 def logged_user=(user)
120 118 reset_session
121 119 if user && user.is_a?(User)
122 120 User.current = user
123 121 session[:user_id] = user.id
124 122 else
125 123 User.current = User.anonymous
126 124 end
127 125 end
128 126
129 127 # Logs out current user
130 128 def logout_user
131 129 if User.current.logged?
132 130 cookies.delete :autologin
133 131 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
134 132 self.logged_user = nil
135 133 end
136 134 end
137 135
138 136 # check if login is globally required to access the application
139 137 def check_if_login_required
140 138 # no check needed if user is already logged in
141 139 return true if User.current.logged?
142 140 require_login if Setting.login_required?
143 141 end
144 142
145 143 def set_localization
146 144 lang = nil
147 145 if User.current.logged?
148 146 lang = find_language(User.current.language)
149 147 end
150 148 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
151 149 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
152 150 if !accept_lang.blank?
153 151 accept_lang = accept_lang.downcase
154 152 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
155 153 end
156 154 end
157 155 lang ||= Setting.default_language
158 156 set_language_if_valid(lang)
159 157 end
160 158
161 159 def require_login
162 160 if !User.current.logged?
163 161 # Extract only the basic url parameters on non-GET requests
164 162 if request.get?
165 163 url = url_for(params)
166 164 else
167 165 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
168 166 end
169 167 respond_to do |format|
170 168 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
171 169 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
172 170 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
173 171 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
174 172 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
175 173 end
176 174 return false
177 175 end
178 176 true
179 177 end
180 178
181 179 def require_admin
182 180 return unless require_login
183 181 if !User.current.admin?
184 182 render_403
185 183 return false
186 184 end
187 185 true
188 186 end
189 187
190 188 def deny_access
191 189 User.current.logged? ? render_403 : require_login
192 190 end
193 191
194 192 # Authorize the user for the requested action
195 193 def authorize(ctrl = params[:controller], action = params[:action], global = false)
196 194 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
197 195 if allowed
198 196 true
199 197 else
200 198 if @project && @project.archived?
201 199 render_403 :message => :notice_not_authorized_archived_project
202 200 else
203 201 deny_access
204 202 end
205 203 end
206 204 end
207 205
208 206 # Authorize the user for the requested action outside a project
209 207 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
210 208 authorize(ctrl, action, global)
211 209 end
212 210
213 211 # Find project of id params[:id]
214 212 def find_project
215 213 @project = Project.find(params[:id])
216 214 rescue ActiveRecord::RecordNotFound
217 215 render_404
218 216 end
219 217
220 218 # Find project of id params[:project_id]
221 219 def find_project_by_project_id
222 220 @project = Project.find(params[:project_id])
223 221 rescue ActiveRecord::RecordNotFound
224 222 render_404
225 223 end
226 224
227 225 # Find a project based on params[:project_id]
228 226 # TODO: some subclasses override this, see about merging their logic
229 227 def find_optional_project
230 228 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
231 229 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
232 230 allowed ? true : deny_access
233 231 rescue ActiveRecord::RecordNotFound
234 232 render_404
235 233 end
236 234
237 235 # Finds and sets @project based on @object.project
238 236 def find_project_from_association
239 237 render_404 unless @object.present?
240 238
241 239 @project = @object.project
242 240 end
243 241
244 242 def find_model_object
245 model = self.class.read_inheritable_attribute('model_object')
243 model = self.class.model_object
246 244 if model
247 245 @object = model.find(params[:id])
248 246 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
249 247 end
250 248 rescue ActiveRecord::RecordNotFound
251 249 render_404
252 250 end
253 251
254 252 def self.model_object(model)
255 write_inheritable_attribute('model_object', model)
253 self.model_object = model
256 254 end
257 255
258 256 # Filter for bulk issue operations
259 257 def find_issues
260 258 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
261 259 raise ActiveRecord::RecordNotFound if @issues.empty?
262 260 if @issues.detect {|issue| !issue.visible?}
263 261 deny_access
264 262 return
265 263 end
266 264 @projects = @issues.collect(&:project).compact.uniq
267 265 @project = @projects.first if @projects.size == 1
268 266 rescue ActiveRecord::RecordNotFound
269 267 render_404
270 268 end
271 269
272 270 # make sure that the user is a member of the project (or admin) if project is private
273 271 # used as a before_filter for actions that do not require any particular permission on the project
274 272 def check_project_privacy
275 273 if @project && @project.active?
276 274 if @project.visible?
277 275 true
278 276 else
279 277 deny_access
280 278 end
281 279 else
282 280 @project = nil
283 281 render_404
284 282 false
285 283 end
286 284 end
287 285
288 286 def back_url
289 287 params[:back_url] || request.env['HTTP_REFERER']
290 288 end
291 289
292 290 def redirect_back_or_default(default)
293 291 back_url = CGI.unescape(params[:back_url].to_s)
294 292 if !back_url.blank?
295 293 begin
296 294 uri = URI.parse(back_url)
297 295 # do not redirect user to another host or to the login or register page
298 296 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
299 297 redirect_to(back_url)
300 298 return
301 299 end
302 300 rescue URI::InvalidURIError
303 301 # redirect to default
304 302 end
305 303 end
306 304 redirect_to default
307 305 false
308 306 end
309 307
310 308 # Redirects to the request referer if present, redirects to args or call block otherwise.
311 309 def redirect_to_referer_or(*args, &block)
312 310 redirect_to :back
313 311 rescue ::ActionController::RedirectBackError
314 312 if args.any?
315 313 redirect_to *args
316 314 elsif block_given?
317 315 block.call
318 316 else
319 317 raise "#redirect_to_referer_or takes arguments or a block"
320 318 end
321 319 end
322 320
323 321 def render_403(options={})
324 322 @project = nil
325 323 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
326 324 return false
327 325 end
328 326
329 327 def render_404(options={})
330 328 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
331 329 return false
332 330 end
333 331
334 332 # Renders an error response
335 333 def render_error(arg)
336 334 arg = {:message => arg} unless arg.is_a?(Hash)
337 335
338 336 @message = arg[:message]
339 337 @message = l(@message) if @message.is_a?(Symbol)
340 338 @status = arg[:status] || 500
341 339
342 340 respond_to do |format|
343 341 format.html {
344 342 render :template => 'common/error', :layout => use_layout, :status => @status
345 343 }
346 344 format.atom { head @status }
347 345 format.xml { head @status }
348 346 format.js { head @status }
349 347 format.json { head @status }
350 348 end
351 349 end
352 350
353 351 # Filter for actions that provide an API response
354 352 # but have no HTML representation for non admin users
355 353 def require_admin_or_api_request
356 354 return true if api_request?
357 355 if User.current.admin?
358 356 true
359 357 elsif User.current.logged?
360 358 render_error(:status => 406)
361 359 else
362 360 deny_access
363 361 end
364 362 end
365 363
366 364 # Picks which layout to use based on the request
367 365 #
368 366 # @return [boolean, string] name of the layout to use or false for no layout
369 367 def use_layout
370 368 request.xhr? ? false : 'base'
371 369 end
372 370
373 371 def invalid_authenticity_token
374 372 if api_request?
375 373 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
376 374 end
377 375 render_error "Invalid form authenticity token."
378 376 end
379 377
380 378 def render_feed(items, options={})
381 379 @items = items || []
382 380 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
383 381 @items = @items.slice(0, Setting.feeds_limit.to_i)
384 382 @title = options[:title] || Setting.app_title
385 383 render :template => "common/feed.atom", :layout => false,
386 384 :content_type => 'application/atom+xml'
387 385 end
388 386
389 387 def self.accept_rss_auth(*actions)
390 388 if actions.any?
391 write_inheritable_attribute('accept_rss_auth_actions', actions)
389 self.accept_rss_auth_actions = actions
392 390 else
393 read_inheritable_attribute('accept_rss_auth_actions') || []
391 self.accept_rss_auth_actions || []
394 392 end
395 393 end
396 394
397 395 def accept_rss_auth?(action=action_name)
398 396 self.class.accept_rss_auth.include?(action.to_sym)
399 397 end
400 398
401 399 def self.accept_api_auth(*actions)
402 400 if actions.any?
403 write_inheritable_attribute('accept_api_auth_actions', actions)
401 self.accept_api_auth_actions = actions
404 402 else
405 read_inheritable_attribute('accept_api_auth_actions') || []
403 self.accept_api_auth_actions || []
406 404 end
407 405 end
408 406
409 407 def accept_api_auth?(action=action_name)
410 408 self.class.accept_api_auth.include?(action.to_sym)
411 409 end
412 410
413 411 # Returns the number of objects that should be displayed
414 412 # on the paginated list
415 413 def per_page_option
416 414 per_page = nil
417 415 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
418 416 per_page = params[:per_page].to_s.to_i
419 417 session[:per_page] = per_page
420 418 elsif session[:per_page]
421 419 per_page = session[:per_page]
422 420 else
423 421 per_page = Setting.per_page_options_array.first || 25
424 422 end
425 423 per_page
426 424 end
427 425
428 426 # Returns offset and limit used to retrieve objects
429 427 # for an API response based on offset, limit and page parameters
430 428 def api_offset_and_limit(options=params)
431 429 if options[:offset].present?
432 430 offset = options[:offset].to_i
433 431 if offset < 0
434 432 offset = 0
435 433 end
436 434 end
437 435 limit = options[:limit].to_i
438 436 if limit < 1
439 437 limit = 25
440 438 elsif limit > 100
441 439 limit = 100
442 440 end
443 441 if offset.nil? && options[:page].present?
444 442 offset = (options[:page].to_i - 1) * limit
445 443 offset = 0 if offset < 0
446 444 end
447 445 offset ||= 0
448 446
449 447 [offset, limit]
450 448 end
451 449
452 450 # qvalues http header parser
453 451 # code taken from webrick
454 452 def parse_qvalues(value)
455 453 tmp = []
456 454 if value
457 455 parts = value.split(/,\s*/)
458 456 parts.each {|part|
459 457 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
460 458 val = m[1]
461 459 q = (m[2] or 1).to_f
462 460 tmp.push([val, q])
463 461 end
464 462 }
465 463 tmp = tmp.sort_by{|val, q| -q}
466 464 tmp.collect!{|val, q| val}
467 465 end
468 466 return tmp
469 467 rescue
470 468 nil
471 469 end
472 470
473 471 # Returns a string that can be used as filename value in Content-Disposition header
474 472 def filename_for_content_disposition(name)
475 473 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
476 474 end
477 475
478 476 def api_request?
479 477 %w(xml json).include? params[:format]
480 478 end
481 479
482 480 # Returns the API key present in the request
483 481 def api_key_from_request
484 482 if params[:key].present?
485 483 params[:key]
486 484 elsif request.headers["X-Redmine-API-Key"].present?
487 485 request.headers["X-Redmine-API-Key"]
488 486 end
489 487 end
490 488
491 489 # Renders a warning flash if obj has unsaved attachments
492 490 def render_attachment_warning_if_needed(obj)
493 491 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
494 492 end
495 493
496 494 # Sets the `flash` notice or error based the number of issues that did not save
497 495 #
498 496 # @param [Array, Issue] issues all of the saved and unsaved Issues
499 497 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
500 498 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
501 499 if unsaved_issue_ids.empty?
502 500 flash[:notice] = l(:notice_successful_update) unless issues.empty?
503 501 else
504 502 flash[:error] = l(:notice_failed_to_save_issues,
505 503 :count => unsaved_issue_ids.size,
506 504 :total => issues.size,
507 505 :ids => '#' + unsaved_issue_ids.join(', #'))
508 506 end
509 507 end
510 508
511 509 # Rescues an invalid query statement. Just in case...
512 510 def query_statement_invalid(exception)
513 511 logger.error "Query::StatementInvalid: #{exception.message}" if logger
514 512 session.delete(:query)
515 513 sort_clear if respond_to?(:sort_clear)
516 514 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
517 515 end
518 516
519 517 # Renders API response on validation failure
520 518 def render_validation_errors(objects)
521 519 if objects.is_a?(Array)
522 520 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
523 521 else
524 522 @error_messages = objects.errors.full_messages
525 523 end
526 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => false
527 end
528
529 # Overrides #default_template so that the api template
530 # is used automatically if it exists
531 def default_template(action_name = self.action_name)
532 if api_request?
533 begin
534 return self.view_paths.find_template(default_template_name(action_name), 'api')
535 rescue ::ActionView::MissingTemplate
536 # the api template was not found
537 # fallback to the default behaviour
538 end
539 end
540 super
524 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
541 525 end
542 526
543 # Overrides #pick_layout so that #render with no arguments
527 # Overrides #_include_layout? so that #render with no arguments
544 528 # doesn't use the layout for api requests
545 def pick_layout(*args)
546 api_request? ? nil : super
529 def _include_layout?(*args)
530 api_request? ? false : super
547 531 end
548 532 end
@@ -1,143 +1,144
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MessagesController < ApplicationController
19 19 menu_item :boards
20 20 default_search_scope :messages
21 21 before_filter :find_board, :only => [:new, :preview]
22 22 before_filter :find_message, :except => [:new, :preview]
23 23 before_filter :authorize, :except => [:preview, :edit, :destroy]
24 24
25 25 helper :watchers
26 26 helper :attachments
27 27 include AttachmentsHelper
28 28
29 29 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
30 30
31 31 # Show a topic and its replies
32 32 def show
33 33 page = params[:page]
34 34 # Find the page of the requested reply
35 35 if params[:r] && page.nil?
36 36 offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
37 37 page = 1 + offset / REPLIES_PER_PAGE
38 38 end
39 39
40 40 @reply_count = @topic.children.count
41 41 @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
42 42 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
43 43 :order => "#{Message.table_name}.created_on ASC",
44 44 :limit => @reply_pages.items_per_page,
45 45 :offset => @reply_pages.current.offset)
46 46
47 47 @reply = Message.new(:subject => "RE: #{@message.subject}")
48 48 render :action => "show", :layout => false if request.xhr?
49 49 end
50 50
51 51 # Create a new topic
52 52 def new
53 53 @message = Message.new
54 54 @message.author = User.current
55 55 @message.board = @board
56 56 @message.safe_attributes = params[:message]
57 57 if request.post?
58 58 @message.save_attachments(params[:attachments])
59 59 if @message.save
60 60 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
61 61 render_attachment_warning_if_needed(@message)
62 62 redirect_to :action => 'show', :id => @message
63 63 end
64 64 end
65 65 end
66 66
67 67 # Reply to a topic
68 68 def reply
69 69 @reply = Message.new
70 70 @reply.author = User.current
71 71 @reply.board = @board
72 72 @reply.safe_attributes = params[:reply]
73 73 @topic.children << @reply
74 74 if !@reply.new_record?
75 75 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
76 76 attachments = Attachment.attach_files(@reply, params[:attachments])
77 77 render_attachment_warning_if_needed(@reply)
78 78 end
79 79 redirect_to :action => 'show', :id => @topic, :r => @reply
80 80 end
81 81
82 82 # Edit a message
83 83 def edit
84 84 (render_403; return false) unless @message.editable_by?(User.current)
85 85 @message.safe_attributes = params[:message]
86 86 if request.post? && @message.save
87 87 attachments = Attachment.attach_files(@message, params[:attachments])
88 88 render_attachment_warning_if_needed(@message)
89 89 flash[:notice] = l(:notice_successful_update)
90 90 @message.reload
91 91 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
92 92 end
93 93 end
94 94
95 95 # Delete a messages
96 96 def destroy
97 97 (render_403; return false) unless @message.destroyable_by?(User.current)
98 r = @message.to_param
98 99 @message.destroy
99 100 redirect_to @message.parent.nil? ?
100 101 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
101 { :action => 'show', :id => @message.parent, :r => @message }
102 { :action => 'show', :id => @message.parent, :r => r }
102 103 end
103 104
104 105 def quote
105 106 user = @message.author
106 107 text = @message.content
107 108 subject = @message.subject.gsub('"', '\"')
108 109 subject = "RE: #{subject}" unless subject.starts_with?('RE:')
109 110 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
110 111 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
111 112 render(:update) { |page|
112 113 page << "$('message_subject').value = \"#{subject}\";"
113 114 page.<< "$('message_content').value = \"#{content}\";"
114 115 page.show 'reply'
115 116 page << "Form.Element.focus('message_content');"
116 117 page << "Element.scrollTo('reply');"
117 118 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
118 119 }
119 120 end
120 121
121 122 def preview
122 123 message = @board.messages.find_by_id(params[:id])
123 124 @attachements = message.attachments if message
124 125 @text = (params[:message] || params[:reply])[:content]
125 126 render :partial => 'common/preview'
126 127 end
127 128
128 129 private
129 130 def find_message
130 131 find_board
131 132 @message = @board.messages.find(params[:id], :include => :parent)
132 133 @topic = @message.root
133 134 rescue ActiveRecord::RecordNotFound
134 135 render_404
135 136 end
136 137
137 138 def find_board
138 139 @board = Board.find(params[:board_id], :include => :project)
139 140 @project = @board.project
140 141 rescue ActiveRecord::RecordNotFound
141 142 render_404
142 143 end
143 144 end
@@ -1,426 +1,426
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 require 'redmine/scm/adapters/abstract_adapter'
21 22
22 23 class ChangesetNotFound < Exception; end
23 24 class InvalidRevisionParam < Exception; end
24 25
25 26 class RepositoriesController < ApplicationController
26 27 menu_item :repository
27 28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
28 29 default_search_scope :changesets
29 30
30 31 before_filter :find_project_by_project_id, :only => [:new, :create]
31 32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
32 33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
33 34 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
34 35 before_filter :authorize
35 36 accept_rss_auth :revisions
36 37
37 38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
38 39
39 40 def new
40 41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
41 42 @repository = Repository.factory(scm)
42 43 @repository.is_default = @project.repository.nil?
43 44 @repository.project = @project
44 45 render :layout => !request.xhr?
45 46 end
46 47
47 48 def create
48 49 @repository = Repository.factory(params[:repository_scm], params[:repository])
49 50 @repository.project = @project
50 51 if request.post? && @repository.save
51 52 redirect_to settings_project_path(@project, :tab => 'repositories')
52 53 else
53 54 render :action => 'new'
54 55 end
55 56 end
56 57
57 58 def edit
58 59 end
59 60
60 61 def update
61 62 @repository.attributes = params[:repository]
62 63 @repository.project = @project
63 64 if request.put? && @repository.save
64 65 redirect_to settings_project_path(@project, :tab => 'repositories')
65 66 else
66 67 render :action => 'edit'
67 68 end
68 69 end
69 70
70 71 def committers
71 72 @committers = @repository.committers
72 73 @users = @project.users
73 74 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
74 75 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
75 76 @users.compact!
76 77 @users.sort!
77 78 if request.post? && params[:committers].is_a?(Hash)
78 79 # Build a hash with repository usernames as keys and corresponding user ids as values
79 80 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
80 81 flash[:notice] = l(:notice_successful_update)
81 82 redirect_to settings_project_path(@project, :tab => 'repositories')
82 83 end
83 84 end
84 85
85 86 def destroy
86 87 @repository.destroy if request.delete?
87 88 redirect_to settings_project_path(@project, :tab => 'repositories')
88 89 end
89 90
90 91 def show
91 92 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
92 93
93 94 @entries = @repository.entries(@path, @rev)
94 95 @changeset = @repository.find_changeset_by_name(@rev)
95 96 if request.xhr?
96 97 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
97 98 else
98 99 (show_error_not_found; return) unless @entries
99 100 @changesets = @repository.latest_changesets(@path, @rev)
100 101 @properties = @repository.properties(@path, @rev)
101 102 @repositories = @project.repositories
102 103 render :action => 'show'
103 104 end
104 105 end
105 106
106 107 alias_method :browse, :show
107 108
108 109 def changes
109 110 @entry = @repository.entry(@path, @rev)
110 111 (show_error_not_found; return) unless @entry
111 112 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
112 113 @properties = @repository.properties(@path, @rev)
113 114 @changeset = @repository.find_changeset_by_name(@rev)
114 115 end
115 116
116 117 def revisions
117 118 @changeset_count = @repository.changesets.count
118 119 @changeset_pages = Paginator.new self, @changeset_count,
119 120 per_page_option,
120 121 params['page']
121 122 @changesets = @repository.changesets.find(:all,
122 123 :limit => @changeset_pages.items_per_page,
123 124 :offset => @changeset_pages.current.offset,
124 125 :include => [:user, :repository, :parents])
125 126
126 127 respond_to do |format|
127 128 format.html { render :layout => false if request.xhr? }
128 129 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
129 130 end
130 131 end
131 132
132 133 def entry
133 134 @entry = @repository.entry(@path, @rev)
134 135 (show_error_not_found; return) unless @entry
135 136
136 137 # If the entry is a dir, show the browser
137 138 (show; return) if @entry.is_dir?
138 139
139 140 @content = @repository.cat(@path, @rev)
140 141 (show_error_not_found; return) unless @content
141 142 if 'raw' == params[:format] ||
142 143 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
143 144 ! is_entry_text_data?(@content, @path)
144 145 # Force the download
145 146 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
146 147 send_type = Redmine::MimeType.of(@path)
147 148 send_opt[:type] = send_type.to_s if send_type
148 149 send_data @content, send_opt
149 150 else
150 151 # Prevent empty lines when displaying a file with Windows style eol
151 152 # TODO: UTF-16
152 153 # Is this needs? AttachmentsController reads file simply.
153 154 @content.gsub!("\r\n", "\n")
154 155 @changeset = @repository.find_changeset_by_name(@rev)
155 156 end
156 157 end
157 158
158 159 def is_entry_text_data?(ent, path)
159 160 # UTF-16 contains "\x00".
160 161 # It is very strict that file contains less than 30% of ascii symbols
161 162 # in non Western Europe.
162 163 return true if Redmine::MimeType.is_type?('text', path)
163 164 # Ruby 1.8.6 has a bug of integer divisions.
164 165 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
165 166 return false if ent.is_binary_data?
166 167 true
167 168 end
168 169 private :is_entry_text_data?
169 170
170 171 def annotate
171 172 @entry = @repository.entry(@path, @rev)
172 173 (show_error_not_found; return) unless @entry
173 174
174 175 @annotate = @repository.scm.annotate(@path, @rev)
175 176 if @annotate.nil? || @annotate.empty?
176 177 (render_error l(:error_scm_annotate); return)
177 178 end
178 179 ann_buf_size = 0
179 180 @annotate.lines.each do |buf|
180 181 ann_buf_size += buf.size
181 182 end
182 183 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
183 184 (render_error l(:error_scm_annotate_big_text_file); return)
184 185 end
185 186 @changeset = @repository.find_changeset_by_name(@rev)
186 187 end
187 188
188 189 def revision
189 190 respond_to do |format|
190 191 format.html
191 192 format.js {render :layout => false}
192 193 end
193 194 end
194 195
195 196 # Adds a related issue to a changeset
196 197 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
197 198 def add_related_issue
198 199 @issue = @changeset.find_referenced_issue_by_id(params[:issue_id])
199 200 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
200 201 @issue = nil
201 202 end
202 203
203 204 if @issue
204 205 @changeset.issues << @issue
205 206 respond_to do |format|
206 207 format.js {
207 208 render :update do |page|
208 209 page.replace_html "related-issues", :partial => "related_issues"
209 210 page.visual_effect :highlight, "related-issue-#{@issue.id}"
210 211 end
211 212 }
212 213 end
213 214 else
214 215 respond_to do |format|
215 216 format.js {
216 217 render :update do |page|
217 218 page.alert(l(:label_issue) + ' ' + l('activerecord.errors.messages.invalid'))
218 219 end
219 220 }
220 221 end
221 222 end
222 223 end
223 224
224 225 # Removes a related issue from a changeset
225 226 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
226 227 def remove_related_issue
227 228 @issue = Issue.visible.find_by_id(params[:issue_id])
228 229 if @issue
229 230 @changeset.issues.delete(@issue)
230 231 end
231 232
232 233 respond_to do |format|
233 234 format.js {
234 235 render :update do |page|
235 236 page.remove "related-issue-#{@issue.id}"
236 237 end if @issue
237 238 }
238 239 end
239 240 end
240 241
241 242 def diff
242 243 if params[:format] == 'diff'
243 244 @diff = @repository.diff(@path, @rev, @rev_to)
244 245 (show_error_not_found; return) unless @diff
245 246 filename = "changeset_r#{@rev}"
246 247 filename << "_r#{@rev_to}" if @rev_to
247 248 send_data @diff.join, :filename => "#{filename}.diff",
248 249 :type => 'text/x-patch',
249 250 :disposition => 'attachment'
250 251 else
251 252 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
252 253 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
253 254
254 255 # Save diff type as user preference
255 256 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
256 257 User.current.pref[:diff_type] = @diff_type
257 258 User.current.preference.save
258 259 end
259 260 @cache_key = "repositories/diff/#{@repository.id}/" +
260 261 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
261 262 unless read_fragment(@cache_key)
262 263 @diff = @repository.diff(@path, @rev, @rev_to)
263 264 show_error_not_found unless @diff
264 265 end
265 266
266 267 @changeset = @repository.find_changeset_by_name(@rev)
267 268 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
268 269 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
269 270 end
270 271 end
271 272
272 273 def stats
273 274 end
274 275
275 276 def graph
276 277 data = nil
277 278 case params[:graph]
278 279 when "commits_per_month"
279 280 data = graph_commits_per_month(@repository)
280 281 when "commits_per_author"
281 282 data = graph_commits_per_author(@repository)
282 283 end
283 284 if data
284 285 headers["Content-Type"] = "image/svg+xml"
285 286 send_data(data, :type => "image/svg+xml", :disposition => "inline")
286 287 else
287 288 render_404
288 289 end
289 290 end
290 291
291 292 private
292 293
293 294 def find_repository
294 295 @repository = Repository.find(params[:id])
295 296 @project = @repository.project
296 297 rescue ActiveRecord::RecordNotFound
297 298 render_404
298 299 end
299 300
300 301 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
301 302
302 303 def find_project_repository
303 304 @project = Project.find(params[:id])
304 305 if params[:repository_id].present?
305 306 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
306 307 else
307 308 @repository = @project.repository
308 309 end
309 310 (render_404; return false) unless @repository
310 @path = params[:path].join('/') unless params[:path].nil?
311 @path ||= ''
311 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
312 312 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
313 313 @rev_to = params[:rev_to]
314 314
315 315 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
316 316 if @repository.branches.blank?
317 317 raise InvalidRevisionParam
318 318 end
319 319 end
320 320 rescue ActiveRecord::RecordNotFound
321 321 render_404
322 322 rescue InvalidRevisionParam
323 323 show_error_not_found
324 324 end
325 325
326 326 def find_changeset
327 327 if @rev.present?
328 328 @changeset = @repository.find_changeset_by_name(@rev)
329 329 end
330 330 show_error_not_found unless @changeset
331 331 end
332 332
333 333 def show_error_not_found
334 334 render_error :message => l(:error_scm_not_found), :status => 404
335 335 end
336 336
337 337 # Handler for Redmine::Scm::Adapters::CommandFailed exception
338 338 def show_error_command_failed(exception)
339 339 render_error l(:error_scm_command_failed, exception.message)
340 340 end
341 341
342 342 def graph_commits_per_month(repository)
343 343 @date_to = Date.today
344 344 @date_from = @date_to << 11
345 345 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
346 commits_by_day = repository.changesets.count(
346 commits_by_day = Changeset.count(
347 347 :all, :group => :commit_date,
348 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
348 :conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
349 349 commits_by_month = [0] * 12
350 350 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
351 351
352 changes_by_day = repository.changes.count(
353 :all, :group => :commit_date,
354 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
352 changes_by_day = Change.count(
353 :all, :group => :commit_date, :include => :changeset,
354 :conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
355 355 changes_by_month = [0] * 12
356 356 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
357 357
358 358 fields = []
359 359 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
360 360
361 361 graph = SVG::Graph::Bar.new(
362 362 :height => 300,
363 363 :width => 800,
364 364 :fields => fields.reverse,
365 365 :stack => :side,
366 366 :scale_integers => true,
367 367 :step_x_labels => 2,
368 368 :show_data_values => false,
369 369 :graph_title => l(:label_commits_per_month),
370 370 :show_graph_title => true
371 371 )
372 372
373 373 graph.add_data(
374 374 :data => commits_by_month[0..11].reverse,
375 375 :title => l(:label_revision_plural)
376 376 )
377 377
378 378 graph.add_data(
379 379 :data => changes_by_month[0..11].reverse,
380 380 :title => l(:label_change_plural)
381 381 )
382 382
383 383 graph.burn
384 384 end
385 385
386 386 def graph_commits_per_author(repository)
387 commits_by_author = repository.changesets.count(:all, :group => :committer)
387 commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id])
388 388 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
389 389
390 changes_by_author = repository.changes.count(:all, :group => :committer)
390 changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id])
391 391 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
392 392
393 393 fields = commits_by_author.collect {|r| r.first}
394 394 commits_data = commits_by_author.collect {|r| r.last}
395 395 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
396 396
397 397 fields = fields + [""]*(10 - fields.length) if fields.length<10
398 398 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
399 399 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
400 400
401 401 # Remove email adress in usernames
402 402 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
403 403
404 404 graph = SVG::Graph::BarHorizontal.new(
405 405 :height => 400,
406 406 :width => 800,
407 407 :fields => fields,
408 408 :stack => :side,
409 409 :scale_integers => true,
410 410 :show_data_values => false,
411 411 :rotate_y_labels => false,
412 412 :graph_title => l(:label_commits_per_author),
413 413 :show_graph_title => true
414 414 )
415 415 graph.add_data(
416 416 :data => commits_data,
417 417 :title => l(:label_revision_plural)
418 418 )
419 419 graph.add_data(
420 420 :data => changes_data,
421 421 :title => l(:label_change_plural)
422 422 )
423 423 graph.burn
424 424 end
425 425 end
426 426
@@ -1,132 +1,132
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WatchersController < ApplicationController
19 19 before_filter :find_project
20 20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21 21 before_filter :authorize, :only => [:new, :destroy]
22 22
23 23 def watch
24 24 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
25 25 render_403
26 26 else
27 27 set_watcher(User.current, true)
28 28 end
29 29 end
30 30
31 31 def unwatch
32 32 set_watcher(User.current, false)
33 33 end
34 34
35 35 def new
36 36 respond_to do |format|
37 37 format.js do
38 38 render :update do |page|
39 39 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
40 40 page << "showModal('ajax-modal', '400px');"
41 41 page << "$('ajax-modal').addClassName('new-watcher');"
42 42 end
43 43 end
44 44 end
45 45 end
46 46
47 47 def create
48 48 if params[:watcher].is_a?(Hash) && request.post?
49 49 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
50 50 user_ids.each do |user_id|
51 51 Watcher.create(:watchable => @watched, :user_id => user_id)
52 52 end
53 53 end
54 54 respond_to do |format|
55 55 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
56 56 format.js do
57 57 render :update do |page|
58 58 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
59 59 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
60 60 end
61 61 end
62 62 end
63 63 end
64 64
65 65 def append
66 66 if params[:watcher].is_a?(Hash)
67 67 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
68 68 users = User.active.find_all_by_id(user_ids)
69 69 respond_to do |format|
70 70 format.js do
71 71 render :update do |page|
72 72 users.each do |user|
73 73 page.select("#issue_watcher_user_ids_#{user.id}").each do |item|
74 74 page.remove item
75 75 end
76 76 end
77 77 page.insert_html :bottom, 'watchers_inputs', :text => watchers_checkboxes(nil, users, true)
78 78 end
79 79 end
80 80 end
81 81 end
82 82 end
83 83
84 84 def destroy
85 85 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
86 86 respond_to do |format|
87 87 format.html { redirect_to :back }
88 88 format.js do
89 89 render :update do |page|
90 90 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
91 91 end
92 92 end
93 93 end
94 94 end
95 95
96 96 def autocomplete_for_user
97 97 @users = User.active.like(params[:q]).find(:all, :limit => 100)
98 98 if @watched
99 99 @users -= @watched.watcher_users
100 100 end
101 101 render :layout => false
102 102 end
103 103
104 104 private
105 105 def find_project
106 106 if params[:object_type] && params[:object_id]
107 107 klass = Object.const_get(params[:object_type].camelcase)
108 108 return false unless klass.respond_to?('watched_by')
109 109 @watched = klass.find(params[:object_id])
110 110 @project = @watched.project
111 111 elsif params[:project_id]
112 @project = Project.visible.find(params[:project_id])
112 @project = Project.visible.find_by_param(params[:project_id])
113 113 end
114 114 rescue
115 115 render_404
116 116 end
117 117
118 118 def set_watcher(user, watching)
119 119 @watched.set_watcher(user, watching)
120 120 respond_to do |format|
121 121 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
122 122 format.js do
123 123 render(:update) do |page|
124 124 c = watcher_css(@watched)
125 125 page.select(".#{c}").each do |item|
126 126 page.replace_html item, watcher_link(@watched, user)
127 127 end
128 128 end
129 129 end
130 130 end
131 131 end
132 132 end
@@ -1,316 +1,318
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'diff'
19 19
20 20 # The WikiController follows the Rails REST controller pattern but with
21 21 # a few differences
22 22 #
23 23 # * index - shows a list of WikiPages grouped by page or date
24 24 # * new - not used
25 25 # * create - not used
26 26 # * show - will also show the form for creating a new wiki page
27 27 # * edit - used to edit an existing or new page
28 28 # * update - used to save a wiki page update to the database, including new pages
29 29 # * destroy - normal
30 30 #
31 31 # Other member and collection methods are also used
32 32 #
33 33 # TODO: still being worked on
34 34 class WikiController < ApplicationController
35 35 default_search_scope :wiki_pages
36 36 before_filter :find_wiki, :authorize
37 37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
39 39
40 40 helper :attachments
41 41 include AttachmentsHelper
42 42 helper :watchers
43 43 include Redmine::Export::PDF
44 44
45 45 # List of pages, sorted alphabetically and by parent (hierarchy)
46 46 def index
47 47 load_pages_for_index
48 48 @pages_by_parent_id = @pages.group_by(&:parent_id)
49 49 end
50 50
51 51 # List of page, by last update
52 52 def date_index
53 53 load_pages_for_index
54 54 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
55 55 end
56 56
57 57 # display a page (in editing mode if it doesn't exist)
58 58 def show
59 59 if @page.new_record?
60 60 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
61 61 edit
62 62 render :action => 'edit'
63 63 else
64 64 render_404
65 65 end
66 66 return
67 67 end
68 68 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
69 69 # Redirects user to the current version if he's not allowed to view previous versions
70 70 redirect_to :version => nil
71 71 return
72 72 end
73 73 @content = @page.content_for_version(params[:version])
74 74 if User.current.allowed_to?(:export_wiki_pages, @project)
75 75 if params[:format] == 'pdf'
76 76 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
77 77 return
78 78 elsif params[:format] == 'html'
79 79 export = render_to_string :action => 'export', :layout => false
80 80 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
81 81 return
82 82 elsif params[:format] == 'txt'
83 83 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
84 84 return
85 85 end
86 86 end
87 87 @editable = editable?
88 88 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
89 89 @content.current_version? &&
90 90 Redmine::WikiFormatting.supports_section_edit?
91 91
92 92 render :action => 'show'
93 93 end
94 94
95 95 # edit an existing page or a new one
96 96 def edit
97 97 return render_403 unless editable?
98 98 if @page.new_record?
99 99 @page.content = WikiContent.new(:page => @page)
100 100 if params[:parent].present?
101 101 @page.parent = @page.wiki.find_page(params[:parent].to_s)
102 102 end
103 103 end
104 104
105 105 @content = @page.content_for_version(params[:version])
106 106 @content.text = initial_page_content(@page) if @content.text.blank?
107 107 # don't keep previous comment
108 108 @content.comments = nil
109 109
110 110 # To prevent StaleObjectError exception when reverting to a previous version
111 111 @content.version = @page.content.version
112 112
113 113 @text = @content.text
114 114 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
115 115 @section = params[:section].to_i
116 116 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
117 117 render_404 if @text.blank?
118 118 end
119 119 end
120 120
121 121 # Creates a new page or updates an existing one
122 122 def update
123 123 return render_403 unless editable?
124 124 @page.content = WikiContent.new(:page => @page) if @page.new_record?
125 125 @page.safe_attributes = params[:wiki_page]
126 126
127 127 @content = @page.content_for_version(params[:version])
128 128 @content.text = initial_page_content(@page) if @content.text.blank?
129 129 # don't keep previous comment
130 130 @content.comments = nil
131 131
132 132 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
133 133 attachments = Attachment.attach_files(@page, params[:attachments])
134 134 render_attachment_warning_if_needed(@page)
135 135 # don't save content if text wasn't changed
136 136 @page.save
137 137 redirect_to :action => 'show', :project_id => @project, :id => @page.title
138 138 return
139 139 end
140 140
141 141 @content.comments = params[:content][:comments]
142 142 @text = params[:content][:text]
143 143 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
144 144 @section = params[:section].to_i
145 145 @section_hash = params[:section_hash]
146 146 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
147 147 else
148 148 @content.version = params[:content][:version]
149 149 @content.text = @text
150 150 end
151 151 @content.author = User.current
152 152 @page.content = @content
153 153 if @page.save
154 154 attachments = Attachment.attach_files(@page, params[:attachments])
155 155 render_attachment_warning_if_needed(@page)
156 156 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
157 157 redirect_to :action => 'show', :project_id => @project, :id => @page.title
158 158 else
159 159 render :action => 'edit'
160 160 end
161 161
162 162 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
163 163 # Optimistic locking exception
164 164 flash.now[:error] = l(:notice_locking_conflict)
165 165 render :action => 'edit'
166 rescue ActiveRecord::RecordNotSaved
167 render :action => 'edit'
166 168 end
167 169
168 170 # rename a page
169 171 def rename
170 172 return render_403 unless editable?
171 173 @page.redirect_existing_links = true
172 174 # used to display the *original* title if some AR validation errors occur
173 175 @original_title = @page.pretty_title
174 176 if request.post? && @page.update_attributes(params[:wiki_page])
175 177 flash[:notice] = l(:notice_successful_update)
176 178 redirect_to :action => 'show', :project_id => @project, :id => @page.title
177 179 end
178 180 end
179 181
180 182 def protect
181 183 @page.update_attribute :protected, params[:protected]
182 184 redirect_to :action => 'show', :project_id => @project, :id => @page.title
183 185 end
184 186
185 187 # show page history
186 188 def history
187 189 @version_count = @page.content.versions.count
188 190 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
189 191 # don't load text
190 192 @versions = @page.content.versions.find :all,
191 193 :select => "id, author_id, comments, updated_on, version",
192 194 :order => 'version DESC',
193 195 :limit => @version_pages.items_per_page + 1,
194 196 :offset => @version_pages.current.offset
195 197
196 198 render :layout => false if request.xhr?
197 199 end
198 200
199 201 def diff
200 202 @diff = @page.diff(params[:version], params[:version_from])
201 203 render_404 unless @diff
202 204 end
203 205
204 206 def annotate
205 207 @annotate = @page.annotate(params[:version])
206 208 render_404 unless @annotate
207 209 end
208 210
209 211 # Removes a wiki page and its history
210 212 # Children can be either set as root pages, removed or reassigned to another parent page
211 213 def destroy
212 214 return render_403 unless editable?
213 215
214 216 @descendants_count = @page.descendants.size
215 217 if @descendants_count > 0
216 218 case params[:todo]
217 219 when 'nullify'
218 220 # Nothing to do
219 221 when 'destroy'
220 222 # Removes all its descendants
221 223 @page.descendants.each(&:destroy)
222 224 when 'reassign'
223 225 # Reassign children to another parent page
224 226 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
225 227 return unless reassign_to
226 228 @page.children.each do |child|
227 229 child.update_attribute(:parent, reassign_to)
228 230 end
229 231 else
230 232 @reassignable_to = @wiki.pages - @page.self_and_descendants
231 233 return
232 234 end
233 235 end
234 236 @page.destroy
235 237 redirect_to :action => 'index', :project_id => @project
236 238 end
237 239
238 240 # Export wiki to a single pdf or html file
239 241 def export
240 242 @pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
241 243 respond_to do |format|
242 244 format.html {
243 245 export = render_to_string :action => 'export_multiple', :layout => false
244 246 send_data(export, :type => 'text/html', :filename => "wiki.html")
245 247 }
246 248 format.pdf {
247 249 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
248 250 }
249 251 end
250 252 end
251 253
252 254 def preview
253 255 page = @wiki.find_page(params[:id])
254 256 # page is nil when previewing a new page
255 257 return render_403 unless page.nil? || editable?(page)
256 258 if page
257 259 @attachements = page.attachments
258 260 @previewed = page.content
259 261 end
260 262 @text = params[:content][:text]
261 263 render :partial => 'common/preview'
262 264 end
263 265
264 266 def add_attachment
265 267 return render_403 unless editable?
266 268 attachments = Attachment.attach_files(@page, params[:attachments])
267 269 render_attachment_warning_if_needed(@page)
268 270 redirect_to :action => 'show', :id => @page.title, :project_id => @project
269 271 end
270 272
271 273 private
272 274
273 275 def find_wiki
274 276 @project = Project.find(params[:project_id])
275 277 @wiki = @project.wiki
276 278 render_404 unless @wiki
277 279 rescue ActiveRecord::RecordNotFound
278 280 render_404
279 281 end
280 282
281 283 # Finds the requested page or a new page if it doesn't exist
282 284 def find_existing_or_new_page
283 285 @page = @wiki.find_or_new_page(params[:id])
284 286 if @wiki.page_found_with_redirect?
285 287 redirect_to params.update(:id => @page.title)
286 288 end
287 289 end
288 290
289 291 # Finds the requested page and returns a 404 error if it doesn't exist
290 292 def find_existing_page
291 293 @page = @wiki.find_page(params[:id])
292 294 if @page.nil?
293 295 render_404
294 296 return
295 297 end
296 298 if @wiki.page_found_with_redirect?
297 299 redirect_to params.update(:id => @page.title)
298 300 end
299 301 end
300 302
301 303 # Returns true if the current user is allowed to edit the page, otherwise false
302 304 def editable?(page = @page)
303 305 page.editable_by?(User.current)
304 306 end
305 307
306 308 # Returns the default content of a new wiki page
307 309 def initial_page_content(page)
308 310 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
309 311 extend helper unless self.instance_of?(helper)
310 312 helper.instance_method(:initial_page_content).bind(self).call(page)
311 313 end
312 314
313 315 def load_pages_for_index
314 316 @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
315 317 end
316 318 end
@@ -1,1130 +1,1133
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100 opt_only_path = {}
101 101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
102 102 options.delete(:only_path)
103 103 link_to(h(text),
104 104 {:controller => 'attachments', :action => action,
105 105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
106 106 options)
107 107 end
108 108
109 109 # Generates a link to a SCM revision
110 110 # Options:
111 111 # * :text - Link text (default to the formatted revision)
112 112 def link_to_revision(revision, repository, options={})
113 113 if repository.is_a?(Project)
114 114 repository = repository.repository
115 115 end
116 116 text = options.delete(:text) || format_revision(revision)
117 117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
118 118 link_to(
119 119 h(text),
120 120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
121 121 :title => l(:label_revision_id, format_revision(revision))
122 122 )
123 123 end
124 124
125 125 # Generates a link to a message
126 126 def link_to_message(message, options={}, html_options = nil)
127 127 link_to(
128 128 h(truncate(message.subject, :length => 60)),
129 129 { :controller => 'messages', :action => 'show',
130 130 :board_id => message.board_id,
131 131 :id => message.root,
132 132 :r => (message.parent_id && message.id),
133 133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
134 134 }.merge(options),
135 135 html_options
136 136 )
137 137 end
138 138
139 139 # Generates a link to a project if active
140 140 # Examples:
141 141 #
142 142 # link_to_project(project) # => link to the specified project overview
143 143 # link_to_project(project, :action=>'settings') # => link to project settings
144 144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
145 145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
146 146 #
147 147 def link_to_project(project, options={}, html_options = nil)
148 148 if project.active?
149 149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
150 150 link_to(h(project), url, html_options)
151 151 else
152 152 h(project)
153 153 end
154 154 end
155 155
156 156 def toggle_link(name, id, options={})
157 157 onclick = "Element.toggle('#{id}'); "
158 158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
159 159 onclick << "return false;"
160 160 link_to(name, "#", :onclick => onclick)
161 161 end
162 162
163 163 def image_to_function(name, function, html_options = {})
164 164 html_options.symbolize_keys!
165 165 tag(:input, html_options.merge({
166 166 :type => "image", :src => image_path(name),
167 167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 168 }))
169 169 end
170 170
171 171 def prompt_to_remote(name, text, param, url, html_options = {})
172 172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
173 173 link_to name, {}, html_options
174 174 end
175 175
176 176 def format_activity_title(text)
177 177 h(truncate_single_line(text, :length => 100))
178 178 end
179 179
180 180 def format_activity_day(date)
181 181 date == Date.today ? l(:label_today).titleize : format_date(date)
182 182 end
183 183
184 184 def format_activity_description(text)
185 185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
186 186 ).gsub(/[\r\n]+/, "<br />").html_safe
187 187 end
188 188
189 189 def format_version_name(version)
190 190 if version.project == @project
191 191 h(version)
192 192 else
193 193 h("#{version.project} - #{version}")
194 194 end
195 195 end
196 196
197 197 def due_date_distance_in_words(date)
198 198 if date
199 199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
200 200 end
201 201 end
202 202
203 203 def render_page_hierarchy(pages, node=nil, options={})
204 204 content = ''
205 205 if pages[node]
206 206 content << "<ul class=\"pages-hierarchy\">\n"
207 207 pages[node].each do |page|
208 208 content << "<li>"
209 209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
210 210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
211 211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
212 212 content << "</li>\n"
213 213 end
214 214 content << "</ul>\n"
215 215 end
216 216 content.html_safe
217 217 end
218 218
219 219 # Renders flash messages
220 220 def render_flash_messages
221 221 s = ''
222 222 flash.each do |k,v|
223 223 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
224 224 end
225 225 s.html_safe
226 226 end
227 227
228 228 # Renders tabs and their content
229 229 def render_tabs(tabs)
230 230 if tabs.any?
231 231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
232 232 else
233 233 content_tag 'p', l(:label_no_data), :class => "nodata"
234 234 end
235 235 end
236 236
237 237 # Renders the project quick-jump box
238 238 def render_project_jump_box
239 239 return unless User.current.logged?
240 240 projects = User.current.memberships.collect(&:project).compact.uniq
241 241 if projects.any?
242 242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
243 243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
244 244 '<option value="" disabled="disabled">---</option>'
245 245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
246 246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
247 247 end
248 248 s << '</select>'
249 249 s.html_safe
250 250 end
251 251 end
252 252
253 253 def project_tree_options_for_select(projects, options = {})
254 254 s = ''
255 255 project_tree(projects) do |project, level|
256 256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
257 257 tag_options = {:value => project.id}
258 258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
259 259 tag_options[:selected] = 'selected'
260 260 else
261 261 tag_options[:selected] = nil
262 262 end
263 263 tag_options.merge!(yield(project)) if block_given?
264 264 s << content_tag('option', name_prefix + h(project), tag_options)
265 265 end
266 266 s.html_safe
267 267 end
268 268
269 269 # Yields the given block for each project with its level in the tree
270 270 #
271 271 # Wrapper for Project#project_tree
272 272 def project_tree(projects, &block)
273 273 Project.project_tree(projects, &block)
274 274 end
275 275
276 276 def project_nested_ul(projects, &block)
277 277 s = ''
278 278 if projects.any?
279 279 ancestors = []
280 280 projects.sort_by(&:lft).each do |project|
281 281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
282 282 s << "<ul>\n"
283 283 else
284 284 ancestors.pop
285 285 s << "</li>"
286 286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
287 287 ancestors.pop
288 288 s << "</ul></li>\n"
289 289 end
290 290 end
291 291 s << "<li>"
292 292 s << yield(project).to_s
293 293 ancestors << project
294 294 end
295 295 s << ("</li></ul>\n" * ancestors.size)
296 296 end
297 297 s.html_safe
298 298 end
299 299
300 300 def principals_check_box_tags(name, principals)
301 301 s = ''
302 302 principals.sort.each do |principal|
303 303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
304 304 end
305 305 s.html_safe
306 306 end
307 307
308 308 # Returns a string for users/groups option tags
309 309 def principals_options_for_select(collection, selected=nil)
310 310 s = ''
311 311 if collection.include?(User.current)
312 312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
313 313 end
314 314 groups = ''
315 315 collection.sort.each do |element|
316 316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
317 317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
318 318 end
319 319 unless groups.empty?
320 320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
321 321 end
322 322 s.html_safe
323 323 end
324 324
325 325 # Truncates and returns the string as a single line
326 326 def truncate_single_line(string, *args)
327 327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
328 328 end
329 329
330 330 # Truncates at line break after 250 characters or options[:length]
331 331 def truncate_lines(string, options={})
332 332 length = options[:length] || 250
333 333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
334 334 "#{$1}..."
335 335 else
336 336 string
337 337 end
338 338 end
339 339
340 340 def anchor(text)
341 341 text.to_s.gsub(' ', '_')
342 342 end
343 343
344 344 def html_hours(text)
345 345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
346 346 end
347 347
348 348 def authoring(created, author, options={})
349 349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
350 350 end
351 351
352 352 def time_tag(time)
353 353 text = distance_of_time_in_words(Time.now, time)
354 354 if @project
355 355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
356 356 else
357 357 content_tag('acronym', text, :title => format_time(time))
358 358 end
359 359 end
360 360
361 361 def syntax_highlight_lines(name, content)
362 362 lines = []
363 363 syntax_highlight(name, content).each_line { |line| lines << line }
364 364 lines
365 365 end
366 366
367 367 def syntax_highlight(name, content)
368 368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
369 369 end
370 370
371 371 def to_path_param(path)
372 372 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
373 373 end
374 374
375 375 def pagination_links_full(paginator, count=nil, options={})
376 376 page_param = options.delete(:page_param) || :page
377 377 per_page_links = options.delete(:per_page_links)
378 378 url_param = params.dup
379 379
380 380 html = ''
381 381 if paginator.current.previous
382 382 # \xc2\xab(utf-8) = &#171;
383 383 html << link_to_content_update(
384 384 "\xc2\xab " + l(:label_previous),
385 385 url_param.merge(page_param => paginator.current.previous)) + ' '
386 386 end
387 387
388 388 html << (pagination_links_each(paginator, options) do |n|
389 389 link_to_content_update(n.to_s, url_param.merge(page_param => n))
390 390 end || '')
391 391
392 392 if paginator.current.next
393 393 # \xc2\xbb(utf-8) = &#187;
394 394 html << ' ' + link_to_content_update(
395 395 (l(:label_next) + " \xc2\xbb"),
396 396 url_param.merge(page_param => paginator.current.next))
397 397 end
398 398
399 399 unless count.nil?
400 400 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
401 401 if per_page_links != false && links = per_page_links(paginator.items_per_page)
402 402 html << " | #{links}"
403 403 end
404 404 end
405 405
406 406 html.html_safe
407 407 end
408 408
409 409 def per_page_links(selected=nil)
410 410 links = Setting.per_page_options_array.collect do |n|
411 411 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
412 412 end
413 413 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
414 414 end
415 415
416 416 def reorder_links(name, url, method = :post)
417 417 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
418 418 url.merge({"#{name}[move_to]" => 'highest'}),
419 419 :method => method, :title => l(:label_sort_highest)) +
420 420 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
421 421 url.merge({"#{name}[move_to]" => 'higher'}),
422 422 :method => method, :title => l(:label_sort_higher)) +
423 423 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
424 424 url.merge({"#{name}[move_to]" => 'lower'}),
425 425 :method => method, :title => l(:label_sort_lower)) +
426 426 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
427 427 url.merge({"#{name}[move_to]" => 'lowest'}),
428 428 :method => method, :title => l(:label_sort_lowest))
429 429 end
430 430
431 431 def breadcrumb(*args)
432 432 elements = args.flatten
433 433 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
434 434 end
435 435
436 436 def other_formats_links(&block)
437 437 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
438 438 yield Redmine::Views::OtherFormatsBuilder.new(self)
439 439 concat('</p>'.html_safe)
440 440 end
441 441
442 442 def page_header_title
443 443 if @project.nil? || @project.new_record?
444 444 h(Setting.app_title)
445 445 else
446 446 b = []
447 447 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
448 448 if ancestors.any?
449 449 root = ancestors.shift
450 450 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
451 451 if ancestors.size > 2
452 452 b << "\xe2\x80\xa6"
453 453 ancestors = ancestors[-2, 2]
454 454 end
455 455 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
456 456 end
457 457 b << h(@project)
458 458 b.join(" \xc2\xbb ").html_safe
459 459 end
460 460 end
461 461
462 462 def html_title(*args)
463 463 if args.empty?
464 464 title = @html_title || []
465 465 title << @project.name if @project
466 466 title << Setting.app_title unless Setting.app_title == title.last
467 467 title.select {|t| !t.blank? }.join(' - ')
468 468 else
469 469 @html_title ||= []
470 470 @html_title += args
471 471 end
472 472 end
473 473
474 474 # Returns the theme, controller name, and action as css classes for the
475 475 # HTML body.
476 476 def body_css_classes
477 477 css = []
478 478 if theme = Redmine::Themes.theme(Setting.ui_theme)
479 479 css << 'theme-' + theme.name
480 480 end
481 481
482 482 css << 'controller-' + controller_name
483 483 css << 'action-' + action_name
484 484 css.join(' ')
485 485 end
486 486
487 487 def accesskey(s)
488 488 Redmine::AccessKeys.key_for s
489 489 end
490 490
491 491 # Formats text according to system settings.
492 492 # 2 ways to call this method:
493 493 # * with a String: textilizable(text, options)
494 494 # * with an object and one of its attribute: textilizable(issue, :description, options)
495 495 def textilizable(*args)
496 496 options = args.last.is_a?(Hash) ? args.pop : {}
497 497 case args.size
498 498 when 1
499 499 obj = options[:object]
500 500 text = args.shift
501 501 when 2
502 502 obj = args.shift
503 503 attr = args.shift
504 504 text = obj.send(attr).to_s
505 505 else
506 506 raise ArgumentError, 'invalid arguments to textilizable'
507 507 end
508 508 return '' if text.blank?
509 509 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
510 510 only_path = options.delete(:only_path) == false ? false : true
511 511
512 512 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
513 513
514 514 @parsed_headings = []
515 515 @heading_anchors = {}
516 516 @current_section = 0 if options[:edit_section_links]
517 517
518 518 parse_sections(text, project, obj, attr, only_path, options)
519 519 text = parse_non_pre_blocks(text) do |text|
520 520 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
521 521 send method_name, text, project, obj, attr, only_path, options
522 522 end
523 523 end
524 524 parse_headings(text, project, obj, attr, only_path, options)
525 525
526 526 if @parsed_headings.any?
527 527 replace_toc(text, @parsed_headings)
528 528 end
529 529
530 530 text.html_safe
531 531 end
532 532
533 533 def parse_non_pre_blocks(text)
534 534 s = StringScanner.new(text)
535 535 tags = []
536 536 parsed = ''
537 537 while !s.eos?
538 538 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
539 539 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
540 540 if tags.empty?
541 541 yield text
542 542 end
543 543 parsed << text
544 544 if tag
545 545 if closing
546 546 if tags.last == tag.downcase
547 547 tags.pop
548 548 end
549 549 else
550 550 tags << tag.downcase
551 551 end
552 552 parsed << full_tag
553 553 end
554 554 end
555 555 # Close any non closing tags
556 556 while tag = tags.pop
557 557 parsed << "</#{tag}>"
558 558 end
559 559 parsed
560 560 end
561 561
562 562 def parse_inline_attachments(text, project, obj, attr, only_path, options)
563 563 # when using an image link, try to use an attachment, if possible
564 564 if options[:attachments] || (obj && obj.respond_to?(:attachments))
565 565 attachments = options[:attachments] || obj.attachments
566 566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
567 567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
568 568 # search for the picture in attachments
569 569 if found = Attachment.latest_attach(attachments, filename)
570 570 image_url = url_for :only_path => only_path, :controller => 'attachments',
571 571 :action => 'download', :id => found
572 572 desc = found.description.to_s.gsub('"', '')
573 573 if !desc.blank? && alttext.blank?
574 574 alt = " title=\"#{desc}\" alt=\"#{desc}\""
575 575 end
576 576 "src=\"#{image_url}\"#{alt}"
577 577 else
578 578 m
579 579 end
580 580 end
581 581 end
582 582 end
583 583
584 584 # Wiki links
585 585 #
586 586 # Examples:
587 587 # [[mypage]]
588 588 # [[mypage|mytext]]
589 589 # wiki links can refer other project wikis, using project name or identifier:
590 590 # [[project:]] -> wiki starting page
591 591 # [[project:|mytext]]
592 592 # [[project:mypage]]
593 593 # [[project:mypage|mytext]]
594 594 def parse_wiki_links(text, project, obj, attr, only_path, options)
595 595 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
596 596 link_project = project
597 597 esc, all, page, title = $1, $2, $3, $5
598 598 if esc.nil?
599 599 if page =~ /^([^\:]+)\:(.*)$/
600 600 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
601 601 page = $2
602 602 title ||= $1 if page.blank?
603 603 end
604 604
605 605 if link_project && link_project.wiki
606 606 # extract anchor
607 607 anchor = nil
608 608 if page =~ /^(.+?)\#(.+)$/
609 609 page, anchor = $1, $2
610 610 end
611 611 anchor = sanitize_anchor_name(anchor) if anchor.present?
612 612 # check if page exists
613 613 wiki_page = link_project.wiki.find_page(page)
614 614 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
615 615 "##{anchor}"
616 616 else
617 617 case options[:wiki_links]
618 618 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
619 619 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
620 620 else
621 621 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
622 622 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
623 623 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
624 624 :id => wiki_page_id, :anchor => anchor, :parent => parent)
625 625 end
626 626 end
627 627 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
628 628 else
629 629 # project or wiki doesn't exist
630 630 all
631 631 end
632 632 else
633 633 all
634 634 end
635 635 end
636 636 end
637 637
638 638 # Redmine links
639 639 #
640 640 # Examples:
641 641 # Issues:
642 642 # #52 -> Link to issue #52
643 643 # Changesets:
644 644 # r52 -> Link to revision 52
645 645 # commit:a85130f -> Link to scmid starting with a85130f
646 646 # Documents:
647 647 # document#17 -> Link to document with id 17
648 648 # document:Greetings -> Link to the document with title "Greetings"
649 649 # document:"Some document" -> Link to the document with title "Some document"
650 650 # Versions:
651 651 # version#3 -> Link to version with id 3
652 652 # version:1.0.0 -> Link to version named "1.0.0"
653 653 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
654 654 # Attachments:
655 655 # attachment:file.zip -> Link to the attachment of the current object named file.zip
656 656 # Source files:
657 657 # source:some/file -> Link to the file located at /some/file in the project's repository
658 658 # source:some/file@52 -> Link to the file's revision 52
659 659 # source:some/file#L120 -> Link to line 120 of the file
660 660 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
661 661 # export:some/file -> Force the download of the file
662 662 # Forum messages:
663 663 # message#1218 -> Link to message with id 1218
664 664 #
665 665 # Links can refer other objects from other projects, using project identifier:
666 666 # identifier:r52
667 667 # identifier:document:"Some document"
668 668 # identifier:version:1.0.0
669 669 # identifier:source:some/file
670 670 def parse_redmine_links(text, project, obj, attr, only_path, options)
671 671 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
672 672 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
673 673 link = nil
674 674 if project_identifier
675 675 project = Project.visible.find_by_identifier(project_identifier)
676 676 end
677 677 if esc.nil?
678 678 if prefix.nil? && sep == 'r'
679 679 if project
680 680 repository = nil
681 681 if repo_identifier
682 682 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
683 683 else
684 684 repository = project.repository
685 685 end
686 686 # project.changesets.visible raises an SQL error because of a double join on repositories
687 687 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
688 688 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
689 689 :class => 'changeset',
690 690 :title => truncate_single_line(changeset.comments, :length => 100))
691 691 end
692 692 end
693 693 elsif sep == '#'
694 694 oid = identifier.to_i
695 695 case prefix
696 696 when nil
697 697 if issue = Issue.visible.find_by_id(oid, :include => :status)
698 698 anchor = comment_id ? "note-#{comment_id}" : nil
699 699 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
700 700 :class => issue.css_classes,
701 701 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
702 702 end
703 703 when 'document'
704 704 if document = Document.visible.find_by_id(oid)
705 705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
706 706 :class => 'document'
707 707 end
708 708 when 'version'
709 709 if version = Version.visible.find_by_id(oid)
710 710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
711 711 :class => 'version'
712 712 end
713 713 when 'message'
714 714 if message = Message.visible.find_by_id(oid, :include => :parent)
715 715 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
716 716 end
717 717 when 'forum'
718 718 if board = Board.visible.find_by_id(oid)
719 719 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
720 720 :class => 'board'
721 721 end
722 722 when 'news'
723 723 if news = News.visible.find_by_id(oid)
724 724 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
725 725 :class => 'news'
726 726 end
727 727 when 'project'
728 728 if p = Project.visible.find_by_id(oid)
729 729 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
730 730 end
731 731 end
732 732 elsif sep == ':'
733 733 # removes the double quotes if any
734 734 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
735 735 case prefix
736 736 when 'document'
737 737 if project && document = project.documents.visible.find_by_title(name)
738 738 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
739 739 :class => 'document'
740 740 end
741 741 when 'version'
742 742 if project && version = project.versions.visible.find_by_name(name)
743 743 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
744 744 :class => 'version'
745 745 end
746 746 when 'forum'
747 747 if project && board = project.boards.visible.find_by_name(name)
748 748 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
749 749 :class => 'board'
750 750 end
751 751 when 'news'
752 752 if project && news = project.news.visible.find_by_title(name)
753 753 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
754 754 :class => 'news'
755 755 end
756 756 when 'commit', 'source', 'export'
757 757 if project
758 758 repository = nil
759 759 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
760 760 repo_prefix, repo_identifier, name = $1, $2, $3
761 761 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
762 762 else
763 763 repository = project.repository
764 764 end
765 765 if prefix == 'commit'
766 766 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
767 767 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
768 768 :class => 'changeset',
769 769 :title => truncate_single_line(h(changeset.comments), :length => 100)
770 770 end
771 771 else
772 772 if repository && User.current.allowed_to?(:browse_repository, project)
773 773 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
774 774 path, rev, anchor = $1, $3, $5
775 775 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
776 776 :path => to_path_param(path),
777 777 :rev => rev,
778 778 :anchor => anchor,
779 779 :format => (prefix == 'export' ? 'raw' : nil)},
780 780 :class => (prefix == 'export' ? 'source download' : 'source')
781 781 end
782 782 end
783 783 repo_prefix = nil
784 784 end
785 785 when 'attachment'
786 786 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
787 787 if attachments && attachment = attachments.detect {|a| a.filename == name }
788 788 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
789 789 :class => 'attachment'
790 790 end
791 791 when 'project'
792 792 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
793 793 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
794 794 end
795 795 end
796 796 end
797 797 end
798 798 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
799 799 end
800 800 end
801 801
802 802 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
803 803
804 804 def parse_sections(text, project, obj, attr, only_path, options)
805 805 return unless options[:edit_section_links]
806 806 text.gsub!(HEADING_RE) do
807 807 heading = $1
808 808 @current_section += 1
809 809 if @current_section > 1
810 810 content_tag('div',
811 811 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
812 812 :class => 'contextual',
813 813 :title => l(:button_edit_section)) + heading.html_safe
814 814 else
815 815 heading
816 816 end
817 817 end
818 818 end
819 819
820 820 # Headings and TOC
821 821 # Adds ids and links to headings unless options[:headings] is set to false
822 822 def parse_headings(text, project, obj, attr, only_path, options)
823 823 return if options[:headings] == false
824 824
825 825 text.gsub!(HEADING_RE) do
826 826 level, attrs, content = $2.to_i, $3, $4
827 827 item = strip_tags(content).strip
828 828 anchor = sanitize_anchor_name(item)
829 829 # used for single-file wiki export
830 830 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
831 831 @heading_anchors[anchor] ||= 0
832 832 idx = (@heading_anchors[anchor] += 1)
833 833 if idx > 1
834 834 anchor = "#{anchor}-#{idx}"
835 835 end
836 836 @parsed_headings << [level, anchor, item]
837 837 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
838 838 end
839 839 end
840 840
841 841 MACROS_RE = /
842 842 (!)? # escaping
843 843 (
844 844 \{\{ # opening tag
845 845 ([\w]+) # macro name
846 846 (\(([^\}]*)\))? # optional arguments
847 847 \}\} # closing tag
848 848 )
849 849 /x unless const_defined?(:MACROS_RE)
850 850
851 851 # Macros substitution
852 852 def parse_macros(text, project, obj, attr, only_path, options)
853 853 text.gsub!(MACROS_RE) do
854 854 esc, all, macro = $1, $2, $3.downcase
855 855 args = ($5 || '').split(',').each(&:strip)
856 856 if esc.nil?
857 857 begin
858 858 exec_macro(macro, obj, args)
859 859 rescue => e
860 860 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
861 861 end || all
862 862 else
863 863 all
864 864 end
865 865 end
866 866 end
867 867
868 868 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
869 869
870 870 # Renders the TOC with given headings
871 871 def replace_toc(text, headings)
872 872 text.gsub!(TOC_RE) do
873 873 if headings.empty?
874 874 ''
875 875 else
876 876 div_class = 'toc'
877 877 div_class << ' right' if $1 == '>'
878 878 div_class << ' left' if $1 == '<'
879 879 out = "<ul class=\"#{div_class}\"><li>"
880 880 root = headings.map(&:first).min
881 881 current = root
882 882 started = false
883 883 headings.each do |level, anchor, item|
884 884 if level > current
885 885 out << '<ul><li>' * (level - current)
886 886 elsif level < current
887 887 out << "</li></ul>\n" * (current - level) + "</li><li>"
888 888 elsif started
889 889 out << '</li><li>'
890 890 end
891 891 out << "<a href=\"##{anchor}\">#{item}</a>"
892 892 current = level
893 893 started = true
894 894 end
895 895 out << '</li></ul>' * (current - root)
896 896 out << '</li></ul>'
897 897 end
898 898 end
899 899 end
900 900
901 901 # Same as Rails' simple_format helper without using paragraphs
902 902 def simple_format_without_paragraph(text)
903 903 text.to_s.
904 904 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
905 905 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
906 906 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
907 907 html_safe
908 908 end
909 909
910 910 def lang_options_for_select(blank=true)
911 911 (blank ? [["(auto)", ""]] : []) +
912 912 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
913 913 end
914 914
915 915 def label_tag_for(name, option_tags = nil, options = {})
916 916 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
917 917 content_tag("label", label_text)
918 918 end
919 919
920 920 def labelled_tabular_form_for(*args, &proc)
921 921 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
922 922 args << {} unless args.last.is_a?(Hash)
923 923 options = args.last
924 924 options[:html] ||= {}
925 925 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
926 926 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
927 927 form_for(*args, &proc)
928 928 end
929 929
930 930 def labelled_form_for(*args, &proc)
931 931 args << {} unless args.last.is_a?(Hash)
932 932 options = args.last
933 if args.first.is_a?(Symbol)
934 options.merge!(:as => args.shift)
935 end
933 936 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
934 937 form_for(*args, &proc)
935 938 end
936 939
937 940 def labelled_fields_for(*args, &proc)
938 941 args << {} unless args.last.is_a?(Hash)
939 942 options = args.last
940 943 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
941 944 fields_for(*args, &proc)
942 945 end
943 946
944 947 def labelled_remote_form_for(*args, &proc)
945 948 args << {} unless args.last.is_a?(Hash)
946 949 options = args.last
947 950 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
948 951 remote_form_for(*args, &proc)
949 952 end
950 953
951 954 def error_messages_for(*objects)
952 955 html = ""
953 956 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
954 957 errors = objects.map {|o| o.errors.full_messages}.flatten
955 958 if errors.any?
956 959 html << "<div id='errorExplanation'><ul>\n"
957 960 errors.each do |error|
958 961 html << "<li>#{h error}</li>\n"
959 962 end
960 963 html << "</ul></div>\n"
961 964 end
962 965 html.html_safe
963 966 end
964 967
965 968 def back_url_hidden_field_tag
966 969 back_url = params[:back_url] || request.env['HTTP_REFERER']
967 970 back_url = CGI.unescape(back_url.to_s)
968 971 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
969 972 end
970 973
971 974 def check_all_links(form_name)
972 975 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
973 976 " | ".html_safe +
974 977 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
975 978 end
976 979
977 980 def progress_bar(pcts, options={})
978 981 pcts = [pcts, pcts] unless pcts.is_a?(Array)
979 982 pcts = pcts.collect(&:round)
980 983 pcts[1] = pcts[1] - pcts[0]
981 984 pcts << (100 - pcts[1] - pcts[0])
982 985 width = options[:width] || '100px;'
983 986 legend = options[:legend] || ''
984 987 content_tag('table',
985 988 content_tag('tr',
986 989 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
987 990 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
988 991 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
989 992 ), :class => 'progress', :style => "width: #{width};").html_safe +
990 993 content_tag('p', legend, :class => 'pourcent').html_safe
991 994 end
992 995
993 996 def checked_image(checked=true)
994 997 if checked
995 998 image_tag 'toggle_check.png'
996 999 end
997 1000 end
998 1001
999 1002 def context_menu(url)
1000 1003 unless @context_menu_included
1001 1004 content_for :header_tags do
1002 1005 javascript_include_tag('context_menu') +
1003 1006 stylesheet_link_tag('context_menu')
1004 1007 end
1005 1008 if l(:direction) == 'rtl'
1006 1009 content_for :header_tags do
1007 1010 stylesheet_link_tag('context_menu_rtl')
1008 1011 end
1009 1012 end
1010 1013 @context_menu_included = true
1011 1014 end
1012 1015 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1013 1016 end
1014 1017
1015 1018 def calendar_for(field_id)
1016 1019 include_calendar_headers_tags
1017 1020 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1018 1021 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1019 1022 end
1020 1023
1021 1024 def include_calendar_headers_tags
1022 1025 unless @calendar_headers_tags_included
1023 1026 @calendar_headers_tags_included = true
1024 1027 content_for :header_tags do
1025 1028 start_of_week = case Setting.start_of_week.to_i
1026 1029 when 1
1027 1030 'Calendar._FD = 1;' # Monday
1028 1031 when 7
1029 1032 'Calendar._FD = 0;' # Sunday
1030 1033 when 6
1031 1034 'Calendar._FD = 6;' # Saturday
1032 1035 else
1033 1036 '' # use language
1034 1037 end
1035 1038
1036 1039 javascript_include_tag('calendar/calendar') +
1037 1040 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1038 1041 javascript_tag(start_of_week) +
1039 1042 javascript_include_tag('calendar/calendar-setup') +
1040 1043 stylesheet_link_tag('calendar')
1041 1044 end
1042 1045 end
1043 1046 end
1044 1047
1045 1048 def content_for(name, content = nil, &block)
1046 1049 @has_content ||= {}
1047 1050 @has_content[name] = true
1048 1051 super(name, content, &block)
1049 1052 end
1050 1053
1051 1054 def has_content?(name)
1052 1055 (@has_content && @has_content[name]) || false
1053 1056 end
1054 1057
1055 1058 def email_delivery_enabled?
1056 1059 !!ActionMailer::Base.perform_deliveries
1057 1060 end
1058 1061
1059 1062 # Returns the avatar image tag for the given +user+ if avatars are enabled
1060 1063 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1061 1064 def avatar(user, options = { })
1062 1065 if Setting.gravatar_enabled?
1063 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1066 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1064 1067 email = nil
1065 1068 if user.respond_to?(:mail)
1066 1069 email = user.mail
1067 1070 elsif user.to_s =~ %r{<(.+?)>}
1068 1071 email = $1
1069 1072 end
1070 1073 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1071 1074 else
1072 1075 ''
1073 1076 end
1074 1077 end
1075 1078
1076 1079 def sanitize_anchor_name(anchor)
1077 1080 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1078 1081 end
1079 1082
1080 1083 # Returns the javascript tags that are included in the html layout head
1081 1084 def javascript_heads
1082 tags = javascript_include_tag(:defaults)
1085 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1083 1086 unless User.current.pref.warn_on_leaving_unsaved == '0'
1084 1087 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1085 1088 end
1086 1089 tags
1087 1090 end
1088 1091
1089 1092 def favicon
1090 1093 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1091 1094 end
1092 1095
1093 1096 def robot_exclusion_tag
1094 1097 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1095 1098 end
1096 1099
1097 1100 # Returns true if arg is expected in the API response
1098 1101 def include_in_api_response?(arg)
1099 1102 unless @included_in_api_response
1100 1103 param = params[:include]
1101 1104 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1102 1105 @included_in_api_response.collect!(&:strip)
1103 1106 end
1104 1107 @included_in_api_response.include?(arg.to_s)
1105 1108 end
1106 1109
1107 1110 # Returns options or nil if nometa param or X-Redmine-Nometa header
1108 1111 # was set in the request
1109 1112 def api_meta(options)
1110 1113 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1111 1114 # compatibility mode for activeresource clients that raise
1112 1115 # an error when unserializing an array with attributes
1113 1116 nil
1114 1117 else
1115 1118 options
1116 1119 end
1117 1120 end
1118 1121
1119 1122 private
1120 1123
1121 1124 def wiki_helper
1122 1125 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1123 1126 extend helper
1124 1127 return self
1125 1128 end
1126 1129
1127 1130 def link_to_content_update(text, url_params = {}, html_options = {})
1128 1131 link_to(text, url_params, html_options)
1129 1132 end
1130 1133 end
@@ -1,43 +1,43
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WikiHelper
21 21
22 22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
23 23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
24 s = ''
24 s = ''.html_safe
25 25 if pages.has_key?(parent)
26 26 pages[parent].each do |page|
27 27 attrs = "value='#{page.id}'"
28 28 attrs << " selected='selected'" if selected == page
29 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
29 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
30 30
31 s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
31 s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
32 32 wiki_page_options_for_select(pages, selected, page, level + 1)
33 33 end
34 34 end
35 35 s
36 36 end
37 37
38 38 def wiki_page_breadcrumb(page)
39 39 breadcrumb(page.ancestors.reverse.collect {|parent|
40 40 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project})
41 41 })
42 42 end
43 43 end
@@ -1,231 +1,227
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomField < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 has_many :custom_values, :dependent => :delete_all
22 22 acts_as_list :scope => 'type = \'#{self.class}\''
23 23 serialize :possible_values
24 24
25 25 validates_presence_of :name, :field_format
26 26 validates_uniqueness_of :name, :scope => :type
27 27 validates_length_of :name, :maximum => 30
28 28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29 29
30 30 validate :validate_custom_field
31 31 before_validation :set_searchable
32 32
33 33 def initialize(attributes=nil, *args)
34 34 super
35 35 self.possible_values ||= []
36 36 end
37 37
38 38 def set_searchable
39 39 # make sure these fields are not searchable
40 40 self.searchable = false if %w(int float date bool).include?(field_format)
41 41 # make sure only these fields can have multiple values
42 42 self.multiple = false unless %w(list user version).include?(field_format)
43 43 true
44 44 end
45 45
46 46 def validate_custom_field
47 47 if self.field_format == "list"
48 48 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
49 49 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
50 50 end
51 51
52 52 if regexp.present?
53 53 begin
54 54 Regexp.new(regexp)
55 55 rescue
56 56 errors.add(:regexp, :invalid)
57 57 end
58 58 end
59 59
60 60 if default_value.present? && !valid_field_value?(default_value)
61 61 errors.add(:default_value, :invalid)
62 62 end
63 63 end
64 64
65 65 def possible_values_options(obj=nil)
66 66 case field_format
67 67 when 'user', 'version'
68 68 if obj.respond_to?(:project) && obj.project
69 69 case field_format
70 70 when 'user'
71 71 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
72 72 when 'version'
73 73 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
74 74 end
75 75 elsif obj.is_a?(Array)
76 76 obj.collect {|o| possible_values_options(o)}.reduce(:&)
77 77 else
78 78 []
79 79 end
80 80 when 'bool'
81 81 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
82 82 else
83 read_possible_values_utf8_encoded || []
83 possible_values || []
84 84 end
85 85 end
86 86
87 87 def possible_values(obj=nil)
88 88 case field_format
89 89 when 'user', 'version'
90 90 possible_values_options(obj).collect(&:last)
91 91 when 'bool'
92 92 ['1', '0']
93 93 else
94 read_possible_values_utf8_encoded
94 values = super()
95 if values.is_a?(Array)
96 values.each do |value|
97 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
98 end
99 end
100 values
95 101 end
96 102 end
97 103
98 104 # Makes possible_values accept a multiline string
99 105 def possible_values=(arg)
100 106 if arg.is_a?(Array)
101 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
107 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
102 108 else
103 109 self.possible_values = arg.to_s.split(/[\n\r]+/)
104 110 end
105 111 end
106 112
107 113 def cast_value(value)
108 114 casted = nil
109 115 unless value.blank?
110 116 case field_format
111 117 when 'string', 'text', 'list'
112 118 casted = value
113 119 when 'date'
114 120 casted = begin; value.to_date; rescue; nil end
115 121 when 'bool'
116 122 casted = (value == '1' ? true : false)
117 123 when 'int'
118 124 casted = value.to_i
119 125 when 'float'
120 126 casted = value.to_f
121 127 when 'user', 'version'
122 128 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
123 129 end
124 130 end
125 131 casted
126 132 end
127 133
128 134 # Returns a ORDER BY clause that can used to sort customized
129 135 # objects by their value of the custom field.
130 136 # Returns false, if the custom field can not be used for sorting.
131 137 def order_statement
132 138 return nil if multiple?
133 139 case field_format
134 140 when 'string', 'text', 'list', 'date', 'bool'
135 141 # COALESCE is here to make sure that blank and NULL values are sorted equally
136 142 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
137 143 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
138 144 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
139 145 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
140 146 when 'int', 'float'
141 147 # Make the database cast values into numeric
142 148 # Postgresql will raise an error if a value can not be casted!
143 149 # CustomValue validations should ensure that it doesn't occur
144 150 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
145 151 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
146 152 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
147 153 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
148 154 else
149 155 nil
150 156 end
151 157 end
152 158
153 159 def <=>(field)
154 160 position <=> field.position
155 161 end
156 162
157 163 def self.customized_class
158 164 self.name =~ /^(.+)CustomField$/
159 165 begin; $1.constantize; rescue nil; end
160 166 end
161 167
162 168 # to move in project_custom_field
163 169 def self.for_all
164 170 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
165 171 end
166 172
167 173 def type_name
168 174 nil
169 175 end
170 176
171 177 # Returns the error messages for the given value
172 178 # or an empty array if value is a valid value for the custom field
173 179 def validate_field_value(value)
174 180 errs = []
175 181 if value.is_a?(Array)
176 182 if !multiple?
177 183 errs << ::I18n.t('activerecord.errors.messages.invalid')
178 184 end
179 185 if is_required? && value.detect(&:present?).nil?
180 186 errs << ::I18n.t('activerecord.errors.messages.blank')
181 187 end
182 188 value.each {|v| errs += validate_field_value_format(v)}
183 189 else
184 190 if is_required? && value.blank?
185 191 errs << ::I18n.t('activerecord.errors.messages.blank')
186 192 end
187 193 errs += validate_field_value_format(value)
188 194 end
189 195 errs
190 196 end
191 197
192 198 # Returns true if value is a valid value for the custom field
193 199 def valid_field_value?(value)
194 200 validate_field_value(value).empty?
195 201 end
196 202
197 203 protected
198 204
199 205 # Returns the error message for the given value regarding its format
200 206 def validate_field_value_format(value)
201 207 errs = []
202 208 if value.present?
203 209 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
204 210 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
205 211 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
206 212
207 213 # Format specific validations
208 214 case field_format
209 215 when 'int'
210 216 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
211 217 when 'float'
212 218 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
213 219 when 'date'
214 220 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
215 221 when 'list'
216 222 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
217 223 end
218 224 end
219 225 errs
220 226 end
221
222 def read_possible_values_utf8_encoded
223 values = read_attribute(:possible_values)
224 if values.is_a?(Array)
225 values.each do |value|
226 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
227 end
228 end
229 values
230 end
231 227 end
@@ -1,1078 +1,1078
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 belongs_to :project
22 22 belongs_to :tracker
23 23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 29
30 30 has_many :journals, :as => :journalized, :dependent => :destroy
31 31 has_many :time_entries, :dependent => :delete_all
32 32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33 33
34 34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 36
37 37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 39 acts_as_customizable
40 40 acts_as_watchable
41 41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 42 :include => [:project, :journals],
43 43 # sort by id so that limited eager loading doesn't break with postgresql
44 44 :order_column => "#{table_name}.id"
45 45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48 48
49 49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 50 :author_key => :author_id
51 51
52 52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53 53
54 54 attr_reader :current_journal
55 55
56 56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57 57
58 58 validates_length_of :subject, :maximum => 255
59 59 validates_inclusion_of :done_ratio, :in => 0..100
60 60 validates_numericality_of :estimated_hours, :allow_nil => true
61 61 validate :validate_issue
62 62
63 63 named_scope :visible, lambda {|*args| { :include => :project,
64 64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 65
66 66 named_scope :open, lambda {|*args|
67 67 is_closed = args.size > 0 ? !args.first : false
68 68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 69 }
70 70
71 71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75 75
76 76 before_create :default_assign
77 77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 80 after_destroy :update_parent_attributes
81 81
82 82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 83 def self.visible_condition(user, options={})
84 84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 85 case role.issues_visibility
86 86 when 'all'
87 87 nil
88 88 when 'default'
89 89 user_ids = [user.id] + user.groups.map(&:id)
90 90 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 91 when 'own'
92 92 user_ids = [user.id] + user.groups.map(&:id)
93 93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 94 else
95 95 '1=0'
96 96 end
97 97 end
98 98 end
99 99
100 100 # Returns true if usr or current user is allowed to view the issue
101 101 def visible?(usr=nil)
102 102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 103 case role.issues_visibility
104 104 when 'all'
105 105 true
106 106 when 'default'
107 107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 108 when 'own'
109 109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 110 else
111 111 false
112 112 end
113 113 end
114 114 end
115 115
116 116 def initialize(attributes=nil, *args)
117 117 super
118 118 if new_record?
119 119 # set default values for new records only
120 120 self.status ||= IssueStatus.default
121 121 self.priority ||= IssuePriority.default
122 122 self.watcher_user_ids = []
123 123 end
124 124 end
125 125
126 126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
127 127 def available_custom_fields
128 128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
129 129 end
130 130
131 131 # Copies attributes from another issue, arg can be an id or an Issue
132 132 def copy_from(arg, options={})
133 133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
134 134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
135 135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
136 136 self.status = issue.status
137 137 self.author = User.current
138 138 unless options[:attachments] == false
139 139 self.attachments = issue.attachments.map do |attachement|
140 140 attachement.copy(:container => self)
141 141 end
142 142 end
143 143 @copied_from = issue
144 144 self
145 145 end
146 146
147 147 # Returns an unsaved copy of the issue
148 148 def copy(attributes=nil, copy_options={})
149 149 copy = self.class.new.copy_from(self, copy_options)
150 150 copy.attributes = attributes if attributes
151 151 copy
152 152 end
153 153
154 154 # Returns true if the issue is a copy
155 155 def copy?
156 156 @copied_from.present?
157 157 end
158 158
159 159 # Moves/copies an issue to a new project and tracker
160 160 # Returns the moved/copied issue on success, false on failure
161 161 def move_to_project(new_project, new_tracker=nil, options={})
162 162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
163 163
164 164 if options[:copy]
165 165 issue = self.copy
166 166 else
167 167 issue = self
168 168 end
169 169
170 170 issue.init_journal(User.current, options[:notes])
171 171
172 172 # Preserve previous behaviour
173 173 # #move_to_project doesn't change tracker automatically
174 174 issue.send :project=, new_project, true
175 175 if new_tracker
176 176 issue.tracker = new_tracker
177 177 end
178 178 # Allow bulk setting of attributes on the issue
179 179 if options[:attributes]
180 180 issue.attributes = options[:attributes]
181 181 end
182 182
183 183 issue.save ? issue : false
184 184 end
185 185
186 186 def status_id=(sid)
187 187 self.status = nil
188 188 write_attribute(:status_id, sid)
189 189 end
190 190
191 191 def priority_id=(pid)
192 192 self.priority = nil
193 193 write_attribute(:priority_id, pid)
194 194 end
195 195
196 196 def category_id=(cid)
197 197 self.category = nil
198 198 write_attribute(:category_id, cid)
199 199 end
200 200
201 201 def fixed_version_id=(vid)
202 202 self.fixed_version = nil
203 203 write_attribute(:fixed_version_id, vid)
204 204 end
205 205
206 206 def tracker_id=(tid)
207 207 self.tracker = nil
208 208 result = write_attribute(:tracker_id, tid)
209 209 @custom_field_values = nil
210 210 result
211 211 end
212 212
213 213 def project_id=(project_id)
214 214 if project_id.to_s != self.project_id.to_s
215 215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
216 216 end
217 217 end
218 218
219 219 def project=(project, keep_tracker=false)
220 220 project_was = self.project
221 221 write_attribute(:project_id, project ? project.id : nil)
222 222 association_instance_set('project', project)
223 223 if project_was && project && project_was != project
224 224 unless keep_tracker || project.trackers.include?(tracker)
225 225 self.tracker = project.trackers.first
226 226 end
227 227 # Reassign to the category with same name if any
228 228 if category
229 229 self.category = project.issue_categories.find_by_name(category.name)
230 230 end
231 231 # Keep the fixed_version if it's still valid in the new_project
232 232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
233 233 self.fixed_version = nil
234 234 end
235 235 if parent && parent.project_id != project_id
236 236 self.parent_issue_id = nil
237 237 end
238 238 @custom_field_values = nil
239 239 end
240 240 end
241 241
242 242 def description=(arg)
243 243 if arg.is_a?(String)
244 244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
245 245 end
246 246 write_attribute(:description, arg)
247 247 end
248 248
249 # Overrides attributes= so that project and tracker get assigned first
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
249 # Overrides assign_attributes so that project and tracker get assigned first
250 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
251 251 return if new_attributes.nil?
252 252 attrs = new_attributes.dup
253 253 attrs.stringify_keys!
254 254
255 255 %w(project project_id tracker tracker_id).each do |attr|
256 256 if attrs.has_key?(attr)
257 257 send "#{attr}=", attrs.delete(attr)
258 258 end
259 259 end
260 send :attributes_without_project_and_tracker_first=, attrs, *args
260 send :assign_attributes_without_project_and_tracker_first, attrs, *args
261 261 end
262 262 # Do not redefine alias chain on reload (see #4838)
263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
263 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
264 264
265 265 def estimated_hours=(h)
266 266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
267 267 end
268 268
269 269 safe_attributes 'project_id',
270 270 :if => lambda {|issue, user|
271 271 if issue.new_record?
272 272 issue.copy?
273 273 elsif user.allowed_to?(:move_issues, issue.project)
274 274 projects = Issue.allowed_target_projects_on_move(user)
275 275 projects.include?(issue.project) && projects.size > 1
276 276 end
277 277 }
278 278
279 279 safe_attributes 'tracker_id',
280 280 'status_id',
281 281 'category_id',
282 282 'assigned_to_id',
283 283 'priority_id',
284 284 'fixed_version_id',
285 285 'subject',
286 286 'description',
287 287 'start_date',
288 288 'due_date',
289 289 'done_ratio',
290 290 'estimated_hours',
291 291 'custom_field_values',
292 292 'custom_fields',
293 293 'lock_version',
294 294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
295 295
296 296 safe_attributes 'status_id',
297 297 'assigned_to_id',
298 298 'fixed_version_id',
299 299 'done_ratio',
300 300 'lock_version',
301 301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
302 302
303 303 safe_attributes 'watcher_user_ids',
304 304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
305 305
306 306 safe_attributes 'is_private',
307 307 :if => lambda {|issue, user|
308 308 user.allowed_to?(:set_issues_private, issue.project) ||
309 309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
310 310 }
311 311
312 312 safe_attributes 'parent_issue_id',
313 313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
314 314 user.allowed_to?(:manage_subtasks, issue.project)}
315 315
316 316 # Safely sets attributes
317 317 # Should be called from controllers instead of #attributes=
318 318 # attr_accessible is too rough because we still want things like
319 319 # Issue.new(:project => foo) to work
320 320 def safe_attributes=(attrs, user=User.current)
321 321 return unless attrs.is_a?(Hash)
322 322
323 323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
324 324 attrs = delete_unsafe_attributes(attrs, user)
325 325 return if attrs.empty?
326 326
327 327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
328 328 if p = attrs.delete('project_id')
329 329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
330 330 self.project_id = p
331 331 end
332 332 end
333 333
334 334 if t = attrs.delete('tracker_id')
335 335 self.tracker_id = t
336 336 end
337 337
338 338 if attrs['status_id']
339 339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
340 340 attrs.delete('status_id')
341 341 end
342 342 end
343 343
344 344 unless leaf?
345 345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
346 346 end
347 347
348 348 if attrs['parent_issue_id'].present?
349 349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
350 350 end
351 351
352 352 # mass-assignment security bypass
353 self.send :attributes=, attrs, false
353 assign_attributes attrs, :without_protection => true
354 354 end
355 355
356 356 def done_ratio
357 357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
358 358 status.default_done_ratio
359 359 else
360 360 read_attribute(:done_ratio)
361 361 end
362 362 end
363 363
364 364 def self.use_status_for_done_ratio?
365 365 Setting.issue_done_ratio == 'issue_status'
366 366 end
367 367
368 368 def self.use_field_for_done_ratio?
369 369 Setting.issue_done_ratio == 'issue_field'
370 370 end
371 371
372 372 def validate_issue
373 373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
374 374 errors.add :due_date, :not_a_date
375 375 end
376 376
377 377 if self.due_date and self.start_date and self.due_date < self.start_date
378 378 errors.add :due_date, :greater_than_start_date
379 379 end
380 380
381 381 if start_date && soonest_start && start_date < soonest_start
382 382 errors.add :start_date, :invalid
383 383 end
384 384
385 385 if fixed_version
386 386 if !assignable_versions.include?(fixed_version)
387 387 errors.add :fixed_version_id, :inclusion
388 388 elsif reopened? && fixed_version.closed?
389 389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
390 390 end
391 391 end
392 392
393 393 # Checks that the issue can not be added/moved to a disabled tracker
394 394 if project && (tracker_id_changed? || project_id_changed?)
395 395 unless project.trackers.include?(tracker)
396 396 errors.add :tracker_id, :inclusion
397 397 end
398 398 end
399 399
400 400 # Checks parent issue assignment
401 401 if @parent_issue
402 402 if @parent_issue.project_id != project_id
403 403 errors.add :parent_issue_id, :not_same_project
404 404 elsif !new_record?
405 405 # moving an existing issue
406 406 if @parent_issue.root_id != root_id
407 407 # we can always move to another tree
408 408 elsif move_possible?(@parent_issue)
409 409 # move accepted inside tree
410 410 else
411 411 errors.add :parent_issue_id, :not_a_valid_parent
412 412 end
413 413 end
414 414 end
415 415 end
416 416
417 417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
418 418 # even if the user turns off the setting later
419 419 def update_done_ratio_from_issue_status
420 420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
421 421 self.done_ratio = status.default_done_ratio
422 422 end
423 423 end
424 424
425 425 def init_journal(user, notes = "")
426 426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
427 427 if new_record?
428 428 @current_journal.notify = false
429 429 else
430 430 @attributes_before_change = attributes.dup
431 431 @custom_values_before_change = {}
432 432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
433 433 end
434 434 # Make sure updated_on is updated when adding a note.
435 435 updated_on_will_change!
436 436 @current_journal
437 437 end
438 438
439 439 # Returns the id of the last journal or nil
440 440 def last_journal_id
441 441 if new_record?
442 442 nil
443 443 else
444 444 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
445 445 end
446 446 end
447 447
448 448 # Return true if the issue is closed, otherwise false
449 449 def closed?
450 450 self.status.is_closed?
451 451 end
452 452
453 453 # Return true if the issue is being reopened
454 454 def reopened?
455 455 if !new_record? && status_id_changed?
456 456 status_was = IssueStatus.find_by_id(status_id_was)
457 457 status_new = IssueStatus.find_by_id(status_id)
458 458 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
459 459 return true
460 460 end
461 461 end
462 462 false
463 463 end
464 464
465 465 # Return true if the issue is being closed
466 466 def closing?
467 467 if !new_record? && status_id_changed?
468 468 status_was = IssueStatus.find_by_id(status_id_was)
469 469 status_new = IssueStatus.find_by_id(status_id)
470 470 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
471 471 return true
472 472 end
473 473 end
474 474 false
475 475 end
476 476
477 477 # Returns true if the issue is overdue
478 478 def overdue?
479 479 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
480 480 end
481 481
482 482 # Is the amount of work done less than it should for the due date
483 483 def behind_schedule?
484 484 return false if start_date.nil? || due_date.nil?
485 485 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
486 486 return done_date <= Date.today
487 487 end
488 488
489 489 # Does this issue have children?
490 490 def children?
491 491 !leaf?
492 492 end
493 493
494 494 # Users the issue can be assigned to
495 495 def assignable_users
496 496 users = project.assignable_users
497 497 users << author if author
498 498 users << assigned_to if assigned_to
499 499 users.uniq.sort
500 500 end
501 501
502 502 # Versions that the issue can be assigned to
503 503 def assignable_versions
504 504 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
505 505 end
506 506
507 507 # Returns true if this issue is blocked by another issue that is still open
508 508 def blocked?
509 509 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
510 510 end
511 511
512 512 # Returns an array of statuses that user is able to apply
513 513 def new_statuses_allowed_to(user=User.current, include_default=false)
514 514 if new_record? && @copied_from
515 515 [IssueStatus.default, @copied_from.status].compact.uniq.sort
516 516 else
517 517 initial_status = nil
518 518 if new_record?
519 519 initial_status = IssueStatus.default
520 520 elsif status_id_was
521 521 initial_status = IssueStatus.find_by_id(status_id_was)
522 522 end
523 523 initial_status ||= status
524 524
525 525 statuses = initial_status.find_new_statuses_allowed_to(
526 526 user.admin ? Role.all : user.roles_for_project(project),
527 527 tracker,
528 528 author == user,
529 529 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
530 530 )
531 531 statuses << initial_status unless statuses.empty?
532 532 statuses << IssueStatus.default if include_default
533 533 statuses = statuses.compact.uniq.sort
534 534 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
535 535 end
536 536 end
537 537
538 538 def assigned_to_was
539 539 if assigned_to_id_changed? && assigned_to_id_was.present?
540 540 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
541 541 end
542 542 end
543 543
544 544 # Returns the mail adresses of users that should be notified
545 545 def recipients
546 546 notified = []
547 547 # Author and assignee are always notified unless they have been
548 548 # locked or don't want to be notified
549 549 notified << author if author
550 550 if assigned_to
551 551 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
552 552 end
553 553 if assigned_to_was
554 554 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
555 555 end
556 556 notified = notified.select {|u| u.active? && u.notify_about?(self)}
557 557
558 558 notified += project.notified_users
559 559 notified.uniq!
560 560 # Remove users that can not view the issue
561 561 notified.reject! {|user| !visible?(user)}
562 562 notified.collect(&:mail)
563 563 end
564 564
565 565 # Returns the number of hours spent on this issue
566 566 def spent_hours
567 567 @spent_hours ||= time_entries.sum(:hours) || 0
568 568 end
569 569
570 570 # Returns the total number of hours spent on this issue and its descendants
571 571 #
572 572 # Example:
573 573 # spent_hours => 0.0
574 574 # spent_hours => 50.2
575 575 def total_spent_hours
576 576 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
577 577 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
578 578 end
579 579
580 580 def relations
581 581 @relations ||= (relations_from + relations_to).sort
582 582 end
583 583
584 584 # Preloads relations for a collection of issues
585 585 def self.load_relations(issues)
586 586 if issues.any?
587 587 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
588 588 issues.each do |issue|
589 589 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
590 590 end
591 591 end
592 592 end
593 593
594 594 # Preloads visible spent time for a collection of issues
595 595 def self.load_visible_spent_hours(issues, user=User.current)
596 596 if issues.any?
597 597 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
598 598 issues.each do |issue|
599 599 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
600 600 end
601 601 end
602 602 end
603 603
604 604 # Finds an issue relation given its id.
605 605 def find_relation(relation_id)
606 606 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
607 607 end
608 608
609 609 def all_dependent_issues(except=[])
610 610 except << self
611 611 dependencies = []
612 612 relations_from.each do |relation|
613 613 if relation.issue_to && !except.include?(relation.issue_to)
614 614 dependencies << relation.issue_to
615 615 dependencies += relation.issue_to.all_dependent_issues(except)
616 616 end
617 617 end
618 618 dependencies
619 619 end
620 620
621 621 # Returns an array of issues that duplicate this one
622 622 def duplicates
623 623 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
624 624 end
625 625
626 626 # Returns the due date or the target due date if any
627 627 # Used on gantt chart
628 628 def due_before
629 629 due_date || (fixed_version ? fixed_version.effective_date : nil)
630 630 end
631 631
632 632 # Returns the time scheduled for this issue.
633 633 #
634 634 # Example:
635 635 # Start Date: 2/26/09, End Date: 3/04/09
636 636 # duration => 6
637 637 def duration
638 638 (start_date && due_date) ? due_date - start_date : 0
639 639 end
640 640
641 641 def soonest_start
642 642 @soonest_start ||= (
643 643 relations_to.collect{|relation| relation.successor_soonest_start} +
644 644 ancestors.collect(&:soonest_start)
645 645 ).compact.max
646 646 end
647 647
648 648 def reschedule_after(date)
649 649 return if date.nil?
650 650 if leaf?
651 651 if start_date.nil? || start_date < date
652 652 self.start_date, self.due_date = date, date + duration
653 653 begin
654 654 save
655 655 rescue ActiveRecord::StaleObjectError
656 656 reload
657 657 self.start_date, self.due_date = date, date + duration
658 658 save
659 659 end
660 660 end
661 661 else
662 662 leaves.each do |leaf|
663 663 leaf.reschedule_after(date)
664 664 end
665 665 end
666 666 end
667 667
668 668 def <=>(issue)
669 669 if issue.nil?
670 670 -1
671 671 elsif root_id != issue.root_id
672 672 (root_id || 0) <=> (issue.root_id || 0)
673 673 else
674 674 (lft || 0) <=> (issue.lft || 0)
675 675 end
676 676 end
677 677
678 678 def to_s
679 679 "#{tracker} ##{id}: #{subject}"
680 680 end
681 681
682 682 # Returns a string of css classes that apply to the issue
683 683 def css_classes
684 684 s = "issue status-#{status.position} priority-#{priority.position}"
685 685 s << ' closed' if closed?
686 686 s << ' overdue' if overdue?
687 687 s << ' child' if child?
688 688 s << ' parent' unless leaf?
689 689 s << ' private' if is_private?
690 690 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
691 691 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
692 692 s
693 693 end
694 694
695 695 # Saves an issue and a time_entry from the parameters
696 696 def save_issue_with_child_records(params, existing_time_entry=nil)
697 697 Issue.transaction do
698 698 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
699 699 @time_entry = existing_time_entry || TimeEntry.new
700 700 @time_entry.project = project
701 701 @time_entry.issue = self
702 702 @time_entry.user = User.current
703 703 @time_entry.spent_on = User.current.today
704 704 @time_entry.attributes = params[:time_entry]
705 705 self.time_entries << @time_entry
706 706 end
707 707
708 708 # TODO: Rename hook
709 709 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
710 710 if save
711 711 # TODO: Rename hook
712 712 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
713 713 else
714 714 raise ActiveRecord::Rollback
715 715 end
716 716 end
717 717 end
718 718
719 719 # Unassigns issues from +version+ if it's no longer shared with issue's project
720 720 def self.update_versions_from_sharing_change(version)
721 721 # Update issues assigned to the version
722 722 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
723 723 end
724 724
725 725 # Unassigns issues from versions that are no longer shared
726 726 # after +project+ was moved
727 727 def self.update_versions_from_hierarchy_change(project)
728 728 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
729 729 # Update issues of the moved projects and issues assigned to a version of a moved project
730 730 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
731 731 end
732 732
733 733 def parent_issue_id=(arg)
734 734 parent_issue_id = arg.blank? ? nil : arg.to_i
735 735 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
736 736 @parent_issue.id
737 737 else
738 738 @parent_issue = nil
739 739 nil
740 740 end
741 741 end
742 742
743 743 def parent_issue_id
744 744 if instance_variable_defined? :@parent_issue
745 745 @parent_issue.nil? ? nil : @parent_issue.id
746 746 else
747 747 parent_id
748 748 end
749 749 end
750 750
751 751 # Extracted from the ReportsController.
752 752 def self.by_tracker(project)
753 753 count_and_group_by(:project => project,
754 754 :field => 'tracker_id',
755 755 :joins => Tracker.table_name)
756 756 end
757 757
758 758 def self.by_version(project)
759 759 count_and_group_by(:project => project,
760 760 :field => 'fixed_version_id',
761 761 :joins => Version.table_name)
762 762 end
763 763
764 764 def self.by_priority(project)
765 765 count_and_group_by(:project => project,
766 766 :field => 'priority_id',
767 767 :joins => IssuePriority.table_name)
768 768 end
769 769
770 770 def self.by_category(project)
771 771 count_and_group_by(:project => project,
772 772 :field => 'category_id',
773 773 :joins => IssueCategory.table_name)
774 774 end
775 775
776 776 def self.by_assigned_to(project)
777 777 count_and_group_by(:project => project,
778 778 :field => 'assigned_to_id',
779 779 :joins => User.table_name)
780 780 end
781 781
782 782 def self.by_author(project)
783 783 count_and_group_by(:project => project,
784 784 :field => 'author_id',
785 785 :joins => User.table_name)
786 786 end
787 787
788 788 def self.by_subproject(project)
789 789 ActiveRecord::Base.connection.select_all("select s.id as status_id,
790 790 s.is_closed as closed,
791 791 #{Issue.table_name}.project_id as project_id,
792 792 count(#{Issue.table_name}.id) as total
793 793 from
794 794 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
795 795 where
796 796 #{Issue.table_name}.status_id=s.id
797 797 and #{Issue.table_name}.project_id = #{Project.table_name}.id
798 798 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
799 799 and #{Issue.table_name}.project_id <> #{project.id}
800 800 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
801 801 end
802 802 # End ReportsController extraction
803 803
804 804 # Returns an array of projects that user can assign the issue to
805 805 def allowed_target_projects(user=User.current)
806 806 if new_record?
807 807 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
808 808 else
809 809 self.class.allowed_target_projects_on_move(user)
810 810 end
811 811 end
812 812
813 813 # Returns an array of projects that user can move issues to
814 814 def self.allowed_target_projects_on_move(user=User.current)
815 815 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
816 816 end
817 817
818 818 private
819 819
820 820 def after_project_change
821 821 # Update project_id on related time entries
822 822 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
823 823
824 824 # Delete issue relations
825 825 unless Setting.cross_project_issue_relations?
826 826 relations_from.clear
827 827 relations_to.clear
828 828 end
829 829
830 830 # Move subtasks
831 831 children.each do |child|
832 832 # Change project and keep project
833 833 child.send :project=, project, true
834 834 unless child.save
835 835 raise ActiveRecord::Rollback
836 836 end
837 837 end
838 838 end
839 839
840 840 def update_nested_set_attributes
841 841 if root_id.nil?
842 842 # issue was just created
843 843 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
844 844 set_default_left_and_right
845 845 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
846 846 if @parent_issue
847 847 move_to_child_of(@parent_issue)
848 848 end
849 849 reload
850 850 elsif parent_issue_id != parent_id
851 851 former_parent_id = parent_id
852 852 # moving an existing issue
853 853 if @parent_issue && @parent_issue.root_id == root_id
854 854 # inside the same tree
855 855 move_to_child_of(@parent_issue)
856 856 else
857 857 # to another tree
858 858 unless root?
859 859 move_to_right_of(root)
860 860 reload
861 861 end
862 862 old_root_id = root_id
863 863 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
864 864 target_maxright = nested_set_scope.maximum(right_column_name) || 0
865 865 offset = target_maxright + 1 - lft
866 866 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
867 867 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
868 868 self[left_column_name] = lft + offset
869 869 self[right_column_name] = rgt + offset
870 870 if @parent_issue
871 871 move_to_child_of(@parent_issue)
872 872 end
873 873 end
874 874 reload
875 875 # delete invalid relations of all descendants
876 876 self_and_descendants.each do |issue|
877 877 issue.relations.each do |relation|
878 878 relation.destroy unless relation.valid?
879 879 end
880 880 end
881 881 # update former parent
882 882 recalculate_attributes_for(former_parent_id) if former_parent_id
883 883 end
884 884 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
885 885 end
886 886
887 887 def update_parent_attributes
888 888 recalculate_attributes_for(parent_id) if parent_id
889 889 end
890 890
891 891 def recalculate_attributes_for(issue_id)
892 892 if issue_id && p = Issue.find_by_id(issue_id)
893 893 # priority = highest priority of children
894 894 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
895 895 p.priority = IssuePriority.find_by_position(priority_position)
896 896 end
897 897
898 898 # start/due dates = lowest/highest dates of children
899 899 p.start_date = p.children.minimum(:start_date)
900 900 p.due_date = p.children.maximum(:due_date)
901 901 if p.start_date && p.due_date && p.due_date < p.start_date
902 902 p.start_date, p.due_date = p.due_date, p.start_date
903 903 end
904 904
905 905 # done ratio = weighted average ratio of leaves
906 906 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
907 907 leaves_count = p.leaves.count
908 908 if leaves_count > 0
909 909 average = p.leaves.average(:estimated_hours).to_f
910 910 if average == 0
911 911 average = 1
912 912 end
913 913 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
914 914 progress = done / (average * leaves_count)
915 915 p.done_ratio = progress.round
916 916 end
917 917 end
918 918
919 919 # estimate = sum of leaves estimates
920 920 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
921 921 p.estimated_hours = nil if p.estimated_hours == 0.0
922 922
923 923 # ancestors will be recursively updated
924 p.save(false)
924 p.save(:validate => false)
925 925 end
926 926 end
927 927
928 928 # Update issues so their versions are not pointing to a
929 929 # fixed_version that is not shared with the issue's project
930 930 def self.update_versions(conditions=nil)
931 931 # Only need to update issues with a fixed_version from
932 932 # a different project and that is not systemwide shared
933 933 Issue.scoped(:conditions => conditions).all(
934 934 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
935 935 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
936 936 " AND #{Version.table_name}.sharing <> 'system'",
937 937 :include => [:project, :fixed_version]
938 938 ).each do |issue|
939 939 next if issue.project.nil? || issue.fixed_version.nil?
940 940 unless issue.project.shared_versions.include?(issue.fixed_version)
941 941 issue.init_journal(User.current)
942 942 issue.fixed_version = nil
943 943 issue.save
944 944 end
945 945 end
946 946 end
947 947
948 948 # Callback on attachment deletion
949 949 def attachment_added(obj)
950 950 if @current_journal && !obj.new_record?
951 951 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
952 952 end
953 953 end
954 954
955 955 # Callback on attachment deletion
956 956 def attachment_removed(obj)
957 957 if @current_journal && !obj.new_record?
958 958 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
959 959 @current_journal.save
960 960 end
961 961 end
962 962
963 963 # Default assignment based on category
964 964 def default_assign
965 965 if assigned_to.nil? && category && category.assigned_to
966 966 self.assigned_to = category.assigned_to
967 967 end
968 968 end
969 969
970 970 # Updates start/due dates of following issues
971 971 def reschedule_following_issues
972 972 if start_date_changed? || due_date_changed?
973 973 relations_from.each do |relation|
974 974 relation.set_issue_to_dates
975 975 end
976 976 end
977 977 end
978 978
979 979 # Closes duplicates if the issue is being closed
980 980 def close_duplicates
981 981 if closing?
982 982 duplicates.each do |duplicate|
983 983 # Reload is need in case the duplicate was updated by a previous duplicate
984 984 duplicate.reload
985 985 # Don't re-close it if it's already closed
986 986 next if duplicate.closed?
987 987 # Same user and notes
988 988 if @current_journal
989 989 duplicate.init_journal(@current_journal.user, @current_journal.notes)
990 990 end
991 991 duplicate.update_attribute :status, self.status
992 992 end
993 993 end
994 994 end
995 995
996 996 # Saves the changes in a Journal
997 997 # Called after_save
998 998 def create_journal
999 999 if @current_journal
1000 1000 # attributes changes
1001 1001 if @attributes_before_change
1002 1002 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1003 1003 before = @attributes_before_change[c]
1004 1004 after = send(c)
1005 1005 next if before == after || (before.blank? && after.blank?)
1006 1006 @current_journal.details << JournalDetail.new(:property => 'attr',
1007 1007 :prop_key => c,
1008 1008 :old_value => before,
1009 1009 :value => after)
1010 1010 }
1011 1011 end
1012 1012 if @custom_values_before_change
1013 1013 # custom fields changes
1014 1014 custom_field_values.each {|c|
1015 1015 before = @custom_values_before_change[c.custom_field_id]
1016 1016 after = c.value
1017 1017 next if before == after || (before.blank? && after.blank?)
1018 1018
1019 1019 if before.is_a?(Array) || after.is_a?(Array)
1020 1020 before = [before] unless before.is_a?(Array)
1021 1021 after = [after] unless after.is_a?(Array)
1022 1022
1023 1023 # values removed
1024 1024 (before - after).reject(&:blank?).each do |value|
1025 1025 @current_journal.details << JournalDetail.new(:property => 'cf',
1026 1026 :prop_key => c.custom_field_id,
1027 1027 :old_value => value,
1028 1028 :value => nil)
1029 1029 end
1030 1030 # values added
1031 1031 (after - before).reject(&:blank?).each do |value|
1032 1032 @current_journal.details << JournalDetail.new(:property => 'cf',
1033 1033 :prop_key => c.custom_field_id,
1034 1034 :old_value => nil,
1035 1035 :value => value)
1036 1036 end
1037 1037 else
1038 1038 @current_journal.details << JournalDetail.new(:property => 'cf',
1039 1039 :prop_key => c.custom_field_id,
1040 1040 :old_value => before,
1041 1041 :value => after)
1042 1042 end
1043 1043 }
1044 1044 end
1045 1045 @current_journal.save
1046 1046 # reset current journal
1047 1047 init_journal @current_journal.user, @current_journal.notes
1048 1048 end
1049 1049 end
1050 1050
1051 1051 # Query generator for selecting groups of issue counts for a project
1052 1052 # based on specific criteria
1053 1053 #
1054 1054 # Options
1055 1055 # * project - Project to search in.
1056 1056 # * field - String. Issue field to key off of in the grouping.
1057 1057 # * joins - String. The table name to join against.
1058 1058 def self.count_and_group_by(options)
1059 1059 project = options.delete(:project)
1060 1060 select_field = options.delete(:field)
1061 1061 joins = options.delete(:joins)
1062 1062
1063 1063 where = "#{Issue.table_name}.#{select_field}=j.id"
1064 1064
1065 1065 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1066 1066 s.is_closed as closed,
1067 1067 j.id as #{select_field},
1068 1068 count(#{Issue.table_name}.id) as total
1069 1069 from
1070 1070 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1071 1071 where
1072 1072 #{Issue.table_name}.status_id=s.id
1073 1073 and #{where}
1074 1074 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1075 1075 and #{visible_condition(User.current, :project => project)}
1076 1076 group by s.id, s.is_closed, j.id")
1077 1077 end
1078 1078 end
@@ -1,446 +1,455
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 class MailHandler < ActionMailer::Base
18 require 'vendor/tmail'
19
20 class MailHandler
19 21 include ActionView::Helpers::SanitizeHelper
20 22 include Redmine::I18n
21 23
22 24 class UnauthorizedAction < StandardError; end
23 25 class MissingInformation < StandardError; end
24 26
25 27 attr_reader :email, :user
26 28
27 29 def self.receive(email, options={})
28 30 @@handler_options = options.dup
29 31
30 32 @@handler_options[:issue] ||= {}
31 33
32 34 if @@handler_options[:allow_override].is_a?(String)
33 35 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 36 end
35 37 @@handler_options[:allow_override] ||= []
36 38 # Project needs to be overridable if not specified
37 39 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 40 # Status overridable by default
39 41 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40 42
41 43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
42 super email
44
45 mail = TMail::Mail.parse(email)
46 mail.base64_decode
47 new.receive(mail)
48 end
49
50 def logger
51 Rails.logger
43 52 end
44 53
45 54 cattr_accessor :ignored_emails_headers
46 55 @@ignored_emails_headers = {
47 56 'X-Auto-Response-Suppress' => 'OOF',
48 57 'Auto-Submitted' => 'auto-replied'
49 58 }
50 59
51 60 # Processes incoming emails
52 61 # Returns the created object (eg. an issue, a message) or false
53 62 def receive(email)
54 63 @email = email
55 64 sender_email = email.from.to_a.first.to_s.strip
56 65 # Ignore emails received from the application emission address to avoid hell cycles
57 66 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
58 67 if logger && logger.info
59 68 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
60 69 end
61 70 return false
62 71 end
63 72 # Ignore auto generated emails
64 73 self.class.ignored_emails_headers.each do |key, ignored_value|
65 74 value = email.header_string(key)
66 75 if value && value.to_s.downcase == ignored_value.downcase
67 76 if logger && logger.info
68 77 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
69 78 end
70 79 return false
71 80 end
72 81 end
73 82 @user = User.find_by_mail(sender_email) if sender_email.present?
74 83 if @user && !@user.active?
75 84 if logger && logger.info
76 85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
77 86 end
78 87 return false
79 88 end
80 89 if @user.nil?
81 90 # Email was submitted by an unknown user
82 91 case @@handler_options[:unknown_user]
83 92 when 'accept'
84 93 @user = User.anonymous
85 94 when 'create'
86 95 @user = create_user_from_email
87 96 if @user
88 97 if logger && logger.info
89 98 logger.info "MailHandler: [#{@user.login}] account created"
90 99 end
91 100 Mailer.deliver_account_information(@user, @user.password)
92 101 else
93 102 if logger && logger.error
94 103 logger.error "MailHandler: could not create account for [#{sender_email}]"
95 104 end
96 105 return false
97 106 end
98 107 else
99 108 # Default behaviour, emails from unknown users are ignored
100 109 if logger && logger.info
101 110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
102 111 end
103 112 return false
104 113 end
105 114 end
106 115 User.current = @user
107 116 dispatch
108 117 end
109 118
110 119 private
111 120
112 121 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
113 122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
114 123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
115 124
116 125 def dispatch
117 126 headers = [email.in_reply_to, email.references].flatten.compact
118 127 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
119 128 klass, object_id = $1, $2.to_i
120 129 method_name = "receive_#{klass}_reply"
121 130 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
122 131 send method_name, object_id
123 132 else
124 133 # ignoring it
125 134 end
126 135 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
127 136 receive_issue_reply(m[1].to_i)
128 137 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
129 138 receive_message_reply(m[1].to_i)
130 139 else
131 140 dispatch_to_default
132 141 end
133 142 rescue ActiveRecord::RecordInvalid => e
134 143 # TODO: send a email to the user
135 144 logger.error e.message if logger
136 145 false
137 146 rescue MissingInformation => e
138 147 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
139 148 false
140 149 rescue UnauthorizedAction => e
141 150 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
142 151 false
143 152 end
144 153
145 154 def dispatch_to_default
146 155 receive_issue
147 156 end
148 157
149 158 # Creates a new issue
150 159 def receive_issue
151 160 project = target_project
152 161 # check permission
153 162 unless @@handler_options[:no_permission_check]
154 163 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
155 164 end
156 165
157 166 issue = Issue.new(:author => user, :project => project)
158 167 issue.safe_attributes = issue_attributes_from_keywords(issue)
159 168 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
160 169 issue.subject = email.subject.to_s.chomp[0,255]
161 170 if issue.subject.blank?
162 171 issue.subject = '(no subject)'
163 172 end
164 173 issue.description = cleaned_up_text_body
165 174
166 175 # add To and Cc as watchers before saving so the watchers can reply to Redmine
167 176 add_watchers(issue)
168 177 issue.save!
169 178 add_attachments(issue)
170 179 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
171 180 issue
172 181 end
173 182
174 183 # Adds a note to an existing issue
175 184 def receive_issue_reply(issue_id)
176 185 issue = Issue.find_by_id(issue_id)
177 186 return unless issue
178 187 # check permission
179 188 unless @@handler_options[:no_permission_check]
180 189 unless user.allowed_to?(:add_issue_notes, issue.project) ||
181 190 user.allowed_to?(:edit_issues, issue.project)
182 191 raise UnauthorizedAction
183 192 end
184 193 end
185 194
186 195 # ignore CLI-supplied defaults for new issues
187 196 @@handler_options[:issue].clear
188 197
189 198 journal = issue.init_journal(user)
190 199 issue.safe_attributes = issue_attributes_from_keywords(issue)
191 200 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
192 201 journal.notes = cleaned_up_text_body
193 202 add_attachments(issue)
194 203 issue.save!
195 204 if logger && logger.info
196 205 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
197 206 end
198 207 journal
199 208 end
200 209
201 210 # Reply will be added to the issue
202 211 def receive_journal_reply(journal_id)
203 212 journal = Journal.find_by_id(journal_id)
204 213 if journal && journal.journalized_type == 'Issue'
205 214 receive_issue_reply(journal.journalized_id)
206 215 end
207 216 end
208 217
209 218 # Receives a reply to a forum message
210 219 def receive_message_reply(message_id)
211 220 message = Message.find_by_id(message_id)
212 221 if message
213 222 message = message.root
214 223
215 224 unless @@handler_options[:no_permission_check]
216 225 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
217 226 end
218 227
219 228 if !message.locked?
220 229 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
221 230 :content => cleaned_up_text_body)
222 231 reply.author = user
223 232 reply.board = message.board
224 233 message.children << reply
225 234 add_attachments(reply)
226 235 reply
227 236 else
228 237 if logger && logger.info
229 238 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
230 239 end
231 240 end
232 241 end
233 242 end
234 243
235 244 def add_attachments(obj)
236 245 if email.attachments && email.attachments.any?
237 246 email.attachments.each do |attachment|
238 247 obj.attachments << Attachment.create(:container => obj,
239 248 :file => attachment,
240 249 :author => user,
241 250 :content_type => attachment.content_type)
242 251 end
243 252 end
244 253 end
245 254
246 255 # Adds To and Cc as watchers of the given object if the sender has the
247 256 # appropriate permission
248 257 def add_watchers(obj)
249 258 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
250 259 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
251 260 unless addresses.empty?
252 261 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
253 262 watchers.each {|w| obj.add_watcher(w)}
254 263 end
255 264 end
256 265 end
257 266
258 267 def get_keyword(attr, options={})
259 268 @keywords ||= {}
260 269 if @keywords.has_key?(attr)
261 270 @keywords[attr]
262 271 else
263 272 @keywords[attr] = begin
264 273 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
265 274 (v = extract_keyword!(plain_text_body, attr, options[:format]))
266 275 v
267 276 elsif !@@handler_options[:issue][attr].blank?
268 277 @@handler_options[:issue][attr]
269 278 end
270 279 end
271 280 end
272 281 end
273 282
274 283 # Destructively extracts the value for +attr+ in +text+
275 284 # Returns nil if no matching keyword found
276 285 def extract_keyword!(text, attr, format=nil)
277 286 keys = [attr.to_s.humanize]
278 287 if attr.is_a?(Symbol)
279 288 if user && user.language.present?
280 289 keys << l("field_#{attr}", :default => '', :locale => user.language)
281 290 end
282 291 if Setting.default_language.present?
283 292 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
284 293 end
285 294 end
286 295 keys.reject! {|k| k.blank?}
287 296 keys.collect! {|k| Regexp.escape(k)}
288 297 format ||= '.+'
289 298 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
290 299 $2 && $2.strip
291 300 end
292 301
293 302 def target_project
294 303 # TODO: other ways to specify project:
295 304 # * parse the email To field
296 305 # * specific project (eg. Setting.mail_handler_target_project)
297 306 target = Project.find_by_identifier(get_keyword(:project))
298 307 raise MissingInformation.new('Unable to determine target project') if target.nil?
299 308 target
300 309 end
301 310
302 311 # Returns a Hash of issue attributes extracted from keywords in the email body
303 312 def issue_attributes_from_keywords(issue)
304 313 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
305 314
306 315 attrs = {
307 316 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
308 317 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
309 318 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
310 319 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
311 320 'assigned_to_id' => assigned_to.try(:id),
312 321 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
313 322 issue.project.shared_versions.named(k).first.try(:id),
314 323 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
315 324 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
316 325 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
317 326 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
318 327 }.delete_if {|k, v| v.blank? }
319 328
320 329 if issue.new_record? && attrs['tracker_id'].nil?
321 330 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
322 331 end
323 332
324 333 attrs
325 334 end
326 335
327 336 # Returns a Hash of issue custom field values extracted from keywords in the email body
328 337 def custom_field_values_from_keywords(customized)
329 338 customized.custom_field_values.inject({}) do |h, v|
330 339 if value = get_keyword(v.custom_field.name, :override => true)
331 340 h[v.custom_field.id.to_s] = value
332 341 end
333 342 h
334 343 end
335 344 end
336 345
337 346 # Returns the text/plain part of the email
338 347 # If not found (eg. HTML-only email), returns the body with tags removed
339 348 def plain_text_body
340 349 return @plain_text_body unless @plain_text_body.nil?
341 350 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
342 351 if parts.empty?
343 352 parts << @email
344 353 end
345 354 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
346 355 if plain_text_part.nil?
347 356 # no text/plain part found, assuming html-only email
348 357 # strip html tags and remove doctype directive
349 358 @plain_text_body = strip_tags(@email.body.to_s)
350 359 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
351 360 else
352 361 @plain_text_body = plain_text_part.body.to_s
353 362 end
354 363 @plain_text_body.strip!
355 364 @plain_text_body
356 365 end
357 366
358 367 def cleaned_up_text_body
359 368 cleanup_body(plain_text_body)
360 369 end
361 370
362 371 def self.full_sanitizer
363 372 @full_sanitizer ||= HTML::FullSanitizer.new
364 373 end
365 374
366 375 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
367 376 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
368 377 value = value.to_s.slice(0, limit)
369 378 object.send("#{attribute}=", value)
370 379 end
371 380
372 381 # Returns a User from an email address and a full name
373 382 def self.new_user_from_attributes(email_address, fullname=nil)
374 383 user = User.new
375 384
376 385 # Truncating the email address would result in an invalid format
377 386 user.mail = email_address
378 387 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
379 388
380 389 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
381 390 assign_string_attribute_with_limit(user, 'firstname', names.shift)
382 391 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
383 392 user.lastname = '-' if user.lastname.blank?
384 393
385 394 password_length = [Setting.password_min_length.to_i, 10].max
386 395 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
387 396 user.language = Setting.default_language
388 397
389 398 unless user.valid?
390 399 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
391 400 user.firstname = "-" unless user.errors[:firstname].blank?
392 401 user.lastname = "-" unless user.errors[:lastname].blank?
393 402 end
394 403
395 404 user
396 405 end
397 406
398 407 # Creates a User for the +email+ sender
399 408 # Returns the user or nil if it could not be created
400 409 def create_user_from_email
401 410 addr = email.from_addrs.to_a.first
402 411 if addr && !addr.spec.blank?
403 412 user = self.class.new_user_from_attributes(addr.spec, TMail::Unquoter.unquote_and_convert_to(addr.name, 'utf-8'))
404 413 if user.save
405 414 user
406 415 else
407 416 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
408 417 nil
409 418 end
410 419 else
411 420 logger.error "MailHandler: failed to create User: no FROM address found" if logger
412 421 nil
413 422 end
414 423 end
415 424
416 425 # Removes the email body of text after the truncation configurations.
417 426 def cleanup_body(body)
418 427 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
419 428 unless delimiters.empty?
420 429 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
421 430 body = body.gsub(regex, '')
422 431 end
423 432 body.strip
424 433 end
425 434
426 435 def find_assignee_from_keyword(keyword, issue)
427 436 keyword = keyword.to_s.downcase
428 437 assignable = issue.assignable_users
429 438 assignee = nil
430 439 assignee ||= assignable.detect {|a|
431 440 a.mail.to_s.downcase == keyword ||
432 441 a.login.to_s.downcase == keyword
433 442 }
434 443 if assignee.nil? && keyword.match(/ /)
435 444 firstname, lastname = *(keyword.split) # "First Last Throwaway"
436 445 assignee ||= assignable.detect {|a|
437 446 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
438 447 a.lastname.to_s.downcase == lastname
439 448 }
440 449 end
441 450 if assignee.nil?
442 451 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
443 452 end
444 453 assignee
445 454 end
446 455 end
@@ -1,498 +1,461
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Mailer < ActionMailer::Base
19 19 layout 'mailer'
20 20 helper :application
21 21 helper :issues
22 22 helper :custom_fields
23 23
24 include ActionController::UrlWriter
25 24 include Redmine::I18n
26 25
27 26 def self.default_url_options
28 h = Setting.host_name
29 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
30 { :host => h, :protocol => Setting.protocol }
27 { :host => Setting.host_name, :protocol => Setting.protocol }
31 28 end
32 29
33 30 # Builds a tmail object used to email recipients of the added issue.
34 31 #
35 32 # Example:
36 33 # issue_add(issue) => tmail object
37 34 # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
38 35 def issue_add(issue)
39 36 redmine_headers 'Project' => issue.project.identifier,
40 37 'Issue-Id' => issue.id,
41 38 'Issue-Author' => issue.author.login
42 39 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
43 40 message_id issue
44 41 @author = issue.author
45 recipients issue.recipients
46 cc(issue.watcher_recipients - @recipients)
47 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
48 body :issue => issue,
49 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
50 render_multipart('issue_add', body)
42 @issue = issue
43 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
44 recipients = issue.recipients
45 cc = issue.watcher_recipients - recipients
46 mail :to => recipients,
47 :cc => cc,
48 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
51 49 end
52 50
53 51 # Builds a tmail object used to email recipients of the edited issue.
54 52 #
55 53 # Example:
56 54 # issue_edit(journal) => tmail object
57 55 # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
58 56 def issue_edit(journal)
59 57 issue = journal.journalized.reload
60 58 redmine_headers 'Project' => issue.project.identifier,
61 59 'Issue-Id' => issue.id,
62 60 'Issue-Author' => issue.author.login
63 61 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
64 62 message_id journal
65 63 references issue
66 64 @author = journal.user
67 recipients issue.recipients
65 recipients = issue.recipients
68 66 # Watchers in cc
69 cc(issue.watcher_recipients - @recipients)
67 cc = issue.watcher_recipients - recipients
70 68 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
71 69 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
72 70 s << issue.subject
73 subject s
74 body :issue => issue,
75 :journal => journal,
76 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
77
78 render_multipart('issue_edit', body)
71 @issue = issue
72 @journal = journal
73 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
74 mail :to => recipients,
75 :cc => cc,
76 :subject => s
79 77 end
80 78
81 79 def reminder(user, issues, days)
82 80 set_language_if_valid user.language
83 recipients user.mail
84 subject l(:mail_subject_reminder, :count => issues.size, :days => days)
85 body :issues => issues,
86 :days => days,
87 :issues_url => url_for(:controller => 'issues', :action => 'index',
81 @issues = issues
82 @days = days
83 @issues_url = url_for(:controller => 'issues', :action => 'index',
88 84 :set_filter => 1, :assigned_to_id => user.id,
89 85 :sort => 'due_date:asc')
90 render_multipart('reminder', body)
86 mail :to => user.mail,
87 :subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
91 88 end
92 89
93 90 # Builds a tmail object used to email users belonging to the added document's project.
94 91 #
95 92 # Example:
96 93 # document_added(document) => tmail object
97 94 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
98 95 def document_added(document)
99 96 redmine_headers 'Project' => document.project.identifier
100 recipients document.recipients
101 97 @author = User.current
102 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
103 body :document => document,
104 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
105 render_multipart('document_added', body)
98 @document = document
99 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
100 mail :to => document.recipients,
101 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
106 102 end
107 103
108 104 # Builds a tmail object used to email recipients of a project when an attachements are added.
109 105 #
110 106 # Example:
111 107 # attachments_added(attachments) => tmail object
112 108 # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
113 109 def attachments_added(attachments)
114 110 container = attachments.first.container
115 111 added_to = ''
116 112 added_to_url = ''
117 113 @author = attachments.first.author
118 114 case container.class.name
119 115 when 'Project'
120 116 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
121 117 added_to = "#{l(:label_project)}: #{container}"
122 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
118 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
123 119 when 'Version'
124 120 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
125 121 added_to = "#{l(:label_version)}: #{container.name}"
126 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
122 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
127 123 when 'Document'
128 124 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
129 125 added_to = "#{l(:label_document)}: #{container.title}"
130 recipients container.recipients
126 recipients = container.recipients
131 127 end
132 128 redmine_headers 'Project' => container.project.identifier
133 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
134 body :attachments => attachments,
135 :added_to => added_to,
136 :added_to_url => added_to_url
137 render_multipart('attachments_added', body)
129 @attachments = attachments
130 @added_to = added_to
131 @added_to_url = added_to_url
132 mail :to => recipients,
133 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
138 134 end
139 135
140 136 # Builds a tmail object used to email recipients of a news' project when a news item is added.
141 137 #
142 138 # Example:
143 139 # news_added(news) => tmail object
144 140 # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
145 141 def news_added(news)
146 142 redmine_headers 'Project' => news.project.identifier
147 143 @author = news.author
148 144 message_id news
149 recipients news.recipients
150 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
151 body :news => news,
152 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
153 render_multipart('news_added', body)
145 @news = news
146 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
147 mail :to => news.recipients,
148 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
154 149 end
155 150
156 151 # Builds a tmail object used to email recipients of a news' project when a news comment is added.
157 152 #
158 153 # Example:
159 154 # news_comment_added(comment) => tmail object
160 155 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
161 156 def news_comment_added(comment)
162 157 news = comment.commented
163 158 redmine_headers 'Project' => news.project.identifier
164 159 @author = comment.author
165 160 message_id comment
166 recipients news.recipients
167 cc news.watcher_recipients
168 subject "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
169 body :news => news,
170 :comment => comment,
171 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
172 render_multipart('news_comment_added', body)
161 @news = news
162 @comment = comment
163 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
164 mail :to => news.recipients,
165 :cc => news.watcher_recipients,
166 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
173 167 end
174 168
175 169 # Builds a tmail object used to email the recipients of the specified message that was posted.
176 170 #
177 171 # Example:
178 172 # message_posted(message) => tmail object
179 173 # Mailer.deliver_message_posted(message) => sends an email to the recipients
180 174 def message_posted(message)
181 175 redmine_headers 'Project' => message.project.identifier,
182 176 'Topic-Id' => (message.parent_id || message.id)
183 177 @author = message.author
184 178 message_id message
185 179 references message.parent unless message.parent.nil?
186 recipients(message.recipients)
187 cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
188 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
189 body :message => message,
190 :message_url => url_for(message.event_url)
191 render_multipart('message_posted', body)
180 recipients = message.recipients
181 cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
182 @message = message
183 @message_url = url_for(message.event_url)
184 mail :to => recipients,
185 :cc => cc,
186 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
192 187 end
193 188
194 189 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
195 190 #
196 191 # Example:
197 192 # wiki_content_added(wiki_content) => tmail object
198 193 # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
199 194 def wiki_content_added(wiki_content)
200 195 redmine_headers 'Project' => wiki_content.project.identifier,
201 196 'Wiki-Page-Id' => wiki_content.page.id
202 197 @author = wiki_content.author
203 198 message_id wiki_content
204 recipients wiki_content.recipients
205 cc(wiki_content.page.wiki.watcher_recipients - recipients)
206 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
207 body :wiki_content => wiki_content,
208 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
199 recipients = wiki_content.recipients
200 cc = wiki_content.page.wiki.watcher_recipients - recipients
201 @wiki_content = wiki_content
202 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
209 203 :project_id => wiki_content.project,
210 204 :id => wiki_content.page.title)
211 render_multipart('wiki_content_added', body)
205 mail :to => recipients,
206 :cc => cc,
207 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
212 208 end
213 209
214 210 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
215 211 #
216 212 # Example:
217 213 # wiki_content_updated(wiki_content) => tmail object
218 214 # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
219 215 def wiki_content_updated(wiki_content)
220 216 redmine_headers 'Project' => wiki_content.project.identifier,
221 217 'Wiki-Page-Id' => wiki_content.page.id
222 218 @author = wiki_content.author
223 219 message_id wiki_content
224 recipients wiki_content.recipients
225 cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
226 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
227 body :wiki_content => wiki_content,
228 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
220 recipients = wiki_content.recipients
221 cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
222 @wiki_content = wiki_content
223 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
229 224 :project_id => wiki_content.project,
230 :id => wiki_content.page.title),
231 :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff',
225 :id => wiki_content.page.title)
226 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
232 227 :project_id => wiki_content.project, :id => wiki_content.page.title,
233 228 :version => wiki_content.version)
234 render_multipart('wiki_content_updated', body)
229 mail :to => recipients,
230 :cc => cc,
231 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
235 232 end
236 233
237 234 # Builds a tmail object used to email the specified user their account information.
238 235 #
239 236 # Example:
240 237 # account_information(user, password) => tmail object
241 238 # Mailer.deliver_account_information(user, password) => sends account information to the user
242 239 def account_information(user, password)
243 240 set_language_if_valid user.language
244 recipients user.mail
245 subject l(:mail_subject_register, Setting.app_title)
246 body :user => user,
247 :password => password,
248 :login_url => url_for(:controller => 'account', :action => 'login')
249 render_multipart('account_information', body)
241 @user = user
242 @password = password
243 @login_url = url_for(:controller => 'account', :action => 'login')
244 mail :to => user.mail,
245 :subject => l(:mail_subject_register, Setting.app_title)
250 246 end
251 247
252 248 # Builds a tmail object used to email all active administrators of an account activation request.
253 249 #
254 250 # Example:
255 251 # account_activation_request(user) => tmail object
256 252 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
257 253 def account_activation_request(user)
258 254 # Send the email to all active administrators
259 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
260 subject l(:mail_subject_account_activation_request, Setting.app_title)
261 body :user => user,
262 :url => url_for(:controller => 'users', :action => 'index',
255 recipients = User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
256 @user = user
257 @url = url_for(:controller => 'users', :action => 'index',
263 258 :status => User::STATUS_REGISTERED,
264 259 :sort_key => 'created_on', :sort_order => 'desc')
265 render_multipart('account_activation_request', body)
260 mail :to => recipients,
261 :subject => l(:mail_subject_account_activation_request, Setting.app_title)
266 262 end
267 263
268 264 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
269 265 #
270 266 # Example:
271 267 # account_activated(user) => tmail object
272 268 # Mailer.deliver_account_activated(user) => sends an email to the registered user
273 269 def account_activated(user)
274 270 set_language_if_valid user.language
275 recipients user.mail
276 subject l(:mail_subject_register, Setting.app_title)
277 body :user => user,
278 :login_url => url_for(:controller => 'account', :action => 'login')
279 render_multipart('account_activated', body)
271 @user = user
272 @login_url = url_for(:controller => 'account', :action => 'login')
273 mail :to => user.mail,
274 :subject => l(:mail_subject_register, Setting.app_title)
280 275 end
281 276
282 277 def lost_password(token)
283 278 set_language_if_valid(token.user.language)
284 recipients token.user.mail
285 subject l(:mail_subject_lost_password, Setting.app_title)
286 body :token => token,
287 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
288 render_multipart('lost_password', body)
279 @token = token
280 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
281 mail :to => token.user.mail,
282 :subject => l(:mail_subject_lost_password, Setting.app_title)
289 283 end
290 284
291 285 def register(token)
292 286 set_language_if_valid(token.user.language)
293 recipients token.user.mail
294 subject l(:mail_subject_register, Setting.app_title)
295 body :token => token,
296 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
297 render_multipart('register', body)
287 @token = token
288 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
289 mail :to => token.user.mail,
290 :subject => l(:mail_subject_register, Setting.app_title)
298 291 end
299 292
300 293 def test_email(user)
301 294 set_language_if_valid(user.language)
302 recipients user.mail
303 subject 'Redmine test'
304 body :url => url_for(:controller => 'welcome')
305 render_multipart('test_email', body)
295 @url = url_for(:controller => 'welcome')
296 mail :to => user.mail,
297 :subject => 'Redmine test'
306 298 end
307 299
308 300 # Overrides default deliver! method to prevent from sending an email
309 301 # with no recipient, cc or bcc
310 302 def deliver!(mail = @mail)
311 303 set_language_if_valid @initial_language
312 304 return false if (recipients.nil? || recipients.empty?) &&
313 305 (cc.nil? || cc.empty?) &&
314 306 (bcc.nil? || bcc.empty?)
315 307
316 # Set Message-Id and References
317 if @message_id_object
318 mail.message_id = self.class.message_id_for(@message_id_object)
319 end
320 if @references_objects
321 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
322 end
323 308
324 309 # Log errors when raise_delivery_errors is set to false, Rails does not
325 310 raise_errors = self.class.raise_delivery_errors
326 311 self.class.raise_delivery_errors = true
327 312 begin
328 313 return super(mail)
329 314 rescue Exception => e
330 315 if raise_errors
331 316 raise e
332 317 elsif mylogger
333 318 mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
334 319 end
335 320 ensure
336 321 self.class.raise_delivery_errors = raise_errors
337 322 end
338 323 end
339 324
340 325 # Sends reminders to issue assignees
341 326 # Available options:
342 327 # * :days => how many days in the future to remind about (defaults to 7)
343 328 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
344 329 # * :project => id or identifier of project to process (defaults to all projects)
345 330 # * :users => array of user ids who should be reminded
346 331 def self.reminders(options={})
347 332 days = options[:days] || 7
348 333 project = options[:project] ? Project.find(options[:project]) : nil
349 334 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
350 335 user_ids = options[:users]
351 336
352 337 scope = Issue.open.scoped(:conditions => ["#{Issue.table_name}.assigned_to_id IS NOT NULL" +
353 338 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
354 339 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date]
355 340 )
356 341 scope = scope.scoped(:conditions => {:assigned_to_id => user_ids}) if user_ids.present?
357 342 scope = scope.scoped(:conditions => {:project_id => project.id}) if project
358 343 scope = scope.scoped(:conditions => {:tracker_id => tracker.id}) if tracker
359 344
360 345 issues_by_assignee = scope.all(:include => [:status, :assigned_to, :project, :tracker]).group_by(&:assigned_to)
361 346 issues_by_assignee.each do |assignee, issues|
362 347 deliver_reminder(assignee, issues, days) if assignee.is_a?(User) && assignee.active?
363 348 end
364 349 end
365 350
366 351 # Activates/desactivates email deliveries during +block+
367 352 def self.with_deliveries(enabled = true, &block)
368 353 was_enabled = ActionMailer::Base.perform_deliveries
369 354 ActionMailer::Base.perform_deliveries = !!enabled
370 355 yield
371 356 ensure
372 357 ActionMailer::Base.perform_deliveries = was_enabled
373 358 end
374 359
375 360 # Sends emails synchronously in the given block
376 361 def self.with_synched_deliveries(&block)
377 362 saved_method = ActionMailer::Base.delivery_method
378 363 if m = saved_method.to_s.match(%r{^async_(.+)$})
379 364 ActionMailer::Base.delivery_method = m[1].to_sym
380 365 end
381 366 yield
382 367 ensure
383 368 ActionMailer::Base.delivery_method = saved_method
384 369 end
385 370
386 private
387 def initialize_defaults(method_name)
388 super
389 @initial_language = current_language
390 set_language_if_valid Setting.default_language
391 from Setting.mail_from
392
393 # Common headers
394 headers 'X-Mailer' => 'Redmine',
371 def mail(headers={})
372 headers.merge! 'X-Mailer' => 'Redmine',
395 373 'X-Redmine-Host' => Setting.host_name,
396 374 'X-Redmine-Site' => Setting.app_title,
397 375 'X-Auto-Response-Suppress' => 'OOF',
398 'Auto-Submitted' => 'auto-generated'
399 end
376 'Auto-Submitted' => 'auto-generated',
377 'From' => Setting.mail_from
400 378
401 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
402 def redmine_headers(h)
403 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
404 end
405
406 # Overrides the create_mail method
407 def create_mail
408 379 # Removes the author from the recipients and cc
409 380 # if he doesn't want to receive notifications about what he does
410 381 if @author && @author.logged? && @author.pref[:no_self_notified]
411 if recipients
412 recipients((recipients.is_a?(Array) ? recipients : [recipients]) - [@author.mail])
413 end
414 if cc
415 cc((cc.is_a?(Array) ? cc : [cc]) - [@author.mail])
416 end
382 headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
383 headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
417 384 end
418 385
419 386 if @author && @author.logged?
420 387 redmine_headers 'Sender' => @author.login
421 388 end
422 389
423 notified_users = [recipients, cc].flatten.compact.uniq
424 # Rails would log recipients only, not cc and bcc
425 mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger
426
427 390 # Blind carbon copy recipients
428 391 if Setting.bcc_recipients?
429 bcc(notified_users)
430 recipients []
431 cc []
392 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
393 headers[:to] = nil
394 headers[:cc] = nil
395 end
396
397 if @message_id_object
398 headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
399 end
400 if @references_objects
401 headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
402 end
403
404 super headers do |format|
405 format.text
406 format.html unless Setting.plain_text_mail?
432 407 end
408
409 set_language_if_valid @initial_language
410 end
411
412 def initialize(*args)
413 @initial_language = current_language
414 set_language_if_valid Setting.default_language
415 super
416 end
417
418 def self.deliver_mail(mail)
419 return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
433 420 super
434 421 end
435 422
436 # Rails 2.3 has problems rendering implicit multipart messages with
437 # layouts so this method will wrap an multipart messages with
438 # explicit parts.
439 #
440 # https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
441 # https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
442
443 def render_multipart(method_name, body)
444 if Setting.plain_text_mail?
445 content_type "text/plain"
446 body render(:file => "#{method_name}.text.erb",
447 :body => body,
448 :layout => 'mailer.text.erb')
423 def self.method_missing(method, *args, &block)
424 if m = method.to_s.match(%r{^deliver_(.+)$})
425 send(m[1], *args).deliver
449 426 else
450 content_type "multipart/alternative"
451 part :content_type => "text/plain",
452 :body => render(:file => "#{method_name}.text.erb",
453 :body => body, :layout => 'mailer.text.erb')
454 part :content_type => "text/html",
455 :body => render_message("#{method_name}.html.erb", body)
427 super
456 428 end
457 429 end
458 430
459 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
460 def self.controller_path
461 ''
462 end unless respond_to?('controller_path')
431 private
432
433 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
434 def redmine_headers(h)
435 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
436 end
463 437
464 438 # Returns a predictable Message-Id for the given object
465 439 def self.message_id_for(object)
466 440 # id + timestamp should reduce the odds of a collision
467 441 # as far as we don't send multiple emails for the same object
468 442 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
469 443 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
470 444 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
471 445 host = "#{::Socket.gethostname}.redmine" if host.empty?
472 "<#{hash}@#{host}>"
446 "#{hash}@#{host}"
473 447 end
474 448
475 private
476
477 449 def message_id(object)
478 450 @message_id_object = object
479 451 end
480 452
481 453 def references(object)
482 454 @references_objects ||= []
483 455 @references_objects << object
484 456 end
485 457
486 458 def mylogger
487 459 Rails.logger
488 460 end
489 461 end
490
491 # Patch TMail so that message_id is not overwritten
492 module TMail
493 class Mail
494 def add_message_id( fqdn = nil )
495 self.message_id ||= ::TMail::new_message_id(fqdn)
496 end
497 end
498 end
@@ -1,918 +1,922
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_ARCHIVED = 9
24 24
25 25 # Maximum length for project identifiers
26 26 IDENTIFIER_MAX_LENGTH = 100
27 27
28 28 # Specific overidden Activities
29 29 has_many :time_entry_activities
30 30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 31 has_many :memberships, :class_name => 'Member'
32 32 has_many :member_principals, :class_name => 'Member',
33 33 :include => :principal,
34 34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 35 has_many :users, :through => :members
36 36 has_many :principals, :through => :member_principals, :source => :principal
37 37
38 38 has_many :enabled_modules, :dependent => :delete_all
39 39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 40 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 41 has_many :issue_changes, :through => :issues, :source => :journals
42 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 43 has_many :time_entries, :dependent => :delete_all
44 44 has_many :queries, :dependent => :delete_all
45 45 has_many :documents, :dependent => :destroy
46 46 has_many :news, :dependent => :destroy, :include => :author
47 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 49 has_one :repository, :conditions => ["is_default = ?", true]
50 50 has_many :repositories, :dependent => :destroy
51 51 has_many :changesets, :through => :repository
52 52 has_one :wiki, :dependent => :destroy
53 53 # Custom field for the project issues
54 54 has_and_belongs_to_many :issue_custom_fields,
55 55 :class_name => 'IssueCustomField',
56 56 :order => "#{CustomField.table_name}.position",
57 57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 58 :association_foreign_key => 'custom_field_id'
59 59
60 60 acts_as_nested_set :order => 'name', :dependent => :destroy
61 61 acts_as_attachable :view_permission => :view_files,
62 62 :delete_permission => :manage_files
63 63
64 64 acts_as_customizable
65 65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 68 :author => nil
69 69
70 70 attr_protected :status
71 71
72 72 validates_presence_of :name, :identifier
73 73 validates_uniqueness_of :identifier
74 74 validates_associated :repository, :wiki
75 75 validates_length_of :name, :maximum => 255
76 76 validates_length_of :homepage, :maximum => 255
77 77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 78 # donwcase letters, digits, dashes but not digits only
79 79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
80 80 # reserved words
81 81 validates_exclusion_of :identifier, :in => %w( new )
82 82
83 83 before_destroy :delete_all_members
84 84
85 85 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] } }
86 86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
87 87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
88 88 named_scope :all_public, { :conditions => { :is_public => true } }
89 89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
90 90 named_scope :allowed_to, lambda {|*args|
91 91 user = User.current
92 92 permission = nil
93 93 if args.first.is_a?(Symbol)
94 94 permission = args.shift
95 95 else
96 96 user = args.shift
97 97 permission = args.shift
98 98 end
99 99 { :conditions => Project.allowed_to_condition(user, permission, *args) }
100 100 }
101 101 named_scope :like, lambda {|arg|
102 102 if arg.blank?
103 103 {}
104 104 else
105 105 pattern = "%#{arg.to_s.strip.downcase}%"
106 106 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
107 107 end
108 108 }
109 109
110 110 def initialize(attributes=nil, *args)
111 111 super
112 112
113 113 initialized = (attributes || {}).stringify_keys
114 114 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
115 115 self.identifier = Project.next_identifier
116 116 end
117 117 if !initialized.key?('is_public')
118 118 self.is_public = Setting.default_projects_public?
119 119 end
120 120 if !initialized.key?('enabled_module_names')
121 121 self.enabled_module_names = Setting.default_projects_modules
122 122 end
123 123 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
124 124 self.trackers = Tracker.all
125 125 end
126 126 end
127 127
128 128 def identifier=(identifier)
129 129 super unless identifier_frozen?
130 130 end
131 131
132 132 def identifier_frozen?
133 133 errors[:identifier].nil? && !(new_record? || identifier.blank?)
134 134 end
135 135
136 136 # returns latest created projects
137 137 # non public projects will be returned only if user is a member of those
138 138 def self.latest(user=nil, count=5)
139 139 visible(user).find(:all, :limit => count, :order => "created_on DESC")
140 140 end
141 141
142 142 # Returns true if the project is visible to +user+ or to the current user.
143 143 def visible?(user=User.current)
144 144 user.allowed_to?(:view_project, self)
145 145 end
146 146
147 147 # Returns a SQL conditions string used to find all projects visible by the specified user.
148 148 #
149 149 # Examples:
150 150 # Project.visible_condition(admin) => "projects.status = 1"
151 151 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
152 152 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
153 153 def self.visible_condition(user, options={})
154 154 allowed_to_condition(user, :view_project, options)
155 155 end
156 156
157 157 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
158 158 #
159 159 # Valid options:
160 160 # * :project => limit the condition to project
161 161 # * :with_subprojects => limit the condition to project and its subprojects
162 162 # * :member => limit the condition to the user projects
163 163 def self.allowed_to_condition(user, permission, options={})
164 164 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
165 165 if perm = Redmine::AccessControl.permission(permission)
166 166 unless perm.project_module.nil?
167 167 # If the permission belongs to a project module, make sure the module is enabled
168 168 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
169 169 end
170 170 end
171 171 if options[:project]
172 172 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
173 173 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
174 174 base_statement = "(#{project_statement}) AND (#{base_statement})"
175 175 end
176 176
177 177 if user.admin?
178 178 base_statement
179 179 else
180 180 statement_by_role = {}
181 181 unless options[:member]
182 182 role = user.logged? ? Role.non_member : Role.anonymous
183 183 if role.allowed_to?(permission)
184 184 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
185 185 end
186 186 end
187 187 if user.logged?
188 188 user.projects_by_role.each do |role, projects|
189 189 if role.allowed_to?(permission)
190 190 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
191 191 end
192 192 end
193 193 end
194 194 if statement_by_role.empty?
195 195 "1=0"
196 196 else
197 197 if block_given?
198 198 statement_by_role.each do |role, statement|
199 199 if s = yield(role, user)
200 200 statement_by_role[role] = "(#{statement} AND (#{s}))"
201 201 end
202 202 end
203 203 end
204 204 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
205 205 end
206 206 end
207 207 end
208 208
209 209 # Returns the Systemwide and project specific activities
210 210 def activities(include_inactive=false)
211 211 if include_inactive
212 212 return all_activities
213 213 else
214 214 return active_activities
215 215 end
216 216 end
217 217
218 218 # Will create a new Project specific Activity or update an existing one
219 219 #
220 220 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
221 221 # does not successfully save.
222 222 def update_or_create_time_entry_activity(id, activity_hash)
223 223 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
224 224 self.create_time_entry_activity_if_needed(activity_hash)
225 225 else
226 226 activity = project.time_entry_activities.find_by_id(id.to_i)
227 227 activity.update_attributes(activity_hash) if activity
228 228 end
229 229 end
230 230
231 231 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
232 232 #
233 233 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
234 234 # does not successfully save.
235 235 def create_time_entry_activity_if_needed(activity)
236 236 if activity['parent_id']
237 237
238 238 parent_activity = TimeEntryActivity.find(activity['parent_id'])
239 239 activity['name'] = parent_activity.name
240 240 activity['position'] = parent_activity.position
241 241
242 242 if Enumeration.overridding_change?(activity, parent_activity)
243 243 project_activity = self.time_entry_activities.create(activity)
244 244
245 245 if project_activity.new_record?
246 246 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
247 247 else
248 248 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
249 249 end
250 250 end
251 251 end
252 252 end
253 253
254 254 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
255 255 #
256 256 # Examples:
257 257 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
258 258 # project.project_condition(false) => "projects.id = 1"
259 259 def project_condition(with_subprojects)
260 260 cond = "#{Project.table_name}.id = #{id}"
261 261 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
262 262 cond
263 263 end
264 264
265 265 def self.find(*args)
266 266 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
267 267 project = find_by_identifier(*args)
268 268 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
269 269 project
270 270 else
271 271 super
272 272 end
273 273 end
274 274
275 def self.find_by_param(*args)
276 self.find(*args)
277 end
278
275 279 def reload(*args)
276 280 @shared_versions = nil
277 281 @rolled_up_versions = nil
278 282 @rolled_up_trackers = nil
279 283 @all_issue_custom_fields = nil
280 284 @all_time_entry_custom_fields = nil
281 285 @to_param = nil
282 286 @allowed_parents = nil
283 287 @allowed_permissions = nil
284 288 @actions_allowed = nil
285 289 super
286 290 end
287 291
288 292 def to_param
289 293 # id is used for projects with a numeric identifier (compatibility)
290 294 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
291 295 end
292 296
293 297 def active?
294 298 self.status == STATUS_ACTIVE
295 299 end
296 300
297 301 def archived?
298 302 self.status == STATUS_ARCHIVED
299 303 end
300 304
301 305 # Archives the project and its descendants
302 306 def archive
303 307 # Check that there is no issue of a non descendant project that is assigned
304 308 # to one of the project or descendant versions
305 309 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
306 310 if v_ids.any? && Issue.find(:first, :include => :project,
307 311 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
308 312 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
309 313 return false
310 314 end
311 315 Project.transaction do
312 316 archive!
313 317 end
314 318 true
315 319 end
316 320
317 321 # Unarchives the project
318 322 # All its ancestors must be active
319 323 def unarchive
320 324 return false if ancestors.detect {|a| !a.active?}
321 325 update_attribute :status, STATUS_ACTIVE
322 326 end
323 327
324 328 # Returns an array of projects the project can be moved to
325 329 # by the current user
326 330 def allowed_parents
327 331 return @allowed_parents if @allowed_parents
328 332 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
329 333 @allowed_parents = @allowed_parents - self_and_descendants
330 334 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
331 335 @allowed_parents << nil
332 336 end
333 337 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
334 338 @allowed_parents << parent
335 339 end
336 340 @allowed_parents
337 341 end
338 342
339 343 # Sets the parent of the project with authorization check
340 344 def set_allowed_parent!(p)
341 345 unless p.nil? || p.is_a?(Project)
342 346 if p.to_s.blank?
343 347 p = nil
344 348 else
345 349 p = Project.find_by_id(p)
346 350 return false unless p
347 351 end
348 352 end
349 353 if p.nil?
350 354 if !new_record? && allowed_parents.empty?
351 355 return false
352 356 end
353 357 elsif !allowed_parents.include?(p)
354 358 return false
355 359 end
356 360 set_parent!(p)
357 361 end
358 362
359 363 # Sets the parent of the project
360 364 # Argument can be either a Project, a String, a Fixnum or nil
361 365 def set_parent!(p)
362 366 unless p.nil? || p.is_a?(Project)
363 367 if p.to_s.blank?
364 368 p = nil
365 369 else
366 370 p = Project.find_by_id(p)
367 371 return false unless p
368 372 end
369 373 end
370 374 if p == parent && !p.nil?
371 375 # Nothing to do
372 376 true
373 377 elsif p.nil? || (p.active? && move_possible?(p))
374 378 # Insert the project so that target's children or root projects stay alphabetically sorted
375 379 sibs = (p.nil? ? self.class.roots : p.children)
376 380 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
377 381 if to_be_inserted_before
378 382 move_to_left_of(to_be_inserted_before)
379 383 elsif p.nil?
380 384 if sibs.empty?
381 385 # move_to_root adds the project in first (ie. left) position
382 386 move_to_root
383 387 else
384 388 move_to_right_of(sibs.last) unless self == sibs.last
385 389 end
386 390 else
387 391 # move_to_child_of adds the project in last (ie.right) position
388 392 move_to_child_of(p)
389 393 end
390 394 Issue.update_versions_from_hierarchy_change(self)
391 395 true
392 396 else
393 397 # Can not move to the given target
394 398 false
395 399 end
396 400 end
397 401
398 402 # Returns an array of the trackers used by the project and its active sub projects
399 403 def rolled_up_trackers
400 404 @rolled_up_trackers ||=
401 405 Tracker.find(:all, :joins => :projects,
402 406 :select => "DISTINCT #{Tracker.table_name}.*",
403 407 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
404 408 :order => "#{Tracker.table_name}.position")
405 409 end
406 410
407 411 # Closes open and locked project versions that are completed
408 412 def close_completed_versions
409 413 Version.transaction do
410 414 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
411 415 if version.completed?
412 416 version.update_attribute(:status, 'closed')
413 417 end
414 418 end
415 419 end
416 420 end
417 421
418 422 # Returns a scope of the Versions on subprojects
419 423 def rolled_up_versions
420 424 @rolled_up_versions ||=
421 425 Version.scoped(:include => :project,
422 426 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
423 427 end
424 428
425 429 # Returns a scope of the Versions used by the project
426 430 def shared_versions
427 431 if new_record?
428 432 Version.scoped(:include => :project,
429 433 :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
430 434 else
431 435 @shared_versions ||= begin
432 436 r = root? ? self : root
433 437 Version.scoped(:include => :project,
434 438 :conditions => "#{Project.table_name}.id = #{id}" +
435 439 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
436 440 " #{Version.table_name}.sharing = 'system'" +
437 441 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
438 442 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
439 443 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
440 444 "))")
441 445 end
442 446 end
443 447 end
444 448
445 449 # Returns a hash of project users grouped by role
446 450 def users_by_role
447 451 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
448 452 m.roles.each do |r|
449 453 h[r] ||= []
450 454 h[r] << m.user
451 455 end
452 456 h
453 457 end
454 458 end
455 459
456 460 # Deletes all project's members
457 461 def delete_all_members
458 462 me, mr = Member.table_name, MemberRole.table_name
459 463 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
460 464 Member.delete_all(['project_id = ?', id])
461 465 end
462 466
463 467 # Users/groups issues can be assigned to
464 468 def assignable_users
465 469 assignable = Setting.issue_group_assignment? ? member_principals : members
466 470 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
467 471 end
468 472
469 473 # Returns the mail adresses of users that should be always notified on project events
470 474 def recipients
471 475 notified_users.collect {|user| user.mail}
472 476 end
473 477
474 478 # Returns the users that should be notified on project events
475 479 def notified_users
476 480 # TODO: User part should be extracted to User#notify_about?
477 481 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
478 482 end
479 483
480 484 # Returns an array of all custom fields enabled for project issues
481 485 # (explictly associated custom fields and custom fields enabled for all projects)
482 486 def all_issue_custom_fields
483 487 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
484 488 end
485 489
486 490 # Returns an array of all custom fields enabled for project time entries
487 491 # (explictly associated custom fields and custom fields enabled for all projects)
488 492 def all_time_entry_custom_fields
489 493 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
490 494 end
491 495
492 496 def project
493 497 self
494 498 end
495 499
496 500 def <=>(project)
497 501 name.downcase <=> project.name.downcase
498 502 end
499 503
500 504 def to_s
501 505 name
502 506 end
503 507
504 508 # Returns a short description of the projects (first lines)
505 509 def short_description(length = 255)
506 510 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
507 511 end
508 512
509 513 def css_classes
510 514 s = 'project'
511 515 s << ' root' if root?
512 516 s << ' child' if child?
513 517 s << (leaf? ? ' leaf' : ' parent')
514 518 s
515 519 end
516 520
517 521 # The earliest start date of a project, based on it's issues and versions
518 522 def start_date
519 523 [
520 524 issues.minimum('start_date'),
521 525 shared_versions.collect(&:effective_date),
522 526 shared_versions.collect(&:start_date)
523 527 ].flatten.compact.min
524 528 end
525 529
526 530 # The latest due date of an issue or version
527 531 def due_date
528 532 [
529 533 issues.maximum('due_date'),
530 534 shared_versions.collect(&:effective_date),
531 535 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
532 536 ].flatten.compact.max
533 537 end
534 538
535 539 def overdue?
536 540 active? && !due_date.nil? && (due_date < Date.today)
537 541 end
538 542
539 543 # Returns the percent completed for this project, based on the
540 544 # progress on it's versions.
541 545 def completed_percent(options={:include_subprojects => false})
542 546 if options.delete(:include_subprojects)
543 547 total = self_and_descendants.collect(&:completed_percent).sum
544 548
545 549 total / self_and_descendants.count
546 550 else
547 551 if versions.count > 0
548 552 total = versions.collect(&:completed_pourcent).sum
549 553
550 554 total / versions.count
551 555 else
552 556 100
553 557 end
554 558 end
555 559 end
556 560
557 561 # Return true if this project is allowed to do the specified action.
558 562 # action can be:
559 563 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
560 564 # * a permission Symbol (eg. :edit_project)
561 565 def allows_to?(action)
562 566 if action.is_a? Hash
563 567 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
564 568 else
565 569 allowed_permissions.include? action
566 570 end
567 571 end
568 572
569 573 def module_enabled?(module_name)
570 574 module_name = module_name.to_s
571 575 enabled_modules.detect {|m| m.name == module_name}
572 576 end
573 577
574 578 def enabled_module_names=(module_names)
575 579 if module_names && module_names.is_a?(Array)
576 580 module_names = module_names.collect(&:to_s).reject(&:blank?)
577 581 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
578 582 else
579 583 enabled_modules.clear
580 584 end
581 585 end
582 586
583 587 # Returns an array of the enabled modules names
584 588 def enabled_module_names
585 589 enabled_modules.collect(&:name)
586 590 end
587 591
588 592 # Enable a specific module
589 593 #
590 594 # Examples:
591 595 # project.enable_module!(:issue_tracking)
592 596 # project.enable_module!("issue_tracking")
593 597 def enable_module!(name)
594 598 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
595 599 end
596 600
597 601 # Disable a module if it exists
598 602 #
599 603 # Examples:
600 604 # project.disable_module!(:issue_tracking)
601 605 # project.disable_module!("issue_tracking")
602 606 # project.disable_module!(project.enabled_modules.first)
603 607 def disable_module!(target)
604 608 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
605 609 target.destroy unless target.blank?
606 610 end
607 611
608 612 safe_attributes 'name',
609 613 'description',
610 614 'homepage',
611 615 'is_public',
612 616 'identifier',
613 617 'custom_field_values',
614 618 'custom_fields',
615 619 'tracker_ids',
616 620 'issue_custom_field_ids'
617 621
618 622 safe_attributes 'enabled_module_names',
619 623 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
620 624
621 625 # Returns an array of projects that are in this project's hierarchy
622 626 #
623 627 # Example: parents, children, siblings
624 628 def hierarchy
625 629 parents = project.self_and_ancestors || []
626 630 descendants = project.descendants || []
627 631 project_hierarchy = parents | descendants # Set union
628 632 end
629 633
630 634 # Returns an auto-generated project identifier based on the last identifier used
631 635 def self.next_identifier
632 636 p = Project.find(:first, :order => 'created_on DESC')
633 637 p.nil? ? nil : p.identifier.to_s.succ
634 638 end
635 639
636 640 # Copies and saves the Project instance based on the +project+.
637 641 # Duplicates the source project's:
638 642 # * Wiki
639 643 # * Versions
640 644 # * Categories
641 645 # * Issues
642 646 # * Members
643 647 # * Queries
644 648 #
645 649 # Accepts an +options+ argument to specify what to copy
646 650 #
647 651 # Examples:
648 652 # project.copy(1) # => copies everything
649 653 # project.copy(1, :only => 'members') # => copies members only
650 654 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
651 655 def copy(project, options={})
652 656 project = project.is_a?(Project) ? project : Project.find(project)
653 657
654 658 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
655 659 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
656 660
657 661 Project.transaction do
658 662 if save
659 663 reload
660 664 to_be_copied.each do |name|
661 665 send "copy_#{name}", project
662 666 end
663 667 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
664 668 save
665 669 end
666 670 end
667 671 end
668 672
669 673
670 674 # Copies +project+ and returns the new instance. This will not save
671 675 # the copy
672 676 def self.copy_from(project)
673 677 begin
674 678 project = project.is_a?(Project) ? project : Project.find(project)
675 679 if project
676 680 # clear unique attributes
677 681 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
678 682 copy = Project.new(attributes)
679 683 copy.enabled_modules = project.enabled_modules
680 684 copy.trackers = project.trackers
681 685 copy.custom_values = project.custom_values.collect {|v| v.clone}
682 686 copy.issue_custom_fields = project.issue_custom_fields
683 687 return copy
684 688 else
685 689 return nil
686 690 end
687 691 rescue ActiveRecord::RecordNotFound
688 692 return nil
689 693 end
690 694 end
691 695
692 696 # Yields the given block for each project with its level in the tree
693 697 def self.project_tree(projects, &block)
694 698 ancestors = []
695 699 projects.sort_by(&:lft).each do |project|
696 700 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
697 701 ancestors.pop
698 702 end
699 703 yield project, ancestors.size
700 704 ancestors << project
701 705 end
702 706 end
703 707
704 708 private
705 709
706 710 # Copies wiki from +project+
707 711 def copy_wiki(project)
708 712 # Check that the source project has a wiki first
709 713 unless project.wiki.nil?
710 714 self.wiki ||= Wiki.new
711 715 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
712 716 wiki_pages_map = {}
713 717 project.wiki.pages.each do |page|
714 718 # Skip pages without content
715 719 next if page.content.nil?
716 720 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
717 721 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
718 722 new_wiki_page.content = new_wiki_content
719 723 wiki.pages << new_wiki_page
720 724 wiki_pages_map[page.id] = new_wiki_page
721 725 end
722 726 wiki.save
723 727 # Reproduce page hierarchy
724 728 project.wiki.pages.each do |page|
725 729 if page.parent_id && wiki_pages_map[page.id]
726 730 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
727 731 wiki_pages_map[page.id].save
728 732 end
729 733 end
730 734 end
731 735 end
732 736
733 737 # Copies versions from +project+
734 738 def copy_versions(project)
735 739 project.versions.each do |version|
736 740 new_version = Version.new
737 741 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
738 742 self.versions << new_version
739 743 end
740 744 end
741 745
742 746 # Copies issue categories from +project+
743 747 def copy_issue_categories(project)
744 748 project.issue_categories.each do |issue_category|
745 749 new_issue_category = IssueCategory.new
746 750 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
747 751 self.issue_categories << new_issue_category
748 752 end
749 753 end
750 754
751 755 # Copies issues from +project+
752 756 # Note: issues assigned to a closed version won't be copied due to validation rules
753 757 def copy_issues(project)
754 758 # Stores the source issue id as a key and the copied issues as the
755 759 # value. Used to map the two togeather for issue relations.
756 760 issues_map = {}
757 761
758 762 # Get issues sorted by root_id, lft so that parent issues
759 763 # get copied before their children
760 764 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
761 765 new_issue = Issue.new
762 766 new_issue.copy_from(issue)
763 767 new_issue.project = self
764 768 # Reassign fixed_versions by name, since names are unique per
765 769 # project and the versions for self are not yet saved
766 770 if issue.fixed_version
767 771 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
768 772 end
769 773 # Reassign the category by name, since names are unique per
770 774 # project and the categories for self are not yet saved
771 775 if issue.category
772 776 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
773 777 end
774 778 # Parent issue
775 779 if issue.parent_id
776 780 if copied_parent = issues_map[issue.parent_id]
777 781 new_issue.parent_issue_id = copied_parent.id
778 782 end
779 783 end
780 784
781 785 self.issues << new_issue
782 786 if new_issue.new_record?
783 787 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
784 788 else
785 789 issues_map[issue.id] = new_issue unless new_issue.new_record?
786 790 end
787 791 end
788 792
789 793 # Relations after in case issues related each other
790 794 project.issues.each do |issue|
791 795 new_issue = issues_map[issue.id]
792 796 unless new_issue
793 797 # Issue was not copied
794 798 next
795 799 end
796 800
797 801 # Relations
798 802 issue.relations_from.each do |source_relation|
799 803 new_issue_relation = IssueRelation.new
800 804 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
801 805 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
802 806 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
803 807 new_issue_relation.issue_to = source_relation.issue_to
804 808 end
805 809 new_issue.relations_from << new_issue_relation
806 810 end
807 811
808 812 issue.relations_to.each do |source_relation|
809 813 new_issue_relation = IssueRelation.new
810 814 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
811 815 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
812 816 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
813 817 new_issue_relation.issue_from = source_relation.issue_from
814 818 end
815 819 new_issue.relations_to << new_issue_relation
816 820 end
817 821 end
818 822 end
819 823
820 824 # Copies members from +project+
821 825 def copy_members(project)
822 826 # Copy users first, then groups to handle members with inherited and given roles
823 827 members_to_copy = []
824 828 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
825 829 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
826 830
827 831 members_to_copy.each do |member|
828 832 new_member = Member.new
829 833 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
830 834 # only copy non inherited roles
831 835 # inherited roles will be added when copying the group membership
832 836 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
833 837 next if role_ids.empty?
834 838 new_member.role_ids = role_ids
835 839 new_member.project = self
836 840 self.members << new_member
837 841 end
838 842 end
839 843
840 844 # Copies queries from +project+
841 845 def copy_queries(project)
842 846 project.queries.each do |query|
843 847 new_query = ::Query.new
844 848 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
845 849 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
846 850 new_query.project = self
847 851 new_query.user_id = query.user_id
848 852 self.queries << new_query
849 853 end
850 854 end
851 855
852 856 # Copies boards from +project+
853 857 def copy_boards(project)
854 858 project.boards.each do |board|
855 859 new_board = Board.new
856 860 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
857 861 new_board.project = self
858 862 self.boards << new_board
859 863 end
860 864 end
861 865
862 866 def allowed_permissions
863 867 @allowed_permissions ||= begin
864 868 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
865 869 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
866 870 end
867 871 end
868 872
869 873 def allowed_actions
870 874 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
871 875 end
872 876
873 877 # Returns all the active Systemwide and project specific activities
874 878 def active_activities
875 879 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
876 880
877 881 if overridden_activity_ids.empty?
878 882 return TimeEntryActivity.shared.active
879 883 else
880 884 return system_activities_and_project_overrides
881 885 end
882 886 end
883 887
884 888 # Returns all the Systemwide and project specific activities
885 889 # (inactive and active)
886 890 def all_activities
887 891 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
888 892
889 893 if overridden_activity_ids.empty?
890 894 return TimeEntryActivity.shared
891 895 else
892 896 return system_activities_and_project_overrides(true)
893 897 end
894 898 end
895 899
896 900 # Returns the systemwide active activities merged with the project specific overrides
897 901 def system_activities_and_project_overrides(include_inactive=false)
898 902 if include_inactive
899 903 return TimeEntryActivity.shared.
900 904 find(:all,
901 905 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
902 906 self.time_entry_activities
903 907 else
904 908 return TimeEntryActivity.shared.active.
905 909 find(:all,
906 910 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
907 911 self.time_entry_activities.active
908 912 end
909 913 end
910 914
911 915 # Archives subprojects recursively
912 916 def archive!
913 917 children.each do |subproject|
914 918 subproject.send :archive!
915 919 end
916 920 update_attribute :status, STATUS_ARCHIVED
917 921 end
918 922 end
@@ -1,419 +1,419
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ScmFetchError < Exception; end
19 19
20 20 class Repository < ActiveRecord::Base
21 21 include Redmine::Ciphering
22 22
23 23 belongs_to :project
24 24 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
25 25 has_many :changes, :through => :changesets
26 26
27 27 serialize :extra_info
28 28
29 29 before_save :check_default
30 30
31 31 # Raw SQL to delete changesets and changes in the database
32 32 # has_many :changesets, :dependent => :destroy is too slow for big repositories
33 33 before_destroy :clear_changesets
34 34
35 35 validates_length_of :password, :maximum => 255, :allow_nil => true
36 36 validates_length_of :identifier, :maximum => 255, :allow_blank => true
37 37 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
38 38 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
39 39 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
40 40 # donwcase letters, digits, dashes but not digits only
41 41 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
42 42 # Checks if the SCM is enabled when creating a repository
43 43 validate :repo_create_validation, :on => :create
44 44
45 45 def repo_create_validation
46 46 unless Setting.enabled_scm.include?(self.class.name.demodulize)
47 47 errors.add(:type, :invalid)
48 48 end
49 49 end
50 50
51 51 def self.human_attribute_name(attribute_key_name, *args)
52 52 attr_name = attribute_key_name.to_s
53 53 if attr_name == "log_encoding"
54 54 attr_name = "commit_logs_encoding"
55 55 end
56 56 super(attr_name, *args)
57 57 end
58 58
59 59 alias :attributes_without_extra_info= :attributes=
60 def attributes=(new_attributes, guard_protected_attributes = true)
60 def attributes=(new_attributes)
61 61 return if new_attributes.nil?
62 62 attributes = new_attributes.dup
63 63 attributes.stringify_keys!
64 64
65 65 p = {}
66 66 p_extra = {}
67 67 attributes.each do |k, v|
68 68 if k =~ /^extra_/
69 69 p_extra[k] = v
70 70 else
71 71 p[k] = v
72 72 end
73 73 end
74 74
75 send :attributes_without_extra_info=, p, guard_protected_attributes
75 send :attributes_without_extra_info=, p
76 76 if p_extra.keys.any?
77 77 merge_extra_info(p_extra)
78 78 end
79 79 end
80 80
81 81 # Removes leading and trailing whitespace
82 82 def url=(arg)
83 83 write_attribute(:url, arg ? arg.to_s.strip : nil)
84 84 end
85 85
86 86 # Removes leading and trailing whitespace
87 87 def root_url=(arg)
88 88 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
89 89 end
90 90
91 91 def password
92 92 read_ciphered_attribute(:password)
93 93 end
94 94
95 95 def password=(arg)
96 96 write_ciphered_attribute(:password, arg)
97 97 end
98 98
99 99 def scm_adapter
100 100 self.class.scm_adapter_class
101 101 end
102 102
103 103 def scm
104 104 unless @scm
105 105 @scm = self.scm_adapter.new(url, root_url,
106 106 login, password, path_encoding)
107 107 if root_url.blank? && @scm.root_url.present?
108 108 update_attribute(:root_url, @scm.root_url)
109 109 end
110 110 end
111 111 @scm
112 112 end
113 113
114 114 def scm_name
115 115 self.class.scm_name
116 116 end
117 117
118 118 def name
119 119 if identifier.present?
120 120 identifier
121 121 elsif is_default?
122 122 l(:field_repository_is_default)
123 123 else
124 124 scm_name
125 125 end
126 126 end
127 127
128 128 def identifier_param
129 129 if is_default?
130 130 nil
131 131 elsif identifier.present?
132 132 identifier
133 133 else
134 134 id.to_s
135 135 end
136 136 end
137 137
138 138 def <=>(repository)
139 139 if is_default?
140 140 -1
141 141 elsif repository.is_default?
142 142 1
143 143 else
144 144 identifier <=> repository.identifier
145 145 end
146 146 end
147 147
148 148 def self.find_by_identifier_param(param)
149 149 if param.to_s =~ /^\d+$/
150 150 find_by_id(param)
151 151 else
152 152 find_by_identifier(param)
153 153 end
154 154 end
155 155
156 156 def merge_extra_info(arg)
157 157 h = extra_info || {}
158 158 return h if arg.nil?
159 159 h.merge!(arg)
160 160 write_attribute(:extra_info, h)
161 161 end
162 162
163 163 def report_last_commit
164 164 true
165 165 end
166 166
167 167 def supports_cat?
168 168 scm.supports_cat?
169 169 end
170 170
171 171 def supports_annotate?
172 172 scm.supports_annotate?
173 173 end
174 174
175 175 def supports_all_revisions?
176 176 true
177 177 end
178 178
179 179 def supports_directory_revisions?
180 180 false
181 181 end
182 182
183 183 def supports_revision_graph?
184 184 false
185 185 end
186 186
187 187 def entry(path=nil, identifier=nil)
188 188 scm.entry(path, identifier)
189 189 end
190 190
191 191 def entries(path=nil, identifier=nil)
192 192 scm.entries(path, identifier)
193 193 end
194 194
195 195 def branches
196 196 scm.branches
197 197 end
198 198
199 199 def tags
200 200 scm.tags
201 201 end
202 202
203 203 def default_branch
204 204 nil
205 205 end
206 206
207 207 def properties(path, identifier=nil)
208 208 scm.properties(path, identifier)
209 209 end
210 210
211 211 def cat(path, identifier=nil)
212 212 scm.cat(path, identifier)
213 213 end
214 214
215 215 def diff(path, rev, rev_to)
216 216 scm.diff(path, rev, rev_to)
217 217 end
218 218
219 219 def diff_format_revisions(cs, cs_to, sep=':')
220 220 text = ""
221 221 text << cs_to.format_identifier + sep if cs_to
222 222 text << cs.format_identifier if cs
223 223 text
224 224 end
225 225
226 226 # Returns a path relative to the url of the repository
227 227 def relative_path(path)
228 228 path
229 229 end
230 230
231 231 # Finds and returns a revision with a number or the beginning of a hash
232 232 def find_changeset_by_name(name)
233 233 return nil if name.blank?
234 234 s = name.to_s
235 235 changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
236 236 ["revision = ?", s] : ["revision LIKE ?", s + '%']))
237 237 end
238 238
239 239 def latest_changeset
240 240 @latest_changeset ||= changesets.find(:first)
241 241 end
242 242
243 243 # Returns the latest changesets for +path+
244 244 # Default behaviour is to search in cached changesets
245 245 def latest_changesets(path, rev, limit=10)
246 246 if path.blank?
247 247 changesets.find(
248 248 :all,
249 249 :include => :user,
250 250 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
251 251 :limit => limit)
252 252 else
253 253 changes.find(
254 254 :all,
255 255 :include => {:changeset => :user},
256 256 :conditions => ["path = ?", path.with_leading_slash],
257 257 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
258 258 :limit => limit
259 259 ).collect(&:changeset)
260 260 end
261 261 end
262 262
263 263 def scan_changesets_for_issue_ids
264 264 self.changesets.each(&:scan_comment_for_issue_ids)
265 265 end
266 266
267 267 # Returns an array of committers usernames and associated user_id
268 268 def committers
269 269 @committers ||= Changeset.connection.select_rows(
270 270 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
271 271 end
272 272
273 273 # Maps committers username to a user ids
274 274 def committer_ids=(h)
275 275 if h.is_a?(Hash)
276 276 committers.each do |committer, user_id|
277 277 new_user_id = h[committer]
278 278 if new_user_id && (new_user_id.to_i != user_id.to_i)
279 279 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
280 280 Changeset.update_all(
281 281 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
282 282 ["repository_id = ? AND committer = ?", id, committer])
283 283 end
284 284 end
285 285 @committers = nil
286 286 @found_committer_users = nil
287 287 true
288 288 else
289 289 false
290 290 end
291 291 end
292 292
293 293 # Returns the Redmine User corresponding to the given +committer+
294 294 # It will return nil if the committer is not yet mapped and if no User
295 295 # with the same username or email was found
296 296 def find_committer_user(committer)
297 297 unless committer.blank?
298 298 @found_committer_users ||= {}
299 299 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
300 300
301 301 user = nil
302 302 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
303 303 if c && c.user
304 304 user = c.user
305 305 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
306 306 username, email = $1.strip, $3
307 307 u = User.find_by_login(username)
308 308 u ||= User.find_by_mail(email) unless email.blank?
309 309 user = u
310 310 end
311 311 @found_committer_users[committer] = user
312 312 user
313 313 end
314 314 end
315 315
316 316 def repo_log_encoding
317 317 encoding = log_encoding.to_s.strip
318 318 encoding.blank? ? 'UTF-8' : encoding
319 319 end
320 320
321 321 # Fetches new changesets for all repositories of active projects
322 322 # Can be called periodically by an external script
323 323 # eg. ruby script/runner "Repository.fetch_changesets"
324 324 def self.fetch_changesets
325 325 Project.active.has_module(:repository).all.each do |project|
326 326 project.repositories.each do |repository|
327 327 begin
328 328 repository.fetch_changesets
329 329 rescue Redmine::Scm::Adapters::CommandFailed => e
330 330 logger.error "scm: error during fetching changesets: #{e.message}"
331 331 end
332 332 end
333 333 end
334 334 end
335 335
336 336 # scan changeset comments to find related and fixed issues for all repositories
337 337 def self.scan_changesets_for_issue_ids
338 338 find(:all).each(&:scan_changesets_for_issue_ids)
339 339 end
340 340
341 341 def self.scm_name
342 342 'Abstract'
343 343 end
344 344
345 345 def self.available_scm
346 346 subclasses.collect {|klass| [klass.scm_name, klass.name]}
347 347 end
348 348
349 349 def self.factory(klass_name, *args)
350 350 klass = "Repository::#{klass_name}".constantize
351 351 klass.new(*args)
352 352 rescue
353 353 nil
354 354 end
355 355
356 356 def self.scm_adapter_class
357 357 nil
358 358 end
359 359
360 360 def self.scm_command
361 361 ret = ""
362 362 begin
363 363 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
364 364 rescue Exception => e
365 365 logger.error "scm: error during get command: #{e.message}"
366 366 end
367 367 ret
368 368 end
369 369
370 370 def self.scm_version_string
371 371 ret = ""
372 372 begin
373 373 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
374 374 rescue Exception => e
375 375 logger.error "scm: error during get version string: #{e.message}"
376 376 end
377 377 ret
378 378 end
379 379
380 380 def self.scm_available
381 381 ret = false
382 382 begin
383 383 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
384 384 rescue Exception => e
385 385 logger.error "scm: error during get scm available: #{e.message}"
386 386 end
387 387 ret
388 388 end
389 389
390 390 def set_as_default?
391 391 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
392 392 end
393 393
394 394 protected
395 395
396 396 def check_default
397 397 if !is_default? && set_as_default?
398 398 self.is_default = true
399 399 end
400 400 if is_default? && is_default_changed?
401 401 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
402 402 end
403 403 end
404 404
405 405 private
406 406
407 407 # Deletes repository data
408 408 def clear_changesets
409 409 cs = Changeset.table_name
410 410 ch = Change.table_name
411 411 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
412 412 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
413 413
414 414 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
415 415 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
416 416 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
417 417 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
418 418 end
419 419 end
@@ -1,177 +1,177
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Role < ActiveRecord::Base
19 19 # Built-in roles
20 20 BUILTIN_NON_MEMBER = 1
21 21 BUILTIN_ANONYMOUS = 2
22 22
23 23 ISSUES_VISIBILITY_OPTIONS = [
24 24 ['all', :label_issues_visibility_all],
25 25 ['default', :label_issues_visibility_public],
26 26 ['own', :label_issues_visibility_own]
27 27 ]
28 28
29 29 named_scope :sorted, {:order => 'builtin, position'}
30 30 named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
31 31 named_scope :builtin, lambda { |*args|
32 32 compare = 'not' if args.first == true
33 33 { :conditions => "#{compare} builtin = 0" }
34 34 }
35 35
36 36 before_destroy :check_deletable
37 37 has_many :workflows, :dependent => :delete_all do
38 38 def copy(source_role)
39 Workflow.copy(nil, source_role, nil, proxy_owner)
39 Workflow.copy(nil, source_role, nil, proxy_association.owner)
40 40 end
41 41 end
42 42
43 43 has_many :member_roles, :dependent => :destroy
44 44 has_many :members, :through => :member_roles
45 45 acts_as_list
46 46
47 47 serialize :permissions, Array
48 48 attr_protected :builtin
49 49
50 50 validates_presence_of :name
51 51 validates_uniqueness_of :name
52 52 validates_length_of :name, :maximum => 30
53 53 validates_inclusion_of :issues_visibility,
54 54 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
55 55 :if => lambda {|role| role.respond_to?(:issues_visibility)}
56 56
57 57 def permissions
58 58 read_attribute(:permissions) || []
59 59 end
60 60
61 61 def permissions=(perms)
62 62 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
63 63 write_attribute(:permissions, perms)
64 64 end
65 65
66 66 def add_permission!(*perms)
67 67 self.permissions = [] unless permissions.is_a?(Array)
68 68
69 69 permissions_will_change!
70 70 perms.each do |p|
71 71 p = p.to_sym
72 72 permissions << p unless permissions.include?(p)
73 73 end
74 74 save!
75 75 end
76 76
77 77 def remove_permission!(*perms)
78 78 return unless permissions.is_a?(Array)
79 79 permissions_will_change!
80 80 perms.each { |p| permissions.delete(p.to_sym) }
81 81 save!
82 82 end
83 83
84 84 # Returns true if the role has the given permission
85 85 def has_permission?(perm)
86 86 !permissions.nil? && permissions.include?(perm.to_sym)
87 87 end
88 88
89 89 def <=>(role)
90 90 role ? position <=> role.position : -1
91 91 end
92 92
93 93 def to_s
94 94 name
95 95 end
96 96
97 97 def name
98 98 case builtin
99 99 when 1; l(:label_role_non_member, :default => read_attribute(:name))
100 100 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
101 101 else; read_attribute(:name)
102 102 end
103 103 end
104 104
105 105 # Return true if the role is a builtin role
106 106 def builtin?
107 107 self.builtin != 0
108 108 end
109 109
110 110 # Return true if the role is a project member role
111 111 def member?
112 112 !self.builtin?
113 113 end
114 114
115 115 # Return true if role is allowed to do the specified action
116 116 # action can be:
117 117 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
118 118 # * a permission Symbol (eg. :edit_project)
119 119 def allowed_to?(action)
120 120 if action.is_a? Hash
121 121 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
122 122 else
123 123 allowed_permissions.include? action
124 124 end
125 125 end
126 126
127 127 # Return all the permissions that can be given to the role
128 128 def setable_permissions
129 129 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
130 130 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
131 131 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
132 132 setable_permissions
133 133 end
134 134
135 135 # Find all the roles that can be given to a project member
136 136 def self.find_all_givable
137 137 find(:all, :conditions => {:builtin => 0}, :order => 'position')
138 138 end
139 139
140 140 # Return the builtin 'non member' role. If the role doesn't exist,
141 141 # it will be created on the fly.
142 142 def self.non_member
143 143 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
144 144 end
145 145
146 146 # Return the builtin 'anonymous' role. If the role doesn't exist,
147 147 # it will be created on the fly.
148 148 def self.anonymous
149 149 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
150 150 end
151 151
152 152 private
153 153
154 154 def allowed_permissions
155 155 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
156 156 end
157 157
158 158 def allowed_actions
159 159 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
160 160 end
161 161
162 162 def check_deletable
163 163 raise "Can't delete role" if members.any?
164 164 raise "Can't delete builtin role" if builtin?
165 165 end
166 166
167 167 def self.find_or_create_system_role(builtin, name)
168 168 role = first(:conditions => {:builtin => builtin})
169 169 if role.nil?
170 170 role = create(:name => name, :position => 0) do |r|
171 171 r.builtin = builtin
172 172 end
173 173 raise "Unable to create the #{name} role." if role.new_record?
174 174 end
175 175 role
176 176 end
177 177 end
@@ -1,68 +1,68
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Tracker < ActiveRecord::Base
19 19 before_destroy :check_integrity
20 20 has_many :issues
21 21 has_many :workflows, :dependent => :delete_all do
22 22 def copy(source_tracker)
23 Workflow.copy(source_tracker, nil, proxy_owner, nil)
23 Workflow.copy(source_tracker, nil, proxy_association.owner, nil)
24 24 end
25 25 end
26 26
27 27 has_and_belongs_to_many :projects
28 28 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
29 29 acts_as_list
30 30
31 31 validates_presence_of :name
32 32 validates_uniqueness_of :name
33 33 validates_length_of :name, :maximum => 30
34 34
35 35 named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
36 36
37 37 def to_s; name end
38 38
39 39 def <=>(tracker)
40 40 name <=> tracker.name
41 41 end
42 42
43 43 def self.all
44 44 find(:all, :order => 'position')
45 45 end
46 46
47 47 # Returns an array of IssueStatus that are used
48 48 # in the tracker's workflows
49 49 def issue_statuses
50 50 if @issue_statuses
51 51 return @issue_statuses
52 52 elsif new_record?
53 53 return []
54 54 end
55 55
56 56 ids = Workflow.
57 57 connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
58 58 flatten.
59 59 uniq
60 60
61 61 @issue_statuses = IssueStatus.find_all_by_id(ids).sort
62 62 end
63 63
64 64 private
65 65 def check_integrity
66 66 raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
67 67 end
68 68 end
@@ -1,42 +1,42
1 1 <%= call_hook :view_account_login_top %>
2 2 <div id="login-form">
3 <% form_tag({:action=> "login"}) do %>
3 <%= form_tag({:action=> "login"}) do %>
4 4 <%= back_url_hidden_field_tag %>
5 5 <table>
6 6 <tr>
7 7 <td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
8 8 <td align="left"><%= text_field_tag 'username', nil, :tabindex => '1' %></td>
9 9 </tr>
10 10 <tr>
11 11 <td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
12 12 <td align="left"><%= password_field_tag 'password', nil, :tabindex => '2' %></td>
13 13 </tr>
14 14 <% if Setting.openid? %>
15 15 <tr>
16 16 <td align="right"><label for="openid_url"><%=l(:field_identity_url)%></label></td>
17 17 <td align="left"><%= text_field_tag "openid_url", nil, :tabindex => '3' %></td>
18 18 </tr>
19 19 <% end %>
20 20 <tr>
21 21 <td></td>
22 22 <td align="left">
23 23 <% if Setting.autologin? %>
24 24 <label for="autologin"><%= check_box_tag 'autologin', 1, false, :tabindex => 4 %> <%= l(:label_stay_logged_in) %></label>
25 25 <% end %>
26 26 </td>
27 27 </tr>
28 28 <tr>
29 29 <td align="left">
30 30 <% if Setting.lost_password? %>
31 31 <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %>
32 32 <% end %>
33 33 </td>
34 34 <td align="right">
35 35 <input type="submit" name="login" value="<%=l(:button_login)%> &#187;" tabindex="5"/>
36 36 </td>
37 37 </tr>
38 38 </table>
39 39 <%= javascript_tag "Form.Element.focus('username');" %>
40 40 <% end %>
41 41 </div>
42 42 <%= call_hook :view_account_login_bottom %>
@@ -1,11 +1,11
1 1 <h2><%=l(:label_password_lost)%></h2>
2 2
3 3 <div class="box">
4 <% form_tag({:action=> "lost_password"}, :class => "tabular") do %>
4 <%= form_tag({:action=> "lost_password"}, :class => "tabular") do %>
5 5
6 6 <p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
7 7 <%= text_field_tag 'mail', nil, :size => 40 %>
8 8 <%= submit_tag l(:button_submit) %></p>
9 9
10 10 <% end %>
11 11 </div>
@@ -1,15 +1,15
1 1 <h2><%=l(:label_password_lost)%></h2>
2 2
3 3 <%= error_messages_for 'user' %>
4 4
5 <% form_tag({:token => @token.value}) do %>
5 <%= form_tag({:token => @token.value}) do %>
6 6 <div class="box tabular">
7 7 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
8 8 <%= password_field_tag 'new_password', nil, :size => 25 %>
9 9 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
10 10
11 11 <p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
12 12 <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
13 13 </div>
14 14 <p><%= submit_tag l(:button_save) %></p>
15 15 <% end %>
@@ -1,31 +1,31
1 1 <h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
2 2
3 <% labelled_form_for @user, :url => {:action => 'register'} do |f| %>
3 <%= labelled_form_for @user, :url => {:action => 'register'} do |f| %>
4 4 <%= error_messages_for 'user' %>
5 5
6 6 <div class="box tabular">
7 7 <% if @user.auth_source_id.nil? %>
8 8 <p><%= f.text_field :login, :size => 25, :required => true %></p>
9 9
10 10 <p><%= f.password_field :password, :size => 25, :required => true %>
11 11 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
12 12
13 13 <p><%= f.password_field :password_confirmation, :size => 25, :required => true %></p>
14 14 <% end %>
15 15
16 16 <p><%= f.text_field :firstname, :required => true %></p>
17 17 <p><%= f.text_field :lastname, :required => true %></p>
18 18 <p><%= f.text_field :mail, :required => true %></p>
19 19 <p><%= f.select :language, lang_options_for_select %></p>
20 20
21 21 <% if Setting.openid? %>
22 22 <p><%= f.text_field :identity_url %></p>
23 23 <% end %>
24 24
25 25 <% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %>
26 26 <p><%= custom_field_tag_with_label :user, value %></p>
27 27 <% end %>
28 28 </div>
29 29
30 30 <%= submit_tag l(:button_submit) %>
31 31 <% end %>
@@ -1,59 +1,59
1 1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
2 2 <p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
3 3
4 4 <div id="activity">
5 5 <% @events_by_day.keys.sort.reverse.each do |day| %>
6 6 <h3><%= format_activity_day(day) %></h3>
7 7 <dl>
8 8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
9 9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
10 10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
11 11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
12 12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
13 13 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
14 14 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
15 15 <span class="author"><%= link_to_user(e.event_author) if e.respond_to?(:event_author) %></span></dd>
16 16 <% end -%>
17 17 </dl>
18 18 <% end -%>
19 19 </div>
20 20
21 21 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
22 22
23 23 <div style="float:left;">
24 24 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
25 25 params.merge(:from => @date_to - @days - 1),
26 26 :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))) %>
27 27 </div>
28 28 <div style="float:right;">
29 29 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
30 30 params.merge(:from => @date_to + @days - 1),
31 31 :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))) unless @date_to >= Date.today %>
32 32 </div>
33 33 &nbsp;
34 34 <% other_formats_links do |f| %>
35 35 <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %>
36 36 <% end %>
37 37
38 38 <% content_for :header_tags do %>
39 39 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
40 40 <% end %>
41 41
42 42 <% content_for :sidebar do %>
43 <% form_tag({}, :method => :get) do %>
43 <%= form_tag({}, :method => :get) do %>
44 44 <h3><%= l(:label_activity) %></h3>
45 45 <p><% @activity.event_types.each do |t| %>
46 46 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
47 47 <label for="show_<%=t%>"><%= link_to(l("label_#{t.singularize}_plural"), {"show_#{t}" => 1, :user_id => params[:user_id]})%></label>
48 48 <br />
49 49 <% end %></p>
50 50 <% if @project && @project.descendants.active.any? %>
51 51 <%= hidden_field_tag 'with_subprojects', 0 %>
52 52 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
53 53 <% end %>
54 54 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
55 55 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
56 56 <% end %>
57 57 <% end %>
58 58
59 59 <% html_title(l(:label_activity), @author) -%>
@@ -1,8 +1,8
1 1 <div class="nodata">
2 <% form_tag({:action => 'default_configuration'}) do %>
2 <%= form_tag({:action => 'default_configuration'}) do %>
3 3 <%= simple_format(l(:text_no_configuration_data)) %>
4 4 <p><%= l(:field_language) %>:
5 5 <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %>
6 6 <%= submit_tag l(:text_load_default_configuration) %></p>
7 7 <% end %>
8 8 </div>
@@ -1,45 +1,45
1 1 <div class="contextual">
2 2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_project_plural)%></h2>
6 6
7 <% form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 9 <label for='status'><%= l(:field_status) %> :</label>
10 10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 11 <label for='name'><%= l(:label_project) %>:</label>
12 12 <%= text_field_tag 'name', params[:name], :size => 30 %>
13 13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
14 14 <%= link_to l(:button_clear), {:controller => 'admin', :action => 'projects'}, :class => 'icon icon-reload' %>
15 15 </fieldset>
16 16 <% end %>
17 17 &nbsp;
18 18
19 19 <div class="autoscroll">
20 20 <table class="list">
21 21 <thead><tr>
22 22 <th><%=l(:label_project)%></th>
23 23 <th><%=l(:field_is_public)%></th>
24 24 <th><%=l(:field_created_on)%></th>
25 25 <th></th>
26 26 </tr></thead>
27 27 <tbody>
28 28 <% project_tree(@projects) do |project, level| %>
29 29 <tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
30 30 <td class="name"><span><%= link_to_project(project, {:action => 'settings'}, :title => project.short_description) %></span></td>
31 31 <td align="center"><%= checked_image project.is_public? %></td>
32 32 <td align="center"><%= format_date(project.created_on) %></td>
33 33 <td class="buttons">
34 34 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
35 35 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
36 36 <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
37 37 <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
38 38 </td>
39 39 </tr>
40 40 <% end %>
41 41 </tbody>
42 42 </table>
43 43 </div>
44 44
45 45 <% html_title(l(:label_project_plural)) -%>
@@ -1,24 +1,24
1 1 <h2><%=h @attachment.filename %></h2>
2 2
3 3 <div class="attachments">
4 4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
5 5 <span class="author"><%= link_to_user(@attachment.author) %>, <%= format_time(@attachment.created_on) %></span></p>
6 6 <p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
7 7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
8 8 </div>
9 9 <p>
10 <% form_tag({}, :method => 'get') do %>
10 <%= form_tag({}, :method => 'get') do %>
11 11 <label><%= l(:label_view_diff) %></label>
12 12 <%= select_tag 'type',
13 13 options_for_select(
14 14 [[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
15 15 :onchange => "if (this.value != '') {this.form.submit()}" %>
16 16 <% end %>
17 17 </p>
18 18 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
19 19
20 20 <% html_title @attachment.filename %>
21 21
22 22 <% content_for :header_tags do -%>
23 23 <%= stylesheet_link_tag "scm" -%>
24 24 <% end -%>
@@ -1,6 +1,6
1 1 <h2><%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
2 2
3 <% form_tag({:action => 'update', :id => @auth_source}, :method => :put, :class => "tabular") do %>
3 <%= form_tag({:action => 'update', :id => @auth_source}, :method => :put, :class => "tabular") do %>
4 4 <%= render :partial => auth_source_partial_name(@auth_source) %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,7 +1,7
1 1 <h2><%=l(:label_auth_source_new)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
2 2
3 <% form_tag({:action => 'create'}, :class => "tabular") do %>
3 <%= form_tag({:action => 'create'}, :class => "tabular") do %>
4 4 <%= hidden_field_tag 'type', @auth_source.type %>
5 5 <%= render :partial => auth_source_partial_name(@auth_source) %>
6 6 <%= submit_tag l(:button_create) %>
7 7 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= l(:label_board) %></h2>
2 2
3 <% labelled_form_for @board, :url => project_board_path(@project, @board) do |f| %>
3 <%= labelled_form_for @board, :url => project_board_path(@project, @board) do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= l(:label_board_new) %></h2>
2 2
3 <% labelled_form_for @board, :url => project_boards_path(@project) do |f| %>
3 <%= labelled_form_for @board, :url => project_boards_path(@project) do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <% end %>
@@ -1,73 +1,73
1 1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)) %>
2 2
3 3 <div class="contextual">
4 4 <%= link_to_if_authorized l(:label_message_new),
5 5 {:controller => 'messages', :action => 'new', :board_id => @board},
6 6 :class => 'icon icon-add',
7 7 :onclick => 'Element.show("add-message"); Form.Element.focus("message_subject"); return false;' %>
8 8 <%= watcher_tag(@board, User.current) %>
9 9 </div>
10 10
11 11 <div id="add-message" style="display:none;">
12 12 <% if authorize_for('messages', 'new') %>
13 13 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
14 <% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
14 <%= form_for @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
15 15 <%= render :partial => 'messages/form', :locals => {:f => f} %>
16 16 <p><%= submit_tag l(:button_create) %>
17 17 <%= link_to_remote l(:label_preview),
18 18 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
19 19 :method => 'post',
20 20 :update => 'preview',
21 21 :with => "Form.serialize('message-form')",
22 22 :complete => "Element.scrollTo('preview')"
23 23 }, :accesskey => accesskey(:preview) %> |
24 24 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
25 25 <% end %>
26 26 <div id="preview" class="wiki"></div>
27 27 <% end %>
28 28 </div>
29 29
30 30 <h2><%=h @board.name %></h2>
31 31 <p class="subtitle"><%=h @board.description %></p>
32 32
33 33 <% if @topics.any? %>
34 34 <table class="list messages">
35 35 <thead><tr>
36 36 <th><%= l(:field_subject) %></th>
37 37 <th><%= l(:field_author) %></th>
38 38 <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
39 39 <%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %>
40 40 <%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %>
41 41 </tr></thead>
42 42 <tbody>
43 43 <% @topics.each do |topic| %>
44 44 <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
45 45 <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic } %></td>
46 46 <td class="author" align="center"><%= link_to_user(topic.author) %></td>
47 47 <td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
48 48 <td class="replies" align="center"><%= topic.replies_count %></td>
49 49 <td class="last_message">
50 50 <% if topic.last_reply %>
51 51 <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
52 52 <%= link_to_message topic.last_reply %>
53 53 <% end %>
54 54 </td>
55 55 </tr>
56 56 <% end %>
57 57 </tbody>
58 58 </table>
59 59 <p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
60 60 <% else %>
61 61 <p class="nodata"><%= l(:label_no_data) %></p>
62 62 <% end %>
63 63
64 64 <% other_formats_links do |f| %>
65 65 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
66 66 <% end %>
67 67
68 68 <% html_title @board.name %>
69 69
70 70 <% content_for :header_tags do %>
71 71 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
72 72 <%= stylesheet_link_tag 'scm' %>
73 73 <% end %>
@@ -1,42 +1,43
1 1 <h2><%= @query.new_record? ? l(:label_calendar) : h(@query.name) %></h2>
2 2
3 <% form_tag({:controller => 'calendars', :action => 'show', :project_id => @project}, :method => :get, :id => 'query_form') do %>
3 <%= form_tag({:controller => 'calendars', :action => 'show', :project_id => @project},
4 :method => :get, :id => 'query_form') do %>
4 5 <%= hidden_field_tag 'set_filter', '1' %>
5 6 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
6 7 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
7 8 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
8 9 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
9 10 </div>
10 11 </fieldset>
11 12
12 13 <p style="float:right;">
13 14 <%= link_to_previous_month(@year, @month) %> | <%= link_to_next_month(@year, @month) %>
14 15 </p>
15 16
16 17 <p class="buttons">
17 18 <%= label_tag('month', l(:label_month)) %>
18 19 <%= select_month(@month, :prefix => "month", :discard_type => true) %>
19 20 <%= label_tag('year', l(:label_year)) %>
20 21 <%= select_year(@year, :prefix => "year", :discard_type => true) %>
21 22
22 23 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
23 24 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, :class => 'icon icon-reload' %>
24 25 </p>
25 26 <% end %>
26 27
27 28 <%= error_messages_for 'query' %>
28 29 <% if @query.valid? %>
29 30 <%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>
30 31
31 32 <p class="legend cal">
32 33 <span class="starting"><%= l(:text_tip_issue_begin_day) %></span>
33 34 <span class="ending"><%= l(:text_tip_issue_end_day) %></span>
34 35 <span class="starting ending"><%= l(:text_tip_issue_begin_end_day) %></span>
35 36 </p>
36 37 <% end %>
37 38
38 39 <% content_for :sidebar do %>
39 40 <%= render :partial => 'issues/sidebar' %>
40 41 <% end %>
41 42
42 43 <% html_title(l(:label_calendar)) -%>
@@ -1,31 +1,31
1 1 xml.instruct!
2 2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3 3 xml.title truncate_single_line(@title, :length => 100)
4 xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false, :escape => false))
5 xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil, :escape => false))
4 xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false))
5 xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil))
6 6 xml.id url_for(:controller => 'welcome', :only_path => false)
7 7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
8 8 xml.author { xml.name "#{Setting.app_title}" }
9 9 xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
10 10 @items.each do |item|
11 11 xml.entry do
12 12 url = url_for(item.event_url(:only_path => false))
13 13 if @project
14 14 xml.title truncate_single_line(item.event_title, :length => 100)
15 15 else
16 16 xml.title truncate_single_line("#{item.project} - #{item.event_title}", :length => 100)
17 17 end
18 18 xml.link "rel" => "alternate", "href" => url
19 19 xml.id url
20 20 xml.updated item.event_datetime.xmlschema
21 21 author = item.event_author if item.respond_to?(:event_author)
22 22 xml.author do
23 23 xml.name(author)
24 24 xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail
25 25 end if author
26 26 xml.content "type" => "html" do
27 27 xml.text! textilizable(item, :event_description, :only_path => false)
28 28 end
29 29 end
30 30 end
31 31 end
@@ -1,8 +1,8
1 1 <h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
2 2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
3 3 &#187; <%=h @custom_field.name %></h2>
4 4
5 <% labelled_form_for :custom_field, @custom_field, :url => custom_field_path(@custom_field), :html => {:method => :put} do |f| %>
5 <%= labelled_form_for :custom_field, @custom_field, :url => custom_field_path(@custom_field), :html => {:method => :put} do |f| %>
6 6 <%= render :partial => 'form', :locals => { :f => f } %>
7 7 <%= submit_tag l(:button_save) %>
8 8 <% end %>
@@ -1,9 +1,9
1 1 <h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
2 2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
3 3 &#187; <%= l(:label_custom_field_new) %></h2>
4 4
5 <% labelled_form_for :custom_field, @custom_field, :url => custom_fields_path do |f| %>
5 <%= labelled_form_for :custom_field, @custom_field, :url => custom_fields_path do |f| %>
6 6 <%= render :partial => 'form', :locals => { :f => f } %>
7 7 <%= hidden_field_tag 'type', @custom_field.type %>
8 8 <%= submit_tag l(:button_save) %>
9 9 <% end %>
@@ -1,8 +1,8
1 1 <h2><%=l(:label_document)%></h2>
2 2
3 <% labelled_form_for @document do |f| %>
3 <%= labelled_form_for @document do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <p><%= submit_tag l(:button_save) %></p>
6 6 <% end %>
7 7
8 8
@@ -1,34 +1,34
1 1 <div class="contextual">
2 2 <%= link_to l(:label_document_new), new_project_document_path(@project), :class => 'icon icon-add',
3 3 :onclick => 'Element.show("add-document"); Form.Element.focus("document_title"); return false;' if User.current.allowed_to?(:manage_documents, @project) %>
4 4 </div>
5 5
6 6 <div id="add-document" style="display:none;">
7 7 <h2><%=l(:label_document_new)%></h2>
8 <% labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
8 <%= labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
9 9 <%= render :partial => 'form', :locals => {:f => f} %>
10 10 <p>
11 11 <%= submit_tag l(:button_create) %>
12 12 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-document")' %>
13 13 </p>
14 14 <% end %>
15 15 </div>
16 16
17 17 <h2><%=l(:label_document_plural)%></h2>
18 18
19 19 <% if @grouped.empty? %><p class="nodata"><%= l(:label_no_data) %></p><% end %>
20 20
21 21 <% @grouped.keys.sort.each do |group| %>
22 22 <h3><%= group %></h3>
23 23 <%= render :partial => 'documents/document', :collection => @grouped[group] %>
24 24 <% end %>
25 25
26 26 <% content_for :sidebar do %>
27 27 <h3><%= l(:label_sort_by, '') %></h3>
28 28 <%= link_to l(:field_category), {:sort_by => 'category'}, :class => (@sort_by == 'category' ? 'selected' :nil) %><br />
29 29 <%= link_to l(:label_date), {:sort_by => 'date'}, :class => (@sort_by == 'date' ? 'selected' :nil) %><br />
30 30 <%= link_to l(:field_title), {:sort_by => 'title'}, :class => (@sort_by == 'title' ? 'selected' :nil) %><br />
31 31 <%= link_to l(:field_author), {:sort_by => 'author'}, :class => (@sort_by == 'author' ? 'selected' :nil) %>
32 32 <% end %>
33 33
34 34 <% html_title(l(:label_document_plural)) -%>
@@ -1,6 +1,6
1 1 <h2><%=l(:label_document_new)%></h2>
2 2
3 <% labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
3 <%= labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <p><%= submit_tag l(:button_create) %></p>
6 6 <% end %>
@@ -1,34 +1,34
1 1 <div class="contextual">
2 2 <% if User.current.allowed_to?(:manage_documents, @project) %>
3 3 <%= link_to l(:button_edit), edit_document_path(@document), :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
4 4 <%= link_to l(:button_delete), document_path(@document), :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %>
5 5 <% end %>
6 6 </div>
7 7
8 8 <h2><%=h @document.title %></h2>
9 9
10 10 <p><em><%=h @document.category.name %><br />
11 11 <%= format_date @document.created_on %></em></p>
12 12 <div class="wiki">
13 13 <%= textilizable @document.description, :attachments => @document.attachments %>
14 14 </div>
15 15
16 16 <h3><%= l(:label_attachment_plural) %></h3>
17 17 <%= link_to_attachments @document %>
18 18
19 19 <% if authorize_for('documents', 'add_attachment') %>
20 20 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
21 21 :id => 'attach_files_link' %></p>
22 <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
22 <%= form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
23 23 <div class="box">
24 24 <p><%= render :partial => 'attachments/form' %></p>
25 25 </div>
26 26 <%= submit_tag l(:button_add) %>
27 27 <% end %>
28 28 <% end %>
29 29
30 30 <% html_title @document.title -%>
31 31
32 32 <% content_for :header_tags do %>
33 33 <%= stylesheet_link_tag 'scm' %>
34 34 <% end %>
@@ -1,12 +1,12
1 1 <h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
2 2
3 <% form_tag({}, :method => :delete) do %>
3 <%= form_tag({}, :method => :delete) do %>
4 4 <div class="box">
5 5 <p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
6 6 <p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
7 7 <%= select_tag 'reassign_to_id', ("<option>--- #{l(:actionview_instancetag_blank_option)} ---</option>" + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
8 8 </div>
9 9
10 10 <%= submit_tag l(:button_apply) %>
11 11 <%= link_to l(:button_cancel), enumerations_path %>
12 12 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=h @enumeration %></h2>
2 2
3 <% labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %>
3 <%= labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,7 +1,7
1 1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=l(:label_enumeration_new)%></h2>
2 2
3 <% labelled_form_for :enumeration, @enumeration, :url => enumerations_path do |f| %>
3 <%= labelled_form_for :enumeration, @enumeration, :url => enumerations_path do |f| %>
4 4 <%= f.hidden_field :type %>
5 5 <%= render :partial => 'form', :locals => {:f => f} %>
6 6 <%= submit_tag l(:button_create) %>
7 7 <% end %>
@@ -1,16 +1,16
1 1 <h2><%=l(:label_attachment_new)%></h2>
2 2
3 3 <%= error_messages_for 'attachment' %>
4 <% form_tag(project_files_path(@project), :multipart => true, :class => "tabular") do %>
4 <%= form_tag(project_files_path(@project), :multipart => true, :class => "tabular") do %>
5 5 <div class="box">
6 6
7 7 <% if @versions.any? %>
8 8 <p><label for="version_id"><%=l(:field_version)%></label>
9 9 <%= select_tag "version_id", content_tag('option', '') +
10 10 options_from_collection_for_select(@versions, "id", "name") %></p>
11 11 <% end %>
12 12
13 13 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
14 14 </div>
15 15 <%= submit_tag l(:button_add) %>
16 16 <% end %>
@@ -1,187 +1,190
1 1 <% @gantt.view = self %>
2 2 <h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
3 3
4 <% form_tag({:controller => 'gantts', :action => 'show', :project_id => @project, :month => params[:month], :year => params[:year], :months => params[:months]}, :method => :get, :id => 'query_form') do %>
4 <%= form_tag({:controller => 'gantts', :action => 'show',
5 :project_id => @project, :month => params[:month],
6 :year => params[:year], :months => params[:months]},
7 :method => :get, :id => 'query_form') do %>
5 8 <%= hidden_field_tag 'set_filter', '1' %>
6 9 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
7 10 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
8 11 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
9 12 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
10 13 </div>
11 14 </fieldset>
12 15
13 16 <p class="contextual">
14 17 <%= gantt_zoom_link(@gantt, :in) %>
15 18 <%= gantt_zoom_link(@gantt, :out) %>
16 19 </p>
17 20
18 21 <p class="buttons">
19 22 <%= text_field_tag 'months', @gantt.months, :size => 2 %>
20 23 <%= l(:label_months_from) %>
21 24 <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
22 25 <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
23 26 <%= hidden_field_tag 'zoom', @gantt.zoom %>
24 27
25 28 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
26 29 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, :class => 'icon icon-reload' %>
27 30 </p>
28 31 <% end %>
29 32
30 33 <%= error_messages_for 'query' %>
31 34 <% if @query.valid? %>
32 35 <% zoom = 1
33 36 @gantt.zoom.times { zoom = zoom * 2 }
34 37
35 38 subject_width = 330
36 39 header_heigth = 18
37 40
38 41 headers_height = header_heigth
39 42 show_weeks = false
40 43 show_days = false
41 44
42 45 if @gantt.zoom > 1
43 46 show_weeks = true
44 47 headers_height = 2 * header_heigth
45 48 if @gantt.zoom > 2
46 49 show_days = true
47 50 headers_height = 3 * header_heigth
48 51 end
49 52 end
50 53
51 54 # Width of the entire chart
52 55 g_width = ((@gantt.date_to - @gantt.date_from + 1) * zoom).to_i
53 56
54 57 @gantt.render(:top => headers_height + 8, :zoom => zoom, :g_width => g_width, :subject_width => subject_width)
55 58
56 59 g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max
57 60 t_height = g_height + headers_height
58 61
59 62
60 63 %>
61 64
62 65 <% if @gantt.truncated %>
63 66 <p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
64 67 <% end %>
65 68
66 69 <table style="width:100%; border:0; border-collapse: collapse;">
67 70 <tr>
68 71 <td style="width:<%= subject_width %>px; padding:0px;">
69 72
70 73 <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
71 74 <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
72 75 <div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
73 76
74 77 <div class="gantt_subjects">
75 78 <%= @gantt.subjects.html_safe %>
76 79 </div>
77 80
78 81 </div>
79 82 </td>
80 83 <td>
81 84
82 85 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
83 86 <div style="width:<%= g_width - 1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
84 87 <%
85 88 #
86 89 # Months headers
87 90 #
88 91 month_f = @gantt.date_from
89 92 left = 0
90 93 height = (show_weeks ? header_heigth : header_heigth + g_height)
91 94 @gantt.months.times do
92 95 width = (((month_f >> 1) - month_f) * zoom - 1).to_i
93 96 %>
94 97 <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
95 98 <%= link_to h("#{month_f.year}-#{month_f.month}"), @gantt.params.merge(:year => month_f.year, :month => month_f.month), :title => "#{month_name(month_f.month)} #{month_f.year}"%>
96 99 </div>
97 100 <%
98 101 left = left + width + 1
99 102 month_f = month_f >> 1
100 103 end %>
101 104
102 105 <%
103 106 #
104 107 # Weeks headers
105 108 #
106 109 if show_weeks
107 110 left = 0
108 111 height = (show_days ? header_heigth - 1 : header_heigth - 1 + g_height)
109 112 if @gantt.date_from.cwday == 1
110 113 # @date_from is monday
111 114 week_f = @gantt.date_from
112 115 else
113 116 # find next monday after @date_from
114 117 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
115 118 width = (7 - @gantt.date_from.cwday + 1) * zoom - 1
116 119 %>
117 120 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div>
118 121 <%
119 122 left = left + width + 1
120 123 end %>
121 124 <%
122 125 while week_f <= @gantt.date_to
123 126 width = ((week_f + 6 <= @gantt.date_to) ? 7 * zoom - 1 : (@gantt.date_to - week_f + 1) * zoom - 1).to_i
124 127 %>
125 128 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
126 129 <small><%= week_f.cweek if width >= 16 %></small>
127 130 </div>
128 131 <%
129 132 left = left + width + 1
130 133 week_f = week_f + 7
131 134 end
132 135 end %>
133 136
134 137 <%
135 138 #
136 139 # Days headers
137 140 #
138 141 if show_days
139 142 left = 0
140 143 height = g_height + header_heigth - 1
141 144 wday = @gantt.date_from.cwday
142 145 (@gantt.date_to - @gantt.date_from + 1).to_i.times do
143 146 width = zoom - 1
144 147 %>
145 148 <div style="left:<%= left %>px;top:37px;width:<%= width %>px;height:<%= height %>px;font-size:0.7em;<%= "background:#f1f1f1;" if wday > 5 %>" class="gantt_hdr">
146 149 <%= day_name(wday).first %>
147 150 </div>
148 151 <%
149 152 left = left + width + 1
150 153 wday = wday + 1
151 154 wday = 1 if wday > 7
152 155 end
153 156 end %>
154 157
155 158 <%= @gantt.lines.html_safe %>
156 159
157 160 <%
158 161 #
159 162 # Today red line (excluded from cache)
160 163 #
161 164 if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
162 165 <div style="position: absolute;height:<%= g_height %>px;top:<%= headers_height + 1 %>px;left:<%= (((Date.today - @gantt.date_from + 1) * zoom).floor() - 1).to_i %>px;width:10px;border-left: 1px dashed red;">&nbsp;</div>
163 166 <% end %>
164 167
165 168 </div>
166 169 </td>
167 170 </tr>
168 171 </table>
169 172
170 173 <table style="width:100%">
171 174 <tr>
172 175 <td align="left"><%= link_to_content_update("\xc2\xab " + l(:label_previous), params.merge(@gantt.params_previous)) %></td>
173 176 <td align="right"><%= link_to_content_update(l(:label_next) + " \xc2\xbb", params.merge(@gantt.params_next)) %></td>
174 177 </tr>
175 178 </table>
176 179
177 180 <% other_formats_links do |f| %>
178 181 <%= f.link_to 'PDF', :url => params.merge(@gantt.params) %>
179 182 <%= f.link_to('PNG', :url => params.merge(@gantt.params)) if @gantt.respond_to?('to_image') %>
180 183 <% end %>
181 184 <% end # query.valid? %>
182 185
183 186 <% content_for :sidebar do %>
184 187 <%= render :partial => 'issues/sidebar' %>
185 188 <% end %>
186 189
187 190 <% html_title(l(:label_gantt)) -%>
@@ -1,4 +1,4
1 <% labelled_form_for @group do |f| %>
1 <%= labelled_form_for @group do |f| %>
2 2 <%= render :partial => 'form', :locals => { :f => f } %>
3 3 <%= submit_tag l(:button_save) %>
4 4 <% end %>
@@ -1,69 +1,70
1 1 <% roles = Role.find_all_givable %>
2 2 <% projects = Project.active.find(:all, :order => 'lft') %>
3 3
4 4 <div class="splitcontentleft">
5 5 <% if @group.memberships.any? %>
6 6 <table class="list memberships">
7 7 <thead><tr>
8 8 <th><%= l(:label_project) %></th>
9 9 <th><%= l(:label_role_plural) %></th>
10 10 <th style="width:15%"></th>
11 11 </tr></thead>
12 12 <tbody>
13 13 <% @group.memberships.each do |membership| %>
14 14 <% next if membership.new_record? %>
15 15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 16 <td class="project"><%=h membership.project %></td>
17 17 <td class="roles">
18 18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
19 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
20 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
19 <%= form_for(:membership, :remote => true,
20 :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 22 <p><% roles.each do |role| %>
22 23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 24 <% end %></p>
24 25 <p><%= submit_tag l(:button_change) %>
25 26 <%= link_to_function(
26 27 l(:button_cancel),
27 28 "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;"
28 29 ) %></p>
29 30 <% end %>
30 31 </td>
31 32 <td class="buttons">
32 33 <%= link_to_function(
33 34 l(:button_edit),
34 35 "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;",
35 36 :class => 'icon icon-edit'
36 37 ) %>
37 38 <%= link_to_remote(
38 39 l(:button_delete),
39 40 { :url => { :controller => 'groups',
40 41 :action => 'destroy_membership',
41 42 :id => @group,
42 43 :membership_id => membership },
43 44 :method => :post },
44 45 :class => 'icon icon-del' ) %>
45 46 </td>
46 47 </tr>
47 48 <% end; reset_cycle %>
48 49 </tbody>
49 50 </table>
50 51 <% else %>
51 52 <p class="nodata"><%= l(:label_no_data) %></p>
52 53 <% end %>
53 54 </div>
54 55
55 56 <div class="splitcontentright">
56 57 <% if projects.any? %>
57 58 <fieldset><legend><%=l(:label_project_new)%></legend>
58 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group }) do %>
59 <%= form_for(:membership, :remote => true, :url => { :action => 'edit_membership', :id => @group }) do %>
59 60 <%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
60 61 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
61 62 <p><%= l(:label_role_plural) %>:
62 63 <% roles.each do |role| %>
63 64 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
64 65 <% end %></p>
65 66 <p><%= submit_tag l(:button_add) %></p>
66 67 <% end %>
67 68 </fieldset>
68 69 <% end %>
69 70 </div>
@@ -1,55 +1,56
1 1 <div class="splitcontentleft">
2 2 <% if @group.users.any? %>
3 3 <table class="list users">
4 4 <thead><tr>
5 5 <th><%= l(:label_user) %></th>
6 6 <th style="width:15%"></th>
7 7 </tr></thead>
8 8 <tbody>
9 9 <% @group.users.sort.each do |user| %>
10 10 <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
11 11 <td class="user"><%= link_to_user user %></td>
12 12 <td class="buttons">
13 13 <%= link_to_remote(
14 14 l(:button_delete),
15 15 { :url => group_user_path(@group, :user_id => user),
16 16 :method => :delete },
17 17 :class => 'icon icon-del'
18 18 ) %>
19 19 </td>
20 20 </tr>
21 21 <% end %>
22 22 </tbody>
23 23 </table>
24 24 <% else %>
25 25 <p class="nodata"><%= l(:label_no_data) %></p>
26 26 <% end %>
27 27 </div>
28 28
29 29 <div class="splitcontentright">
30 30 <% users = User.active.not_in_group(@group).all(:limit => 100) %>
31 31 <% if users.any? %>
32 <% remote_form_for(@group, :url => group_users_path(@group), :html => {:method => :post}) do |f| %>
32 <%= form_for(@group, :remote => true, :url => group_users_path(@group),
33 :html => {:method => :post}) do |f| %>
33 34 <fieldset><legend><%=l(:label_user_new)%></legend>
34 35
35 36 <p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
36 37 <%= observe_field(:user_search,
37 38 :frequency => 0.5,
38 39 :update => :users,
39 40 :url => autocomplete_for_user_group_path(@group),
40 41 :method => :get,
41 42 :before => '$("user_search").addClassName("ajax-loading")',
42 43 :complete => '$("user_search").removeClassName("ajax-loading")',
43 44 :with => 'q')
44 45 %>
45 46
46 47 <div id="users">
47 48 <%= principals_check_box_tags 'user_ids[]', users %>
48 49 </div>
49 50
50 51 <p><%= submit_tag l(:button_add) %></p>
51 52 </fieldset>
52 53 <% end %>
53 54 <% end %>
54 55
55 56 </div>
@@ -1,9 +1,9
1 1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= l(:label_group_new) %></h2>
2 2
3 <% labelled_form_for @group do |f| %>
3 <%= labelled_form_for @group do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <p>
6 6 <%= f.submit l(:button_create) %>
7 7 <%= f.submit l(:button_create_and_continue), :name => 'continue' %>
8 8 </p>
9 9 <% end %>
@@ -1,16 +1,16
1 1 <h2><%=l(:label_issue_category)%>: <%=h @category.name %></h2>
2 2
3 <% form_tag(issue_category_path(@category), :method => :delete) do %>
3 <%= form_tag(issue_category_path(@category), :method => :delete) do %>
4 4 <div class="box">
5 5 <p><strong><%= l(:text_issue_category_destroy_question, @issue_count) %></strong></p>
6 6 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %></label><br />
7 7 <% if @categories.size > 0 %>
8 8 <label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_issue_category_reassign_to) %></label>:
9 9 <%= label_tag "reassign_to_id", l(:description_issue_category_reassign), :class => "hidden-for-sighted" %>
10 10 <%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %></p>
11 11 <% end %>
12 12 </div>
13 13
14 14 <%= submit_tag l(:button_apply) %>
15 15 <%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' %>
16 16 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=h @issue_status %></h2>
2 2
3 <% labelled_form_for @issue_status do |f| %>
3 <%= labelled_form_for @issue_status do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=l(:label_issue_status_new)%></h2>
2 2
3 <% labelled_form_for @issue_status do |f| %>
3 <%= labelled_form_for @issue_status do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <% end %>
@@ -1,7 +1,7
1 1 <div class="contextual">
2 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 => 'new', :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 <%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %>
4 4 <%= watcher_tag(@issue, User.current) %>
5 5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon icon-copy' %>
6 6 <%= link_to l(:button_delete), issue_path(@issue), :confirm => issues_destroy_confirmation_message(@issue), :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %>
7 7 </div>
@@ -1,68 +1,68
1 <% labelled_fields_for :issue, @issue do |f| %>
1 <%= labelled_fields_for :issue, @issue do |f| %>
2 2
3 3 <div class="splitcontent">
4 4 <div class="splitcontentleft">
5 5 <% if @issue.safe_attribute? 'status_id' %>
6 6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
7 7 <% else %>
8 8 <p><label><%= l(:field_status) %></label> <%= h(@issue.status.name) %></p>
9 9 <% end %>
10 10
11 11 <% if @issue.safe_attribute? 'priority_id' %>
12 12 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
13 13 <% end %>
14 14
15 15 <% if @issue.safe_attribute? 'assigned_to_id' %>
16 16 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
17 17 <% end %>
18 18
19 19 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
20 20 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
21 21 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
22 22 l(:label_issue_category_new),
23 23 'issue_category[name]',
24 24 {:controller => 'issue_categories', :action => 'create', :project_id => @issue.project},
25 25 :title => l(:label_issue_category_new),
26 26 :tabindex => 199) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
27 27 <% end %>
28 28
29 29 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
30 30 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
31 31 <%= link_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
32 32 {:url => new_project_version_path(@issue.project), :method => 'get'},
33 33 :title => l(:label_version_new),
34 34 :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %>
35 35 </p>
36 36 <% end %>
37 37 </div>
38 38
39 39 <div class="splitcontentright">
40 40 <% if @issue.safe_attribute? 'parent_issue_id' %>
41 41 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
42 42 <div id="parent_issue_candidates" class="autocomplete"></div>
43 43 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @issue.project) }')" %>
44 44 <% end %>
45 45
46 46 <% if @issue.safe_attribute? 'start_date' %>
47 47 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
48 48 <% end %>
49 49
50 50 <% if @issue.safe_attribute? 'due_date' %>
51 51 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
52 52 <% end %>
53 53
54 54 <% if @issue.safe_attribute? 'estimated_hours' %>
55 55 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
56 56 <% end %>
57 57
58 58 <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
59 59 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
60 60 <% end %>
61 61 </div>
62 62 </div>
63 63
64 64 <% if @issue.safe_attribute? 'custom_field_values' %>
65 65 <%= render :partial => 'issues/form_custom_fields' %>
66 66 <% end %>
67 67
68 68 <% end %>
@@ -1,50 +1,50
1 <% labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
1 <%= labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 2 <%= error_messages_for 'issue', 'time_entry' %>
3 3 <%= render :partial => 'conflict' if @conflict %>
4 4 <div class="box">
5 5 <% if @edit_allowed || !@allowed_statuses.empty? %>
6 6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
7 7 <div id="all_attributes">
8 8 <%= render :partial => 'form', :locals => {:f => f} %>
9 9 </div>
10 10 </fieldset>
11 11 <% end %>
12 12 <% if User.current.allowed_to?(:log_time, @project) %>
13 13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
14 <% labelled_fields_for :time_entry, @time_entry do |time_entry| %>
14 <%= labelled_fields_for :time_entry, @time_entry do |time_entry| %>
15 15 <div class="splitcontentleft">
16 16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
17 17 </div>
18 18 <div class="splitcontentright">
19 19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
20 20 </div>
21 21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
22 22 <% @time_entry.custom_field_values.each do |value| %>
23 23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
24 24 <% end %>
25 25 <% end %>
26 26 </fieldset>
27 27 <% end %>
28 28
29 29 <fieldset><legend><%= l(:field_notes) %></legend>
30 30 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
31 31 <%= wikitoolbar_for 'notes' %>
32 32 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
33 33
34 34 <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
35 35 </fieldset>
36 36 </div>
37 37
38 38 <%= f.hidden_field :lock_version %>
39 39 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
40 40 <%= submit_tag l(:button_submit) %>
41 41 <%= link_to_remote l(:label_preview),
42 42 { :url => preview_edit_issue_path(:project_id => @project, :id => @issue),
43 43 :method => 'post',
44 44 :update => 'preview',
45 45 :with => 'Form.serialize("issue-form")',
46 46 :complete => "Element.scrollTo('preview')"
47 47 }, :accesskey => accesskey(:preview) %>
48 48 <% end %>
49 49
50 50 <div id="preview" class="wiki"></div>
@@ -1,48 +1,48
1 <% labelled_fields_for :issue, @issue do |f| %>
1 <%= labelled_fields_for :issue, @issue do |f| %>
2 2 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
3 3
4 4 <% if @issue.safe_attribute? 'is_private' %>
5 5 <p style="float:right; margin-right:1em;">
6 6 <label class="inline" for="issue_is_private" id="issue_is_private_label"><%= f.check_box :is_private, :no_label => true %> <%= l(:field_is_private) %></label>
7 7 </p>
8 8 <% end %>
9 9
10 10 <% if @issue.safe_attribute? 'project_id' %>
11 11 <p><%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), :required => true %></p>
12 12 <%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue, :project_change => '1'),
13 13 :with => "Form.serialize('issue-form')" %>
14 14 <% end %>
15 15
16 16 <% if @issue.safe_attribute? 'tracker_id' %>
17 17 <p><%= f.select :tracker_id, @issue.project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
18 18 <%= observe_field :issue_tracker_id, :url => project_issue_form_path(@project, :id => @issue),
19 19 :with => "Form.serialize('issue-form')" %>
20 20 <% end %>
21 21
22 22 <% if @issue.safe_attribute? 'subject' %>
23 23 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
24 24 <% end %>
25 25
26 26 <% if @issue.safe_attribute? 'description' %>
27 27 <p>
28 28 <label><%= l(:field_description) %></label>
29 29 <%= link_to_function image_tag('edit.png'),
30 30 'Element.hide(this); Effect.toggle("issue_description_and_toolbar", "appear", {duration:0.3})' unless @issue.new_record? %>
31 <% content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %>
31 <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %>
32 32 <%= f.text_area :description,
33 33 :cols => 60,
34 34 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
35 35 :accesskey => accesskey(:edit),
36 36 :class => 'wiki-edit',
37 37 :no_label => true %>
38 38 <% end %>
39 39 </p>
40 40 <%= wikitoolbar_for 'issue_description' %>
41 41 <% end %>
42 42
43 43 <div id="attributes" class="attributes">
44 44 <%= render :partial => 'issues/attributes' %>
45 45 </div>
46 46
47 47 <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
48 48 <% end %>
@@ -1,37 +1,37
1 <% form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
3 3 <div class="autoscroll">
4 4 <table class="list issues">
5 5 <thead><tr>
6 6 <th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
7 7 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
8 8 </th>
9 9 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
10 10 <% query.columns.each do |column| %>
11 11 <%= column_header(column) %>
12 12 <% end %>
13 13 </tr></thead>
14 14 <% previous_group = false %>
15 15 <tbody>
16 16 <% issue_list(issues) do |issue, level| -%>
17 17 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
18 18 <% reset_cycle %>
19 19 <tr class="group open">
20 20 <td colspan="<%= query.columns.size + 2 %>">
21 21 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
22 22 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
23 23 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
24 24 </td>
25 25 </tr>
26 26 <% previous_group = group %>
27 27 <% end %>
28 28 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 29 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
30 <td class="id"><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
31 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.css_classes %><% end %>
30 <td class="id"><%= link_to issue.id, issue_path(issue) %></td>
31 <%= raw query.columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
32 32 </tr>
33 33 <% end -%>
34 34 </tbody>
35 35 </table>
36 36 </div>
37 37 <% end -%>
@@ -1,29 +1,29
1 1 <% if issues && issues.any? %>
2 <% form_tag({}) do %>
2 <%= form_tag({}) do %>
3 3 <table class="list issues">
4 4 <thead><tr>
5 5 <th>#</th>
6 6 <th><%=l(:field_project)%></th>
7 7 <th><%=l(:field_tracker)%></th>
8 8 <th><%=l(:field_subject)%></th>
9 9 </tr></thead>
10 10 <tbody>
11 11 <% for issue in issues %>
12 12 <tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
13 13 <td class="id">
14 14 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %>
15 15 <%= link_to(h(issue.id), :controller => 'issues', :action => 'show', :id => issue) %>
16 16 </td>
17 17 <td class="project"><%= link_to_project(issue.project) %></td>
18 18 <td class="tracker"><%=h issue.tracker %></td>
19 19 <td class="subject">
20 20 <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>)
21 21 </td>
22 22 </tr>
23 23 <% end %>
24 24 </tbody>
25 25 </table>
26 26 <% end %>
27 27 <% else %>
28 28 <p class="nodata"><%= l(:label_no_data) %></p>
29 29 <% end %>
@@ -1,127 +1,127
1 1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
2 2
3 3 <ul><%= @issues.collect {|i|
4 4 content_tag('li',
5 5 link_to(h("#{i.tracker} ##{i.id}"),
6 6 { :action => 'show', :id => i }
7 7 ) + h(": #{i.subject}"))
8 8 }.join("\n").html_safe %></ul>
9 9
10 <% form_tag({:action => 'bulk_update'}, :id => 'bulk_edit_form') do %>
10 <%= form_tag({:action => 'bulk_update'}, :id => 'bulk_edit_form') do %>
11 11 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
12 12 <div class="box tabular">
13 13 <fieldset class="attributes">
14 14 <legend><%= l(:label_change_properties) %></legend>
15 15
16 16 <div class="splitcontentleft">
17 17 <% if @allowed_projects.present? %>
18 18 <p>
19 19 <label for="issue_project_id"><%= l(:field_project) %></label>
20 20 <%= select_tag('issue[project_id]', content_tag('option', l(:label_no_change_option), :value => '') + project_tree_options_for_select(@allowed_projects, :selected => @target_project)) %>
21 21 </p>
22 22 <%= observe_field :issue_project_id, :url => {:action => 'bulk_edit'},
23 23 :update => 'content',
24 24 :with => "Form.serialize('bulk_edit_form')" %>
25 25 <% end %>
26 26 <p>
27 27 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
28 28 <%= select_tag('issue[tracker_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@trackers, :id, :name)) %>
29 29 </p>
30 30 <% if @available_statuses.any? %>
31 31 <p>
32 32 <label for='issue_status_id'><%= l(:field_status) %></label>
33 33 <%= select_tag('issue[status_id]',content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@available_statuses, :id, :name)) %>
34 34 </p>
35 35 <% end %>
36 36 <p>
37 37 <label for='issue_priority_id'><%= l(:field_priority) %></label>
38 38 <%= select_tag('issue[priority_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
39 39 </p>
40 40 <p>
41 41 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
42 42 <%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
43 43 content_tag('option', l(:label_nobody), :value => 'none') +
44 44 principals_options_for_select(@assignables)) %>
45 45 </p>
46 46 <p>
47 47 <label for='issue_category_id'><%= l(:field_category) %></label>
48 48 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
49 49 content_tag('option', l(:label_none), :value => 'none') +
50 50 options_from_collection_for_select(@categories, :id, :name)) %>
51 51 </p>
52 52 <p>
53 53 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
54 54 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
55 55 content_tag('option', l(:label_none), :value => 'none') +
56 56 version_options_for_select(@versions.sort)) %>
57 57 </p>
58 58
59 59 <% @custom_fields.each do |custom_field| %>
60 60 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('issue', custom_field, @projects) %></p>
61 61 <% end %>
62 62
63 63 <% if @copy && @attachments_present %>
64 64 <p>
65 65 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
66 66 <%= check_box_tag 'copy_attachments', '1', true %>
67 67 </p>
68 68 <% end %>
69 69
70 70 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
71 71 </div>
72 72
73 73 <div class="splitcontentright">
74 74 <% if @safe_attributes.include?('is_private') %>
75 75 <p>
76 76 <label for='issue_is_private'><%= l(:field_is_private) %></label>
77 77 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
78 78 content_tag('option', l(:general_text_Yes), :value => '1') +
79 79 content_tag('option', l(:general_text_No), :value => '0')) %>
80 80 </p>
81 81 <% end %>
82 82 <% if @project && User.current.allowed_to?(:manage_subtasks, @project) %>
83 83 <p>
84 84 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
85 85 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10 %>
86 86 </p>
87 87 <div id="parent_issue_candidates" class="autocomplete"></div>
88 88 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:project_id => @project) }')" %>
89 89 <% end %>
90 90 <p>
91 91 <label for='issue_start_date'><%= l(:field_start_date) %></label>
92 92 <%= text_field_tag 'issue[start_date]', '', :size => 10 %><%= calendar_for('issue_start_date') %>
93 93 </p>
94 94 <p>
95 95 <label for='issue_due_date'><%= l(:field_due_date) %></label>
96 96 <%= text_field_tag 'issue[due_date]', '', :size => 10 %><%= calendar_for('issue_due_date') %>
97 97 </p>
98 98 <% if Issue.use_field_for_done_ratio? %>
99 99 <p>
100 100 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
101 101 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>
102 102 </p>
103 103 <% end %>
104 104 </div>
105 105
106 106 </fieldset>
107 107
108 108 <fieldset><legend><%= l(:field_notes) %></legend>
109 109 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
110 110 <%= wikitoolbar_for 'notes' %>
111 111 </fieldset>
112 112 </div>
113 113
114 114 <p>
115 115 <% if @copy %>
116 116 <%= hidden_field_tag 'copy', '1' %>
117 117 <%= submit_tag l(:button_copy) %>
118 118 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
119 119 <% elsif @target_project %>
120 120 <%= submit_tag l(:button_move) %>
121 121 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
122 122 <% else %>
123 123 <%= submit_tag l(:button_submit) %>
124 124 <% end %>
125 125 </p>
126 126
127 127 <% end %>
@@ -1,15 +1,15
1 1 <h2><%= l(:label_confirmation) %></h2>
2 2
3 <% form_tag({}, :method => :delete) do %>
3 <%= form_tag({}, :method => :delete) do %>
4 4 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
5 5 <div class="box">
6 6 <p><strong><%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %></strong></p>
7 7 <p>
8 8 <label><%= radio_button_tag 'todo', 'destroy', true %> <%= l(:text_destroy_time_entries) %></label><br />
9 9 <label><%= radio_button_tag 'todo', 'nullify', false %> <%= l(:text_assign_time_entries_to_project) %></label><br />
10 10 <label><%= radio_button_tag 'todo', 'reassign', false, :onchange => 'if (this.checked) { $("reassign_to_id").focus(); }' %> <%= l(:text_reassign_time_entries) %></label>
11 11 <%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("todo_reassign").checked=true;' %>
12 12 </p>
13 13 </div>
14 14 <%= submit_tag l(:button_apply) %>
15 15 <% end %>
@@ -1,105 +1,105
1 1 <div class="contextual">
2 2 <% if !@query.new_record? && @query.editable_by?(User.current) %>
3 3 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
4 4 <%= link_to l(:button_delete), query_path(@query), :confirm => l(:text_are_you_sure),
5 5 :method => :delete, :class => 'icon icon-del' %>
6 6 <% end %>
7 7 </div>
8 8
9 9 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
10 10 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
11 11
12 <% form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
12 <%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
13 13 :method => :get, :id => 'query_form') do %>
14 14 <%= hidden_field_tag 'set_filter', '1' %>
15 15 <div id="query_form_content" class="hide-when-print">
16 16 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
17 17 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
18 18 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
19 19 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
20 20 </div>
21 21 </fieldset>
22 22 <fieldset class="collapsible collapsed">
23 23 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
24 24 <div style="display: none;">
25 25 <table>
26 26 <tr>
27 27 <td><%= l(:field_column_names) %></td>
28 28 <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
29 29 </tr>
30 30 <tr>
31 31 <td><label for='group_by'><%= l(:field_group_by) %></label></td>
32 32 <td><%= select_tag('group_by',
33 33 options_for_select(
34 34 [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
35 35 @query.group_by)
36 36 ) %></td>
37 37 </tr>
38 38 </table>
39 39 </div>
40 40 </fieldset>
41 41 </div>
42 42 <p class="buttons hide-when-print">
43 43
44 44 <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
45 45 <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
46 46 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
47 47 <%= link_to_function l(:button_save),
48 48 "$('query_form').action='#{ @project ? new_project_query_path(@project) : new_query_path }'; submit_query_form('query_form')",
49 49 :class => 'icon icon-save' %>
50 50 <% end %>
51 51 </p>
52 52 <% end %>
53 53
54 54 <%= error_messages_for 'query' %>
55 55 <% if @query.valid? %>
56 56 <% if @issues.empty? %>
57 57 <p class="nodata"><%= l(:label_no_data) %></p>
58 58 <% else %>
59 59 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
60 60 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
61 61 <% end %>
62 62
63 63 <% other_formats_links do |f| %>
64 64 <%= f.link_to 'Atom', :url => params.merge(:key => User.current.rss_key) %>
65 65 <%= f.link_to 'CSV', :url => params, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
66 66 <%= f.link_to 'PDF', :url => params %>
67 67 <% end %>
68 68
69 69 <div id="csv-export-options" style="display:none;">
70 70 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
71 <% form_tag(params.merge({:format => 'csv',:page=>nil}), :method => :get, :id => 'csv-export-form') do %>
71 <%= form_tag(params.merge({:format => 'csv',:page=>nil}), :method => :get, :id => 'csv-export-form') do %>
72 72 <p>
73 73 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
74 74 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
75 75 </p>
76 76 <p>
77 77 <label><%= check_box_tag 'description', '1' %> <%= l(:field_description) %></label>
78 78 </p>
79 79 <p class="buttons">
80 80 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
81 81 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
82 82 </p>
83 83 <% end %>
84 84 </div>
85 85
86 86 <% end %>
87 87 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
88 88
89 89 <% content_for :sidebar do %>
90 90 <%= render :partial => 'issues/sidebar' %>
91 91 <% end %>
92 92
93 93 <% content_for :header_tags do %>
94 94 <%= auto_discovery_link_tag(:atom,
95 95 {:query_id => @query, :format => 'atom',
96 96 :page => nil, :key => User.current.rss_key},
97 97 :title => l(:label_issue_plural)) %>
98 98 <%= auto_discovery_link_tag(:atom,
99 99 {:controller => 'journals', :action => 'index',
100 100 :query_id => @query, :format => 'atom',
101 101 :page => nil, :key => User.current.rss_key},
102 102 :title => l(:label_changes_details)) %>
103 103 <% end %>
104 104
105 105 <%= context_menu issues_context_menu_path %>
@@ -1,55 +1,55
1 1 <h2><%=l(:label_issue_new)%></h2>
2 2
3 3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
4 4
5 <% labelled_form_for @issue, :url => project_issues_path(@project),
5 <%= labelled_form_for @issue, :url => project_issues_path(@project),
6 6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
7 7 <%= error_messages_for 'issue' %>
8 8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
9 9 <div class="box tabular">
10 10 <div id="all_attributes">
11 11 <%= render :partial => 'issues/form', :locals => {:f => f} %>
12 12 </div>
13 13
14 14 <% if @copy_from && @copy_from.attachments.any? %>
15 15 <p>
16 16 <label for="copy_attachments"><%= l(:label_copy_attachments) %></label>
17 17 <%= check_box_tag 'copy_attachments', '1', @copy_attachments %>
18 18 </p>
19 19 <% end %>
20 20
21 21 <p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
22 22
23 23 <% if @issue.safe_attribute? 'watcher_user_ids' -%>
24 24 <p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
25 25 <span id="watchers_inputs">
26 26 <%= watchers_checkboxes(@issue, @available_watchers) %>
27 27 </span>
28 28 <span class="search_for_watchers">
29 29 <%= link_to_remote l(:label_search_for_watchers),
30 30 :url => {:controller => 'watchers', :action => 'new', :project_id => @issue.project},
31 31 :method => 'get' %>
32 32 </span>
33 33 </p>
34 34 <% end %>
35 35 </div>
36 36
37 37 <%= submit_tag l(:button_create) %>
38 38 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
39 39 <%= link_to_remote l(:label_preview),
40 40 { :url => preview_new_issue_path(:project_id => @project),
41 41 :method => 'post',
42 42 :update => 'preview',
43 43 :with => "Form.serialize('issue-form')",
44 44 :complete => "Element.scrollTo('preview')"
45 45 }, :accesskey => accesskey(:preview) %>
46 46
47 47 <%= javascript_tag "Form.Element.focus('issue_subject');" %>
48 48 <% end %>
49 49
50 50 <div id="preview" class="wiki"></div>
51 51
52 52 <% content_for :header_tags do %>
53 53 <%= stylesheet_link_tag 'scm' %>
54 54 <%= robot_exclusion_tag %>
55 55 <% end %>
@@ -1,22 +1,22
1 <% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
1 <%= form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
2 2 <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %>
3 3 <%= text_area_tag :notes, @journal.notes,
4 4 :id => "journal_#{@journal.id}_notes",
5 5 :class => 'wiki-edit',
6 6 :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
7 7 <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
8 8 <p><%= submit_tag l(:button_save) %>
9 9 <%= link_to_remote l(:label_preview),
10 10 { :url => preview_edit_issue_path(:project_id => @project, :id => @journal.issue),
11 11 :method => 'post',
12 12 :update => "journal_#{@journal.id}_preview",
13 13 :with => "Form.serialize('journal-#{@journal.id}-form')",
14 14 :complete => "Element.scrollTo('journal_#{@journal.id}_preview')"
15 15 }, :accesskey => accesskey(:preview) %>
16 16 |
17 17 <%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
18 18 "Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
19 19
20 20 <div id="journal_<%= @journal.id %>_preview" class="wiki"></div>
21 21 <% end %>
22 22 <%= wikitoolbar_for "journal_#{@journal.id}_notes" %>
@@ -1,88 +1,88
1 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 3 <head>
4 4 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 5 <title><%=h html_title %></title>
6 6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 7 <meta name="keywords" content="issue,bug,tracker" />
8 8 <meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
9 9 <%= csrf_meta_tag %>
10 10 <%= favicon %>
11 11 <%= stylesheet_link_tag 'application', :media => 'all' %>
12 12 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
13 13 <%= javascript_heads %>
14 14 <%= heads_for_theme %>
15 15 <!--[if IE 6]>
16 16 <style type="text/css">
17 17 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
18 18 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
19 19 </style>
20 20 <![endif]-->
21 21 <%= call_hook :view_layouts_base_html_head %>
22 22 <!-- page specific tags -->
23 23 <%= yield :header_tags -%>
24 24 </head>
25 25 <body class="<%=h body_css_classes %>">
26 26 <div id="wrapper">
27 27 <div id="wrapper2">
28 28 <div id="top-menu">
29 29 <div id="account">
30 30 <%= render_menu :account_menu -%>
31 31 </div>
32 <%= content_tag(
33 'div',
34 "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe,
35 :id => 'loggedas') if User.current.logged? %>
32 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
36 33 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
37 34 </div>
38 35
39 36 <div id="header">
40 37 <% if User.current.logged? || !Setting.login_required? %>
41 38 <div id="quick-search">
42 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
39 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
43 40 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
44 41 <label for='q'>
45 42 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
46 43 </label>
47 44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
48 45 <% end %>
49 46 <%= render_project_jump_box %>
50 47 </div>
51 48 <% end %>
52 49
53 50 <h1><%= page_header_title %></h1>
54 51
55 52 <% if display_main_menu?(@project) %>
56 53 <div id="main-menu">
57 54 <%= render_main_menu(@project) %>
58 55 </div>
59 56 <% end %>
60 57 </div>
61 58
59 <% content_for :sidebar do %>
60 <%= call_hook :view_layouts_base_sidebar %>
61 <% end %>
62
62 63 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
63 64 <div id="sidebar">
64 65 <%= yield :sidebar %>
65 <%= call_hook :view_layouts_base_sidebar %>
66 66 </div>
67 67
68 68 <div id="content">
69 69 <%= render_flash_messages %>
70 70 <%= yield %>
71 71 <%= call_hook :view_layouts_base_content %>
72 72 <div style="clear:both;"></div>
73 73 </div>
74 74 </div>
75 75
76 76 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
77 77 <div id="ajax-modal" style="display:none;"></div>
78 78
79 79 <div id="footer">
80 80 <div class="bgl"><div class="bgr">
81 81 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2012 Jean-Philippe Lang
82 82 </div></div>
83 83 </div>
84 84 </div>
85 85 </div>
86 86 <%= call_hook :view_layouts_base_body_bottom %>
87 87 </body>
88 88 </html>
@@ -1,26 +1,28
1 1 <h2><%= link_to h(@board.name), :controller => 'boards',
2 2 :action => 'show', :project_id => @project,
3 3 :id => @board %> &#187; <%= h @message.subject %></h2>
4 4
5 <% form_for :message, @message,
6 :url => {:action => 'edit'},
7 :html => {:multipart => true,
8 :id => 'message-form',
9 :method => :post} do |f| %>
5 <%= form_for @message, {
6 :as => :message,
7 :url => {:action => 'edit'},
8 :html => {:multipart => true,
9 :id => 'message-form',
10 :method => :post}
11 } do |f| %>
10 12 <%= render :partial => 'form',
11 13 :locals => {:f => f, :replying => !@message.parent.nil?} %>
12 14 <%= submit_tag l(:button_save) %>
13 15 <%= link_to_remote l(:label_preview),
14 16 { :url => { :controller => 'messages',
15 17 :action => 'preview', :board_id => @board, :id => @message },
16 18 :method => 'post',
17 19 :update => 'preview',
18 20 :with => "Form.serialize('message-form')",
19 21 :complete => "Element.scrollTo('preview')"
20 22 }, :accesskey => accesskey(:preview) %>
21 23 <% end %>
22 24 <div id="preview" class="wiki"></div>
23 25
24 26 <% content_for :header_tags do %>
25 27 <%= stylesheet_link_tag 'scm' %>
26 28 <% end %>
@@ -1,15 +1,15
1 1 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
2 2
3 <% form_for :message, @message, :url => {:action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
3 <%= form_for @message, :url => {:action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <%= link_to_remote l(:label_preview),
7 7 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
8 8 :method => 'post',
9 9 :update => 'preview',
10 10 :with => "Form.serialize('message-form')",
11 11 :complete => "Element.scrollTo('preview')"
12 12 }, :accesskey => accesskey(:preview) %>
13 13 <% end %>
14 14
15 15 <div id="preview" class="wiki"></div>
@@ -1,94 +1,94
1 1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)),
2 2 link_to(h(@board.name), project_board_path(@project, @board)) %>
3 3
4 4 <div class="contextual">
5 5 <%= watcher_tag(@topic, User.current) %>
6 6 <%= link_to_remote_if_authorized(
7 7 l(:button_quote),
8 8 { :url => {:action => 'quote', :id => @topic} },
9 9 :class => 'icon icon-comment'
10 10 ) unless @topic.locked? %>
11 11 <%= link_to(
12 12 l(:button_edit),
13 13 {:action => 'edit', :id => @topic},
14 14 :class => 'icon icon-edit'
15 15 ) if @message.editable_by?(User.current) %>
16 16 <%= link_to(
17 17 l(:button_delete),
18 18 {:action => 'destroy', :id => @topic},
19 19 :method => :post,
20 20 :confirm => l(:text_are_you_sure),
21 21 :class => 'icon icon-del'
22 22 ) if @message.destroyable_by?(User.current) %>
23 23 </div>
24 24
25 25 <h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
26 26
27 27 <div class="message">
28 28 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
29 29 <div class="wiki">
30 30 <%= textilizable(@topic, :content) %>
31 31 </div>
32 32 <%= link_to_attachments @topic, :author => false %>
33 33 </div>
34 34 <br />
35 35
36 36 <% unless @replies.empty? %>
37 37 <h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
38 38 <% @replies.each do |message| %>
39 39 <div class="message reply" id="<%= "message-#{message.id}" %>">
40 40 <div class="contextual">
41 41 <%= link_to_remote_if_authorized(
42 42 image_tag('comment.png'),
43 43 { :url => {:action => 'quote', :id => message} },
44 44 :title => l(:button_quote)
45 45 ) unless @topic.locked? %>
46 46 <%= link_to(
47 47 image_tag('edit.png'),
48 48 {:action => 'edit', :id => message},
49 49 :title => l(:button_edit)
50 50 ) if message.editable_by?(User.current) %>
51 51 <%= link_to(
52 52 image_tag('delete.png'),
53 53 {:action => 'destroy', :id => message},
54 54 :method => :post,
55 55 :confirm => l(:text_are_you_sure),
56 56 :title => l(:button_delete)
57 57 ) if message.destroyable_by?(User.current) %>
58 58 </div>
59 59 <h4>
60 60 <%= avatar(message.author, :size => "24") %>
61 61 <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
62 62 -
63 63 <%= authoring message.created_on, message.author %>
64 64 </h4>
65 65 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
66 66 <%= link_to_attachments message, :author => false %>
67 67 </div>
68 68 <% end %>
69 69 <p class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></p>
70 70 <% end %>
71 71
72 72 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
73 73 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
74 74 <div id="reply" style="display:none;">
75 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
75 <%= form_for @reply, :as => :reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
76 76 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
77 77 <%= submit_tag l(:button_submit) %>
78 78 <%= link_to_remote l(:label_preview),
79 79 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
80 80 :method => 'post',
81 81 :update => 'preview',
82 82 :with => "Form.serialize('message-form')",
83 83 :complete => "Element.scrollTo('preview')"
84 84 }, :accesskey => accesskey(:preview) %>
85 85 <% end %>
86 86 <div id="preview" class="wiki"></div>
87 87 </div>
88 88 <% end %>
89 89
90 90 <% content_for :header_tags do %>
91 91 <%= stylesheet_link_tag 'scm' %>
92 92 <% end %>
93 93
94 94 <% html_title @topic.subject %>
@@ -1,51 +1,51
1 1 <div class="contextual">
2 2 <%= link_to(l(:button_change_password), {:action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
3 3 <%= call_hook(:view_my_account_contextual, :user => @user)%>
4 4 </div>
5 5
6 6 <h2><%=l(:label_my_account)%></h2>
7 7 <%= error_messages_for 'user' %>
8 8
9 <% labelled_form_for :user, @user,
9 <%= labelled_form_for :user, @user,
10 10 :url => { :action => "account" },
11 11 :html => { :id => 'my_account_form',
12 12 :method => :post } do |f| %>
13 13 <div class="splitcontentleft">
14 14 <fieldset class="box tabular">
15 15 <legend><%=l(:label_information_plural)%></legend>
16 16 <p><%= f.text_field :firstname, :required => true %></p>
17 17 <p><%= f.text_field :lastname, :required => true %></p>
18 18 <p><%= f.text_field :mail, :required => true %></p>
19 19 <p><%= f.select :language, lang_options_for_select %></p>
20 20 <% if Setting.openid? %>
21 21 <p><%= f.text_field :identity_url %></p>
22 22 <% end %>
23 23
24 24 <% @user.custom_field_values.select(&:editable?).each do |value| %>
25 25 <p><%= custom_field_tag_with_label :user, value %></p>
26 26 <% end %>
27 27 <%= call_hook(:view_my_account, :user => @user, :form => f) %>
28 28 </fieldset>
29 29
30 30 <%= submit_tag l(:button_save) %>
31 31 </div>
32 32
33 33 <div class="splitcontentright">
34 34 <fieldset class="box">
35 35 <legend><%=l(:field_mail_notification)%></legend>
36 36 <%= render :partial => 'users/mail_notifications' %>
37 37 </fieldset>
38 38
39 39 <fieldset class="box tabular">
40 40 <legend><%=l(:label_preferences)%></legend>
41 41 <%= render :partial => 'users/preferences' %>
42 42 </fieldset>
43 43
44 44 </div>
45 45 <% end %>
46 46
47 47 <% content_for :sidebar do %>
48 48 <%= render :partial => 'sidebar' %>
49 49 <% end %>
50 50
51 51 <% html_title(l(:label_my_account)) -%>
@@ -1,11 +1,11
1 1 <h2><%=l(:label_confirmation)%></h2>
2 2 <div class="warning">
3 3 <p><%= simple_format l(:text_account_destroy_confirmation)%></p>
4 4 <p>
5 <% form_tag({}) do %>
5 <%= form_tag({}) do %>
6 6 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
7 7 <%= submit_tag l(:button_delete_my_account) %> |
8 8 <%= link_to l(:button_cancel), :action => 'account' %>
9 9 <% end %>
10 10 </p>
11 11 </div>
@@ -1,109 +1,109
1 1 <script language="JavaScript">
2 2 //<![CDATA[
3 3 function recreateSortables() {
4 4 Sortable.destroy('list-top');
5 5 Sortable.destroy('list-left');
6 6 Sortable.destroy('list-right');
7 7
8 8 Sortable.create("list-top", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('<%= url_for(:controller => 'my', :action => 'order_blocks', :group => 'top') %>', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-top")})}, only:'mypage-box', tag:'div'})
9 9 Sortable.create("list-left", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('<%= url_for(:controller => 'my', :action => 'order_blocks', :group => 'left') %>', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-left")})}, only:'mypage-box', tag:'div'})
10 10 Sortable.create("list-right", {constraint:false, containment:['list-top','list-left','list-right'], dropOnEmpty:true, handle:'handle', onUpdate:function(){new Ajax.Request('<%= url_for(:controller => 'my', :action => 'order_blocks', :group => 'right') %>', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize("list-right")})}, only:'mypage-box', tag:'div'})
11 11 }
12 12
13 13 function updateSelect() {
14 14 s = $('block-select')
15 15 for (var i = 0; i < s.options.length; i++) {
16 16 if ($('block_' + s.options[i].value)) {
17 17 s.options[i].disabled = true;
18 18 } else {
19 19 s.options[i].disabled = false;
20 20 }
21 21 }
22 22 s.options[0].selected = true;
23 23 }
24 24
25 25 function afterAddBlock() {
26 26 recreateSortables();
27 27 updateSelect();
28 28 }
29 29
30 30 function removeBlock(block) {
31 31 Effect.DropOut(block);
32 32 updateSelect();
33 33 }
34 34 //]]>
35 35 </script>
36 36
37 37 <div class="contextual">
38 <% form_tag({:action => "add_block"}, :id => "block-form") do %>
38 <%= form_tag({:action => "add_block"}, :id => "block-form") do %>
39 39 <%= label_tag('block-select', l(:label_my_page_block)) %>:
40 40 <%= select_tag 'block',
41 41 "<option></option>".html_safe + options_for_select(@block_options),
42 42 :id => "block-select" %>
43 43 <%= link_to_remote l(:button_add),
44 44 {:url => { :action => "add_block" },
45 45 :with => "Form.serialize('block-form')",
46 46 :update => "list-top",
47 47 :position => :top,
48 48 :complete => "afterAddBlock();"
49 49 }, :class => 'icon icon-add'
50 50 %>
51 51 <% end %>
52 52 <%= link_to l(:button_back), {:action => 'page'}, :class => 'icon icon-cancel' %>
53 53 </div>
54 54
55 55 <h2><%=l(:label_my_page)%></h2>
56 56
57 57 <div id="list-top" class="block-receiver">
58 58 <% @blocks['top'].each do |b|
59 59 next unless MyController::BLOCKS.keys.include? b %>
60 60 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
61 61 <% end if @blocks['top'] %>
62 62 </div>
63 63
64 64 <div id="list-left" class="splitcontentleft block-receiver">
65 65 <% @blocks['left'].each do |b|
66 66 next unless MyController::BLOCKS.keys.include? b %>
67 67 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
68 68 <% end if @blocks['left'] %>
69 69 </div>
70 70
71 71 <div id="list-right" class="splitcontentright block-receiver">
72 72 <% @blocks['right'].each do |b|
73 73 next unless MyController::BLOCKS.keys.include? b %>
74 74 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
75 75 <% end if @blocks['right'] %>
76 76 </div>
77 77
78 78 <%= sortable_element 'list-top',
79 79 :tag => 'div',
80 80 :only => 'mypage-box',
81 81 :handle => "handle",
82 82 :dropOnEmpty => true,
83 83 :containment => ['list-top', 'list-left', 'list-right'],
84 84 :constraint => false,
85 85 :url => { :action => "order_blocks", :group => "top" }
86 86 %>
87 87
88 88 <%= sortable_element 'list-left',
89 89 :tag => 'div',
90 90 :only => 'mypage-box',
91 91 :handle => "handle",
92 92 :dropOnEmpty => true,
93 93 :containment => ['list-top', 'list-left', 'list-right'],
94 94 :constraint => false,
95 95 :url => { :action => "order_blocks", :group => "left" }
96 96 %>
97 97
98 98 <%= sortable_element 'list-right',
99 99 :tag => 'div',
100 100 :only => 'mypage-box',
101 101 :handle => "handle",
102 102 :dropOnEmpty => true,
103 103 :containment => ['list-top', 'list-left', 'list-right'],
104 104 :constraint => false,
105 105 :url => { :action => "order_blocks", :group => "right" }
106 106 %>
107 107
108 108 <%= javascript_tag "updateSelect()" %>
109 109 <% html_title(l(:label_my_page)) -%>
@@ -1,22 +1,22
1 1 <h2><%=l(:button_change_password)%></h2>
2 2
3 3 <%= error_messages_for 'user' %>
4 4
5 <% form_tag({}, :class => "tabular") do %>
5 <%= form_tag({}, :class => "tabular") do %>
6 6 <div class="box">
7 7 <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
8 8 <%= password_field_tag 'password', nil, :size => 25 %></p>
9 9
10 10 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
11 11 <%= password_field_tag 'new_password', nil, :size => 25 %>
12 12 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
13 13
14 14 <p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
15 15 <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
16 16 </div>
17 17 <%= submit_tag l(:button_apply) %>
18 18 <% end %>
19 19
20 20 <% content_for :sidebar do %>
21 21 <%= render :partial => 'sidebar' %>
22 22 <% end %>
@@ -1,17 +1,17
1 1 <h2><%=l(:label_news)%></h2>
2 2
3 <% labelled_form_for @news, :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
3 <%= labelled_form_for @news, :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <%= link_to_remote l(:label_preview),
7 7 { :url => preview_news_path(:project_id => @project),
8 8 :method => 'get',
9 9 :update => 'preview',
10 10 :with => "Form.serialize('news-form')"
11 11 }, :accesskey => accesskey(:preview) %>
12 12 <% end %>
13 13 <div id="preview" class="wiki"></div>
14 14
15 15 <% content_for :header_tags do %>
16 16 <%= stylesheet_link_tag 'scm' %>
17 17 <% end %>
@@ -1,51 +1,51
1 1 <div class="contextual">
2 2 <%= link_to(l(:label_news_new),
3 3 new_project_news_path(@project),
4 4 :class => 'icon icon-add',
5 5 :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project && User.current.allowed_to?(:manage_news, @project) %>
6 6 </div>
7 7
8 8 <div id="add-news" style="display:none;">
9 9 <h2><%=l(:label_news_new)%></h2>
10 <% labelled_form_for @news, :url => project_news_index_path(@project),
10 <%= labelled_form_for @news, :url => project_news_index_path(@project),
11 11 :html => { :id => 'news-form', :multipart => true } do |f| %>
12 12 <%= render :partial => 'news/form', :locals => { :f => f } %>
13 13 <%= submit_tag l(:button_create) %>
14 14 <%= link_to_remote l(:label_preview),
15 15 { :url => preview_news_path(:project_id => @project),
16 16 :method => 'get',
17 17 :update => 'preview',
18 18 :with => "Form.serialize('news-form')"
19 19 }, :accesskey => accesskey(:preview) %> |
20 20 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %>
21 21 <% end if @project %>
22 22 <div id="preview" class="wiki"></div>
23 23 </div>
24 24
25 25 <h2><%=l(:label_news_plural)%></h2>
26 26
27 27 <% if @newss.empty? %>
28 28 <p class="nodata"><%= l(:label_no_data) %></p>
29 29 <% else %>
30 30 <% @newss.each do |news| %>
31 31 <h3><%= avatar(news.author, :size => "24") %><%= link_to_project(news.project) + ': ' unless news.project == @project %>
32 32 <%= link_to h(news.title), news_path(news) %>
33 33 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
34 34 <p class="author"><%= authoring news.created_on, news.author %></p>
35 35 <div class="wiki">
36 36 <%= textilizable(news, :description) %>
37 37 </div>
38 38 <% end %>
39 39 <% end %>
40 40 <p class="pagination"><%= pagination_links_full @news_pages %></p>
41 41
42 42 <% other_formats_links do |f| %>
43 43 <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
44 44 <% end %>
45 45
46 46 <% content_for :header_tags do %>
47 47 <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
48 48 <%= stylesheet_link_tag 'scm' %>
49 49 <% end %>
50 50
51 51 <% html_title(l(:label_news_plural)) -%>
@@ -1,14 +1,14
1 1 <h2><%=l(:label_news_new)%></h2>
2 2
3 <% labelled_form_for @news, :url => project_news_index_path(@project),
3 <%= labelled_form_for @news, :url => project_news_index_path(@project),
4 4 :html => { :id => 'news-form', :multipart => true } do |f| %>
5 5 <%= render :partial => 'news/form', :locals => { :f => f } %>
6 6 <%= submit_tag l(:button_create) %>
7 7 <%= link_to_remote l(:label_preview),
8 8 { :url => preview_news_path(:project_id => @project),
9 9 :method => 'get',
10 10 :update => 'preview',
11 11 :with => "Form.serialize('news-form')"
12 12 }, :accesskey => accesskey(:preview) %>
13 13 <% end %>
14 14 <div id="preview" class="wiki"></div>
@@ -1,71 +1,71
1 1 <div class="contextual">
2 2 <%= watcher_tag(@news, User.current) %>
3 3 <%= link_to(l(:button_edit),
4 4 edit_news_path(@news),
5 5 :class => 'icon icon-edit',
6 6 :accesskey => accesskey(:edit),
7 7 :onclick => 'Element.show("edit-news"); return false;') if User.current.allowed_to?(:manage_news, @project) %>
8 8 <%= link_to(l(:button_delete),
9 9 news_path(@news),
10 10 :confirm => l(:text_are_you_sure),
11 11 :method => :delete,
12 12 :class => 'icon icon-del') if User.current.allowed_to?(:manage_news, @project) %>
13 13 </div>
14 14
15 15 <h2><%= avatar(@news.author, :size => "24") %><%=h @news.title %></h2>
16 16
17 17 <% if authorize_for('news', 'edit') %>
18 18 <div id="edit-news" style="display:none;">
19 <% labelled_form_for :news, @news, :url => news_path(@news),
19 <%= labelled_form_for :news, @news, :url => news_path(@news),
20 20 :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
21 21 <%= render :partial => 'form', :locals => { :f => f } %>
22 22 <%= submit_tag l(:button_save) %>
23 23 <%= link_to_remote l(:label_preview),
24 24 { :url => preview_news_path(:project_id => @project),
25 25 :method => 'get',
26 26 :update => 'preview',
27 27 :with => "Form.serialize('news-form')"
28 28 }, :accesskey => accesskey(:preview) %> |
29 29 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("edit-news"); return false;' %>
30 30 <% end %>
31 31 <div id="preview" class="wiki"></div>
32 32 </div>
33 33 <% end %>
34 34
35 35 <p><% unless @news.summary.blank? %><em><%=h @news.summary %></em><br /><% end %>
36 36 <span class="author"><%= authoring @news.created_on, @news.author %></span></p>
37 37 <div class="wiki">
38 38 <%= textilizable(@news, :description) %>
39 39 </div>
40 40 <%= link_to_attachments @news %>
41 41 <br />
42 42
43 43 <div id="comments" style="margin-bottom:16px;">
44 44 <h3 class="comments"><%= l(:label_comment_plural) %></h3>
45 45 <% @comments.each do |comment| %>
46 46 <% next if comment.new_record? %>
47 47 <div class="contextual">
48 48 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
49 49 :confirm => l(:text_are_you_sure), :method => :delete, :title => l(:button_delete) %>
50 50 </div>
51 51 <h4><%= avatar(comment.author, :size => "24") %><%= authoring comment.created_on, comment.author %></h4>
52 52 <%= textilizable(comment.comments) %>
53 53 <% end if @comments.any? %>
54 54 </div>
55 55
56 56 <% if @news.commentable? %>
57 57 <p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p>
58 <% form_tag({:controller => 'comments', :action => 'create', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %>
58 <%= form_tag({:controller => 'comments', :action => 'create', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %>
59 59 <div class="box">
60 60 <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
61 61 <%= wikitoolbar_for 'comment_comments' %>
62 62 </div>
63 63 <p><%= submit_tag l(:button_add) %></p>
64 64 <% end %>
65 65 <% end %>
66 66
67 67 <% html_title @news.title -%>
68 68
69 69 <% content_for :header_tags do %>
70 70 <%= stylesheet_link_tag 'scm' %>
71 71 <% end %>
@@ -1,4 +1,4
1 <% labelled_form_for @project do |f| %>
1 <%= labelled_form_for @project do |f| %>
2 2 <%= render :partial => 'form', :locals => { :f => f } %>
3 3 <%= submit_tag l(:button_save) %>
4 4 <% end %>
@@ -1,20 +1,20
1 1 <h2><%=l(:label_project_new)%></h2>
2 2
3 <% labelled_form_for @project, :url => { :action => "copy" } do |f| %>
3 <%= labelled_form_for @project, :url => { :action => "copy" } do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5
6 6 <fieldset class="box tabular"><legend><%= l(:button_copy) %></legend>
7 7 <label class="block"><%= check_box_tag 'only[]', 'members', true %> <%= l(:label_member_plural) %> (<%= @source_project.members.count %>)</label>
8 8 <label class="block"><%= check_box_tag 'only[]', 'versions', true %> <%= l(:label_version_plural) %> (<%= @source_project.versions.count %>)</label>
9 9 <label class="block"><%= check_box_tag 'only[]', 'issue_categories', true %> <%= l(:label_issue_category_plural) %> (<%= @source_project.issue_categories.count %>)</label>
10 10 <label class="block"><%= check_box_tag 'only[]', 'issues', true %> <%= l(:label_issue_plural) %> (<%= @source_project.issues.count %>)</label>
11 11 <label class="block"><%= check_box_tag 'only[]', 'queries', true %> <%= l(:label_query_plural) %> (<%= @source_project.queries.count %>)</label>
12 12 <label class="block"><%= check_box_tag 'only[]', 'boards', true %> <%= l(:label_board_plural) %> (<%= @source_project.boards.count %>)</label>
13 13 <label class="block"><%= check_box_tag 'only[]', 'wiki', true %> <%= l(:label_wiki_page_plural) %> (<%= @source_project.wiki.nil? ? 0 : @source_project.wiki.pages.count %>)</label>
14 14 <%= hidden_field_tag 'only[]', '' %>
15 15 <br />
16 16 <label class="block"><%= check_box_tag 'notifications', 1, params[:notifications] %> <%= l(:label_project_copy_notifications) %></label>
17 17 </fieldset>
18 18
19 19 <%= submit_tag l(:button_copy) %>
20 20 <% end %>
@@ -1,16 +1,16
1 1 <h2><%=l(:label_confirmation)%></h2>
2 2 <div class="warning">
3 3 <p><strong><%=h @project_to_destroy %></strong><br />
4 4 <%=l(:text_project_destroy_confirmation)%>
5 5
6 6 <% if @project_to_destroy.descendants.any? %>
7 7 <br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
8 8 <% end %>
9 9 </p>
10 10 <p>
11 <% form_tag(project_path(@project_to_destroy), :method => :delete) do %>
11 <%= form_tag(project_path(@project_to_destroy), :method => :delete) do %>
12 12 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
13 13 <%= submit_tag l(:button_delete) %>
14 14 <% end %>
15 15 </p>
16 16 </div>
@@ -1,28 +1,28
1 1 <% content_for :header_tags do %>
2 2 <%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
3 3 <% end %>
4 4
5 5 <div class="contextual">
6 6 <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
7 <%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
7 <%= link_to(l(:label_issue_view_all), issues_path) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
8 8 <%= link_to(l(:label_overall_spent_time), time_entries_path) + ' |' if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
9 9 <%= link_to l(:label_overall_activity),
10 10 { :controller => 'activities', :action => 'index',
11 11 :id => nil } %>
12 12 </div>
13 13
14 14 <h2><%=l(:label_project_plural)%></h2>
15 15
16 16 <%= render_project_hierarchy(@projects)%>
17 17
18 18 <% if User.current.logged? %>
19 19 <p style="text-align:right;">
20 20 <span class="my-project"><%= l(:label_my_projects) %></span>
21 21 </p>
22 22 <% end %>
23 23
24 24 <% other_formats_links do |f| %>
25 25 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
26 26 <% end %>
27 27
28 28 <% html_title(l(:label_project_plural)) -%>
@@ -1,8 +1,8
1 1 <h2><%=l(:label_project_new)%></h2>
2 2
3 <% labelled_form_for @project do |f| %>
3 <%= labelled_form_for @project do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
7 7 <%= javascript_tag "Form.Element.focus('project_name');" %>
8 8 <% end %>
@@ -1,42 +1,42
1 <% form_tag(project_enumerations_path(@project), :method => :put, :class => "tabular") do %>
1 <%= form_tag(project_enumerations_path(@project), :method => :put, :class => "tabular") do %>
2 2
3 3 <table class="list">
4 4 <thead><tr>
5 5 <th><%= l(:field_name) %></th>
6 6 <th><%= l(:enumeration_system_activity) %></th>
7 7 <% TimeEntryActivity.new.available_custom_fields.each do |value| %>
8 8 <th><%= h value.name %></th>
9 9 <% end %>
10 10 <th style="width:15%;"><%= l(:field_active) %></th>
11 11 </tr></thead>
12 12
13 13 <% @project.activities(true).each do |enumeration| %>
14 <% fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %>
14 <%= fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %>
15 15 <tr class="<%= cycle('odd', 'even') %>">
16 16 <td>
17 17 <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %>
18 18 <%= h(enumeration) %>
19 19 </td>
20 20 <td align="center" style="width:15%;"><%= checked_image !enumeration.project %></td>
21 21 <% enumeration.custom_field_values.each do |value| %>
22 22 <td align="center">
23 23 <%= custom_field_tag "enumerations[#{enumeration.id}]", value %>
24 24 </td>
25 25 <% end %>
26 26 <td align="center" style="width:15%;">
27 27 <%= ff.check_box :active %>
28 28 </td>
29 29 </tr>
30 30 <% end %>
31 31 <% end %>
32 32 </table>
33 33
34 34 <div class="contextual">
35 35 <%= link_to(l(:button_reset), project_enumerations_path(@project),
36 36 :method => :delete,
37 37 :confirm => l(:text_are_you_sure),
38 38 :class => 'icon icon-del') %>
39 39 </div>
40 40
41 41 <%= submit_tag l(:button_save) %>
42 42 <% end %>
@@ -1,20 +1,20
1 <% form_for :project, @project,
1 <%= form_for @project,
2 2 :url => { :action => 'modules', :id => @project },
3 3 :html => {:id => 'modules-form',
4 4 :method => :post} do |f| %>
5 5
6 6 <div class="box">
7 7 <fieldset>
8 8 <legend><%= l(:text_select_project_modules) %></legend>
9 9
10 10 <% Redmine::AccessControl.available_project_modules.each do |m| %>
11 11 <p><label><%= check_box_tag 'enabled_module_names[]', m, @project.module_enabled?(m) -%>
12 12 <%= l_or_humanize(m, :prefix => "project_module_") %></label></p>
13 13 <% end %>
14 14 </fieldset>
15 15 </div>
16 16
17 17 <p><%= check_all_links 'modules-form' %></p>
18 18 <p><%= submit_tag l(:button_save) %></p>
19 19
20 20 <% end %>
@@ -1,82 +1,82
1 1 <div class="contextual">
2 2 <% if User.current.allowed_to?(:add_subprojects, @project) %>
3 3 <%= link_to l(:label_subproject_new), {:controller => 'projects', :action => 'new', :parent_id => @project}, :class => 'icon icon-add' %>
4 4 <% end %>
5 5 </div>
6 6
7 7 <h2><%=l(:label_overview)%></h2>
8 8
9 9 <div class="splitcontentleft">
10 10 <div class="wiki">
11 11 <%= textilizable @project.description %>
12 12 </div>
13 13 <ul>
14 14 <% unless @project.homepage.blank? %>
15 <li><%=l(:field_homepage)%>: <%= auto_link(h(@project.homepage)).html_safe %></li>
15 <li><%=l(:field_homepage)%>: <%= link_to h(@project.homepage), @project.homepage %></li>
16 16 <% end %>
17 17 <% if @subprojects.any? %>
18 18 <li><%=l(:label_subproject_plural)%>:
19 19 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ").html_safe %></li>
20 20 <% end %>
21 21 <% @project.visible_custom_field_values.each do |custom_value| %>
22 22 <% if !custom_value.value.blank? %>
23 23 <li><%=h custom_value.custom_field.name %>: <%=h show_value(custom_value) %></li>
24 24 <% end %>
25 25 <% end %>
26 26 </ul>
27 27
28 28 <% if User.current.allowed_to?(:view_issues, @project) %>
29 29 <div class="issues box">
30 30 <h3><%=l(:label_issue_tracking)%></h3>
31 31 <ul>
32 32 <% for tracker in @trackers %>
33 33 <li><%= link_to h(tracker.name), :controller => 'issues', :action => 'index', :project_id => @project,
34 34 :set_filter => 1,
35 35 "tracker_id" => tracker.id %>:
36 36 <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
37 37 :total => @total_issues_by_tracker[tracker].to_i) %>
38 38 </li>
39 39 <% end %>
40 40 </ul>
41 41 <p>
42 42 <%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %>
43 43 <% if User.current.allowed_to?(:view_calendar, @project, :global => true) %>
44 44 | <%= link_to(l(:label_calendar), :controller => 'calendars', :action => 'show', :project_id => @project) %>
45 45 <% end %>
46 46 <% if User.current.allowed_to?(:view_gantt, @project, :global => true) %>
47 47 | <%= link_to(l(:label_gantt), :controller => 'gantts', :action => 'show', :project_id => @project) %>
48 48 <% end %>
49 49 </p>
50 50 </div>
51 51 <% end %>
52 52 <%= call_hook(:view_projects_show_left, :project => @project) %>
53 53 </div>
54 54
55 55 <div class="splitcontentright">
56 56 <%= render :partial => 'members_box' %>
57 57
58 58 <% if @news.any? && authorize_for('news', 'index') %>
59 59 <div class="news box">
60 60 <h3><%=l(:label_news_latest)%></h3>
61 61 <%= render :partial => 'news/news', :collection => @news %>
62 62 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
63 63 </div>
64 64 <% end %>
65 65 <%= call_hook(:view_projects_show_right, :project => @project) %>
66 66 </div>
67 67
68 68 <% content_for :sidebar do %>
69 69 <% if @total_hours.present? %>
70 70 <h3><%= l(:label_spent_time) %></h3>
71 71 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
72 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'index', :project_id => @project}) %> |
73 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
72 <p><%= link_to(l(:label_details), project_time_entries_path(@project)) %> |
73 <%= link_to(l(:label_report), report_project_time_entries_path(@project)) %></p>
74 74 <% end %>
75 75 <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %>
76 76 <% end %>
77 77
78 78 <% content_for :header_tags do %>
79 79 <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
80 80 <% end %>
81 81
82 82 <% html_title(l(:label_overview)) -%>
@@ -1,53 +1,53
1 1 <table style="width:100%">
2 2 <tr>
3 3 <td>
4 4 <table>
5 5 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
6 6 <% field = filter[0]
7 7 options = filter[1] %>
8 <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
8 <tr <%= 'style="display:none;"'.html_safe unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
9 9 <td class="field">
10 10 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
11 11 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
12 12 </td>
13 13 <td class="operator">
14 14 <%= label_tag "operators_#{field}", l(:description_filter), :class => "hidden-for-sighted" %>
15 15 <%= select_tag "op[#{field}]", options_for_select(operators_for_select(options[:type]),
16 16 query.operator_for(field)), :id => "operators_#{field}",
17 17 :onchange => "toggle_operator('#{field}');" %>
18 18 </td>
19 19 <td class="values">
20 20 <div id="div_values_<%= field %>" style="display:none;">
21 21 <% case options[:type]
22 22 when :list, :list_optional, :list_status, :list_subprojects %>
23 23 <span class="span_values_<%= field %>">
24 24 <%= select_tag "v[#{field}][]", options_for_select(options[:values], query.values_for(field)), :class => "values_#{field}", :id => "values_#{field}_1", :multiple => (query.values_for(field) && query.values_for(field).length > 1) %>
25 25 <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('values_#{field}_1');" %>
26 26 </span>
27 27 <% when :date, :date_past %>
28 28 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 10, :class => "values_#{field}", :id => "values_#{field}_1" %> <%= calendar_for "values_#{field}_1" %></span>
29 29 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :size => 10, :class => "values_#{field}", :id => "values_#{field}_2" %> <%= calendar_for "values_#{field}_2" %></span>
30 30 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 3, :class => "values_#{field}" %> <%= l(:label_day_plural) %></span>
31 31 <% when :string, :text %>
32 32 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}", :size => 30 %></span>
33 33 <% when :integer, :float %>
34 34 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}_1", :size => 6 %></span>
35 35 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :class => "values_#{field}", :id => "values_#{field}_2", :size => 6 %></span>
36 36 <% end %>
37 37 </div>
38 38 <script type="text/javascript">toggle_filter('<%= field %>');</script>
39 39 </td>
40 40 </tr>
41 41 <% end %>
42 42 </table>
43 43 </td>
44 44 <td class="add-filter">
45 45 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
46 46 <%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact),
47 47 :onchange => "add_filter();",
48 48 :name => nil %>
49 49 </td>
50 50 </tr>
51 51 </table>
52 52 <%= hidden_field_tag 'f[]', '' %>
53 53 <%= javascript_tag 'Event.observe(window,"load",apply_filters_observer);' %>
@@ -1,6 +1,6
1 1 <h2><%= l(:label_query) %></h2>
2 2
3 <% form_tag(query_path(@query), :onsubmit => 'selectAllOptions("selected_columns");', :method => :put) do %>
3 <%= form_tag(query_path(@query), :onsubmit => 'selectAllOptions("selected_columns");', :method => :put) do %>
4 4 <%= render :partial => 'form', :locals => {:query => @query} %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= l(:label_query_new) %></h2>
2 2
3 <% form_tag(@project ? project_queries_path : queries_path, :onsubmit => 'selectAllOptions("selected_columns");') do %>
3 <%= form_tag(@project ? project_queries_path(@project) : queries_path, :onsubmit => 'selectAllOptions("selected_columns");') do %>
4 4 <%= render :partial => 'form', :locals => {:query => @query} %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,34 +1,34
1 1 <% content_for :header_tags do %>
2 2 <%= javascript_include_tag 'repository_navigation' %>
3 3 <% end %>
4 4
5 5 <%= link_to l(:label_statistics),
6 6 {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param},
7 7 :class => 'icon icon-stats' if @repository.supports_all_revisions? %>
8 8
9 <% form_tag({:action => controller.action_name,
9 <%= form_tag({:action => controller.action_name,
10 10 :id => @project,
11 11 :repository_id => @repository.identifier_param,
12 12 :path => to_path_param(@path),
13 13 :rev => nil},
14 14 {:method => :get, :id => 'revision_selector'}) do -%>
15 15 <!-- Branches Dropdown -->
16 16 <% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
17 17 | <%= l(:label_branch) %>:
18 18 <%= select_tag :branch,
19 19 options_for_select([''] + @repository.branches, @rev),
20 20 :id => 'branch' %>
21 21 <% end -%>
22 22
23 23 <% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
24 24 | <%= l(:label_tag) %>:
25 25 <%= select_tag :tag,
26 26 options_for_select([''] + @repository.tags, @rev),
27 27 :id => 'tag' %>
28 28 <% end -%>
29 29
30 30 <% if @repository.supports_all_revisions? %>
31 31 | <%= l(:label_revision) %>:
32 32 <%= text_field_tag 'rev', @rev, :size => 8 %>
33 33 <% end %>
34 34 <% end -%>
@@ -1,48 +1,48
1 1 <% show_revision_graph = ( @repository.supports_revision_graph? && path.blank? ) %>
2 2 <%= if show_revision_graph && revisions && revisions.any?
3 3 indexed_commits, graph_space = index_commits(revisions, @repository.branches) do |scmid|
4 4 url_for(
5 5 :controller => 'repositories',
6 6 :action => 'revision',
7 7 :id => project,
8 8 :repository_id => @repository.identifier_param,
9 9 :rev => scmid)
10 10 end
11 11 render :partial => 'revision_graph',
12 12 :locals => {
13 13 :commits => indexed_commits,
14 14 :space => graph_space
15 15 }
16 16 end %>
17 <% form_tag(
17 <%= form_tag(
18 18 {:controller => 'repositories', :action => 'diff', :id => project,
19 19 :repository_id => @repository.identifier_param, :path => to_path_param(path)},
20 20 :method => :get
21 21 ) do %>
22 22 <table class="list changesets">
23 23 <thead><tr>
24 24 <th>#</th>
25 25 <th></th>
26 26 <th></th>
27 27 <th><%= l(:label_date) %></th>
28 28 <th><%= l(:field_author) %></th>
29 29 <th><%= l(:field_comments) %></th>
30 30 </tr></thead>
31 31 <tbody>
32 32 <% show_diff = revisions.size > 1 %>
33 33 <% line_num = 1 %>
34 34 <% revisions.each do |changeset| %>
35 35 <tr class="changeset <%= cycle 'odd', 'even' %>">
36 36 <td class="id"<%= show_revision_graph ? " style=\"padding-left:#{(graph_space + 1) * 20}px\"" : '' %>><%= link_to_revision(changeset, @repository) %></td>
37 37 <td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
38 38 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
39 39 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
40 40 <td class="author"><%= h truncate(changeset.author.to_s, :length => 30) %></td>
41 41 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
42 42 </tr>
43 43 <% line_num += 1 %>
44 44 <% end %>
45 45 </tbody>
46 46 </table>
47 47 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
48 48 <% end %>
@@ -1,42 +1,42
1 1 <h2><%= l(:label_repository) %></h2>
2 2
3 3 <%= simple_format(l(:text_repository_usernames_mapping)) %>
4 4
5 5 <% if @committers.empty? %>
6 6 <p class="nodata"><%= l(:label_no_data) %></p>
7 7 <% else %>
8 8
9 <% form_tag({}) do %>
9 <%= form_tag({}) do %>
10 10 <table class="list">
11 11 <thead>
12 12 <tr>
13 13 <th><%= l(:field_login) %></th>
14 14 <th><%= l(:label_user) %></th>
15 15 </tr>
16 16 </thead>
17 17 <tbody>
18 18 <% i = 0 -%>
19 19 <% @committers.each do |committer, user_id| -%>
20 20 <tr class="<%= cycle 'odd', 'even' %>">
21 21 <td><%=h committer %></td>
22 22 <td>
23 23 <%= hidden_field_tag "committers[#{i}][]", committer %>
24 24 <%= select_tag "committers[#{i}][]",
25 25 content_tag(
26 26 'option',
27 27 "-- #{l :actionview_instancetag_blank_option} --",
28 28 :value => ''
29 29 ) +
30 30 options_from_collection_for_select(
31 31 @users, 'id', 'name', user_id.to_i
32 32 ) %>
33 33 </td>
34 34 </tr>
35 35 <% i += 1 -%>
36 36 <% end -%>
37 37 </tbody>
38 38 </table>
39 39 <p><%= submit_tag(l(:button_update)) %></p>
40 40 <% end %>
41 41
42 42 <% end %>
@@ -1,28 +1,28
1 1 <h2><%= l(:label_revision) %> <%= @diff_format_revisions %> <%=h @path %></h2>
2 2
3 3 <!-- Choose view type -->
4 <% form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
4 <%= form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
5 5 <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
6 6 <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
7 7 <p>
8 8 <label><%= l(:label_view_diff) %></label>
9 9 <%= select_tag 'type',
10 10 options_for_select(
11 11 [[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
12 12 :onchange => "if (this.value != '') {this.form.submit()}" %>
13 13 </p>
14 14 <% end %>
15 15
16 16 <% cache(@cache_key) do -%>
17 17 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
18 18 <% end -%>
19 19
20 20 <% other_formats_links do |f| %>
21 21 <%= f.link_to 'Diff', :url => params, :caption => 'Unified diff' %>
22 22 <% end %>
23 23
24 24 <% html_title(with_leading_slash(@path), 'Diff') -%>
25 25
26 26 <% content_for :header_tags do %>
27 27 <%= stylesheet_link_tag "scm" %>
28 28 <% end %>
@@ -1,5 +1,5
1 1 <h2><%= l(:label_repository) %></h2>
2 2
3 <% labelled_form_for :repository, @repository, :url => repository_path(@path), :html => {:method => :put} do |f| %>
3 <%= labelled_form_for :repository, @repository, :url => repository_path(@path), :html => {:method => :put} do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <% end %>
@@ -1,5 +1,5
1 1 <h2><%= l(:label_repository_new) %></h2>
2 2
3 <% labelled_form_for :repository, @repository, :url => project_repositories_path(@project) do |f| %>
3 <%= labelled_form_for :repository, @repository, :url => project_repositories_path(@project) do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <% end %>
@@ -1,97 +1,97
1 1 <div class="contextual">
2 2 &#171;
3 3 <% unless @changeset.previous.nil? -%>
4 4 <%= link_to_revision(@changeset.previous, @repository, :text => l(:label_previous)) %>
5 5 <% else -%>
6 6 <%= l(:label_previous) %>
7 7 <% end -%>
8 8 |
9 9 <% unless @changeset.next.nil? -%>
10 10 <%= link_to_revision(@changeset.next, @repository, :text => l(:label_next)) %>
11 11 <% else -%>
12 12 <%= l(:label_next) %>
13 13 <% end -%>
14 14 &#187;&nbsp;
15 15
16 <% form_tag({:controller => 'repositories',
16 <%= form_tag({:controller => 'repositories',
17 17 :action => 'revision',
18 18 :id => @project,
19 19 :repository_id => @repository.identifier_param,
20 20 :rev => nil},
21 21 :method => :get) do %>
22 22 <%= text_field_tag 'rev', @rev, :size => 8 %>
23 23 <%= submit_tag 'OK', :name => nil %>
24 24 <% end %>
25 25 </div>
26 26
27 27 <h2><%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
28 28
29 29 <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %>
30 30 <table class="revision-info">
31 31 <% if @changeset.scmid.present? %>
32 32 <tr>
33 33 <td>ID</td><td><%= h(@changeset.scmid) %></td>
34 34 </tr>
35 35 <% end %>
36 36 <% if @changeset.parents.present? %>
37 37 <tr>
38 38 <td><%= l(:label_parent_revision) %></td>
39 39 <td>
40 40 <%= @changeset.parents.collect{
41 41 |p| link_to_revision(p, @repository, :text => format_revision(p))
42 42 }.join(", ").html_safe %>
43 43 </td>
44 44 </tr>
45 45 <% end %>
46 46 <% if @changeset.children.present? %>
47 47 <tr>
48 48 <td><%= l(:label_child_revision) %></td>
49 49 <td>
50 50 <%= @changeset.children.collect{
51 51 |p| link_to_revision(p, @repository, :text => format_revision(p))
52 52 }.join(", ").html_safe %>
53 53 </td>
54 54 </tr>
55 55 <% end %>
56 56 </table>
57 57 <% end %>
58 58
59 59 <p>
60 60 <span class="author">
61 61 <%= authoring(@changeset.committed_on, @changeset.author) %>
62 62 </span>
63 63 </p>
64 64
65 65 <%= textilizable @changeset.comments %>
66 66
67 67 <% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %>
68 68 <%= render :partial => 'related_issues' %>
69 69 <% end %>
70 70
71 71 <% if User.current.allowed_to?(:browse_repository, @project) %>
72 72 <h3><%= l(:label_attachment_plural) %></h3>
73 73 <ul id="changes-legend">
74 74 <li class="change change-A"><%= l(:label_added) %></li>
75 75 <li class="change change-M"><%= l(:label_modified) %></li>
76 76 <li class="change change-C"><%= l(:label_copied) %></li>
77 77 <li class="change change-R"><%= l(:label_renamed) %></li>
78 78 <li class="change change-D"><%= l(:label_deleted) %></li>
79 79 </ul>
80 80
81 81 <p><%= link_to(l(:label_view_diff),
82 82 :action => 'diff',
83 83 :id => @project,
84 84 :repository_id => @repository.identifier_param,
85 85 :path => "",
86 86 :rev => @changeset.identifier) if @changeset.changes.any? %></p>
87 87
88 88 <div class="changeset-changes">
89 89 <%= render_changeset_changes %>
90 90 </div>
91 91 <% end %>
92 92
93 93 <% content_for :header_tags do %>
94 94 <%= stylesheet_link_tag "scm" %>
95 95 <% end %>
96 96
97 97 <% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%>
@@ -1,33 +1,33
1 1 <div class="contextual">
2 <% form_tag(
3 {:action => 'revision', :id => @project,
2 <%= form_tag(
3 {:controller => 'repositories', :action => 'revision', :id => @project,
4 4 :repository_id => @repository.identifier_param}
5 5 ) do %>
6 6 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
7 7 <%= submit_tag 'OK' %>
8 8 <% end %>
9 9 </div>
10 10
11 11 <h2><%= l(:label_revision_plural) %></h2>
12 12
13 13 <%= render :partial => 'revisions',
14 14 :locals => {:project => @project,
15 15 :path => '',
16 16 :revisions => @changesets,
17 17 :entry => nil } %>
18 18
19 19 <p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p>
20 20
21 21 <% content_for :header_tags do %>
22 22 <%= stylesheet_link_tag "scm" %>
23 23 <%= auto_discovery_link_tag(
24 24 :atom,
25 25 params.merge(
26 26 {:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
27 27 <% end %>
28 28
29 29 <% other_formats_links do |f| %>
30 30 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
31 31 <% end %>
32 32
33 33 <% html_title(l(:label_revision_plural)) -%>
@@ -1,6 +1,6
1 1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=h @role.name %></h2>
2 2
3 <% labelled_form_for @role do |f| %>
3 <%= labelled_form_for @role do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_role_new)%></h2>
2 2
3 <% labelled_form_for @role do |f| %>
3 <%= labelled_form_for @role do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <% end %>
@@ -1,55 +1,55
1 1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_permissions_report)%></h2>
2 2
3 <% form_tag(permissions_roles_path, :id => 'permissions_form') do %>
3 <%= form_tag(permissions_roles_path, :id => 'permissions_form') do %>
4 4 <%= hidden_field_tag 'permissions[0]', '', :id => nil %>
5 5 <div class="autoscroll">
6 6 <table class="list permissions">
7 7 <thead>
8 8 <tr>
9 9 <th><%=l(:label_permissions)%></th>
10 10 <% @roles.each do |role| %>
11 11 <th>
12 12 <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %>
13 13 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')",
14 14 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
15 15 </th>
16 16 <% end %>
17 17 </tr>
18 18 </thead>
19 19 <tbody>
20 20 <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
21 21 <% perms_by_module.keys.sort.each do |mod| %>
22 22 <% unless mod.blank? %>
23 23 <tr class="group open">
24 24 <td>
25 25 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
26 26 <%= l_or_humanize(mod, :prefix => 'project_module_') %>
27 27 </td>
28 28 <% @roles.each do |role| %>
29 29 <td class="role"><%= h(role.name) %></td>
30 30 <% end %>
31 31 </tr>
32 32 <% end %>
33 33 <% perms_by_module[mod].each do |permission| %>
34 34 <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>">
35 35 <td>
36 36 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')",
37 37 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
38 38 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
39 39 </td>
40 40 <% @roles.each do |role| %>
41 41 <td align="center">
42 42 <% if role.setable_permissions.include? permission %>
43 43 <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %>
44 44 <% end %>
45 45 </td>
46 46 <% end %>
47 47 </tr>
48 48 <% end %>
49 49 <% end %>
50 50 </tbody>
51 51 </table>
52 52 </div>
53 53 <p><%= check_all_links 'permissions_form' %></p>
54 54 <p><%= submit_tag l(:button_save) %></p>
55 55 <% end %>
@@ -1,52 +1,52
1 1 <h2><%= l(:label_search) %></h2>
2 2
3 3 <div class="box">
4 <% form_tag({}, :method => :get) do %>
4 <%= form_tag({}, :method => :get) do %>
5 5 <%= label_tag "search-input", l(:description_search), :class => "hidden-for-sighted" %>
6 6 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
7 7 <%= javascript_tag "Field.focus('search-input')" %>
8 8 <%= project_select_tag %>
9 9 <%= hidden_field_tag 'all_words', '', :id => nil %>
10 10 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
11 11 <%= hidden_field_tag 'titles_only', '', :id => nil %>
12 12 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
13 13 </p>
14 14 <p>
15 15 <% @object_types.each do |t| %>
16 16 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
17 17 <% end %>
18 18 </p>
19 19
20 20 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
21 21 <% end %>
22 22 </div>
23 23
24 24 <% if @results %>
25 25 <div id="search-results-counts">
26 26 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
27 27 </div>
28 28
29 29 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
30 30 <dl id="search-results">
31 31 <% @results.each do |e| %>
32 32 <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(h(e.event_title), :length => 255), @tokens), e.event_url %></dt>
33 33 <dd><span class="description"><%= highlight_tokens(h(e.event_description), @tokens) %></span>
34 34 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
35 35 <% end %>
36 36 </dl>
37 37 <% end %>
38 38
39 39 <p><center>
40 40 <% if @pagination_previous_date %>
41 41 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
42 42 params.merge(:previous => 1,
43 43 :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
44 44 <% end %>
45 45 <% if @pagination_next_date %>
46 46 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
47 47 params.merge(:previous => nil,
48 48 :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
49 49 <% end %>
50 50 </center></p>
51 51
52 52 <% html_title(l(:label_search)) -%>
@@ -1,25 +1,25
1 <% form_tag({:action => 'edit', :tab => 'authentication'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'authentication'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_check_box :login_required %></p>
5 5
6 6 <p><%= setting_select :autologin, [[l(:label_disabled), 0]] + [1, 7, 30, 365].collect{|days| [l('datetime.distance_in_words.x_days', :count => days), days.to_s]} %></p>
7 7
8 8 <p><%= setting_select :self_registration, [[l(:label_disabled), "0"],
9 9 [l(:label_registration_activation_by_email), "1"],
10 10 [l(:label_registration_manual_activation), "2"],
11 11 [l(:label_registration_automatic_activation), "3"]] %></p>
12 12
13 13 <p><%= setting_check_box :unsubscribe %></p>
14 14
15 15 <p><%= setting_text_field :password_min_length, :size => 6 %></p>
16 16
17 17 <p><%= setting_check_box :lost_password, :label => :label_password_lost %></p>
18 18
19 19 <p><%= setting_check_box :openid, :disabled => !Object.const_defined?(:OpenID) %></p>
20 20
21 21 <p><%= setting_check_box :rest_api_enabled %></p>
22 22 </div>
23 23
24 24 <%= submit_tag l(:button_save) %>
25 25 <% end %>
@@ -1,22 +1,22
1 <% form_tag({:action => 'edit', :tab => 'display'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'display'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_select :ui_theme, Redmine::Themes.themes.collect {|t| [t.name, t.id]}, :blank => :label_default, :label => :label_theme %></p>
5 5
6 6 <p><%= setting_select :default_language, lang_options_for_select(false) %></p>
7 7
8 8 <p><%= setting_select :start_of_week, [[day_name(1),'1'], [day_name(6),'6'], [day_name(7),'7']], :blank => :label_language_based %></p>
9 9
10 10 <p><%= setting_select :date_format, Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, :blank => :label_language_based %></p>
11 11
12 12 <p><%= setting_select :time_format, Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, :blank => :label_language_based %></p>
13 13
14 14 <p><%= setting_select :user_format, @options[:user_format] %></p>
15 15
16 16 <p><%= setting_check_box :gravatar_enabled %></p>
17 17
18 18 <p><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></p>
19 19 </div>
20 20
21 21 <%= submit_tag l(:button_save) %>
22 22 <% end %>
@@ -1,40 +1,40
1 <% form_tag({:action => 'edit'}) do %>
1 <%= form_tag({:action => 'edit'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_text_field :app_title, :size => 30 %></p>
5 5
6 6 <p><%= setting_text_area :welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit' %></p>
7 7 <%= wikitoolbar_for 'settings_welcome_text' %>
8 8
9 9 <p><%= setting_text_field :attachment_max_size, :size => 6 %> <%= l(:"number.human.storage_units.units.kb") %></p>
10 10
11 11 <p><%= setting_text_field :per_page_options, :size => 20 %>
12 12 <em class="info"><%= l(:text_comma_separated) %></em></p>
13 13
14 14 <p><%= setting_text_field :activity_days_default, :size => 6 %> <%= l(:label_day_plural) %></p>
15 15
16 16 <p><%= setting_text_field :host_name, :size => 60 %>
17 17 <em class="info"><%= l(:label_example) %>: <%= @guessed_host_and_path %></em></p>
18 18
19 19 <p><%= setting_select :protocol, [['HTTP', 'http'], ['HTTPS', 'https']] %></p>
20 20
21 21 <p><%= setting_select :text_formatting, Redmine::WikiFormatting.format_names.collect{|name| [name, name.to_s]}, :blank => :label_none %></p>
22 22
23 23 <p><%= setting_check_box :cache_formatted_text %></p>
24 24
25 25 <p><%= setting_select :wiki_compression, [['Gzip', 'gzip']], :blank => :label_none %></p>
26 26
27 27 <p><%= setting_text_field :feeds_limit, :size => 6 %></p>
28 28
29 29 <p><%= setting_text_field :file_max_size_displayed, :size => 6 %> <%= l(:"number.human.storage_units.units.kb") %></p>
30 30
31 31 <p><%= setting_text_field :diff_max_lines_displayed, :size => 6 %></p>
32 32
33 33 <p><%= setting_text_field :repositories_encodings, :size => 60 %>
34 34 <em class="info"><%= l(:text_comma_separated) %></em></p>
35 35
36 36 <%= call_hook(:view_settings_general_form) %>
37 37 </div>
38 38
39 39 <%= submit_tag l(:button_save) %>
40 40 <% end %>
@@ -1,25 +1,25
1 <% form_tag({:action => 'edit', :tab => 'issues'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'issues'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_check_box :cross_project_issue_relations %></p>
5 5
6 6 <p><%= setting_check_box :issue_group_assignment %></p>
7 7
8 8 <p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
9 9
10 10 <p><%= setting_check_box :display_subprojects_issues %></p>
11 11
12 12 <p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
13 13
14 14 <p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
15 15
16 16 <p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
17 17 </div>
18 18
19 19 <fieldset class="box settings"><legend><%= l(:setting_issue_list_default_columns) %></legend>
20 20 <%= setting_multiselect(:issue_list_default_columns,
21 21 Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label => false) %>
22 22 </fieldset>
23 23
24 24 <%= submit_tag l(:button_save) %>
25 25 <% end %>
@@ -1,23 +1,23
1 <% form_tag({:action => 'edit', :tab => 'mail_handler'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'mail_handler'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p>
5 5 <%= setting_text_area :mail_handler_body_delimiters, :rows => 5 %>
6 6 <em class="info"><%= l(:text_line_separated) %></em>
7 7 </p>
8 8 </div>
9 9
10 10 <div class="box tabular settings">
11 11 <p><%= setting_check_box :mail_handler_api_enabled,
12 12 :onclick => "if (this.checked) { Form.Element.enable('settings_mail_handler_api_key'); } else { Form.Element.disable('settings_mail_handler_api_key'); }"%></p>
13 13
14 14 <p><%= setting_text_field :mail_handler_api_key, :size => 30,
15 15 :id => 'settings_mail_handler_api_key',
16 16 :disabled => !Setting.mail_handler_api_enabled? %>
17 17 <%= link_to_function l(:label_generate_key), "if ($('settings_mail_handler_api_key').disabled == false) { $('settings_mail_handler_api_key').value = randomKey(20) }" %>
18 18 </p>
19 19 </div>
20 20
21 21 <%= submit_tag l(:button_save) %>
22 22
23 23 <% end %>
@@ -1,42 +1,42
1 1 <% if @deliveries %>
2 <% form_tag({:action => 'edit', :tab => 'notifications'}) do %>
2 <%= form_tag({:action => 'edit', :tab => 'notifications'}) do %>
3 3
4 4 <div class="box tabular settings">
5 5 <p><%= setting_text_field :mail_from, :size => 60 %></p>
6 6
7 7 <p><%= setting_check_box :bcc_recipients %></p>
8 8
9 9 <p><%= setting_check_box :plain_text_mail %></p>
10 10
11 11 <p><%= setting_select(:default_notification_option, User.valid_notification_options.collect {|o| [l(o.last), o.first.to_s]}) %></p>
12 12
13 13 </div>
14 14
15 15 <fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend>
16 16 <%= hidden_field_tag 'settings[notified_events][]', '' %>
17 17 <% @notifiables.each do |notifiable| %>
18 18 <%= notification_field notifiable %>
19 19 <br />
20 20 <% end %>
21 21 <p><%= check_all_links('notified_events') %></p>
22 22 </fieldset>
23 23
24 24 <fieldset class="box"><legend><%= l(:setting_emails_header) %></legend>
25 25 <%= setting_text_area :emails_header, :label => false, :class => 'wiki-edit', :rows => 5 %>
26 26 </fieldset>
27 27
28 28 <fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend>
29 29 <%= setting_text_area :emails_footer, :label => false, :class => 'wiki-edit', :rows => 5 %>
30 30 </fieldset>
31 31
32 32 <div style="float:right;">
33 33 <%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %>
34 34 </div>
35 35
36 36 <%= submit_tag l(:button_save) %>
37 37 <% end %>
38 38 <% else %>
39 39 <div class="nodata">
40 40 <%= simple_format(l(:text_email_delivery_not_configured)) %>
41 41 </div>
42 42 <% end %>
@@ -1,17 +1,17
1 <% form_tag({:action => 'edit', :tab => 'projects'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'projects'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_check_box :default_projects_public %></p>
5 5
6 6 <p><%= setting_multiselect(:default_projects_modules,
7 7 Redmine::AccessControl.available_project_modules.collect {|m| [l_or_humanize(m, :prefix => "project_module_"), m.to_s]}) %></p>
8 8
9 9 <p><%= setting_check_box :sequential_project_identifiers %></p>
10 10
11 11 <p><%= setting_select :new_project_user_role_id,
12 12 Role.find_all_givable.collect {|r| [r.name, r.id.to_s]},
13 13 :blank => "--- #{l(:actionview_instancetag_blank_option)} ---" %></p>
14 14 </div>
15 15
16 16 <%= submit_tag l(:button_save) %>
17 17 <% end %>
@@ -1,97 +1,97
1 <% form_tag({:action => 'edit', :tab => 'repositories'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'repositories'}) do %>
2 2
3 3 <fieldset class="box settings enabled_scm">
4 4 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
5 5 <legend><%= l(:setting_enabled_scm) %></legend>
6 6 <table>
7 7 <tr>
8 8 <th></th>
9 9 <th><%= l(:text_scm_command) %></th>
10 10 <th><%= l(:text_scm_command_version) %></th>
11 11 </tr>
12 12 <% Redmine::Scm::Base.all.collect do |choice| %>
13 13 <% scm_class = "Repository::#{choice}".constantize %>
14 14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
15 15 <% setting = :enabled_scm %>
16 16 <% enabled = Setting.send(setting).include?(value) %>
17 17 <tr>
18 18 <td class="scm_name">
19 19 <%=
20 20 check_box_tag(
21 21 "settings[#{setting}][]",
22 22 value,
23 23 enabled)
24 24 %>
25 25 <%= text.to_s %>
26 26 </td>
27 27 <td>
28 28 <% if enabled %>
29 29 <%=
30 30 image_tag(
31 31 (scm_class.scm_available ? 'true.png' : 'exclamation.png'),
32 32 :style => "vertical-align:bottom;"
33 33 )
34 34 %>
35 35 <%= scm_class.scm_command %>
36 36 <% end %>
37 37 </td>
38 38 <td>
39 39 <%= scm_class.scm_version_string if enabled %>
40 40 </td>
41 41 </tr>
42 42 <% end %>
43 43 </table>
44 44 <p><em class="info"><%= l(:text_scm_config) %></em></p>
45 45 </fieldset>
46 46
47 47 <div class="box tabular settings">
48 48 <p><%= setting_check_box :autofetch_changesets %></p>
49 49
50 50 <p><%= setting_check_box :sys_api_enabled,
51 51 :onclick =>
52 52 "if (this.checked) { Form.Element.enable('settings_sys_api_key'); } else { Form.Element.disable('settings_sys_api_key'); }" %></p>
53 53
54 54 <p><%= setting_text_field :sys_api_key,
55 55 :size => 30,
56 56 :id => 'settings_sys_api_key',
57 57 :disabled => !Setting.sys_api_enabled?,
58 58 :label => :setting_mail_handler_api_key %>
59 59 <%= link_to_function l(:label_generate_key),
60 60 "if ($('settings_sys_api_key').disabled == false) { $('settings_sys_api_key').value = randomKey(20) }" %>
61 61 </p>
62 62
63 63 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
64 64 </div>
65 65
66 66 <fieldset class="box tabular settings">
67 67 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
68 68 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
69 69 <em class="info"><%= l(:text_comma_separated) %></em></p>
70 70
71 71 <p><%= setting_text_field :commit_fix_keywords, :size => 30 %>
72 72 &nbsp;<%= l(:label_applied_status) %>: <%= setting_select :commit_fix_status_id,
73 73 [["", 0]] +
74 74 IssueStatus.find(:all).collect{
75 75 |status| [status.name, status.id.to_s]
76 76 },
77 77 :label => false %>
78 78 &nbsp;<%= l(:field_done_ratio) %>: <%= setting_select :commit_fix_done_ratio,
79 79 (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
80 80 :blank => :label_no_change_option,
81 81 :label => false %>
82 82 <em class="info"><%= l(:text_comma_separated) %></em></p>
83 83
84 84 <p><%= setting_check_box :commit_cross_project_ref %></p>
85 85
86 86 <p><%= setting_check_box :commit_logtime_enabled,
87 87 :onclick =>
88 88 "if (this.checked) { Form.Element.enable('settings_commit_logtime_activity_id'); } else { Form.Element.disable('settings_commit_logtime_activity_id'); }"%></p>
89 89
90 90 <p><%= setting_select :commit_logtime_activity_id,
91 91 [[l(:label_default), 0]] +
92 92 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
93 93 :disabled => !Setting.commit_logtime_enabled?%></p>
94 94 </fieldset>
95 95
96 96 <%= submit_tag l(:button_save) %>
97 97 <% end %>
@@ -1,10 +1,10
1 1 <h2><%= l(:label_settings) %>: <%=h @plugin.name %></h2>
2 2
3 3 <div id="settings">
4 <% form_tag({:action => 'plugin'}) do %>
4 <%= form_tag({:action => 'plugin'}) do %>
5 5 <div class="box tabular">
6 6 <%= render :partial => @partial, :locals => {:settings => @settings}%>
7 7 </div>
8 8 <%= submit_tag l(:button_apply) %>
9 9 <% end %>
10 10 </div>
@@ -1,38 +1,38
1 1 <fieldset id="date-range" class="collapsible">
2 2 <legend onclick="toggleFieldset(this);"><%= l(:label_date_range) %></legend>
3 3 <div>
4 4 <p>
5 5 <%= label_tag "period_type_list", l(:description_date_range_list), :class => "hidden-for-sighted" %>
6 6 <%= radio_button_tag 'period_type', '1', !@free_period, :onclick => 'Form.Element.disable("from");Form.Element.disable("to");Form.Element.enable("period");', :id => "period_type_list"%>
7 7 <%= select_tag 'period', options_for_period_select(params[:period]),
8 8 :onchange => 'this.form.submit();',
9 9 :onfocus => '$("period_type_1").checked = true;',
10 10 :disabled => @free_period %>
11 11 </p>
12 12 <p>
13 13 <%= label_tag "period_type_interval", l(:description_date_range_interval), :class => "hidden-for-sighted" %>
14 14 <%= radio_button_tag 'period_type', '2', @free_period, :onclick => 'Form.Element.enable("from");Form.Element.enable("to");Form.Element.disable("period");', :id => "period_type_interval" %>
15 15 <span onclick="$('period_type_interval').checked = true;Form.Element.enable('from');Form.Element.enable('to');Form.Element.disable('period');">
16 16 <%= l(:label_date_from_to,
17 17 :start => ((label_tag "from", l(:description_date_from), :class => "hidden-for-sighted") +
18 18 text_field_tag('from', @from, :size => 10, :disabled => !@free_period) + calendar_for('from')),
19 19 :end => ((label_tag "to", l(:description_date_to), :class => "hidden-for-sighted") +
20 20 text_field_tag('to', @to, :size => 10, :disabled => !@free_period) + calendar_for('to'))).html_safe %>
21 21 </span>
22 22 </p>
23 23 </div>
24 24 </fieldset>
25 25 <p class="buttons">
26 26 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
27 27 <%= link_to l(:button_clear), {:controller => controller_name, :action => action_name, :project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
28 28 </p>
29 29
30 30 <div class="tabs">
31 31 <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
32 32 <ul>
33 33 <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }),
34 :class => (@controller.action_name == 'index' ? 'selected' : nil)) %></li>
34 :class => (action_name == 'index' ? 'selected' : nil)) %></li>
35 35 <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
36 :class => (@controller.action_name == 'report' ? 'selected' : nil)) %></li>
36 :class => (action_name == 'report' ? 'selected' : nil)) %></li>
37 37 </ul>
38 38 </div>
@@ -1,55 +1,55
1 <% form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 3 <div class="autoscroll">
4 4 <table class="list time-entries">
5 5 <thead>
6 6 <tr>
7 7 <th class="checkbox hide-when-print">
8 8 <%= link_to image_tag('toggle_check.png'),
9 9 {},
10 10 :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
11 11 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
12 12 </th>
13 13 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
14 14 <%= sort_header_tag('user', :caption => l(:label_member)) %>
15 15 <%= sort_header_tag('activity', :caption => l(:label_activity)) %>
16 16 <%= sort_header_tag('project', :caption => l(:label_project)) %>
17 17 <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
18 18 <th><%= l(:field_comments) %></th>
19 19 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
20 20 <th></th>
21 21 </tr>
22 22 </thead>
23 23 <tbody>
24 24 <% entries.each do |entry| -%>
25 25 <tr class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
26 26 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
27 27 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
28 28 <td class="user"><%= link_to_user(entry.user) %></td>
29 29 <td class="activity"><%=h entry.activity %></td>
30 30 <td class="project"><%= link_to_project(entry.project) %></td>
31 31 <td class="subject">
32 32 <% if entry.issue -%>
33 33 <%= entry.issue.visible? ? link_to_issue(entry.issue, :truncate => 50) : "##{entry.issue.id}" -%>
34 34 <% end -%>
35 35 </td>
36 36 <td class="comments"><%=h entry.comments %></td>
37 37 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
38 38 <td align="center">
39 39 <% if entry.editable_by?(User.current) -%>
40 40 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
41 41 :title => l(:button_edit) %>
42 42 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
43 43 :confirm => l(:text_are_you_sure),
44 44 :method => :delete,
45 45 :title => l(:button_delete) %>
46 46 <% end -%>
47 47 </td>
48 48 </tr>
49 49 <% end -%>
50 50 </tbody>
51 51 </table>
52 52 </div>
53 53 <% end -%>
54 54
55 55 <%= context_menu time_entries_context_menu_path %>
@@ -1,50 +1,50
1 1 <h2><%= l(:label_bulk_edit_selected_time_entries) %></h2>
2 2
3 3 <ul>
4 4 <%= @time_entries.collect {|i| content_tag('li',
5 5 link_to(h("#{i.spent_on.strftime("%Y-%m-%d")} - #{i.project}: #{l(:label_f_hour_plural, :value => i.hours)}"),
6 6 { :action => 'edit', :id => i })
7 7 )}.join("\n").html_safe %>
8 8 </ul>
9 9
10 <% form_tag(:action => 'bulk_update') do %>
10 <%= form_tag(:action => 'bulk_update') do %>
11 11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
12 12 <div class="box tabular">
13 13 <div>
14 14 <p>
15 15 <label><%= l(:field_issue) %></label>
16 16 <%= text_field :time_entry, :issue_id, :size => 6 %>
17 17 </p>
18 18
19 19 <p>
20 20 <label><%= l(:field_spent_on) %></label>
21 21 <%= text_field :time_entry, :spent_on, :size => 10 %><%= calendar_for('time_entry_spent_on') %>
22 22 </p>
23 23
24 24 <p>
25 25 <label><%= l(:field_hours) %></label>
26 26 <%= text_field :time_entry, :hours, :size => 6 %>
27 27 </p>
28 28
29 29 <% if @available_activities.any? %>
30 30 <p>
31 31 <label><%= l(:field_activity) %></label>
32 32 <%= select_tag('time_entry[activity_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_activities, :id, :name)) %>
33 33 </p>
34 34 <% end %>
35 35
36 36 <p>
37 37 <label><%= l(:field_comments) %></label>
38 38 <%= text_field(:time_entry, :comments, :size => 100) %>
39 39 </p>
40 40
41 41 <% @custom_fields.each do |custom_field| %>
42 42 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('time_entry', custom_field, @projects) %></p>
43 43 <% end %>
44 44
45 45 <%= call_hook(:view_time_entries_bulk_edit_details_bottom, { :time_entries => @time_entries }) %>
46 46 </div>
47 47 </div>
48 48
49 49 <p><%= submit_tag l(:button_submit) %></p>
50 50 <% end %>
@@ -1,6 +1,6
1 1 <h2><%= l(:label_spent_time) %></h2>
2 2
3 <% labelled_form_for @time_entry, :url => project_time_entry_path(@time_entry.project, @time_entry) do |f| %>
3 <%= labelled_form_for @time_entry, :url => project_time_entry_path(@time_entry.project, @time_entry) do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
@@ -1,33 +1,33
1 1 <div class="contextual">
2 2 <%= link_to l(:button_log_time),
3 3 {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue},
4 4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
5 5 </div>
6 6
7 7 <%= render_timelog_breadcrumb %>
8 8
9 9 <h2><%= l(:label_spent_time) %></h2>
10 10
11 <% form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
11 <%= form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
12 12 <%= render :partial => 'date_range' %>
13 13 <% end %>
14 14
15 15 <div class="total-hours">
16 16 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
17 17 </div>
18 18
19 19 <% unless @entries.empty? %>
20 20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
21 21 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
22 22
23 23 <% other_formats_links do |f| %>
24 24 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
25 25 <%= f.link_to 'CSV', :url => params %>
26 26 <% end %>
27 27 <% end %>
28 28
29 29 <% html_title l(:label_spent_time), l(:label_details) %>
30 30
31 31 <% content_for :header_tags do %>
32 32 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
33 33 <% end %>
@@ -1,7 +1,7
1 1 <h2><%= l(:label_spent_time) %></h2>
2 2
3 <% labelled_form_for @time_entry, :url => time_entries_path do |f| %>
3 <%= labelled_form_for @time_entry, :url => time_entries_path do |f| %>
4 4 <%= render :partial => 'form', :locals => {:f => f} %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
7 7 <% end %>
@@ -1,72 +1,74
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 3 </div>
4 4
5 5 <%= render_timelog_breadcrumb %>
6 6
7 7 <h2><%= l(:label_spent_time) %></h2>
8 8
9 <% form_tag({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
9 <%= form_tag({:controller => 'timelog', :action => 'report',
10 :project_id => @project, :issue_id => @issue},
11 :method => :get, :id => 'query_form') do %>
10 12 <% @report.criteria.each do |criterion| %>
11 13 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
12 14 <% end %>
13 15 <%= render :partial => 'timelog/date_range' %>
14 16
15 17 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
16 18 [l(:label_month), 'month'],
17 19 [l(:label_week), 'week'],
18 20 [l(:label_day_plural).titleize, 'day']], @report.columns),
19 21 :onchange => "this.form.submit();" %>
20 22
21 23 <label for='criterias'><%= l(:button_add) %></label>: <%= select_tag('criteria[]', options_for_select([[]] + (@report.available_criteria.keys - @report.criteria).collect{|k| [l_or_humanize(@report.available_criteria[k][:label]), k]}),
22 24 :onchange => "this.form.submit();",
23 25 :style => 'width: 200px',
24 26 :id => nil,
25 27 :disabled => (@report.criteria.length >= 3), :id => "criterias") %>
26 28 <%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @report.columns}, :class => 'icon icon-reload' %></p>
27 29 <% end %>
28 30
29 31 <% unless @report.criteria.empty? %>
30 32 <div class="total-hours">
31 33 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
32 34 </div>
33 35
34 36 <% unless @report.hours.empty? %>
35 37 <div class="autoscroll">
36 38 <table class="list" id="time-report">
37 39 <thead>
38 40 <tr>
39 41 <% @report.criteria.each do |criteria| %>
40 42 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
41 43 <% end %>
42 44 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
43 45 <% @report.periods.each do |period| %>
44 46 <th class="period" width="<%= columns_width %>%"><%= period %></th>
45 47 <% end %>
46 48 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
47 49 </tr>
48 50 </thead>
49 51 <tbody>
50 52 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
51 53 <tr class="total">
52 54 <td><%= l(:label_total) %></td>
53 55 <%= ('<td></td>' * (@report.criteria.size - 1)).html_safe %>
54 56 <% total = 0 -%>
55 57 <% @report.periods.each do |period| -%>
56 58 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
57 59 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
58 60 <% end -%>
59 61 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
60 62 </tr>
61 63 </tbody>
62 64 </table>
63 65 </div>
64 66
65 67 <% other_formats_links do |f| %>
66 68 <%= f.link_to 'CSV', :url => params %>
67 69 <% end %>
68 70 <% end %>
69 71 <% end %>
70 72
71 73 <% html_title l(:label_spent_time), l(:label_report) %>
72 74
@@ -1,5 +1,5
1 1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=h @tracker %></h2>
2 2
3 <% labelled_form_for @tracker do |f| %>
3 <%= labelled_form_for @tracker do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <% end %>
@@ -1,5 +1,5
1 1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=l(:label_tracker_new)%></h2>
2 2
3 <% labelled_form_for @tracker do |f| %>
3 <%= labelled_form_for @tracker do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <% end %>
@@ -1,7 +1,7
1 <% labelled_form_for @user do |f| %>
1 <%= labelled_form_for @user do |f| %>
2 2 <%= render :partial => 'form', :locals => { :f => f } %>
3 3 <% if @user.active? && email_delivery_enabled? -%>
4 4 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
5 5 <% end -%>
6 6 <p><%= submit_tag l(:button_save) %></p>
7 7 <% end %>
@@ -1,9 +1,9
1 <% form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %>
1 <%= form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %>
2 2 <div class="box">
3 3 <% Group.all.sort.each do |group| %>
4 4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
5 5 <% end %>
6 6 <%= hidden_field_tag 'user[group_ids][]', '' %>
7 7 </div>
8 8 <%= submit_tag l(:button_save) %>
9 9 <% end %>
@@ -1,70 +1,72
1 1 <% roles = Role.find_all_givable %>
2 2 <% projects = Project.active.find(:all, :order => 'lft') %>
3 3
4 4 <div class="splitcontentleft">
5 5 <% if @user.memberships.any? %>
6 6 <table class="list memberships">
7 7 <thead><tr>
8 8 <th><%= l(:label_project) %></th>
9 9 <th><%= l(:label_role_plural) %></th>
10 10 <th style="width:15%"></th>
11 11 <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
12 12 </tr></thead>
13 13 <tbody>
14 14 <% @user.memberships.each do |membership| %>
15 15 <% next if membership.new_record? %>
16 16 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
17 17 <td class="project">
18 18 <%= link_to_project membership.project %>
19 19 </td>
20 20 <td class="roles">
21 21 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
22 <% remote_form_for(:membership, :url => user_membership_path(@user, membership), :method => :put,
23 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
22 <%= form_for(:membership, :remote => true,
23 :url => user_membership_path(@user, membership), :method => :put,
24 :html => {:id => "member-#{membership.id}-roles-form",
25 :style => 'display:none;'}) do %>
24 26 <p><% roles.each do |role| %>
25 27 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
26 28 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
27 29 <% end %></p>
28 30 <%= hidden_field_tag 'membership[role_ids][]', '' %>
29 31 <p><%= submit_tag l(:button_change) %>
30 32 <%= link_to_function l(:button_cancel),
31 33 "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;"
32 34 %></p>
33 35 <% end %>
34 36 </td>
35 37 <td class="buttons">
36 38 <%= link_to_function l(:button_edit),
37 39 "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;",
38 40 :class => 'icon icon-edit'
39 41 %>
40 42 <%= link_to_remote(
41 43 l(:button_delete),
42 44 { :url => user_membership_path(@user, membership),
43 45 :method => :delete },
44 46 :class => 'icon icon-del'
45 47 ) if membership.deletable? %>
46 48 </td>
47 49 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
48 50 </tr>
49 51 <% end; reset_cycle %>
50 52 </tbody>
51 53 </table>
52 54 <% else %>
53 55 <p class="nodata"><%= l(:label_no_data) %></p>
54 56 <% end %>
55 57 </div>
56 58
57 59 <div class="splitcontentright">
58 60 <% if projects.any? %>
59 61 <fieldset><legend><%=l(:label_project_new)%></legend>
60 <% remote_form_for(:membership, :url => user_memberships_path(@user)) do %>
62 <%= form_for(:membership, :remote => true, :url => user_memberships_path(@user)) do %>
61 63 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
62 64 <p><%= l(:label_role_plural) %>:
63 65 <% roles.each do |role| %>
64 66 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
65 67 <% end %></p>
66 68 <p><%= submit_tag l(:button_add) %></p>
67 69 <% end %>
68 70 </fieldset>
69 71 <% end %>
70 72 </div>
@@ -1,7 +1,7
1 <% labelled_fields_for :pref, @user.pref do |pref_fields| %>
1 <%= labelled_fields_for :pref, @user.pref do |pref_fields| %>
2 2 <p><%= pref_fields.check_box :hide_mail %></p>
3 3 <p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
4 4 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
5 5 <p><%= pref_fields.check_box :warn_on_leaving_unsaved %></p>
6 6 <% end %>
7 7
@@ -1,58 +1,58
1 1 <div class="contextual">
2 2 <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_user_plural)%></h2>
6 6
7 <% form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 9 <label for='status'><%= l(:field_status) %>:</label>
10 10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 11
12 12 <% if @groups.present? %>
13 13 <label for='group_id'><%= l(:label_group) %>:</label>
14 14 <%= select_tag 'group_id', '<option></option>' + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %>
15 15 <% end %>
16 16
17 17 <label for='name'><%= l(:label_user) %>:</label>
18 18 <%= text_field_tag 'name', params[:name], :size => 30 %>
19 19 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
20 20 <%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
21 21 </fieldset>
22 22 <% end %>
23 23 &nbsp;
24 24
25 25 <div class="autoscroll">
26 26 <table class="list">
27 27 <thead><tr>
28 28 <%= sort_header_tag('login', :caption => l(:field_login)) %>
29 29 <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
30 30 <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
31 31 <%= sort_header_tag('mail', :caption => l(:field_mail)) %>
32 32 <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
33 33 <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
34 34 <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
35 35 <th></th>
36 36 </tr></thead>
37 37 <tbody>
38 38 <% for user in @users -%>
39 39 <tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">
40 40 <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
41 41 <td class="firstname"><%= h(user.firstname) %></td>
42 42 <td class="lastname"><%= h(user.lastname) %></td>
43 43 <td class="email"><%= mail_to(h(user.mail)) %></td>
44 44 <td align="center"><%= checked_image user.admin? %></td>
45 45 <td class="created_on" align="center"><%= format_time(user.created_on) %></td>
46 46 <td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
47 47 <td class="buttons">
48 48 <%= change_status_link(user) %>
49 49 <%= link_to(l(:button_delete), user_path(user), :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del') unless User.current == user %>
50 50 </td>
51 51 </tr>
52 52 <% end -%>
53 53 </tbody>
54 54 </table>
55 55 </div>
56 56 <p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p>
57 57
58 58 <% html_title(l(:label_user_plural)) -%>
@@ -1,12 +1,12
1 1 <h2><%= link_to l(:label_user_plural), users_path %> &#187; <%=l(:label_user_new)%></h2>
2 2
3 <% labelled_form_for @user do |f| %>
3 <%= labelled_form_for @user do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <% if email_delivery_enabled? %>
6 6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
7 7 <% end %>
8 8 <p>
9 9 <%= submit_tag l(:button_create) %>
10 10 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
11 11 </p>
12 12 <% end %>
@@ -1,35 +1,35
1 <% form_tag({}, :id => "status_by_form") do -%>
1 <%= form_tag({}, :id => "status_by_form") do -%>
2 2 <fieldset>
3 3 <legend>
4 4 <%= l(:label_issues_by,
5 5 select_tag('status_by',
6 6 status_by_options_for_select(criteria),
7 7 :id => 'status_by_select',
8 8 :onchange => remote_function(:url => status_by_version_path(version),
9 9 :with => "Form.serialize('status_by_form')"))).html_safe %>
10 10 </legend>
11 11 <% if counts.empty? %>
12 12 <p><em><%= l(:label_no_data) %></em></p>
13 13 <% else %>
14 14 <table>
15 15 <% counts.each do |count| %>
16 16 <tr>
17 17 <td width="130px" align="right" >
18 18 <%= link_to h(count[:group]), {:controller => 'issues',
19 19 :action => 'index',
20 20 :project_id => version.project,
21 21 :set_filter => 1,
22 22 :status_id => '*',
23 23 :fixed_version_id => version}.merge("#{criteria}_id".to_sym => count[:group]) %>
24 24 </td>
25 25 <td width="240px">
26 26 <%= progress_bar((count[:closed].to_f / count[:total])*100,
27 27 :legend => "#{count[:closed]}/#{count[:total]}",
28 28 :width => "#{(count[:total].to_f / max * 200).floor}px;") %>
29 29 </td>
30 30 </tr>
31 31 <% end %>
32 32 </table>
33 33 <% end %>
34 34 </fieldset>
35 35 <% end %>
@@ -1,9 +1,9
1 1 <h3 class="title"><%=l(:label_version_new)%></h3>
2 2
3 <% labelled_remote_form_for @version, :url => project_versions_path(@project) do |f| %>
3 <%= labelled_remote_form_for :version, @version, :url => project_versions_path(@project) do |f| %>
4 4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
5 5 <p class="buttons">
6 6 <%= submit_tag l(:button_create), :name => nil %>
7 7 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
8 8 </p>
9 9 <% end %>
@@ -1,7 +1,7
1 1 <h2><%=l(:label_version)%></h2>
2 2
3 <% labelled_form_for @version do |f| %>
3 <%= labelled_form_for @version do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5 <%= submit_tag l(:button_save) %>
6 6 <% end %>
7 7
@@ -1,68 +1,68
1 1 <div class="contextual">
2 2 <%= link_to l(:label_version_new), new_project_version_path(@project), :class => 'icon icon-add' if User.current.allowed_to?(:manage_versions, @project) %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_roadmap)%></h2>
6 6
7 7 <% if @versions.empty? %>
8 8 <p class="nodata"><%= l(:label_no_data) %></p>
9 9 <% else %>
10 10 <div id="roadmap">
11 11 <% @versions.each do |version| %>
12 12 <h3 class="version"><%= tag 'a', :name => anchor(version.name) %><%= link_to_version version %></h3>
13 13 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
14 14 <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
15 15
16 16 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
17 <% form_tag({}) do -%>
17 <%= form_tag({}) do -%>
18 18 <table class="list related-issues">
19 19 <caption><%= l(:label_related_issues) %></caption>
20 20 <% issues.each do |issue| -%>
21 21 <tr class="hascontextmenu">
22 22 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
23 23 <td><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
24 24 </tr>
25 25 <% end -%>
26 26 </table>
27 27 <% end %>
28 28 <% end %>
29 29 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
30 30 <% end %>
31 31 </div>
32 32 <% end %>
33 33
34 34 <% content_for :sidebar do %>
35 <% form_tag({}, :method => :get) do %>
35 <%= form_tag({}, :method => :get) do %>
36 36 <h3><%= l(:label_roadmap) %></h3>
37 37 <% @trackers.each do |tracker| %>
38 38 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
39 39 <%=h tracker.name %></label><br />
40 40 <% end %>
41 41 <br />
42 42 <label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
43 43 <% if @project.descendants.active.any? %>
44 44 <%= hidden_field_tag 'with_subprojects', 0 %>
45 45 <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
46 46 <% end %>
47 47 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
48 48 <% end %>
49 49
50 50 <h3><%= l(:label_version_plural) %></h3>
51 51 <% @versions.each do |version| %>
52 52 <%= link_to format_version_name(version), "##{anchor(version.name)}" %><br />
53 53 <% end %>
54 54 <% if @completed_versions.present? %>
55 55 <p>
56 56 <%= link_to_function l(:label_completed_versions),
57 57 'Element.toggleClassName("toggle-completed-versions", "collapsed"); Element.toggle("completed-versions")',
58 58 :id => 'toggle-completed-versions', :class => 'collapsible collapsed' %><br />
59 59 <span id="completed-versions" style="display:none;">
60 60 <%= @completed_versions.map {|version| link_to format_version_name(version), version_path(version)}.join("<br />\n").html_safe %>
61 61 </span>
62 62 </p>
63 63 <% end %>
64 64 <% end %>
65 65
66 66 <% html_title(l(:label_roadmap)) %>
67 67
68 68 <%= context_menu issues_context_menu_path %>
@@ -1,6 +1,6
1 1 <h2><%=l(:label_version_new)%></h2>
2 2
3 <% labelled_form_for @version, :url => project_versions_path(@project) do |f| %>
3 <%= labelled_form_for @version, :url => project_versions_path(@project) do |f| %>
4 4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
5 5 <%= submit_tag l(:button_create) %>
6 6 <% end %>
@@ -1,56 +1,56
1 1 <div class="contextual">
2 2 <%= link_to(l(:button_edit), edit_version_path(@version), :class => 'icon icon-edit') if User.current.allowed_to?(:manage_versions, @version.project) %>
3 3 <%= link_to_if_authorized(l(:button_edit_associated_wikipage, :page_title => @version.wiki_page_title), {:controller => 'wiki', :action => 'edit', :project_id => @version.project, :id => Wiki.titleize(@version.wiki_page_title)}, :class => 'icon icon-edit') unless @version.wiki_page_title.blank? || @version.project.wiki.nil? %>
4 4 <%= link_to(l(:button_delete), version_path(@version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)),
5 5 :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del') if User.current.allowed_to?(:manage_versions, @version.project) %>
6 6 <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
7 7 </div>
8 8
9 9 <h2><%= h(@version.name) %></h2>
10 10
11 11 <div id="roadmap">
12 12 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
13 13 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
14 14
15 15 <div id="version-summary">
16 16 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
17 17 <fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
18 18 <table>
19 19 <tr>
20 20 <th><%= l(:field_estimated_hours) %></th>
21 21 <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
22 22 </tr>
23 23 <% if User.current.allowed_to?(:view_time_entries, @project) %>
24 24 <tr>
25 25 <th><%= l(:label_spent_time) %></th>
26 26 <td class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td>
27 27 </tr>
28 28 <% end %>
29 29 </table>
30 30 </fieldset>
31 31 <% end %>
32 32
33 33 <div id="status_by">
34 34 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
35 35 </div>
36 36 </div>
37 37
38 38 <% if @issues.present? %>
39 <% form_tag({}) do -%>
39 <%= form_tag({}) do -%>
40 40 <table class="list related-issues">
41 41 <caption><%= l(:label_related_issues) %></caption>
42 42 <%- @issues.each do |issue| -%>
43 43 <tr class="hascontextmenu">
44 44 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
45 45 <td><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
46 46 </tr>
47 47 <% end %>
48 48 </table>
49 49 <% end %>
50 50 <%= context_menu issues_context_menu_path %>
51 51 <% end %>
52 52 </div>
53 53
54 54 <%= call_hook :view_versions_show_bottom, :version => @version %>
55 55
56 56 <% html_title @version.name %>
@@ -1,32 +1,32
1 1 <h3 class="title"><%= l(:permission_add_issue_watchers) %></h3>
2 2
3 <% form_remote_tag :url => {:controller => 'watchers',
3 <%= form_remote_tag :url => {:controller => 'watchers',
4 4 :action => (watched ? 'create' : 'append'),
5 5 :object_type => watched.class.name.underscore,
6 6 :object_id => watched},
7 7 :method => :post,
8 8 :html => {:id => 'new-watcher-form'} do %>
9 9
10 10 <p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
11 11 <%= observe_field(:user_search,
12 12 :frequency => 0.5,
13 13 :update => :users_for_watcher,
14 14 :method => :get,
15 15 :before => '$("user_search").addClassName("ajax-loading")',
16 16 :complete => '$("user_search").removeClassName("ajax-loading")',
17 17 :url => {
18 18 :controller => 'watchers',
19 19 :action => 'autocomplete_for_user',
20 20 :object_type => watched.class.name.underscore,
21 21 :object_id => watched},
22 22 :with => 'q') %>
23 23
24 24 <div id="users_for_watcher">
25 25 <%= principals_check_box_tags 'watcher[user_ids][]', (watched ? watched.addable_watcher_users : User.active.all(:limit => 100)) %>
26 26 </div>
27 27
28 28 <p class="buttons">
29 29 <%= submit_tag l(:button_add), :name => nil, :onclick => "hideModal(this);" %>
30 30 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
31 31 </p>
32 32 <% end %>
@@ -1,9 +1,9
1 1 <% if @wiki && @wiki.sidebar -%>
2 2 <%= textilizable @wiki.sidebar.content, :text %>
3 3 <% end -%>
4 4
5 5 <h3><%= l(:label_wiki) %></h3>
6 6
7 7 <%= link_to l(:field_start_page), {:action => 'show', :id => nil} %><br />
8 8 <%= link_to l(:label_index_by_title), {:action => 'index'} %><br />
9 <%= link_to l(:label_index_by_date), {:action => 'date_index'} %><br />
9 <%= link_to l(:label_index_by_date), {:controller => 'wiki', :project_id => @project, :action => 'date_index'} %><br />
@@ -1,22 +1,22
1 1 <%= wiki_page_breadcrumb(@page) %>
2 2
3 3 <h2><%=h @page.pretty_title %></h2>
4 4
5 <% form_tag({}, :method => :delete) do %>
5 <%= form_tag({}, :method => :delete) do %>
6 6 <div class="box">
7 7 <p><strong><%= l(:text_wiki_page_destroy_question, :descendants => @descendants_count) %></strong></p>
8 8 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_wiki_page_nullify_children) %></label><br />
9 9 <label><%= radio_button_tag 'todo', 'destroy', false %> <%= l(:text_wiki_page_destroy_children) %></label>
10 10 <% if @reassignable_to.any? %>
11 11 <br />
12 12 <label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_wiki_page_reassign_children) %></label>:
13 13 <%= label_tag "reassign_to_id", l(:description_wiki_subpages_reassign), :class => "hidden-for-sighted" %>
14 14 <%= select_tag 'reassign_to_id', wiki_page_options_for_select(@reassignable_to),
15 15 :onclick => "$('todo_reassign').checked = true;" %>
16 16 <% end %>
17 17 </p>
18 18 </div>
19 19
20 20 <%= submit_tag l(:button_apply) %>
21 21 <%= link_to l(:button_cancel), :controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title %>
22 22 <% end %>
@@ -1,49 +1,49
1 1 <%= wiki_page_breadcrumb(@page) %>
2 2
3 3 <h2><%= h @page.pretty_title %></h2>
4 4
5 <% form_for :content, @content,
5 <%= form_for @content, :as => :content,
6 6 :url => {:action => 'update', :id => @page.title},
7 7 :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %>
8 8 <%= f.hidden_field :version %>
9 9 <% if @section %>
10 10 <%= hidden_field_tag 'section', @section %>
11 11 <%= hidden_field_tag 'section_hash', @section_hash %>
12 12 <% end %>
13 13 <%= error_messages_for 'content' %>
14 14
15 15 <div class="box tabular">
16 16 <%= text_area_tag 'content[text]', @text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>
17 17
18 18 <% if @page.safe_attribute_names.include?('parent_id') && @wiki.pages.any? %>
19 <% fields_for @page do |fp| %>
19 <%= fields_for @page do |fp| %>
20 20 <p>
21 21 <label><%= l(:field_parent_title) %></label>
22 <%= fp.select :parent_id, "<option value=''></option>" + wiki_page_options_for_select(@wiki.pages.all(:include => :parent) - @page.self_and_descendants, @page.parent) %>
22 <%= fp.select :parent_id, content_tag('option', '', :value => '') + wiki_page_options_for_select(@wiki.pages.all(:include => :parent) - @page.self_and_descendants, @page.parent) %>
23 23 </p>
24 24 <% end %>
25 25 <% end %>
26 26
27 27 <p><label><%= l(:field_comments) %></label><%= f.text_field :comments, :size => 120 %></p>
28 28 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
29 29 </div>
30 30
31 31 <p><%= submit_tag l(:button_save) %>
32 32 <%= link_to_remote l(:label_preview),
33 33 { :url => { :controller => 'wiki', :action => 'preview', :project_id => @project, :id => @page.title },
34 34 :method => :post,
35 35 :update => 'preview',
36 36 :with => "Form.serialize('wiki_form')",
37 37 :complete => "Element.scrollTo('preview')"
38 38 }, :accesskey => accesskey(:preview) %></p>
39 39 <%= wikitoolbar_for 'content_text' %>
40 40 <% end %>
41 41
42 42 <div id="preview" class="wiki"></div>
43 43
44 44 <% content_for :header_tags do %>
45 45 <%= stylesheet_link_tag 'scm' %>
46 46 <%= robot_exclusion_tag %>
47 47 <% end %>
48 48
49 49 <% html_title @page.pretty_title %>
@@ -1,37 +1,39
1 1 <%= wiki_page_breadcrumb(@page) %>
2 2
3 3 <h2><%= h(@page.pretty_title) %></h2>
4 4
5 5 <h3><%= l(:label_history) %></h3>
6 6
7 <% form_tag({:controller => 'wiki', :action => 'diff', :project_id => @page.project, :id => @page.title}, :method => :get) do %>
7 <%= form_tag({:controller => 'wiki', :action => 'diff',
8 :project_id => @page.project, :id => @page.title},
9 :method => :get) do %>
8 10 <table class="list wiki-page-versions">
9 11 <thead><tr>
10 12 <th>#</th>
11 13 <th></th>
12 14 <th></th>
13 15 <th><%= l(:field_updated_on) %></th>
14 16 <th><%= l(:field_author) %></th>
15 17 <th><%= l(:field_comments) %></th>
16 18 <th></th>
17 19 </tr></thead>
18 20 <tbody>
19 21 <% show_diff = @versions.size > 1 %>
20 22 <% line_num = 1 %>
21 23 <% @versions.each do |ver| %>
22 24 <tr class="wiki-page-version <%= cycle("odd", "even") %>">
23 25 <td class="id"><%= link_to h(ver.version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
24 26 <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td>
25 27 <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
26 28 <td class="updated_on"><%= format_time(ver.updated_on) %></td>
27 29 <td class="author"><%= link_to_user ver.author %></td>
28 30 <td class="comments"><%=h ver.comments %></td>
29 31 <td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
30 32 </tr>
31 33 <% line_num += 1 %>
32 34 <% end %>
33 35 </tbody>
34 36 </table>
35 37 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
36 38 <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
37 39 <% end %>
@@ -1,21 +1,21
1 1 <%= wiki_page_breadcrumb(@page) %>
2 2
3 3 <h2><%= h @original_title %></h2>
4 4
5 5 <%= error_messages_for 'page' %>
6 6
7 <% labelled_form_for :wiki_page, @page,
7 <%= labelled_form_for :wiki_page, @page,
8 8 :url => { :action => 'rename' },
9 9 :html => { :method => :post } do |f| %>
10 10 <div class="box tabular">
11 11 <p><%= f.text_field :title, :required => true, :size => 100 %></p>
12 12 <p><%= f.check_box :redirect_existing_links %></p>
13 13 <p><%= f.select :parent_id,
14 "<option value=''></option>" +
14 content_tag('option', '', :value => '') +
15 15 wiki_page_options_for_select(
16 16 @wiki.pages.all(:include => :parent) - @page.self_and_descendants,
17 17 @page.parent),
18 18 :label => :field_parent_title %></p>
19 19 </div>
20 20 <%= submit_tag l(:button_rename) %>
21 21 <% end %>
@@ -1,69 +1,72
1 1 <div class="contextual">
2 2 <% if @editable %>
3 3 <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :id => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.current_version? %>
4 4 <%= watcher_tag(@page, User.current) %>
5 5 <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :id => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
6 6 <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :id => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
7 7 <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :id => @page.title}, :class => 'icon icon-move') if @content.current_version? %>
8 8 <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :id => @page.title}, :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
9 9 <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :id => @page.title, :version => @content.version }, :class => 'icon icon-cancel') unless @content.current_version? %>
10 10 <% end %>
11 11 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %>
12 12 </div>
13 13
14 14 <%= wiki_page_breadcrumb(@page) %>
15 15
16 16 <% unless @content.current_version? %>
17 17 <p>
18 18 <%= link_to(("\xc2\xab " + l(:label_previous)),
19 19 :action => 'show', :id => @page.title, :project_id => @page.project,
20 20 :version => (@content.version - 1)) + " - " if @content.version > 1 %>
21 21 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
22 22 <%= '('.html_safe + link_to(l(:label_diff), :controller => 'wiki', :action => 'diff',
23 23 :id => @page.title, :project_id => @page.project,
24 24 :version => @content.version) + ')'.html_safe if @content.version > 1 %> -
25 25 <%= link_to((l(:label_next) + " \xc2\xbb"), :action => 'show',
26 26 :id => @page.title, :project_id => @page.project,
27 27 :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
28 28 <%= link_to(l(:label_current_version), :action => 'show', :id => @page.title, :project_id => @page.project) %>
29 29 <br />
30 30 <em><%= @content.author ? link_to_user(@content.author) : l(:label_user_anonymous)
31 31 %>, <%= format_time(@content.updated_on) %> </em><br />
32 32 <%=h @content.comments %>
33 33 </p>
34 34 <hr />
35 35 <% end %>
36 36
37 37 <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
38 38
39 39 <%= link_to_attachments @page %>
40 40
41 41 <% if @editable && authorize_for('wiki', 'add_attachment') %>
42 42 <div id="wiki_add_attachment">
43 43 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
44 44 :id => 'attach_files_link' %></p>
45 <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :project_id => @project, :id => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
45 <%= form_tag({:controller => 'wiki', :action => 'add_attachment',
46 :project_id => @project, :id => @page.title},
47 :multipart => true, :id => "add_attachment_form",
48 :style => "display:none;") do %>
46 49 <div class="box">
47 50 <p><%= render :partial => 'attachments/form' %></p>
48 51 </div>
49 52 <%= submit_tag l(:button_add) %>
50 53 <%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
51 54 <% end %>
52 55 </div>
53 56 <% end %>
54 57
55 58 <% other_formats_links do |f| %>
56 59 <%= f.link_to 'PDF', :url => {:id => @page.title, :version => params[:version]} %>
57 60 <%= f.link_to 'HTML', :url => {:id => @page.title, :version => params[:version]} %>
58 61 <%= f.link_to 'TXT', :url => {:id => @page.title, :version => params[:version]} %>
59 62 <% end if User.current.allowed_to?(:export_wiki_pages, @project) %>
60 63
61 64 <% content_for :header_tags do %>
62 65 <%= stylesheet_link_tag 'scm' %>
63 66 <% end %>
64 67
65 68 <% content_for :sidebar do %>
66 69 <%= render :partial => 'sidebar' %>
67 70 <% end %>
68 71
69 72 <% html_title @page.pretty_title %>
@@ -1,10 +1,10
1 1 <h2><%=l(:label_confirmation)%></h2>
2 2
3 3 <div class="box"><center>
4 4 <p><strong><%= h(@project.name) %></strong><br /><%=l(:text_wiki_destroy_confirmation)%></p>
5 5
6 <% form_tag({:controller => 'wikis', :action => 'destroy', :id => @project}) do %>
6 <%= form_tag({:controller => 'wikis', :action => 'destroy', :id => @project}) do %>
7 7 <%= hidden_field_tag "confirm", 1 %>
8 8 <%= submit_tag l(:button_delete) %>
9 9 <% end %>
10 10 </center></div>
@@ -1,40 +1,40
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%=l(:label_workflow)%></h2>
4 4
5 <% form_tag({}, :id => 'workflow_copy_form') do %>
5 <%= form_tag({}, :id => 'workflow_copy_form') do %>
6 6 <fieldset class="tabular box">
7 7 <legend><%= l(:label_copy_source) %></legend>
8 8 <p>
9 9 <label><%= l(:label_tracker) %></label>
10 10 <%= select_tag('source_tracker_id',
11 11 "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
12 12 "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" +
13 13 options_from_collection_for_select(@trackers, 'id', 'name', @source_tracker && @source_tracker.id)) %>
14 14 </p>
15 15 <p>
16 16 <label><%= l(:label_role) %></label>
17 17 <%= select_tag('source_role_id',
18 18 "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
19 19 "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" +
20 20 options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %>
21 21 </p>
22 22 </fieldset>
23 23
24 24 <fieldset class="tabular box">
25 25 <legend><%= l(:label_copy_target) %></legend>
26 26 <p>
27 27 <label><%= l(:label_tracker) %></label>
28 28 <%= select_tag 'target_tracker_ids',
29 29 "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
30 30 options_from_collection_for_select(@trackers, 'id', 'name', @target_trackers && @target_trackers.map(&:id)), :multiple => true %>
31 31 </p>
32 32 <p>
33 33 <label><%= l(:label_role) %></label>
34 34 <%= select_tag 'target_role_ids',
35 35 "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
36 36 options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %>
37 37 </p>
38 38 </fieldset>
39 39 <%= submit_tag l(:button_copy) %>
40 40 <% end %>
@@ -1,50 +1,50
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <h2><%=l(:label_workflow)%></h2>
4 4
5 5 <p><%=l(:text_workflow_edit)%>:</p>
6 6
7 <% form_tag({}, :method => 'get') do %>
7 <%= form_tag({}, :method => 'get') do %>
8 8 <p>
9 9 <label><%=l(:label_role)%>:
10 10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
11 11
12 12 <label><%=l(:label_tracker)%>:
13 13 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
14 14
15 15 <%= hidden_field_tag 'used_statuses_only', '0' %>
16 16 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
17 17 </p>
18 18 <p>
19 19 <%= submit_tag l(:button_edit), :name => nil %>
20 20 </p>
21 21 <% end %>
22 22
23 23 <% if @tracker && @role && @statuses.any? %>
24 <% form_tag({}, :id => 'workflow_form' ) do %>
24 <%= form_tag({}, :id => 'workflow_form' ) do %>
25 25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
26 26 <%= hidden_field_tag 'role_id', @role.id %>
27 27 <div class="autoscroll">
28 28 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
29 29
30 30 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
31 31 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
32 32 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
33 33 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
34 34 </div>
35 35 </fieldset>
36 36 <%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
37 37
38 38 <fieldset class="collapsible" style="padding: 0;">
39 39 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
40 40 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
41 41 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
42 42 </div>
43 43 </fieldset>
44 44 <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
45 45 </div>
46 46 <%= submit_tag l(:button_save) %>
47 47 <% end %>
48 48 <% end %>
49 49
50 50 <% html_title(l(:label_workflow)) -%>
@@ -1,124 +1,6
1 # Don't change this file!
2 # Configure your app in config/environment.rb and config/environments/*.rb
1 require 'rubygems'
3 2
4 if RUBY_VERSION >= '1.9'
5 require 'yaml'
6 YAML::ENGINE.yamler = 'syck'
7 end
3 # Set up gems listed in the Gemfile.
4 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
8 5
9 RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
10
11 module Rails
12 class << self
13 def boot!
14 unless booted?
15 preinitialize
16 pick_boot.run
17 end
18 end
19
20 def booted?
21 defined? Rails::Initializer
22 end
23
24 def pick_boot
25 (vendor_rails? ? VendorBoot : GemBoot).new
26 end
27
28 def vendor_rails?
29 File.exist?("#{RAILS_ROOT}/vendor/rails")
30 end
31
32 def preinitialize
33 load(preinitializer_path) if File.exist?(preinitializer_path)
34 end
35
36 def preinitializer_path
37 "#{RAILS_ROOT}/config/preinitializer.rb"
38 end
39 end
40
41 class Boot
42 def run
43 load_initializer
44 Rails::Initializer.class_eval do
45 def load_gems
46 @bundler_loaded ||= Bundler.require :default, Rails.env
47 end
48 end
49 Rails::Initializer.run(:set_load_path)
50 end
51 end
52
53 class VendorBoot < Boot
54 def load_initializer
55 require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
56 Rails::Initializer.run(:install_gem_spec_stubs)
57 Rails::GemDependency.add_frozen_gem_path
58 end
59 end
60
61 class GemBoot < Boot
62 def load_initializer
63 self.class.load_rubygems
64 load_rails_gem
65 require 'initializer'
66 end
67
68 def load_rails_gem
69 if version = self.class.gem_version
70 gem 'rails', version
71 else
72 gem 'rails'
73 end
74 rescue Gem::LoadError => load_error
75 if load_error.message =~ /Could not find RubyGem rails/
76 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.)
77 exit 1
78 else
79 raise
80 end
81 end
82
83 class << self
84 def rubygems_version
85 Gem::RubyGemsVersion rescue nil
86 end
87
88 def gem_version
89 if defined? RAILS_GEM_VERSION
90 RAILS_GEM_VERSION
91 elsif ENV.include?('RAILS_GEM_VERSION')
92 ENV['RAILS_GEM_VERSION']
93 else
94 parse_gem_version(read_environment_rb)
95 end
96 end
97
98 def load_rubygems
99 min_version = '1.3.2'
100 require 'rubygems'
101 unless rubygems_version >= min_version
102 $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
103 exit 1
104 end
105
106 rescue LoadError
107 $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
108 exit 1
109 end
110
111 def parse_gem_version(text)
112 $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
113 end
114
115 private
116 def read_environment_rb
117 File.read("#{RAILS_ROOT}/config/environment.rb")
118 end
119 end
120 end
121 end
122
123 # All that for this:
124 Rails.boot!
6 require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
@@ -1,59 +1,5
1 # Be sure to restart your web server when you modify this file.
1 # Load the rails application
2 require File.expand_path('../application', __FILE__)
2 3
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
5 # ENV['RAILS_ENV'] ||= 'production'
6
7 # Bootstrap the Rails environment, frameworks, and default configuration
8 require File.join(File.dirname(__FILE__), 'boot')
9
10 if RUBY_VERSION >= '1.9' && defined?(Rails) && Rails::VERSION::MAJOR < 3
11 Encoding.default_external = 'UTF-8'
12 end
13
14 # Load Engine plugin if available
15 begin
16 require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
17 rescue LoadError
18 # Not available
19 end
20
21 Rails::Initializer.run do |config|
22 # Settings in config/environments/* take precedence those specified here
23
24 # Skip frameworks you're not going to use
25 # config.frameworks -= [ :action_web_service, :action_mailer ]
26
27 # Add additional load paths for sweepers
28 config.autoload_paths += %W( #{RAILS_ROOT}/app/sweepers )
29
30 # Force all environments to use the same logger level
31 # (by default production uses :info, the others :debug)
32 # config.log_level = :debug
33
34 # Enable page/fragment caching by setting a file-based store
35 # (remember to create the caching directory and make it readable to the application)
36 # config.action_controller.cache_store = :file_store, "#{RAILS_ROOT}/tmp/cache"
37
38 # Activate observers that should always be running
39 # config.active_record.observers = :cacher, :garbage_collector
40 config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
41
42 # Make Active Record use UTC-base instead of local time
43 # config.active_record.default_timezone = :utc
44
45 # Use Active Record's schema dumper instead of SQL when creating the test database
46 # (enables use of different database adapters for development and test environments)
47 # config.active_record.schema_format = :ruby
48
49 # Deliveries are disabled by default. Do NOT modify this section.
50 # Define your email configuration in configuration.yml instead.
51 # It will automatically turn deliveries on
52 config.action_mailer.perform_deliveries = false
53
54 # Load any local configuration that is kept out of source control
55 # (e.g. gems, patches).
56 if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
57 instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
58 end
59 end
4 # Initialize the rails application
5 RedmineApp::Application.initialize!
@@ -1,16 +1,19
1 1 # Settings specified here will take precedence over those in config/environment.rb
2 RedmineApp::Application.configure do
3 # In the development environment your application's code is reloaded on
4 # every request. This slows down response time but is perfect for development
5 # since you don't have to restart the webserver when you make code changes.
6 config.cache_classes = false
2 7
3 # In the development environment your application's code is reloaded on
4 # every request. This slows down response time but is perfect for development
5 # since you don't have to restart the webserver when you make code changes.
6 config.cache_classes = false
8 # Log error messages when you accidentally call methods on nil.
9 config.whiny_nils = true
7 10
8 # Log error messages when you accidentally call methods on nil.
9 config.whiny_nils = true
11 # Show full error reports and disable caching
12 #config.action_controller.consider_all_requests_local = true
13 config.action_controller.perform_caching = false
10 14
11 # Show full error reports and disable caching
12 config.action_controller.consider_all_requests_local = true
13 config.action_controller.perform_caching = false
15 # Don't care if the mailer can't send
16 config.action_mailer.raise_delivery_errors = false
14 17
15 # Don't care if the mailer can't send
16 config.action_mailer.raise_delivery_errors = false
18 config.active_support.deprecation = :log
19 end
@@ -1,30 +1,32
1 1 # Settings specified here will take precedence over those in config/environment.rb
2 RedmineApp::Application.configure do
3 # The production environment is meant for finished, "live" apps.
4 # Code is not reloaded between requests
5 config.cache_classes = true
2 6
3 # The production environment is meant for finished, "live" apps.
4 # Code is not reloaded between requests
5 config.cache_classes = true
7 #####
8 # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html)
9 #
10 # Use a different logger for distributed setups
11 # config.logger = SyslogLogger.new
12 #
13 # Rotate logs bigger than 1MB, keeps no more than 7 rotated logs around.
14 # When setting a new Logger, make sure to set it's log level too.
15 #
16 # config.logger = Logger.new(config.log_path, 7, 1048576)
17 # config.logger.level = Logger::INFO
6 18
7 #####
8 # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html)
9 #
10 # Use a different logger for distributed setups
11 # config.logger = SyslogLogger.new
12 #
13 # Rotate logs bigger than 1MB, keeps no more than 7 rotated logs around.
14 # When setting a new Logger, make sure to set it's log level too.
15 #
16 # config.logger = Logger.new(config.log_path, 7, 1048576)
17 # config.logger.level = Logger::INFO
19 # Full error reports are disabled and caching is turned on
20 config.action_controller.perform_caching = true
18 21
19 # Full error reports are disabled and caching is turned on
20 config.action_controller.consider_all_requests_local = false
21 config.action_controller.perform_caching = true
22 # Enable serving of images, stylesheets, and javascripts from an asset server
23 # config.action_controller.asset_host = "http://assets.example.com"
22 24
23 # Enable serving of images, stylesheets, and javascripts from an asset server
24 # config.action_controller.asset_host = "http://assets.example.com"
25 # Disable delivery errors if you bad email addresses should just be ignored
26 config.action_mailer.raise_delivery_errors = false
25 27
26 # Disable delivery errors if you bad email addresses should just be ignored
27 config.action_mailer.raise_delivery_errors = false
28 # No email in production log
29 config.action_mailer.logger = nil
28 30
29 # No email in production log
30 config.action_mailer.logger = nil
31 config.active_support.deprecation = :log
32 end
@@ -1,25 +1,25
1 1 # Settings specified here will take precedence over those in config/environment.rb
2 RedmineApp::Application.configure do
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
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!
7 config.cache_classes = true
2 8
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
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!
7 config.cache_classes = true
9 # Log error messages when you accidentally call methods on nil.
10 config.whiny_nils = true
8 11
9 # Log error messages when you accidentally call methods on nil.
10 config.whiny_nils = true
12 # Show full error reports and disable caching
13 #config.action_controller.consider_all_requests_local = true
14 config.action_controller.perform_caching = false
11 15
12 # Show full error reports and disable caching
13 config.action_controller.consider_all_requests_local = true
14 config.action_controller.perform_caching = false
16 config.action_mailer.perform_deliveries = true
17 config.action_mailer.delivery_method = :test
15 18
16 config.action_mailer.perform_deliveries = true
17 config.action_mailer.delivery_method = :test
19 # Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
20 config.action_controller.allow_forgery_protection = false
18 21
19 config.action_controller.session = {
20 :key => "_test_session",
21 :secret => "some secret phrase for the tests."
22 }
22 config.active_support.deprecation = :log
23 23
24 # Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
25 config.action_controller.allow_forgery_protection = false
24 config.secret_token = 'a secret token for running the tests'
25 end
@@ -1,88 +1,86
1 1 # Patches active_support/core_ext/load_error.rb to support 1.9.3 LoadError message
2 2 if RUBY_VERSION >= '1.9.3'
3 3 MissingSourceFile::REGEXPS << [/^cannot load such file -- (.+)$/i, 1]
4 4 end
5 5
6 6 require 'active_record'
7 7
8 8 module ActiveRecord
9 9 class Base
10 10 include Redmine::I18n
11 def self.named_scope(*args)
12 scope(*args)
13 end
11 14
12 15 # Translate attribute names for validation errors display
13 16 def self.human_attribute_name(attr, *args)
14 17 l("field_#{attr.to_s.gsub(/_id$/, '')}", :default => attr)
15 18 end
16 19 end
17 20 end
18 21
19 22 module ActionView
20 23 module Helpers
21 24 module DateHelper
22 25 # distance_of_time_in_words breaks when difference is greater than 30 years
23 26 def distance_of_date_in_words(from_date, to_date = 0, options = {})
24 27 from_date = from_date.to_date if from_date.respond_to?(:to_date)
25 28 to_date = to_date.to_date if to_date.respond_to?(:to_date)
26 29 distance_in_days = (to_date - from_date).abs
27 30
28 31 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
29 32 case distance_in_days
30 33 when 0..60 then locale.t :x_days, :count => distance_in_days.round
31 34 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
32 35 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
33 36 end
34 37 end
35 38 end
36 39 end
37 40 end
41
42 class Resolver
43 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
44 cached(key, [name, prefix, partial], details, locals) do
45 if details[:formats] & [:xml, :json]
46 details = details.dup
47 details[:formats] = details[:formats].dup + [:api]
48 end
49 find_templates(name, prefix, partial, details)
50 end
51 end
52 end
38 53 end
39 54
40 55 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
41 56
42 57 module AsynchronousMailer
43 58 # Adds :async_smtp and :async_sendmail delivery methods
44 59 # to perform email deliveries asynchronously
45 60 %w(smtp sendmail).each do |type|
46 61 define_method("perform_delivery_async_#{type}") do |mail|
47 62 Thread.start do
48 63 send "perform_delivery_#{type}", mail
49 64 end
50 65 end
51 66 end
52 67
53 68 # Adds a delivery method that writes emails in tmp/emails for testing purpose
54 69 def perform_delivery_tmp_file(mail)
55 70 dest_dir = File.join(Rails.root, 'tmp', 'emails')
56 71 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
57 72 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
58 73 end
59 74 end
60 75
61 76 ActionMailer::Base.send :include, AsynchronousMailer
62 77
63 module TMail
64 # TMail::Unquoter.convert_to_with_fallback_on_iso_8859_1 introduced in TMail 1.2.7
65 # triggers a test failure in test_add_issue_with_japanese_keywords(MailHandlerTest)
66 class Unquoter
67 class << self
68 alias_method :convert_to, :convert_to_without_fallback_on_iso_8859_1
69 end
70 end
71
72 # Patch for TMail 1.2.7. See http://www.redmine.org/issues/8751
73 class Encoder
74 def puts_meta(str)
75 add_text str
76 end
77 end
78 end
79
80 78 module ActionController
81 79 module MimeResponds
82 class Responder
80 class Collector
83 81 def api(&block)
84 82 any(:xml, :json, &block)
85 83 end
86 84 end
87 85 end
88 86 end
@@ -1,5 +1,4
1 1 # Add new mime types for use in respond_to blocks:
2 2
3 3 Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
4 Mime::Type.register 'application/pdf', :pdf
5 Mime::Type.register 'image/png', :png
4
@@ -1,5 +1,7
1 1 I18n.default_locale = 'en'
2 2 # Adds fallback to default locale for untranslated strings
3 3 I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
4 4
5 5 require 'redmine'
6
7 Redmine::Plugin.load
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
The requested commit or file is too big and content was truncated. Show full diff
1 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
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