##// 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
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,31 +1,32
1 /.project
1 /.project
2 /.loadpath
2 /.loadpath
3 /config/additional_environment.rb
3 /config/additional_environment.rb
4 /config/configuration.yml
4 /config/configuration.yml
5 /config/database.yml
5 /config/database.yml
6 /config/email.yml
6 /config/email.yml
7 /config/initializers/session_store.rb
7 /config/initializers/session_store.rb
8 /config/initializers/secret_token.rb
8 /coverage
9 /coverage
9 /db/*.db
10 /db/*.db
10 /db/*.sqlite3
11 /db/*.sqlite3
11 /db/schema.rb
12 /db/schema.rb
12 /files/*
13 /files/*
13 /lib/redmine/scm/adapters/mercurial/redminehelper.pyc
14 /lib/redmine/scm/adapters/mercurial/redminehelper.pyc
14 /lib/redmine/scm/adapters/mercurial/redminehelper.pyo
15 /lib/redmine/scm/adapters/mercurial/redminehelper.pyo
15 /log/*.log*
16 /log/*.log*
16 /log/mongrel_debug
17 /log/mongrel_debug
17 /public/dispatch.*
18 /public/dispatch.*
18 /public/plugin_assets
19 /public/plugin_assets
19 /tmp/*
20 /tmp/*
20 /tmp/cache/*
21 /tmp/cache/*
21 /tmp/sessions/*
22 /tmp/sessions/*
22 /tmp/sockets/*
23 /tmp/sockets/*
23 /tmp/test/*
24 /tmp/test/*
24 /vendor/cache
25 /vendor/cache
25 /vendor/rails
26 /vendor/rails
26 *.rbc
27 *.rbc
27
28
28 /.bundle
29 /.bundle
29 /Gemfile.lock
30 /Gemfile.lock
30 /Gemfile.local
31 /Gemfile.local
31
32
@@ -1,36 +1,37
1 syntax: glob
1 syntax: glob
2
2
3 .project
3 .project
4 .loadpath
4 .loadpath
5 config/additional_environment.rb
5 config/additional_environment.rb
6 config/configuration.yml
6 config/configuration.yml
7 config/database.yml
7 config/database.yml
8 config/email.yml
8 config/email.yml
9 config/initializers/session_store.rb
9 config/initializers/session_store.rb
10 config/initializers/secret_token.rb
10 coverage
11 coverage
11 db/*.db
12 db/*.db
12 db/*.sqlite3
13 db/*.sqlite3
13 db/schema.rb
14 db/schema.rb
14 files/*
15 files/*
15 lib/redmine/scm/adapters/mercurial/redminehelper.pyc
16 lib/redmine/scm/adapters/mercurial/redminehelper.pyc
16 lib/redmine/scm/adapters/mercurial/redminehelper.pyo
17 lib/redmine/scm/adapters/mercurial/redminehelper.pyo
17 log/*.log*
18 log/*.log*
18 log/mongrel_debug
19 log/mongrel_debug
19 public/dispatch.*
20 public/dispatch.*
20 public/plugin_assets
21 public/plugin_assets
21 tmp/*
22 tmp/*
22 tmp/cache/*
23 tmp/cache/*
23 tmp/sessions/*
24 tmp/sessions/*
24 tmp/sockets/*
25 tmp/sockets/*
25 tmp/test/*
26 tmp/test/*
26 vendor/cache
27 vendor/cache
27 vendor/rails
28 vendor/rails
28 *.rbc
29 *.rbc
29
30
30 .svn/
31 .svn/
31 .git/
32 .git/
32
33
33 .bundle
34 .bundle
34 Gemfile.lock
35 Gemfile.lock
35 Gemfile.local
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"
3 gem 'rails', '3.2.3'
4 gem "i18n", "~> 0.4.2"
4 gem 'prototype-rails', '3.2.1'
5 gem "i18n", "~> 0.6.0"
5 gem "coderay", "~> 1.0.6"
6 gem "coderay", "~> 1.0.6"
6 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
7 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
7 gem "tzinfo", "~> 0.3.31"
8 gem "tzinfo", "~> 0.3.31"
9 gem "builder"
8
10
9 # Optional gem for LDAP authentication
11 # Optional gem for LDAP authentication
10 group :ldap do
12 group :ldap do
11 gem "net-ldap", "~> 0.3.1"
13 gem "net-ldap", "~> 0.3.1"
12 end
14 end
13
15
14 # Optional gem for OpenID authentication
16 # Optional gem for OpenID authentication
15 group :openid do
17 group :openid do
16 gem "ruby-openid", "~> 2.1.4", :require => "openid"
18 gem "ruby-openid", "~> 2.1.4", :require => "openid"
19 gem "rack-openid"
17 end
20 end
18
21
19 # Optional gem for exporting the gantt to a PNG file, not supported with jruby
22 # Optional gem for exporting the gantt to a PNG file, not supported with jruby
20 platforms :mri, :mingw do
23 platforms :mri, :mingw do
21 group :rmagick do
24 group :rmagick do
22 # RMagick 2 supports ruby 1.9
25 # RMagick 2 supports ruby 1.9
23 # RMagick 1 would be fine for ruby 1.8 but Bundler does not support
26 # RMagick 1 would be fine for ruby 1.8 but Bundler does not support
24 # different requirements for the same gem on different platforms
27 # different requirements for the same gem on different platforms
25 gem "rmagick", ">= 2.0.0"
28 gem "rmagick", ">= 2.0.0"
26 end
29 end
27 end
30 end
28
31
29 # Database gems
32 # Database gems
30 platforms :mri, :mingw do
33 platforms :mri, :mingw do
31 group :postgresql do
34 group :postgresql do
32 gem "pg", ">= 0.11.0"
35 gem "pg", ">= 0.11.0"
33 end
36 end
34
37
35 group :sqlite do
38 group :sqlite do
36 gem "sqlite3"
39 gem "sqlite3"
37 end
40 end
38 end
41 end
39
42
40 platforms :mri_18, :mingw_18 do
43 platforms :mri_18, :mingw_18 do
41 group :mysql do
44 group :mysql do
42 gem "mysql"
45 gem "mysql"
43 end
46 end
44 end
47 end
45
48
46 platforms :mri_19, :mingw_19 do
49 platforms :mri_19, :mingw_19 do
47 group :mysql do
50 group :mysql do
48 gem "mysql2", "~> 0.2.7"
51 gem "mysql2", "~> 0.3.11"
49 end
52 end
50 end
53 end
51
54
52 platforms :jruby do
55 platforms :jruby do
53 gem "jruby-openssl"
56 gem "jruby-openssl"
54
57
55 group :mysql do
58 group :mysql do
56 gem "activerecord-jdbcmysql-adapter"
59 gem "activerecord-jdbcmysql-adapter"
57 end
60 end
58
61
59 group :postgresql do
62 group :postgresql do
60 gem "activerecord-jdbcpostgresql-adapter"
63 gem "activerecord-jdbcpostgresql-adapter"
61 end
64 end
62
65
63 group :sqlite do
66 group :sqlite do
64 gem "activerecord-jdbcsqlite3-adapter"
67 gem "activerecord-jdbcsqlite3-adapter"
65 end
68 end
66 end
69 end
67
70
68 group :development do
71 group :development do
69 gem "rdoc", ">= 2.4.2"
72 gem "rdoc", ">= 2.4.2"
70 end
73 end
71
74
75
72 group :test do
76 group :test do
73 gem "shoulda", "~> 2.10.3"
77 gem "shoulda"
74 gem "mocha"
78 gem "mocha"
75 end
79 end
76
80
77 local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
81 local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
78 if File.exists?(local_gemfile)
82 if File.exists?(local_gemfile)
79 puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v`
83 puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v`
80 instance_eval File.read(local_gemfile)
84 instance_eval File.read(local_gemfile)
81 end
85 end
82
86
83 # Load plugins' Gemfiles
87 # Load plugins' Gemfiles
84 Dir.glob File.expand_path("../vendor/plugins/*/Gemfile", __FILE__) do |file|
88 Dir.glob File.expand_path("../vendor/plugins/*/Gemfile", __FILE__) do |file|
85 puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
89 puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
86 instance_eval File.read(file)
90 instance_eval File.read(file)
87 end
91 end
@@ -1,15 +1,7
1 #!/usr/bin/env rake
1 # Add your own tasks in files placed in lib/tasks ending in .rake,
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 RedmineApp::Application.load_tasks
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'
@@ -1,548 +1,532
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class Unauthorized < Exception; end
21 class Unauthorized < Exception; end
22
22
23 class ApplicationController < ActionController::Base
23 class ApplicationController < ActionController::Base
24 include Redmine::I18n
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 layout 'base'
30 layout 'base'
27 exempt_from_layout 'builder', 'rsb'
28
31
29 protect_from_forgery
32 protect_from_forgery
30 def handle_unverified_request
33 def handle_unverified_request
31 super
34 super
32 cookies.delete(:autologin)
35 cookies.delete(:autologin)
33 end
36 end
34 # Remove broken cookie after upgrade from 0.8.x (#4292)
37 # Remove broken cookie after upgrade from 0.8.x (#4292)
35 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
38 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
36 # TODO: remove it when Rails is fixed
39 # TODO: remove it when Rails is fixed
37 before_filter :delete_broken_cookies
40 before_filter :delete_broken_cookies
38 def delete_broken_cookies
41 def delete_broken_cookies
39 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
42 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
40 cookies.delete '_redmine_session'
43 cookies.delete '_redmine_session'
41 redirect_to home_path
44 redirect_to home_path
42 return false
45 return false
43 end
46 end
44 end
47 end
45
48
46 # FIXME: Remove this when all of Rack and Rails have learned how to
49 # FIXME: Remove this when all of Rack and Rails have learned how to
47 # properly use encodings
50 # properly use encodings
48 before_filter :params_filter
51 before_filter :params_filter
49
52
50 def params_filter
53 def params_filter
51 if RUBY_VERSION >= '1.9' && defined?(Rails) && Rails::VERSION::MAJOR < 3
54 if RUBY_VERSION >= '1.9' && defined?(Rails) && Rails::VERSION::MAJOR < 3
52 self.utf8nize!(params)
55 self.utf8nize!(params)
53 end
56 end
54 end
57 end
55
58
56 def utf8nize!(obj)
59 def utf8nize!(obj)
57 if obj.frozen?
60 if obj.frozen?
58 obj
61 obj
59 elsif obj.is_a? String
62 elsif obj.is_a? String
60 obj.respond_to?(:force_encoding) ? obj.force_encoding("UTF-8") : obj
63 obj.respond_to?(:force_encoding) ? obj.force_encoding("UTF-8") : obj
61 elsif obj.is_a? Hash
64 elsif obj.is_a? Hash
62 obj.each {|k, v| obj[k] = self.utf8nize!(v)}
65 obj.each {|k, v| obj[k] = self.utf8nize!(v)}
63 elsif obj.is_a? Array
66 elsif obj.is_a? Array
64 obj.each {|v| self.utf8nize!(v)}
67 obj.each {|v| self.utf8nize!(v)}
65 else
68 else
66 obj
69 obj
67 end
70 end
68 end
71 end
69
72
70 before_filter :user_setup, :check_if_login_required, :set_localization
73 before_filter :user_setup, :check_if_login_required, :set_localization
71 filter_parameter_logging :password
72
74
73 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
75 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
74 rescue_from ::Unauthorized, :with => :deny_access
76 rescue_from ::Unauthorized, :with => :deny_access
75
77
76 include Redmine::Search::Controller
78 include Redmine::Search::Controller
77 include Redmine::MenuManager::MenuController
79 include Redmine::MenuManager::MenuController
78 helper Redmine::MenuManager::MenuHelper
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 def user_setup
82 def user_setup
85 # Check the settings cache for each request
83 # Check the settings cache for each request
86 Setting.check_cache
84 Setting.check_cache
87 # Find the current user
85 # Find the current user
88 User.current = find_current_user
86 User.current = find_current_user
89 end
87 end
90
88
91 # Returns the current user or nil if no user is logged in
89 # Returns the current user or nil if no user is logged in
92 # and starts a session if needed
90 # and starts a session if needed
93 def find_current_user
91 def find_current_user
94 if session[:user_id]
92 if session[:user_id]
95 # existing session
93 # existing session
96 (User.active.find(session[:user_id]) rescue nil)
94 (User.active.find(session[:user_id]) rescue nil)
97 elsif cookies[:autologin] && Setting.autologin?
95 elsif cookies[:autologin] && Setting.autologin?
98 # auto-login feature starts a new session
96 # auto-login feature starts a new session
99 user = User.try_to_autologin(cookies[:autologin])
97 user = User.try_to_autologin(cookies[:autologin])
100 session[:user_id] = user.id if user
98 session[:user_id] = user.id if user
101 user
99 user
102 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
100 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
103 # RSS key authentication does not start a session
101 # RSS key authentication does not start a session
104 User.find_by_rss_key(params[:key])
102 User.find_by_rss_key(params[:key])
105 elsif Setting.rest_api_enabled? && accept_api_auth?
103 elsif Setting.rest_api_enabled? && accept_api_auth?
106 if (key = api_key_from_request)
104 if (key = api_key_from_request)
107 # Use API key
105 # Use API key
108 User.find_by_api_key(key)
106 User.find_by_api_key(key)
109 else
107 else
110 # HTTP Basic, either username/password or API key/random
108 # HTTP Basic, either username/password or API key/random
111 authenticate_with_http_basic do |username, password|
109 authenticate_with_http_basic do |username, password|
112 User.try_to_login(username, password) || User.find_by_api_key(username)
110 User.try_to_login(username, password) || User.find_by_api_key(username)
113 end
111 end
114 end
112 end
115 end
113 end
116 end
114 end
117
115
118 # Sets the logged in user
116 # Sets the logged in user
119 def logged_user=(user)
117 def logged_user=(user)
120 reset_session
118 reset_session
121 if user && user.is_a?(User)
119 if user && user.is_a?(User)
122 User.current = user
120 User.current = user
123 session[:user_id] = user.id
121 session[:user_id] = user.id
124 else
122 else
125 User.current = User.anonymous
123 User.current = User.anonymous
126 end
124 end
127 end
125 end
128
126
129 # Logs out current user
127 # Logs out current user
130 def logout_user
128 def logout_user
131 if User.current.logged?
129 if User.current.logged?
132 cookies.delete :autologin
130 cookies.delete :autologin
133 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
131 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
134 self.logged_user = nil
132 self.logged_user = nil
135 end
133 end
136 end
134 end
137
135
138 # check if login is globally required to access the application
136 # check if login is globally required to access the application
139 def check_if_login_required
137 def check_if_login_required
140 # no check needed if user is already logged in
138 # no check needed if user is already logged in
141 return true if User.current.logged?
139 return true if User.current.logged?
142 require_login if Setting.login_required?
140 require_login if Setting.login_required?
143 end
141 end
144
142
145 def set_localization
143 def set_localization
146 lang = nil
144 lang = nil
147 if User.current.logged?
145 if User.current.logged?
148 lang = find_language(User.current.language)
146 lang = find_language(User.current.language)
149 end
147 end
150 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
148 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
151 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
149 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
152 if !accept_lang.blank?
150 if !accept_lang.blank?
153 accept_lang = accept_lang.downcase
151 accept_lang = accept_lang.downcase
154 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
152 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
155 end
153 end
156 end
154 end
157 lang ||= Setting.default_language
155 lang ||= Setting.default_language
158 set_language_if_valid(lang)
156 set_language_if_valid(lang)
159 end
157 end
160
158
161 def require_login
159 def require_login
162 if !User.current.logged?
160 if !User.current.logged?
163 # Extract only the basic url parameters on non-GET requests
161 # Extract only the basic url parameters on non-GET requests
164 if request.get?
162 if request.get?
165 url = url_for(params)
163 url = url_for(params)
166 else
164 else
167 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
165 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
168 end
166 end
169 respond_to do |format|
167 respond_to do |format|
170 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
168 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
171 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
169 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
172 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
170 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
173 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
171 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
174 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
172 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
175 end
173 end
176 return false
174 return false
177 end
175 end
178 true
176 true
179 end
177 end
180
178
181 def require_admin
179 def require_admin
182 return unless require_login
180 return unless require_login
183 if !User.current.admin?
181 if !User.current.admin?
184 render_403
182 render_403
185 return false
183 return false
186 end
184 end
187 true
185 true
188 end
186 end
189
187
190 def deny_access
188 def deny_access
191 User.current.logged? ? render_403 : require_login
189 User.current.logged? ? render_403 : require_login
192 end
190 end
193
191
194 # Authorize the user for the requested action
192 # Authorize the user for the requested action
195 def authorize(ctrl = params[:controller], action = params[:action], global = false)
193 def authorize(ctrl = params[:controller], action = params[:action], global = false)
196 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
194 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
197 if allowed
195 if allowed
198 true
196 true
199 else
197 else
200 if @project && @project.archived?
198 if @project && @project.archived?
201 render_403 :message => :notice_not_authorized_archived_project
199 render_403 :message => :notice_not_authorized_archived_project
202 else
200 else
203 deny_access
201 deny_access
204 end
202 end
205 end
203 end
206 end
204 end
207
205
208 # Authorize the user for the requested action outside a project
206 # Authorize the user for the requested action outside a project
209 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
207 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
210 authorize(ctrl, action, global)
208 authorize(ctrl, action, global)
211 end
209 end
212
210
213 # Find project of id params[:id]
211 # Find project of id params[:id]
214 def find_project
212 def find_project
215 @project = Project.find(params[:id])
213 @project = Project.find(params[:id])
216 rescue ActiveRecord::RecordNotFound
214 rescue ActiveRecord::RecordNotFound
217 render_404
215 render_404
218 end
216 end
219
217
220 # Find project of id params[:project_id]
218 # Find project of id params[:project_id]
221 def find_project_by_project_id
219 def find_project_by_project_id
222 @project = Project.find(params[:project_id])
220 @project = Project.find(params[:project_id])
223 rescue ActiveRecord::RecordNotFound
221 rescue ActiveRecord::RecordNotFound
224 render_404
222 render_404
225 end
223 end
226
224
227 # Find a project based on params[:project_id]
225 # Find a project based on params[:project_id]
228 # TODO: some subclasses override this, see about merging their logic
226 # TODO: some subclasses override this, see about merging their logic
229 def find_optional_project
227 def find_optional_project
230 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
228 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
231 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
229 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
232 allowed ? true : deny_access
230 allowed ? true : deny_access
233 rescue ActiveRecord::RecordNotFound
231 rescue ActiveRecord::RecordNotFound
234 render_404
232 render_404
235 end
233 end
236
234
237 # Finds and sets @project based on @object.project
235 # Finds and sets @project based on @object.project
238 def find_project_from_association
236 def find_project_from_association
239 render_404 unless @object.present?
237 render_404 unless @object.present?
240
238
241 @project = @object.project
239 @project = @object.project
242 end
240 end
243
241
244 def find_model_object
242 def find_model_object
245 model = self.class.read_inheritable_attribute('model_object')
243 model = self.class.model_object
246 if model
244 if model
247 @object = model.find(params[:id])
245 @object = model.find(params[:id])
248 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
246 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
249 end
247 end
250 rescue ActiveRecord::RecordNotFound
248 rescue ActiveRecord::RecordNotFound
251 render_404
249 render_404
252 end
250 end
253
251
254 def self.model_object(model)
252 def self.model_object(model)
255 write_inheritable_attribute('model_object', model)
253 self.model_object = model
256 end
254 end
257
255
258 # Filter for bulk issue operations
256 # Filter for bulk issue operations
259 def find_issues
257 def find_issues
260 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
258 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
261 raise ActiveRecord::RecordNotFound if @issues.empty?
259 raise ActiveRecord::RecordNotFound if @issues.empty?
262 if @issues.detect {|issue| !issue.visible?}
260 if @issues.detect {|issue| !issue.visible?}
263 deny_access
261 deny_access
264 return
262 return
265 end
263 end
266 @projects = @issues.collect(&:project).compact.uniq
264 @projects = @issues.collect(&:project).compact.uniq
267 @project = @projects.first if @projects.size == 1
265 @project = @projects.first if @projects.size == 1
268 rescue ActiveRecord::RecordNotFound
266 rescue ActiveRecord::RecordNotFound
269 render_404
267 render_404
270 end
268 end
271
269
272 # make sure that the user is a member of the project (or admin) if project is private
270 # make sure that the user is a member of the project (or admin) if project is private
273 # used as a before_filter for actions that do not require any particular permission on the project
271 # used as a before_filter for actions that do not require any particular permission on the project
274 def check_project_privacy
272 def check_project_privacy
275 if @project && @project.active?
273 if @project && @project.active?
276 if @project.visible?
274 if @project.visible?
277 true
275 true
278 else
276 else
279 deny_access
277 deny_access
280 end
278 end
281 else
279 else
282 @project = nil
280 @project = nil
283 render_404
281 render_404
284 false
282 false
285 end
283 end
286 end
284 end
287
285
288 def back_url
286 def back_url
289 params[:back_url] || request.env['HTTP_REFERER']
287 params[:back_url] || request.env['HTTP_REFERER']
290 end
288 end
291
289
292 def redirect_back_or_default(default)
290 def redirect_back_or_default(default)
293 back_url = CGI.unescape(params[:back_url].to_s)
291 back_url = CGI.unescape(params[:back_url].to_s)
294 if !back_url.blank?
292 if !back_url.blank?
295 begin
293 begin
296 uri = URI.parse(back_url)
294 uri = URI.parse(back_url)
297 # do not redirect user to another host or to the login or register page
295 # do not redirect user to another host or to the login or register page
298 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
296 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
299 redirect_to(back_url)
297 redirect_to(back_url)
300 return
298 return
301 end
299 end
302 rescue URI::InvalidURIError
300 rescue URI::InvalidURIError
303 # redirect to default
301 # redirect to default
304 end
302 end
305 end
303 end
306 redirect_to default
304 redirect_to default
307 false
305 false
308 end
306 end
309
307
310 # Redirects to the request referer if present, redirects to args or call block otherwise.
308 # Redirects to the request referer if present, redirects to args or call block otherwise.
311 def redirect_to_referer_or(*args, &block)
309 def redirect_to_referer_or(*args, &block)
312 redirect_to :back
310 redirect_to :back
313 rescue ::ActionController::RedirectBackError
311 rescue ::ActionController::RedirectBackError
314 if args.any?
312 if args.any?
315 redirect_to *args
313 redirect_to *args
316 elsif block_given?
314 elsif block_given?
317 block.call
315 block.call
318 else
316 else
319 raise "#redirect_to_referer_or takes arguments or a block"
317 raise "#redirect_to_referer_or takes arguments or a block"
320 end
318 end
321 end
319 end
322
320
323 def render_403(options={})
321 def render_403(options={})
324 @project = nil
322 @project = nil
325 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
323 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
326 return false
324 return false
327 end
325 end
328
326
329 def render_404(options={})
327 def render_404(options={})
330 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
328 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
331 return false
329 return false
332 end
330 end
333
331
334 # Renders an error response
332 # Renders an error response
335 def render_error(arg)
333 def render_error(arg)
336 arg = {:message => arg} unless arg.is_a?(Hash)
334 arg = {:message => arg} unless arg.is_a?(Hash)
337
335
338 @message = arg[:message]
336 @message = arg[:message]
339 @message = l(@message) if @message.is_a?(Symbol)
337 @message = l(@message) if @message.is_a?(Symbol)
340 @status = arg[:status] || 500
338 @status = arg[:status] || 500
341
339
342 respond_to do |format|
340 respond_to do |format|
343 format.html {
341 format.html {
344 render :template => 'common/error', :layout => use_layout, :status => @status
342 render :template => 'common/error', :layout => use_layout, :status => @status
345 }
343 }
346 format.atom { head @status }
344 format.atom { head @status }
347 format.xml { head @status }
345 format.xml { head @status }
348 format.js { head @status }
346 format.js { head @status }
349 format.json { head @status }
347 format.json { head @status }
350 end
348 end
351 end
349 end
352
350
353 # Filter for actions that provide an API response
351 # Filter for actions that provide an API response
354 # but have no HTML representation for non admin users
352 # but have no HTML representation for non admin users
355 def require_admin_or_api_request
353 def require_admin_or_api_request
356 return true if api_request?
354 return true if api_request?
357 if User.current.admin?
355 if User.current.admin?
358 true
356 true
359 elsif User.current.logged?
357 elsif User.current.logged?
360 render_error(:status => 406)
358 render_error(:status => 406)
361 else
359 else
362 deny_access
360 deny_access
363 end
361 end
364 end
362 end
365
363
366 # Picks which layout to use based on the request
364 # Picks which layout to use based on the request
367 #
365 #
368 # @return [boolean, string] name of the layout to use or false for no layout
366 # @return [boolean, string] name of the layout to use or false for no layout
369 def use_layout
367 def use_layout
370 request.xhr? ? false : 'base'
368 request.xhr? ? false : 'base'
371 end
369 end
372
370
373 def invalid_authenticity_token
371 def invalid_authenticity_token
374 if api_request?
372 if api_request?
375 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
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 end
374 end
377 render_error "Invalid form authenticity token."
375 render_error "Invalid form authenticity token."
378 end
376 end
379
377
380 def render_feed(items, options={})
378 def render_feed(items, options={})
381 @items = items || []
379 @items = items || []
382 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
380 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
383 @items = @items.slice(0, Setting.feeds_limit.to_i)
381 @items = @items.slice(0, Setting.feeds_limit.to_i)
384 @title = options[:title] || Setting.app_title
382 @title = options[:title] || Setting.app_title
385 render :template => "common/feed.atom", :layout => false,
383 render :template => "common/feed.atom", :layout => false,
386 :content_type => 'application/atom+xml'
384 :content_type => 'application/atom+xml'
387 end
385 end
388
386
389 def self.accept_rss_auth(*actions)
387 def self.accept_rss_auth(*actions)
390 if actions.any?
388 if actions.any?
391 write_inheritable_attribute('accept_rss_auth_actions', actions)
389 self.accept_rss_auth_actions = actions
392 else
390 else
393 read_inheritable_attribute('accept_rss_auth_actions') || []
391 self.accept_rss_auth_actions || []
394 end
392 end
395 end
393 end
396
394
397 def accept_rss_auth?(action=action_name)
395 def accept_rss_auth?(action=action_name)
398 self.class.accept_rss_auth.include?(action.to_sym)
396 self.class.accept_rss_auth.include?(action.to_sym)
399 end
397 end
400
398
401 def self.accept_api_auth(*actions)
399 def self.accept_api_auth(*actions)
402 if actions.any?
400 if actions.any?
403 write_inheritable_attribute('accept_api_auth_actions', actions)
401 self.accept_api_auth_actions = actions
404 else
402 else
405 read_inheritable_attribute('accept_api_auth_actions') || []
403 self.accept_api_auth_actions || []
406 end
404 end
407 end
405 end
408
406
409 def accept_api_auth?(action=action_name)
407 def accept_api_auth?(action=action_name)
410 self.class.accept_api_auth.include?(action.to_sym)
408 self.class.accept_api_auth.include?(action.to_sym)
411 end
409 end
412
410
413 # Returns the number of objects that should be displayed
411 # Returns the number of objects that should be displayed
414 # on the paginated list
412 # on the paginated list
415 def per_page_option
413 def per_page_option
416 per_page = nil
414 per_page = nil
417 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
415 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
418 per_page = params[:per_page].to_s.to_i
416 per_page = params[:per_page].to_s.to_i
419 session[:per_page] = per_page
417 session[:per_page] = per_page
420 elsif session[:per_page]
418 elsif session[:per_page]
421 per_page = session[:per_page]
419 per_page = session[:per_page]
422 else
420 else
423 per_page = Setting.per_page_options_array.first || 25
421 per_page = Setting.per_page_options_array.first || 25
424 end
422 end
425 per_page
423 per_page
426 end
424 end
427
425
428 # Returns offset and limit used to retrieve objects
426 # Returns offset and limit used to retrieve objects
429 # for an API response based on offset, limit and page parameters
427 # for an API response based on offset, limit and page parameters
430 def api_offset_and_limit(options=params)
428 def api_offset_and_limit(options=params)
431 if options[:offset].present?
429 if options[:offset].present?
432 offset = options[:offset].to_i
430 offset = options[:offset].to_i
433 if offset < 0
431 if offset < 0
434 offset = 0
432 offset = 0
435 end
433 end
436 end
434 end
437 limit = options[:limit].to_i
435 limit = options[:limit].to_i
438 if limit < 1
436 if limit < 1
439 limit = 25
437 limit = 25
440 elsif limit > 100
438 elsif limit > 100
441 limit = 100
439 limit = 100
442 end
440 end
443 if offset.nil? && options[:page].present?
441 if offset.nil? && options[:page].present?
444 offset = (options[:page].to_i - 1) * limit
442 offset = (options[:page].to_i - 1) * limit
445 offset = 0 if offset < 0
443 offset = 0 if offset < 0
446 end
444 end
447 offset ||= 0
445 offset ||= 0
448
446
449 [offset, limit]
447 [offset, limit]
450 end
448 end
451
449
452 # qvalues http header parser
450 # qvalues http header parser
453 # code taken from webrick
451 # code taken from webrick
454 def parse_qvalues(value)
452 def parse_qvalues(value)
455 tmp = []
453 tmp = []
456 if value
454 if value
457 parts = value.split(/,\s*/)
455 parts = value.split(/,\s*/)
458 parts.each {|part|
456 parts.each {|part|
459 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
457 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
460 val = m[1]
458 val = m[1]
461 q = (m[2] or 1).to_f
459 q = (m[2] or 1).to_f
462 tmp.push([val, q])
460 tmp.push([val, q])
463 end
461 end
464 }
462 }
465 tmp = tmp.sort_by{|val, q| -q}
463 tmp = tmp.sort_by{|val, q| -q}
466 tmp.collect!{|val, q| val}
464 tmp.collect!{|val, q| val}
467 end
465 end
468 return tmp
466 return tmp
469 rescue
467 rescue
470 nil
468 nil
471 end
469 end
472
470
473 # Returns a string that can be used as filename value in Content-Disposition header
471 # Returns a string that can be used as filename value in Content-Disposition header
474 def filename_for_content_disposition(name)
472 def filename_for_content_disposition(name)
475 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
473 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
476 end
474 end
477
475
478 def api_request?
476 def api_request?
479 %w(xml json).include? params[:format]
477 %w(xml json).include? params[:format]
480 end
478 end
481
479
482 # Returns the API key present in the request
480 # Returns the API key present in the request
483 def api_key_from_request
481 def api_key_from_request
484 if params[:key].present?
482 if params[:key].present?
485 params[:key]
483 params[:key]
486 elsif request.headers["X-Redmine-API-Key"].present?
484 elsif request.headers["X-Redmine-API-Key"].present?
487 request.headers["X-Redmine-API-Key"]
485 request.headers["X-Redmine-API-Key"]
488 end
486 end
489 end
487 end
490
488
491 # Renders a warning flash if obj has unsaved attachments
489 # Renders a warning flash if obj has unsaved attachments
492 def render_attachment_warning_if_needed(obj)
490 def render_attachment_warning_if_needed(obj)
493 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
491 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
494 end
492 end
495
493
496 # Sets the `flash` notice or error based the number of issues that did not save
494 # Sets the `flash` notice or error based the number of issues that did not save
497 #
495 #
498 # @param [Array, Issue] issues all of the saved and unsaved Issues
496 # @param [Array, Issue] issues all of the saved and unsaved Issues
499 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
497 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
500 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
498 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
501 if unsaved_issue_ids.empty?
499 if unsaved_issue_ids.empty?
502 flash[:notice] = l(:notice_successful_update) unless issues.empty?
500 flash[:notice] = l(:notice_successful_update) unless issues.empty?
503 else
501 else
504 flash[:error] = l(:notice_failed_to_save_issues,
502 flash[:error] = l(:notice_failed_to_save_issues,
505 :count => unsaved_issue_ids.size,
503 :count => unsaved_issue_ids.size,
506 :total => issues.size,
504 :total => issues.size,
507 :ids => '#' + unsaved_issue_ids.join(', #'))
505 :ids => '#' + unsaved_issue_ids.join(', #'))
508 end
506 end
509 end
507 end
510
508
511 # Rescues an invalid query statement. Just in case...
509 # Rescues an invalid query statement. Just in case...
512 def query_statement_invalid(exception)
510 def query_statement_invalid(exception)
513 logger.error "Query::StatementInvalid: #{exception.message}" if logger
511 logger.error "Query::StatementInvalid: #{exception.message}" if logger
514 session.delete(:query)
512 session.delete(:query)
515 sort_clear if respond_to?(:sort_clear)
513 sort_clear if respond_to?(:sort_clear)
516 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
514 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
517 end
515 end
518
516
519 # Renders API response on validation failure
517 # Renders API response on validation failure
520 def render_validation_errors(objects)
518 def render_validation_errors(objects)
521 if objects.is_a?(Array)
519 if objects.is_a?(Array)
522 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
520 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
523 else
521 else
524 @error_messages = objects.errors.full_messages
522 @error_messages = objects.errors.full_messages
525 end
523 end
526 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => false
524 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
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
541 end
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 # doesn't use the layout for api requests
528 # doesn't use the layout for api requests
545 def pick_layout(*args)
529 def _include_layout?(*args)
546 api_request? ? nil : super
530 api_request? ? false : super
547 end
531 end
548 end
532 end
@@ -1,143 +1,144
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MessagesController < ApplicationController
18 class MessagesController < ApplicationController
19 menu_item :boards
19 menu_item :boards
20 default_search_scope :messages
20 default_search_scope :messages
21 before_filter :find_board, :only => [:new, :preview]
21 before_filter :find_board, :only => [:new, :preview]
22 before_filter :find_message, :except => [:new, :preview]
22 before_filter :find_message, :except => [:new, :preview]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
24
24
25 helper :watchers
25 helper :watchers
26 helper :attachments
26 helper :attachments
27 include AttachmentsHelper
27 include AttachmentsHelper
28
28
29 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
29 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
30
30
31 # Show a topic and its replies
31 # Show a topic and its replies
32 def show
32 def show
33 page = params[:page]
33 page = params[:page]
34 # Find the page of the requested reply
34 # Find the page of the requested reply
35 if params[:r] && page.nil?
35 if params[:r] && page.nil?
36 offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
36 offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
37 page = 1 + offset / REPLIES_PER_PAGE
37 page = 1 + offset / REPLIES_PER_PAGE
38 end
38 end
39
39
40 @reply_count = @topic.children.count
40 @reply_count = @topic.children.count
41 @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
41 @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
42 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
42 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
43 :order => "#{Message.table_name}.created_on ASC",
43 :order => "#{Message.table_name}.created_on ASC",
44 :limit => @reply_pages.items_per_page,
44 :limit => @reply_pages.items_per_page,
45 :offset => @reply_pages.current.offset)
45 :offset => @reply_pages.current.offset)
46
46
47 @reply = Message.new(:subject => "RE: #{@message.subject}")
47 @reply = Message.new(:subject => "RE: #{@message.subject}")
48 render :action => "show", :layout => false if request.xhr?
48 render :action => "show", :layout => false if request.xhr?
49 end
49 end
50
50
51 # Create a new topic
51 # Create a new topic
52 def new
52 def new
53 @message = Message.new
53 @message = Message.new
54 @message.author = User.current
54 @message.author = User.current
55 @message.board = @board
55 @message.board = @board
56 @message.safe_attributes = params[:message]
56 @message.safe_attributes = params[:message]
57 if request.post?
57 if request.post?
58 @message.save_attachments(params[:attachments])
58 @message.save_attachments(params[:attachments])
59 if @message.save
59 if @message.save
60 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
60 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
61 render_attachment_warning_if_needed(@message)
61 render_attachment_warning_if_needed(@message)
62 redirect_to :action => 'show', :id => @message
62 redirect_to :action => 'show', :id => @message
63 end
63 end
64 end
64 end
65 end
65 end
66
66
67 # Reply to a topic
67 # Reply to a topic
68 def reply
68 def reply
69 @reply = Message.new
69 @reply = Message.new
70 @reply.author = User.current
70 @reply.author = User.current
71 @reply.board = @board
71 @reply.board = @board
72 @reply.safe_attributes = params[:reply]
72 @reply.safe_attributes = params[:reply]
73 @topic.children << @reply
73 @topic.children << @reply
74 if !@reply.new_record?
74 if !@reply.new_record?
75 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
75 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
76 attachments = Attachment.attach_files(@reply, params[:attachments])
76 attachments = Attachment.attach_files(@reply, params[:attachments])
77 render_attachment_warning_if_needed(@reply)
77 render_attachment_warning_if_needed(@reply)
78 end
78 end
79 redirect_to :action => 'show', :id => @topic, :r => @reply
79 redirect_to :action => 'show', :id => @topic, :r => @reply
80 end
80 end
81
81
82 # Edit a message
82 # Edit a message
83 def edit
83 def edit
84 (render_403; return false) unless @message.editable_by?(User.current)
84 (render_403; return false) unless @message.editable_by?(User.current)
85 @message.safe_attributes = params[:message]
85 @message.safe_attributes = params[:message]
86 if request.post? && @message.save
86 if request.post? && @message.save
87 attachments = Attachment.attach_files(@message, params[:attachments])
87 attachments = Attachment.attach_files(@message, params[:attachments])
88 render_attachment_warning_if_needed(@message)
88 render_attachment_warning_if_needed(@message)
89 flash[:notice] = l(:notice_successful_update)
89 flash[:notice] = l(:notice_successful_update)
90 @message.reload
90 @message.reload
91 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
91 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
92 end
92 end
93 end
93 end
94
94
95 # Delete a messages
95 # Delete a messages
96 def destroy
96 def destroy
97 (render_403; return false) unless @message.destroyable_by?(User.current)
97 (render_403; return false) unless @message.destroyable_by?(User.current)
98 r = @message.to_param
98 @message.destroy
99 @message.destroy
99 redirect_to @message.parent.nil? ?
100 redirect_to @message.parent.nil? ?
100 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
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 end
103 end
103
104
104 def quote
105 def quote
105 user = @message.author
106 user = @message.author
106 text = @message.content
107 text = @message.content
107 subject = @message.subject.gsub('"', '\"')
108 subject = @message.subject.gsub('"', '\"')
108 subject = "RE: #{subject}" unless subject.starts_with?('RE:')
109 subject = "RE: #{subject}" unless subject.starts_with?('RE:')
109 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
110 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
110 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
111 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
111 render(:update) { |page|
112 render(:update) { |page|
112 page << "$('message_subject').value = \"#{subject}\";"
113 page << "$('message_subject').value = \"#{subject}\";"
113 page.<< "$('message_content').value = \"#{content}\";"
114 page.<< "$('message_content').value = \"#{content}\";"
114 page.show 'reply'
115 page.show 'reply'
115 page << "Form.Element.focus('message_content');"
116 page << "Form.Element.focus('message_content');"
116 page << "Element.scrollTo('reply');"
117 page << "Element.scrollTo('reply');"
117 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
118 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
118 }
119 }
119 end
120 end
120
121
121 def preview
122 def preview
122 message = @board.messages.find_by_id(params[:id])
123 message = @board.messages.find_by_id(params[:id])
123 @attachements = message.attachments if message
124 @attachements = message.attachments if message
124 @text = (params[:message] || params[:reply])[:content]
125 @text = (params[:message] || params[:reply])[:content]
125 render :partial => 'common/preview'
126 render :partial => 'common/preview'
126 end
127 end
127
128
128 private
129 private
129 def find_message
130 def find_message
130 find_board
131 find_board
131 @message = @board.messages.find(params[:id], :include => :parent)
132 @message = @board.messages.find(params[:id], :include => :parent)
132 @topic = @message.root
133 @topic = @message.root
133 rescue ActiveRecord::RecordNotFound
134 rescue ActiveRecord::RecordNotFound
134 render_404
135 render_404
135 end
136 end
136
137
137 def find_board
138 def find_board
138 @board = Board.find(params[:board_id], :include => :project)
139 @board = Board.find(params[:board_id], :include => :project)
139 @project = @board.project
140 @project = @board.project
140 rescue ActiveRecord::RecordNotFound
141 rescue ActiveRecord::RecordNotFound
141 render_404
142 render_404
142 end
143 end
143 end
144 end
@@ -1,426 +1,426
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21 require 'redmine/scm/adapters/abstract_adapter'
21
22
22 class ChangesetNotFound < Exception; end
23 class ChangesetNotFound < Exception; end
23 class InvalidRevisionParam < Exception; end
24 class InvalidRevisionParam < Exception; end
24
25
25 class RepositoriesController < ApplicationController
26 class RepositoriesController < ApplicationController
26 menu_item :repository
27 menu_item :repository
27 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
28 default_search_scope :changesets
29 default_search_scope :changesets
29
30
30 before_filter :find_project_by_project_id, :only => [:new, :create]
31 before_filter :find_project_by_project_id, :only => [:new, :create]
31 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
32 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
33 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
34 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
34 before_filter :authorize
35 before_filter :authorize
35 accept_rss_auth :revisions
36 accept_rss_auth :revisions
36
37
37 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
38
39
39 def new
40 def new
40 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
41 @repository = Repository.factory(scm)
42 @repository = Repository.factory(scm)
42 @repository.is_default = @project.repository.nil?
43 @repository.is_default = @project.repository.nil?
43 @repository.project = @project
44 @repository.project = @project
44 render :layout => !request.xhr?
45 render :layout => !request.xhr?
45 end
46 end
46
47
47 def create
48 def create
48 @repository = Repository.factory(params[:repository_scm], params[:repository])
49 @repository = Repository.factory(params[:repository_scm], params[:repository])
49 @repository.project = @project
50 @repository.project = @project
50 if request.post? && @repository.save
51 if request.post? && @repository.save
51 redirect_to settings_project_path(@project, :tab => 'repositories')
52 redirect_to settings_project_path(@project, :tab => 'repositories')
52 else
53 else
53 render :action => 'new'
54 render :action => 'new'
54 end
55 end
55 end
56 end
56
57
57 def edit
58 def edit
58 end
59 end
59
60
60 def update
61 def update
61 @repository.attributes = params[:repository]
62 @repository.attributes = params[:repository]
62 @repository.project = @project
63 @repository.project = @project
63 if request.put? && @repository.save
64 if request.put? && @repository.save
64 redirect_to settings_project_path(@project, :tab => 'repositories')
65 redirect_to settings_project_path(@project, :tab => 'repositories')
65 else
66 else
66 render :action => 'edit'
67 render :action => 'edit'
67 end
68 end
68 end
69 end
69
70
70 def committers
71 def committers
71 @committers = @repository.committers
72 @committers = @repository.committers
72 @users = @project.users
73 @users = @project.users
73 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
74 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
74 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
75 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
75 @users.compact!
76 @users.compact!
76 @users.sort!
77 @users.sort!
77 if request.post? && params[:committers].is_a?(Hash)
78 if request.post? && params[:committers].is_a?(Hash)
78 # Build a hash with repository usernames as keys and corresponding user ids as values
79 # Build a hash with repository usernames as keys and corresponding user ids as values
79 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
80 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
80 flash[:notice] = l(:notice_successful_update)
81 flash[:notice] = l(:notice_successful_update)
81 redirect_to settings_project_path(@project, :tab => 'repositories')
82 redirect_to settings_project_path(@project, :tab => 'repositories')
82 end
83 end
83 end
84 end
84
85
85 def destroy
86 def destroy
86 @repository.destroy if request.delete?
87 @repository.destroy if request.delete?
87 redirect_to settings_project_path(@project, :tab => 'repositories')
88 redirect_to settings_project_path(@project, :tab => 'repositories')
88 end
89 end
89
90
90 def show
91 def show
91 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
92 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
92
93
93 @entries = @repository.entries(@path, @rev)
94 @entries = @repository.entries(@path, @rev)
94 @changeset = @repository.find_changeset_by_name(@rev)
95 @changeset = @repository.find_changeset_by_name(@rev)
95 if request.xhr?
96 if request.xhr?
96 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
97 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
97 else
98 else
98 (show_error_not_found; return) unless @entries
99 (show_error_not_found; return) unless @entries
99 @changesets = @repository.latest_changesets(@path, @rev)
100 @changesets = @repository.latest_changesets(@path, @rev)
100 @properties = @repository.properties(@path, @rev)
101 @properties = @repository.properties(@path, @rev)
101 @repositories = @project.repositories
102 @repositories = @project.repositories
102 render :action => 'show'
103 render :action => 'show'
103 end
104 end
104 end
105 end
105
106
106 alias_method :browse, :show
107 alias_method :browse, :show
107
108
108 def changes
109 def changes
109 @entry = @repository.entry(@path, @rev)
110 @entry = @repository.entry(@path, @rev)
110 (show_error_not_found; return) unless @entry
111 (show_error_not_found; return) unless @entry
111 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
112 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
112 @properties = @repository.properties(@path, @rev)
113 @properties = @repository.properties(@path, @rev)
113 @changeset = @repository.find_changeset_by_name(@rev)
114 @changeset = @repository.find_changeset_by_name(@rev)
114 end
115 end
115
116
116 def revisions
117 def revisions
117 @changeset_count = @repository.changesets.count
118 @changeset_count = @repository.changesets.count
118 @changeset_pages = Paginator.new self, @changeset_count,
119 @changeset_pages = Paginator.new self, @changeset_count,
119 per_page_option,
120 per_page_option,
120 params['page']
121 params['page']
121 @changesets = @repository.changesets.find(:all,
122 @changesets = @repository.changesets.find(:all,
122 :limit => @changeset_pages.items_per_page,
123 :limit => @changeset_pages.items_per_page,
123 :offset => @changeset_pages.current.offset,
124 :offset => @changeset_pages.current.offset,
124 :include => [:user, :repository, :parents])
125 :include => [:user, :repository, :parents])
125
126
126 respond_to do |format|
127 respond_to do |format|
127 format.html { render :layout => false if request.xhr? }
128 format.html { render :layout => false if request.xhr? }
128 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
129 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
129 end
130 end
130 end
131 end
131
132
132 def entry
133 def entry
133 @entry = @repository.entry(@path, @rev)
134 @entry = @repository.entry(@path, @rev)
134 (show_error_not_found; return) unless @entry
135 (show_error_not_found; return) unless @entry
135
136
136 # If the entry is a dir, show the browser
137 # If the entry is a dir, show the browser
137 (show; return) if @entry.is_dir?
138 (show; return) if @entry.is_dir?
138
139
139 @content = @repository.cat(@path, @rev)
140 @content = @repository.cat(@path, @rev)
140 (show_error_not_found; return) unless @content
141 (show_error_not_found; return) unless @content
141 if 'raw' == params[:format] ||
142 if 'raw' == params[:format] ||
142 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
143 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
143 ! is_entry_text_data?(@content, @path)
144 ! is_entry_text_data?(@content, @path)
144 # Force the download
145 # Force the download
145 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
146 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
146 send_type = Redmine::MimeType.of(@path)
147 send_type = Redmine::MimeType.of(@path)
147 send_opt[:type] = send_type.to_s if send_type
148 send_opt[:type] = send_type.to_s if send_type
148 send_data @content, send_opt
149 send_data @content, send_opt
149 else
150 else
150 # Prevent empty lines when displaying a file with Windows style eol
151 # Prevent empty lines when displaying a file with Windows style eol
151 # TODO: UTF-16
152 # TODO: UTF-16
152 # Is this needs? AttachmentsController reads file simply.
153 # Is this needs? AttachmentsController reads file simply.
153 @content.gsub!("\r\n", "\n")
154 @content.gsub!("\r\n", "\n")
154 @changeset = @repository.find_changeset_by_name(@rev)
155 @changeset = @repository.find_changeset_by_name(@rev)
155 end
156 end
156 end
157 end
157
158
158 def is_entry_text_data?(ent, path)
159 def is_entry_text_data?(ent, path)
159 # UTF-16 contains "\x00".
160 # UTF-16 contains "\x00".
160 # It is very strict that file contains less than 30% of ascii symbols
161 # It is very strict that file contains less than 30% of ascii symbols
161 # in non Western Europe.
162 # in non Western Europe.
162 return true if Redmine::MimeType.is_type?('text', path)
163 return true if Redmine::MimeType.is_type?('text', path)
163 # Ruby 1.8.6 has a bug of integer divisions.
164 # Ruby 1.8.6 has a bug of integer divisions.
164 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
165 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
165 return false if ent.is_binary_data?
166 return false if ent.is_binary_data?
166 true
167 true
167 end
168 end
168 private :is_entry_text_data?
169 private :is_entry_text_data?
169
170
170 def annotate
171 def annotate
171 @entry = @repository.entry(@path, @rev)
172 @entry = @repository.entry(@path, @rev)
172 (show_error_not_found; return) unless @entry
173 (show_error_not_found; return) unless @entry
173
174
174 @annotate = @repository.scm.annotate(@path, @rev)
175 @annotate = @repository.scm.annotate(@path, @rev)
175 if @annotate.nil? || @annotate.empty?
176 if @annotate.nil? || @annotate.empty?
176 (render_error l(:error_scm_annotate); return)
177 (render_error l(:error_scm_annotate); return)
177 end
178 end
178 ann_buf_size = 0
179 ann_buf_size = 0
179 @annotate.lines.each do |buf|
180 @annotate.lines.each do |buf|
180 ann_buf_size += buf.size
181 ann_buf_size += buf.size
181 end
182 end
182 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
183 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
183 (render_error l(:error_scm_annotate_big_text_file); return)
184 (render_error l(:error_scm_annotate_big_text_file); return)
184 end
185 end
185 @changeset = @repository.find_changeset_by_name(@rev)
186 @changeset = @repository.find_changeset_by_name(@rev)
186 end
187 end
187
188
188 def revision
189 def revision
189 respond_to do |format|
190 respond_to do |format|
190 format.html
191 format.html
191 format.js {render :layout => false}
192 format.js {render :layout => false}
192 end
193 end
193 end
194 end
194
195
195 # Adds a related issue to a changeset
196 # Adds a related issue to a changeset
196 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
197 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
197 def add_related_issue
198 def add_related_issue
198 @issue = @changeset.find_referenced_issue_by_id(params[:issue_id])
199 @issue = @changeset.find_referenced_issue_by_id(params[:issue_id])
199 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
200 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
200 @issue = nil
201 @issue = nil
201 end
202 end
202
203
203 if @issue
204 if @issue
204 @changeset.issues << @issue
205 @changeset.issues << @issue
205 respond_to do |format|
206 respond_to do |format|
206 format.js {
207 format.js {
207 render :update do |page|
208 render :update do |page|
208 page.replace_html "related-issues", :partial => "related_issues"
209 page.replace_html "related-issues", :partial => "related_issues"
209 page.visual_effect :highlight, "related-issue-#{@issue.id}"
210 page.visual_effect :highlight, "related-issue-#{@issue.id}"
210 end
211 end
211 }
212 }
212 end
213 end
213 else
214 else
214 respond_to do |format|
215 respond_to do |format|
215 format.js {
216 format.js {
216 render :update do |page|
217 render :update do |page|
217 page.alert(l(:label_issue) + ' ' + l('activerecord.errors.messages.invalid'))
218 page.alert(l(:label_issue) + ' ' + l('activerecord.errors.messages.invalid'))
218 end
219 end
219 }
220 }
220 end
221 end
221 end
222 end
222 end
223 end
223
224
224 # Removes a related issue from a changeset
225 # Removes a related issue from a changeset
225 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
226 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
226 def remove_related_issue
227 def remove_related_issue
227 @issue = Issue.visible.find_by_id(params[:issue_id])
228 @issue = Issue.visible.find_by_id(params[:issue_id])
228 if @issue
229 if @issue
229 @changeset.issues.delete(@issue)
230 @changeset.issues.delete(@issue)
230 end
231 end
231
232
232 respond_to do |format|
233 respond_to do |format|
233 format.js {
234 format.js {
234 render :update do |page|
235 render :update do |page|
235 page.remove "related-issue-#{@issue.id}"
236 page.remove "related-issue-#{@issue.id}"
236 end if @issue
237 end if @issue
237 }
238 }
238 end
239 end
239 end
240 end
240
241
241 def diff
242 def diff
242 if params[:format] == 'diff'
243 if params[:format] == 'diff'
243 @diff = @repository.diff(@path, @rev, @rev_to)
244 @diff = @repository.diff(@path, @rev, @rev_to)
244 (show_error_not_found; return) unless @diff
245 (show_error_not_found; return) unless @diff
245 filename = "changeset_r#{@rev}"
246 filename = "changeset_r#{@rev}"
246 filename << "_r#{@rev_to}" if @rev_to
247 filename << "_r#{@rev_to}" if @rev_to
247 send_data @diff.join, :filename => "#{filename}.diff",
248 send_data @diff.join, :filename => "#{filename}.diff",
248 :type => 'text/x-patch',
249 :type => 'text/x-patch',
249 :disposition => 'attachment'
250 :disposition => 'attachment'
250 else
251 else
251 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
252 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
252 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
253 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
253
254
254 # Save diff type as user preference
255 # Save diff type as user preference
255 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
256 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
256 User.current.pref[:diff_type] = @diff_type
257 User.current.pref[:diff_type] = @diff_type
257 User.current.preference.save
258 User.current.preference.save
258 end
259 end
259 @cache_key = "repositories/diff/#{@repository.id}/" +
260 @cache_key = "repositories/diff/#{@repository.id}/" +
260 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
261 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
261 unless read_fragment(@cache_key)
262 unless read_fragment(@cache_key)
262 @diff = @repository.diff(@path, @rev, @rev_to)
263 @diff = @repository.diff(@path, @rev, @rev_to)
263 show_error_not_found unless @diff
264 show_error_not_found unless @diff
264 end
265 end
265
266
266 @changeset = @repository.find_changeset_by_name(@rev)
267 @changeset = @repository.find_changeset_by_name(@rev)
267 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
268 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
268 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
269 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
269 end
270 end
270 end
271 end
271
272
272 def stats
273 def stats
273 end
274 end
274
275
275 def graph
276 def graph
276 data = nil
277 data = nil
277 case params[:graph]
278 case params[:graph]
278 when "commits_per_month"
279 when "commits_per_month"
279 data = graph_commits_per_month(@repository)
280 data = graph_commits_per_month(@repository)
280 when "commits_per_author"
281 when "commits_per_author"
281 data = graph_commits_per_author(@repository)
282 data = graph_commits_per_author(@repository)
282 end
283 end
283 if data
284 if data
284 headers["Content-Type"] = "image/svg+xml"
285 headers["Content-Type"] = "image/svg+xml"
285 send_data(data, :type => "image/svg+xml", :disposition => "inline")
286 send_data(data, :type => "image/svg+xml", :disposition => "inline")
286 else
287 else
287 render_404
288 render_404
288 end
289 end
289 end
290 end
290
291
291 private
292 private
292
293
293 def find_repository
294 def find_repository
294 @repository = Repository.find(params[:id])
295 @repository = Repository.find(params[:id])
295 @project = @repository.project
296 @project = @repository.project
296 rescue ActiveRecord::RecordNotFound
297 rescue ActiveRecord::RecordNotFound
297 render_404
298 render_404
298 end
299 end
299
300
300 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
301 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
301
302
302 def find_project_repository
303 def find_project_repository
303 @project = Project.find(params[:id])
304 @project = Project.find(params[:id])
304 if params[:repository_id].present?
305 if params[:repository_id].present?
305 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
306 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
306 else
307 else
307 @repository = @project.repository
308 @repository = @project.repository
308 end
309 end
309 (render_404; return false) unless @repository
310 (render_404; return false) unless @repository
310 @path = params[:path].join('/') unless params[:path].nil?
311 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
311 @path ||= ''
312 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
312 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
313 @rev_to = params[:rev_to]
313 @rev_to = params[:rev_to]
314
314
315 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
315 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
316 if @repository.branches.blank?
316 if @repository.branches.blank?
317 raise InvalidRevisionParam
317 raise InvalidRevisionParam
318 end
318 end
319 end
319 end
320 rescue ActiveRecord::RecordNotFound
320 rescue ActiveRecord::RecordNotFound
321 render_404
321 render_404
322 rescue InvalidRevisionParam
322 rescue InvalidRevisionParam
323 show_error_not_found
323 show_error_not_found
324 end
324 end
325
325
326 def find_changeset
326 def find_changeset
327 if @rev.present?
327 if @rev.present?
328 @changeset = @repository.find_changeset_by_name(@rev)
328 @changeset = @repository.find_changeset_by_name(@rev)
329 end
329 end
330 show_error_not_found unless @changeset
330 show_error_not_found unless @changeset
331 end
331 end
332
332
333 def show_error_not_found
333 def show_error_not_found
334 render_error :message => l(:error_scm_not_found), :status => 404
334 render_error :message => l(:error_scm_not_found), :status => 404
335 end
335 end
336
336
337 # Handler for Redmine::Scm::Adapters::CommandFailed exception
337 # Handler for Redmine::Scm::Adapters::CommandFailed exception
338 def show_error_command_failed(exception)
338 def show_error_command_failed(exception)
339 render_error l(:error_scm_command_failed, exception.message)
339 render_error l(:error_scm_command_failed, exception.message)
340 end
340 end
341
341
342 def graph_commits_per_month(repository)
342 def graph_commits_per_month(repository)
343 @date_to = Date.today
343 @date_to = Date.today
344 @date_from = @date_to << 11
344 @date_from = @date_to << 11
345 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
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 :all, :group => :commit_date,
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 commits_by_month = [0] * 12
349 commits_by_month = [0] * 12
350 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
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(
352 changes_by_day = Change.count(
353 :all, :group => :commit_date,
353 :all, :group => :commit_date, :include => :changeset,
354 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
354 :conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
355 changes_by_month = [0] * 12
355 changes_by_month = [0] * 12
356 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
356 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
357
357
358 fields = []
358 fields = []
359 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
359 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
360
360
361 graph = SVG::Graph::Bar.new(
361 graph = SVG::Graph::Bar.new(
362 :height => 300,
362 :height => 300,
363 :width => 800,
363 :width => 800,
364 :fields => fields.reverse,
364 :fields => fields.reverse,
365 :stack => :side,
365 :stack => :side,
366 :scale_integers => true,
366 :scale_integers => true,
367 :step_x_labels => 2,
367 :step_x_labels => 2,
368 :show_data_values => false,
368 :show_data_values => false,
369 :graph_title => l(:label_commits_per_month),
369 :graph_title => l(:label_commits_per_month),
370 :show_graph_title => true
370 :show_graph_title => true
371 )
371 )
372
372
373 graph.add_data(
373 graph.add_data(
374 :data => commits_by_month[0..11].reverse,
374 :data => commits_by_month[0..11].reverse,
375 :title => l(:label_revision_plural)
375 :title => l(:label_revision_plural)
376 )
376 )
377
377
378 graph.add_data(
378 graph.add_data(
379 :data => changes_by_month[0..11].reverse,
379 :data => changes_by_month[0..11].reverse,
380 :title => l(:label_change_plural)
380 :title => l(:label_change_plural)
381 )
381 )
382
382
383 graph.burn
383 graph.burn
384 end
384 end
385
385
386 def graph_commits_per_author(repository)
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 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
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 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
391 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
392
392
393 fields = commits_by_author.collect {|r| r.first}
393 fields = commits_by_author.collect {|r| r.first}
394 commits_data = commits_by_author.collect {|r| r.last}
394 commits_data = commits_by_author.collect {|r| r.last}
395 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
395 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
396
396
397 fields = fields + [""]*(10 - fields.length) if fields.length<10
397 fields = fields + [""]*(10 - fields.length) if fields.length<10
398 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
398 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
399 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
399 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
400
400
401 # Remove email adress in usernames
401 # Remove email adress in usernames
402 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
402 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
403
403
404 graph = SVG::Graph::BarHorizontal.new(
404 graph = SVG::Graph::BarHorizontal.new(
405 :height => 400,
405 :height => 400,
406 :width => 800,
406 :width => 800,
407 :fields => fields,
407 :fields => fields,
408 :stack => :side,
408 :stack => :side,
409 :scale_integers => true,
409 :scale_integers => true,
410 :show_data_values => false,
410 :show_data_values => false,
411 :rotate_y_labels => false,
411 :rotate_y_labels => false,
412 :graph_title => l(:label_commits_per_author),
412 :graph_title => l(:label_commits_per_author),
413 :show_graph_title => true
413 :show_graph_title => true
414 )
414 )
415 graph.add_data(
415 graph.add_data(
416 :data => commits_data,
416 :data => commits_data,
417 :title => l(:label_revision_plural)
417 :title => l(:label_revision_plural)
418 )
418 )
419 graph.add_data(
419 graph.add_data(
420 :data => changes_data,
420 :data => changes_data,
421 :title => l(:label_change_plural)
421 :title => l(:label_change_plural)
422 )
422 )
423 graph.burn
423 graph.burn
424 end
424 end
425 end
425 end
426
426
@@ -1,132 +1,132
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class WatchersController < ApplicationController
18 class WatchersController < ApplicationController
19 before_filter :find_project
19 before_filter :find_project
20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
20 before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
21 before_filter :authorize, :only => [:new, :destroy]
21 before_filter :authorize, :only => [:new, :destroy]
22
22
23 def watch
23 def watch
24 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
24 if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
25 render_403
25 render_403
26 else
26 else
27 set_watcher(User.current, true)
27 set_watcher(User.current, true)
28 end
28 end
29 end
29 end
30
30
31 def unwatch
31 def unwatch
32 set_watcher(User.current, false)
32 set_watcher(User.current, false)
33 end
33 end
34
34
35 def new
35 def new
36 respond_to do |format|
36 respond_to do |format|
37 format.js do
37 format.js do
38 render :update do |page|
38 render :update do |page|
39 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
39 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
40 page << "showModal('ajax-modal', '400px');"
40 page << "showModal('ajax-modal', '400px');"
41 page << "$('ajax-modal').addClassName('new-watcher');"
41 page << "$('ajax-modal').addClassName('new-watcher');"
42 end
42 end
43 end
43 end
44 end
44 end
45 end
45 end
46
46
47 def create
47 def create
48 if params[:watcher].is_a?(Hash) && request.post?
48 if params[:watcher].is_a?(Hash) && request.post?
49 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
49 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
50 user_ids.each do |user_id|
50 user_ids.each do |user_id|
51 Watcher.create(:watchable => @watched, :user_id => user_id)
51 Watcher.create(:watchable => @watched, :user_id => user_id)
52 end
52 end
53 end
53 end
54 respond_to do |format|
54 respond_to do |format|
55 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
55 format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
56 format.js do
56 format.js do
57 render :update do |page|
57 render :update do |page|
58 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
58 page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
59 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
59 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
60 end
60 end
61 end
61 end
62 end
62 end
63 end
63 end
64
64
65 def append
65 def append
66 if params[:watcher].is_a?(Hash)
66 if params[:watcher].is_a?(Hash)
67 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
67 user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
68 users = User.active.find_all_by_id(user_ids)
68 users = User.active.find_all_by_id(user_ids)
69 respond_to do |format|
69 respond_to do |format|
70 format.js do
70 format.js do
71 render :update do |page|
71 render :update do |page|
72 users.each do |user|
72 users.each do |user|
73 page.select("#issue_watcher_user_ids_#{user.id}").each do |item|
73 page.select("#issue_watcher_user_ids_#{user.id}").each do |item|
74 page.remove item
74 page.remove item
75 end
75 end
76 end
76 end
77 page.insert_html :bottom, 'watchers_inputs', :text => watchers_checkboxes(nil, users, true)
77 page.insert_html :bottom, 'watchers_inputs', :text => watchers_checkboxes(nil, users, true)
78 end
78 end
79 end
79 end
80 end
80 end
81 end
81 end
82 end
82 end
83
83
84 def destroy
84 def destroy
85 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
85 @watched.set_watcher(User.find(params[:user_id]), false) if request.post?
86 respond_to do |format|
86 respond_to do |format|
87 format.html { redirect_to :back }
87 format.html { redirect_to :back }
88 format.js do
88 format.js do
89 render :update do |page|
89 render :update do |page|
90 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
90 page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
91 end
91 end
92 end
92 end
93 end
93 end
94 end
94 end
95
95
96 def autocomplete_for_user
96 def autocomplete_for_user
97 @users = User.active.like(params[:q]).find(:all, :limit => 100)
97 @users = User.active.like(params[:q]).find(:all, :limit => 100)
98 if @watched
98 if @watched
99 @users -= @watched.watcher_users
99 @users -= @watched.watcher_users
100 end
100 end
101 render :layout => false
101 render :layout => false
102 end
102 end
103
103
104 private
104 private
105 def find_project
105 def find_project
106 if params[:object_type] && params[:object_id]
106 if params[:object_type] && params[:object_id]
107 klass = Object.const_get(params[:object_type].camelcase)
107 klass = Object.const_get(params[:object_type].camelcase)
108 return false unless klass.respond_to?('watched_by')
108 return false unless klass.respond_to?('watched_by')
109 @watched = klass.find(params[:object_id])
109 @watched = klass.find(params[:object_id])
110 @project = @watched.project
110 @project = @watched.project
111 elsif params[:project_id]
111 elsif params[:project_id]
112 @project = Project.visible.find(params[:project_id])
112 @project = Project.visible.find_by_param(params[:project_id])
113 end
113 end
114 rescue
114 rescue
115 render_404
115 render_404
116 end
116 end
117
117
118 def set_watcher(user, watching)
118 def set_watcher(user, watching)
119 @watched.set_watcher(user, watching)
119 @watched.set_watcher(user, watching)
120 respond_to do |format|
120 respond_to do |format|
121 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
121 format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
122 format.js do
122 format.js do
123 render(:update) do |page|
123 render(:update) do |page|
124 c = watcher_css(@watched)
124 c = watcher_css(@watched)
125 page.select(".#{c}").each do |item|
125 page.select(".#{c}").each do |item|
126 page.replace_html item, watcher_link(@watched, user)
126 page.replace_html item, watcher_link(@watched, user)
127 end
127 end
128 end
128 end
129 end
129 end
130 end
130 end
131 end
131 end
132 end
132 end
@@ -1,316 +1,318
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'diff'
18 require 'diff'
19
19
20 # The WikiController follows the Rails REST controller pattern but with
20 # The WikiController follows the Rails REST controller pattern but with
21 # a few differences
21 # a few differences
22 #
22 #
23 # * index - shows a list of WikiPages grouped by page or date
23 # * index - shows a list of WikiPages grouped by page or date
24 # * new - not used
24 # * new - not used
25 # * create - not used
25 # * create - not used
26 # * show - will also show the form for creating a new wiki page
26 # * show - will also show the form for creating a new wiki page
27 # * edit - used to edit an existing or new page
27 # * edit - used to edit an existing or new page
28 # * update - used to save a wiki page update to the database, including new pages
28 # * update - used to save a wiki page update to the database, including new pages
29 # * destroy - normal
29 # * destroy - normal
30 #
30 #
31 # Other member and collection methods are also used
31 # Other member and collection methods are also used
32 #
32 #
33 # TODO: still being worked on
33 # TODO: still being worked on
34 class WikiController < ApplicationController
34 class WikiController < ApplicationController
35 default_search_scope :wiki_pages
35 default_search_scope :wiki_pages
36 before_filter :find_wiki, :authorize
36 before_filter :find_wiki, :authorize
37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
39
39
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :watchers
42 helper :watchers
43 include Redmine::Export::PDF
43 include Redmine::Export::PDF
44
44
45 # List of pages, sorted alphabetically and by parent (hierarchy)
45 # List of pages, sorted alphabetically and by parent (hierarchy)
46 def index
46 def index
47 load_pages_for_index
47 load_pages_for_index
48 @pages_by_parent_id = @pages.group_by(&:parent_id)
48 @pages_by_parent_id = @pages.group_by(&:parent_id)
49 end
49 end
50
50
51 # List of page, by last update
51 # List of page, by last update
52 def date_index
52 def date_index
53 load_pages_for_index
53 load_pages_for_index
54 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
54 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
55 end
55 end
56
56
57 # display a page (in editing mode if it doesn't exist)
57 # display a page (in editing mode if it doesn't exist)
58 def show
58 def show
59 if @page.new_record?
59 if @page.new_record?
60 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
60 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
61 edit
61 edit
62 render :action => 'edit'
62 render :action => 'edit'
63 else
63 else
64 render_404
64 render_404
65 end
65 end
66 return
66 return
67 end
67 end
68 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
68 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
69 # Redirects user to the current version if he's not allowed to view previous versions
69 # Redirects user to the current version if he's not allowed to view previous versions
70 redirect_to :version => nil
70 redirect_to :version => nil
71 return
71 return
72 end
72 end
73 @content = @page.content_for_version(params[:version])
73 @content = @page.content_for_version(params[:version])
74 if User.current.allowed_to?(:export_wiki_pages, @project)
74 if User.current.allowed_to?(:export_wiki_pages, @project)
75 if params[:format] == 'pdf'
75 if params[:format] == 'pdf'
76 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
76 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
77 return
77 return
78 elsif params[:format] == 'html'
78 elsif params[:format] == 'html'
79 export = render_to_string :action => 'export', :layout => false
79 export = render_to_string :action => 'export', :layout => false
80 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
80 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
81 return
81 return
82 elsif params[:format] == 'txt'
82 elsif params[:format] == 'txt'
83 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
83 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
84 return
84 return
85 end
85 end
86 end
86 end
87 @editable = editable?
87 @editable = editable?
88 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
88 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
89 @content.current_version? &&
89 @content.current_version? &&
90 Redmine::WikiFormatting.supports_section_edit?
90 Redmine::WikiFormatting.supports_section_edit?
91
91
92 render :action => 'show'
92 render :action => 'show'
93 end
93 end
94
94
95 # edit an existing page or a new one
95 # edit an existing page or a new one
96 def edit
96 def edit
97 return render_403 unless editable?
97 return render_403 unless editable?
98 if @page.new_record?
98 if @page.new_record?
99 @page.content = WikiContent.new(:page => @page)
99 @page.content = WikiContent.new(:page => @page)
100 if params[:parent].present?
100 if params[:parent].present?
101 @page.parent = @page.wiki.find_page(params[:parent].to_s)
101 @page.parent = @page.wiki.find_page(params[:parent].to_s)
102 end
102 end
103 end
103 end
104
104
105 @content = @page.content_for_version(params[:version])
105 @content = @page.content_for_version(params[:version])
106 @content.text = initial_page_content(@page) if @content.text.blank?
106 @content.text = initial_page_content(@page) if @content.text.blank?
107 # don't keep previous comment
107 # don't keep previous comment
108 @content.comments = nil
108 @content.comments = nil
109
109
110 # To prevent StaleObjectError exception when reverting to a previous version
110 # To prevent StaleObjectError exception when reverting to a previous version
111 @content.version = @page.content.version
111 @content.version = @page.content.version
112
112
113 @text = @content.text
113 @text = @content.text
114 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
114 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
115 @section = params[:section].to_i
115 @section = params[:section].to_i
116 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
116 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
117 render_404 if @text.blank?
117 render_404 if @text.blank?
118 end
118 end
119 end
119 end
120
120
121 # Creates a new page or updates an existing one
121 # Creates a new page or updates an existing one
122 def update
122 def update
123 return render_403 unless editable?
123 return render_403 unless editable?
124 @page.content = WikiContent.new(:page => @page) if @page.new_record?
124 @page.content = WikiContent.new(:page => @page) if @page.new_record?
125 @page.safe_attributes = params[:wiki_page]
125 @page.safe_attributes = params[:wiki_page]
126
126
127 @content = @page.content_for_version(params[:version])
127 @content = @page.content_for_version(params[:version])
128 @content.text = initial_page_content(@page) if @content.text.blank?
128 @content.text = initial_page_content(@page) if @content.text.blank?
129 # don't keep previous comment
129 # don't keep previous comment
130 @content.comments = nil
130 @content.comments = nil
131
131
132 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
132 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
133 attachments = Attachment.attach_files(@page, params[:attachments])
133 attachments = Attachment.attach_files(@page, params[:attachments])
134 render_attachment_warning_if_needed(@page)
134 render_attachment_warning_if_needed(@page)
135 # don't save content if text wasn't changed
135 # don't save content if text wasn't changed
136 @page.save
136 @page.save
137 redirect_to :action => 'show', :project_id => @project, :id => @page.title
137 redirect_to :action => 'show', :project_id => @project, :id => @page.title
138 return
138 return
139 end
139 end
140
140
141 @content.comments = params[:content][:comments]
141 @content.comments = params[:content][:comments]
142 @text = params[:content][:text]
142 @text = params[:content][:text]
143 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
143 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
144 @section = params[:section].to_i
144 @section = params[:section].to_i
145 @section_hash = params[:section_hash]
145 @section_hash = params[:section_hash]
146 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
146 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
147 else
147 else
148 @content.version = params[:content][:version]
148 @content.version = params[:content][:version]
149 @content.text = @text
149 @content.text = @text
150 end
150 end
151 @content.author = User.current
151 @content.author = User.current
152 @page.content = @content
152 @page.content = @content
153 if @page.save
153 if @page.save
154 attachments = Attachment.attach_files(@page, params[:attachments])
154 attachments = Attachment.attach_files(@page, params[:attachments])
155 render_attachment_warning_if_needed(@page)
155 render_attachment_warning_if_needed(@page)
156 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
156 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
157 redirect_to :action => 'show', :project_id => @project, :id => @page.title
157 redirect_to :action => 'show', :project_id => @project, :id => @page.title
158 else
158 else
159 render :action => 'edit'
159 render :action => 'edit'
160 end
160 end
161
161
162 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
162 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
163 # Optimistic locking exception
163 # Optimistic locking exception
164 flash.now[:error] = l(:notice_locking_conflict)
164 flash.now[:error] = l(:notice_locking_conflict)
165 render :action => 'edit'
165 render :action => 'edit'
166 rescue ActiveRecord::RecordNotSaved
167 render :action => 'edit'
166 end
168 end
167
169
168 # rename a page
170 # rename a page
169 def rename
171 def rename
170 return render_403 unless editable?
172 return render_403 unless editable?
171 @page.redirect_existing_links = true
173 @page.redirect_existing_links = true
172 # used to display the *original* title if some AR validation errors occur
174 # used to display the *original* title if some AR validation errors occur
173 @original_title = @page.pretty_title
175 @original_title = @page.pretty_title
174 if request.post? && @page.update_attributes(params[:wiki_page])
176 if request.post? && @page.update_attributes(params[:wiki_page])
175 flash[:notice] = l(:notice_successful_update)
177 flash[:notice] = l(:notice_successful_update)
176 redirect_to :action => 'show', :project_id => @project, :id => @page.title
178 redirect_to :action => 'show', :project_id => @project, :id => @page.title
177 end
179 end
178 end
180 end
179
181
180 def protect
182 def protect
181 @page.update_attribute :protected, params[:protected]
183 @page.update_attribute :protected, params[:protected]
182 redirect_to :action => 'show', :project_id => @project, :id => @page.title
184 redirect_to :action => 'show', :project_id => @project, :id => @page.title
183 end
185 end
184
186
185 # show page history
187 # show page history
186 def history
188 def history
187 @version_count = @page.content.versions.count
189 @version_count = @page.content.versions.count
188 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
190 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
189 # don't load text
191 # don't load text
190 @versions = @page.content.versions.find :all,
192 @versions = @page.content.versions.find :all,
191 :select => "id, author_id, comments, updated_on, version",
193 :select => "id, author_id, comments, updated_on, version",
192 :order => 'version DESC',
194 :order => 'version DESC',
193 :limit => @version_pages.items_per_page + 1,
195 :limit => @version_pages.items_per_page + 1,
194 :offset => @version_pages.current.offset
196 :offset => @version_pages.current.offset
195
197
196 render :layout => false if request.xhr?
198 render :layout => false if request.xhr?
197 end
199 end
198
200
199 def diff
201 def diff
200 @diff = @page.diff(params[:version], params[:version_from])
202 @diff = @page.diff(params[:version], params[:version_from])
201 render_404 unless @diff
203 render_404 unless @diff
202 end
204 end
203
205
204 def annotate
206 def annotate
205 @annotate = @page.annotate(params[:version])
207 @annotate = @page.annotate(params[:version])
206 render_404 unless @annotate
208 render_404 unless @annotate
207 end
209 end
208
210
209 # Removes a wiki page and its history
211 # Removes a wiki page and its history
210 # Children can be either set as root pages, removed or reassigned to another parent page
212 # Children can be either set as root pages, removed or reassigned to another parent page
211 def destroy
213 def destroy
212 return render_403 unless editable?
214 return render_403 unless editable?
213
215
214 @descendants_count = @page.descendants.size
216 @descendants_count = @page.descendants.size
215 if @descendants_count > 0
217 if @descendants_count > 0
216 case params[:todo]
218 case params[:todo]
217 when 'nullify'
219 when 'nullify'
218 # Nothing to do
220 # Nothing to do
219 when 'destroy'
221 when 'destroy'
220 # Removes all its descendants
222 # Removes all its descendants
221 @page.descendants.each(&:destroy)
223 @page.descendants.each(&:destroy)
222 when 'reassign'
224 when 'reassign'
223 # Reassign children to another parent page
225 # Reassign children to another parent page
224 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
226 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
225 return unless reassign_to
227 return unless reassign_to
226 @page.children.each do |child|
228 @page.children.each do |child|
227 child.update_attribute(:parent, reassign_to)
229 child.update_attribute(:parent, reassign_to)
228 end
230 end
229 else
231 else
230 @reassignable_to = @wiki.pages - @page.self_and_descendants
232 @reassignable_to = @wiki.pages - @page.self_and_descendants
231 return
233 return
232 end
234 end
233 end
235 end
234 @page.destroy
236 @page.destroy
235 redirect_to :action => 'index', :project_id => @project
237 redirect_to :action => 'index', :project_id => @project
236 end
238 end
237
239
238 # Export wiki to a single pdf or html file
240 # Export wiki to a single pdf or html file
239 def export
241 def export
240 @pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
242 @pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
241 respond_to do |format|
243 respond_to do |format|
242 format.html {
244 format.html {
243 export = render_to_string :action => 'export_multiple', :layout => false
245 export = render_to_string :action => 'export_multiple', :layout => false
244 send_data(export, :type => 'text/html', :filename => "wiki.html")
246 send_data(export, :type => 'text/html', :filename => "wiki.html")
245 }
247 }
246 format.pdf {
248 format.pdf {
247 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
249 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
248 }
250 }
249 end
251 end
250 end
252 end
251
253
252 def preview
254 def preview
253 page = @wiki.find_page(params[:id])
255 page = @wiki.find_page(params[:id])
254 # page is nil when previewing a new page
256 # page is nil when previewing a new page
255 return render_403 unless page.nil? || editable?(page)
257 return render_403 unless page.nil? || editable?(page)
256 if page
258 if page
257 @attachements = page.attachments
259 @attachements = page.attachments
258 @previewed = page.content
260 @previewed = page.content
259 end
261 end
260 @text = params[:content][:text]
262 @text = params[:content][:text]
261 render :partial => 'common/preview'
263 render :partial => 'common/preview'
262 end
264 end
263
265
264 def add_attachment
266 def add_attachment
265 return render_403 unless editable?
267 return render_403 unless editable?
266 attachments = Attachment.attach_files(@page, params[:attachments])
268 attachments = Attachment.attach_files(@page, params[:attachments])
267 render_attachment_warning_if_needed(@page)
269 render_attachment_warning_if_needed(@page)
268 redirect_to :action => 'show', :id => @page.title, :project_id => @project
270 redirect_to :action => 'show', :id => @page.title, :project_id => @project
269 end
271 end
270
272
271 private
273 private
272
274
273 def find_wiki
275 def find_wiki
274 @project = Project.find(params[:project_id])
276 @project = Project.find(params[:project_id])
275 @wiki = @project.wiki
277 @wiki = @project.wiki
276 render_404 unless @wiki
278 render_404 unless @wiki
277 rescue ActiveRecord::RecordNotFound
279 rescue ActiveRecord::RecordNotFound
278 render_404
280 render_404
279 end
281 end
280
282
281 # Finds the requested page or a new page if it doesn't exist
283 # Finds the requested page or a new page if it doesn't exist
282 def find_existing_or_new_page
284 def find_existing_or_new_page
283 @page = @wiki.find_or_new_page(params[:id])
285 @page = @wiki.find_or_new_page(params[:id])
284 if @wiki.page_found_with_redirect?
286 if @wiki.page_found_with_redirect?
285 redirect_to params.update(:id => @page.title)
287 redirect_to params.update(:id => @page.title)
286 end
288 end
287 end
289 end
288
290
289 # Finds the requested page and returns a 404 error if it doesn't exist
291 # Finds the requested page and returns a 404 error if it doesn't exist
290 def find_existing_page
292 def find_existing_page
291 @page = @wiki.find_page(params[:id])
293 @page = @wiki.find_page(params[:id])
292 if @page.nil?
294 if @page.nil?
293 render_404
295 render_404
294 return
296 return
295 end
297 end
296 if @wiki.page_found_with_redirect?
298 if @wiki.page_found_with_redirect?
297 redirect_to params.update(:id => @page.title)
299 redirect_to params.update(:id => @page.title)
298 end
300 end
299 end
301 end
300
302
301 # Returns true if the current user is allowed to edit the page, otherwise false
303 # Returns true if the current user is allowed to edit the page, otherwise false
302 def editable?(page = @page)
304 def editable?(page = @page)
303 page.editable_by?(User.current)
305 page.editable_by?(User.current)
304 end
306 end
305
307
306 # Returns the default content of a new wiki page
308 # Returns the default content of a new wiki page
307 def initial_page_content(page)
309 def initial_page_content(page)
308 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
310 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
309 extend helper unless self.instance_of?(helper)
311 extend helper unless self.instance_of?(helper)
310 helper.instance_method(:initial_page_content).bind(self).call(page)
312 helper.instance_method(:initial_page_content).bind(self).call(page)
311 end
313 end
312
314
313 def load_pages_for_index
315 def load_pages_for_index
314 @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
316 @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
315 end
317 end
316 end
318 end
@@ -1,1130 +1,1133
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Display a link to remote if user is authorized
46 # Display a link to remote if user is authorized
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 url = options[:url] || {}
48 url = options[:url] || {}
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 end
50 end
51
51
52 # Displays a link to user's account page if active
52 # Displays a link to user's account page if active
53 def link_to_user(user, options={})
53 def link_to_user(user, options={})
54 if user.is_a?(User)
54 if user.is_a?(User)
55 name = h(user.name(options[:format]))
55 name = h(user.name(options[:format]))
56 if user.active?
56 if user.active?
57 link_to name, :controller => 'users', :action => 'show', :id => user
57 link_to name, :controller => 'users', :action => 'show', :id => user
58 else
58 else
59 name
59 name
60 end
60 end
61 else
61 else
62 h(user.to_s)
62 h(user.to_s)
63 end
63 end
64 end
64 end
65
65
66 # Displays a link to +issue+ with its subject.
66 # Displays a link to +issue+ with its subject.
67 # Examples:
67 # Examples:
68 #
68 #
69 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue) # => Defect #6: This is the subject
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :subject => false) # => Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 if options[:subject] == false
77 if options[:subject] == false
78 title = truncate(issue.subject, :length => 60)
78 title = truncate(issue.subject, :length => 60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if options[:truncate]
81 if options[:truncate]
82 subject = truncate(subject, :length => options[:truncate])
82 subject = truncate(subject, :length => options[:truncate])
83 end
83 end
84 end
84 end
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 :class => issue.css_classes,
86 :class => issue.css_classes,
87 :title => title
87 :title => title
88 s << h(": #{subject}") if subject
88 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
89 s = h("#{issue.project} - ") + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 action = options.delete(:download) ? 'download' : 'show'
99 action = options.delete(:download) ? 'download' : 'show'
100 opt_only_path = {}
100 opt_only_path = {}
101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
102 options.delete(:only_path)
102 options.delete(:only_path)
103 link_to(h(text),
103 link_to(h(text),
104 {:controller => 'attachments', :action => action,
104 {:controller => 'attachments', :action => action,
105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
106 options)
106 options)
107 end
107 end
108
108
109 # Generates a link to a SCM revision
109 # Generates a link to a SCM revision
110 # Options:
110 # Options:
111 # * :text - Link text (default to the formatted revision)
111 # * :text - Link text (default to the formatted revision)
112 def link_to_revision(revision, repository, options={})
112 def link_to_revision(revision, repository, options={})
113 if repository.is_a?(Project)
113 if repository.is_a?(Project)
114 repository = repository.repository
114 repository = repository.repository
115 end
115 end
116 text = options.delete(:text) || format_revision(revision)
116 text = options.delete(:text) || format_revision(revision)
117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
118 link_to(
118 link_to(
119 h(text),
119 h(text),
120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
121 :title => l(:label_revision_id, format_revision(revision))
121 :title => l(:label_revision_id, format_revision(revision))
122 )
122 )
123 end
123 end
124
124
125 # Generates a link to a message
125 # Generates a link to a message
126 def link_to_message(message, options={}, html_options = nil)
126 def link_to_message(message, options={}, html_options = nil)
127 link_to(
127 link_to(
128 h(truncate(message.subject, :length => 60)),
128 h(truncate(message.subject, :length => 60)),
129 { :controller => 'messages', :action => 'show',
129 { :controller => 'messages', :action => 'show',
130 :board_id => message.board_id,
130 :board_id => message.board_id,
131 :id => message.root,
131 :id => message.root,
132 :r => (message.parent_id && message.id),
132 :r => (message.parent_id && message.id),
133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
134 }.merge(options),
134 }.merge(options),
135 html_options
135 html_options
136 )
136 )
137 end
137 end
138
138
139 # Generates a link to a project if active
139 # Generates a link to a project if active
140 # Examples:
140 # Examples:
141 #
141 #
142 # link_to_project(project) # => link to the specified project overview
142 # link_to_project(project) # => link to the specified project overview
143 # link_to_project(project, :action=>'settings') # => link to project settings
143 # link_to_project(project, :action=>'settings') # => link to project settings
144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
146 #
146 #
147 def link_to_project(project, options={}, html_options = nil)
147 def link_to_project(project, options={}, html_options = nil)
148 if project.active?
148 if project.active?
149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
150 link_to(h(project), url, html_options)
150 link_to(h(project), url, html_options)
151 else
151 else
152 h(project)
152 h(project)
153 end
153 end
154 end
154 end
155
155
156 def toggle_link(name, id, options={})
156 def toggle_link(name, id, options={})
157 onclick = "Element.toggle('#{id}'); "
157 onclick = "Element.toggle('#{id}'); "
158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
159 onclick << "return false;"
159 onclick << "return false;"
160 link_to(name, "#", :onclick => onclick)
160 link_to(name, "#", :onclick => onclick)
161 end
161 end
162
162
163 def image_to_function(name, function, html_options = {})
163 def image_to_function(name, function, html_options = {})
164 html_options.symbolize_keys!
164 html_options.symbolize_keys!
165 tag(:input, html_options.merge({
165 tag(:input, html_options.merge({
166 :type => "image", :src => image_path(name),
166 :type => "image", :src => image_path(name),
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 }))
168 }))
169 end
169 end
170
170
171 def prompt_to_remote(name, text, param, url, html_options = {})
171 def prompt_to_remote(name, text, param, url, html_options = {})
172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
173 link_to name, {}, html_options
173 link_to name, {}, html_options
174 end
174 end
175
175
176 def format_activity_title(text)
176 def format_activity_title(text)
177 h(truncate_single_line(text, :length => 100))
177 h(truncate_single_line(text, :length => 100))
178 end
178 end
179
179
180 def format_activity_day(date)
180 def format_activity_day(date)
181 date == Date.today ? l(:label_today).titleize : format_date(date)
181 date == Date.today ? l(:label_today).titleize : format_date(date)
182 end
182 end
183
183
184 def format_activity_description(text)
184 def format_activity_description(text)
185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
186 ).gsub(/[\r\n]+/, "<br />").html_safe
186 ).gsub(/[\r\n]+/, "<br />").html_safe
187 end
187 end
188
188
189 def format_version_name(version)
189 def format_version_name(version)
190 if version.project == @project
190 if version.project == @project
191 h(version)
191 h(version)
192 else
192 else
193 h("#{version.project} - #{version}")
193 h("#{version.project} - #{version}")
194 end
194 end
195 end
195 end
196
196
197 def due_date_distance_in_words(date)
197 def due_date_distance_in_words(date)
198 if date
198 if date
199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
200 end
200 end
201 end
201 end
202
202
203 def render_page_hierarchy(pages, node=nil, options={})
203 def render_page_hierarchy(pages, node=nil, options={})
204 content = ''
204 content = ''
205 if pages[node]
205 if pages[node]
206 content << "<ul class=\"pages-hierarchy\">\n"
206 content << "<ul class=\"pages-hierarchy\">\n"
207 pages[node].each do |page|
207 pages[node].each do |page|
208 content << "<li>"
208 content << "<li>"
209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
212 content << "</li>\n"
212 content << "</li>\n"
213 end
213 end
214 content << "</ul>\n"
214 content << "</ul>\n"
215 end
215 end
216 content.html_safe
216 content.html_safe
217 end
217 end
218
218
219 # Renders flash messages
219 # Renders flash messages
220 def render_flash_messages
220 def render_flash_messages
221 s = ''
221 s = ''
222 flash.each do |k,v|
222 flash.each do |k,v|
223 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
223 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
224 end
224 end
225 s.html_safe
225 s.html_safe
226 end
226 end
227
227
228 # Renders tabs and their content
228 # Renders tabs and their content
229 def render_tabs(tabs)
229 def render_tabs(tabs)
230 if tabs.any?
230 if tabs.any?
231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
232 else
232 else
233 content_tag 'p', l(:label_no_data), :class => "nodata"
233 content_tag 'p', l(:label_no_data), :class => "nodata"
234 end
234 end
235 end
235 end
236
236
237 # Renders the project quick-jump box
237 # Renders the project quick-jump box
238 def render_project_jump_box
238 def render_project_jump_box
239 return unless User.current.logged?
239 return unless User.current.logged?
240 projects = User.current.memberships.collect(&:project).compact.uniq
240 projects = User.current.memberships.collect(&:project).compact.uniq
241 if projects.any?
241 if projects.any?
242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
244 '<option value="" disabled="disabled">---</option>'
244 '<option value="" disabled="disabled">---</option>'
245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
247 end
247 end
248 s << '</select>'
248 s << '</select>'
249 s.html_safe
249 s.html_safe
250 end
250 end
251 end
251 end
252
252
253 def project_tree_options_for_select(projects, options = {})
253 def project_tree_options_for_select(projects, options = {})
254 s = ''
254 s = ''
255 project_tree(projects) do |project, level|
255 project_tree(projects) do |project, level|
256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
257 tag_options = {:value => project.id}
257 tag_options = {:value => project.id}
258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
259 tag_options[:selected] = 'selected'
259 tag_options[:selected] = 'selected'
260 else
260 else
261 tag_options[:selected] = nil
261 tag_options[:selected] = nil
262 end
262 end
263 tag_options.merge!(yield(project)) if block_given?
263 tag_options.merge!(yield(project)) if block_given?
264 s << content_tag('option', name_prefix + h(project), tag_options)
264 s << content_tag('option', name_prefix + h(project), tag_options)
265 end
265 end
266 s.html_safe
266 s.html_safe
267 end
267 end
268
268
269 # Yields the given block for each project with its level in the tree
269 # Yields the given block for each project with its level in the tree
270 #
270 #
271 # Wrapper for Project#project_tree
271 # Wrapper for Project#project_tree
272 def project_tree(projects, &block)
272 def project_tree(projects, &block)
273 Project.project_tree(projects, &block)
273 Project.project_tree(projects, &block)
274 end
274 end
275
275
276 def project_nested_ul(projects, &block)
276 def project_nested_ul(projects, &block)
277 s = ''
277 s = ''
278 if projects.any?
278 if projects.any?
279 ancestors = []
279 ancestors = []
280 projects.sort_by(&:lft).each do |project|
280 projects.sort_by(&:lft).each do |project|
281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
282 s << "<ul>\n"
282 s << "<ul>\n"
283 else
283 else
284 ancestors.pop
284 ancestors.pop
285 s << "</li>"
285 s << "</li>"
286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
287 ancestors.pop
287 ancestors.pop
288 s << "</ul></li>\n"
288 s << "</ul></li>\n"
289 end
289 end
290 end
290 end
291 s << "<li>"
291 s << "<li>"
292 s << yield(project).to_s
292 s << yield(project).to_s
293 ancestors << project
293 ancestors << project
294 end
294 end
295 s << ("</li></ul>\n" * ancestors.size)
295 s << ("</li></ul>\n" * ancestors.size)
296 end
296 end
297 s.html_safe
297 s.html_safe
298 end
298 end
299
299
300 def principals_check_box_tags(name, principals)
300 def principals_check_box_tags(name, principals)
301 s = ''
301 s = ''
302 principals.sort.each do |principal|
302 principals.sort.each do |principal|
303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
304 end
304 end
305 s.html_safe
305 s.html_safe
306 end
306 end
307
307
308 # Returns a string for users/groups option tags
308 # Returns a string for users/groups option tags
309 def principals_options_for_select(collection, selected=nil)
309 def principals_options_for_select(collection, selected=nil)
310 s = ''
310 s = ''
311 if collection.include?(User.current)
311 if collection.include?(User.current)
312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
313 end
313 end
314 groups = ''
314 groups = ''
315 collection.sort.each do |element|
315 collection.sort.each do |element|
316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
318 end
318 end
319 unless groups.empty?
319 unless groups.empty?
320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
321 end
321 end
322 s.html_safe
322 s.html_safe
323 end
323 end
324
324
325 # Truncates and returns the string as a single line
325 # Truncates and returns the string as a single line
326 def truncate_single_line(string, *args)
326 def truncate_single_line(string, *args)
327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
328 end
328 end
329
329
330 # Truncates at line break after 250 characters or options[:length]
330 # Truncates at line break after 250 characters or options[:length]
331 def truncate_lines(string, options={})
331 def truncate_lines(string, options={})
332 length = options[:length] || 250
332 length = options[:length] || 250
333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
334 "#{$1}..."
334 "#{$1}..."
335 else
335 else
336 string
336 string
337 end
337 end
338 end
338 end
339
339
340 def anchor(text)
340 def anchor(text)
341 text.to_s.gsub(' ', '_')
341 text.to_s.gsub(' ', '_')
342 end
342 end
343
343
344 def html_hours(text)
344 def html_hours(text)
345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
346 end
346 end
347
347
348 def authoring(created, author, options={})
348 def authoring(created, author, options={})
349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
350 end
350 end
351
351
352 def time_tag(time)
352 def time_tag(time)
353 text = distance_of_time_in_words(Time.now, time)
353 text = distance_of_time_in_words(Time.now, time)
354 if @project
354 if @project
355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
356 else
356 else
357 content_tag('acronym', text, :title => format_time(time))
357 content_tag('acronym', text, :title => format_time(time))
358 end
358 end
359 end
359 end
360
360
361 def syntax_highlight_lines(name, content)
361 def syntax_highlight_lines(name, content)
362 lines = []
362 lines = []
363 syntax_highlight(name, content).each_line { |line| lines << line }
363 syntax_highlight(name, content).each_line { |line| lines << line }
364 lines
364 lines
365 end
365 end
366
366
367 def syntax_highlight(name, content)
367 def syntax_highlight(name, content)
368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
369 end
369 end
370
370
371 def to_path_param(path)
371 def to_path_param(path)
372 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
372 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
373 end
373 end
374
374
375 def pagination_links_full(paginator, count=nil, options={})
375 def pagination_links_full(paginator, count=nil, options={})
376 page_param = options.delete(:page_param) || :page
376 page_param = options.delete(:page_param) || :page
377 per_page_links = options.delete(:per_page_links)
377 per_page_links = options.delete(:per_page_links)
378 url_param = params.dup
378 url_param = params.dup
379
379
380 html = ''
380 html = ''
381 if paginator.current.previous
381 if paginator.current.previous
382 # \xc2\xab(utf-8) = &#171;
382 # \xc2\xab(utf-8) = &#171;
383 html << link_to_content_update(
383 html << link_to_content_update(
384 "\xc2\xab " + l(:label_previous),
384 "\xc2\xab " + l(:label_previous),
385 url_param.merge(page_param => paginator.current.previous)) + ' '
385 url_param.merge(page_param => paginator.current.previous)) + ' '
386 end
386 end
387
387
388 html << (pagination_links_each(paginator, options) do |n|
388 html << (pagination_links_each(paginator, options) do |n|
389 link_to_content_update(n.to_s, url_param.merge(page_param => n))
389 link_to_content_update(n.to_s, url_param.merge(page_param => n))
390 end || '')
390 end || '')
391
391
392 if paginator.current.next
392 if paginator.current.next
393 # \xc2\xbb(utf-8) = &#187;
393 # \xc2\xbb(utf-8) = &#187;
394 html << ' ' + link_to_content_update(
394 html << ' ' + link_to_content_update(
395 (l(:label_next) + " \xc2\xbb"),
395 (l(:label_next) + " \xc2\xbb"),
396 url_param.merge(page_param => paginator.current.next))
396 url_param.merge(page_param => paginator.current.next))
397 end
397 end
398
398
399 unless count.nil?
399 unless count.nil?
400 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
400 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
401 if per_page_links != false && links = per_page_links(paginator.items_per_page)
401 if per_page_links != false && links = per_page_links(paginator.items_per_page)
402 html << " | #{links}"
402 html << " | #{links}"
403 end
403 end
404 end
404 end
405
405
406 html.html_safe
406 html.html_safe
407 end
407 end
408
408
409 def per_page_links(selected=nil)
409 def per_page_links(selected=nil)
410 links = Setting.per_page_options_array.collect do |n|
410 links = Setting.per_page_options_array.collect do |n|
411 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
411 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
412 end
412 end
413 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
413 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
414 end
414 end
415
415
416 def reorder_links(name, url, method = :post)
416 def reorder_links(name, url, method = :post)
417 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
417 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
418 url.merge({"#{name}[move_to]" => 'highest'}),
418 url.merge({"#{name}[move_to]" => 'highest'}),
419 :method => method, :title => l(:label_sort_highest)) +
419 :method => method, :title => l(:label_sort_highest)) +
420 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
420 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
421 url.merge({"#{name}[move_to]" => 'higher'}),
421 url.merge({"#{name}[move_to]" => 'higher'}),
422 :method => method, :title => l(:label_sort_higher)) +
422 :method => method, :title => l(:label_sort_higher)) +
423 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
423 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
424 url.merge({"#{name}[move_to]" => 'lower'}),
424 url.merge({"#{name}[move_to]" => 'lower'}),
425 :method => method, :title => l(:label_sort_lower)) +
425 :method => method, :title => l(:label_sort_lower)) +
426 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
426 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
427 url.merge({"#{name}[move_to]" => 'lowest'}),
427 url.merge({"#{name}[move_to]" => 'lowest'}),
428 :method => method, :title => l(:label_sort_lowest))
428 :method => method, :title => l(:label_sort_lowest))
429 end
429 end
430
430
431 def breadcrumb(*args)
431 def breadcrumb(*args)
432 elements = args.flatten
432 elements = args.flatten
433 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
433 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
434 end
434 end
435
435
436 def other_formats_links(&block)
436 def other_formats_links(&block)
437 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
437 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
438 yield Redmine::Views::OtherFormatsBuilder.new(self)
438 yield Redmine::Views::OtherFormatsBuilder.new(self)
439 concat('</p>'.html_safe)
439 concat('</p>'.html_safe)
440 end
440 end
441
441
442 def page_header_title
442 def page_header_title
443 if @project.nil? || @project.new_record?
443 if @project.nil? || @project.new_record?
444 h(Setting.app_title)
444 h(Setting.app_title)
445 else
445 else
446 b = []
446 b = []
447 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
447 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
448 if ancestors.any?
448 if ancestors.any?
449 root = ancestors.shift
449 root = ancestors.shift
450 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
450 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
451 if ancestors.size > 2
451 if ancestors.size > 2
452 b << "\xe2\x80\xa6"
452 b << "\xe2\x80\xa6"
453 ancestors = ancestors[-2, 2]
453 ancestors = ancestors[-2, 2]
454 end
454 end
455 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
455 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
456 end
456 end
457 b << h(@project)
457 b << h(@project)
458 b.join(" \xc2\xbb ").html_safe
458 b.join(" \xc2\xbb ").html_safe
459 end
459 end
460 end
460 end
461
461
462 def html_title(*args)
462 def html_title(*args)
463 if args.empty?
463 if args.empty?
464 title = @html_title || []
464 title = @html_title || []
465 title << @project.name if @project
465 title << @project.name if @project
466 title << Setting.app_title unless Setting.app_title == title.last
466 title << Setting.app_title unless Setting.app_title == title.last
467 title.select {|t| !t.blank? }.join(' - ')
467 title.select {|t| !t.blank? }.join(' - ')
468 else
468 else
469 @html_title ||= []
469 @html_title ||= []
470 @html_title += args
470 @html_title += args
471 end
471 end
472 end
472 end
473
473
474 # Returns the theme, controller name, and action as css classes for the
474 # Returns the theme, controller name, and action as css classes for the
475 # HTML body.
475 # HTML body.
476 def body_css_classes
476 def body_css_classes
477 css = []
477 css = []
478 if theme = Redmine::Themes.theme(Setting.ui_theme)
478 if theme = Redmine::Themes.theme(Setting.ui_theme)
479 css << 'theme-' + theme.name
479 css << 'theme-' + theme.name
480 end
480 end
481
481
482 css << 'controller-' + controller_name
482 css << 'controller-' + controller_name
483 css << 'action-' + action_name
483 css << 'action-' + action_name
484 css.join(' ')
484 css.join(' ')
485 end
485 end
486
486
487 def accesskey(s)
487 def accesskey(s)
488 Redmine::AccessKeys.key_for s
488 Redmine::AccessKeys.key_for s
489 end
489 end
490
490
491 # Formats text according to system settings.
491 # Formats text according to system settings.
492 # 2 ways to call this method:
492 # 2 ways to call this method:
493 # * with a String: textilizable(text, options)
493 # * with a String: textilizable(text, options)
494 # * with an object and one of its attribute: textilizable(issue, :description, options)
494 # * with an object and one of its attribute: textilizable(issue, :description, options)
495 def textilizable(*args)
495 def textilizable(*args)
496 options = args.last.is_a?(Hash) ? args.pop : {}
496 options = args.last.is_a?(Hash) ? args.pop : {}
497 case args.size
497 case args.size
498 when 1
498 when 1
499 obj = options[:object]
499 obj = options[:object]
500 text = args.shift
500 text = args.shift
501 when 2
501 when 2
502 obj = args.shift
502 obj = args.shift
503 attr = args.shift
503 attr = args.shift
504 text = obj.send(attr).to_s
504 text = obj.send(attr).to_s
505 else
505 else
506 raise ArgumentError, 'invalid arguments to textilizable'
506 raise ArgumentError, 'invalid arguments to textilizable'
507 end
507 end
508 return '' if text.blank?
508 return '' if text.blank?
509 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
509 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
510 only_path = options.delete(:only_path) == false ? false : true
510 only_path = options.delete(:only_path) == false ? false : true
511
511
512 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
512 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
513
513
514 @parsed_headings = []
514 @parsed_headings = []
515 @heading_anchors = {}
515 @heading_anchors = {}
516 @current_section = 0 if options[:edit_section_links]
516 @current_section = 0 if options[:edit_section_links]
517
517
518 parse_sections(text, project, obj, attr, only_path, options)
518 parse_sections(text, project, obj, attr, only_path, options)
519 text = parse_non_pre_blocks(text) do |text|
519 text = parse_non_pre_blocks(text) do |text|
520 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
520 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
521 send method_name, text, project, obj, attr, only_path, options
521 send method_name, text, project, obj, attr, only_path, options
522 end
522 end
523 end
523 end
524 parse_headings(text, project, obj, attr, only_path, options)
524 parse_headings(text, project, obj, attr, only_path, options)
525
525
526 if @parsed_headings.any?
526 if @parsed_headings.any?
527 replace_toc(text, @parsed_headings)
527 replace_toc(text, @parsed_headings)
528 end
528 end
529
529
530 text.html_safe
530 text.html_safe
531 end
531 end
532
532
533 def parse_non_pre_blocks(text)
533 def parse_non_pre_blocks(text)
534 s = StringScanner.new(text)
534 s = StringScanner.new(text)
535 tags = []
535 tags = []
536 parsed = ''
536 parsed = ''
537 while !s.eos?
537 while !s.eos?
538 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
538 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
539 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
539 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
540 if tags.empty?
540 if tags.empty?
541 yield text
541 yield text
542 end
542 end
543 parsed << text
543 parsed << text
544 if tag
544 if tag
545 if closing
545 if closing
546 if tags.last == tag.downcase
546 if tags.last == tag.downcase
547 tags.pop
547 tags.pop
548 end
548 end
549 else
549 else
550 tags << tag.downcase
550 tags << tag.downcase
551 end
551 end
552 parsed << full_tag
552 parsed << full_tag
553 end
553 end
554 end
554 end
555 # Close any non closing tags
555 # Close any non closing tags
556 while tag = tags.pop
556 while tag = tags.pop
557 parsed << "</#{tag}>"
557 parsed << "</#{tag}>"
558 end
558 end
559 parsed
559 parsed
560 end
560 end
561
561
562 def parse_inline_attachments(text, project, obj, attr, only_path, options)
562 def parse_inline_attachments(text, project, obj, attr, only_path, options)
563 # when using an image link, try to use an attachment, if possible
563 # when using an image link, try to use an attachment, if possible
564 if options[:attachments] || (obj && obj.respond_to?(:attachments))
564 if options[:attachments] || (obj && obj.respond_to?(:attachments))
565 attachments = options[:attachments] || obj.attachments
565 attachments = options[:attachments] || obj.attachments
566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
568 # search for the picture in attachments
568 # search for the picture in attachments
569 if found = Attachment.latest_attach(attachments, filename)
569 if found = Attachment.latest_attach(attachments, filename)
570 image_url = url_for :only_path => only_path, :controller => 'attachments',
570 image_url = url_for :only_path => only_path, :controller => 'attachments',
571 :action => 'download', :id => found
571 :action => 'download', :id => found
572 desc = found.description.to_s.gsub('"', '')
572 desc = found.description.to_s.gsub('"', '')
573 if !desc.blank? && alttext.blank?
573 if !desc.blank? && alttext.blank?
574 alt = " title=\"#{desc}\" alt=\"#{desc}\""
574 alt = " title=\"#{desc}\" alt=\"#{desc}\""
575 end
575 end
576 "src=\"#{image_url}\"#{alt}"
576 "src=\"#{image_url}\"#{alt}"
577 else
577 else
578 m
578 m
579 end
579 end
580 end
580 end
581 end
581 end
582 end
582 end
583
583
584 # Wiki links
584 # Wiki links
585 #
585 #
586 # Examples:
586 # Examples:
587 # [[mypage]]
587 # [[mypage]]
588 # [[mypage|mytext]]
588 # [[mypage|mytext]]
589 # wiki links can refer other project wikis, using project name or identifier:
589 # wiki links can refer other project wikis, using project name or identifier:
590 # [[project:]] -> wiki starting page
590 # [[project:]] -> wiki starting page
591 # [[project:|mytext]]
591 # [[project:|mytext]]
592 # [[project:mypage]]
592 # [[project:mypage]]
593 # [[project:mypage|mytext]]
593 # [[project:mypage|mytext]]
594 def parse_wiki_links(text, project, obj, attr, only_path, options)
594 def parse_wiki_links(text, project, obj, attr, only_path, options)
595 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
595 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
596 link_project = project
596 link_project = project
597 esc, all, page, title = $1, $2, $3, $5
597 esc, all, page, title = $1, $2, $3, $5
598 if esc.nil?
598 if esc.nil?
599 if page =~ /^([^\:]+)\:(.*)$/
599 if page =~ /^([^\:]+)\:(.*)$/
600 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
600 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
601 page = $2
601 page = $2
602 title ||= $1 if page.blank?
602 title ||= $1 if page.blank?
603 end
603 end
604
604
605 if link_project && link_project.wiki
605 if link_project && link_project.wiki
606 # extract anchor
606 # extract anchor
607 anchor = nil
607 anchor = nil
608 if page =~ /^(.+?)\#(.+)$/
608 if page =~ /^(.+?)\#(.+)$/
609 page, anchor = $1, $2
609 page, anchor = $1, $2
610 end
610 end
611 anchor = sanitize_anchor_name(anchor) if anchor.present?
611 anchor = sanitize_anchor_name(anchor) if anchor.present?
612 # check if page exists
612 # check if page exists
613 wiki_page = link_project.wiki.find_page(page)
613 wiki_page = link_project.wiki.find_page(page)
614 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
614 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
615 "##{anchor}"
615 "##{anchor}"
616 else
616 else
617 case options[:wiki_links]
617 case options[:wiki_links]
618 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
618 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
619 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
619 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
620 else
620 else
621 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
621 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
622 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
622 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
623 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
623 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
624 :id => wiki_page_id, :anchor => anchor, :parent => parent)
624 :id => wiki_page_id, :anchor => anchor, :parent => parent)
625 end
625 end
626 end
626 end
627 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
627 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
628 else
628 else
629 # project or wiki doesn't exist
629 # project or wiki doesn't exist
630 all
630 all
631 end
631 end
632 else
632 else
633 all
633 all
634 end
634 end
635 end
635 end
636 end
636 end
637
637
638 # Redmine links
638 # Redmine links
639 #
639 #
640 # Examples:
640 # Examples:
641 # Issues:
641 # Issues:
642 # #52 -> Link to issue #52
642 # #52 -> Link to issue #52
643 # Changesets:
643 # Changesets:
644 # r52 -> Link to revision 52
644 # r52 -> Link to revision 52
645 # commit:a85130f -> Link to scmid starting with a85130f
645 # commit:a85130f -> Link to scmid starting with a85130f
646 # Documents:
646 # Documents:
647 # document#17 -> Link to document with id 17
647 # document#17 -> Link to document with id 17
648 # document:Greetings -> Link to the document with title "Greetings"
648 # document:Greetings -> Link to the document with title "Greetings"
649 # document:"Some document" -> Link to the document with title "Some document"
649 # document:"Some document" -> Link to the document with title "Some document"
650 # Versions:
650 # Versions:
651 # version#3 -> Link to version with id 3
651 # version#3 -> Link to version with id 3
652 # version:1.0.0 -> Link to version named "1.0.0"
652 # version:1.0.0 -> Link to version named "1.0.0"
653 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
653 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
654 # Attachments:
654 # Attachments:
655 # attachment:file.zip -> Link to the attachment of the current object named file.zip
655 # attachment:file.zip -> Link to the attachment of the current object named file.zip
656 # Source files:
656 # Source files:
657 # source:some/file -> Link to the file located at /some/file in the project's repository
657 # source:some/file -> Link to the file located at /some/file in the project's repository
658 # source:some/file@52 -> Link to the file's revision 52
658 # source:some/file@52 -> Link to the file's revision 52
659 # source:some/file#L120 -> Link to line 120 of the file
659 # source:some/file#L120 -> Link to line 120 of the file
660 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
660 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
661 # export:some/file -> Force the download of the file
661 # export:some/file -> Force the download of the file
662 # Forum messages:
662 # Forum messages:
663 # message#1218 -> Link to message with id 1218
663 # message#1218 -> Link to message with id 1218
664 #
664 #
665 # Links can refer other objects from other projects, using project identifier:
665 # Links can refer other objects from other projects, using project identifier:
666 # identifier:r52
666 # identifier:r52
667 # identifier:document:"Some document"
667 # identifier:document:"Some document"
668 # identifier:version:1.0.0
668 # identifier:version:1.0.0
669 # identifier:source:some/file
669 # identifier:source:some/file
670 def parse_redmine_links(text, project, obj, attr, only_path, options)
670 def parse_redmine_links(text, project, obj, attr, only_path, options)
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|
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 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
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 link = nil
673 link = nil
674 if project_identifier
674 if project_identifier
675 project = Project.visible.find_by_identifier(project_identifier)
675 project = Project.visible.find_by_identifier(project_identifier)
676 end
676 end
677 if esc.nil?
677 if esc.nil?
678 if prefix.nil? && sep == 'r'
678 if prefix.nil? && sep == 'r'
679 if project
679 if project
680 repository = nil
680 repository = nil
681 if repo_identifier
681 if repo_identifier
682 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
682 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
683 else
683 else
684 repository = project.repository
684 repository = project.repository
685 end
685 end
686 # project.changesets.visible raises an SQL error because of a double join on repositories
686 # project.changesets.visible raises an SQL error because of a double join on repositories
687 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
687 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
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},
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 :class => 'changeset',
689 :class => 'changeset',
690 :title => truncate_single_line(changeset.comments, :length => 100))
690 :title => truncate_single_line(changeset.comments, :length => 100))
691 end
691 end
692 end
692 end
693 elsif sep == '#'
693 elsif sep == '#'
694 oid = identifier.to_i
694 oid = identifier.to_i
695 case prefix
695 case prefix
696 when nil
696 when nil
697 if issue = Issue.visible.find_by_id(oid, :include => :status)
697 if issue = Issue.visible.find_by_id(oid, :include => :status)
698 anchor = comment_id ? "note-#{comment_id}" : nil
698 anchor = comment_id ? "note-#{comment_id}" : nil
699 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
699 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
700 :class => issue.css_classes,
700 :class => issue.css_classes,
701 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
701 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
702 end
702 end
703 when 'document'
703 when 'document'
704 if document = Document.visible.find_by_id(oid)
704 if document = Document.visible.find_by_id(oid)
705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
706 :class => 'document'
706 :class => 'document'
707 end
707 end
708 when 'version'
708 when 'version'
709 if version = Version.visible.find_by_id(oid)
709 if version = Version.visible.find_by_id(oid)
710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
711 :class => 'version'
711 :class => 'version'
712 end
712 end
713 when 'message'
713 when 'message'
714 if message = Message.visible.find_by_id(oid, :include => :parent)
714 if message = Message.visible.find_by_id(oid, :include => :parent)
715 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
715 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
716 end
716 end
717 when 'forum'
717 when 'forum'
718 if board = Board.visible.find_by_id(oid)
718 if board = Board.visible.find_by_id(oid)
719 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
719 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
720 :class => 'board'
720 :class => 'board'
721 end
721 end
722 when 'news'
722 when 'news'
723 if news = News.visible.find_by_id(oid)
723 if news = News.visible.find_by_id(oid)
724 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
724 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
725 :class => 'news'
725 :class => 'news'
726 end
726 end
727 when 'project'
727 when 'project'
728 if p = Project.visible.find_by_id(oid)
728 if p = Project.visible.find_by_id(oid)
729 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
729 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
730 end
730 end
731 end
731 end
732 elsif sep == ':'
732 elsif sep == ':'
733 # removes the double quotes if any
733 # removes the double quotes if any
734 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
734 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
735 case prefix
735 case prefix
736 when 'document'
736 when 'document'
737 if project && document = project.documents.visible.find_by_title(name)
737 if project && document = project.documents.visible.find_by_title(name)
738 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
738 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
739 :class => 'document'
739 :class => 'document'
740 end
740 end
741 when 'version'
741 when 'version'
742 if project && version = project.versions.visible.find_by_name(name)
742 if project && version = project.versions.visible.find_by_name(name)
743 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
743 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
744 :class => 'version'
744 :class => 'version'
745 end
745 end
746 when 'forum'
746 when 'forum'
747 if project && board = project.boards.visible.find_by_name(name)
747 if project && board = project.boards.visible.find_by_name(name)
748 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
748 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
749 :class => 'board'
749 :class => 'board'
750 end
750 end
751 when 'news'
751 when 'news'
752 if project && news = project.news.visible.find_by_title(name)
752 if project && news = project.news.visible.find_by_title(name)
753 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
753 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
754 :class => 'news'
754 :class => 'news'
755 end
755 end
756 when 'commit', 'source', 'export'
756 when 'commit', 'source', 'export'
757 if project
757 if project
758 repository = nil
758 repository = nil
759 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
759 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
760 repo_prefix, repo_identifier, name = $1, $2, $3
760 repo_prefix, repo_identifier, name = $1, $2, $3
761 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
762 else
762 else
763 repository = project.repository
763 repository = project.repository
764 end
764 end
765 if prefix == 'commit'
765 if prefix == 'commit'
766 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
766 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
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},
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 :class => 'changeset',
768 :class => 'changeset',
769 :title => truncate_single_line(h(changeset.comments), :length => 100)
769 :title => truncate_single_line(h(changeset.comments), :length => 100)
770 end
770 end
771 else
771 else
772 if repository && User.current.allowed_to?(:browse_repository, project)
772 if repository && User.current.allowed_to?(:browse_repository, project)
773 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
773 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
774 path, rev, anchor = $1, $3, $5
774 path, rev, anchor = $1, $3, $5
775 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
775 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
776 :path => to_path_param(path),
776 :path => to_path_param(path),
777 :rev => rev,
777 :rev => rev,
778 :anchor => anchor,
778 :anchor => anchor,
779 :format => (prefix == 'export' ? 'raw' : nil)},
779 :format => (prefix == 'export' ? 'raw' : nil)},
780 :class => (prefix == 'export' ? 'source download' : 'source')
780 :class => (prefix == 'export' ? 'source download' : 'source')
781 end
781 end
782 end
782 end
783 repo_prefix = nil
783 repo_prefix = nil
784 end
784 end
785 when 'attachment'
785 when 'attachment'
786 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
786 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
787 if attachments && attachment = attachments.detect {|a| a.filename == name }
787 if attachments && attachment = attachments.detect {|a| a.filename == name }
788 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
788 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
789 :class => 'attachment'
789 :class => 'attachment'
790 end
790 end
791 when 'project'
791 when 'project'
792 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
792 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
793 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
793 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
794 end
794 end
795 end
795 end
796 end
796 end
797 end
797 end
798 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
798 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
799 end
799 end
800 end
800 end
801
801
802 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
802 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
803
803
804 def parse_sections(text, project, obj, attr, only_path, options)
804 def parse_sections(text, project, obj, attr, only_path, options)
805 return unless options[:edit_section_links]
805 return unless options[:edit_section_links]
806 text.gsub!(HEADING_RE) do
806 text.gsub!(HEADING_RE) do
807 heading = $1
807 heading = $1
808 @current_section += 1
808 @current_section += 1
809 if @current_section > 1
809 if @current_section > 1
810 content_tag('div',
810 content_tag('div',
811 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
811 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
812 :class => 'contextual',
812 :class => 'contextual',
813 :title => l(:button_edit_section)) + heading.html_safe
813 :title => l(:button_edit_section)) + heading.html_safe
814 else
814 else
815 heading
815 heading
816 end
816 end
817 end
817 end
818 end
818 end
819
819
820 # Headings and TOC
820 # Headings and TOC
821 # Adds ids and links to headings unless options[:headings] is set to false
821 # Adds ids and links to headings unless options[:headings] is set to false
822 def parse_headings(text, project, obj, attr, only_path, options)
822 def parse_headings(text, project, obj, attr, only_path, options)
823 return if options[:headings] == false
823 return if options[:headings] == false
824
824
825 text.gsub!(HEADING_RE) do
825 text.gsub!(HEADING_RE) do
826 level, attrs, content = $2.to_i, $3, $4
826 level, attrs, content = $2.to_i, $3, $4
827 item = strip_tags(content).strip
827 item = strip_tags(content).strip
828 anchor = sanitize_anchor_name(item)
828 anchor = sanitize_anchor_name(item)
829 # used for single-file wiki export
829 # used for single-file wiki export
830 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
830 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
831 @heading_anchors[anchor] ||= 0
831 @heading_anchors[anchor] ||= 0
832 idx = (@heading_anchors[anchor] += 1)
832 idx = (@heading_anchors[anchor] += 1)
833 if idx > 1
833 if idx > 1
834 anchor = "#{anchor}-#{idx}"
834 anchor = "#{anchor}-#{idx}"
835 end
835 end
836 @parsed_headings << [level, anchor, item]
836 @parsed_headings << [level, anchor, item]
837 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
837 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
838 end
838 end
839 end
839 end
840
840
841 MACROS_RE = /
841 MACROS_RE = /
842 (!)? # escaping
842 (!)? # escaping
843 (
843 (
844 \{\{ # opening tag
844 \{\{ # opening tag
845 ([\w]+) # macro name
845 ([\w]+) # macro name
846 (\(([^\}]*)\))? # optional arguments
846 (\(([^\}]*)\))? # optional arguments
847 \}\} # closing tag
847 \}\} # closing tag
848 )
848 )
849 /x unless const_defined?(:MACROS_RE)
849 /x unless const_defined?(:MACROS_RE)
850
850
851 # Macros substitution
851 # Macros substitution
852 def parse_macros(text, project, obj, attr, only_path, options)
852 def parse_macros(text, project, obj, attr, only_path, options)
853 text.gsub!(MACROS_RE) do
853 text.gsub!(MACROS_RE) do
854 esc, all, macro = $1, $2, $3.downcase
854 esc, all, macro = $1, $2, $3.downcase
855 args = ($5 || '').split(',').each(&:strip)
855 args = ($5 || '').split(',').each(&:strip)
856 if esc.nil?
856 if esc.nil?
857 begin
857 begin
858 exec_macro(macro, obj, args)
858 exec_macro(macro, obj, args)
859 rescue => e
859 rescue => e
860 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
860 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
861 end || all
861 end || all
862 else
862 else
863 all
863 all
864 end
864 end
865 end
865 end
866 end
866 end
867
867
868 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
868 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
869
869
870 # Renders the TOC with given headings
870 # Renders the TOC with given headings
871 def replace_toc(text, headings)
871 def replace_toc(text, headings)
872 text.gsub!(TOC_RE) do
872 text.gsub!(TOC_RE) do
873 if headings.empty?
873 if headings.empty?
874 ''
874 ''
875 else
875 else
876 div_class = 'toc'
876 div_class = 'toc'
877 div_class << ' right' if $1 == '>'
877 div_class << ' right' if $1 == '>'
878 div_class << ' left' if $1 == '<'
878 div_class << ' left' if $1 == '<'
879 out = "<ul class=\"#{div_class}\"><li>"
879 out = "<ul class=\"#{div_class}\"><li>"
880 root = headings.map(&:first).min
880 root = headings.map(&:first).min
881 current = root
881 current = root
882 started = false
882 started = false
883 headings.each do |level, anchor, item|
883 headings.each do |level, anchor, item|
884 if level > current
884 if level > current
885 out << '<ul><li>' * (level - current)
885 out << '<ul><li>' * (level - current)
886 elsif level < current
886 elsif level < current
887 out << "</li></ul>\n" * (current - level) + "</li><li>"
887 out << "</li></ul>\n" * (current - level) + "</li><li>"
888 elsif started
888 elsif started
889 out << '</li><li>'
889 out << '</li><li>'
890 end
890 end
891 out << "<a href=\"##{anchor}\">#{item}</a>"
891 out << "<a href=\"##{anchor}\">#{item}</a>"
892 current = level
892 current = level
893 started = true
893 started = true
894 end
894 end
895 out << '</li></ul>' * (current - root)
895 out << '</li></ul>' * (current - root)
896 out << '</li></ul>'
896 out << '</li></ul>'
897 end
897 end
898 end
898 end
899 end
899 end
900
900
901 # Same as Rails' simple_format helper without using paragraphs
901 # Same as Rails' simple_format helper without using paragraphs
902 def simple_format_without_paragraph(text)
902 def simple_format_without_paragraph(text)
903 text.to_s.
903 text.to_s.
904 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
904 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
905 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
905 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
906 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
906 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
907 html_safe
907 html_safe
908 end
908 end
909
909
910 def lang_options_for_select(blank=true)
910 def lang_options_for_select(blank=true)
911 (blank ? [["(auto)", ""]] : []) +
911 (blank ? [["(auto)", ""]] : []) +
912 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
912 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
913 end
913 end
914
914
915 def label_tag_for(name, option_tags = nil, options = {})
915 def label_tag_for(name, option_tags = nil, options = {})
916 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
916 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
917 content_tag("label", label_text)
917 content_tag("label", label_text)
918 end
918 end
919
919
920 def labelled_tabular_form_for(*args, &proc)
920 def labelled_tabular_form_for(*args, &proc)
921 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
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 args << {} unless args.last.is_a?(Hash)
922 args << {} unless args.last.is_a?(Hash)
923 options = args.last
923 options = args.last
924 options[:html] ||= {}
924 options[:html] ||= {}
925 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
925 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
926 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
926 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
927 form_for(*args, &proc)
927 form_for(*args, &proc)
928 end
928 end
929
929
930 def labelled_form_for(*args, &proc)
930 def labelled_form_for(*args, &proc)
931 args << {} unless args.last.is_a?(Hash)
931 args << {} unless args.last.is_a?(Hash)
932 options = args.last
932 options = args.last
933 if args.first.is_a?(Symbol)
934 options.merge!(:as => args.shift)
935 end
933 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
936 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
934 form_for(*args, &proc)
937 form_for(*args, &proc)
935 end
938 end
936
939
937 def labelled_fields_for(*args, &proc)
940 def labelled_fields_for(*args, &proc)
938 args << {} unless args.last.is_a?(Hash)
941 args << {} unless args.last.is_a?(Hash)
939 options = args.last
942 options = args.last
940 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
943 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
941 fields_for(*args, &proc)
944 fields_for(*args, &proc)
942 end
945 end
943
946
944 def labelled_remote_form_for(*args, &proc)
947 def labelled_remote_form_for(*args, &proc)
945 args << {} unless args.last.is_a?(Hash)
948 args << {} unless args.last.is_a?(Hash)
946 options = args.last
949 options = args.last
947 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
950 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
948 remote_form_for(*args, &proc)
951 remote_form_for(*args, &proc)
949 end
952 end
950
953
951 def error_messages_for(*objects)
954 def error_messages_for(*objects)
952 html = ""
955 html = ""
953 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
956 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
954 errors = objects.map {|o| o.errors.full_messages}.flatten
957 errors = objects.map {|o| o.errors.full_messages}.flatten
955 if errors.any?
958 if errors.any?
956 html << "<div id='errorExplanation'><ul>\n"
959 html << "<div id='errorExplanation'><ul>\n"
957 errors.each do |error|
960 errors.each do |error|
958 html << "<li>#{h error}</li>\n"
961 html << "<li>#{h error}</li>\n"
959 end
962 end
960 html << "</ul></div>\n"
963 html << "</ul></div>\n"
961 end
964 end
962 html.html_safe
965 html.html_safe
963 end
966 end
964
967
965 def back_url_hidden_field_tag
968 def back_url_hidden_field_tag
966 back_url = params[:back_url] || request.env['HTTP_REFERER']
969 back_url = params[:back_url] || request.env['HTTP_REFERER']
967 back_url = CGI.unescape(back_url.to_s)
970 back_url = CGI.unescape(back_url.to_s)
968 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
971 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
969 end
972 end
970
973
971 def check_all_links(form_name)
974 def check_all_links(form_name)
972 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
975 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
973 " | ".html_safe +
976 " | ".html_safe +
974 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
977 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
975 end
978 end
976
979
977 def progress_bar(pcts, options={})
980 def progress_bar(pcts, options={})
978 pcts = [pcts, pcts] unless pcts.is_a?(Array)
981 pcts = [pcts, pcts] unless pcts.is_a?(Array)
979 pcts = pcts.collect(&:round)
982 pcts = pcts.collect(&:round)
980 pcts[1] = pcts[1] - pcts[0]
983 pcts[1] = pcts[1] - pcts[0]
981 pcts << (100 - pcts[1] - pcts[0])
984 pcts << (100 - pcts[1] - pcts[0])
982 width = options[:width] || '100px;'
985 width = options[:width] || '100px;'
983 legend = options[:legend] || ''
986 legend = options[:legend] || ''
984 content_tag('table',
987 content_tag('table',
985 content_tag('tr',
988 content_tag('tr',
986 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
989 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
987 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
990 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
988 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
991 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
989 ), :class => 'progress', :style => "width: #{width};").html_safe +
992 ), :class => 'progress', :style => "width: #{width};").html_safe +
990 content_tag('p', legend, :class => 'pourcent').html_safe
993 content_tag('p', legend, :class => 'pourcent').html_safe
991 end
994 end
992
995
993 def checked_image(checked=true)
996 def checked_image(checked=true)
994 if checked
997 if checked
995 image_tag 'toggle_check.png'
998 image_tag 'toggle_check.png'
996 end
999 end
997 end
1000 end
998
1001
999 def context_menu(url)
1002 def context_menu(url)
1000 unless @context_menu_included
1003 unless @context_menu_included
1001 content_for :header_tags do
1004 content_for :header_tags do
1002 javascript_include_tag('context_menu') +
1005 javascript_include_tag('context_menu') +
1003 stylesheet_link_tag('context_menu')
1006 stylesheet_link_tag('context_menu')
1004 end
1007 end
1005 if l(:direction) == 'rtl'
1008 if l(:direction) == 'rtl'
1006 content_for :header_tags do
1009 content_for :header_tags do
1007 stylesheet_link_tag('context_menu_rtl')
1010 stylesheet_link_tag('context_menu_rtl')
1008 end
1011 end
1009 end
1012 end
1010 @context_menu_included = true
1013 @context_menu_included = true
1011 end
1014 end
1012 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1015 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1013 end
1016 end
1014
1017
1015 def calendar_for(field_id)
1018 def calendar_for(field_id)
1016 include_calendar_headers_tags
1019 include_calendar_headers_tags
1017 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1020 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1018 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1021 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1019 end
1022 end
1020
1023
1021 def include_calendar_headers_tags
1024 def include_calendar_headers_tags
1022 unless @calendar_headers_tags_included
1025 unless @calendar_headers_tags_included
1023 @calendar_headers_tags_included = true
1026 @calendar_headers_tags_included = true
1024 content_for :header_tags do
1027 content_for :header_tags do
1025 start_of_week = case Setting.start_of_week.to_i
1028 start_of_week = case Setting.start_of_week.to_i
1026 when 1
1029 when 1
1027 'Calendar._FD = 1;' # Monday
1030 'Calendar._FD = 1;' # Monday
1028 when 7
1031 when 7
1029 'Calendar._FD = 0;' # Sunday
1032 'Calendar._FD = 0;' # Sunday
1030 when 6
1033 when 6
1031 'Calendar._FD = 6;' # Saturday
1034 'Calendar._FD = 6;' # Saturday
1032 else
1035 else
1033 '' # use language
1036 '' # use language
1034 end
1037 end
1035
1038
1036 javascript_include_tag('calendar/calendar') +
1039 javascript_include_tag('calendar/calendar') +
1037 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1040 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1038 javascript_tag(start_of_week) +
1041 javascript_tag(start_of_week) +
1039 javascript_include_tag('calendar/calendar-setup') +
1042 javascript_include_tag('calendar/calendar-setup') +
1040 stylesheet_link_tag('calendar')
1043 stylesheet_link_tag('calendar')
1041 end
1044 end
1042 end
1045 end
1043 end
1046 end
1044
1047
1045 def content_for(name, content = nil, &block)
1048 def content_for(name, content = nil, &block)
1046 @has_content ||= {}
1049 @has_content ||= {}
1047 @has_content[name] = true
1050 @has_content[name] = true
1048 super(name, content, &block)
1051 super(name, content, &block)
1049 end
1052 end
1050
1053
1051 def has_content?(name)
1054 def has_content?(name)
1052 (@has_content && @has_content[name]) || false
1055 (@has_content && @has_content[name]) || false
1053 end
1056 end
1054
1057
1055 def email_delivery_enabled?
1058 def email_delivery_enabled?
1056 !!ActionMailer::Base.perform_deliveries
1059 !!ActionMailer::Base.perform_deliveries
1057 end
1060 end
1058
1061
1059 # Returns the avatar image tag for the given +user+ if avatars are enabled
1062 # Returns the avatar image tag for the given +user+ if avatars are enabled
1060 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1063 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1061 def avatar(user, options = { })
1064 def avatar(user, options = { })
1062 if Setting.gravatar_enabled?
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 email = nil
1067 email = nil
1065 if user.respond_to?(:mail)
1068 if user.respond_to?(:mail)
1066 email = user.mail
1069 email = user.mail
1067 elsif user.to_s =~ %r{<(.+?)>}
1070 elsif user.to_s =~ %r{<(.+?)>}
1068 email = $1
1071 email = $1
1069 end
1072 end
1070 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1073 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1071 else
1074 else
1072 ''
1075 ''
1073 end
1076 end
1074 end
1077 end
1075
1078
1076 def sanitize_anchor_name(anchor)
1079 def sanitize_anchor_name(anchor)
1077 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1080 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1078 end
1081 end
1079
1082
1080 # Returns the javascript tags that are included in the html layout head
1083 # Returns the javascript tags that are included in the html layout head
1081 def javascript_heads
1084 def javascript_heads
1082 tags = javascript_include_tag(:defaults)
1085 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1083 unless User.current.pref.warn_on_leaving_unsaved == '0'
1086 unless User.current.pref.warn_on_leaving_unsaved == '0'
1084 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1087 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1085 end
1088 end
1086 tags
1089 tags
1087 end
1090 end
1088
1091
1089 def favicon
1092 def favicon
1090 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1093 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1091 end
1094 end
1092
1095
1093 def robot_exclusion_tag
1096 def robot_exclusion_tag
1094 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1097 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1095 end
1098 end
1096
1099
1097 # Returns true if arg is expected in the API response
1100 # Returns true if arg is expected in the API response
1098 def include_in_api_response?(arg)
1101 def include_in_api_response?(arg)
1099 unless @included_in_api_response
1102 unless @included_in_api_response
1100 param = params[:include]
1103 param = params[:include]
1101 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1104 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1102 @included_in_api_response.collect!(&:strip)
1105 @included_in_api_response.collect!(&:strip)
1103 end
1106 end
1104 @included_in_api_response.include?(arg.to_s)
1107 @included_in_api_response.include?(arg.to_s)
1105 end
1108 end
1106
1109
1107 # Returns options or nil if nometa param or X-Redmine-Nometa header
1110 # Returns options or nil if nometa param or X-Redmine-Nometa header
1108 # was set in the request
1111 # was set in the request
1109 def api_meta(options)
1112 def api_meta(options)
1110 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1113 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1111 # compatibility mode for activeresource clients that raise
1114 # compatibility mode for activeresource clients that raise
1112 # an error when unserializing an array with attributes
1115 # an error when unserializing an array with attributes
1113 nil
1116 nil
1114 else
1117 else
1115 options
1118 options
1116 end
1119 end
1117 end
1120 end
1118
1121
1119 private
1122 private
1120
1123
1121 def wiki_helper
1124 def wiki_helper
1122 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1125 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1123 extend helper
1126 extend helper
1124 return self
1127 return self
1125 end
1128 end
1126
1129
1127 def link_to_content_update(text, url_params = {}, html_options = {})
1130 def link_to_content_update(text, url_params = {}, html_options = {})
1128 link_to(text, url_params, html_options)
1131 link_to(text, url_params, html_options)
1129 end
1132 end
1130 end
1133 end
@@ -1,43 +1,43
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module WikiHelper
20 module WikiHelper
21
21
22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
24 s = ''
24 s = ''.html_safe
25 if pages.has_key?(parent)
25 if pages.has_key?(parent)
26 pages[parent].each do |page|
26 pages[parent].each do |page|
27 attrs = "value='#{page.id}'"
27 attrs = "value='#{page.id}'"
28 attrs << " selected='selected'" if selected == page
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 wiki_page_options_for_select(pages, selected, page, level + 1)
32 wiki_page_options_for_select(pages, selected, page, level + 1)
33 end
33 end
34 end
34 end
35 s
35 s
36 end
36 end
37
37
38 def wiki_page_breadcrumb(page)
38 def wiki_page_breadcrumb(page)
39 breadcrumb(page.ancestors.reverse.collect {|parent|
39 breadcrumb(page.ancestors.reverse.collect {|parent|
40 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project})
40 link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project})
41 })
41 })
42 end
42 end
43 end
43 end
@@ -1,231 +1,227
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :custom_values, :dependent => :delete_all
21 has_many :custom_values, :dependent => :delete_all
22 acts_as_list :scope => 'type = \'#{self.class}\''
22 acts_as_list :scope => 'type = \'#{self.class}\''
23 serialize :possible_values
23 serialize :possible_values
24
24
25 validates_presence_of :name, :field_format
25 validates_presence_of :name, :field_format
26 validates_uniqueness_of :name, :scope => :type
26 validates_uniqueness_of :name, :scope => :type
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29
29
30 validate :validate_custom_field
30 validate :validate_custom_field
31 before_validation :set_searchable
31 before_validation :set_searchable
32
32
33 def initialize(attributes=nil, *args)
33 def initialize(attributes=nil, *args)
34 super
34 super
35 self.possible_values ||= []
35 self.possible_values ||= []
36 end
36 end
37
37
38 def set_searchable
38 def set_searchable
39 # make sure these fields are not searchable
39 # make sure these fields are not searchable
40 self.searchable = false if %w(int float date bool).include?(field_format)
40 self.searchable = false if %w(int float date bool).include?(field_format)
41 # make sure only these fields can have multiple values
41 # make sure only these fields can have multiple values
42 self.multiple = false unless %w(list user version).include?(field_format)
42 self.multiple = false unless %w(list user version).include?(field_format)
43 true
43 true
44 end
44 end
45
45
46 def validate_custom_field
46 def validate_custom_field
47 if self.field_format == "list"
47 if self.field_format == "list"
48 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
48 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
49 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
49 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
50 end
50 end
51
51
52 if regexp.present?
52 if regexp.present?
53 begin
53 begin
54 Regexp.new(regexp)
54 Regexp.new(regexp)
55 rescue
55 rescue
56 errors.add(:regexp, :invalid)
56 errors.add(:regexp, :invalid)
57 end
57 end
58 end
58 end
59
59
60 if default_value.present? && !valid_field_value?(default_value)
60 if default_value.present? && !valid_field_value?(default_value)
61 errors.add(:default_value, :invalid)
61 errors.add(:default_value, :invalid)
62 end
62 end
63 end
63 end
64
64
65 def possible_values_options(obj=nil)
65 def possible_values_options(obj=nil)
66 case field_format
66 case field_format
67 when 'user', 'version'
67 when 'user', 'version'
68 if obj.respond_to?(:project) && obj.project
68 if obj.respond_to?(:project) && obj.project
69 case field_format
69 case field_format
70 when 'user'
70 when 'user'
71 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
71 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
72 when 'version'
72 when 'version'
73 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
73 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
74 end
74 end
75 elsif obj.is_a?(Array)
75 elsif obj.is_a?(Array)
76 obj.collect {|o| possible_values_options(o)}.reduce(:&)
76 obj.collect {|o| possible_values_options(o)}.reduce(:&)
77 else
77 else
78 []
78 []
79 end
79 end
80 when 'bool'
80 when 'bool'
81 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
81 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
82 else
82 else
83 read_possible_values_utf8_encoded || []
83 possible_values || []
84 end
84 end
85 end
85 end
86
86
87 def possible_values(obj=nil)
87 def possible_values(obj=nil)
88 case field_format
88 case field_format
89 when 'user', 'version'
89 when 'user', 'version'
90 possible_values_options(obj).collect(&:last)
90 possible_values_options(obj).collect(&:last)
91 when 'bool'
91 when 'bool'
92 ['1', '0']
92 ['1', '0']
93 else
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 end
101 end
96 end
102 end
97
103
98 # Makes possible_values accept a multiline string
104 # Makes possible_values accept a multiline string
99 def possible_values=(arg)
105 def possible_values=(arg)
100 if arg.is_a?(Array)
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 else
108 else
103 self.possible_values = arg.to_s.split(/[\n\r]+/)
109 self.possible_values = arg.to_s.split(/[\n\r]+/)
104 end
110 end
105 end
111 end
106
112
107 def cast_value(value)
113 def cast_value(value)
108 casted = nil
114 casted = nil
109 unless value.blank?
115 unless value.blank?
110 case field_format
116 case field_format
111 when 'string', 'text', 'list'
117 when 'string', 'text', 'list'
112 casted = value
118 casted = value
113 when 'date'
119 when 'date'
114 casted = begin; value.to_date; rescue; nil end
120 casted = begin; value.to_date; rescue; nil end
115 when 'bool'
121 when 'bool'
116 casted = (value == '1' ? true : false)
122 casted = (value == '1' ? true : false)
117 when 'int'
123 when 'int'
118 casted = value.to_i
124 casted = value.to_i
119 when 'float'
125 when 'float'
120 casted = value.to_f
126 casted = value.to_f
121 when 'user', 'version'
127 when 'user', 'version'
122 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
128 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
123 end
129 end
124 end
130 end
125 casted
131 casted
126 end
132 end
127
133
128 # Returns a ORDER BY clause that can used to sort customized
134 # Returns a ORDER BY clause that can used to sort customized
129 # objects by their value of the custom field.
135 # objects by their value of the custom field.
130 # Returns false, if the custom field can not be used for sorting.
136 # Returns false, if the custom field can not be used for sorting.
131 def order_statement
137 def order_statement
132 return nil if multiple?
138 return nil if multiple?
133 case field_format
139 case field_format
134 when 'string', 'text', 'list', 'date', 'bool'
140 when 'string', 'text', 'list', 'date', 'bool'
135 # COALESCE is here to make sure that blank and NULL values are sorted equally
141 # COALESCE is here to make sure that blank and NULL values are sorted equally
136 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
142 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
137 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
143 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
138 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
144 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
139 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
145 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
140 when 'int', 'float'
146 when 'int', 'float'
141 # Make the database cast values into numeric
147 # Make the database cast values into numeric
142 # Postgresql will raise an error if a value can not be casted!
148 # Postgresql will raise an error if a value can not be casted!
143 # CustomValue validations should ensure that it doesn't occur
149 # CustomValue validations should ensure that it doesn't occur
144 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
150 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
145 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
151 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
146 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
152 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
147 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
153 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
148 else
154 else
149 nil
155 nil
150 end
156 end
151 end
157 end
152
158
153 def <=>(field)
159 def <=>(field)
154 position <=> field.position
160 position <=> field.position
155 end
161 end
156
162
157 def self.customized_class
163 def self.customized_class
158 self.name =~ /^(.+)CustomField$/
164 self.name =~ /^(.+)CustomField$/
159 begin; $1.constantize; rescue nil; end
165 begin; $1.constantize; rescue nil; end
160 end
166 end
161
167
162 # to move in project_custom_field
168 # to move in project_custom_field
163 def self.for_all
169 def self.for_all
164 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
170 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
165 end
171 end
166
172
167 def type_name
173 def type_name
168 nil
174 nil
169 end
175 end
170
176
171 # Returns the error messages for the given value
177 # Returns the error messages for the given value
172 # or an empty array if value is a valid value for the custom field
178 # or an empty array if value is a valid value for the custom field
173 def validate_field_value(value)
179 def validate_field_value(value)
174 errs = []
180 errs = []
175 if value.is_a?(Array)
181 if value.is_a?(Array)
176 if !multiple?
182 if !multiple?
177 errs << ::I18n.t('activerecord.errors.messages.invalid')
183 errs << ::I18n.t('activerecord.errors.messages.invalid')
178 end
184 end
179 if is_required? && value.detect(&:present?).nil?
185 if is_required? && value.detect(&:present?).nil?
180 errs << ::I18n.t('activerecord.errors.messages.blank')
186 errs << ::I18n.t('activerecord.errors.messages.blank')
181 end
187 end
182 value.each {|v| errs += validate_field_value_format(v)}
188 value.each {|v| errs += validate_field_value_format(v)}
183 else
189 else
184 if is_required? && value.blank?
190 if is_required? && value.blank?
185 errs << ::I18n.t('activerecord.errors.messages.blank')
191 errs << ::I18n.t('activerecord.errors.messages.blank')
186 end
192 end
187 errs += validate_field_value_format(value)
193 errs += validate_field_value_format(value)
188 end
194 end
189 errs
195 errs
190 end
196 end
191
197
192 # Returns true if value is a valid value for the custom field
198 # Returns true if value is a valid value for the custom field
193 def valid_field_value?(value)
199 def valid_field_value?(value)
194 validate_field_value(value).empty?
200 validate_field_value(value).empty?
195 end
201 end
196
202
197 protected
203 protected
198
204
199 # Returns the error message for the given value regarding its format
205 # Returns the error message for the given value regarding its format
200 def validate_field_value_format(value)
206 def validate_field_value_format(value)
201 errs = []
207 errs = []
202 if value.present?
208 if value.present?
203 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
209 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
204 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
210 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
205 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
211 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
206
212
207 # Format specific validations
213 # Format specific validations
208 case field_format
214 case field_format
209 when 'int'
215 when 'int'
210 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
216 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
211 when 'float'
217 when 'float'
212 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
218 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
213 when 'date'
219 when 'date'
214 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
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 when 'list'
221 when 'list'
216 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
222 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
217 end
223 end
218 end
224 end
219 errs
225 errs
220 end
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 end
227 end
@@ -1,1078 +1,1078
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue
61 validate :validate_issue
62
62
63 named_scope :visible, lambda {|*args| { :include => :project,
63 named_scope :visible, lambda {|*args| { :include => :project,
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65
65
66 named_scope :open, lambda {|*args|
66 named_scope :open, lambda {|*args|
67 is_closed = args.size > 0 ? !args.first : false
67 is_closed = args.size > 0 ? !args.first : false
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
68 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 }
69 }
70
70
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
71 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
72 named_scope :with_limit, lambda { |limit| { :limit => limit} }
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
73 named_scope :on_active_project, :include => [:status, :project, :tracker],
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75
75
76 before_create :default_assign
76 before_create :default_assign
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
77 before_save :close_duplicates, :update_done_ratio_from_issue_status
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 after_destroy :update_parent_attributes
80 after_destroy :update_parent_attributes
81
81
82 # Returns a SQL conditions string used to find all issues visible by the specified user
82 # Returns a SQL conditions string used to find all issues visible by the specified user
83 def self.visible_condition(user, options={})
83 def self.visible_condition(user, options={})
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
84 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 case role.issues_visibility
85 case role.issues_visibility
86 when 'all'
86 when 'all'
87 nil
87 nil
88 when 'default'
88 when 'default'
89 user_ids = [user.id] + user.groups.map(&:id)
89 user_ids = [user.id] + user.groups.map(&:id)
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(',')}))"
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 when 'own'
91 when 'own'
92 user_ids = [user.id] + user.groups.map(&:id)
92 user_ids = [user.id] + user.groups.map(&:id)
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 else
94 else
95 '1=0'
95 '1=0'
96 end
96 end
97 end
97 end
98 end
98 end
99
99
100 # Returns true if usr or current user is allowed to view the issue
100 # Returns true if usr or current user is allowed to view the issue
101 def visible?(usr=nil)
101 def visible?(usr=nil)
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
102 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 case role.issues_visibility
103 case role.issues_visibility
104 when 'all'
104 when 'all'
105 true
105 true
106 when 'default'
106 when 'default'
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
107 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 when 'own'
108 when 'own'
109 self.author == user || user.is_or_belongs_to?(assigned_to)
109 self.author == user || user.is_or_belongs_to?(assigned_to)
110 else
110 else
111 false
111 false
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 def initialize(attributes=nil, *args)
116 def initialize(attributes=nil, *args)
117 super
117 super
118 if new_record?
118 if new_record?
119 # set default values for new records only
119 # set default values for new records only
120 self.status ||= IssueStatus.default
120 self.status ||= IssueStatus.default
121 self.priority ||= IssuePriority.default
121 self.priority ||= IssuePriority.default
122 self.watcher_user_ids = []
122 self.watcher_user_ids = []
123 end
123 end
124 end
124 end
125
125
126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
126 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
127 def available_custom_fields
127 def available_custom_fields
128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
128 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
129 end
129 end
130
130
131 # Copies attributes from another issue, arg can be an id or an Issue
131 # Copies attributes from another issue, arg can be an id or an Issue
132 def copy_from(arg, options={})
132 def copy_from(arg, options={})
133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
133 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
134 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
135 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
136 self.status = issue.status
136 self.status = issue.status
137 self.author = User.current
137 self.author = User.current
138 unless options[:attachments] == false
138 unless options[:attachments] == false
139 self.attachments = issue.attachments.map do |attachement|
139 self.attachments = issue.attachments.map do |attachement|
140 attachement.copy(:container => self)
140 attachement.copy(:container => self)
141 end
141 end
142 end
142 end
143 @copied_from = issue
143 @copied_from = issue
144 self
144 self
145 end
145 end
146
146
147 # Returns an unsaved copy of the issue
147 # Returns an unsaved copy of the issue
148 def copy(attributes=nil, copy_options={})
148 def copy(attributes=nil, copy_options={})
149 copy = self.class.new.copy_from(self, copy_options)
149 copy = self.class.new.copy_from(self, copy_options)
150 copy.attributes = attributes if attributes
150 copy.attributes = attributes if attributes
151 copy
151 copy
152 end
152 end
153
153
154 # Returns true if the issue is a copy
154 # Returns true if the issue is a copy
155 def copy?
155 def copy?
156 @copied_from.present?
156 @copied_from.present?
157 end
157 end
158
158
159 # Moves/copies an issue to a new project and tracker
159 # Moves/copies an issue to a new project and tracker
160 # Returns the moved/copied issue on success, false on failure
160 # Returns the moved/copied issue on success, false on failure
161 def move_to_project(new_project, new_tracker=nil, options={})
161 def move_to_project(new_project, new_tracker=nil, options={})
162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
162 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
163
163
164 if options[:copy]
164 if options[:copy]
165 issue = self.copy
165 issue = self.copy
166 else
166 else
167 issue = self
167 issue = self
168 end
168 end
169
169
170 issue.init_journal(User.current, options[:notes])
170 issue.init_journal(User.current, options[:notes])
171
171
172 # Preserve previous behaviour
172 # Preserve previous behaviour
173 # #move_to_project doesn't change tracker automatically
173 # #move_to_project doesn't change tracker automatically
174 issue.send :project=, new_project, true
174 issue.send :project=, new_project, true
175 if new_tracker
175 if new_tracker
176 issue.tracker = new_tracker
176 issue.tracker = new_tracker
177 end
177 end
178 # Allow bulk setting of attributes on the issue
178 # Allow bulk setting of attributes on the issue
179 if options[:attributes]
179 if options[:attributes]
180 issue.attributes = options[:attributes]
180 issue.attributes = options[:attributes]
181 end
181 end
182
182
183 issue.save ? issue : false
183 issue.save ? issue : false
184 end
184 end
185
185
186 def status_id=(sid)
186 def status_id=(sid)
187 self.status = nil
187 self.status = nil
188 write_attribute(:status_id, sid)
188 write_attribute(:status_id, sid)
189 end
189 end
190
190
191 def priority_id=(pid)
191 def priority_id=(pid)
192 self.priority = nil
192 self.priority = nil
193 write_attribute(:priority_id, pid)
193 write_attribute(:priority_id, pid)
194 end
194 end
195
195
196 def category_id=(cid)
196 def category_id=(cid)
197 self.category = nil
197 self.category = nil
198 write_attribute(:category_id, cid)
198 write_attribute(:category_id, cid)
199 end
199 end
200
200
201 def fixed_version_id=(vid)
201 def fixed_version_id=(vid)
202 self.fixed_version = nil
202 self.fixed_version = nil
203 write_attribute(:fixed_version_id, vid)
203 write_attribute(:fixed_version_id, vid)
204 end
204 end
205
205
206 def tracker_id=(tid)
206 def tracker_id=(tid)
207 self.tracker = nil
207 self.tracker = nil
208 result = write_attribute(:tracker_id, tid)
208 result = write_attribute(:tracker_id, tid)
209 @custom_field_values = nil
209 @custom_field_values = nil
210 result
210 result
211 end
211 end
212
212
213 def project_id=(project_id)
213 def project_id=(project_id)
214 if project_id.to_s != self.project_id.to_s
214 if project_id.to_s != self.project_id.to_s
215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
215 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
216 end
216 end
217 end
217 end
218
218
219 def project=(project, keep_tracker=false)
219 def project=(project, keep_tracker=false)
220 project_was = self.project
220 project_was = self.project
221 write_attribute(:project_id, project ? project.id : nil)
221 write_attribute(:project_id, project ? project.id : nil)
222 association_instance_set('project', project)
222 association_instance_set('project', project)
223 if project_was && project && project_was != project
223 if project_was && project && project_was != project
224 unless keep_tracker || project.trackers.include?(tracker)
224 unless keep_tracker || project.trackers.include?(tracker)
225 self.tracker = project.trackers.first
225 self.tracker = project.trackers.first
226 end
226 end
227 # Reassign to the category with same name if any
227 # Reassign to the category with same name if any
228 if category
228 if category
229 self.category = project.issue_categories.find_by_name(category.name)
229 self.category = project.issue_categories.find_by_name(category.name)
230 end
230 end
231 # Keep the fixed_version if it's still valid in the new_project
231 # Keep the fixed_version if it's still valid in the new_project
232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
232 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
233 self.fixed_version = nil
233 self.fixed_version = nil
234 end
234 end
235 if parent && parent.project_id != project_id
235 if parent && parent.project_id != project_id
236 self.parent_issue_id = nil
236 self.parent_issue_id = nil
237 end
237 end
238 @custom_field_values = nil
238 @custom_field_values = nil
239 end
239 end
240 end
240 end
241
241
242 def description=(arg)
242 def description=(arg)
243 if arg.is_a?(String)
243 if arg.is_a?(String)
244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
244 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
245 end
245 end
246 write_attribute(:description, arg)
246 write_attribute(:description, arg)
247 end
247 end
248
248
249 # Overrides attributes= so that project and tracker get assigned first
249 # Overrides assign_attributes so that project and tracker get assigned first
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
250 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
251 return if new_attributes.nil?
251 return if new_attributes.nil?
252 attrs = new_attributes.dup
252 attrs = new_attributes.dup
253 attrs.stringify_keys!
253 attrs.stringify_keys!
254
254
255 %w(project project_id tracker tracker_id).each do |attr|
255 %w(project project_id tracker tracker_id).each do |attr|
256 if attrs.has_key?(attr)
256 if attrs.has_key?(attr)
257 send "#{attr}=", attrs.delete(attr)
257 send "#{attr}=", attrs.delete(attr)
258 end
258 end
259 end
259 end
260 send :attributes_without_project_and_tracker_first=, attrs, *args
260 send :assign_attributes_without_project_and_tracker_first, attrs, *args
261 end
261 end
262 # Do not redefine alias chain on reload (see #4838)
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 def estimated_hours=(h)
265 def estimated_hours=(h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
267 end
267 end
268
268
269 safe_attributes 'project_id',
269 safe_attributes 'project_id',
270 :if => lambda {|issue, user|
270 :if => lambda {|issue, user|
271 if issue.new_record?
271 if issue.new_record?
272 issue.copy?
272 issue.copy?
273 elsif user.allowed_to?(:move_issues, issue.project)
273 elsif user.allowed_to?(:move_issues, issue.project)
274 projects = Issue.allowed_target_projects_on_move(user)
274 projects = Issue.allowed_target_projects_on_move(user)
275 projects.include?(issue.project) && projects.size > 1
275 projects.include?(issue.project) && projects.size > 1
276 end
276 end
277 }
277 }
278
278
279 safe_attributes 'tracker_id',
279 safe_attributes 'tracker_id',
280 'status_id',
280 'status_id',
281 'category_id',
281 'category_id',
282 'assigned_to_id',
282 'assigned_to_id',
283 'priority_id',
283 'priority_id',
284 'fixed_version_id',
284 'fixed_version_id',
285 'subject',
285 'subject',
286 'description',
286 'description',
287 'start_date',
287 'start_date',
288 'due_date',
288 'due_date',
289 'done_ratio',
289 'done_ratio',
290 'estimated_hours',
290 'estimated_hours',
291 'custom_field_values',
291 'custom_field_values',
292 'custom_fields',
292 'custom_fields',
293 'lock_version',
293 'lock_version',
294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
294 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
295
295
296 safe_attributes 'status_id',
296 safe_attributes 'status_id',
297 'assigned_to_id',
297 'assigned_to_id',
298 'fixed_version_id',
298 'fixed_version_id',
299 'done_ratio',
299 'done_ratio',
300 'lock_version',
300 'lock_version',
301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
301 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
302
302
303 safe_attributes 'watcher_user_ids',
303 safe_attributes 'watcher_user_ids',
304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
304 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
305
305
306 safe_attributes 'is_private',
306 safe_attributes 'is_private',
307 :if => lambda {|issue, user|
307 :if => lambda {|issue, user|
308 user.allowed_to?(:set_issues_private, issue.project) ||
308 user.allowed_to?(:set_issues_private, issue.project) ||
309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
309 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
310 }
310 }
311
311
312 safe_attributes 'parent_issue_id',
312 safe_attributes 'parent_issue_id',
313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
313 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
314 user.allowed_to?(:manage_subtasks, issue.project)}
314 user.allowed_to?(:manage_subtasks, issue.project)}
315
315
316 # Safely sets attributes
316 # Safely sets attributes
317 # Should be called from controllers instead of #attributes=
317 # Should be called from controllers instead of #attributes=
318 # attr_accessible is too rough because we still want things like
318 # attr_accessible is too rough because we still want things like
319 # Issue.new(:project => foo) to work
319 # Issue.new(:project => foo) to work
320 def safe_attributes=(attrs, user=User.current)
320 def safe_attributes=(attrs, user=User.current)
321 return unless attrs.is_a?(Hash)
321 return unless attrs.is_a?(Hash)
322
322
323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
323 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
324 attrs = delete_unsafe_attributes(attrs, user)
324 attrs = delete_unsafe_attributes(attrs, user)
325 return if attrs.empty?
325 return if attrs.empty?
326
326
327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
327 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
328 if p = attrs.delete('project_id')
328 if p = attrs.delete('project_id')
329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
329 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
330 self.project_id = p
330 self.project_id = p
331 end
331 end
332 end
332 end
333
333
334 if t = attrs.delete('tracker_id')
334 if t = attrs.delete('tracker_id')
335 self.tracker_id = t
335 self.tracker_id = t
336 end
336 end
337
337
338 if attrs['status_id']
338 if attrs['status_id']
339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
339 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
340 attrs.delete('status_id')
340 attrs.delete('status_id')
341 end
341 end
342 end
342 end
343
343
344 unless leaf?
344 unless leaf?
345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
345 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
346 end
346 end
347
347
348 if attrs['parent_issue_id'].present?
348 if attrs['parent_issue_id'].present?
349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
349 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
350 end
350 end
351
351
352 # mass-assignment security bypass
352 # mass-assignment security bypass
353 self.send :attributes=, attrs, false
353 assign_attributes attrs, :without_protection => true
354 end
354 end
355
355
356 def done_ratio
356 def done_ratio
357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
357 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
358 status.default_done_ratio
358 status.default_done_ratio
359 else
359 else
360 read_attribute(:done_ratio)
360 read_attribute(:done_ratio)
361 end
361 end
362 end
362 end
363
363
364 def self.use_status_for_done_ratio?
364 def self.use_status_for_done_ratio?
365 Setting.issue_done_ratio == 'issue_status'
365 Setting.issue_done_ratio == 'issue_status'
366 end
366 end
367
367
368 def self.use_field_for_done_ratio?
368 def self.use_field_for_done_ratio?
369 Setting.issue_done_ratio == 'issue_field'
369 Setting.issue_done_ratio == 'issue_field'
370 end
370 end
371
371
372 def validate_issue
372 def validate_issue
373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
373 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
374 errors.add :due_date, :not_a_date
374 errors.add :due_date, :not_a_date
375 end
375 end
376
376
377 if self.due_date and self.start_date and self.due_date < self.start_date
377 if self.due_date and self.start_date and self.due_date < self.start_date
378 errors.add :due_date, :greater_than_start_date
378 errors.add :due_date, :greater_than_start_date
379 end
379 end
380
380
381 if start_date && soonest_start && start_date < soonest_start
381 if start_date && soonest_start && start_date < soonest_start
382 errors.add :start_date, :invalid
382 errors.add :start_date, :invalid
383 end
383 end
384
384
385 if fixed_version
385 if fixed_version
386 if !assignable_versions.include?(fixed_version)
386 if !assignable_versions.include?(fixed_version)
387 errors.add :fixed_version_id, :inclusion
387 errors.add :fixed_version_id, :inclusion
388 elsif reopened? && fixed_version.closed?
388 elsif reopened? && fixed_version.closed?
389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
389 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
390 end
390 end
391 end
391 end
392
392
393 # Checks that the issue can not be added/moved to a disabled tracker
393 # Checks that the issue can not be added/moved to a disabled tracker
394 if project && (tracker_id_changed? || project_id_changed?)
394 if project && (tracker_id_changed? || project_id_changed?)
395 unless project.trackers.include?(tracker)
395 unless project.trackers.include?(tracker)
396 errors.add :tracker_id, :inclusion
396 errors.add :tracker_id, :inclusion
397 end
397 end
398 end
398 end
399
399
400 # Checks parent issue assignment
400 # Checks parent issue assignment
401 if @parent_issue
401 if @parent_issue
402 if @parent_issue.project_id != project_id
402 if @parent_issue.project_id != project_id
403 errors.add :parent_issue_id, :not_same_project
403 errors.add :parent_issue_id, :not_same_project
404 elsif !new_record?
404 elsif !new_record?
405 # moving an existing issue
405 # moving an existing issue
406 if @parent_issue.root_id != root_id
406 if @parent_issue.root_id != root_id
407 # we can always move to another tree
407 # we can always move to another tree
408 elsif move_possible?(@parent_issue)
408 elsif move_possible?(@parent_issue)
409 # move accepted inside tree
409 # move accepted inside tree
410 else
410 else
411 errors.add :parent_issue_id, :not_a_valid_parent
411 errors.add :parent_issue_id, :not_a_valid_parent
412 end
412 end
413 end
413 end
414 end
414 end
415 end
415 end
416
416
417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
417 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
418 # even if the user turns off the setting later
418 # even if the user turns off the setting later
419 def update_done_ratio_from_issue_status
419 def update_done_ratio_from_issue_status
420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
420 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
421 self.done_ratio = status.default_done_ratio
421 self.done_ratio = status.default_done_ratio
422 end
422 end
423 end
423 end
424
424
425 def init_journal(user, notes = "")
425 def init_journal(user, notes = "")
426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
426 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
427 if new_record?
427 if new_record?
428 @current_journal.notify = false
428 @current_journal.notify = false
429 else
429 else
430 @attributes_before_change = attributes.dup
430 @attributes_before_change = attributes.dup
431 @custom_values_before_change = {}
431 @custom_values_before_change = {}
432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
432 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
433 end
433 end
434 # Make sure updated_on is updated when adding a note.
434 # Make sure updated_on is updated when adding a note.
435 updated_on_will_change!
435 updated_on_will_change!
436 @current_journal
436 @current_journal
437 end
437 end
438
438
439 # Returns the id of the last journal or nil
439 # Returns the id of the last journal or nil
440 def last_journal_id
440 def last_journal_id
441 if new_record?
441 if new_record?
442 nil
442 nil
443 else
443 else
444 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
444 journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
445 end
445 end
446 end
446 end
447
447
448 # Return true if the issue is closed, otherwise false
448 # Return true if the issue is closed, otherwise false
449 def closed?
449 def closed?
450 self.status.is_closed?
450 self.status.is_closed?
451 end
451 end
452
452
453 # Return true if the issue is being reopened
453 # Return true if the issue is being reopened
454 def reopened?
454 def reopened?
455 if !new_record? && status_id_changed?
455 if !new_record? && status_id_changed?
456 status_was = IssueStatus.find_by_id(status_id_was)
456 status_was = IssueStatus.find_by_id(status_id_was)
457 status_new = IssueStatus.find_by_id(status_id)
457 status_new = IssueStatus.find_by_id(status_id)
458 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
458 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
459 return true
459 return true
460 end
460 end
461 end
461 end
462 false
462 false
463 end
463 end
464
464
465 # Return true if the issue is being closed
465 # Return true if the issue is being closed
466 def closing?
466 def closing?
467 if !new_record? && status_id_changed?
467 if !new_record? && status_id_changed?
468 status_was = IssueStatus.find_by_id(status_id_was)
468 status_was = IssueStatus.find_by_id(status_id_was)
469 status_new = IssueStatus.find_by_id(status_id)
469 status_new = IssueStatus.find_by_id(status_id)
470 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
470 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
471 return true
471 return true
472 end
472 end
473 end
473 end
474 false
474 false
475 end
475 end
476
476
477 # Returns true if the issue is overdue
477 # Returns true if the issue is overdue
478 def overdue?
478 def overdue?
479 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
479 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
480 end
480 end
481
481
482 # Is the amount of work done less than it should for the due date
482 # Is the amount of work done less than it should for the due date
483 def behind_schedule?
483 def behind_schedule?
484 return false if start_date.nil? || due_date.nil?
484 return false if start_date.nil? || due_date.nil?
485 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
485 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
486 return done_date <= Date.today
486 return done_date <= Date.today
487 end
487 end
488
488
489 # Does this issue have children?
489 # Does this issue have children?
490 def children?
490 def children?
491 !leaf?
491 !leaf?
492 end
492 end
493
493
494 # Users the issue can be assigned to
494 # Users the issue can be assigned to
495 def assignable_users
495 def assignable_users
496 users = project.assignable_users
496 users = project.assignable_users
497 users << author if author
497 users << author if author
498 users << assigned_to if assigned_to
498 users << assigned_to if assigned_to
499 users.uniq.sort
499 users.uniq.sort
500 end
500 end
501
501
502 # Versions that the issue can be assigned to
502 # Versions that the issue can be assigned to
503 def assignable_versions
503 def assignable_versions
504 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
504 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
505 end
505 end
506
506
507 # Returns true if this issue is blocked by another issue that is still open
507 # Returns true if this issue is blocked by another issue that is still open
508 def blocked?
508 def blocked?
509 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
509 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
510 end
510 end
511
511
512 # Returns an array of statuses that user is able to apply
512 # Returns an array of statuses that user is able to apply
513 def new_statuses_allowed_to(user=User.current, include_default=false)
513 def new_statuses_allowed_to(user=User.current, include_default=false)
514 if new_record? && @copied_from
514 if new_record? && @copied_from
515 [IssueStatus.default, @copied_from.status].compact.uniq.sort
515 [IssueStatus.default, @copied_from.status].compact.uniq.sort
516 else
516 else
517 initial_status = nil
517 initial_status = nil
518 if new_record?
518 if new_record?
519 initial_status = IssueStatus.default
519 initial_status = IssueStatus.default
520 elsif status_id_was
520 elsif status_id_was
521 initial_status = IssueStatus.find_by_id(status_id_was)
521 initial_status = IssueStatus.find_by_id(status_id_was)
522 end
522 end
523 initial_status ||= status
523 initial_status ||= status
524
524
525 statuses = initial_status.find_new_statuses_allowed_to(
525 statuses = initial_status.find_new_statuses_allowed_to(
526 user.admin ? Role.all : user.roles_for_project(project),
526 user.admin ? Role.all : user.roles_for_project(project),
527 tracker,
527 tracker,
528 author == user,
528 author == user,
529 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
529 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
530 )
530 )
531 statuses << initial_status unless statuses.empty?
531 statuses << initial_status unless statuses.empty?
532 statuses << IssueStatus.default if include_default
532 statuses << IssueStatus.default if include_default
533 statuses = statuses.compact.uniq.sort
533 statuses = statuses.compact.uniq.sort
534 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
534 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
535 end
535 end
536 end
536 end
537
537
538 def assigned_to_was
538 def assigned_to_was
539 if assigned_to_id_changed? && assigned_to_id_was.present?
539 if assigned_to_id_changed? && assigned_to_id_was.present?
540 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
540 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
541 end
541 end
542 end
542 end
543
543
544 # Returns the mail adresses of users that should be notified
544 # Returns the mail adresses of users that should be notified
545 def recipients
545 def recipients
546 notified = []
546 notified = []
547 # Author and assignee are always notified unless they have been
547 # Author and assignee are always notified unless they have been
548 # locked or don't want to be notified
548 # locked or don't want to be notified
549 notified << author if author
549 notified << author if author
550 if assigned_to
550 if assigned_to
551 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
551 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
552 end
552 end
553 if assigned_to_was
553 if assigned_to_was
554 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
554 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
555 end
555 end
556 notified = notified.select {|u| u.active? && u.notify_about?(self)}
556 notified = notified.select {|u| u.active? && u.notify_about?(self)}
557
557
558 notified += project.notified_users
558 notified += project.notified_users
559 notified.uniq!
559 notified.uniq!
560 # Remove users that can not view the issue
560 # Remove users that can not view the issue
561 notified.reject! {|user| !visible?(user)}
561 notified.reject! {|user| !visible?(user)}
562 notified.collect(&:mail)
562 notified.collect(&:mail)
563 end
563 end
564
564
565 # Returns the number of hours spent on this issue
565 # Returns the number of hours spent on this issue
566 def spent_hours
566 def spent_hours
567 @spent_hours ||= time_entries.sum(:hours) || 0
567 @spent_hours ||= time_entries.sum(:hours) || 0
568 end
568 end
569
569
570 # Returns the total number of hours spent on this issue and its descendants
570 # Returns the total number of hours spent on this issue and its descendants
571 #
571 #
572 # Example:
572 # Example:
573 # spent_hours => 0.0
573 # spent_hours => 0.0
574 # spent_hours => 50.2
574 # spent_hours => 50.2
575 def total_spent_hours
575 def total_spent_hours
576 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
576 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
577 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
577 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
578 end
578 end
579
579
580 def relations
580 def relations
581 @relations ||= (relations_from + relations_to).sort
581 @relations ||= (relations_from + relations_to).sort
582 end
582 end
583
583
584 # Preloads relations for a collection of issues
584 # Preloads relations for a collection of issues
585 def self.load_relations(issues)
585 def self.load_relations(issues)
586 if issues.any?
586 if issues.any?
587 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
587 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
588 issues.each do |issue|
588 issues.each do |issue|
589 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
589 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
590 end
590 end
591 end
591 end
592 end
592 end
593
593
594 # Preloads visible spent time for a collection of issues
594 # Preloads visible spent time for a collection of issues
595 def self.load_visible_spent_hours(issues, user=User.current)
595 def self.load_visible_spent_hours(issues, user=User.current)
596 if issues.any?
596 if issues.any?
597 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
597 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
598 issues.each do |issue|
598 issues.each do |issue|
599 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
599 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
600 end
600 end
601 end
601 end
602 end
602 end
603
603
604 # Finds an issue relation given its id.
604 # Finds an issue relation given its id.
605 def find_relation(relation_id)
605 def find_relation(relation_id)
606 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
606 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
607 end
607 end
608
608
609 def all_dependent_issues(except=[])
609 def all_dependent_issues(except=[])
610 except << self
610 except << self
611 dependencies = []
611 dependencies = []
612 relations_from.each do |relation|
612 relations_from.each do |relation|
613 if relation.issue_to && !except.include?(relation.issue_to)
613 if relation.issue_to && !except.include?(relation.issue_to)
614 dependencies << relation.issue_to
614 dependencies << relation.issue_to
615 dependencies += relation.issue_to.all_dependent_issues(except)
615 dependencies += relation.issue_to.all_dependent_issues(except)
616 end
616 end
617 end
617 end
618 dependencies
618 dependencies
619 end
619 end
620
620
621 # Returns an array of issues that duplicate this one
621 # Returns an array of issues that duplicate this one
622 def duplicates
622 def duplicates
623 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
623 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
624 end
624 end
625
625
626 # Returns the due date or the target due date if any
626 # Returns the due date or the target due date if any
627 # Used on gantt chart
627 # Used on gantt chart
628 def due_before
628 def due_before
629 due_date || (fixed_version ? fixed_version.effective_date : nil)
629 due_date || (fixed_version ? fixed_version.effective_date : nil)
630 end
630 end
631
631
632 # Returns the time scheduled for this issue.
632 # Returns the time scheduled for this issue.
633 #
633 #
634 # Example:
634 # Example:
635 # Start Date: 2/26/09, End Date: 3/04/09
635 # Start Date: 2/26/09, End Date: 3/04/09
636 # duration => 6
636 # duration => 6
637 def duration
637 def duration
638 (start_date && due_date) ? due_date - start_date : 0
638 (start_date && due_date) ? due_date - start_date : 0
639 end
639 end
640
640
641 def soonest_start
641 def soonest_start
642 @soonest_start ||= (
642 @soonest_start ||= (
643 relations_to.collect{|relation| relation.successor_soonest_start} +
643 relations_to.collect{|relation| relation.successor_soonest_start} +
644 ancestors.collect(&:soonest_start)
644 ancestors.collect(&:soonest_start)
645 ).compact.max
645 ).compact.max
646 end
646 end
647
647
648 def reschedule_after(date)
648 def reschedule_after(date)
649 return if date.nil?
649 return if date.nil?
650 if leaf?
650 if leaf?
651 if start_date.nil? || start_date < date
651 if start_date.nil? || start_date < date
652 self.start_date, self.due_date = date, date + duration
652 self.start_date, self.due_date = date, date + duration
653 begin
653 begin
654 save
654 save
655 rescue ActiveRecord::StaleObjectError
655 rescue ActiveRecord::StaleObjectError
656 reload
656 reload
657 self.start_date, self.due_date = date, date + duration
657 self.start_date, self.due_date = date, date + duration
658 save
658 save
659 end
659 end
660 end
660 end
661 else
661 else
662 leaves.each do |leaf|
662 leaves.each do |leaf|
663 leaf.reschedule_after(date)
663 leaf.reschedule_after(date)
664 end
664 end
665 end
665 end
666 end
666 end
667
667
668 def <=>(issue)
668 def <=>(issue)
669 if issue.nil?
669 if issue.nil?
670 -1
670 -1
671 elsif root_id != issue.root_id
671 elsif root_id != issue.root_id
672 (root_id || 0) <=> (issue.root_id || 0)
672 (root_id || 0) <=> (issue.root_id || 0)
673 else
673 else
674 (lft || 0) <=> (issue.lft || 0)
674 (lft || 0) <=> (issue.lft || 0)
675 end
675 end
676 end
676 end
677
677
678 def to_s
678 def to_s
679 "#{tracker} ##{id}: #{subject}"
679 "#{tracker} ##{id}: #{subject}"
680 end
680 end
681
681
682 # Returns a string of css classes that apply to the issue
682 # Returns a string of css classes that apply to the issue
683 def css_classes
683 def css_classes
684 s = "issue status-#{status.position} priority-#{priority.position}"
684 s = "issue status-#{status.position} priority-#{priority.position}"
685 s << ' closed' if closed?
685 s << ' closed' if closed?
686 s << ' overdue' if overdue?
686 s << ' overdue' if overdue?
687 s << ' child' if child?
687 s << ' child' if child?
688 s << ' parent' unless leaf?
688 s << ' parent' unless leaf?
689 s << ' private' if is_private?
689 s << ' private' if is_private?
690 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
690 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
691 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
691 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
692 s
692 s
693 end
693 end
694
694
695 # Saves an issue and a time_entry from the parameters
695 # Saves an issue and a time_entry from the parameters
696 def save_issue_with_child_records(params, existing_time_entry=nil)
696 def save_issue_with_child_records(params, existing_time_entry=nil)
697 Issue.transaction do
697 Issue.transaction do
698 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
698 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
699 @time_entry = existing_time_entry || TimeEntry.new
699 @time_entry = existing_time_entry || TimeEntry.new
700 @time_entry.project = project
700 @time_entry.project = project
701 @time_entry.issue = self
701 @time_entry.issue = self
702 @time_entry.user = User.current
702 @time_entry.user = User.current
703 @time_entry.spent_on = User.current.today
703 @time_entry.spent_on = User.current.today
704 @time_entry.attributes = params[:time_entry]
704 @time_entry.attributes = params[:time_entry]
705 self.time_entries << @time_entry
705 self.time_entries << @time_entry
706 end
706 end
707
707
708 # TODO: Rename hook
708 # TODO: Rename hook
709 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
709 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
710 if save
710 if save
711 # TODO: Rename hook
711 # TODO: Rename hook
712 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
712 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
713 else
713 else
714 raise ActiveRecord::Rollback
714 raise ActiveRecord::Rollback
715 end
715 end
716 end
716 end
717 end
717 end
718
718
719 # Unassigns issues from +version+ if it's no longer shared with issue's project
719 # Unassigns issues from +version+ if it's no longer shared with issue's project
720 def self.update_versions_from_sharing_change(version)
720 def self.update_versions_from_sharing_change(version)
721 # Update issues assigned to the version
721 # Update issues assigned to the version
722 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
722 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
723 end
723 end
724
724
725 # Unassigns issues from versions that are no longer shared
725 # Unassigns issues from versions that are no longer shared
726 # after +project+ was moved
726 # after +project+ was moved
727 def self.update_versions_from_hierarchy_change(project)
727 def self.update_versions_from_hierarchy_change(project)
728 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
728 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
729 # Update issues of the moved projects and issues assigned to a version of a moved project
729 # Update issues of the moved projects and issues assigned to a version of a moved project
730 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
730 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
731 end
731 end
732
732
733 def parent_issue_id=(arg)
733 def parent_issue_id=(arg)
734 parent_issue_id = arg.blank? ? nil : arg.to_i
734 parent_issue_id = arg.blank? ? nil : arg.to_i
735 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
735 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
736 @parent_issue.id
736 @parent_issue.id
737 else
737 else
738 @parent_issue = nil
738 @parent_issue = nil
739 nil
739 nil
740 end
740 end
741 end
741 end
742
742
743 def parent_issue_id
743 def parent_issue_id
744 if instance_variable_defined? :@parent_issue
744 if instance_variable_defined? :@parent_issue
745 @parent_issue.nil? ? nil : @parent_issue.id
745 @parent_issue.nil? ? nil : @parent_issue.id
746 else
746 else
747 parent_id
747 parent_id
748 end
748 end
749 end
749 end
750
750
751 # Extracted from the ReportsController.
751 # Extracted from the ReportsController.
752 def self.by_tracker(project)
752 def self.by_tracker(project)
753 count_and_group_by(:project => project,
753 count_and_group_by(:project => project,
754 :field => 'tracker_id',
754 :field => 'tracker_id',
755 :joins => Tracker.table_name)
755 :joins => Tracker.table_name)
756 end
756 end
757
757
758 def self.by_version(project)
758 def self.by_version(project)
759 count_and_group_by(:project => project,
759 count_and_group_by(:project => project,
760 :field => 'fixed_version_id',
760 :field => 'fixed_version_id',
761 :joins => Version.table_name)
761 :joins => Version.table_name)
762 end
762 end
763
763
764 def self.by_priority(project)
764 def self.by_priority(project)
765 count_and_group_by(:project => project,
765 count_and_group_by(:project => project,
766 :field => 'priority_id',
766 :field => 'priority_id',
767 :joins => IssuePriority.table_name)
767 :joins => IssuePriority.table_name)
768 end
768 end
769
769
770 def self.by_category(project)
770 def self.by_category(project)
771 count_and_group_by(:project => project,
771 count_and_group_by(:project => project,
772 :field => 'category_id',
772 :field => 'category_id',
773 :joins => IssueCategory.table_name)
773 :joins => IssueCategory.table_name)
774 end
774 end
775
775
776 def self.by_assigned_to(project)
776 def self.by_assigned_to(project)
777 count_and_group_by(:project => project,
777 count_and_group_by(:project => project,
778 :field => 'assigned_to_id',
778 :field => 'assigned_to_id',
779 :joins => User.table_name)
779 :joins => User.table_name)
780 end
780 end
781
781
782 def self.by_author(project)
782 def self.by_author(project)
783 count_and_group_by(:project => project,
783 count_and_group_by(:project => project,
784 :field => 'author_id',
784 :field => 'author_id',
785 :joins => User.table_name)
785 :joins => User.table_name)
786 end
786 end
787
787
788 def self.by_subproject(project)
788 def self.by_subproject(project)
789 ActiveRecord::Base.connection.select_all("select s.id as status_id,
789 ActiveRecord::Base.connection.select_all("select s.id as status_id,
790 s.is_closed as closed,
790 s.is_closed as closed,
791 #{Issue.table_name}.project_id as project_id,
791 #{Issue.table_name}.project_id as project_id,
792 count(#{Issue.table_name}.id) as total
792 count(#{Issue.table_name}.id) as total
793 from
793 from
794 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
794 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
795 where
795 where
796 #{Issue.table_name}.status_id=s.id
796 #{Issue.table_name}.status_id=s.id
797 and #{Issue.table_name}.project_id = #{Project.table_name}.id
797 and #{Issue.table_name}.project_id = #{Project.table_name}.id
798 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
798 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
799 and #{Issue.table_name}.project_id <> #{project.id}
799 and #{Issue.table_name}.project_id <> #{project.id}
800 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
800 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
801 end
801 end
802 # End ReportsController extraction
802 # End ReportsController extraction
803
803
804 # Returns an array of projects that user can assign the issue to
804 # Returns an array of projects that user can assign the issue to
805 def allowed_target_projects(user=User.current)
805 def allowed_target_projects(user=User.current)
806 if new_record?
806 if new_record?
807 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
807 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
808 else
808 else
809 self.class.allowed_target_projects_on_move(user)
809 self.class.allowed_target_projects_on_move(user)
810 end
810 end
811 end
811 end
812
812
813 # Returns an array of projects that user can move issues to
813 # Returns an array of projects that user can move issues to
814 def self.allowed_target_projects_on_move(user=User.current)
814 def self.allowed_target_projects_on_move(user=User.current)
815 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
815 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
816 end
816 end
817
817
818 private
818 private
819
819
820 def after_project_change
820 def after_project_change
821 # Update project_id on related time entries
821 # Update project_id on related time entries
822 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
822 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
823
823
824 # Delete issue relations
824 # Delete issue relations
825 unless Setting.cross_project_issue_relations?
825 unless Setting.cross_project_issue_relations?
826 relations_from.clear
826 relations_from.clear
827 relations_to.clear
827 relations_to.clear
828 end
828 end
829
829
830 # Move subtasks
830 # Move subtasks
831 children.each do |child|
831 children.each do |child|
832 # Change project and keep project
832 # Change project and keep project
833 child.send :project=, project, true
833 child.send :project=, project, true
834 unless child.save
834 unless child.save
835 raise ActiveRecord::Rollback
835 raise ActiveRecord::Rollback
836 end
836 end
837 end
837 end
838 end
838 end
839
839
840 def update_nested_set_attributes
840 def update_nested_set_attributes
841 if root_id.nil?
841 if root_id.nil?
842 # issue was just created
842 # issue was just created
843 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
843 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
844 set_default_left_and_right
844 set_default_left_and_right
845 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
845 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
846 if @parent_issue
846 if @parent_issue
847 move_to_child_of(@parent_issue)
847 move_to_child_of(@parent_issue)
848 end
848 end
849 reload
849 reload
850 elsif parent_issue_id != parent_id
850 elsif parent_issue_id != parent_id
851 former_parent_id = parent_id
851 former_parent_id = parent_id
852 # moving an existing issue
852 # moving an existing issue
853 if @parent_issue && @parent_issue.root_id == root_id
853 if @parent_issue && @parent_issue.root_id == root_id
854 # inside the same tree
854 # inside the same tree
855 move_to_child_of(@parent_issue)
855 move_to_child_of(@parent_issue)
856 else
856 else
857 # to another tree
857 # to another tree
858 unless root?
858 unless root?
859 move_to_right_of(root)
859 move_to_right_of(root)
860 reload
860 reload
861 end
861 end
862 old_root_id = root_id
862 old_root_id = root_id
863 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
863 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
864 target_maxright = nested_set_scope.maximum(right_column_name) || 0
864 target_maxright = nested_set_scope.maximum(right_column_name) || 0
865 offset = target_maxright + 1 - lft
865 offset = target_maxright + 1 - lft
866 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
866 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
867 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
867 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
868 self[left_column_name] = lft + offset
868 self[left_column_name] = lft + offset
869 self[right_column_name] = rgt + offset
869 self[right_column_name] = rgt + offset
870 if @parent_issue
870 if @parent_issue
871 move_to_child_of(@parent_issue)
871 move_to_child_of(@parent_issue)
872 end
872 end
873 end
873 end
874 reload
874 reload
875 # delete invalid relations of all descendants
875 # delete invalid relations of all descendants
876 self_and_descendants.each do |issue|
876 self_and_descendants.each do |issue|
877 issue.relations.each do |relation|
877 issue.relations.each do |relation|
878 relation.destroy unless relation.valid?
878 relation.destroy unless relation.valid?
879 end
879 end
880 end
880 end
881 # update former parent
881 # update former parent
882 recalculate_attributes_for(former_parent_id) if former_parent_id
882 recalculate_attributes_for(former_parent_id) if former_parent_id
883 end
883 end
884 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
884 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
885 end
885 end
886
886
887 def update_parent_attributes
887 def update_parent_attributes
888 recalculate_attributes_for(parent_id) if parent_id
888 recalculate_attributes_for(parent_id) if parent_id
889 end
889 end
890
890
891 def recalculate_attributes_for(issue_id)
891 def recalculate_attributes_for(issue_id)
892 if issue_id && p = Issue.find_by_id(issue_id)
892 if issue_id && p = Issue.find_by_id(issue_id)
893 # priority = highest priority of children
893 # priority = highest priority of children
894 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
894 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
895 p.priority = IssuePriority.find_by_position(priority_position)
895 p.priority = IssuePriority.find_by_position(priority_position)
896 end
896 end
897
897
898 # start/due dates = lowest/highest dates of children
898 # start/due dates = lowest/highest dates of children
899 p.start_date = p.children.minimum(:start_date)
899 p.start_date = p.children.minimum(:start_date)
900 p.due_date = p.children.maximum(:due_date)
900 p.due_date = p.children.maximum(:due_date)
901 if p.start_date && p.due_date && p.due_date < p.start_date
901 if p.start_date && p.due_date && p.due_date < p.start_date
902 p.start_date, p.due_date = p.due_date, p.start_date
902 p.start_date, p.due_date = p.due_date, p.start_date
903 end
903 end
904
904
905 # done ratio = weighted average ratio of leaves
905 # done ratio = weighted average ratio of leaves
906 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
906 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
907 leaves_count = p.leaves.count
907 leaves_count = p.leaves.count
908 if leaves_count > 0
908 if leaves_count > 0
909 average = p.leaves.average(:estimated_hours).to_f
909 average = p.leaves.average(:estimated_hours).to_f
910 if average == 0
910 if average == 0
911 average = 1
911 average = 1
912 end
912 end
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
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 progress = done / (average * leaves_count)
914 progress = done / (average * leaves_count)
915 p.done_ratio = progress.round
915 p.done_ratio = progress.round
916 end
916 end
917 end
917 end
918
918
919 # estimate = sum of leaves estimates
919 # estimate = sum of leaves estimates
920 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
920 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
921 p.estimated_hours = nil if p.estimated_hours == 0.0
921 p.estimated_hours = nil if p.estimated_hours == 0.0
922
922
923 # ancestors will be recursively updated
923 # ancestors will be recursively updated
924 p.save(false)
924 p.save(:validate => false)
925 end
925 end
926 end
926 end
927
927
928 # Update issues so their versions are not pointing to a
928 # Update issues so their versions are not pointing to a
929 # fixed_version that is not shared with the issue's project
929 # fixed_version that is not shared with the issue's project
930 def self.update_versions(conditions=nil)
930 def self.update_versions(conditions=nil)
931 # Only need to update issues with a fixed_version from
931 # Only need to update issues with a fixed_version from
932 # a different project and that is not systemwide shared
932 # a different project and that is not systemwide shared
933 Issue.scoped(:conditions => conditions).all(
933 Issue.scoped(:conditions => conditions).all(
934 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
934 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
935 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
935 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
936 " AND #{Version.table_name}.sharing <> 'system'",
936 " AND #{Version.table_name}.sharing <> 'system'",
937 :include => [:project, :fixed_version]
937 :include => [:project, :fixed_version]
938 ).each do |issue|
938 ).each do |issue|
939 next if issue.project.nil? || issue.fixed_version.nil?
939 next if issue.project.nil? || issue.fixed_version.nil?
940 unless issue.project.shared_versions.include?(issue.fixed_version)
940 unless issue.project.shared_versions.include?(issue.fixed_version)
941 issue.init_journal(User.current)
941 issue.init_journal(User.current)
942 issue.fixed_version = nil
942 issue.fixed_version = nil
943 issue.save
943 issue.save
944 end
944 end
945 end
945 end
946 end
946 end
947
947
948 # Callback on attachment deletion
948 # Callback on attachment deletion
949 def attachment_added(obj)
949 def attachment_added(obj)
950 if @current_journal && !obj.new_record?
950 if @current_journal && !obj.new_record?
951 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
951 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
952 end
952 end
953 end
953 end
954
954
955 # Callback on attachment deletion
955 # Callback on attachment deletion
956 def attachment_removed(obj)
956 def attachment_removed(obj)
957 if @current_journal && !obj.new_record?
957 if @current_journal && !obj.new_record?
958 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
958 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
959 @current_journal.save
959 @current_journal.save
960 end
960 end
961 end
961 end
962
962
963 # Default assignment based on category
963 # Default assignment based on category
964 def default_assign
964 def default_assign
965 if assigned_to.nil? && category && category.assigned_to
965 if assigned_to.nil? && category && category.assigned_to
966 self.assigned_to = category.assigned_to
966 self.assigned_to = category.assigned_to
967 end
967 end
968 end
968 end
969
969
970 # Updates start/due dates of following issues
970 # Updates start/due dates of following issues
971 def reschedule_following_issues
971 def reschedule_following_issues
972 if start_date_changed? || due_date_changed?
972 if start_date_changed? || due_date_changed?
973 relations_from.each do |relation|
973 relations_from.each do |relation|
974 relation.set_issue_to_dates
974 relation.set_issue_to_dates
975 end
975 end
976 end
976 end
977 end
977 end
978
978
979 # Closes duplicates if the issue is being closed
979 # Closes duplicates if the issue is being closed
980 def close_duplicates
980 def close_duplicates
981 if closing?
981 if closing?
982 duplicates.each do |duplicate|
982 duplicates.each do |duplicate|
983 # Reload is need in case the duplicate was updated by a previous duplicate
983 # Reload is need in case the duplicate was updated by a previous duplicate
984 duplicate.reload
984 duplicate.reload
985 # Don't re-close it if it's already closed
985 # Don't re-close it if it's already closed
986 next if duplicate.closed?
986 next if duplicate.closed?
987 # Same user and notes
987 # Same user and notes
988 if @current_journal
988 if @current_journal
989 duplicate.init_journal(@current_journal.user, @current_journal.notes)
989 duplicate.init_journal(@current_journal.user, @current_journal.notes)
990 end
990 end
991 duplicate.update_attribute :status, self.status
991 duplicate.update_attribute :status, self.status
992 end
992 end
993 end
993 end
994 end
994 end
995
995
996 # Saves the changes in a Journal
996 # Saves the changes in a Journal
997 # Called after_save
997 # Called after_save
998 def create_journal
998 def create_journal
999 if @current_journal
999 if @current_journal
1000 # attributes changes
1000 # attributes changes
1001 if @attributes_before_change
1001 if @attributes_before_change
1002 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1002 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1003 before = @attributes_before_change[c]
1003 before = @attributes_before_change[c]
1004 after = send(c)
1004 after = send(c)
1005 next if before == after || (before.blank? && after.blank?)
1005 next if before == after || (before.blank? && after.blank?)
1006 @current_journal.details << JournalDetail.new(:property => 'attr',
1006 @current_journal.details << JournalDetail.new(:property => 'attr',
1007 :prop_key => c,
1007 :prop_key => c,
1008 :old_value => before,
1008 :old_value => before,
1009 :value => after)
1009 :value => after)
1010 }
1010 }
1011 end
1011 end
1012 if @custom_values_before_change
1012 if @custom_values_before_change
1013 # custom fields changes
1013 # custom fields changes
1014 custom_field_values.each {|c|
1014 custom_field_values.each {|c|
1015 before = @custom_values_before_change[c.custom_field_id]
1015 before = @custom_values_before_change[c.custom_field_id]
1016 after = c.value
1016 after = c.value
1017 next if before == after || (before.blank? && after.blank?)
1017 next if before == after || (before.blank? && after.blank?)
1018
1018
1019 if before.is_a?(Array) || after.is_a?(Array)
1019 if before.is_a?(Array) || after.is_a?(Array)
1020 before = [before] unless before.is_a?(Array)
1020 before = [before] unless before.is_a?(Array)
1021 after = [after] unless after.is_a?(Array)
1021 after = [after] unless after.is_a?(Array)
1022
1022
1023 # values removed
1023 # values removed
1024 (before - after).reject(&:blank?).each do |value|
1024 (before - after).reject(&:blank?).each do |value|
1025 @current_journal.details << JournalDetail.new(:property => 'cf',
1025 @current_journal.details << JournalDetail.new(:property => 'cf',
1026 :prop_key => c.custom_field_id,
1026 :prop_key => c.custom_field_id,
1027 :old_value => value,
1027 :old_value => value,
1028 :value => nil)
1028 :value => nil)
1029 end
1029 end
1030 # values added
1030 # values added
1031 (after - before).reject(&:blank?).each do |value|
1031 (after - before).reject(&:blank?).each do |value|
1032 @current_journal.details << JournalDetail.new(:property => 'cf',
1032 @current_journal.details << JournalDetail.new(:property => 'cf',
1033 :prop_key => c.custom_field_id,
1033 :prop_key => c.custom_field_id,
1034 :old_value => nil,
1034 :old_value => nil,
1035 :value => value)
1035 :value => value)
1036 end
1036 end
1037 else
1037 else
1038 @current_journal.details << JournalDetail.new(:property => 'cf',
1038 @current_journal.details << JournalDetail.new(:property => 'cf',
1039 :prop_key => c.custom_field_id,
1039 :prop_key => c.custom_field_id,
1040 :old_value => before,
1040 :old_value => before,
1041 :value => after)
1041 :value => after)
1042 end
1042 end
1043 }
1043 }
1044 end
1044 end
1045 @current_journal.save
1045 @current_journal.save
1046 # reset current journal
1046 # reset current journal
1047 init_journal @current_journal.user, @current_journal.notes
1047 init_journal @current_journal.user, @current_journal.notes
1048 end
1048 end
1049 end
1049 end
1050
1050
1051 # Query generator for selecting groups of issue counts for a project
1051 # Query generator for selecting groups of issue counts for a project
1052 # based on specific criteria
1052 # based on specific criteria
1053 #
1053 #
1054 # Options
1054 # Options
1055 # * project - Project to search in.
1055 # * project - Project to search in.
1056 # * field - String. Issue field to key off of in the grouping.
1056 # * field - String. Issue field to key off of in the grouping.
1057 # * joins - String. The table name to join against.
1057 # * joins - String. The table name to join against.
1058 def self.count_and_group_by(options)
1058 def self.count_and_group_by(options)
1059 project = options.delete(:project)
1059 project = options.delete(:project)
1060 select_field = options.delete(:field)
1060 select_field = options.delete(:field)
1061 joins = options.delete(:joins)
1061 joins = options.delete(:joins)
1062
1062
1063 where = "#{Issue.table_name}.#{select_field}=j.id"
1063 where = "#{Issue.table_name}.#{select_field}=j.id"
1064
1064
1065 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1065 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1066 s.is_closed as closed,
1066 s.is_closed as closed,
1067 j.id as #{select_field},
1067 j.id as #{select_field},
1068 count(#{Issue.table_name}.id) as total
1068 count(#{Issue.table_name}.id) as total
1069 from
1069 from
1070 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1070 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1071 where
1071 where
1072 #{Issue.table_name}.status_id=s.id
1072 #{Issue.table_name}.status_id=s.id
1073 and #{where}
1073 and #{where}
1074 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1074 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1075 and #{visible_condition(User.current, :project => project)}
1075 and #{visible_condition(User.current, :project => project)}
1076 group by s.id, s.is_closed, j.id")
1076 group by s.id, s.is_closed, j.id")
1077 end
1077 end
1078 end
1078 end
@@ -1,446 +1,455
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 require 'vendor/tmail'
19
20 class MailHandler
19 include ActionView::Helpers::SanitizeHelper
21 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
22 include Redmine::I18n
21
23
22 class UnauthorizedAction < StandardError; end
24 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
25 class MissingInformation < StandardError; end
24
26
25 attr_reader :email, :user
27 attr_reader :email, :user
26
28
27 def self.receive(email, options={})
29 def self.receive(email, options={})
28 @@handler_options = options.dup
30 @@handler_options = options.dup
29
31
30 @@handler_options[:issue] ||= {}
32 @@handler_options[:issue] ||= {}
31
33
32 if @@handler_options[:allow_override].is_a?(String)
34 if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
35 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 end
36 end
35 @@handler_options[:allow_override] ||= []
37 @@handler_options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
38 # Project needs to be overridable if not specified
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
39 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 # Status overridable by default
40 # Status overridable by default
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
41 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
42
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
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 end
52 end
44
53
45 cattr_accessor :ignored_emails_headers
54 cattr_accessor :ignored_emails_headers
46 @@ignored_emails_headers = {
55 @@ignored_emails_headers = {
47 'X-Auto-Response-Suppress' => 'OOF',
56 'X-Auto-Response-Suppress' => 'OOF',
48 'Auto-Submitted' => 'auto-replied'
57 'Auto-Submitted' => 'auto-replied'
49 }
58 }
50
59
51 # Processes incoming emails
60 # Processes incoming emails
52 # Returns the created object (eg. an issue, a message) or false
61 # Returns the created object (eg. an issue, a message) or false
53 def receive(email)
62 def receive(email)
54 @email = email
63 @email = email
55 sender_email = email.from.to_a.first.to_s.strip
64 sender_email = email.from.to_a.first.to_s.strip
56 # Ignore emails received from the application emission address to avoid hell cycles
65 # Ignore emails received from the application emission address to avoid hell cycles
57 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
66 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
58 if logger && logger.info
67 if logger && logger.info
59 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
68 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
60 end
69 end
61 return false
70 return false
62 end
71 end
63 # Ignore auto generated emails
72 # Ignore auto generated emails
64 self.class.ignored_emails_headers.each do |key, ignored_value|
73 self.class.ignored_emails_headers.each do |key, ignored_value|
65 value = email.header_string(key)
74 value = email.header_string(key)
66 if value && value.to_s.downcase == ignored_value.downcase
75 if value && value.to_s.downcase == ignored_value.downcase
67 if logger && logger.info
76 if logger && logger.info
68 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
69 end
78 end
70 return false
79 return false
71 end
80 end
72 end
81 end
73 @user = User.find_by_mail(sender_email) if sender_email.present?
82 @user = User.find_by_mail(sender_email) if sender_email.present?
74 if @user && !@user.active?
83 if @user && !@user.active?
75 if logger && logger.info
84 if logger && logger.info
76 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
77 end
86 end
78 return false
87 return false
79 end
88 end
80 if @user.nil?
89 if @user.nil?
81 # Email was submitted by an unknown user
90 # Email was submitted by an unknown user
82 case @@handler_options[:unknown_user]
91 case @@handler_options[:unknown_user]
83 when 'accept'
92 when 'accept'
84 @user = User.anonymous
93 @user = User.anonymous
85 when 'create'
94 when 'create'
86 @user = create_user_from_email
95 @user = create_user_from_email
87 if @user
96 if @user
88 if logger && logger.info
97 if logger && logger.info
89 logger.info "MailHandler: [#{@user.login}] account created"
98 logger.info "MailHandler: [#{@user.login}] account created"
90 end
99 end
91 Mailer.deliver_account_information(@user, @user.password)
100 Mailer.deliver_account_information(@user, @user.password)
92 else
101 else
93 if logger && logger.error
102 if logger && logger.error
94 logger.error "MailHandler: could not create account for [#{sender_email}]"
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
95 end
104 end
96 return false
105 return false
97 end
106 end
98 else
107 else
99 # Default behaviour, emails from unknown users are ignored
108 # Default behaviour, emails from unknown users are ignored
100 if logger && logger.info
109 if logger && logger.info
101 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
102 end
111 end
103 return false
112 return false
104 end
113 end
105 end
114 end
106 User.current = @user
115 User.current = @user
107 dispatch
116 dispatch
108 end
117 end
109
118
110 private
119 private
111
120
112 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
121 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
113 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
114 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
115
124
116 def dispatch
125 def dispatch
117 headers = [email.in_reply_to, email.references].flatten.compact
126 headers = [email.in_reply_to, email.references].flatten.compact
118 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
127 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
119 klass, object_id = $1, $2.to_i
128 klass, object_id = $1, $2.to_i
120 method_name = "receive_#{klass}_reply"
129 method_name = "receive_#{klass}_reply"
121 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
130 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
122 send method_name, object_id
131 send method_name, object_id
123 else
132 else
124 # ignoring it
133 # ignoring it
125 end
134 end
126 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
135 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
127 receive_issue_reply(m[1].to_i)
136 receive_issue_reply(m[1].to_i)
128 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
137 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
129 receive_message_reply(m[1].to_i)
138 receive_message_reply(m[1].to_i)
130 else
139 else
131 dispatch_to_default
140 dispatch_to_default
132 end
141 end
133 rescue ActiveRecord::RecordInvalid => e
142 rescue ActiveRecord::RecordInvalid => e
134 # TODO: send a email to the user
143 # TODO: send a email to the user
135 logger.error e.message if logger
144 logger.error e.message if logger
136 false
145 false
137 rescue MissingInformation => e
146 rescue MissingInformation => e
138 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
147 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
139 false
148 false
140 rescue UnauthorizedAction => e
149 rescue UnauthorizedAction => e
141 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
150 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
142 false
151 false
143 end
152 end
144
153
145 def dispatch_to_default
154 def dispatch_to_default
146 receive_issue
155 receive_issue
147 end
156 end
148
157
149 # Creates a new issue
158 # Creates a new issue
150 def receive_issue
159 def receive_issue
151 project = target_project
160 project = target_project
152 # check permission
161 # check permission
153 unless @@handler_options[:no_permission_check]
162 unless @@handler_options[:no_permission_check]
154 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
163 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
155 end
164 end
156
165
157 issue = Issue.new(:author => user, :project => project)
166 issue = Issue.new(:author => user, :project => project)
158 issue.safe_attributes = issue_attributes_from_keywords(issue)
167 issue.safe_attributes = issue_attributes_from_keywords(issue)
159 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
168 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
160 issue.subject = email.subject.to_s.chomp[0,255]
169 issue.subject = email.subject.to_s.chomp[0,255]
161 if issue.subject.blank?
170 if issue.subject.blank?
162 issue.subject = '(no subject)'
171 issue.subject = '(no subject)'
163 end
172 end
164 issue.description = cleaned_up_text_body
173 issue.description = cleaned_up_text_body
165
174
166 # add To and Cc as watchers before saving so the watchers can reply to Redmine
175 # add To and Cc as watchers before saving so the watchers can reply to Redmine
167 add_watchers(issue)
176 add_watchers(issue)
168 issue.save!
177 issue.save!
169 add_attachments(issue)
178 add_attachments(issue)
170 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
179 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
171 issue
180 issue
172 end
181 end
173
182
174 # Adds a note to an existing issue
183 # Adds a note to an existing issue
175 def receive_issue_reply(issue_id)
184 def receive_issue_reply(issue_id)
176 issue = Issue.find_by_id(issue_id)
185 issue = Issue.find_by_id(issue_id)
177 return unless issue
186 return unless issue
178 # check permission
187 # check permission
179 unless @@handler_options[:no_permission_check]
188 unless @@handler_options[:no_permission_check]
180 unless user.allowed_to?(:add_issue_notes, issue.project) ||
189 unless user.allowed_to?(:add_issue_notes, issue.project) ||
181 user.allowed_to?(:edit_issues, issue.project)
190 user.allowed_to?(:edit_issues, issue.project)
182 raise UnauthorizedAction
191 raise UnauthorizedAction
183 end
192 end
184 end
193 end
185
194
186 # ignore CLI-supplied defaults for new issues
195 # ignore CLI-supplied defaults for new issues
187 @@handler_options[:issue].clear
196 @@handler_options[:issue].clear
188
197
189 journal = issue.init_journal(user)
198 journal = issue.init_journal(user)
190 issue.safe_attributes = issue_attributes_from_keywords(issue)
199 issue.safe_attributes = issue_attributes_from_keywords(issue)
191 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
200 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
192 journal.notes = cleaned_up_text_body
201 journal.notes = cleaned_up_text_body
193 add_attachments(issue)
202 add_attachments(issue)
194 issue.save!
203 issue.save!
195 if logger && logger.info
204 if logger && logger.info
196 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
205 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
197 end
206 end
198 journal
207 journal
199 end
208 end
200
209
201 # Reply will be added to the issue
210 # Reply will be added to the issue
202 def receive_journal_reply(journal_id)
211 def receive_journal_reply(journal_id)
203 journal = Journal.find_by_id(journal_id)
212 journal = Journal.find_by_id(journal_id)
204 if journal && journal.journalized_type == 'Issue'
213 if journal && journal.journalized_type == 'Issue'
205 receive_issue_reply(journal.journalized_id)
214 receive_issue_reply(journal.journalized_id)
206 end
215 end
207 end
216 end
208
217
209 # Receives a reply to a forum message
218 # Receives a reply to a forum message
210 def receive_message_reply(message_id)
219 def receive_message_reply(message_id)
211 message = Message.find_by_id(message_id)
220 message = Message.find_by_id(message_id)
212 if message
221 if message
213 message = message.root
222 message = message.root
214
223
215 unless @@handler_options[:no_permission_check]
224 unless @@handler_options[:no_permission_check]
216 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
225 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
217 end
226 end
218
227
219 if !message.locked?
228 if !message.locked?
220 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
229 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
221 :content => cleaned_up_text_body)
230 :content => cleaned_up_text_body)
222 reply.author = user
231 reply.author = user
223 reply.board = message.board
232 reply.board = message.board
224 message.children << reply
233 message.children << reply
225 add_attachments(reply)
234 add_attachments(reply)
226 reply
235 reply
227 else
236 else
228 if logger && logger.info
237 if logger && logger.info
229 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
238 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
230 end
239 end
231 end
240 end
232 end
241 end
233 end
242 end
234
243
235 def add_attachments(obj)
244 def add_attachments(obj)
236 if email.attachments && email.attachments.any?
245 if email.attachments && email.attachments.any?
237 email.attachments.each do |attachment|
246 email.attachments.each do |attachment|
238 obj.attachments << Attachment.create(:container => obj,
247 obj.attachments << Attachment.create(:container => obj,
239 :file => attachment,
248 :file => attachment,
240 :author => user,
249 :author => user,
241 :content_type => attachment.content_type)
250 :content_type => attachment.content_type)
242 end
251 end
243 end
252 end
244 end
253 end
245
254
246 # Adds To and Cc as watchers of the given object if the sender has the
255 # Adds To and Cc as watchers of the given object if the sender has the
247 # appropriate permission
256 # appropriate permission
248 def add_watchers(obj)
257 def add_watchers(obj)
249 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
258 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
250 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
259 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
251 unless addresses.empty?
260 unless addresses.empty?
252 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
261 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
253 watchers.each {|w| obj.add_watcher(w)}
262 watchers.each {|w| obj.add_watcher(w)}
254 end
263 end
255 end
264 end
256 end
265 end
257
266
258 def get_keyword(attr, options={})
267 def get_keyword(attr, options={})
259 @keywords ||= {}
268 @keywords ||= {}
260 if @keywords.has_key?(attr)
269 if @keywords.has_key?(attr)
261 @keywords[attr]
270 @keywords[attr]
262 else
271 else
263 @keywords[attr] = begin
272 @keywords[attr] = begin
264 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
273 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
265 (v = extract_keyword!(plain_text_body, attr, options[:format]))
274 (v = extract_keyword!(plain_text_body, attr, options[:format]))
266 v
275 v
267 elsif !@@handler_options[:issue][attr].blank?
276 elsif !@@handler_options[:issue][attr].blank?
268 @@handler_options[:issue][attr]
277 @@handler_options[:issue][attr]
269 end
278 end
270 end
279 end
271 end
280 end
272 end
281 end
273
282
274 # Destructively extracts the value for +attr+ in +text+
283 # Destructively extracts the value for +attr+ in +text+
275 # Returns nil if no matching keyword found
284 # Returns nil if no matching keyword found
276 def extract_keyword!(text, attr, format=nil)
285 def extract_keyword!(text, attr, format=nil)
277 keys = [attr.to_s.humanize]
286 keys = [attr.to_s.humanize]
278 if attr.is_a?(Symbol)
287 if attr.is_a?(Symbol)
279 if user && user.language.present?
288 if user && user.language.present?
280 keys << l("field_#{attr}", :default => '', :locale => user.language)
289 keys << l("field_#{attr}", :default => '', :locale => user.language)
281 end
290 end
282 if Setting.default_language.present?
291 if Setting.default_language.present?
283 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
292 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
284 end
293 end
285 end
294 end
286 keys.reject! {|k| k.blank?}
295 keys.reject! {|k| k.blank?}
287 keys.collect! {|k| Regexp.escape(k)}
296 keys.collect! {|k| Regexp.escape(k)}
288 format ||= '.+'
297 format ||= '.+'
289 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
298 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
290 $2 && $2.strip
299 $2 && $2.strip
291 end
300 end
292
301
293 def target_project
302 def target_project
294 # TODO: other ways to specify project:
303 # TODO: other ways to specify project:
295 # * parse the email To field
304 # * parse the email To field
296 # * specific project (eg. Setting.mail_handler_target_project)
305 # * specific project (eg. Setting.mail_handler_target_project)
297 target = Project.find_by_identifier(get_keyword(:project))
306 target = Project.find_by_identifier(get_keyword(:project))
298 raise MissingInformation.new('Unable to determine target project') if target.nil?
307 raise MissingInformation.new('Unable to determine target project') if target.nil?
299 target
308 target
300 end
309 end
301
310
302 # Returns a Hash of issue attributes extracted from keywords in the email body
311 # Returns a Hash of issue attributes extracted from keywords in the email body
303 def issue_attributes_from_keywords(issue)
312 def issue_attributes_from_keywords(issue)
304 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
313 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
305
314
306 attrs = {
315 attrs = {
307 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
316 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
308 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
317 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
309 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
318 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
310 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
319 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
311 'assigned_to_id' => assigned_to.try(:id),
320 'assigned_to_id' => assigned_to.try(:id),
312 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
321 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
313 issue.project.shared_versions.named(k).first.try(:id),
322 issue.project.shared_versions.named(k).first.try(:id),
314 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
323 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
315 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
324 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
316 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
325 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
317 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
326 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
318 }.delete_if {|k, v| v.blank? }
327 }.delete_if {|k, v| v.blank? }
319
328
320 if issue.new_record? && attrs['tracker_id'].nil?
329 if issue.new_record? && attrs['tracker_id'].nil?
321 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
330 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
322 end
331 end
323
332
324 attrs
333 attrs
325 end
334 end
326
335
327 # Returns a Hash of issue custom field values extracted from keywords in the email body
336 # Returns a Hash of issue custom field values extracted from keywords in the email body
328 def custom_field_values_from_keywords(customized)
337 def custom_field_values_from_keywords(customized)
329 customized.custom_field_values.inject({}) do |h, v|
338 customized.custom_field_values.inject({}) do |h, v|
330 if value = get_keyword(v.custom_field.name, :override => true)
339 if value = get_keyword(v.custom_field.name, :override => true)
331 h[v.custom_field.id.to_s] = value
340 h[v.custom_field.id.to_s] = value
332 end
341 end
333 h
342 h
334 end
343 end
335 end
344 end
336
345
337 # Returns the text/plain part of the email
346 # Returns the text/plain part of the email
338 # If not found (eg. HTML-only email), returns the body with tags removed
347 # If not found (eg. HTML-only email), returns the body with tags removed
339 def plain_text_body
348 def plain_text_body
340 return @plain_text_body unless @plain_text_body.nil?
349 return @plain_text_body unless @plain_text_body.nil?
341 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
350 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
342 if parts.empty?
351 if parts.empty?
343 parts << @email
352 parts << @email
344 end
353 end
345 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
354 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
346 if plain_text_part.nil?
355 if plain_text_part.nil?
347 # no text/plain part found, assuming html-only email
356 # no text/plain part found, assuming html-only email
348 # strip html tags and remove doctype directive
357 # strip html tags and remove doctype directive
349 @plain_text_body = strip_tags(@email.body.to_s)
358 @plain_text_body = strip_tags(@email.body.to_s)
350 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
359 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
351 else
360 else
352 @plain_text_body = plain_text_part.body.to_s
361 @plain_text_body = plain_text_part.body.to_s
353 end
362 end
354 @plain_text_body.strip!
363 @plain_text_body.strip!
355 @plain_text_body
364 @plain_text_body
356 end
365 end
357
366
358 def cleaned_up_text_body
367 def cleaned_up_text_body
359 cleanup_body(plain_text_body)
368 cleanup_body(plain_text_body)
360 end
369 end
361
370
362 def self.full_sanitizer
371 def self.full_sanitizer
363 @full_sanitizer ||= HTML::FullSanitizer.new
372 @full_sanitizer ||= HTML::FullSanitizer.new
364 end
373 end
365
374
366 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
375 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
367 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
376 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
368 value = value.to_s.slice(0, limit)
377 value = value.to_s.slice(0, limit)
369 object.send("#{attribute}=", value)
378 object.send("#{attribute}=", value)
370 end
379 end
371
380
372 # Returns a User from an email address and a full name
381 # Returns a User from an email address and a full name
373 def self.new_user_from_attributes(email_address, fullname=nil)
382 def self.new_user_from_attributes(email_address, fullname=nil)
374 user = User.new
383 user = User.new
375
384
376 # Truncating the email address would result in an invalid format
385 # Truncating the email address would result in an invalid format
377 user.mail = email_address
386 user.mail = email_address
378 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
387 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
379
388
380 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
389 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
381 assign_string_attribute_with_limit(user, 'firstname', names.shift)
390 assign_string_attribute_with_limit(user, 'firstname', names.shift)
382 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
391 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
383 user.lastname = '-' if user.lastname.blank?
392 user.lastname = '-' if user.lastname.blank?
384
393
385 password_length = [Setting.password_min_length.to_i, 10].max
394 password_length = [Setting.password_min_length.to_i, 10].max
386 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
395 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
387 user.language = Setting.default_language
396 user.language = Setting.default_language
388
397
389 unless user.valid?
398 unless user.valid?
390 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
399 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
391 user.firstname = "-" unless user.errors[:firstname].blank?
400 user.firstname = "-" unless user.errors[:firstname].blank?
392 user.lastname = "-" unless user.errors[:lastname].blank?
401 user.lastname = "-" unless user.errors[:lastname].blank?
393 end
402 end
394
403
395 user
404 user
396 end
405 end
397
406
398 # Creates a User for the +email+ sender
407 # Creates a User for the +email+ sender
399 # Returns the user or nil if it could not be created
408 # Returns the user or nil if it could not be created
400 def create_user_from_email
409 def create_user_from_email
401 addr = email.from_addrs.to_a.first
410 addr = email.from_addrs.to_a.first
402 if addr && !addr.spec.blank?
411 if addr && !addr.spec.blank?
403 user = self.class.new_user_from_attributes(addr.spec, TMail::Unquoter.unquote_and_convert_to(addr.name, 'utf-8'))
412 user = self.class.new_user_from_attributes(addr.spec, TMail::Unquoter.unquote_and_convert_to(addr.name, 'utf-8'))
404 if user.save
413 if user.save
405 user
414 user
406 else
415 else
407 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
416 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
408 nil
417 nil
409 end
418 end
410 else
419 else
411 logger.error "MailHandler: failed to create User: no FROM address found" if logger
420 logger.error "MailHandler: failed to create User: no FROM address found" if logger
412 nil
421 nil
413 end
422 end
414 end
423 end
415
424
416 # Removes the email body of text after the truncation configurations.
425 # Removes the email body of text after the truncation configurations.
417 def cleanup_body(body)
426 def cleanup_body(body)
418 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
427 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
419 unless delimiters.empty?
428 unless delimiters.empty?
420 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
429 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
421 body = body.gsub(regex, '')
430 body = body.gsub(regex, '')
422 end
431 end
423 body.strip
432 body.strip
424 end
433 end
425
434
426 def find_assignee_from_keyword(keyword, issue)
435 def find_assignee_from_keyword(keyword, issue)
427 keyword = keyword.to_s.downcase
436 keyword = keyword.to_s.downcase
428 assignable = issue.assignable_users
437 assignable = issue.assignable_users
429 assignee = nil
438 assignee = nil
430 assignee ||= assignable.detect {|a|
439 assignee ||= assignable.detect {|a|
431 a.mail.to_s.downcase == keyword ||
440 a.mail.to_s.downcase == keyword ||
432 a.login.to_s.downcase == keyword
441 a.login.to_s.downcase == keyword
433 }
442 }
434 if assignee.nil? && keyword.match(/ /)
443 if assignee.nil? && keyword.match(/ /)
435 firstname, lastname = *(keyword.split) # "First Last Throwaway"
444 firstname, lastname = *(keyword.split) # "First Last Throwaway"
436 assignee ||= assignable.detect {|a|
445 assignee ||= assignable.detect {|a|
437 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
446 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
438 a.lastname.to_s.downcase == lastname
447 a.lastname.to_s.downcase == lastname
439 }
448 }
440 end
449 end
441 if assignee.nil?
450 if assignee.nil?
442 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
451 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
443 end
452 end
444 assignee
453 assignee
445 end
454 end
446 end
455 end
@@ -1,498 +1,461
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Mailer < ActionMailer::Base
18 class Mailer < ActionMailer::Base
19 layout 'mailer'
19 layout 'mailer'
20 helper :application
20 helper :application
21 helper :issues
21 helper :issues
22 helper :custom_fields
22 helper :custom_fields
23
23
24 include ActionController::UrlWriter
25 include Redmine::I18n
24 include Redmine::I18n
26
25
27 def self.default_url_options
26 def self.default_url_options
28 h = Setting.host_name
27 { :host => Setting.host_name, :protocol => Setting.protocol }
29 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
30 { :host => h, :protocol => Setting.protocol }
31 end
28 end
32
29
33 # Builds a tmail object used to email recipients of the added issue.
30 # Builds a tmail object used to email recipients of the added issue.
34 #
31 #
35 # Example:
32 # Example:
36 # issue_add(issue) => tmail object
33 # issue_add(issue) => tmail object
37 # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
34 # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
38 def issue_add(issue)
35 def issue_add(issue)
39 redmine_headers 'Project' => issue.project.identifier,
36 redmine_headers 'Project' => issue.project.identifier,
40 'Issue-Id' => issue.id,
37 'Issue-Id' => issue.id,
41 'Issue-Author' => issue.author.login
38 'Issue-Author' => issue.author.login
42 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
39 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
43 message_id issue
40 message_id issue
44 @author = issue.author
41 @author = issue.author
45 recipients issue.recipients
42 @issue = issue
46 cc(issue.watcher_recipients - @recipients)
43 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
47 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
44 recipients = issue.recipients
48 body :issue => issue,
45 cc = issue.watcher_recipients - recipients
49 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
46 mail :to => recipients,
50 render_multipart('issue_add', body)
47 :cc => cc,
48 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
51 end
49 end
52
50
53 # Builds a tmail object used to email recipients of the edited issue.
51 # Builds a tmail object used to email recipients of the edited issue.
54 #
52 #
55 # Example:
53 # Example:
56 # issue_edit(journal) => tmail object
54 # issue_edit(journal) => tmail object
57 # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
55 # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
58 def issue_edit(journal)
56 def issue_edit(journal)
59 issue = journal.journalized.reload
57 issue = journal.journalized.reload
60 redmine_headers 'Project' => issue.project.identifier,
58 redmine_headers 'Project' => issue.project.identifier,
61 'Issue-Id' => issue.id,
59 'Issue-Id' => issue.id,
62 'Issue-Author' => issue.author.login
60 'Issue-Author' => issue.author.login
63 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
61 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
64 message_id journal
62 message_id journal
65 references issue
63 references issue
66 @author = journal.user
64 @author = journal.user
67 recipients issue.recipients
65 recipients = issue.recipients
68 # Watchers in cc
66 # Watchers in cc
69 cc(issue.watcher_recipients - @recipients)
67 cc = issue.watcher_recipients - recipients
70 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
68 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
71 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
69 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
72 s << issue.subject
70 s << issue.subject
73 subject s
71 @issue = issue
74 body :issue => issue,
72 @journal = journal
75 :journal => journal,
73 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
76 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
74 mail :to => recipients,
77
75 :cc => cc,
78 render_multipart('issue_edit', body)
76 :subject => s
79 end
77 end
80
78
81 def reminder(user, issues, days)
79 def reminder(user, issues, days)
82 set_language_if_valid user.language
80 set_language_if_valid user.language
83 recipients user.mail
81 @issues = issues
84 subject l(:mail_subject_reminder, :count => issues.size, :days => days)
82 @days = days
85 body :issues => issues,
83 @issues_url = url_for(:controller => 'issues', :action => 'index',
86 :days => days,
87 :issues_url => url_for(:controller => 'issues', :action => 'index',
88 :set_filter => 1, :assigned_to_id => user.id,
84 :set_filter => 1, :assigned_to_id => user.id,
89 :sort => 'due_date:asc')
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 end
88 end
92
89
93 # Builds a tmail object used to email users belonging to the added document's project.
90 # Builds a tmail object used to email users belonging to the added document's project.
94 #
91 #
95 # Example:
92 # Example:
96 # document_added(document) => tmail object
93 # document_added(document) => tmail object
97 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
94 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
98 def document_added(document)
95 def document_added(document)
99 redmine_headers 'Project' => document.project.identifier
96 redmine_headers 'Project' => document.project.identifier
100 recipients document.recipients
101 @author = User.current
97 @author = User.current
102 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
98 @document = document
103 body :document => document,
99 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
104 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
100 mail :to => document.recipients,
105 render_multipart('document_added', body)
101 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
106 end
102 end
107
103
108 # Builds a tmail object used to email recipients of a project when an attachements are added.
104 # Builds a tmail object used to email recipients of a project when an attachements are added.
109 #
105 #
110 # Example:
106 # Example:
111 # attachments_added(attachments) => tmail object
107 # attachments_added(attachments) => tmail object
112 # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
108 # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
113 def attachments_added(attachments)
109 def attachments_added(attachments)
114 container = attachments.first.container
110 container = attachments.first.container
115 added_to = ''
111 added_to = ''
116 added_to_url = ''
112 added_to_url = ''
117 @author = attachments.first.author
113 @author = attachments.first.author
118 case container.class.name
114 case container.class.name
119 when 'Project'
115 when 'Project'
120 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
116 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
121 added_to = "#{l(:label_project)}: #{container}"
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 when 'Version'
119 when 'Version'
124 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
120 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
125 added_to = "#{l(:label_version)}: #{container.name}"
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 when 'Document'
123 when 'Document'
128 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
124 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
129 added_to = "#{l(:label_document)}: #{container.title}"
125 added_to = "#{l(:label_document)}: #{container.title}"
130 recipients container.recipients
126 recipients = container.recipients
131 end
127 end
132 redmine_headers 'Project' => container.project.identifier
128 redmine_headers 'Project' => container.project.identifier
133 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
129 @attachments = attachments
134 body :attachments => attachments,
130 @added_to = added_to
135 :added_to => added_to,
131 @added_to_url = added_to_url
136 :added_to_url => added_to_url
132 mail :to => recipients,
137 render_multipart('attachments_added', body)
133 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
138 end
134 end
139
135
140 # Builds a tmail object used to email recipients of a news' project when a news item is added.
136 # Builds a tmail object used to email recipients of a news' project when a news item is added.
141 #
137 #
142 # Example:
138 # Example:
143 # news_added(news) => tmail object
139 # news_added(news) => tmail object
144 # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
140 # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
145 def news_added(news)
141 def news_added(news)
146 redmine_headers 'Project' => news.project.identifier
142 redmine_headers 'Project' => news.project.identifier
147 @author = news.author
143 @author = news.author
148 message_id news
144 message_id news
149 recipients news.recipients
145 @news = news
150 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
146 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
151 body :news => news,
147 mail :to => news.recipients,
152 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
148 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
153 render_multipart('news_added', body)
154 end
149 end
155
150
156 # Builds a tmail object used to email recipients of a news' project when a news comment is added.
151 # Builds a tmail object used to email recipients of a news' project when a news comment is added.
157 #
152 #
158 # Example:
153 # Example:
159 # news_comment_added(comment) => tmail object
154 # news_comment_added(comment) => tmail object
160 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
155 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
161 def news_comment_added(comment)
156 def news_comment_added(comment)
162 news = comment.commented
157 news = comment.commented
163 redmine_headers 'Project' => news.project.identifier
158 redmine_headers 'Project' => news.project.identifier
164 @author = comment.author
159 @author = comment.author
165 message_id comment
160 message_id comment
166 recipients news.recipients
161 @news = news
167 cc news.watcher_recipients
162 @comment = comment
168 subject "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
163 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
169 body :news => news,
164 mail :to => news.recipients,
170 :comment => comment,
165 :cc => news.watcher_recipients,
171 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
166 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
172 render_multipart('news_comment_added', body)
173 end
167 end
174
168
175 # Builds a tmail object used to email the recipients of the specified message that was posted.
169 # Builds a tmail object used to email the recipients of the specified message that was posted.
176 #
170 #
177 # Example:
171 # Example:
178 # message_posted(message) => tmail object
172 # message_posted(message) => tmail object
179 # Mailer.deliver_message_posted(message) => sends an email to the recipients
173 # Mailer.deliver_message_posted(message) => sends an email to the recipients
180 def message_posted(message)
174 def message_posted(message)
181 redmine_headers 'Project' => message.project.identifier,
175 redmine_headers 'Project' => message.project.identifier,
182 'Topic-Id' => (message.parent_id || message.id)
176 'Topic-Id' => (message.parent_id || message.id)
183 @author = message.author
177 @author = message.author
184 message_id message
178 message_id message
185 references message.parent unless message.parent.nil?
179 references message.parent unless message.parent.nil?
186 recipients(message.recipients)
180 recipients = message.recipients
187 cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
181 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}"
182 @message = message
189 body :message => message,
183 @message_url = url_for(message.event_url)
190 :message_url => url_for(message.event_url)
184 mail :to => recipients,
191 render_multipart('message_posted', body)
185 :cc => cc,
186 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
192 end
187 end
193
188
194 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
189 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
195 #
190 #
196 # Example:
191 # Example:
197 # wiki_content_added(wiki_content) => tmail object
192 # wiki_content_added(wiki_content) => tmail object
198 # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
193 # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
199 def wiki_content_added(wiki_content)
194 def wiki_content_added(wiki_content)
200 redmine_headers 'Project' => wiki_content.project.identifier,
195 redmine_headers 'Project' => wiki_content.project.identifier,
201 'Wiki-Page-Id' => wiki_content.page.id
196 'Wiki-Page-Id' => wiki_content.page.id
202 @author = wiki_content.author
197 @author = wiki_content.author
203 message_id wiki_content
198 message_id wiki_content
204 recipients wiki_content.recipients
199 recipients = wiki_content.recipients
205 cc(wiki_content.page.wiki.watcher_recipients - recipients)
200 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)}"
201 @wiki_content = wiki_content
207 body :wiki_content => wiki_content,
202 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
208 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
209 :project_id => wiki_content.project,
203 :project_id => wiki_content.project,
210 :id => wiki_content.page.title)
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 end
208 end
213
209
214 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
210 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
215 #
211 #
216 # Example:
212 # Example:
217 # wiki_content_updated(wiki_content) => tmail object
213 # wiki_content_updated(wiki_content) => tmail object
218 # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
214 # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
219 def wiki_content_updated(wiki_content)
215 def wiki_content_updated(wiki_content)
220 redmine_headers 'Project' => wiki_content.project.identifier,
216 redmine_headers 'Project' => wiki_content.project.identifier,
221 'Wiki-Page-Id' => wiki_content.page.id
217 'Wiki-Page-Id' => wiki_content.page.id
222 @author = wiki_content.author
218 @author = wiki_content.author
223 message_id wiki_content
219 message_id wiki_content
224 recipients wiki_content.recipients
220 recipients = wiki_content.recipients
225 cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
221 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)}"
222 @wiki_content = wiki_content
227 body :wiki_content => wiki_content,
223 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
228 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
229 :project_id => wiki_content.project,
224 :project_id => wiki_content.project,
230 :id => wiki_content.page.title),
225 :id => wiki_content.page.title)
231 :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff',
226 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
232 :project_id => wiki_content.project, :id => wiki_content.page.title,
227 :project_id => wiki_content.project, :id => wiki_content.page.title,
233 :version => wiki_content.version)
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 end
232 end
236
233
237 # Builds a tmail object used to email the specified user their account information.
234 # Builds a tmail object used to email the specified user their account information.
238 #
235 #
239 # Example:
236 # Example:
240 # account_information(user, password) => tmail object
237 # account_information(user, password) => tmail object
241 # Mailer.deliver_account_information(user, password) => sends account information to the user
238 # Mailer.deliver_account_information(user, password) => sends account information to the user
242 def account_information(user, password)
239 def account_information(user, password)
243 set_language_if_valid user.language
240 set_language_if_valid user.language
244 recipients user.mail
241 @user = user
245 subject l(:mail_subject_register, Setting.app_title)
242 @password = password
246 body :user => user,
243 @login_url = url_for(:controller => 'account', :action => 'login')
247 :password => password,
244 mail :to => user.mail,
248 :login_url => url_for(:controller => 'account', :action => 'login')
245 :subject => l(:mail_subject_register, Setting.app_title)
249 render_multipart('account_information', body)
250 end
246 end
251
247
252 # Builds a tmail object used to email all active administrators of an account activation request.
248 # Builds a tmail object used to email all active administrators of an account activation request.
253 #
249 #
254 # Example:
250 # Example:
255 # account_activation_request(user) => tmail object
251 # account_activation_request(user) => tmail object
256 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
252 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
257 def account_activation_request(user)
253 def account_activation_request(user)
258 # Send the email to all active administrators
254 # Send the email to all active administrators
259 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
255 recipients = User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
260 subject l(:mail_subject_account_activation_request, Setting.app_title)
256 @user = user
261 body :user => user,
257 @url = url_for(:controller => 'users', :action => 'index',
262 :url => url_for(:controller => 'users', :action => 'index',
263 :status => User::STATUS_REGISTERED,
258 :status => User::STATUS_REGISTERED,
264 :sort_key => 'created_on', :sort_order => 'desc')
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 end
262 end
267
263
268 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
264 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
269 #
265 #
270 # Example:
266 # Example:
271 # account_activated(user) => tmail object
267 # account_activated(user) => tmail object
272 # Mailer.deliver_account_activated(user) => sends an email to the registered user
268 # Mailer.deliver_account_activated(user) => sends an email to the registered user
273 def account_activated(user)
269 def account_activated(user)
274 set_language_if_valid user.language
270 set_language_if_valid user.language
275 recipients user.mail
271 @user = user
276 subject l(:mail_subject_register, Setting.app_title)
272 @login_url = url_for(:controller => 'account', :action => 'login')
277 body :user => user,
273 mail :to => user.mail,
278 :login_url => url_for(:controller => 'account', :action => 'login')
274 :subject => l(:mail_subject_register, Setting.app_title)
279 render_multipart('account_activated', body)
280 end
275 end
281
276
282 def lost_password(token)
277 def lost_password(token)
283 set_language_if_valid(token.user.language)
278 set_language_if_valid(token.user.language)
284 recipients token.user.mail
279 @token = token
285 subject l(:mail_subject_lost_password, Setting.app_title)
280 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
286 body :token => token,
281 mail :to => token.user.mail,
287 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
282 :subject => l(:mail_subject_lost_password, Setting.app_title)
288 render_multipart('lost_password', body)
289 end
283 end
290
284
291 def register(token)
285 def register(token)
292 set_language_if_valid(token.user.language)
286 set_language_if_valid(token.user.language)
293 recipients token.user.mail
287 @token = token
294 subject l(:mail_subject_register, Setting.app_title)
288 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
295 body :token => token,
289 mail :to => token.user.mail,
296 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
290 :subject => l(:mail_subject_register, Setting.app_title)
297 render_multipart('register', body)
298 end
291 end
299
292
300 def test_email(user)
293 def test_email(user)
301 set_language_if_valid(user.language)
294 set_language_if_valid(user.language)
302 recipients user.mail
295 @url = url_for(:controller => 'welcome')
303 subject 'Redmine test'
296 mail :to => user.mail,
304 body :url => url_for(:controller => 'welcome')
297 :subject => 'Redmine test'
305 render_multipart('test_email', body)
306 end
298 end
307
299
308 # Overrides default deliver! method to prevent from sending an email
300 # Overrides default deliver! method to prevent from sending an email
309 # with no recipient, cc or bcc
301 # with no recipient, cc or bcc
310 def deliver!(mail = @mail)
302 def deliver!(mail = @mail)
311 set_language_if_valid @initial_language
303 set_language_if_valid @initial_language
312 return false if (recipients.nil? || recipients.empty?) &&
304 return false if (recipients.nil? || recipients.empty?) &&
313 (cc.nil? || cc.empty?) &&
305 (cc.nil? || cc.empty?) &&
314 (bcc.nil? || bcc.empty?)
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 # Log errors when raise_delivery_errors is set to false, Rails does not
309 # Log errors when raise_delivery_errors is set to false, Rails does not
325 raise_errors = self.class.raise_delivery_errors
310 raise_errors = self.class.raise_delivery_errors
326 self.class.raise_delivery_errors = true
311 self.class.raise_delivery_errors = true
327 begin
312 begin
328 return super(mail)
313 return super(mail)
329 rescue Exception => e
314 rescue Exception => e
330 if raise_errors
315 if raise_errors
331 raise e
316 raise e
332 elsif mylogger
317 elsif mylogger
333 mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
318 mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
334 end
319 end
335 ensure
320 ensure
336 self.class.raise_delivery_errors = raise_errors
321 self.class.raise_delivery_errors = raise_errors
337 end
322 end
338 end
323 end
339
324
340 # Sends reminders to issue assignees
325 # Sends reminders to issue assignees
341 # Available options:
326 # Available options:
342 # * :days => how many days in the future to remind about (defaults to 7)
327 # * :days => how many days in the future to remind about (defaults to 7)
343 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
328 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
344 # * :project => id or identifier of project to process (defaults to all projects)
329 # * :project => id or identifier of project to process (defaults to all projects)
345 # * :users => array of user ids who should be reminded
330 # * :users => array of user ids who should be reminded
346 def self.reminders(options={})
331 def self.reminders(options={})
347 days = options[:days] || 7
332 days = options[:days] || 7
348 project = options[:project] ? Project.find(options[:project]) : nil
333 project = options[:project] ? Project.find(options[:project]) : nil
349 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
334 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
350 user_ids = options[:users]
335 user_ids = options[:users]
351
336
352 scope = Issue.open.scoped(:conditions => ["#{Issue.table_name}.assigned_to_id IS NOT NULL" +
337 scope = Issue.open.scoped(:conditions => ["#{Issue.table_name}.assigned_to_id IS NOT NULL" +
353 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
338 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
354 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date]
339 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date]
355 )
340 )
356 scope = scope.scoped(:conditions => {:assigned_to_id => user_ids}) if user_ids.present?
341 scope = scope.scoped(:conditions => {:assigned_to_id => user_ids}) if user_ids.present?
357 scope = scope.scoped(:conditions => {:project_id => project.id}) if project
342 scope = scope.scoped(:conditions => {:project_id => project.id}) if project
358 scope = scope.scoped(:conditions => {:tracker_id => tracker.id}) if tracker
343 scope = scope.scoped(:conditions => {:tracker_id => tracker.id}) if tracker
359
344
360 issues_by_assignee = scope.all(:include => [:status, :assigned_to, :project, :tracker]).group_by(&:assigned_to)
345 issues_by_assignee = scope.all(:include => [:status, :assigned_to, :project, :tracker]).group_by(&:assigned_to)
361 issues_by_assignee.each do |assignee, issues|
346 issues_by_assignee.each do |assignee, issues|
362 deliver_reminder(assignee, issues, days) if assignee.is_a?(User) && assignee.active?
347 deliver_reminder(assignee, issues, days) if assignee.is_a?(User) && assignee.active?
363 end
348 end
364 end
349 end
365
350
366 # Activates/desactivates email deliveries during +block+
351 # Activates/desactivates email deliveries during +block+
367 def self.with_deliveries(enabled = true, &block)
352 def self.with_deliveries(enabled = true, &block)
368 was_enabled = ActionMailer::Base.perform_deliveries
353 was_enabled = ActionMailer::Base.perform_deliveries
369 ActionMailer::Base.perform_deliveries = !!enabled
354 ActionMailer::Base.perform_deliveries = !!enabled
370 yield
355 yield
371 ensure
356 ensure
372 ActionMailer::Base.perform_deliveries = was_enabled
357 ActionMailer::Base.perform_deliveries = was_enabled
373 end
358 end
374
359
375 # Sends emails synchronously in the given block
360 # Sends emails synchronously in the given block
376 def self.with_synched_deliveries(&block)
361 def self.with_synched_deliveries(&block)
377 saved_method = ActionMailer::Base.delivery_method
362 saved_method = ActionMailer::Base.delivery_method
378 if m = saved_method.to_s.match(%r{^async_(.+)$})
363 if m = saved_method.to_s.match(%r{^async_(.+)$})
379 ActionMailer::Base.delivery_method = m[1].to_sym
364 ActionMailer::Base.delivery_method = m[1].to_sym
380 end
365 end
381 yield
366 yield
382 ensure
367 ensure
383 ActionMailer::Base.delivery_method = saved_method
368 ActionMailer::Base.delivery_method = saved_method
384 end
369 end
385
370
386 private
371 def mail(headers={})
387 def initialize_defaults(method_name)
372 headers.merge! 'X-Mailer' => 'Redmine',
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',
395 'X-Redmine-Host' => Setting.host_name,
373 'X-Redmine-Host' => Setting.host_name,
396 'X-Redmine-Site' => Setting.app_title,
374 'X-Redmine-Site' => Setting.app_title,
397 'X-Auto-Response-Suppress' => 'OOF',
375 'X-Auto-Response-Suppress' => 'OOF',
398 'Auto-Submitted' => 'auto-generated'
376 'Auto-Submitted' => 'auto-generated',
399 end
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 # Removes the author from the recipients and cc
379 # Removes the author from the recipients and cc
409 # if he doesn't want to receive notifications about what he does
380 # if he doesn't want to receive notifications about what he does
410 if @author && @author.logged? && @author.pref[:no_self_notified]
381 if @author && @author.logged? && @author.pref[:no_self_notified]
411 if recipients
382 headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
412 recipients((recipients.is_a?(Array) ? recipients : [recipients]) - [@author.mail])
383 headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
413 end
414 if cc
415 cc((cc.is_a?(Array) ? cc : [cc]) - [@author.mail])
416 end
417 end
384 end
418
385
419 if @author && @author.logged?
386 if @author && @author.logged?
420 redmine_headers 'Sender' => @author.login
387 redmine_headers 'Sender' => @author.login
421 end
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 # Blind carbon copy recipients
390 # Blind carbon copy recipients
428 if Setting.bcc_recipients?
391 if Setting.bcc_recipients?
429 bcc(notified_users)
392 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
430 recipients []
393 headers[:to] = nil
431 cc []
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 end
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 super
420 super
434 end
421 end
435
422
436 # Rails 2.3 has problems rendering implicit multipart messages with
423 def self.method_missing(method, *args, &block)
437 # layouts so this method will wrap an multipart messages with
424 if m = method.to_s.match(%r{^deliver_(.+)$})
438 # explicit parts.
425 send(m[1], *args).deliver
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')
449 else
426 else
450 content_type "multipart/alternative"
427 super
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)
456 end
428 end
457 end
429 end
458
430
459 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
431 private
460 def self.controller_path
432
461 ''
433 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
462 end unless respond_to?('controller_path')
434 def redmine_headers(h)
435 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
436 end
463
437
464 # Returns a predictable Message-Id for the given object
438 # Returns a predictable Message-Id for the given object
465 def self.message_id_for(object)
439 def self.message_id_for(object)
466 # id + timestamp should reduce the odds of a collision
440 # id + timestamp should reduce the odds of a collision
467 # as far as we don't send multiple emails for the same object
441 # as far as we don't send multiple emails for the same object
468 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
442 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
469 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
443 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
470 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
444 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
471 host = "#{::Socket.gethostname}.redmine" if host.empty?
445 host = "#{::Socket.gethostname}.redmine" if host.empty?
472 "<#{hash}@#{host}>"
446 "#{hash}@#{host}"
473 end
447 end
474
448
475 private
476
477 def message_id(object)
449 def message_id(object)
478 @message_id_object = object
450 @message_id_object = object
479 end
451 end
480
452
481 def references(object)
453 def references(object)
482 @references_objects ||= []
454 @references_objects ||= []
483 @references_objects << object
455 @references_objects << object
484 end
456 end
485
457
486 def mylogger
458 def mylogger
487 Rails.logger
459 Rails.logger
488 end
460 end
489 end
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 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author
46 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :conditions => ["is_default = ?", true]
49 has_one :repository, :conditions => ["is_default = ?", true]
50 has_many :repositories, :dependent => :destroy
50 has_many :repositories, :dependent => :destroy
51 has_many :changesets, :through => :repository
51 has_many :changesets, :through => :repository
52 has_one :wiki, :dependent => :destroy
52 has_one :wiki, :dependent => :destroy
53 # Custom field for the project issues
53 # Custom field for the project issues
54 has_and_belongs_to_many :issue_custom_fields,
54 has_and_belongs_to_many :issue_custom_fields,
55 :class_name => 'IssueCustomField',
55 :class_name => 'IssueCustomField',
56 :order => "#{CustomField.table_name}.position",
56 :order => "#{CustomField.table_name}.position",
57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :association_foreign_key => 'custom_field_id'
58 :association_foreign_key => 'custom_field_id'
59
59
60 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_attachable :view_permission => :view_files,
61 acts_as_attachable :view_permission => :view_files,
62 :delete_permission => :manage_files
62 :delete_permission => :manage_files
63
63
64 acts_as_customizable
64 acts_as_customizable
65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :author => nil
68 :author => nil
69
69
70 attr_protected :status
70 attr_protected :status
71
71
72 validates_presence_of :name, :identifier
72 validates_presence_of :name, :identifier
73 validates_uniqueness_of :identifier
73 validates_uniqueness_of :identifier
74 validates_associated :repository, :wiki
74 validates_associated :repository, :wiki
75 validates_length_of :name, :maximum => 255
75 validates_length_of :name, :maximum => 255
76 validates_length_of :homepage, :maximum => 255
76 validates_length_of :homepage, :maximum => 255
77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 # donwcase letters, digits, dashes but not digits only
78 # donwcase letters, digits, dashes but not digits only
79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
80 # reserved words
80 # reserved words
81 validates_exclusion_of :identifier, :in => %w( new )
81 validates_exclusion_of :identifier, :in => %w( new )
82
82
83 before_destroy :delete_all_members
83 before_destroy :delete_all_members
84
84
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] } }
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 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
88 named_scope :all_public, { :conditions => { :is_public => true } }
88 named_scope :all_public, { :conditions => { :is_public => true } }
89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
90 named_scope :allowed_to, lambda {|*args|
90 named_scope :allowed_to, lambda {|*args|
91 user = User.current
91 user = User.current
92 permission = nil
92 permission = nil
93 if args.first.is_a?(Symbol)
93 if args.first.is_a?(Symbol)
94 permission = args.shift
94 permission = args.shift
95 else
95 else
96 user = args.shift
96 user = args.shift
97 permission = args.shift
97 permission = args.shift
98 end
98 end
99 { :conditions => Project.allowed_to_condition(user, permission, *args) }
99 { :conditions => Project.allowed_to_condition(user, permission, *args) }
100 }
100 }
101 named_scope :like, lambda {|arg|
101 named_scope :like, lambda {|arg|
102 if arg.blank?
102 if arg.blank?
103 {}
103 {}
104 else
104 else
105 pattern = "%#{arg.to_s.strip.downcase}%"
105 pattern = "%#{arg.to_s.strip.downcase}%"
106 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
106 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
107 end
107 end
108 }
108 }
109
109
110 def initialize(attributes=nil, *args)
110 def initialize(attributes=nil, *args)
111 super
111 super
112
112
113 initialized = (attributes || {}).stringify_keys
113 initialized = (attributes || {}).stringify_keys
114 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
114 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
115 self.identifier = Project.next_identifier
115 self.identifier = Project.next_identifier
116 end
116 end
117 if !initialized.key?('is_public')
117 if !initialized.key?('is_public')
118 self.is_public = Setting.default_projects_public?
118 self.is_public = Setting.default_projects_public?
119 end
119 end
120 if !initialized.key?('enabled_module_names')
120 if !initialized.key?('enabled_module_names')
121 self.enabled_module_names = Setting.default_projects_modules
121 self.enabled_module_names = Setting.default_projects_modules
122 end
122 end
123 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
123 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
124 self.trackers = Tracker.all
124 self.trackers = Tracker.all
125 end
125 end
126 end
126 end
127
127
128 def identifier=(identifier)
128 def identifier=(identifier)
129 super unless identifier_frozen?
129 super unless identifier_frozen?
130 end
130 end
131
131
132 def identifier_frozen?
132 def identifier_frozen?
133 errors[:identifier].nil? && !(new_record? || identifier.blank?)
133 errors[:identifier].nil? && !(new_record? || identifier.blank?)
134 end
134 end
135
135
136 # returns latest created projects
136 # returns latest created projects
137 # non public projects will be returned only if user is a member of those
137 # non public projects will be returned only if user is a member of those
138 def self.latest(user=nil, count=5)
138 def self.latest(user=nil, count=5)
139 visible(user).find(:all, :limit => count, :order => "created_on DESC")
139 visible(user).find(:all, :limit => count, :order => "created_on DESC")
140 end
140 end
141
141
142 # Returns true if the project is visible to +user+ or to the current user.
142 # Returns true if the project is visible to +user+ or to the current user.
143 def visible?(user=User.current)
143 def visible?(user=User.current)
144 user.allowed_to?(:view_project, self)
144 user.allowed_to?(:view_project, self)
145 end
145 end
146
146
147 # Returns a SQL conditions string used to find all projects visible by the specified user.
147 # Returns a SQL conditions string used to find all projects visible by the specified user.
148 #
148 #
149 # Examples:
149 # Examples:
150 # Project.visible_condition(admin) => "projects.status = 1"
150 # Project.visible_condition(admin) => "projects.status = 1"
151 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
151 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
152 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
152 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
153 def self.visible_condition(user, options={})
153 def self.visible_condition(user, options={})
154 allowed_to_condition(user, :view_project, options)
154 allowed_to_condition(user, :view_project, options)
155 end
155 end
156
156
157 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
157 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
158 #
158 #
159 # Valid options:
159 # Valid options:
160 # * :project => limit the condition to project
160 # * :project => limit the condition to project
161 # * :with_subprojects => limit the condition to project and its subprojects
161 # * :with_subprojects => limit the condition to project and its subprojects
162 # * :member => limit the condition to the user projects
162 # * :member => limit the condition to the user projects
163 def self.allowed_to_condition(user, permission, options={})
163 def self.allowed_to_condition(user, permission, options={})
164 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
164 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
165 if perm = Redmine::AccessControl.permission(permission)
165 if perm = Redmine::AccessControl.permission(permission)
166 unless perm.project_module.nil?
166 unless perm.project_module.nil?
167 # If the permission belongs to a project module, make sure the module is enabled
167 # If the permission belongs to a project module, make sure the module is enabled
168 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
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 end
169 end
170 end
170 end
171 if options[:project]
171 if options[:project]
172 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
172 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
173 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
173 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
174 base_statement = "(#{project_statement}) AND (#{base_statement})"
174 base_statement = "(#{project_statement}) AND (#{base_statement})"
175 end
175 end
176
176
177 if user.admin?
177 if user.admin?
178 base_statement
178 base_statement
179 else
179 else
180 statement_by_role = {}
180 statement_by_role = {}
181 unless options[:member]
181 unless options[:member]
182 role = user.logged? ? Role.non_member : Role.anonymous
182 role = user.logged? ? Role.non_member : Role.anonymous
183 if role.allowed_to?(permission)
183 if role.allowed_to?(permission)
184 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
184 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
185 end
185 end
186 end
186 end
187 if user.logged?
187 if user.logged?
188 user.projects_by_role.each do |role, projects|
188 user.projects_by_role.each do |role, projects|
189 if role.allowed_to?(permission)
189 if role.allowed_to?(permission)
190 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
190 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
191 end
191 end
192 end
192 end
193 end
193 end
194 if statement_by_role.empty?
194 if statement_by_role.empty?
195 "1=0"
195 "1=0"
196 else
196 else
197 if block_given?
197 if block_given?
198 statement_by_role.each do |role, statement|
198 statement_by_role.each do |role, statement|
199 if s = yield(role, user)
199 if s = yield(role, user)
200 statement_by_role[role] = "(#{statement} AND (#{s}))"
200 statement_by_role[role] = "(#{statement} AND (#{s}))"
201 end
201 end
202 end
202 end
203 end
203 end
204 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
204 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
205 end
205 end
206 end
206 end
207 end
207 end
208
208
209 # Returns the Systemwide and project specific activities
209 # Returns the Systemwide and project specific activities
210 def activities(include_inactive=false)
210 def activities(include_inactive=false)
211 if include_inactive
211 if include_inactive
212 return all_activities
212 return all_activities
213 else
213 else
214 return active_activities
214 return active_activities
215 end
215 end
216 end
216 end
217
217
218 # Will create a new Project specific Activity or update an existing one
218 # Will create a new Project specific Activity or update an existing one
219 #
219 #
220 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
220 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
221 # does not successfully save.
221 # does not successfully save.
222 def update_or_create_time_entry_activity(id, activity_hash)
222 def update_or_create_time_entry_activity(id, activity_hash)
223 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
223 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
224 self.create_time_entry_activity_if_needed(activity_hash)
224 self.create_time_entry_activity_if_needed(activity_hash)
225 else
225 else
226 activity = project.time_entry_activities.find_by_id(id.to_i)
226 activity = project.time_entry_activities.find_by_id(id.to_i)
227 activity.update_attributes(activity_hash) if activity
227 activity.update_attributes(activity_hash) if activity
228 end
228 end
229 end
229 end
230
230
231 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
231 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
232 #
232 #
233 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
233 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
234 # does not successfully save.
234 # does not successfully save.
235 def create_time_entry_activity_if_needed(activity)
235 def create_time_entry_activity_if_needed(activity)
236 if activity['parent_id']
236 if activity['parent_id']
237
237
238 parent_activity = TimeEntryActivity.find(activity['parent_id'])
238 parent_activity = TimeEntryActivity.find(activity['parent_id'])
239 activity['name'] = parent_activity.name
239 activity['name'] = parent_activity.name
240 activity['position'] = parent_activity.position
240 activity['position'] = parent_activity.position
241
241
242 if Enumeration.overridding_change?(activity, parent_activity)
242 if Enumeration.overridding_change?(activity, parent_activity)
243 project_activity = self.time_entry_activities.create(activity)
243 project_activity = self.time_entry_activities.create(activity)
244
244
245 if project_activity.new_record?
245 if project_activity.new_record?
246 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
246 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
247 else
247 else
248 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
248 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
249 end
249 end
250 end
250 end
251 end
251 end
252 end
252 end
253
253
254 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
254 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
255 #
255 #
256 # Examples:
256 # Examples:
257 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
257 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
258 # project.project_condition(false) => "projects.id = 1"
258 # project.project_condition(false) => "projects.id = 1"
259 def project_condition(with_subprojects)
259 def project_condition(with_subprojects)
260 cond = "#{Project.table_name}.id = #{id}"
260 cond = "#{Project.table_name}.id = #{id}"
261 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
261 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
262 cond
262 cond
263 end
263 end
264
264
265 def self.find(*args)
265 def self.find(*args)
266 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
266 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
267 project = find_by_identifier(*args)
267 project = find_by_identifier(*args)
268 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
268 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
269 project
269 project
270 else
270 else
271 super
271 super
272 end
272 end
273 end
273 end
274
274
275 def self.find_by_param(*args)
276 self.find(*args)
277 end
278
275 def reload(*args)
279 def reload(*args)
276 @shared_versions = nil
280 @shared_versions = nil
277 @rolled_up_versions = nil
281 @rolled_up_versions = nil
278 @rolled_up_trackers = nil
282 @rolled_up_trackers = nil
279 @all_issue_custom_fields = nil
283 @all_issue_custom_fields = nil
280 @all_time_entry_custom_fields = nil
284 @all_time_entry_custom_fields = nil
281 @to_param = nil
285 @to_param = nil
282 @allowed_parents = nil
286 @allowed_parents = nil
283 @allowed_permissions = nil
287 @allowed_permissions = nil
284 @actions_allowed = nil
288 @actions_allowed = nil
285 super
289 super
286 end
290 end
287
291
288 def to_param
292 def to_param
289 # id is used for projects with a numeric identifier (compatibility)
293 # id is used for projects with a numeric identifier (compatibility)
290 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
294 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
291 end
295 end
292
296
293 def active?
297 def active?
294 self.status == STATUS_ACTIVE
298 self.status == STATUS_ACTIVE
295 end
299 end
296
300
297 def archived?
301 def archived?
298 self.status == STATUS_ARCHIVED
302 self.status == STATUS_ARCHIVED
299 end
303 end
300
304
301 # Archives the project and its descendants
305 # Archives the project and its descendants
302 def archive
306 def archive
303 # Check that there is no issue of a non descendant project that is assigned
307 # Check that there is no issue of a non descendant project that is assigned
304 # to one of the project or descendant versions
308 # to one of the project or descendant versions
305 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
309 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
306 if v_ids.any? && Issue.find(:first, :include => :project,
310 if v_ids.any? && Issue.find(:first, :include => :project,
307 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
311 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
308 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
312 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
309 return false
313 return false
310 end
314 end
311 Project.transaction do
315 Project.transaction do
312 archive!
316 archive!
313 end
317 end
314 true
318 true
315 end
319 end
316
320
317 # Unarchives the project
321 # Unarchives the project
318 # All its ancestors must be active
322 # All its ancestors must be active
319 def unarchive
323 def unarchive
320 return false if ancestors.detect {|a| !a.active?}
324 return false if ancestors.detect {|a| !a.active?}
321 update_attribute :status, STATUS_ACTIVE
325 update_attribute :status, STATUS_ACTIVE
322 end
326 end
323
327
324 # Returns an array of projects the project can be moved to
328 # Returns an array of projects the project can be moved to
325 # by the current user
329 # by the current user
326 def allowed_parents
330 def allowed_parents
327 return @allowed_parents if @allowed_parents
331 return @allowed_parents if @allowed_parents
328 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
332 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
329 @allowed_parents = @allowed_parents - self_and_descendants
333 @allowed_parents = @allowed_parents - self_and_descendants
330 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
334 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
331 @allowed_parents << nil
335 @allowed_parents << nil
332 end
336 end
333 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
337 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
334 @allowed_parents << parent
338 @allowed_parents << parent
335 end
339 end
336 @allowed_parents
340 @allowed_parents
337 end
341 end
338
342
339 # Sets the parent of the project with authorization check
343 # Sets the parent of the project with authorization check
340 def set_allowed_parent!(p)
344 def set_allowed_parent!(p)
341 unless p.nil? || p.is_a?(Project)
345 unless p.nil? || p.is_a?(Project)
342 if p.to_s.blank?
346 if p.to_s.blank?
343 p = nil
347 p = nil
344 else
348 else
345 p = Project.find_by_id(p)
349 p = Project.find_by_id(p)
346 return false unless p
350 return false unless p
347 end
351 end
348 end
352 end
349 if p.nil?
353 if p.nil?
350 if !new_record? && allowed_parents.empty?
354 if !new_record? && allowed_parents.empty?
351 return false
355 return false
352 end
356 end
353 elsif !allowed_parents.include?(p)
357 elsif !allowed_parents.include?(p)
354 return false
358 return false
355 end
359 end
356 set_parent!(p)
360 set_parent!(p)
357 end
361 end
358
362
359 # Sets the parent of the project
363 # Sets the parent of the project
360 # Argument can be either a Project, a String, a Fixnum or nil
364 # Argument can be either a Project, a String, a Fixnum or nil
361 def set_parent!(p)
365 def set_parent!(p)
362 unless p.nil? || p.is_a?(Project)
366 unless p.nil? || p.is_a?(Project)
363 if p.to_s.blank?
367 if p.to_s.blank?
364 p = nil
368 p = nil
365 else
369 else
366 p = Project.find_by_id(p)
370 p = Project.find_by_id(p)
367 return false unless p
371 return false unless p
368 end
372 end
369 end
373 end
370 if p == parent && !p.nil?
374 if p == parent && !p.nil?
371 # Nothing to do
375 # Nothing to do
372 true
376 true
373 elsif p.nil? || (p.active? && move_possible?(p))
377 elsif p.nil? || (p.active? && move_possible?(p))
374 # Insert the project so that target's children or root projects stay alphabetically sorted
378 # Insert the project so that target's children or root projects stay alphabetically sorted
375 sibs = (p.nil? ? self.class.roots : p.children)
379 sibs = (p.nil? ? self.class.roots : p.children)
376 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
380 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
377 if to_be_inserted_before
381 if to_be_inserted_before
378 move_to_left_of(to_be_inserted_before)
382 move_to_left_of(to_be_inserted_before)
379 elsif p.nil?
383 elsif p.nil?
380 if sibs.empty?
384 if sibs.empty?
381 # move_to_root adds the project in first (ie. left) position
385 # move_to_root adds the project in first (ie. left) position
382 move_to_root
386 move_to_root
383 else
387 else
384 move_to_right_of(sibs.last) unless self == sibs.last
388 move_to_right_of(sibs.last) unless self == sibs.last
385 end
389 end
386 else
390 else
387 # move_to_child_of adds the project in last (ie.right) position
391 # move_to_child_of adds the project in last (ie.right) position
388 move_to_child_of(p)
392 move_to_child_of(p)
389 end
393 end
390 Issue.update_versions_from_hierarchy_change(self)
394 Issue.update_versions_from_hierarchy_change(self)
391 true
395 true
392 else
396 else
393 # Can not move to the given target
397 # Can not move to the given target
394 false
398 false
395 end
399 end
396 end
400 end
397
401
398 # Returns an array of the trackers used by the project and its active sub projects
402 # Returns an array of the trackers used by the project and its active sub projects
399 def rolled_up_trackers
403 def rolled_up_trackers
400 @rolled_up_trackers ||=
404 @rolled_up_trackers ||=
401 Tracker.find(:all, :joins => :projects,
405 Tracker.find(:all, :joins => :projects,
402 :select => "DISTINCT #{Tracker.table_name}.*",
406 :select => "DISTINCT #{Tracker.table_name}.*",
403 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
407 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
404 :order => "#{Tracker.table_name}.position")
408 :order => "#{Tracker.table_name}.position")
405 end
409 end
406
410
407 # Closes open and locked project versions that are completed
411 # Closes open and locked project versions that are completed
408 def close_completed_versions
412 def close_completed_versions
409 Version.transaction do
413 Version.transaction do
410 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
414 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
411 if version.completed?
415 if version.completed?
412 version.update_attribute(:status, 'closed')
416 version.update_attribute(:status, 'closed')
413 end
417 end
414 end
418 end
415 end
419 end
416 end
420 end
417
421
418 # Returns a scope of the Versions on subprojects
422 # Returns a scope of the Versions on subprojects
419 def rolled_up_versions
423 def rolled_up_versions
420 @rolled_up_versions ||=
424 @rolled_up_versions ||=
421 Version.scoped(:include => :project,
425 Version.scoped(:include => :project,
422 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
426 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
423 end
427 end
424
428
425 # Returns a scope of the Versions used by the project
429 # Returns a scope of the Versions used by the project
426 def shared_versions
430 def shared_versions
427 if new_record?
431 if new_record?
428 Version.scoped(:include => :project,
432 Version.scoped(:include => :project,
429 :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
433 :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
430 else
434 else
431 @shared_versions ||= begin
435 @shared_versions ||= begin
432 r = root? ? self : root
436 r = root? ? self : root
433 Version.scoped(:include => :project,
437 Version.scoped(:include => :project,
434 :conditions => "#{Project.table_name}.id = #{id}" +
438 :conditions => "#{Project.table_name}.id = #{id}" +
435 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
439 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
436 " #{Version.table_name}.sharing = 'system'" +
440 " #{Version.table_name}.sharing = 'system'" +
437 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
441 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
438 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
442 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
439 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
443 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
440 "))")
444 "))")
441 end
445 end
442 end
446 end
443 end
447 end
444
448
445 # Returns a hash of project users grouped by role
449 # Returns a hash of project users grouped by role
446 def users_by_role
450 def users_by_role
447 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
451 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
448 m.roles.each do |r|
452 m.roles.each do |r|
449 h[r] ||= []
453 h[r] ||= []
450 h[r] << m.user
454 h[r] << m.user
451 end
455 end
452 h
456 h
453 end
457 end
454 end
458 end
455
459
456 # Deletes all project's members
460 # Deletes all project's members
457 def delete_all_members
461 def delete_all_members
458 me, mr = Member.table_name, MemberRole.table_name
462 me, mr = Member.table_name, MemberRole.table_name
459 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
463 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
460 Member.delete_all(['project_id = ?', id])
464 Member.delete_all(['project_id = ?', id])
461 end
465 end
462
466
463 # Users/groups issues can be assigned to
467 # Users/groups issues can be assigned to
464 def assignable_users
468 def assignable_users
465 assignable = Setting.issue_group_assignment? ? member_principals : members
469 assignable = Setting.issue_group_assignment? ? member_principals : members
466 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
470 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
467 end
471 end
468
472
469 # Returns the mail adresses of users that should be always notified on project events
473 # Returns the mail adresses of users that should be always notified on project events
470 def recipients
474 def recipients
471 notified_users.collect {|user| user.mail}
475 notified_users.collect {|user| user.mail}
472 end
476 end
473
477
474 # Returns the users that should be notified on project events
478 # Returns the users that should be notified on project events
475 def notified_users
479 def notified_users
476 # TODO: User part should be extracted to User#notify_about?
480 # TODO: User part should be extracted to User#notify_about?
477 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
481 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
478 end
482 end
479
483
480 # Returns an array of all custom fields enabled for project issues
484 # Returns an array of all custom fields enabled for project issues
481 # (explictly associated custom fields and custom fields enabled for all projects)
485 # (explictly associated custom fields and custom fields enabled for all projects)
482 def all_issue_custom_fields
486 def all_issue_custom_fields
483 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
487 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
484 end
488 end
485
489
486 # Returns an array of all custom fields enabled for project time entries
490 # Returns an array of all custom fields enabled for project time entries
487 # (explictly associated custom fields and custom fields enabled for all projects)
491 # (explictly associated custom fields and custom fields enabled for all projects)
488 def all_time_entry_custom_fields
492 def all_time_entry_custom_fields
489 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
493 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
490 end
494 end
491
495
492 def project
496 def project
493 self
497 self
494 end
498 end
495
499
496 def <=>(project)
500 def <=>(project)
497 name.downcase <=> project.name.downcase
501 name.downcase <=> project.name.downcase
498 end
502 end
499
503
500 def to_s
504 def to_s
501 name
505 name
502 end
506 end
503
507
504 # Returns a short description of the projects (first lines)
508 # Returns a short description of the projects (first lines)
505 def short_description(length = 255)
509 def short_description(length = 255)
506 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
510 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
507 end
511 end
508
512
509 def css_classes
513 def css_classes
510 s = 'project'
514 s = 'project'
511 s << ' root' if root?
515 s << ' root' if root?
512 s << ' child' if child?
516 s << ' child' if child?
513 s << (leaf? ? ' leaf' : ' parent')
517 s << (leaf? ? ' leaf' : ' parent')
514 s
518 s
515 end
519 end
516
520
517 # The earliest start date of a project, based on it's issues and versions
521 # The earliest start date of a project, based on it's issues and versions
518 def start_date
522 def start_date
519 [
523 [
520 issues.minimum('start_date'),
524 issues.minimum('start_date'),
521 shared_versions.collect(&:effective_date),
525 shared_versions.collect(&:effective_date),
522 shared_versions.collect(&:start_date)
526 shared_versions.collect(&:start_date)
523 ].flatten.compact.min
527 ].flatten.compact.min
524 end
528 end
525
529
526 # The latest due date of an issue or version
530 # The latest due date of an issue or version
527 def due_date
531 def due_date
528 [
532 [
529 issues.maximum('due_date'),
533 issues.maximum('due_date'),
530 shared_versions.collect(&:effective_date),
534 shared_versions.collect(&:effective_date),
531 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
535 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
532 ].flatten.compact.max
536 ].flatten.compact.max
533 end
537 end
534
538
535 def overdue?
539 def overdue?
536 active? && !due_date.nil? && (due_date < Date.today)
540 active? && !due_date.nil? && (due_date < Date.today)
537 end
541 end
538
542
539 # Returns the percent completed for this project, based on the
543 # Returns the percent completed for this project, based on the
540 # progress on it's versions.
544 # progress on it's versions.
541 def completed_percent(options={:include_subprojects => false})
545 def completed_percent(options={:include_subprojects => false})
542 if options.delete(:include_subprojects)
546 if options.delete(:include_subprojects)
543 total = self_and_descendants.collect(&:completed_percent).sum
547 total = self_and_descendants.collect(&:completed_percent).sum
544
548
545 total / self_and_descendants.count
549 total / self_and_descendants.count
546 else
550 else
547 if versions.count > 0
551 if versions.count > 0
548 total = versions.collect(&:completed_pourcent).sum
552 total = versions.collect(&:completed_pourcent).sum
549
553
550 total / versions.count
554 total / versions.count
551 else
555 else
552 100
556 100
553 end
557 end
554 end
558 end
555 end
559 end
556
560
557 # Return true if this project is allowed to do the specified action.
561 # Return true if this project is allowed to do the specified action.
558 # action can be:
562 # action can be:
559 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
563 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
560 # * a permission Symbol (eg. :edit_project)
564 # * a permission Symbol (eg. :edit_project)
561 def allows_to?(action)
565 def allows_to?(action)
562 if action.is_a? Hash
566 if action.is_a? Hash
563 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
567 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
564 else
568 else
565 allowed_permissions.include? action
569 allowed_permissions.include? action
566 end
570 end
567 end
571 end
568
572
569 def module_enabled?(module_name)
573 def module_enabled?(module_name)
570 module_name = module_name.to_s
574 module_name = module_name.to_s
571 enabled_modules.detect {|m| m.name == module_name}
575 enabled_modules.detect {|m| m.name == module_name}
572 end
576 end
573
577
574 def enabled_module_names=(module_names)
578 def enabled_module_names=(module_names)
575 if module_names && module_names.is_a?(Array)
579 if module_names && module_names.is_a?(Array)
576 module_names = module_names.collect(&:to_s).reject(&:blank?)
580 module_names = module_names.collect(&:to_s).reject(&:blank?)
577 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
581 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
578 else
582 else
579 enabled_modules.clear
583 enabled_modules.clear
580 end
584 end
581 end
585 end
582
586
583 # Returns an array of the enabled modules names
587 # Returns an array of the enabled modules names
584 def enabled_module_names
588 def enabled_module_names
585 enabled_modules.collect(&:name)
589 enabled_modules.collect(&:name)
586 end
590 end
587
591
588 # Enable a specific module
592 # Enable a specific module
589 #
593 #
590 # Examples:
594 # Examples:
591 # project.enable_module!(:issue_tracking)
595 # project.enable_module!(:issue_tracking)
592 # project.enable_module!("issue_tracking")
596 # project.enable_module!("issue_tracking")
593 def enable_module!(name)
597 def enable_module!(name)
594 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
598 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
595 end
599 end
596
600
597 # Disable a module if it exists
601 # Disable a module if it exists
598 #
602 #
599 # Examples:
603 # Examples:
600 # project.disable_module!(:issue_tracking)
604 # project.disable_module!(:issue_tracking)
601 # project.disable_module!("issue_tracking")
605 # project.disable_module!("issue_tracking")
602 # project.disable_module!(project.enabled_modules.first)
606 # project.disable_module!(project.enabled_modules.first)
603 def disable_module!(target)
607 def disable_module!(target)
604 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
608 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
605 target.destroy unless target.blank?
609 target.destroy unless target.blank?
606 end
610 end
607
611
608 safe_attributes 'name',
612 safe_attributes 'name',
609 'description',
613 'description',
610 'homepage',
614 'homepage',
611 'is_public',
615 'is_public',
612 'identifier',
616 'identifier',
613 'custom_field_values',
617 'custom_field_values',
614 'custom_fields',
618 'custom_fields',
615 'tracker_ids',
619 'tracker_ids',
616 'issue_custom_field_ids'
620 'issue_custom_field_ids'
617
621
618 safe_attributes 'enabled_module_names',
622 safe_attributes 'enabled_module_names',
619 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
623 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
620
624
621 # Returns an array of projects that are in this project's hierarchy
625 # Returns an array of projects that are in this project's hierarchy
622 #
626 #
623 # Example: parents, children, siblings
627 # Example: parents, children, siblings
624 def hierarchy
628 def hierarchy
625 parents = project.self_and_ancestors || []
629 parents = project.self_and_ancestors || []
626 descendants = project.descendants || []
630 descendants = project.descendants || []
627 project_hierarchy = parents | descendants # Set union
631 project_hierarchy = parents | descendants # Set union
628 end
632 end
629
633
630 # Returns an auto-generated project identifier based on the last identifier used
634 # Returns an auto-generated project identifier based on the last identifier used
631 def self.next_identifier
635 def self.next_identifier
632 p = Project.find(:first, :order => 'created_on DESC')
636 p = Project.find(:first, :order => 'created_on DESC')
633 p.nil? ? nil : p.identifier.to_s.succ
637 p.nil? ? nil : p.identifier.to_s.succ
634 end
638 end
635
639
636 # Copies and saves the Project instance based on the +project+.
640 # Copies and saves the Project instance based on the +project+.
637 # Duplicates the source project's:
641 # Duplicates the source project's:
638 # * Wiki
642 # * Wiki
639 # * Versions
643 # * Versions
640 # * Categories
644 # * Categories
641 # * Issues
645 # * Issues
642 # * Members
646 # * Members
643 # * Queries
647 # * Queries
644 #
648 #
645 # Accepts an +options+ argument to specify what to copy
649 # Accepts an +options+ argument to specify what to copy
646 #
650 #
647 # Examples:
651 # Examples:
648 # project.copy(1) # => copies everything
652 # project.copy(1) # => copies everything
649 # project.copy(1, :only => 'members') # => copies members only
653 # project.copy(1, :only => 'members') # => copies members only
650 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
654 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
651 def copy(project, options={})
655 def copy(project, options={})
652 project = project.is_a?(Project) ? project : Project.find(project)
656 project = project.is_a?(Project) ? project : Project.find(project)
653
657
654 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
658 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
655 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
659 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
656
660
657 Project.transaction do
661 Project.transaction do
658 if save
662 if save
659 reload
663 reload
660 to_be_copied.each do |name|
664 to_be_copied.each do |name|
661 send "copy_#{name}", project
665 send "copy_#{name}", project
662 end
666 end
663 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
667 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
664 save
668 save
665 end
669 end
666 end
670 end
667 end
671 end
668
672
669
673
670 # Copies +project+ and returns the new instance. This will not save
674 # Copies +project+ and returns the new instance. This will not save
671 # the copy
675 # the copy
672 def self.copy_from(project)
676 def self.copy_from(project)
673 begin
677 begin
674 project = project.is_a?(Project) ? project : Project.find(project)
678 project = project.is_a?(Project) ? project : Project.find(project)
675 if project
679 if project
676 # clear unique attributes
680 # clear unique attributes
677 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
681 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
678 copy = Project.new(attributes)
682 copy = Project.new(attributes)
679 copy.enabled_modules = project.enabled_modules
683 copy.enabled_modules = project.enabled_modules
680 copy.trackers = project.trackers
684 copy.trackers = project.trackers
681 copy.custom_values = project.custom_values.collect {|v| v.clone}
685 copy.custom_values = project.custom_values.collect {|v| v.clone}
682 copy.issue_custom_fields = project.issue_custom_fields
686 copy.issue_custom_fields = project.issue_custom_fields
683 return copy
687 return copy
684 else
688 else
685 return nil
689 return nil
686 end
690 end
687 rescue ActiveRecord::RecordNotFound
691 rescue ActiveRecord::RecordNotFound
688 return nil
692 return nil
689 end
693 end
690 end
694 end
691
695
692 # Yields the given block for each project with its level in the tree
696 # Yields the given block for each project with its level in the tree
693 def self.project_tree(projects, &block)
697 def self.project_tree(projects, &block)
694 ancestors = []
698 ancestors = []
695 projects.sort_by(&:lft).each do |project|
699 projects.sort_by(&:lft).each do |project|
696 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
700 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
697 ancestors.pop
701 ancestors.pop
698 end
702 end
699 yield project, ancestors.size
703 yield project, ancestors.size
700 ancestors << project
704 ancestors << project
701 end
705 end
702 end
706 end
703
707
704 private
708 private
705
709
706 # Copies wiki from +project+
710 # Copies wiki from +project+
707 def copy_wiki(project)
711 def copy_wiki(project)
708 # Check that the source project has a wiki first
712 # Check that the source project has a wiki first
709 unless project.wiki.nil?
713 unless project.wiki.nil?
710 self.wiki ||= Wiki.new
714 self.wiki ||= Wiki.new
711 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
715 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
712 wiki_pages_map = {}
716 wiki_pages_map = {}
713 project.wiki.pages.each do |page|
717 project.wiki.pages.each do |page|
714 # Skip pages without content
718 # Skip pages without content
715 next if page.content.nil?
719 next if page.content.nil?
716 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
720 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
717 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
721 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
718 new_wiki_page.content = new_wiki_content
722 new_wiki_page.content = new_wiki_content
719 wiki.pages << new_wiki_page
723 wiki.pages << new_wiki_page
720 wiki_pages_map[page.id] = new_wiki_page
724 wiki_pages_map[page.id] = new_wiki_page
721 end
725 end
722 wiki.save
726 wiki.save
723 # Reproduce page hierarchy
727 # Reproduce page hierarchy
724 project.wiki.pages.each do |page|
728 project.wiki.pages.each do |page|
725 if page.parent_id && wiki_pages_map[page.id]
729 if page.parent_id && wiki_pages_map[page.id]
726 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
730 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
727 wiki_pages_map[page.id].save
731 wiki_pages_map[page.id].save
728 end
732 end
729 end
733 end
730 end
734 end
731 end
735 end
732
736
733 # Copies versions from +project+
737 # Copies versions from +project+
734 def copy_versions(project)
738 def copy_versions(project)
735 project.versions.each do |version|
739 project.versions.each do |version|
736 new_version = Version.new
740 new_version = Version.new
737 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
741 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
738 self.versions << new_version
742 self.versions << new_version
739 end
743 end
740 end
744 end
741
745
742 # Copies issue categories from +project+
746 # Copies issue categories from +project+
743 def copy_issue_categories(project)
747 def copy_issue_categories(project)
744 project.issue_categories.each do |issue_category|
748 project.issue_categories.each do |issue_category|
745 new_issue_category = IssueCategory.new
749 new_issue_category = IssueCategory.new
746 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
750 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
747 self.issue_categories << new_issue_category
751 self.issue_categories << new_issue_category
748 end
752 end
749 end
753 end
750
754
751 # Copies issues from +project+
755 # Copies issues from +project+
752 # Note: issues assigned to a closed version won't be copied due to validation rules
756 # Note: issues assigned to a closed version won't be copied due to validation rules
753 def copy_issues(project)
757 def copy_issues(project)
754 # Stores the source issue id as a key and the copied issues as the
758 # Stores the source issue id as a key and the copied issues as the
755 # value. Used to map the two togeather for issue relations.
759 # value. Used to map the two togeather for issue relations.
756 issues_map = {}
760 issues_map = {}
757
761
758 # Get issues sorted by root_id, lft so that parent issues
762 # Get issues sorted by root_id, lft so that parent issues
759 # get copied before their children
763 # get copied before their children
760 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
764 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
761 new_issue = Issue.new
765 new_issue = Issue.new
762 new_issue.copy_from(issue)
766 new_issue.copy_from(issue)
763 new_issue.project = self
767 new_issue.project = self
764 # Reassign fixed_versions by name, since names are unique per
768 # Reassign fixed_versions by name, since names are unique per
765 # project and the versions for self are not yet saved
769 # project and the versions for self are not yet saved
766 if issue.fixed_version
770 if issue.fixed_version
767 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
771 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
768 end
772 end
769 # Reassign the category by name, since names are unique per
773 # Reassign the category by name, since names are unique per
770 # project and the categories for self are not yet saved
774 # project and the categories for self are not yet saved
771 if issue.category
775 if issue.category
772 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
776 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
773 end
777 end
774 # Parent issue
778 # Parent issue
775 if issue.parent_id
779 if issue.parent_id
776 if copied_parent = issues_map[issue.parent_id]
780 if copied_parent = issues_map[issue.parent_id]
777 new_issue.parent_issue_id = copied_parent.id
781 new_issue.parent_issue_id = copied_parent.id
778 end
782 end
779 end
783 end
780
784
781 self.issues << new_issue
785 self.issues << new_issue
782 if new_issue.new_record?
786 if new_issue.new_record?
783 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
787 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
784 else
788 else
785 issues_map[issue.id] = new_issue unless new_issue.new_record?
789 issues_map[issue.id] = new_issue unless new_issue.new_record?
786 end
790 end
787 end
791 end
788
792
789 # Relations after in case issues related each other
793 # Relations after in case issues related each other
790 project.issues.each do |issue|
794 project.issues.each do |issue|
791 new_issue = issues_map[issue.id]
795 new_issue = issues_map[issue.id]
792 unless new_issue
796 unless new_issue
793 # Issue was not copied
797 # Issue was not copied
794 next
798 next
795 end
799 end
796
800
797 # Relations
801 # Relations
798 issue.relations_from.each do |source_relation|
802 issue.relations_from.each do |source_relation|
799 new_issue_relation = IssueRelation.new
803 new_issue_relation = IssueRelation.new
800 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
804 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
801 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
805 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
802 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
806 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
803 new_issue_relation.issue_to = source_relation.issue_to
807 new_issue_relation.issue_to = source_relation.issue_to
804 end
808 end
805 new_issue.relations_from << new_issue_relation
809 new_issue.relations_from << new_issue_relation
806 end
810 end
807
811
808 issue.relations_to.each do |source_relation|
812 issue.relations_to.each do |source_relation|
809 new_issue_relation = IssueRelation.new
813 new_issue_relation = IssueRelation.new
810 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
814 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
811 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
815 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
812 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
816 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
813 new_issue_relation.issue_from = source_relation.issue_from
817 new_issue_relation.issue_from = source_relation.issue_from
814 end
818 end
815 new_issue.relations_to << new_issue_relation
819 new_issue.relations_to << new_issue_relation
816 end
820 end
817 end
821 end
818 end
822 end
819
823
820 # Copies members from +project+
824 # Copies members from +project+
821 def copy_members(project)
825 def copy_members(project)
822 # Copy users first, then groups to handle members with inherited and given roles
826 # Copy users first, then groups to handle members with inherited and given roles
823 members_to_copy = []
827 members_to_copy = []
824 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
828 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
825 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
829 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
826
830
827 members_to_copy.each do |member|
831 members_to_copy.each do |member|
828 new_member = Member.new
832 new_member = Member.new
829 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
833 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
830 # only copy non inherited roles
834 # only copy non inherited roles
831 # inherited roles will be added when copying the group membership
835 # inherited roles will be added when copying the group membership
832 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
836 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
833 next if role_ids.empty?
837 next if role_ids.empty?
834 new_member.role_ids = role_ids
838 new_member.role_ids = role_ids
835 new_member.project = self
839 new_member.project = self
836 self.members << new_member
840 self.members << new_member
837 end
841 end
838 end
842 end
839
843
840 # Copies queries from +project+
844 # Copies queries from +project+
841 def copy_queries(project)
845 def copy_queries(project)
842 project.queries.each do |query|
846 project.queries.each do |query|
843 new_query = ::Query.new
847 new_query = ::Query.new
844 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
848 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
845 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
849 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
846 new_query.project = self
850 new_query.project = self
847 new_query.user_id = query.user_id
851 new_query.user_id = query.user_id
848 self.queries << new_query
852 self.queries << new_query
849 end
853 end
850 end
854 end
851
855
852 # Copies boards from +project+
856 # Copies boards from +project+
853 def copy_boards(project)
857 def copy_boards(project)
854 project.boards.each do |board|
858 project.boards.each do |board|
855 new_board = Board.new
859 new_board = Board.new
856 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
860 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
857 new_board.project = self
861 new_board.project = self
858 self.boards << new_board
862 self.boards << new_board
859 end
863 end
860 end
864 end
861
865
862 def allowed_permissions
866 def allowed_permissions
863 @allowed_permissions ||= begin
867 @allowed_permissions ||= begin
864 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
868 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
865 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
869 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
866 end
870 end
867 end
871 end
868
872
869 def allowed_actions
873 def allowed_actions
870 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
874 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
871 end
875 end
872
876
873 # Returns all the active Systemwide and project specific activities
877 # Returns all the active Systemwide and project specific activities
874 def active_activities
878 def active_activities
875 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
879 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
876
880
877 if overridden_activity_ids.empty?
881 if overridden_activity_ids.empty?
878 return TimeEntryActivity.shared.active
882 return TimeEntryActivity.shared.active
879 else
883 else
880 return system_activities_and_project_overrides
884 return system_activities_and_project_overrides
881 end
885 end
882 end
886 end
883
887
884 # Returns all the Systemwide and project specific activities
888 # Returns all the Systemwide and project specific activities
885 # (inactive and active)
889 # (inactive and active)
886 def all_activities
890 def all_activities
887 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
891 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
888
892
889 if overridden_activity_ids.empty?
893 if overridden_activity_ids.empty?
890 return TimeEntryActivity.shared
894 return TimeEntryActivity.shared
891 else
895 else
892 return system_activities_and_project_overrides(true)
896 return system_activities_and_project_overrides(true)
893 end
897 end
894 end
898 end
895
899
896 # Returns the systemwide active activities merged with the project specific overrides
900 # Returns the systemwide active activities merged with the project specific overrides
897 def system_activities_and_project_overrides(include_inactive=false)
901 def system_activities_and_project_overrides(include_inactive=false)
898 if include_inactive
902 if include_inactive
899 return TimeEntryActivity.shared.
903 return TimeEntryActivity.shared.
900 find(:all,
904 find(:all,
901 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
905 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
902 self.time_entry_activities
906 self.time_entry_activities
903 else
907 else
904 return TimeEntryActivity.shared.active.
908 return TimeEntryActivity.shared.active.
905 find(:all,
909 find(:all,
906 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
910 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
907 self.time_entry_activities.active
911 self.time_entry_activities.active
908 end
912 end
909 end
913 end
910
914
911 # Archives subprojects recursively
915 # Archives subprojects recursively
912 def archive!
916 def archive!
913 children.each do |subproject|
917 children.each do |subproject|
914 subproject.send :archive!
918 subproject.send :archive!
915 end
919 end
916 update_attribute :status, STATUS_ARCHIVED
920 update_attribute :status, STATUS_ARCHIVED
917 end
921 end
918 end
922 end
@@ -1,419 +1,419
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ScmFetchError < Exception; end
18 class ScmFetchError < Exception; end
19
19
20 class Repository < ActiveRecord::Base
20 class Repository < ActiveRecord::Base
21 include Redmine::Ciphering
21 include Redmine::Ciphering
22
22
23 belongs_to :project
23 belongs_to :project
24 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
24 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
25 has_many :changes, :through => :changesets
25 has_many :changes, :through => :changesets
26
26
27 serialize :extra_info
27 serialize :extra_info
28
28
29 before_save :check_default
29 before_save :check_default
30
30
31 # Raw SQL to delete changesets and changes in the database
31 # Raw SQL to delete changesets and changes in the database
32 # has_many :changesets, :dependent => :destroy is too slow for big repositories
32 # has_many :changesets, :dependent => :destroy is too slow for big repositories
33 before_destroy :clear_changesets
33 before_destroy :clear_changesets
34
34
35 validates_length_of :password, :maximum => 255, :allow_nil => true
35 validates_length_of :password, :maximum => 255, :allow_nil => true
36 validates_length_of :identifier, :maximum => 255, :allow_blank => true
36 validates_length_of :identifier, :maximum => 255, :allow_blank => true
37 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
37 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
38 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
38 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
39 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
39 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
40 # donwcase letters, digits, dashes but not digits only
40 # donwcase letters, digits, dashes but not digits only
41 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
41 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
42 # Checks if the SCM is enabled when creating a repository
42 # Checks if the SCM is enabled when creating a repository
43 validate :repo_create_validation, :on => :create
43 validate :repo_create_validation, :on => :create
44
44
45 def repo_create_validation
45 def repo_create_validation
46 unless Setting.enabled_scm.include?(self.class.name.demodulize)
46 unless Setting.enabled_scm.include?(self.class.name.demodulize)
47 errors.add(:type, :invalid)
47 errors.add(:type, :invalid)
48 end
48 end
49 end
49 end
50
50
51 def self.human_attribute_name(attribute_key_name, *args)
51 def self.human_attribute_name(attribute_key_name, *args)
52 attr_name = attribute_key_name.to_s
52 attr_name = attribute_key_name.to_s
53 if attr_name == "log_encoding"
53 if attr_name == "log_encoding"
54 attr_name = "commit_logs_encoding"
54 attr_name = "commit_logs_encoding"
55 end
55 end
56 super(attr_name, *args)
56 super(attr_name, *args)
57 end
57 end
58
58
59 alias :attributes_without_extra_info= :attributes=
59 alias :attributes_without_extra_info= :attributes=
60 def attributes=(new_attributes, guard_protected_attributes = true)
60 def attributes=(new_attributes)
61 return if new_attributes.nil?
61 return if new_attributes.nil?
62 attributes = new_attributes.dup
62 attributes = new_attributes.dup
63 attributes.stringify_keys!
63 attributes.stringify_keys!
64
64
65 p = {}
65 p = {}
66 p_extra = {}
66 p_extra = {}
67 attributes.each do |k, v|
67 attributes.each do |k, v|
68 if k =~ /^extra_/
68 if k =~ /^extra_/
69 p_extra[k] = v
69 p_extra[k] = v
70 else
70 else
71 p[k] = v
71 p[k] = v
72 end
72 end
73 end
73 end
74
74
75 send :attributes_without_extra_info=, p, guard_protected_attributes
75 send :attributes_without_extra_info=, p
76 if p_extra.keys.any?
76 if p_extra.keys.any?
77 merge_extra_info(p_extra)
77 merge_extra_info(p_extra)
78 end
78 end
79 end
79 end
80
80
81 # Removes leading and trailing whitespace
81 # Removes leading and trailing whitespace
82 def url=(arg)
82 def url=(arg)
83 write_attribute(:url, arg ? arg.to_s.strip : nil)
83 write_attribute(:url, arg ? arg.to_s.strip : nil)
84 end
84 end
85
85
86 # Removes leading and trailing whitespace
86 # Removes leading and trailing whitespace
87 def root_url=(arg)
87 def root_url=(arg)
88 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
88 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
89 end
89 end
90
90
91 def password
91 def password
92 read_ciphered_attribute(:password)
92 read_ciphered_attribute(:password)
93 end
93 end
94
94
95 def password=(arg)
95 def password=(arg)
96 write_ciphered_attribute(:password, arg)
96 write_ciphered_attribute(:password, arg)
97 end
97 end
98
98
99 def scm_adapter
99 def scm_adapter
100 self.class.scm_adapter_class
100 self.class.scm_adapter_class
101 end
101 end
102
102
103 def scm
103 def scm
104 unless @scm
104 unless @scm
105 @scm = self.scm_adapter.new(url, root_url,
105 @scm = self.scm_adapter.new(url, root_url,
106 login, password, path_encoding)
106 login, password, path_encoding)
107 if root_url.blank? && @scm.root_url.present?
107 if root_url.blank? && @scm.root_url.present?
108 update_attribute(:root_url, @scm.root_url)
108 update_attribute(:root_url, @scm.root_url)
109 end
109 end
110 end
110 end
111 @scm
111 @scm
112 end
112 end
113
113
114 def scm_name
114 def scm_name
115 self.class.scm_name
115 self.class.scm_name
116 end
116 end
117
117
118 def name
118 def name
119 if identifier.present?
119 if identifier.present?
120 identifier
120 identifier
121 elsif is_default?
121 elsif is_default?
122 l(:field_repository_is_default)
122 l(:field_repository_is_default)
123 else
123 else
124 scm_name
124 scm_name
125 end
125 end
126 end
126 end
127
127
128 def identifier_param
128 def identifier_param
129 if is_default?
129 if is_default?
130 nil
130 nil
131 elsif identifier.present?
131 elsif identifier.present?
132 identifier
132 identifier
133 else
133 else
134 id.to_s
134 id.to_s
135 end
135 end
136 end
136 end
137
137
138 def <=>(repository)
138 def <=>(repository)
139 if is_default?
139 if is_default?
140 -1
140 -1
141 elsif repository.is_default?
141 elsif repository.is_default?
142 1
142 1
143 else
143 else
144 identifier <=> repository.identifier
144 identifier <=> repository.identifier
145 end
145 end
146 end
146 end
147
147
148 def self.find_by_identifier_param(param)
148 def self.find_by_identifier_param(param)
149 if param.to_s =~ /^\d+$/
149 if param.to_s =~ /^\d+$/
150 find_by_id(param)
150 find_by_id(param)
151 else
151 else
152 find_by_identifier(param)
152 find_by_identifier(param)
153 end
153 end
154 end
154 end
155
155
156 def merge_extra_info(arg)
156 def merge_extra_info(arg)
157 h = extra_info || {}
157 h = extra_info || {}
158 return h if arg.nil?
158 return h if arg.nil?
159 h.merge!(arg)
159 h.merge!(arg)
160 write_attribute(:extra_info, h)
160 write_attribute(:extra_info, h)
161 end
161 end
162
162
163 def report_last_commit
163 def report_last_commit
164 true
164 true
165 end
165 end
166
166
167 def supports_cat?
167 def supports_cat?
168 scm.supports_cat?
168 scm.supports_cat?
169 end
169 end
170
170
171 def supports_annotate?
171 def supports_annotate?
172 scm.supports_annotate?
172 scm.supports_annotate?
173 end
173 end
174
174
175 def supports_all_revisions?
175 def supports_all_revisions?
176 true
176 true
177 end
177 end
178
178
179 def supports_directory_revisions?
179 def supports_directory_revisions?
180 false
180 false
181 end
181 end
182
182
183 def supports_revision_graph?
183 def supports_revision_graph?
184 false
184 false
185 end
185 end
186
186
187 def entry(path=nil, identifier=nil)
187 def entry(path=nil, identifier=nil)
188 scm.entry(path, identifier)
188 scm.entry(path, identifier)
189 end
189 end
190
190
191 def entries(path=nil, identifier=nil)
191 def entries(path=nil, identifier=nil)
192 scm.entries(path, identifier)
192 scm.entries(path, identifier)
193 end
193 end
194
194
195 def branches
195 def branches
196 scm.branches
196 scm.branches
197 end
197 end
198
198
199 def tags
199 def tags
200 scm.tags
200 scm.tags
201 end
201 end
202
202
203 def default_branch
203 def default_branch
204 nil
204 nil
205 end
205 end
206
206
207 def properties(path, identifier=nil)
207 def properties(path, identifier=nil)
208 scm.properties(path, identifier)
208 scm.properties(path, identifier)
209 end
209 end
210
210
211 def cat(path, identifier=nil)
211 def cat(path, identifier=nil)
212 scm.cat(path, identifier)
212 scm.cat(path, identifier)
213 end
213 end
214
214
215 def diff(path, rev, rev_to)
215 def diff(path, rev, rev_to)
216 scm.diff(path, rev, rev_to)
216 scm.diff(path, rev, rev_to)
217 end
217 end
218
218
219 def diff_format_revisions(cs, cs_to, sep=':')
219 def diff_format_revisions(cs, cs_to, sep=':')
220 text = ""
220 text = ""
221 text << cs_to.format_identifier + sep if cs_to
221 text << cs_to.format_identifier + sep if cs_to
222 text << cs.format_identifier if cs
222 text << cs.format_identifier if cs
223 text
223 text
224 end
224 end
225
225
226 # Returns a path relative to the url of the repository
226 # Returns a path relative to the url of the repository
227 def relative_path(path)
227 def relative_path(path)
228 path
228 path
229 end
229 end
230
230
231 # Finds and returns a revision with a number or the beginning of a hash
231 # Finds and returns a revision with a number or the beginning of a hash
232 def find_changeset_by_name(name)
232 def find_changeset_by_name(name)
233 return nil if name.blank?
233 return nil if name.blank?
234 s = name.to_s
234 s = name.to_s
235 changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
235 changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
236 ["revision = ?", s] : ["revision LIKE ?", s + '%']))
236 ["revision = ?", s] : ["revision LIKE ?", s + '%']))
237 end
237 end
238
238
239 def latest_changeset
239 def latest_changeset
240 @latest_changeset ||= changesets.find(:first)
240 @latest_changeset ||= changesets.find(:first)
241 end
241 end
242
242
243 # Returns the latest changesets for +path+
243 # Returns the latest changesets for +path+
244 # Default behaviour is to search in cached changesets
244 # Default behaviour is to search in cached changesets
245 def latest_changesets(path, rev, limit=10)
245 def latest_changesets(path, rev, limit=10)
246 if path.blank?
246 if path.blank?
247 changesets.find(
247 changesets.find(
248 :all,
248 :all,
249 :include => :user,
249 :include => :user,
250 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
250 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
251 :limit => limit)
251 :limit => limit)
252 else
252 else
253 changes.find(
253 changes.find(
254 :all,
254 :all,
255 :include => {:changeset => :user},
255 :include => {:changeset => :user},
256 :conditions => ["path = ?", path.with_leading_slash],
256 :conditions => ["path = ?", path.with_leading_slash],
257 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
257 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
258 :limit => limit
258 :limit => limit
259 ).collect(&:changeset)
259 ).collect(&:changeset)
260 end
260 end
261 end
261 end
262
262
263 def scan_changesets_for_issue_ids
263 def scan_changesets_for_issue_ids
264 self.changesets.each(&:scan_comment_for_issue_ids)
264 self.changesets.each(&:scan_comment_for_issue_ids)
265 end
265 end
266
266
267 # Returns an array of committers usernames and associated user_id
267 # Returns an array of committers usernames and associated user_id
268 def committers
268 def committers
269 @committers ||= Changeset.connection.select_rows(
269 @committers ||= Changeset.connection.select_rows(
270 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
270 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
271 end
271 end
272
272
273 # Maps committers username to a user ids
273 # Maps committers username to a user ids
274 def committer_ids=(h)
274 def committer_ids=(h)
275 if h.is_a?(Hash)
275 if h.is_a?(Hash)
276 committers.each do |committer, user_id|
276 committers.each do |committer, user_id|
277 new_user_id = h[committer]
277 new_user_id = h[committer]
278 if new_user_id && (new_user_id.to_i != user_id.to_i)
278 if new_user_id && (new_user_id.to_i != user_id.to_i)
279 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
279 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
280 Changeset.update_all(
280 Changeset.update_all(
281 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
281 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
282 ["repository_id = ? AND committer = ?", id, committer])
282 ["repository_id = ? AND committer = ?", id, committer])
283 end
283 end
284 end
284 end
285 @committers = nil
285 @committers = nil
286 @found_committer_users = nil
286 @found_committer_users = nil
287 true
287 true
288 else
288 else
289 false
289 false
290 end
290 end
291 end
291 end
292
292
293 # Returns the Redmine User corresponding to the given +committer+
293 # Returns the Redmine User corresponding to the given +committer+
294 # It will return nil if the committer is not yet mapped and if no User
294 # It will return nil if the committer is not yet mapped and if no User
295 # with the same username or email was found
295 # with the same username or email was found
296 def find_committer_user(committer)
296 def find_committer_user(committer)
297 unless committer.blank?
297 unless committer.blank?
298 @found_committer_users ||= {}
298 @found_committer_users ||= {}
299 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
299 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
300
300
301 user = nil
301 user = nil
302 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
302 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
303 if c && c.user
303 if c && c.user
304 user = c.user
304 user = c.user
305 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
305 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
306 username, email = $1.strip, $3
306 username, email = $1.strip, $3
307 u = User.find_by_login(username)
307 u = User.find_by_login(username)
308 u ||= User.find_by_mail(email) unless email.blank?
308 u ||= User.find_by_mail(email) unless email.blank?
309 user = u
309 user = u
310 end
310 end
311 @found_committer_users[committer] = user
311 @found_committer_users[committer] = user
312 user
312 user
313 end
313 end
314 end
314 end
315
315
316 def repo_log_encoding
316 def repo_log_encoding
317 encoding = log_encoding.to_s.strip
317 encoding = log_encoding.to_s.strip
318 encoding.blank? ? 'UTF-8' : encoding
318 encoding.blank? ? 'UTF-8' : encoding
319 end
319 end
320
320
321 # Fetches new changesets for all repositories of active projects
321 # Fetches new changesets for all repositories of active projects
322 # Can be called periodically by an external script
322 # Can be called periodically by an external script
323 # eg. ruby script/runner "Repository.fetch_changesets"
323 # eg. ruby script/runner "Repository.fetch_changesets"
324 def self.fetch_changesets
324 def self.fetch_changesets
325 Project.active.has_module(:repository).all.each do |project|
325 Project.active.has_module(:repository).all.each do |project|
326 project.repositories.each do |repository|
326 project.repositories.each do |repository|
327 begin
327 begin
328 repository.fetch_changesets
328 repository.fetch_changesets
329 rescue Redmine::Scm::Adapters::CommandFailed => e
329 rescue Redmine::Scm::Adapters::CommandFailed => e
330 logger.error "scm: error during fetching changesets: #{e.message}"
330 logger.error "scm: error during fetching changesets: #{e.message}"
331 end
331 end
332 end
332 end
333 end
333 end
334 end
334 end
335
335
336 # scan changeset comments to find related and fixed issues for all repositories
336 # scan changeset comments to find related and fixed issues for all repositories
337 def self.scan_changesets_for_issue_ids
337 def self.scan_changesets_for_issue_ids
338 find(:all).each(&:scan_changesets_for_issue_ids)
338 find(:all).each(&:scan_changesets_for_issue_ids)
339 end
339 end
340
340
341 def self.scm_name
341 def self.scm_name
342 'Abstract'
342 'Abstract'
343 end
343 end
344
344
345 def self.available_scm
345 def self.available_scm
346 subclasses.collect {|klass| [klass.scm_name, klass.name]}
346 subclasses.collect {|klass| [klass.scm_name, klass.name]}
347 end
347 end
348
348
349 def self.factory(klass_name, *args)
349 def self.factory(klass_name, *args)
350 klass = "Repository::#{klass_name}".constantize
350 klass = "Repository::#{klass_name}".constantize
351 klass.new(*args)
351 klass.new(*args)
352 rescue
352 rescue
353 nil
353 nil
354 end
354 end
355
355
356 def self.scm_adapter_class
356 def self.scm_adapter_class
357 nil
357 nil
358 end
358 end
359
359
360 def self.scm_command
360 def self.scm_command
361 ret = ""
361 ret = ""
362 begin
362 begin
363 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
363 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
364 rescue Exception => e
364 rescue Exception => e
365 logger.error "scm: error during get command: #{e.message}"
365 logger.error "scm: error during get command: #{e.message}"
366 end
366 end
367 ret
367 ret
368 end
368 end
369
369
370 def self.scm_version_string
370 def self.scm_version_string
371 ret = ""
371 ret = ""
372 begin
372 begin
373 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
373 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
374 rescue Exception => e
374 rescue Exception => e
375 logger.error "scm: error during get version string: #{e.message}"
375 logger.error "scm: error during get version string: #{e.message}"
376 end
376 end
377 ret
377 ret
378 end
378 end
379
379
380 def self.scm_available
380 def self.scm_available
381 ret = false
381 ret = false
382 begin
382 begin
383 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
383 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
384 rescue Exception => e
384 rescue Exception => e
385 logger.error "scm: error during get scm available: #{e.message}"
385 logger.error "scm: error during get scm available: #{e.message}"
386 end
386 end
387 ret
387 ret
388 end
388 end
389
389
390 def set_as_default?
390 def set_as_default?
391 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
391 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
392 end
392 end
393
393
394 protected
394 protected
395
395
396 def check_default
396 def check_default
397 if !is_default? && set_as_default?
397 if !is_default? && set_as_default?
398 self.is_default = true
398 self.is_default = true
399 end
399 end
400 if is_default? && is_default_changed?
400 if is_default? && is_default_changed?
401 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
401 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
402 end
402 end
403 end
403 end
404
404
405 private
405 private
406
406
407 # Deletes repository data
407 # Deletes repository data
408 def clear_changesets
408 def clear_changesets
409 cs = Changeset.table_name
409 cs = Changeset.table_name
410 ch = Change.table_name
410 ch = Change.table_name
411 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
411 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
412 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
412 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
413
413
414 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
414 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
415 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
415 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
416 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
416 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
417 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
417 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
418 end
418 end
419 end
419 end
@@ -1,177 +1,177
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Role < ActiveRecord::Base
18 class Role < ActiveRecord::Base
19 # Built-in roles
19 # Built-in roles
20 BUILTIN_NON_MEMBER = 1
20 BUILTIN_NON_MEMBER = 1
21 BUILTIN_ANONYMOUS = 2
21 BUILTIN_ANONYMOUS = 2
22
22
23 ISSUES_VISIBILITY_OPTIONS = [
23 ISSUES_VISIBILITY_OPTIONS = [
24 ['all', :label_issues_visibility_all],
24 ['all', :label_issues_visibility_all],
25 ['default', :label_issues_visibility_public],
25 ['default', :label_issues_visibility_public],
26 ['own', :label_issues_visibility_own]
26 ['own', :label_issues_visibility_own]
27 ]
27 ]
28
28
29 named_scope :sorted, {:order => 'builtin, position'}
29 named_scope :sorted, {:order => 'builtin, position'}
30 named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
30 named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
31 named_scope :builtin, lambda { |*args|
31 named_scope :builtin, lambda { |*args|
32 compare = 'not' if args.first == true
32 compare = 'not' if args.first == true
33 { :conditions => "#{compare} builtin = 0" }
33 { :conditions => "#{compare} builtin = 0" }
34 }
34 }
35
35
36 before_destroy :check_deletable
36 before_destroy :check_deletable
37 has_many :workflows, :dependent => :delete_all do
37 has_many :workflows, :dependent => :delete_all do
38 def copy(source_role)
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 end
40 end
41 end
41 end
42
42
43 has_many :member_roles, :dependent => :destroy
43 has_many :member_roles, :dependent => :destroy
44 has_many :members, :through => :member_roles
44 has_many :members, :through => :member_roles
45 acts_as_list
45 acts_as_list
46
46
47 serialize :permissions, Array
47 serialize :permissions, Array
48 attr_protected :builtin
48 attr_protected :builtin
49
49
50 validates_presence_of :name
50 validates_presence_of :name
51 validates_uniqueness_of :name
51 validates_uniqueness_of :name
52 validates_length_of :name, :maximum => 30
52 validates_length_of :name, :maximum => 30
53 validates_inclusion_of :issues_visibility,
53 validates_inclusion_of :issues_visibility,
54 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
54 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
55 :if => lambda {|role| role.respond_to?(:issues_visibility)}
55 :if => lambda {|role| role.respond_to?(:issues_visibility)}
56
56
57 def permissions
57 def permissions
58 read_attribute(:permissions) || []
58 read_attribute(:permissions) || []
59 end
59 end
60
60
61 def permissions=(perms)
61 def permissions=(perms)
62 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
62 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
63 write_attribute(:permissions, perms)
63 write_attribute(:permissions, perms)
64 end
64 end
65
65
66 def add_permission!(*perms)
66 def add_permission!(*perms)
67 self.permissions = [] unless permissions.is_a?(Array)
67 self.permissions = [] unless permissions.is_a?(Array)
68
68
69 permissions_will_change!
69 permissions_will_change!
70 perms.each do |p|
70 perms.each do |p|
71 p = p.to_sym
71 p = p.to_sym
72 permissions << p unless permissions.include?(p)
72 permissions << p unless permissions.include?(p)
73 end
73 end
74 save!
74 save!
75 end
75 end
76
76
77 def remove_permission!(*perms)
77 def remove_permission!(*perms)
78 return unless permissions.is_a?(Array)
78 return unless permissions.is_a?(Array)
79 permissions_will_change!
79 permissions_will_change!
80 perms.each { |p| permissions.delete(p.to_sym) }
80 perms.each { |p| permissions.delete(p.to_sym) }
81 save!
81 save!
82 end
82 end
83
83
84 # Returns true if the role has the given permission
84 # Returns true if the role has the given permission
85 def has_permission?(perm)
85 def has_permission?(perm)
86 !permissions.nil? && permissions.include?(perm.to_sym)
86 !permissions.nil? && permissions.include?(perm.to_sym)
87 end
87 end
88
88
89 def <=>(role)
89 def <=>(role)
90 role ? position <=> role.position : -1
90 role ? position <=> role.position : -1
91 end
91 end
92
92
93 def to_s
93 def to_s
94 name
94 name
95 end
95 end
96
96
97 def name
97 def name
98 case builtin
98 case builtin
99 when 1; l(:label_role_non_member, :default => read_attribute(:name))
99 when 1; l(:label_role_non_member, :default => read_attribute(:name))
100 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
100 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
101 else; read_attribute(:name)
101 else; read_attribute(:name)
102 end
102 end
103 end
103 end
104
104
105 # Return true if the role is a builtin role
105 # Return true if the role is a builtin role
106 def builtin?
106 def builtin?
107 self.builtin != 0
107 self.builtin != 0
108 end
108 end
109
109
110 # Return true if the role is a project member role
110 # Return true if the role is a project member role
111 def member?
111 def member?
112 !self.builtin?
112 !self.builtin?
113 end
113 end
114
114
115 # Return true if role is allowed to do the specified action
115 # Return true if role is allowed to do the specified action
116 # action can be:
116 # action can be:
117 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
117 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
118 # * a permission Symbol (eg. :edit_project)
118 # * a permission Symbol (eg. :edit_project)
119 def allowed_to?(action)
119 def allowed_to?(action)
120 if action.is_a? Hash
120 if action.is_a? Hash
121 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
121 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
122 else
122 else
123 allowed_permissions.include? action
123 allowed_permissions.include? action
124 end
124 end
125 end
125 end
126
126
127 # Return all the permissions that can be given to the role
127 # Return all the permissions that can be given to the role
128 def setable_permissions
128 def setable_permissions
129 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
129 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
130 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
130 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
131 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
131 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
132 setable_permissions
132 setable_permissions
133 end
133 end
134
134
135 # Find all the roles that can be given to a project member
135 # Find all the roles that can be given to a project member
136 def self.find_all_givable
136 def self.find_all_givable
137 find(:all, :conditions => {:builtin => 0}, :order => 'position')
137 find(:all, :conditions => {:builtin => 0}, :order => 'position')
138 end
138 end
139
139
140 # Return the builtin 'non member' role. If the role doesn't exist,
140 # Return the builtin 'non member' role. If the role doesn't exist,
141 # it will be created on the fly.
141 # it will be created on the fly.
142 def self.non_member
142 def self.non_member
143 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
143 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
144 end
144 end
145
145
146 # Return the builtin 'anonymous' role. If the role doesn't exist,
146 # Return the builtin 'anonymous' role. If the role doesn't exist,
147 # it will be created on the fly.
147 # it will be created on the fly.
148 def self.anonymous
148 def self.anonymous
149 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
149 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
150 end
150 end
151
151
152 private
152 private
153
153
154 def allowed_permissions
154 def allowed_permissions
155 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
155 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
156 end
156 end
157
157
158 def allowed_actions
158 def allowed_actions
159 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
159 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
160 end
160 end
161
161
162 def check_deletable
162 def check_deletable
163 raise "Can't delete role" if members.any?
163 raise "Can't delete role" if members.any?
164 raise "Can't delete builtin role" if builtin?
164 raise "Can't delete builtin role" if builtin?
165 end
165 end
166
166
167 def self.find_or_create_system_role(builtin, name)
167 def self.find_or_create_system_role(builtin, name)
168 role = first(:conditions => {:builtin => builtin})
168 role = first(:conditions => {:builtin => builtin})
169 if role.nil?
169 if role.nil?
170 role = create(:name => name, :position => 0) do |r|
170 role = create(:name => name, :position => 0) do |r|
171 r.builtin = builtin
171 r.builtin = builtin
172 end
172 end
173 raise "Unable to create the #{name} role." if role.new_record?
173 raise "Unable to create the #{name} role." if role.new_record?
174 end
174 end
175 role
175 role
176 end
176 end
177 end
177 end
@@ -1,68 +1,68
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Tracker < ActiveRecord::Base
18 class Tracker < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 has_many :issues
20 has_many :issues
21 has_many :workflows, :dependent => :delete_all do
21 has_many :workflows, :dependent => :delete_all do
22 def copy(source_tracker)
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 end
24 end
25 end
25 end
26
26
27 has_and_belongs_to_many :projects
27 has_and_belongs_to_many :projects
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'
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 acts_as_list
29 acts_as_list
30
30
31 validates_presence_of :name
31 validates_presence_of :name
32 validates_uniqueness_of :name
32 validates_uniqueness_of :name
33 validates_length_of :name, :maximum => 30
33 validates_length_of :name, :maximum => 30
34
34
35 named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
35 named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
36
36
37 def to_s; name end
37 def to_s; name end
38
38
39 def <=>(tracker)
39 def <=>(tracker)
40 name <=> tracker.name
40 name <=> tracker.name
41 end
41 end
42
42
43 def self.all
43 def self.all
44 find(:all, :order => 'position')
44 find(:all, :order => 'position')
45 end
45 end
46
46
47 # Returns an array of IssueStatus that are used
47 # Returns an array of IssueStatus that are used
48 # in the tracker's workflows
48 # in the tracker's workflows
49 def issue_statuses
49 def issue_statuses
50 if @issue_statuses
50 if @issue_statuses
51 return @issue_statuses
51 return @issue_statuses
52 elsif new_record?
52 elsif new_record?
53 return []
53 return []
54 end
54 end
55
55
56 ids = Workflow.
56 ids = Workflow.
57 connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
57 connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
58 flatten.
58 flatten.
59 uniq
59 uniq
60
60
61 @issue_statuses = IssueStatus.find_all_by_id(ids).sort
61 @issue_statuses = IssueStatus.find_all_by_id(ids).sort
62 end
62 end
63
63
64 private
64 private
65 def check_integrity
65 def check_integrity
66 raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
66 raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
67 end
67 end
68 end
68 end
@@ -1,42 +1,42
1 <%= call_hook :view_account_login_top %>
1 <%= call_hook :view_account_login_top %>
2 <div id="login-form">
2 <div id="login-form">
3 <% form_tag({:action=> "login"}) do %>
3 <%= form_tag({:action=> "login"}) do %>
4 <%= back_url_hidden_field_tag %>
4 <%= back_url_hidden_field_tag %>
5 <table>
5 <table>
6 <tr>
6 <tr>
7 <td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
7 <td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
8 <td align="left"><%= text_field_tag 'username', nil, :tabindex => '1' %></td>
8 <td align="left"><%= text_field_tag 'username', nil, :tabindex => '1' %></td>
9 </tr>
9 </tr>
10 <tr>
10 <tr>
11 <td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
11 <td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
12 <td align="left"><%= password_field_tag 'password', nil, :tabindex => '2' %></td>
12 <td align="left"><%= password_field_tag 'password', nil, :tabindex => '2' %></td>
13 </tr>
13 </tr>
14 <% if Setting.openid? %>
14 <% if Setting.openid? %>
15 <tr>
15 <tr>
16 <td align="right"><label for="openid_url"><%=l(:field_identity_url)%></label></td>
16 <td align="right"><label for="openid_url"><%=l(:field_identity_url)%></label></td>
17 <td align="left"><%= text_field_tag "openid_url", nil, :tabindex => '3' %></td>
17 <td align="left"><%= text_field_tag "openid_url", nil, :tabindex => '3' %></td>
18 </tr>
18 </tr>
19 <% end %>
19 <% end %>
20 <tr>
20 <tr>
21 <td></td>
21 <td></td>
22 <td align="left">
22 <td align="left">
23 <% if Setting.autologin? %>
23 <% if Setting.autologin? %>
24 <label for="autologin"><%= check_box_tag 'autologin', 1, false, :tabindex => 4 %> <%= l(:label_stay_logged_in) %></label>
24 <label for="autologin"><%= check_box_tag 'autologin', 1, false, :tabindex => 4 %> <%= l(:label_stay_logged_in) %></label>
25 <% end %>
25 <% end %>
26 </td>
26 </td>
27 </tr>
27 </tr>
28 <tr>
28 <tr>
29 <td align="left">
29 <td align="left">
30 <% if Setting.lost_password? %>
30 <% if Setting.lost_password? %>
31 <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %>
31 <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %>
32 <% end %>
32 <% end %>
33 </td>
33 </td>
34 <td align="right">
34 <td align="right">
35 <input type="submit" name="login" value="<%=l(:button_login)%> &#187;" tabindex="5"/>
35 <input type="submit" name="login" value="<%=l(:button_login)%> &#187;" tabindex="5"/>
36 </td>
36 </td>
37 </tr>
37 </tr>
38 </table>
38 </table>
39 <%= javascript_tag "Form.Element.focus('username');" %>
39 <%= javascript_tag "Form.Element.focus('username');" %>
40 <% end %>
40 <% end %>
41 </div>
41 </div>
42 <%= call_hook :view_account_login_bottom %>
42 <%= call_hook :view_account_login_bottom %>
@@ -1,11 +1,11
1 <h2><%=l(:label_password_lost)%></h2>
1 <h2><%=l(:label_password_lost)%></h2>
2
2
3 <div class="box">
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 <p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
6 <p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
7 <%= text_field_tag 'mail', nil, :size => 40 %>
7 <%= text_field_tag 'mail', nil, :size => 40 %>
8 <%= submit_tag l(:button_submit) %></p>
8 <%= submit_tag l(:button_submit) %></p>
9
9
10 <% end %>
10 <% end %>
11 </div>
11 </div>
@@ -1,15 +1,15
1 <h2><%=l(:label_password_lost)%></h2>
1 <h2><%=l(:label_password_lost)%></h2>
2
2
3 <%= error_messages_for 'user' %>
3 <%= error_messages_for 'user' %>
4
4
5 <% form_tag({:token => @token.value}) do %>
5 <%= form_tag({:token => @token.value}) do %>
6 <div class="box tabular">
6 <div class="box tabular">
7 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
7 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
8 <%= password_field_tag 'new_password', nil, :size => 25 %>
8 <%= password_field_tag 'new_password', nil, :size => 25 %>
9 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
9 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
10
10
11 <p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
11 <p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
12 <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
12 <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
13 </div>
13 </div>
14 <p><%= submit_tag l(:button_save) %></p>
14 <p><%= submit_tag l(:button_save) %></p>
15 <% end %>
15 <% end %>
@@ -1,31 +1,31
1 <h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
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 <%= error_messages_for 'user' %>
4 <%= error_messages_for 'user' %>
5
5
6 <div class="box tabular">
6 <div class="box tabular">
7 <% if @user.auth_source_id.nil? %>
7 <% if @user.auth_source_id.nil? %>
8 <p><%= f.text_field :login, :size => 25, :required => true %></p>
8 <p><%= f.text_field :login, :size => 25, :required => true %></p>
9
9
10 <p><%= f.password_field :password, :size => 25, :required => true %>
10 <p><%= f.password_field :password, :size => 25, :required => true %>
11 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
11 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
12
12
13 <p><%= f.password_field :password_confirmation, :size => 25, :required => true %></p>
13 <p><%= f.password_field :password_confirmation, :size => 25, :required => true %></p>
14 <% end %>
14 <% end %>
15
15
16 <p><%= f.text_field :firstname, :required => true %></p>
16 <p><%= f.text_field :firstname, :required => true %></p>
17 <p><%= f.text_field :lastname, :required => true %></p>
17 <p><%= f.text_field :lastname, :required => true %></p>
18 <p><%= f.text_field :mail, :required => true %></p>
18 <p><%= f.text_field :mail, :required => true %></p>
19 <p><%= f.select :language, lang_options_for_select %></p>
19 <p><%= f.select :language, lang_options_for_select %></p>
20
20
21 <% if Setting.openid? %>
21 <% if Setting.openid? %>
22 <p><%= f.text_field :identity_url %></p>
22 <p><%= f.text_field :identity_url %></p>
23 <% end %>
23 <% end %>
24
24
25 <% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %>
25 <% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %>
26 <p><%= custom_field_tag_with_label :user, value %></p>
26 <p><%= custom_field_tag_with_label :user, value %></p>
27 <% end %>
27 <% end %>
28 </div>
28 </div>
29
29
30 <%= submit_tag l(:button_submit) %>
30 <%= submit_tag l(:button_submit) %>
31 <% end %>
31 <% end %>
@@ -1,59 +1,59
1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
2 <p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
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 <div id="activity">
4 <div id="activity">
5 <% @events_by_day.keys.sort.reverse.each do |day| %>
5 <% @events_by_day.keys.sort.reverse.each do |day| %>
6 <h3><%= format_activity_day(day) %></h3>
6 <h3><%= format_activity_day(day) %></h3>
7 <dl>
7 <dl>
8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
13 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
13 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
14 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
14 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
15 <span class="author"><%= link_to_user(e.event_author) if e.respond_to?(:event_author) %></span></dd>
15 <span class="author"><%= link_to_user(e.event_author) if e.respond_to?(:event_author) %></span></dd>
16 <% end -%>
16 <% end -%>
17 </dl>
17 </dl>
18 <% end -%>
18 <% end -%>
19 </div>
19 </div>
20
20
21 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
21 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
22
22
23 <div style="float:left;">
23 <div style="float:left;">
24 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
24 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
25 params.merge(:from => @date_to - @days - 1),
25 params.merge(:from => @date_to - @days - 1),
26 :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))) %>
26 :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))) %>
27 </div>
27 </div>
28 <div style="float:right;">
28 <div style="float:right;">
29 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
29 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
30 params.merge(:from => @date_to + @days - 1),
30 params.merge(:from => @date_to + @days - 1),
31 :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))) unless @date_to >= Date.today %>
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 </div>
32 </div>
33 &nbsp;
33 &nbsp;
34 <% other_formats_links do |f| %>
34 <% other_formats_links do |f| %>
35 <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %>
35 <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %>
36 <% end %>
36 <% end %>
37
37
38 <% content_for :header_tags do %>
38 <% content_for :header_tags do %>
39 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
39 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
40 <% end %>
40 <% end %>
41
41
42 <% content_for :sidebar do %>
42 <% content_for :sidebar do %>
43 <% form_tag({}, :method => :get) do %>
43 <%= form_tag({}, :method => :get) do %>
44 <h3><%= l(:label_activity) %></h3>
44 <h3><%= l(:label_activity) %></h3>
45 <p><% @activity.event_types.each do |t| %>
45 <p><% @activity.event_types.each do |t| %>
46 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
46 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
47 <label for="show_<%=t%>"><%= link_to(l("label_#{t.singularize}_plural"), {"show_#{t}" => 1, :user_id => params[:user_id]})%></label>
47 <label for="show_<%=t%>"><%= link_to(l("label_#{t.singularize}_plural"), {"show_#{t}" => 1, :user_id => params[:user_id]})%></label>
48 <br />
48 <br />
49 <% end %></p>
49 <% end %></p>
50 <% if @project && @project.descendants.active.any? %>
50 <% if @project && @project.descendants.active.any? %>
51 <%= hidden_field_tag 'with_subprojects', 0 %>
51 <%= hidden_field_tag 'with_subprojects', 0 %>
52 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
52 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
53 <% end %>
53 <% end %>
54 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
54 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
55 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
55 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
56 <% end %>
56 <% end %>
57 <% end %>
57 <% end %>
58
58
59 <% html_title(l(:label_activity), @author) -%>
59 <% html_title(l(:label_activity), @author) -%>
@@ -1,8 +1,8
1 <div class="nodata">
1 <div class="nodata">
2 <% form_tag({:action => 'default_configuration'}) do %>
2 <%= form_tag({:action => 'default_configuration'}) do %>
3 <%= simple_format(l(:text_no_configuration_data)) %>
3 <%= simple_format(l(:text_no_configuration_data)) %>
4 <p><%= l(:field_language) %>:
4 <p><%= l(:field_language) %>:
5 <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %>
5 <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %>
6 <%= submit_tag l(:text_load_default_configuration) %></p>
6 <%= submit_tag l(:text_load_default_configuration) %></p>
7 <% end %>
7 <% end %>
8 </div>
8 </div>
@@ -1,45 +1,45
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_project_plural)%></h2>
5 <h2><%=l(:label_project_plural)%></h2>
6
6
7 <% form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label for='status'><%= l(:field_status) %> :</label>
9 <label for='status'><%= l(:field_status) %> :</label>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 <label for='name'><%= l(:label_project) %>:</label>
11 <label for='name'><%= l(:label_project) %>:</label>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
14 <%= link_to l(:button_clear), {:controller => 'admin', :action => 'projects'}, :class => 'icon icon-reload' %>
14 <%= link_to l(:button_clear), {:controller => 'admin', :action => 'projects'}, :class => 'icon icon-reload' %>
15 </fieldset>
15 </fieldset>
16 <% end %>
16 <% end %>
17 &nbsp;
17 &nbsp;
18
18
19 <div class="autoscroll">
19 <div class="autoscroll">
20 <table class="list">
20 <table class="list">
21 <thead><tr>
21 <thead><tr>
22 <th><%=l(:label_project)%></th>
22 <th><%=l(:label_project)%></th>
23 <th><%=l(:field_is_public)%></th>
23 <th><%=l(:field_is_public)%></th>
24 <th><%=l(:field_created_on)%></th>
24 <th><%=l(:field_created_on)%></th>
25 <th></th>
25 <th></th>
26 </tr></thead>
26 </tr></thead>
27 <tbody>
27 <tbody>
28 <% project_tree(@projects) do |project, level| %>
28 <% project_tree(@projects) do |project, level| %>
29 <tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 <tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
30 <td class="name"><span><%= link_to_project(project, {:action => 'settings'}, :title => project.short_description) %></span></td>
30 <td class="name"><span><%= link_to_project(project, {:action => 'settings'}, :title => project.short_description) %></span></td>
31 <td align="center"><%= checked_image project.is_public? %></td>
31 <td align="center"><%= checked_image project.is_public? %></td>
32 <td align="center"><%= format_date(project.created_on) %></td>
32 <td align="center"><%= format_date(project.created_on) %></td>
33 <td class="buttons">
33 <td class="buttons">
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? %>
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 <%= 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?) %>
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 <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
36 <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
37 <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
37 <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
38 </td>
38 </td>
39 </tr>
39 </tr>
40 <% end %>
40 <% end %>
41 </tbody>
41 </tbody>
42 </table>
42 </table>
43 </div>
43 </div>
44
44
45 <% html_title(l(:label_project_plural)) -%>
45 <% html_title(l(:label_project_plural)) -%>
@@ -1,24 +1,24
1 <h2><%=h @attachment.filename %></h2>
1 <h2><%=h @attachment.filename %></h2>
2
2
3 <div class="attachments">
3 <div class="attachments">
4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
5 <span class="author"><%= link_to_user(@attachment.author) %>, <%= format_time(@attachment.created_on) %></span></p>
5 <span class="author"><%= link_to_user(@attachment.author) %>, <%= format_time(@attachment.created_on) %></span></p>
6 <p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
6 <p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
8 </div>
8 </div>
9 <p>
9 <p>
10 <% form_tag({}, :method => 'get') do %>
10 <%= form_tag({}, :method => 'get') do %>
11 <label><%= l(:label_view_diff) %></label>
11 <label><%= l(:label_view_diff) %></label>
12 <%= select_tag 'type',
12 <%= select_tag 'type',
13 options_for_select(
13 options_for_select(
14 [[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
14 [[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
15 :onchange => "if (this.value != '') {this.form.submit()}" %>
15 :onchange => "if (this.value != '') {this.form.submit()}" %>
16 <% end %>
16 <% end %>
17 </p>
17 </p>
18 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
18 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
19
19
20 <% html_title @attachment.filename %>
20 <% html_title @attachment.filename %>
21
21
22 <% content_for :header_tags do -%>
22 <% content_for :header_tags do -%>
23 <%= stylesheet_link_tag "scm" -%>
23 <%= stylesheet_link_tag "scm" -%>
24 <% end -%>
24 <% end -%>
@@ -1,6 +1,6
1 <h2><%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
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 <%= render :partial => auth_source_partial_name(@auth_source) %>
4 <%= render :partial => auth_source_partial_name(@auth_source) %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,7 +1,7
1 <h2><%=l(:label_auth_source_new)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
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 <%= hidden_field_tag 'type', @auth_source.type %>
4 <%= hidden_field_tag 'type', @auth_source.type %>
5 <%= render :partial => auth_source_partial_name(@auth_source) %>
5 <%= render :partial => auth_source_partial_name(@auth_source) %>
6 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create) %>
7 <% end %>
7 <% end %>
@@ -1,6 +1,6
1 <h2><%= l(:label_board) %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= l(:label_board_new) %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -1,73 +1,73
1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)) %>
1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)) %>
2
2
3 <div class="contextual">
3 <div class="contextual">
4 <%= link_to_if_authorized l(:label_message_new),
4 <%= link_to_if_authorized l(:label_message_new),
5 {:controller => 'messages', :action => 'new', :board_id => @board},
5 {:controller => 'messages', :action => 'new', :board_id => @board},
6 :class => 'icon icon-add',
6 :class => 'icon icon-add',
7 :onclick => 'Element.show("add-message"); Form.Element.focus("message_subject"); return false;' %>
7 :onclick => 'Element.show("add-message"); Form.Element.focus("message_subject"); return false;' %>
8 <%= watcher_tag(@board, User.current) %>
8 <%= watcher_tag(@board, User.current) %>
9 </div>
9 </div>
10
10
11 <div id="add-message" style="display:none;">
11 <div id="add-message" style="display:none;">
12 <% if authorize_for('messages', 'new') %>
12 <% if authorize_for('messages', 'new') %>
13 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
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 <%= render :partial => 'messages/form', :locals => {:f => f} %>
15 <%= render :partial => 'messages/form', :locals => {:f => f} %>
16 <p><%= submit_tag l(:button_create) %>
16 <p><%= submit_tag l(:button_create) %>
17 <%= link_to_remote l(:label_preview),
17 <%= link_to_remote l(:label_preview),
18 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
18 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
19 :method => 'post',
19 :method => 'post',
20 :update => 'preview',
20 :update => 'preview',
21 :with => "Form.serialize('message-form')",
21 :with => "Form.serialize('message-form')",
22 :complete => "Element.scrollTo('preview')"
22 :complete => "Element.scrollTo('preview')"
23 }, :accesskey => accesskey(:preview) %> |
23 }, :accesskey => accesskey(:preview) %> |
24 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
24 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
25 <% end %>
25 <% end %>
26 <div id="preview" class="wiki"></div>
26 <div id="preview" class="wiki"></div>
27 <% end %>
27 <% end %>
28 </div>
28 </div>
29
29
30 <h2><%=h @board.name %></h2>
30 <h2><%=h @board.name %></h2>
31 <p class="subtitle"><%=h @board.description %></p>
31 <p class="subtitle"><%=h @board.description %></p>
32
32
33 <% if @topics.any? %>
33 <% if @topics.any? %>
34 <table class="list messages">
34 <table class="list messages">
35 <thead><tr>
35 <thead><tr>
36 <th><%= l(:field_subject) %></th>
36 <th><%= l(:field_subject) %></th>
37 <th><%= l(:field_author) %></th>
37 <th><%= l(:field_author) %></th>
38 <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
38 <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
39 <%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %>
39 <%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %>
40 <%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %>
40 <%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %>
41 </tr></thead>
41 </tr></thead>
42 <tbody>
42 <tbody>
43 <% @topics.each do |topic| %>
43 <% @topics.each do |topic| %>
44 <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
44 <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
45 <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic } %></td>
45 <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic } %></td>
46 <td class="author" align="center"><%= link_to_user(topic.author) %></td>
46 <td class="author" align="center"><%= link_to_user(topic.author) %></td>
47 <td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
47 <td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
48 <td class="replies" align="center"><%= topic.replies_count %></td>
48 <td class="replies" align="center"><%= topic.replies_count %></td>
49 <td class="last_message">
49 <td class="last_message">
50 <% if topic.last_reply %>
50 <% if topic.last_reply %>
51 <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
51 <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
52 <%= link_to_message topic.last_reply %>
52 <%= link_to_message topic.last_reply %>
53 <% end %>
53 <% end %>
54 </td>
54 </td>
55 </tr>
55 </tr>
56 <% end %>
56 <% end %>
57 </tbody>
57 </tbody>
58 </table>
58 </table>
59 <p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
59 <p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
60 <% else %>
60 <% else %>
61 <p class="nodata"><%= l(:label_no_data) %></p>
61 <p class="nodata"><%= l(:label_no_data) %></p>
62 <% end %>
62 <% end %>
63
63
64 <% other_formats_links do |f| %>
64 <% other_formats_links do |f| %>
65 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
65 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
66 <% end %>
66 <% end %>
67
67
68 <% html_title @board.name %>
68 <% html_title @board.name %>
69
69
70 <% content_for :header_tags do %>
70 <% content_for :header_tags do %>
71 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
71 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
72 <%= stylesheet_link_tag 'scm' %>
72 <%= stylesheet_link_tag 'scm' %>
73 <% end %>
73 <% end %>
@@ -1,42 +1,43
1 <h2><%= @query.new_record? ? l(:label_calendar) : h(@query.name) %></h2>
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 <%= hidden_field_tag 'set_filter', '1' %>
5 <%= hidden_field_tag 'set_filter', '1' %>
5 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
6 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
6 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
7 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
7 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
8 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
8 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
9 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
9 </div>
10 </div>
10 </fieldset>
11 </fieldset>
11
12
12 <p style="float:right;">
13 <p style="float:right;">
13 <%= link_to_previous_month(@year, @month) %> | <%= link_to_next_month(@year, @month) %>
14 <%= link_to_previous_month(@year, @month) %> | <%= link_to_next_month(@year, @month) %>
14 </p>
15 </p>
15
16
16 <p class="buttons">
17 <p class="buttons">
17 <%= label_tag('month', l(:label_month)) %>
18 <%= label_tag('month', l(:label_month)) %>
18 <%= select_month(@month, :prefix => "month", :discard_type => true) %>
19 <%= select_month(@month, :prefix => "month", :discard_type => true) %>
19 <%= label_tag('year', l(:label_year)) %>
20 <%= label_tag('year', l(:label_year)) %>
20 <%= select_year(@year, :prefix => "year", :discard_type => true) %>
21 <%= select_year(@year, :prefix => "year", :discard_type => true) %>
21
22
22 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
23 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
23 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, :class => 'icon icon-reload' %>
24 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, :class => 'icon icon-reload' %>
24 </p>
25 </p>
25 <% end %>
26 <% end %>
26
27
27 <%= error_messages_for 'query' %>
28 <%= error_messages_for 'query' %>
28 <% if @query.valid? %>
29 <% if @query.valid? %>
29 <%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>
30 <%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>
30
31
31 <p class="legend cal">
32 <p class="legend cal">
32 <span class="starting"><%= l(:text_tip_issue_begin_day) %></span>
33 <span class="starting"><%= l(:text_tip_issue_begin_day) %></span>
33 <span class="ending"><%= l(:text_tip_issue_end_day) %></span>
34 <span class="ending"><%= l(:text_tip_issue_end_day) %></span>
34 <span class="starting ending"><%= l(:text_tip_issue_begin_end_day) %></span>
35 <span class="starting ending"><%= l(:text_tip_issue_begin_end_day) %></span>
35 </p>
36 </p>
36 <% end %>
37 <% end %>
37
38
38 <% content_for :sidebar do %>
39 <% content_for :sidebar do %>
39 <%= render :partial => 'issues/sidebar' %>
40 <%= render :partial => 'issues/sidebar' %>
40 <% end %>
41 <% end %>
41
42
42 <% html_title(l(:label_calendar)) -%>
43 <% html_title(l(:label_calendar)) -%>
@@ -1,31 +1,31
1 xml.instruct!
1 xml.instruct!
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3 xml.title truncate_single_line(@title, :length => 100)
3 xml.title truncate_single_line(@title, :length => 100)
4 xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false, :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, :escape => false))
5 xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil))
6 xml.id url_for(:controller => 'welcome', :only_path => false)
6 xml.id url_for(:controller => 'welcome', :only_path => false)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
8 xml.author { xml.name "#{Setting.app_title}" }
8 xml.author { xml.name "#{Setting.app_title}" }
9 xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
9 xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
10 @items.each do |item|
10 @items.each do |item|
11 xml.entry do
11 xml.entry do
12 url = url_for(item.event_url(:only_path => false))
12 url = url_for(item.event_url(:only_path => false))
13 if @project
13 if @project
14 xml.title truncate_single_line(item.event_title, :length => 100)
14 xml.title truncate_single_line(item.event_title, :length => 100)
15 else
15 else
16 xml.title truncate_single_line("#{item.project} - #{item.event_title}", :length => 100)
16 xml.title truncate_single_line("#{item.project} - #{item.event_title}", :length => 100)
17 end
17 end
18 xml.link "rel" => "alternate", "href" => url
18 xml.link "rel" => "alternate", "href" => url
19 xml.id url
19 xml.id url
20 xml.updated item.event_datetime.xmlschema
20 xml.updated item.event_datetime.xmlschema
21 author = item.event_author if item.respond_to?(:event_author)
21 author = item.event_author if item.respond_to?(:event_author)
22 xml.author do
22 xml.author do
23 xml.name(author)
23 xml.name(author)
24 xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail
24 xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail
25 end if author
25 end if author
26 xml.content "type" => "html" do
26 xml.content "type" => "html" do
27 xml.text! textilizable(item, :event_description, :only_path => false)
27 xml.text! textilizable(item, :event_description, :only_path => false)
28 end
28 end
29 end
29 end
30 end
30 end
31 end
31 end
@@ -1,8 +1,8
1 <h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
1 <h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
3 &#187; <%=h @custom_field.name %></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
7 <%= submit_tag l(:button_save) %>
7 <%= submit_tag l(:button_save) %>
8 <% end %>
8 <% end %>
@@ -1,9 +1,9
1 <h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
1 <h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
3 &#187; <%= l(:label_custom_field_new) %></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
7 <%= hidden_field_tag 'type', @custom_field.type %>
7 <%= hidden_field_tag 'type', @custom_field.type %>
8 <%= submit_tag l(:button_save) %>
8 <%= submit_tag l(:button_save) %>
9 <% end %>
9 <% end %>
@@ -1,8 +1,8
1 <h2><%=l(:label_document)%></h2>
1 <h2><%=l(:label_document)%></h2>
2
2
3 <% labelled_form_for @document do |f| %>
3 <%= labelled_form_for @document do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <p><%= submit_tag l(:button_save) %></p>
5 <p><%= submit_tag l(:button_save) %></p>
6 <% end %>
6 <% end %>
7
7
8
8
@@ -1,34 +1,34
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_document_new), new_project_document_path(@project), :class => 'icon icon-add',
2 <%= link_to l(:label_document_new), new_project_document_path(@project), :class => 'icon icon-add',
3 :onclick => 'Element.show("add-document"); Form.Element.focus("document_title"); return false;' if User.current.allowed_to?(:manage_documents, @project) %>
3 :onclick => 'Element.show("add-document"); Form.Element.focus("document_title"); return false;' if User.current.allowed_to?(:manage_documents, @project) %>
4 </div>
4 </div>
5
5
6 <div id="add-document" style="display:none;">
6 <div id="add-document" style="display:none;">
7 <h2><%=l(:label_document_new)%></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
9 <%= render :partial => 'form', :locals => {:f => f} %>
10 <p>
10 <p>
11 <%= submit_tag l(:button_create) %>
11 <%= submit_tag l(:button_create) %>
12 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-document")' %>
12 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-document")' %>
13 </p>
13 </p>
14 <% end %>
14 <% end %>
15 </div>
15 </div>
16
16
17 <h2><%=l(:label_document_plural)%></h2>
17 <h2><%=l(:label_document_plural)%></h2>
18
18
19 <% if @grouped.empty? %><p class="nodata"><%= l(:label_no_data) %></p><% end %>
19 <% if @grouped.empty? %><p class="nodata"><%= l(:label_no_data) %></p><% end %>
20
20
21 <% @grouped.keys.sort.each do |group| %>
21 <% @grouped.keys.sort.each do |group| %>
22 <h3><%= group %></h3>
22 <h3><%= group %></h3>
23 <%= render :partial => 'documents/document', :collection => @grouped[group] %>
23 <%= render :partial => 'documents/document', :collection => @grouped[group] %>
24 <% end %>
24 <% end %>
25
25
26 <% content_for :sidebar do %>
26 <% content_for :sidebar do %>
27 <h3><%= l(:label_sort_by, '') %></h3>
27 <h3><%= l(:label_sort_by, '') %></h3>
28 <%= link_to l(:field_category), {:sort_by => 'category'}, :class => (@sort_by == 'category' ? 'selected' :nil) %><br />
28 <%= link_to l(:field_category), {:sort_by => 'category'}, :class => (@sort_by == 'category' ? 'selected' :nil) %><br />
29 <%= link_to l(:label_date), {:sort_by => 'date'}, :class => (@sort_by == 'date' ? 'selected' :nil) %><br />
29 <%= link_to l(:label_date), {:sort_by => 'date'}, :class => (@sort_by == 'date' ? 'selected' :nil) %><br />
30 <%= link_to l(:field_title), {:sort_by => 'title'}, :class => (@sort_by == 'title' ? 'selected' :nil) %><br />
30 <%= link_to l(:field_title), {:sort_by => 'title'}, :class => (@sort_by == 'title' ? 'selected' :nil) %><br />
31 <%= link_to l(:field_author), {:sort_by => 'author'}, :class => (@sort_by == 'author' ? 'selected' :nil) %>
31 <%= link_to l(:field_author), {:sort_by => 'author'}, :class => (@sort_by == 'author' ? 'selected' :nil) %>
32 <% end %>
32 <% end %>
33
33
34 <% html_title(l(:label_document_plural)) -%>
34 <% html_title(l(:label_document_plural)) -%>
@@ -1,6 +1,6
1 <h2><%=l(:label_document_new)%></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <p><%= submit_tag l(:button_create) %></p>
5 <p><%= submit_tag l(:button_create) %></p>
6 <% end %>
6 <% end %>
@@ -1,34 +1,34
1 <div class="contextual">
1 <div class="contextual">
2 <% if User.current.allowed_to?(:manage_documents, @project) %>
2 <% if User.current.allowed_to?(:manage_documents, @project) %>
3 <%= link_to l(:button_edit), edit_document_path(@document), :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
3 <%= link_to l(:button_edit), edit_document_path(@document), :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
4 <%= link_to l(:button_delete), document_path(@document), :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %>
4 <%= link_to l(:button_delete), document_path(@document), :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %>
5 <% end %>
5 <% end %>
6 </div>
6 </div>
7
7
8 <h2><%=h @document.title %></h2>
8 <h2><%=h @document.title %></h2>
9
9
10 <p><em><%=h @document.category.name %><br />
10 <p><em><%=h @document.category.name %><br />
11 <%= format_date @document.created_on %></em></p>
11 <%= format_date @document.created_on %></em></p>
12 <div class="wiki">
12 <div class="wiki">
13 <%= textilizable @document.description, :attachments => @document.attachments %>
13 <%= textilizable @document.description, :attachments => @document.attachments %>
14 </div>
14 </div>
15
15
16 <h3><%= l(:label_attachment_plural) %></h3>
16 <h3><%= l(:label_attachment_plural) %></h3>
17 <%= link_to_attachments @document %>
17 <%= link_to_attachments @document %>
18
18
19 <% if authorize_for('documents', 'add_attachment') %>
19 <% if authorize_for('documents', 'add_attachment') %>
20 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
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 :id => 'attach_files_link' %></p>
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 <div class="box">
23 <div class="box">
24 <p><%= render :partial => 'attachments/form' %></p>
24 <p><%= render :partial => 'attachments/form' %></p>
25 </div>
25 </div>
26 <%= submit_tag l(:button_add) %>
26 <%= submit_tag l(:button_add) %>
27 <% end %>
27 <% end %>
28 <% end %>
28 <% end %>
29
29
30 <% html_title @document.title -%>
30 <% html_title @document.title -%>
31
31
32 <% content_for :header_tags do %>
32 <% content_for :header_tags do %>
33 <%= stylesheet_link_tag 'scm' %>
33 <%= stylesheet_link_tag 'scm' %>
34 <% end %>
34 <% end %>
@@ -1,12 +1,12
1 <h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
1 <h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
2
2
3 <% form_tag({}, :method => :delete) do %>
3 <%= form_tag({}, :method => :delete) do %>
4 <div class="box">
4 <div class="box">
5 <p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
5 <p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
6 <p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
6 <p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
7 <%= select_tag 'reassign_to_id', ("<option>--- #{l(:actionview_instancetag_blank_option)} ---</option>" + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
7 <%= select_tag 'reassign_to_id', ("<option>--- #{l(:actionview_instancetag_blank_option)} ---</option>" + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
8 </div>
8 </div>
9
9
10 <%= submit_tag l(:button_apply) %>
10 <%= submit_tag l(:button_apply) %>
11 <%= link_to l(:button_cancel), enumerations_path %>
11 <%= link_to l(:button_cancel), enumerations_path %>
12 <% end %>
12 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=h @enumeration %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,7 +1,7
1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=l(:label_enumeration_new)%></h2>
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 <%= f.hidden_field :type %>
4 <%= f.hidden_field :type %>
5 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= render :partial => 'form', :locals => {:f => f} %>
6 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create) %>
7 <% end %>
7 <% end %>
@@ -1,16 +1,16
1 <h2><%=l(:label_attachment_new)%></h2>
1 <h2><%=l(:label_attachment_new)%></h2>
2
2
3 <%= error_messages_for 'attachment' %>
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 <div class="box">
5 <div class="box">
6
6
7 <% if @versions.any? %>
7 <% if @versions.any? %>
8 <p><label for="version_id"><%=l(:field_version)%></label>
8 <p><label for="version_id"><%=l(:field_version)%></label>
9 <%= select_tag "version_id", content_tag('option', '') +
9 <%= select_tag "version_id", content_tag('option', '') +
10 options_from_collection_for_select(@versions, "id", "name") %></p>
10 options_from_collection_for_select(@versions, "id", "name") %></p>
11 <% end %>
11 <% end %>
12
12
13 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
13 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
14 </div>
14 </div>
15 <%= submit_tag l(:button_add) %>
15 <%= submit_tag l(:button_add) %>
16 <% end %>
16 <% end %>
@@ -1,187 +1,190
1 <% @gantt.view = self %>
1 <% @gantt.view = self %>
2 <h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
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 <%= hidden_field_tag 'set_filter', '1' %>
8 <%= hidden_field_tag 'set_filter', '1' %>
6 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
9 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
7 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
10 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
8 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
11 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
9 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
12 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
10 </div>
13 </div>
11 </fieldset>
14 </fieldset>
12
15
13 <p class="contextual">
16 <p class="contextual">
14 <%= gantt_zoom_link(@gantt, :in) %>
17 <%= gantt_zoom_link(@gantt, :in) %>
15 <%= gantt_zoom_link(@gantt, :out) %>
18 <%= gantt_zoom_link(@gantt, :out) %>
16 </p>
19 </p>
17
20
18 <p class="buttons">
21 <p class="buttons">
19 <%= text_field_tag 'months', @gantt.months, :size => 2 %>
22 <%= text_field_tag 'months', @gantt.months, :size => 2 %>
20 <%= l(:label_months_from) %>
23 <%= l(:label_months_from) %>
21 <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
24 <%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %>
22 <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
25 <%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %>
23 <%= hidden_field_tag 'zoom', @gantt.zoom %>
26 <%= hidden_field_tag 'zoom', @gantt.zoom %>
24
27
25 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
28 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
26 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, :class => 'icon icon-reload' %>
29 <%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, :class => 'icon icon-reload' %>
27 </p>
30 </p>
28 <% end %>
31 <% end %>
29
32
30 <%= error_messages_for 'query' %>
33 <%= error_messages_for 'query' %>
31 <% if @query.valid? %>
34 <% if @query.valid? %>
32 <% zoom = 1
35 <% zoom = 1
33 @gantt.zoom.times { zoom = zoom * 2 }
36 @gantt.zoom.times { zoom = zoom * 2 }
34
37
35 subject_width = 330
38 subject_width = 330
36 header_heigth = 18
39 header_heigth = 18
37
40
38 headers_height = header_heigth
41 headers_height = header_heigth
39 show_weeks = false
42 show_weeks = false
40 show_days = false
43 show_days = false
41
44
42 if @gantt.zoom > 1
45 if @gantt.zoom > 1
43 show_weeks = true
46 show_weeks = true
44 headers_height = 2 * header_heigth
47 headers_height = 2 * header_heigth
45 if @gantt.zoom > 2
48 if @gantt.zoom > 2
46 show_days = true
49 show_days = true
47 headers_height = 3 * header_heigth
50 headers_height = 3 * header_heigth
48 end
51 end
49 end
52 end
50
53
51 # Width of the entire chart
54 # Width of the entire chart
52 g_width = ((@gantt.date_to - @gantt.date_from + 1) * zoom).to_i
55 g_width = ((@gantt.date_to - @gantt.date_from + 1) * zoom).to_i
53
56
54 @gantt.render(:top => headers_height + 8, :zoom => zoom, :g_width => g_width, :subject_width => subject_width)
57 @gantt.render(:top => headers_height + 8, :zoom => zoom, :g_width => g_width, :subject_width => subject_width)
55
58
56 g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max
59 g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max
57 t_height = g_height + headers_height
60 t_height = g_height + headers_height
58
61
59
62
60 %>
63 %>
61
64
62 <% if @gantt.truncated %>
65 <% if @gantt.truncated %>
63 <p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
66 <p class="warning"><%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %></p>
64 <% end %>
67 <% end %>
65
68
66 <table style="width:100%; border:0; border-collapse: collapse;">
69 <table style="width:100%; border:0; border-collapse: collapse;">
67 <tr>
70 <tr>
68 <td style="width:<%= subject_width %>px; padding:0px;">
71 <td style="width:<%= subject_width %>px; padding:0px;">
69
72
70 <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
73 <div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
71 <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
74 <div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
72 <div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
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 <div class="gantt_subjects">
77 <div class="gantt_subjects">
75 <%= @gantt.subjects.html_safe %>
78 <%= @gantt.subjects.html_safe %>
76 </div>
79 </div>
77
80
78 </div>
81 </div>
79 </td>
82 </td>
80 <td>
83 <td>
81
84
82 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
85 <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;">
83 <div style="width:<%= g_width - 1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
86 <div style="width:<%= g_width - 1 %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr">&nbsp;</div>
84 <%
87 <%
85 #
88 #
86 # Months headers
89 # Months headers
87 #
90 #
88 month_f = @gantt.date_from
91 month_f = @gantt.date_from
89 left = 0
92 left = 0
90 height = (show_weeks ? header_heigth : header_heigth + g_height)
93 height = (show_weeks ? header_heigth : header_heigth + g_height)
91 @gantt.months.times do
94 @gantt.months.times do
92 width = (((month_f >> 1) - month_f) * zoom - 1).to_i
95 width = (((month_f >> 1) - month_f) * zoom - 1).to_i
93 %>
96 %>
94 <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
97 <div style="left:<%= left %>px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
95 <%= 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}"%>
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 </div>
99 </div>
97 <%
100 <%
98 left = left + width + 1
101 left = left + width + 1
99 month_f = month_f >> 1
102 month_f = month_f >> 1
100 end %>
103 end %>
101
104
102 <%
105 <%
103 #
106 #
104 # Weeks headers
107 # Weeks headers
105 #
108 #
106 if show_weeks
109 if show_weeks
107 left = 0
110 left = 0
108 height = (show_days ? header_heigth - 1 : header_heigth - 1 + g_height)
111 height = (show_days ? header_heigth - 1 : header_heigth - 1 + g_height)
109 if @gantt.date_from.cwday == 1
112 if @gantt.date_from.cwday == 1
110 # @date_from is monday
113 # @date_from is monday
111 week_f = @gantt.date_from
114 week_f = @gantt.date_from
112 else
115 else
113 # find next monday after @date_from
116 # find next monday after @date_from
114 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
117 week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1)
115 width = (7 - @gantt.date_from.cwday + 1) * zoom - 1
118 width = (7 - @gantt.date_from.cwday + 1) * zoom - 1
116 %>
119 %>
117 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div>
120 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">&nbsp;</div>
118 <%
121 <%
119 left = left + width + 1
122 left = left + width + 1
120 end %>
123 end %>
121 <%
124 <%
122 while week_f <= @gantt.date_to
125 while week_f <= @gantt.date_to
123 width = ((week_f + 6 <= @gantt.date_to) ? 7 * zoom - 1 : (@gantt.date_to - week_f + 1) * zoom - 1).to_i
126 width = ((week_f + 6 <= @gantt.date_to) ? 7 * zoom - 1 : (@gantt.date_to - week_f + 1) * zoom - 1).to_i
124 %>
127 %>
125 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
128 <div style="left:<%= left %>px;top:19px;width:<%= width %>px;height:<%= height %>px;" class="gantt_hdr">
126 <small><%= week_f.cweek if width >= 16 %></small>
129 <small><%= week_f.cweek if width >= 16 %></small>
127 </div>
130 </div>
128 <%
131 <%
129 left = left + width + 1
132 left = left + width + 1
130 week_f = week_f + 7
133 week_f = week_f + 7
131 end
134 end
132 end %>
135 end %>
133
136
134 <%
137 <%
135 #
138 #
136 # Days headers
139 # Days headers
137 #
140 #
138 if show_days
141 if show_days
139 left = 0
142 left = 0
140 height = g_height + header_heigth - 1
143 height = g_height + header_heigth - 1
141 wday = @gantt.date_from.cwday
144 wday = @gantt.date_from.cwday
142 (@gantt.date_to - @gantt.date_from + 1).to_i.times do
145 (@gantt.date_to - @gantt.date_from + 1).to_i.times do
143 width = zoom - 1
146 width = zoom - 1
144 %>
147 %>
145 <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">
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 <%= day_name(wday).first %>
149 <%= day_name(wday).first %>
147 </div>
150 </div>
148 <%
151 <%
149 left = left + width + 1
152 left = left + width + 1
150 wday = wday + 1
153 wday = wday + 1
151 wday = 1 if wday > 7
154 wday = 1 if wday > 7
152 end
155 end
153 end %>
156 end %>
154
157
155 <%= @gantt.lines.html_safe %>
158 <%= @gantt.lines.html_safe %>
156
159
157 <%
160 <%
158 #
161 #
159 # Today red line (excluded from cache)
162 # Today red line (excluded from cache)
160 #
163 #
161 if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
164 if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
162 <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>
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 <% end %>
166 <% end %>
164
167
165 </div>
168 </div>
166 </td>
169 </td>
167 </tr>
170 </tr>
168 </table>
171 </table>
169
172
170 <table style="width:100%">
173 <table style="width:100%">
171 <tr>
174 <tr>
172 <td align="left"><%= link_to_content_update("\xc2\xab " + l(:label_previous), params.merge(@gantt.params_previous)) %></td>
175 <td align="left"><%= link_to_content_update("\xc2\xab " + l(:label_previous), params.merge(@gantt.params_previous)) %></td>
173 <td align="right"><%= link_to_content_update(l(:label_next) + " \xc2\xbb", params.merge(@gantt.params_next)) %></td>
176 <td align="right"><%= link_to_content_update(l(:label_next) + " \xc2\xbb", params.merge(@gantt.params_next)) %></td>
174 </tr>
177 </tr>
175 </table>
178 </table>
176
179
177 <% other_formats_links do |f| %>
180 <% other_formats_links do |f| %>
178 <%= f.link_to 'PDF', :url => params.merge(@gantt.params) %>
181 <%= f.link_to 'PDF', :url => params.merge(@gantt.params) %>
179 <%= f.link_to('PNG', :url => params.merge(@gantt.params)) if @gantt.respond_to?('to_image') %>
182 <%= f.link_to('PNG', :url => params.merge(@gantt.params)) if @gantt.respond_to?('to_image') %>
180 <% end %>
183 <% end %>
181 <% end # query.valid? %>
184 <% end # query.valid? %>
182
185
183 <% content_for :sidebar do %>
186 <% content_for :sidebar do %>
184 <%= render :partial => 'issues/sidebar' %>
187 <%= render :partial => 'issues/sidebar' %>
185 <% end %>
188 <% end %>
186
189
187 <% html_title(l(:label_gantt)) -%>
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 <%= render :partial => 'form', :locals => { :f => f } %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <%= submit_tag l(:button_save) %>
3 <%= submit_tag l(:button_save) %>
4 <% end %>
4 <% end %>
@@ -1,69 +1,70
1 <% roles = Role.find_all_givable %>
1 <% roles = Role.find_all_givable %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
3
3
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
5 <% if @group.memberships.any? %>
5 <% if @group.memberships.any? %>
6 <table class="list memberships">
6 <table class="list memberships">
7 <thead><tr>
7 <thead><tr>
8 <th><%= l(:label_project) %></th>
8 <th><%= l(:label_project) %></th>
9 <th><%= l(:label_role_plural) %></th>
9 <th><%= l(:label_role_plural) %></th>
10 <th style="width:15%"></th>
10 <th style="width:15%"></th>
11 </tr></thead>
11 </tr></thead>
12 <tbody>
12 <tbody>
13 <% @group.memberships.each do |membership| %>
13 <% @group.memberships.each do |membership| %>
14 <% next if membership.new_record? %>
14 <% next if membership.new_record? %>
15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 <td class="project"><%=h membership.project %></td>
16 <td class="project"><%=h membership.project %></td>
17 <td class="roles">
17 <td class="roles">
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
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 },
19 <%= form_for(:membership, :remote => true,
20 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
20 :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 <p><% roles.each do |role| %>
22 <p><% roles.each do |role| %>
22 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <% end %></p>
24 <% end %></p>
24 <p><%= submit_tag l(:button_change) %>
25 <p><%= submit_tag l(:button_change) %>
25 <%= link_to_function(
26 <%= link_to_function(
26 l(:button_cancel),
27 l(:button_cancel),
27 "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;"
28 "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;"
28 ) %></p>
29 ) %></p>
29 <% end %>
30 <% end %>
30 </td>
31 </td>
31 <td class="buttons">
32 <td class="buttons">
32 <%= link_to_function(
33 <%= link_to_function(
33 l(:button_edit),
34 l(:button_edit),
34 "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;",
35 "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;",
35 :class => 'icon icon-edit'
36 :class => 'icon icon-edit'
36 ) %>
37 ) %>
37 <%= link_to_remote(
38 <%= link_to_remote(
38 l(:button_delete),
39 l(:button_delete),
39 { :url => { :controller => 'groups',
40 { :url => { :controller => 'groups',
40 :action => 'destroy_membership',
41 :action => 'destroy_membership',
41 :id => @group,
42 :id => @group,
42 :membership_id => membership },
43 :membership_id => membership },
43 :method => :post },
44 :method => :post },
44 :class => 'icon icon-del' ) %>
45 :class => 'icon icon-del' ) %>
45 </td>
46 </td>
46 </tr>
47 </tr>
47 <% end; reset_cycle %>
48 <% end; reset_cycle %>
48 </tbody>
49 </tbody>
49 </table>
50 </table>
50 <% else %>
51 <% else %>
51 <p class="nodata"><%= l(:label_no_data) %></p>
52 <p class="nodata"><%= l(:label_no_data) %></p>
52 <% end %>
53 <% end %>
53 </div>
54 </div>
54
55
55 <div class="splitcontentright">
56 <div class="splitcontentright">
56 <% if projects.any? %>
57 <% if projects.any? %>
57 <fieldset><legend><%=l(:label_project_new)%></legend>
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 <%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
60 <%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
60 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
61 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
61 <p><%= l(:label_role_plural) %>:
62 <p><%= l(:label_role_plural) %>:
62 <% roles.each do |role| %>
63 <% roles.each do |role| %>
63 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
64 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
64 <% end %></p>
65 <% end %></p>
65 <p><%= submit_tag l(:button_add) %></p>
66 <p><%= submit_tag l(:button_add) %></p>
66 <% end %>
67 <% end %>
67 </fieldset>
68 </fieldset>
68 <% end %>
69 <% end %>
69 </div>
70 </div>
@@ -1,55 +1,56
1 <div class="splitcontentleft">
1 <div class="splitcontentleft">
2 <% if @group.users.any? %>
2 <% if @group.users.any? %>
3 <table class="list users">
3 <table class="list users">
4 <thead><tr>
4 <thead><tr>
5 <th><%= l(:label_user) %></th>
5 <th><%= l(:label_user) %></th>
6 <th style="width:15%"></th>
6 <th style="width:15%"></th>
7 </tr></thead>
7 </tr></thead>
8 <tbody>
8 <tbody>
9 <% @group.users.sort.each do |user| %>
9 <% @group.users.sort.each do |user| %>
10 <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
10 <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
11 <td class="user"><%= link_to_user user %></td>
11 <td class="user"><%= link_to_user user %></td>
12 <td class="buttons">
12 <td class="buttons">
13 <%= link_to_remote(
13 <%= link_to_remote(
14 l(:button_delete),
14 l(:button_delete),
15 { :url => group_user_path(@group, :user_id => user),
15 { :url => group_user_path(@group, :user_id => user),
16 :method => :delete },
16 :method => :delete },
17 :class => 'icon icon-del'
17 :class => 'icon icon-del'
18 ) %>
18 ) %>
19 </td>
19 </td>
20 </tr>
20 </tr>
21 <% end %>
21 <% end %>
22 </tbody>
22 </tbody>
23 </table>
23 </table>
24 <% else %>
24 <% else %>
25 <p class="nodata"><%= l(:label_no_data) %></p>
25 <p class="nodata"><%= l(:label_no_data) %></p>
26 <% end %>
26 <% end %>
27 </div>
27 </div>
28
28
29 <div class="splitcontentright">
29 <div class="splitcontentright">
30 <% users = User.active.not_in_group(@group).all(:limit => 100) %>
30 <% users = User.active.not_in_group(@group).all(:limit => 100) %>
31 <% if users.any? %>
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 <fieldset><legend><%=l(:label_user_new)%></legend>
34 <fieldset><legend><%=l(:label_user_new)%></legend>
34
35
35 <p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
36 <p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
36 <%= observe_field(:user_search,
37 <%= observe_field(:user_search,
37 :frequency => 0.5,
38 :frequency => 0.5,
38 :update => :users,
39 :update => :users,
39 :url => autocomplete_for_user_group_path(@group),
40 :url => autocomplete_for_user_group_path(@group),
40 :method => :get,
41 :method => :get,
41 :before => '$("user_search").addClassName("ajax-loading")',
42 :before => '$("user_search").addClassName("ajax-loading")',
42 :complete => '$("user_search").removeClassName("ajax-loading")',
43 :complete => '$("user_search").removeClassName("ajax-loading")',
43 :with => 'q')
44 :with => 'q')
44 %>
45 %>
45
46
46 <div id="users">
47 <div id="users">
47 <%= principals_check_box_tags 'user_ids[]', users %>
48 <%= principals_check_box_tags 'user_ids[]', users %>
48 </div>
49 </div>
49
50
50 <p><%= submit_tag l(:button_add) %></p>
51 <p><%= submit_tag l(:button_add) %></p>
51 </fieldset>
52 </fieldset>
52 <% end %>
53 <% end %>
53 <% end %>
54 <% end %>
54
55
55 </div>
56 </div>
@@ -1,9 +1,9
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= l(:label_group_new) %></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <p>
5 <p>
6 <%= f.submit l(:button_create) %>
6 <%= f.submit l(:button_create) %>
7 <%= f.submit l(:button_create_and_continue), :name => 'continue' %>
7 <%= f.submit l(:button_create_and_continue), :name => 'continue' %>
8 </p>
8 </p>
9 <% end %>
9 <% end %>
@@ -1,16 +1,16
1 <h2><%=l(:label_issue_category)%>: <%=h @category.name %></h2>
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 <div class="box">
4 <div class="box">
5 <p><strong><%= l(:text_issue_category_destroy_question, @issue_count) %></strong></p>
5 <p><strong><%= l(:text_issue_category_destroy_question, @issue_count) %></strong></p>
6 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %></label><br />
6 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %></label><br />
7 <% if @categories.size > 0 %>
7 <% if @categories.size > 0 %>
8 <label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_issue_category_reassign_to) %></label>:
8 <label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_issue_category_reassign_to) %></label>:
9 <%= label_tag "reassign_to_id", l(:description_issue_category_reassign), :class => "hidden-for-sighted" %>
9 <%= label_tag "reassign_to_id", l(:description_issue_category_reassign), :class => "hidden-for-sighted" %>
10 <%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %></p>
10 <%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %></p>
11 <% end %>
11 <% end %>
12 </div>
12 </div>
13
13
14 <%= submit_tag l(:button_apply) %>
14 <%= submit_tag l(:button_apply) %>
15 <%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' %>
15 <%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' %>
16 <% end %>
16 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=h @issue_status %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=l(:label_issue_status_new)%></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -1,7 +1,7
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => '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 <%= watcher_tag(@issue, User.current) %>
4 <%= watcher_tag(@issue, User.current) %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon icon-copy' %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon icon-copy' %>
6 <%= link_to 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) %>
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 </div>
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 <div class="splitcontent">
3 <div class="splitcontent">
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
5 <% if @issue.safe_attribute? 'status_id' %>
5 <% if @issue.safe_attribute? 'status_id' %>
6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
7 <% else %>
7 <% else %>
8 <p><label><%= l(:field_status) %></label> <%= h(@issue.status.name) %></p>
8 <p><label><%= l(:field_status) %></label> <%= h(@issue.status.name) %></p>
9 <% end %>
9 <% end %>
10
10
11 <% if @issue.safe_attribute? 'priority_id' %>
11 <% if @issue.safe_attribute? 'priority_id' %>
12 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
12 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
13 <% end %>
13 <% end %>
14
14
15 <% if @issue.safe_attribute? 'assigned_to_id' %>
15 <% if @issue.safe_attribute? 'assigned_to_id' %>
16 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
16 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
17 <% end %>
17 <% end %>
18
18
19 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
19 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
20 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
20 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
21 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
21 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
22 l(:label_issue_category_new),
22 l(:label_issue_category_new),
23 'issue_category[name]',
23 'issue_category[name]',
24 {:controller => 'issue_categories', :action => 'create', :project_id => @issue.project},
24 {:controller => 'issue_categories', :action => 'create', :project_id => @issue.project},
25 :title => l(:label_issue_category_new),
25 :title => l(:label_issue_category_new),
26 :tabindex => 199) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
26 :tabindex => 199) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
27 <% end %>
27 <% end %>
28
28
29 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
29 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
30 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
30 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
31 <%= link_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
31 <%= link_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
32 {:url => new_project_version_path(@issue.project), :method => 'get'},
32 {:url => new_project_version_path(@issue.project), :method => 'get'},
33 :title => l(:label_version_new),
33 :title => l(:label_version_new),
34 :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %>
34 :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %>
35 </p>
35 </p>
36 <% end %>
36 <% end %>
37 </div>
37 </div>
38
38
39 <div class="splitcontentright">
39 <div class="splitcontentright">
40 <% if @issue.safe_attribute? 'parent_issue_id' %>
40 <% if @issue.safe_attribute? 'parent_issue_id' %>
41 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
41 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
42 <div id="parent_issue_candidates" class="autocomplete"></div>
42 <div id="parent_issue_candidates" class="autocomplete"></div>
43 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @issue.project) }')" %>
43 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @issue.project) }')" %>
44 <% end %>
44 <% end %>
45
45
46 <% if @issue.safe_attribute? 'start_date' %>
46 <% if @issue.safe_attribute? 'start_date' %>
47 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
47 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
48 <% end %>
48 <% end %>
49
49
50 <% if @issue.safe_attribute? 'due_date' %>
50 <% if @issue.safe_attribute? 'due_date' %>
51 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
51 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
52 <% end %>
52 <% end %>
53
53
54 <% if @issue.safe_attribute? 'estimated_hours' %>
54 <% if @issue.safe_attribute? 'estimated_hours' %>
55 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
55 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
56 <% end %>
56 <% end %>
57
57
58 <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
58 <% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %>
59 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
59 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
60 <% end %>
60 <% end %>
61 </div>
61 </div>
62 </div>
62 </div>
63
63
64 <% if @issue.safe_attribute? 'custom_field_values' %>
64 <% if @issue.safe_attribute? 'custom_field_values' %>
65 <%= render :partial => 'issues/form_custom_fields' %>
65 <%= render :partial => 'issues/form_custom_fields' %>
66 <% end %>
66 <% end %>
67
67
68 <% end %>
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 <%= error_messages_for 'issue', 'time_entry' %>
2 <%= error_messages_for 'issue', 'time_entry' %>
3 <%= render :partial => 'conflict' if @conflict %>
3 <%= render :partial => 'conflict' if @conflict %>
4 <div class="box">
4 <div class="box">
5 <% if @edit_allowed || !@allowed_statuses.empty? %>
5 <% if @edit_allowed || !@allowed_statuses.empty? %>
6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
6 <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
7 <div id="all_attributes">
7 <div id="all_attributes">
8 <%= render :partial => 'form', :locals => {:f => f} %>
8 <%= render :partial => 'form', :locals => {:f => f} %>
9 </div>
9 </div>
10 </fieldset>
10 </fieldset>
11 <% end %>
11 <% end %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
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 <div class="splitcontentleft">
15 <div class="splitcontentleft">
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
17 </div>
17 </div>
18 <div class="splitcontentright">
18 <div class="splitcontentright">
19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
19 <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
20 </div>
20 </div>
21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
21 <p><%= time_entry.text_field :comments, :size => 60 %></p>
22 <% @time_entry.custom_field_values.each do |value| %>
22 <% @time_entry.custom_field_values.each do |value| %>
23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
24 <% end %>
24 <% end %>
25 <% end %>
25 <% end %>
26 </fieldset>
26 </fieldset>
27 <% end %>
27 <% end %>
28
28
29 <fieldset><legend><%= l(:field_notes) %></legend>
29 <fieldset><legend><%= l(:field_notes) %></legend>
30 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
30 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
31 <%= wikitoolbar_for 'notes' %>
31 <%= wikitoolbar_for 'notes' %>
32 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
32 <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
33
33
34 <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
34 <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
35 </fieldset>
35 </fieldset>
36 </div>
36 </div>
37
37
38 <%= f.hidden_field :lock_version %>
38 <%= f.hidden_field :lock_version %>
39 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
39 <%= hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
40 <%= submit_tag l(:button_submit) %>
40 <%= submit_tag l(:button_submit) %>
41 <%= link_to_remote l(:label_preview),
41 <%= link_to_remote l(:label_preview),
42 { :url => preview_edit_issue_path(:project_id => @project, :id => @issue),
42 { :url => preview_edit_issue_path(:project_id => @project, :id => @issue),
43 :method => 'post',
43 :method => 'post',
44 :update => 'preview',
44 :update => 'preview',
45 :with => 'Form.serialize("issue-form")',
45 :with => 'Form.serialize("issue-form")',
46 :complete => "Element.scrollTo('preview')"
46 :complete => "Element.scrollTo('preview')"
47 }, :accesskey => accesskey(:preview) %>
47 }, :accesskey => accesskey(:preview) %>
48 <% end %>
48 <% end %>
49
49
50 <div id="preview" class="wiki"></div>
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 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
2 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
3
3
4 <% if @issue.safe_attribute? 'is_private' %>
4 <% if @issue.safe_attribute? 'is_private' %>
5 <p style="float:right; margin-right:1em;">
5 <p style="float:right; margin-right:1em;">
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>
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 </p>
7 </p>
8 <% end %>
8 <% end %>
9
9
10 <% if @issue.safe_attribute? 'project_id' %>
10 <% if @issue.safe_attribute? 'project_id' %>
11 <p><%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), :required => true %></p>
11 <p><%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), :required => true %></p>
12 <%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue, :project_change => '1'),
12 <%= observe_field :issue_project_id, :url => project_issue_form_path(@project, :id => @issue, :project_change => '1'),
13 :with => "Form.serialize('issue-form')" %>
13 :with => "Form.serialize('issue-form')" %>
14 <% end %>
14 <% end %>
15
15
16 <% if @issue.safe_attribute? 'tracker_id' %>
16 <% if @issue.safe_attribute? 'tracker_id' %>
17 <p><%= f.select :tracker_id, @issue.project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
17 <p><%= f.select :tracker_id, @issue.project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
18 <%= observe_field :issue_tracker_id, :url => project_issue_form_path(@project, :id => @issue),
18 <%= observe_field :issue_tracker_id, :url => project_issue_form_path(@project, :id => @issue),
19 :with => "Form.serialize('issue-form')" %>
19 :with => "Form.serialize('issue-form')" %>
20 <% end %>
20 <% end %>
21
21
22 <% if @issue.safe_attribute? 'subject' %>
22 <% if @issue.safe_attribute? 'subject' %>
23 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
23 <p><%= f.text_field :subject, :size => 80, :required => true %></p>
24 <% end %>
24 <% end %>
25
25
26 <% if @issue.safe_attribute? 'description' %>
26 <% if @issue.safe_attribute? 'description' %>
27 <p>
27 <p>
28 <label><%= l(:field_description) %></label>
28 <label><%= l(:field_description) %></label>
29 <%= link_to_function image_tag('edit.png'),
29 <%= link_to_function image_tag('edit.png'),
30 'Element.hide(this); Effect.toggle("issue_description_and_toolbar", "appear", {duration:0.3})' unless @issue.new_record? %>
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 <%= f.text_area :description,
32 <%= f.text_area :description,
33 :cols => 60,
33 :cols => 60,
34 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
34 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
35 :accesskey => accesskey(:edit),
35 :accesskey => accesskey(:edit),
36 :class => 'wiki-edit',
36 :class => 'wiki-edit',
37 :no_label => true %>
37 :no_label => true %>
38 <% end %>
38 <% end %>
39 </p>
39 </p>
40 <%= wikitoolbar_for 'issue_description' %>
40 <%= wikitoolbar_for 'issue_description' %>
41 <% end %>
41 <% end %>
42
42
43 <div id="attributes" class="attributes">
43 <div id="attributes" class="attributes">
44 <%= render :partial => 'issues/attributes' %>
44 <%= render :partial => 'issues/attributes' %>
45 </div>
45 </div>
46
46
47 <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
47 <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
48 <% end %>
48 <% end %>
@@ -1,37 +1,37
1 <% form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list issues">
4 <table class="list issues">
5 <thead><tr>
5 <thead><tr>
6 <th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
6 <th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
7 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
7 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
8 </th>
8 </th>
9 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
9 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
10 <% query.columns.each do |column| %>
10 <% query.columns.each do |column| %>
11 <%= column_header(column) %>
11 <%= column_header(column) %>
12 <% end %>
12 <% end %>
13 </tr></thead>
13 </tr></thead>
14 <% previous_group = false %>
14 <% previous_group = false %>
15 <tbody>
15 <tbody>
16 <% issue_list(issues) do |issue, level| -%>
16 <% issue_list(issues) do |issue, level| -%>
17 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
17 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
18 <% reset_cycle %>
18 <% reset_cycle %>
19 <tr class="group open">
19 <tr class="group open">
20 <td colspan="<%= query.columns.size + 2 %>">
20 <td colspan="<%= query.columns.size + 2 %>">
21 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
21 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
22 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
22 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
23 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
23 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
24 </td>
24 </td>
25 </tr>
25 </tr>
26 <% previous_group = group %>
26 <% previous_group = group %>
27 <% end %>
27 <% end %>
28 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
28 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
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>
30 <td class="id"><%= link_to issue.id, issue_path(issue) %></td>
31 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.css_classes %><% end %>
31 <%= raw query.columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
32 </tr>
32 </tr>
33 <% end -%>
33 <% end -%>
34 </tbody>
34 </tbody>
35 </table>
35 </table>
36 </div>
36 </div>
37 <% end -%>
37 <% end -%>
@@ -1,29 +1,29
1 <% if issues && issues.any? %>
1 <% if issues && issues.any? %>
2 <% form_tag({}) do %>
2 <%= form_tag({}) do %>
3 <table class="list issues">
3 <table class="list issues">
4 <thead><tr>
4 <thead><tr>
5 <th>#</th>
5 <th>#</th>
6 <th><%=l(:field_project)%></th>
6 <th><%=l(:field_project)%></th>
7 <th><%=l(:field_tracker)%></th>
7 <th><%=l(:field_tracker)%></th>
8 <th><%=l(:field_subject)%></th>
8 <th><%=l(:field_subject)%></th>
9 </tr></thead>
9 </tr></thead>
10 <tbody>
10 <tbody>
11 <% for issue in issues %>
11 <% for issue in issues %>
12 <tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
12 <tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
13 <td class="id">
13 <td class="id">
14 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %>
14 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %>
15 <%= link_to(h(issue.id), :controller => 'issues', :action => 'show', :id => issue) %>
15 <%= link_to(h(issue.id), :controller => 'issues', :action => 'show', :id => issue) %>
16 </td>
16 </td>
17 <td class="project"><%= link_to_project(issue.project) %></td>
17 <td class="project"><%= link_to_project(issue.project) %></td>
18 <td class="tracker"><%=h issue.tracker %></td>
18 <td class="tracker"><%=h issue.tracker %></td>
19 <td class="subject">
19 <td class="subject">
20 <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>)
20 <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>)
21 </td>
21 </td>
22 </tr>
22 </tr>
23 <% end %>
23 <% end %>
24 </tbody>
24 </tbody>
25 </table>
25 </table>
26 <% end %>
26 <% end %>
27 <% else %>
27 <% else %>
28 <p class="nodata"><%= l(:label_no_data) %></p>
28 <p class="nodata"><%= l(:label_no_data) %></p>
29 <% end %>
29 <% end %>
@@ -1,127 +1,127
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
2
2
3 <ul><%= @issues.collect {|i|
3 <ul><%= @issues.collect {|i|
4 content_tag('li',
4 content_tag('li',
5 link_to(h("#{i.tracker} ##{i.id}"),
5 link_to(h("#{i.tracker} ##{i.id}"),
6 { :action => 'show', :id => i }
6 { :action => 'show', :id => i }
7 ) + h(": #{i.subject}"))
7 ) + h(": #{i.subject}"))
8 }.join("\n").html_safe %></ul>
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 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
11 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
12 <div class="box tabular">
12 <div class="box tabular">
13 <fieldset class="attributes">
13 <fieldset class="attributes">
14 <legend><%= l(:label_change_properties) %></legend>
14 <legend><%= l(:label_change_properties) %></legend>
15
15
16 <div class="splitcontentleft">
16 <div class="splitcontentleft">
17 <% if @allowed_projects.present? %>
17 <% if @allowed_projects.present? %>
18 <p>
18 <p>
19 <label for="issue_project_id"><%= l(:field_project) %></label>
19 <label for="issue_project_id"><%= l(:field_project) %></label>
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)) %>
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 </p>
21 </p>
22 <%= observe_field :issue_project_id, :url => {:action => 'bulk_edit'},
22 <%= observe_field :issue_project_id, :url => {:action => 'bulk_edit'},
23 :update => 'content',
23 :update => 'content',
24 :with => "Form.serialize('bulk_edit_form')" %>
24 :with => "Form.serialize('bulk_edit_form')" %>
25 <% end %>
25 <% end %>
26 <p>
26 <p>
27 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
27 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
28 <%= select_tag('issue[tracker_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@trackers, :id, :name)) %>
28 <%= select_tag('issue[tracker_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@trackers, :id, :name)) %>
29 </p>
29 </p>
30 <% if @available_statuses.any? %>
30 <% if @available_statuses.any? %>
31 <p>
31 <p>
32 <label for='issue_status_id'><%= l(:field_status) %></label>
32 <label for='issue_status_id'><%= l(:field_status) %></label>
33 <%= select_tag('issue[status_id]',content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@available_statuses, :id, :name)) %>
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 </p>
34 </p>
35 <% end %>
35 <% end %>
36 <p>
36 <p>
37 <label for='issue_priority_id'><%= l(:field_priority) %></label>
37 <label for='issue_priority_id'><%= l(:field_priority) %></label>
38 <%= select_tag('issue[priority_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
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 </p>
39 </p>
40 <p>
40 <p>
41 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
41 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
42 <%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
42 <%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
43 content_tag('option', l(:label_nobody), :value => 'none') +
43 content_tag('option', l(:label_nobody), :value => 'none') +
44 principals_options_for_select(@assignables)) %>
44 principals_options_for_select(@assignables)) %>
45 </p>
45 </p>
46 <p>
46 <p>
47 <label for='issue_category_id'><%= l(:field_category) %></label>
47 <label for='issue_category_id'><%= l(:field_category) %></label>
48 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
48 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
49 content_tag('option', l(:label_none), :value => 'none') +
49 content_tag('option', l(:label_none), :value => 'none') +
50 options_from_collection_for_select(@categories, :id, :name)) %>
50 options_from_collection_for_select(@categories, :id, :name)) %>
51 </p>
51 </p>
52 <p>
52 <p>
53 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
53 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
54 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
54 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
55 content_tag('option', l(:label_none), :value => 'none') +
55 content_tag('option', l(:label_none), :value => 'none') +
56 version_options_for_select(@versions.sort)) %>
56 version_options_for_select(@versions.sort)) %>
57 </p>
57 </p>
58
58
59 <% @custom_fields.each do |custom_field| %>
59 <% @custom_fields.each do |custom_field| %>
60 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('issue', custom_field, @projects) %></p>
60 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('issue', custom_field, @projects) %></p>
61 <% end %>
61 <% end %>
62
62
63 <% if @copy && @attachments_present %>
63 <% if @copy && @attachments_present %>
64 <p>
64 <p>
65 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
65 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
66 <%= check_box_tag 'copy_attachments', '1', true %>
66 <%= check_box_tag 'copy_attachments', '1', true %>
67 </p>
67 </p>
68 <% end %>
68 <% end %>
69
69
70 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
70 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
71 </div>
71 </div>
72
72
73 <div class="splitcontentright">
73 <div class="splitcontentright">
74 <% if @safe_attributes.include?('is_private') %>
74 <% if @safe_attributes.include?('is_private') %>
75 <p>
75 <p>
76 <label for='issue_is_private'><%= l(:field_is_private) %></label>
76 <label for='issue_is_private'><%= l(:field_is_private) %></label>
77 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
77 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
78 content_tag('option', l(:general_text_Yes), :value => '1') +
78 content_tag('option', l(:general_text_Yes), :value => '1') +
79 content_tag('option', l(:general_text_No), :value => '0')) %>
79 content_tag('option', l(:general_text_No), :value => '0')) %>
80 </p>
80 </p>
81 <% end %>
81 <% end %>
82 <% if @project && User.current.allowed_to?(:manage_subtasks, @project) %>
82 <% if @project && User.current.allowed_to?(:manage_subtasks, @project) %>
83 <p>
83 <p>
84 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
84 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
85 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10 %>
85 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10 %>
86 </p>
86 </p>
87 <div id="parent_issue_candidates" class="autocomplete"></div>
87 <div id="parent_issue_candidates" class="autocomplete"></div>
88 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:project_id => @project) }')" %>
88 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:project_id => @project) }')" %>
89 <% end %>
89 <% end %>
90 <p>
90 <p>
91 <label for='issue_start_date'><%= l(:field_start_date) %></label>
91 <label for='issue_start_date'><%= l(:field_start_date) %></label>
92 <%= text_field_tag 'issue[start_date]', '', :size => 10 %><%= calendar_for('issue_start_date') %>
92 <%= text_field_tag 'issue[start_date]', '', :size => 10 %><%= calendar_for('issue_start_date') %>
93 </p>
93 </p>
94 <p>
94 <p>
95 <label for='issue_due_date'><%= l(:field_due_date) %></label>
95 <label for='issue_due_date'><%= l(:field_due_date) %></label>
96 <%= text_field_tag 'issue[due_date]', '', :size => 10 %><%= calendar_for('issue_due_date') %>
96 <%= text_field_tag 'issue[due_date]', '', :size => 10 %><%= calendar_for('issue_due_date') %>
97 </p>
97 </p>
98 <% if Issue.use_field_for_done_ratio? %>
98 <% if Issue.use_field_for_done_ratio? %>
99 <p>
99 <p>
100 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
100 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
101 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>
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 </p>
102 </p>
103 <% end %>
103 <% end %>
104 </div>
104 </div>
105
105
106 </fieldset>
106 </fieldset>
107
107
108 <fieldset><legend><%= l(:field_notes) %></legend>
108 <fieldset><legend><%= l(:field_notes) %></legend>
109 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
109 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
110 <%= wikitoolbar_for 'notes' %>
110 <%= wikitoolbar_for 'notes' %>
111 </fieldset>
111 </fieldset>
112 </div>
112 </div>
113
113
114 <p>
114 <p>
115 <% if @copy %>
115 <% if @copy %>
116 <%= hidden_field_tag 'copy', '1' %>
116 <%= hidden_field_tag 'copy', '1' %>
117 <%= submit_tag l(:button_copy) %>
117 <%= submit_tag l(:button_copy) %>
118 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
118 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
119 <% elsif @target_project %>
119 <% elsif @target_project %>
120 <%= submit_tag l(:button_move) %>
120 <%= submit_tag l(:button_move) %>
121 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
121 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
122 <% else %>
122 <% else %>
123 <%= submit_tag l(:button_submit) %>
123 <%= submit_tag l(:button_submit) %>
124 <% end %>
124 <% end %>
125 </p>
125 </p>
126
126
127 <% end %>
127 <% end %>
@@ -1,15 +1,15
1 <h2><%= l(:label_confirmation) %></h2>
1 <h2><%= l(:label_confirmation) %></h2>
2
2
3 <% form_tag({}, :method => :delete) do %>
3 <%= form_tag({}, :method => :delete) do %>
4 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
4 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
5 <div class="box">
5 <div class="box">
6 <p><strong><%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %></strong></p>
6 <p><strong><%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %></strong></p>
7 <p>
7 <p>
8 <label><%= radio_button_tag 'todo', 'destroy', true %> <%= l(:text_destroy_time_entries) %></label><br />
8 <label><%= radio_button_tag 'todo', 'destroy', true %> <%= l(:text_destroy_time_entries) %></label><br />
9 <label><%= radio_button_tag 'todo', 'nullify', false %> <%= l(:text_assign_time_entries_to_project) %></label><br />
9 <label><%= radio_button_tag 'todo', 'nullify', false %> <%= l(:text_assign_time_entries_to_project) %></label><br />
10 <label><%= radio_button_tag 'todo', 'reassign', false, :onchange => 'if (this.checked) { $("reassign_to_id").focus(); }' %> <%= l(:text_reassign_time_entries) %></label>
10 <label><%= radio_button_tag 'todo', 'reassign', false, :onchange => 'if (this.checked) { $("reassign_to_id").focus(); }' %> <%= l(:text_reassign_time_entries) %></label>
11 <%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("todo_reassign").checked=true;' %>
11 <%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("todo_reassign").checked=true;' %>
12 </p>
12 </p>
13 </div>
13 </div>
14 <%= submit_tag l(:button_apply) %>
14 <%= submit_tag l(:button_apply) %>
15 <% end %>
15 <% end %>
@@ -1,105 +1,105
1 <div class="contextual">
1 <div class="contextual">
2 <% if !@query.new_record? && @query.editable_by?(User.current) %>
2 <% if !@query.new_record? && @query.editable_by?(User.current) %>
3 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
3 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
4 <%= link_to l(:button_delete), query_path(@query), :confirm => l(:text_are_you_sure),
4 <%= link_to l(:button_delete), query_path(@query), :confirm => l(:text_are_you_sure),
5 :method => :delete, :class => 'icon icon-del' %>
5 :method => :delete, :class => 'icon icon-del' %>
6 <% end %>
6 <% end %>
7 </div>
7 </div>
8
8
9 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
9 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
10 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
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 :method => :get, :id => 'query_form') do %>
13 :method => :get, :id => 'query_form') do %>
14 <%= hidden_field_tag 'set_filter', '1' %>
14 <%= hidden_field_tag 'set_filter', '1' %>
15 <div id="query_form_content" class="hide-when-print">
15 <div id="query_form_content" class="hide-when-print">
16 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
16 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
17 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
17 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
18 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
18 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
19 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
19 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
20 </div>
20 </div>
21 </fieldset>
21 </fieldset>
22 <fieldset class="collapsible collapsed">
22 <fieldset class="collapsible collapsed">
23 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
23 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
24 <div style="display: none;">
24 <div style="display: none;">
25 <table>
25 <table>
26 <tr>
26 <tr>
27 <td><%= l(:field_column_names) %></td>
27 <td><%= l(:field_column_names) %></td>
28 <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
28 <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
29 </tr>
29 </tr>
30 <tr>
30 <tr>
31 <td><label for='group_by'><%= l(:field_group_by) %></label></td>
31 <td><label for='group_by'><%= l(:field_group_by) %></label></td>
32 <td><%= select_tag('group_by',
32 <td><%= select_tag('group_by',
33 options_for_select(
33 options_for_select(
34 [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
34 [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
35 @query.group_by)
35 @query.group_by)
36 ) %></td>
36 ) %></td>
37 </tr>
37 </tr>
38 </table>
38 </table>
39 </div>
39 </div>
40 </fieldset>
40 </fieldset>
41 </div>
41 </div>
42 <p class="buttons hide-when-print">
42 <p class="buttons hide-when-print">
43
43
44 <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
44 <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
45 <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
45 <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
46 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
46 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
47 <%= link_to_function l(:button_save),
47 <%= link_to_function l(:button_save),
48 "$('query_form').action='#{ @project ? new_project_query_path(@project) : new_query_path }'; submit_query_form('query_form')",
48 "$('query_form').action='#{ @project ? new_project_query_path(@project) : new_query_path }'; submit_query_form('query_form')",
49 :class => 'icon icon-save' %>
49 :class => 'icon icon-save' %>
50 <% end %>
50 <% end %>
51 </p>
51 </p>
52 <% end %>
52 <% end %>
53
53
54 <%= error_messages_for 'query' %>
54 <%= error_messages_for 'query' %>
55 <% if @query.valid? %>
55 <% if @query.valid? %>
56 <% if @issues.empty? %>
56 <% if @issues.empty? %>
57 <p class="nodata"><%= l(:label_no_data) %></p>
57 <p class="nodata"><%= l(:label_no_data) %></p>
58 <% else %>
58 <% else %>
59 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
59 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
60 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
60 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
61 <% end %>
61 <% end %>
62
62
63 <% other_formats_links do |f| %>
63 <% other_formats_links do |f| %>
64 <%= f.link_to 'Atom', :url => params.merge(:key => User.current.rss_key) %>
64 <%= f.link_to 'Atom', :url => params.merge(:key => User.current.rss_key) %>
65 <%= f.link_to 'CSV', :url => params, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
65 <%= f.link_to 'CSV', :url => params, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
66 <%= f.link_to 'PDF', :url => params %>
66 <%= f.link_to 'PDF', :url => params %>
67 <% end %>
67 <% end %>
68
68
69 <div id="csv-export-options" style="display:none;">
69 <div id="csv-export-options" style="display:none;">
70 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
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 <p>
72 <p>
73 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
73 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
74 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
74 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
75 </p>
75 </p>
76 <p>
76 <p>
77 <label><%= check_box_tag 'description', '1' %> <%= l(:field_description) %></label>
77 <label><%= check_box_tag 'description', '1' %> <%= l(:field_description) %></label>
78 </p>
78 </p>
79 <p class="buttons">
79 <p class="buttons">
80 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
80 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
81 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
81 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
82 </p>
82 </p>
83 <% end %>
83 <% end %>
84 </div>
84 </div>
85
85
86 <% end %>
86 <% end %>
87 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
87 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
88
88
89 <% content_for :sidebar do %>
89 <% content_for :sidebar do %>
90 <%= render :partial => 'issues/sidebar' %>
90 <%= render :partial => 'issues/sidebar' %>
91 <% end %>
91 <% end %>
92
92
93 <% content_for :header_tags do %>
93 <% content_for :header_tags do %>
94 <%= auto_discovery_link_tag(:atom,
94 <%= auto_discovery_link_tag(:atom,
95 {:query_id => @query, :format => 'atom',
95 {:query_id => @query, :format => 'atom',
96 :page => nil, :key => User.current.rss_key},
96 :page => nil, :key => User.current.rss_key},
97 :title => l(:label_issue_plural)) %>
97 :title => l(:label_issue_plural)) %>
98 <%= auto_discovery_link_tag(:atom,
98 <%= auto_discovery_link_tag(:atom,
99 {:controller => 'journals', :action => 'index',
99 {:controller => 'journals', :action => 'index',
100 :query_id => @query, :format => 'atom',
100 :query_id => @query, :format => 'atom',
101 :page => nil, :key => User.current.rss_key},
101 :page => nil, :key => User.current.rss_key},
102 :title => l(:label_changes_details)) %>
102 :title => l(:label_changes_details)) %>
103 <% end %>
103 <% end %>
104
104
105 <%= context_menu issues_context_menu_path %>
105 <%= context_menu issues_context_menu_path %>
@@ -1,55 +1,55
1 <h2><%=l(:label_issue_new)%></h2>
1 <h2><%=l(:label_issue_new)%></h2>
2
2
3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
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 :html => {:id => 'issue-form', :multipart => true} do |f| %>
6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
7 <%= error_messages_for 'issue' %>
7 <%= error_messages_for 'issue' %>
8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
9 <div class="box tabular">
9 <div class="box tabular">
10 <div id="all_attributes">
10 <div id="all_attributes">
11 <%= render :partial => 'issues/form', :locals => {:f => f} %>
11 <%= render :partial => 'issues/form', :locals => {:f => f} %>
12 </div>
12 </div>
13
13
14 <% if @copy_from && @copy_from.attachments.any? %>
14 <% if @copy_from && @copy_from.attachments.any? %>
15 <p>
15 <p>
16 <label for="copy_attachments"><%= l(:label_copy_attachments) %></label>
16 <label for="copy_attachments"><%= l(:label_copy_attachments) %></label>
17 <%= check_box_tag 'copy_attachments', '1', @copy_attachments %>
17 <%= check_box_tag 'copy_attachments', '1', @copy_attachments %>
18 </p>
18 </p>
19 <% end %>
19 <% end %>
20
20
21 <p id="attachments_form"><%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
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 <% if @issue.safe_attribute? 'watcher_user_ids' -%>
23 <% if @issue.safe_attribute? 'watcher_user_ids' -%>
24 <p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
24 <p id="watchers_form"><label><%= l(:label_issue_watchers) %></label>
25 <span id="watchers_inputs">
25 <span id="watchers_inputs">
26 <%= watchers_checkboxes(@issue, @available_watchers) %>
26 <%= watchers_checkboxes(@issue, @available_watchers) %>
27 </span>
27 </span>
28 <span class="search_for_watchers">
28 <span class="search_for_watchers">
29 <%= link_to_remote l(:label_search_for_watchers),
29 <%= link_to_remote l(:label_search_for_watchers),
30 :url => {:controller => 'watchers', :action => 'new', :project_id => @issue.project},
30 :url => {:controller => 'watchers', :action => 'new', :project_id => @issue.project},
31 :method => 'get' %>
31 :method => 'get' %>
32 </span>
32 </span>
33 </p>
33 </p>
34 <% end %>
34 <% end %>
35 </div>
35 </div>
36
36
37 <%= submit_tag l(:button_create) %>
37 <%= submit_tag l(:button_create) %>
38 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
38 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
39 <%= link_to_remote l(:label_preview),
39 <%= link_to_remote l(:label_preview),
40 { :url => preview_new_issue_path(:project_id => @project),
40 { :url => preview_new_issue_path(:project_id => @project),
41 :method => 'post',
41 :method => 'post',
42 :update => 'preview',
42 :update => 'preview',
43 :with => "Form.serialize('issue-form')",
43 :with => "Form.serialize('issue-form')",
44 :complete => "Element.scrollTo('preview')"
44 :complete => "Element.scrollTo('preview')"
45 }, :accesskey => accesskey(:preview) %>
45 }, :accesskey => accesskey(:preview) %>
46
46
47 <%= javascript_tag "Form.Element.focus('issue_subject');" %>
47 <%= javascript_tag "Form.Element.focus('issue_subject');" %>
48 <% end %>
48 <% end %>
49
49
50 <div id="preview" class="wiki"></div>
50 <div id="preview" class="wiki"></div>
51
51
52 <% content_for :header_tags do %>
52 <% content_for :header_tags do %>
53 <%= stylesheet_link_tag 'scm' %>
53 <%= stylesheet_link_tag 'scm' %>
54 <%= robot_exclusion_tag %>
54 <%= robot_exclusion_tag %>
55 <% end %>
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 <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %>
2 <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %>
3 <%= text_area_tag :notes, @journal.notes,
3 <%= text_area_tag :notes, @journal.notes,
4 :id => "journal_#{@journal.id}_notes",
4 :id => "journal_#{@journal.id}_notes",
5 :class => 'wiki-edit',
5 :class => 'wiki-edit',
6 :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
6 :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
7 <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
7 <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
8 <p><%= submit_tag l(:button_save) %>
8 <p><%= submit_tag l(:button_save) %>
9 <%= link_to_remote l(:label_preview),
9 <%= link_to_remote l(:label_preview),
10 { :url => preview_edit_issue_path(:project_id => @project, :id => @journal.issue),
10 { :url => preview_edit_issue_path(:project_id => @project, :id => @journal.issue),
11 :method => 'post',
11 :method => 'post',
12 :update => "journal_#{@journal.id}_preview",
12 :update => "journal_#{@journal.id}_preview",
13 :with => "Form.serialize('journal-#{@journal.id}-form')",
13 :with => "Form.serialize('journal-#{@journal.id}-form')",
14 :complete => "Element.scrollTo('journal_#{@journal.id}_preview')"
14 :complete => "Element.scrollTo('journal_#{@journal.id}_preview')"
15 }, :accesskey => accesskey(:preview) %>
15 }, :accesskey => accesskey(:preview) %>
16 |
16 |
17 <%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
17 <%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
18 "Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
18 "Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
19
19
20 <div id="journal_<%= @journal.id %>_preview" class="wiki"></div>
20 <div id="journal_<%= @journal.id %>_preview" class="wiki"></div>
21 <% end %>
21 <% end %>
22 <%= wikitoolbar_for "journal_#{@journal.id}_notes" %>
22 <%= wikitoolbar_for "journal_#{@journal.id}_notes" %>
@@ -1,88 +1,88
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
3 <head>
4 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
4 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 <title><%=h html_title %></title>
5 <title><%=h html_title %></title>
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 <meta name="keywords" content="issue,bug,tracker" />
7 <meta name="keywords" content="issue,bug,tracker" />
8 <meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
8 <meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
9 <%= csrf_meta_tag %>
9 <%= csrf_meta_tag %>
10 <%= favicon %>
10 <%= favicon %>
11 <%= stylesheet_link_tag 'application', :media => 'all' %>
11 <%= stylesheet_link_tag 'application', :media => 'all' %>
12 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
12 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
13 <%= javascript_heads %>
13 <%= javascript_heads %>
14 <%= heads_for_theme %>
14 <%= heads_for_theme %>
15 <!--[if IE 6]>
15 <!--[if IE 6]>
16 <style type="text/css">
16 <style type="text/css">
17 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
17 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
18 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
18 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
19 </style>
19 </style>
20 <![endif]-->
20 <![endif]-->
21 <%= call_hook :view_layouts_base_html_head %>
21 <%= call_hook :view_layouts_base_html_head %>
22 <!-- page specific tags -->
22 <!-- page specific tags -->
23 <%= yield :header_tags -%>
23 <%= yield :header_tags -%>
24 </head>
24 </head>
25 <body class="<%=h body_css_classes %>">
25 <body class="<%=h body_css_classes %>">
26 <div id="wrapper">
26 <div id="wrapper">
27 <div id="wrapper2">
27 <div id="wrapper2">
28 <div id="top-menu">
28 <div id="top-menu">
29 <div id="account">
29 <div id="account">
30 <%= render_menu :account_menu -%>
30 <%= render_menu :account_menu -%>
31 </div>
31 </div>
32 <%= content_tag(
32 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
33 'div',
34 "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe,
35 :id => 'loggedas') if User.current.logged? %>
36 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
33 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
37 </div>
34 </div>
38
35
39 <div id="header">
36 <div id="header">
40 <% if User.current.logged? || !Setting.login_required? %>
37 <% if User.current.logged? || !Setting.login_required? %>
41 <div id="quick-search">
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 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
40 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
44 <label for='q'>
41 <label for='q'>
45 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
42 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
46 </label>
43 </label>
47 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
48 <% end %>
45 <% end %>
49 <%= render_project_jump_box %>
46 <%= render_project_jump_box %>
50 </div>
47 </div>
51 <% end %>
48 <% end %>
52
49
53 <h1><%= page_header_title %></h1>
50 <h1><%= page_header_title %></h1>
54
51
55 <% if display_main_menu?(@project) %>
52 <% if display_main_menu?(@project) %>
56 <div id="main-menu">
53 <div id="main-menu">
57 <%= render_main_menu(@project) %>
54 <%= render_main_menu(@project) %>
58 </div>
55 </div>
59 <% end %>
56 <% end %>
60 </div>
57 </div>
61
58
59 <% content_for :sidebar do %>
60 <%= call_hook :view_layouts_base_sidebar %>
61 <% end %>
62
62 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
63 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
63 <div id="sidebar">
64 <div id="sidebar">
64 <%= yield :sidebar %>
65 <%= yield :sidebar %>
65 <%= call_hook :view_layouts_base_sidebar %>
66 </div>
66 </div>
67
67
68 <div id="content">
68 <div id="content">
69 <%= render_flash_messages %>
69 <%= render_flash_messages %>
70 <%= yield %>
70 <%= yield %>
71 <%= call_hook :view_layouts_base_content %>
71 <%= call_hook :view_layouts_base_content %>
72 <div style="clear:both;"></div>
72 <div style="clear:both;"></div>
73 </div>
73 </div>
74 </div>
74 </div>
75
75
76 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
76 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
77 <div id="ajax-modal" style="display:none;"></div>
77 <div id="ajax-modal" style="display:none;"></div>
78
78
79 <div id="footer">
79 <div id="footer">
80 <div class="bgl"><div class="bgr">
80 <div class="bgl"><div class="bgr">
81 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2012 Jean-Philippe Lang
81 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2012 Jean-Philippe Lang
82 </div></div>
82 </div></div>
83 </div>
83 </div>
84 </div>
84 </div>
85 </div>
85 </div>
86 <%= call_hook :view_layouts_base_body_bottom %>
86 <%= call_hook :view_layouts_base_body_bottom %>
87 </body>
87 </body>
88 </html>
88 </html>
@@ -1,26 +1,28
1 <h2><%= link_to h(@board.name), :controller => 'boards',
1 <h2><%= link_to h(@board.name), :controller => 'boards',
2 :action => 'show', :project_id => @project,
2 :action => 'show', :project_id => @project,
3 :id => @board %> &#187; <%= h @message.subject %></h2>
3 :id => @board %> &#187; <%= h @message.subject %></h2>
4
4
5 <% form_for :message, @message,
5 <%= form_for @message, {
6 :url => {:action => 'edit'},
6 :as => :message,
7 :html => {:multipart => true,
7 :url => {:action => 'edit'},
8 :id => 'message-form',
8 :html => {:multipart => true,
9 :method => :post} do |f| %>
9 :id => 'message-form',
10 :method => :post}
11 } do |f| %>
10 <%= render :partial => 'form',
12 <%= render :partial => 'form',
11 :locals => {:f => f, :replying => !@message.parent.nil?} %>
13 :locals => {:f => f, :replying => !@message.parent.nil?} %>
12 <%= submit_tag l(:button_save) %>
14 <%= submit_tag l(:button_save) %>
13 <%= link_to_remote l(:label_preview),
15 <%= link_to_remote l(:label_preview),
14 { :url => { :controller => 'messages',
16 { :url => { :controller => 'messages',
15 :action => 'preview', :board_id => @board, :id => @message },
17 :action => 'preview', :board_id => @board, :id => @message },
16 :method => 'post',
18 :method => 'post',
17 :update => 'preview',
19 :update => 'preview',
18 :with => "Form.serialize('message-form')",
20 :with => "Form.serialize('message-form')",
19 :complete => "Element.scrollTo('preview')"
21 :complete => "Element.scrollTo('preview')"
20 }, :accesskey => accesskey(:preview) %>
22 }, :accesskey => accesskey(:preview) %>
21 <% end %>
23 <% end %>
22 <div id="preview" class="wiki"></div>
24 <div id="preview" class="wiki"></div>
23
25
24 <% content_for :header_tags do %>
26 <% content_for :header_tags do %>
25 <%= stylesheet_link_tag 'scm' %>
27 <%= stylesheet_link_tag 'scm' %>
26 <% end %>
28 <% end %>
@@ -1,15 +1,15
1 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <%= link_to_remote l(:label_preview),
6 <%= link_to_remote l(:label_preview),
7 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
7 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
8 :method => 'post',
8 :method => 'post',
9 :update => 'preview',
9 :update => 'preview',
10 :with => "Form.serialize('message-form')",
10 :with => "Form.serialize('message-form')",
11 :complete => "Element.scrollTo('preview')"
11 :complete => "Element.scrollTo('preview')"
12 }, :accesskey => accesskey(:preview) %>
12 }, :accesskey => accesskey(:preview) %>
13 <% end %>
13 <% end %>
14
14
15 <div id="preview" class="wiki"></div>
15 <div id="preview" class="wiki"></div>
@@ -1,94 +1,94
1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)),
1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)),
2 link_to(h(@board.name), project_board_path(@project, @board)) %>
2 link_to(h(@board.name), project_board_path(@project, @board)) %>
3
3
4 <div class="contextual">
4 <div class="contextual">
5 <%= watcher_tag(@topic, User.current) %>
5 <%= watcher_tag(@topic, User.current) %>
6 <%= link_to_remote_if_authorized(
6 <%= link_to_remote_if_authorized(
7 l(:button_quote),
7 l(:button_quote),
8 { :url => {:action => 'quote', :id => @topic} },
8 { :url => {:action => 'quote', :id => @topic} },
9 :class => 'icon icon-comment'
9 :class => 'icon icon-comment'
10 ) unless @topic.locked? %>
10 ) unless @topic.locked? %>
11 <%= link_to(
11 <%= link_to(
12 l(:button_edit),
12 l(:button_edit),
13 {:action => 'edit', :id => @topic},
13 {:action => 'edit', :id => @topic},
14 :class => 'icon icon-edit'
14 :class => 'icon icon-edit'
15 ) if @message.editable_by?(User.current) %>
15 ) if @message.editable_by?(User.current) %>
16 <%= link_to(
16 <%= link_to(
17 l(:button_delete),
17 l(:button_delete),
18 {:action => 'destroy', :id => @topic},
18 {:action => 'destroy', :id => @topic},
19 :method => :post,
19 :method => :post,
20 :confirm => l(:text_are_you_sure),
20 :confirm => l(:text_are_you_sure),
21 :class => 'icon icon-del'
21 :class => 'icon icon-del'
22 ) if @message.destroyable_by?(User.current) %>
22 ) if @message.destroyable_by?(User.current) %>
23 </div>
23 </div>
24
24
25 <h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
25 <h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
26
26
27 <div class="message">
27 <div class="message">
28 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
28 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
29 <div class="wiki">
29 <div class="wiki">
30 <%= textilizable(@topic, :content) %>
30 <%= textilizable(@topic, :content) %>
31 </div>
31 </div>
32 <%= link_to_attachments @topic, :author => false %>
32 <%= link_to_attachments @topic, :author => false %>
33 </div>
33 </div>
34 <br />
34 <br />
35
35
36 <% unless @replies.empty? %>
36 <% unless @replies.empty? %>
37 <h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
37 <h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
38 <% @replies.each do |message| %>
38 <% @replies.each do |message| %>
39 <div class="message reply" id="<%= "message-#{message.id}" %>">
39 <div class="message reply" id="<%= "message-#{message.id}" %>">
40 <div class="contextual">
40 <div class="contextual">
41 <%= link_to_remote_if_authorized(
41 <%= link_to_remote_if_authorized(
42 image_tag('comment.png'),
42 image_tag('comment.png'),
43 { :url => {:action => 'quote', :id => message} },
43 { :url => {:action => 'quote', :id => message} },
44 :title => l(:button_quote)
44 :title => l(:button_quote)
45 ) unless @topic.locked? %>
45 ) unless @topic.locked? %>
46 <%= link_to(
46 <%= link_to(
47 image_tag('edit.png'),
47 image_tag('edit.png'),
48 {:action => 'edit', :id => message},
48 {:action => 'edit', :id => message},
49 :title => l(:button_edit)
49 :title => l(:button_edit)
50 ) if message.editable_by?(User.current) %>
50 ) if message.editable_by?(User.current) %>
51 <%= link_to(
51 <%= link_to(
52 image_tag('delete.png'),
52 image_tag('delete.png'),
53 {:action => 'destroy', :id => message},
53 {:action => 'destroy', :id => message},
54 :method => :post,
54 :method => :post,
55 :confirm => l(:text_are_you_sure),
55 :confirm => l(:text_are_you_sure),
56 :title => l(:button_delete)
56 :title => l(:button_delete)
57 ) if message.destroyable_by?(User.current) %>
57 ) if message.destroyable_by?(User.current) %>
58 </div>
58 </div>
59 <h4>
59 <h4>
60 <%= avatar(message.author, :size => "24") %>
60 <%= avatar(message.author, :size => "24") %>
61 <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
61 <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
62 -
62 -
63 <%= authoring message.created_on, message.author %>
63 <%= authoring message.created_on, message.author %>
64 </h4>
64 </h4>
65 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
65 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
66 <%= link_to_attachments message, :author => false %>
66 <%= link_to_attachments message, :author => false %>
67 </div>
67 </div>
68 <% end %>
68 <% end %>
69 <p class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></p>
69 <p class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></p>
70 <% end %>
70 <% end %>
71
71
72 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
72 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
73 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
73 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
74 <div id="reply" style="display:none;">
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 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
76 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
77 <%= submit_tag l(:button_submit) %>
77 <%= submit_tag l(:button_submit) %>
78 <%= link_to_remote l(:label_preview),
78 <%= link_to_remote l(:label_preview),
79 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
79 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
80 :method => 'post',
80 :method => 'post',
81 :update => 'preview',
81 :update => 'preview',
82 :with => "Form.serialize('message-form')",
82 :with => "Form.serialize('message-form')",
83 :complete => "Element.scrollTo('preview')"
83 :complete => "Element.scrollTo('preview')"
84 }, :accesskey => accesskey(:preview) %>
84 }, :accesskey => accesskey(:preview) %>
85 <% end %>
85 <% end %>
86 <div id="preview" class="wiki"></div>
86 <div id="preview" class="wiki"></div>
87 </div>
87 </div>
88 <% end %>
88 <% end %>
89
89
90 <% content_for :header_tags do %>
90 <% content_for :header_tags do %>
91 <%= stylesheet_link_tag 'scm' %>
91 <%= stylesheet_link_tag 'scm' %>
92 <% end %>
92 <% end %>
93
93
94 <% html_title @topic.subject %>
94 <% html_title @topic.subject %>
@@ -1,51 +1,51
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:button_change_password), {:action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
2 <%= link_to(l(:button_change_password), {:action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
3 <%= call_hook(:view_my_account_contextual, :user => @user)%>
3 <%= call_hook(:view_my_account_contextual, :user => @user)%>
4 </div>
4 </div>
5
5
6 <h2><%=l(:label_my_account)%></h2>
6 <h2><%=l(:label_my_account)%></h2>
7 <%= error_messages_for 'user' %>
7 <%= error_messages_for 'user' %>
8
8
9 <% labelled_form_for :user, @user,
9 <%= labelled_form_for :user, @user,
10 :url => { :action => "account" },
10 :url => { :action => "account" },
11 :html => { :id => 'my_account_form',
11 :html => { :id => 'my_account_form',
12 :method => :post } do |f| %>
12 :method => :post } do |f| %>
13 <div class="splitcontentleft">
13 <div class="splitcontentleft">
14 <fieldset class="box tabular">
14 <fieldset class="box tabular">
15 <legend><%=l(:label_information_plural)%></legend>
15 <legend><%=l(:label_information_plural)%></legend>
16 <p><%= f.text_field :firstname, :required => true %></p>
16 <p><%= f.text_field :firstname, :required => true %></p>
17 <p><%= f.text_field :lastname, :required => true %></p>
17 <p><%= f.text_field :lastname, :required => true %></p>
18 <p><%= f.text_field :mail, :required => true %></p>
18 <p><%= f.text_field :mail, :required => true %></p>
19 <p><%= f.select :language, lang_options_for_select %></p>
19 <p><%= f.select :language, lang_options_for_select %></p>
20 <% if Setting.openid? %>
20 <% if Setting.openid? %>
21 <p><%= f.text_field :identity_url %></p>
21 <p><%= f.text_field :identity_url %></p>
22 <% end %>
22 <% end %>
23
23
24 <% @user.custom_field_values.select(&:editable?).each do |value| %>
24 <% @user.custom_field_values.select(&:editable?).each do |value| %>
25 <p><%= custom_field_tag_with_label :user, value %></p>
25 <p><%= custom_field_tag_with_label :user, value %></p>
26 <% end %>
26 <% end %>
27 <%= call_hook(:view_my_account, :user => @user, :form => f) %>
27 <%= call_hook(:view_my_account, :user => @user, :form => f) %>
28 </fieldset>
28 </fieldset>
29
29
30 <%= submit_tag l(:button_save) %>
30 <%= submit_tag l(:button_save) %>
31 </div>
31 </div>
32
32
33 <div class="splitcontentright">
33 <div class="splitcontentright">
34 <fieldset class="box">
34 <fieldset class="box">
35 <legend><%=l(:field_mail_notification)%></legend>
35 <legend><%=l(:field_mail_notification)%></legend>
36 <%= render :partial => 'users/mail_notifications' %>
36 <%= render :partial => 'users/mail_notifications' %>
37 </fieldset>
37 </fieldset>
38
38
39 <fieldset class="box tabular">
39 <fieldset class="box tabular">
40 <legend><%=l(:label_preferences)%></legend>
40 <legend><%=l(:label_preferences)%></legend>
41 <%= render :partial => 'users/preferences' %>
41 <%= render :partial => 'users/preferences' %>
42 </fieldset>
42 </fieldset>
43
43
44 </div>
44 </div>
45 <% end %>
45 <% end %>
46
46
47 <% content_for :sidebar do %>
47 <% content_for :sidebar do %>
48 <%= render :partial => 'sidebar' %>
48 <%= render :partial => 'sidebar' %>
49 <% end %>
49 <% end %>
50
50
51 <% html_title(l(:label_my_account)) -%>
51 <% html_title(l(:label_my_account)) -%>
@@ -1,11 +1,11
1 <h2><%=l(:label_confirmation)%></h2>
1 <h2><%=l(:label_confirmation)%></h2>
2 <div class="warning">
2 <div class="warning">
3 <p><%= simple_format l(:text_account_destroy_confirmation)%></p>
3 <p><%= simple_format l(:text_account_destroy_confirmation)%></p>
4 <p>
4 <p>
5 <% form_tag({}) do %>
5 <%= form_tag({}) do %>
6 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
6 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
7 <%= submit_tag l(:button_delete_my_account) %> |
7 <%= submit_tag l(:button_delete_my_account) %> |
8 <%= link_to l(:button_cancel), :action => 'account' %>
8 <%= link_to l(:button_cancel), :action => 'account' %>
9 <% end %>
9 <% end %>
10 </p>
10 </p>
11 </div>
11 </div>
@@ -1,109 +1,109
1 <script language="JavaScript">
1 <script language="JavaScript">
2 //<![CDATA[
2 //<![CDATA[
3 function recreateSortables() {
3 function recreateSortables() {
4 Sortable.destroy('list-top');
4 Sortable.destroy('list-top');
5 Sortable.destroy('list-left');
5 Sortable.destroy('list-left');
6 Sortable.destroy('list-right');
6 Sortable.destroy('list-right');
7
7
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'})
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 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'})
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 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'})
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 function updateSelect() {
13 function updateSelect() {
14 s = $('block-select')
14 s = $('block-select')
15 for (var i = 0; i < s.options.length; i++) {
15 for (var i = 0; i < s.options.length; i++) {
16 if ($('block_' + s.options[i].value)) {
16 if ($('block_' + s.options[i].value)) {
17 s.options[i].disabled = true;
17 s.options[i].disabled = true;
18 } else {
18 } else {
19 s.options[i].disabled = false;
19 s.options[i].disabled = false;
20 }
20 }
21 }
21 }
22 s.options[0].selected = true;
22 s.options[0].selected = true;
23 }
23 }
24
24
25 function afterAddBlock() {
25 function afterAddBlock() {
26 recreateSortables();
26 recreateSortables();
27 updateSelect();
27 updateSelect();
28 }
28 }
29
29
30 function removeBlock(block) {
30 function removeBlock(block) {
31 Effect.DropOut(block);
31 Effect.DropOut(block);
32 updateSelect();
32 updateSelect();
33 }
33 }
34 //]]>
34 //]]>
35 </script>
35 </script>
36
36
37 <div class="contextual">
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 <%= label_tag('block-select', l(:label_my_page_block)) %>:
39 <%= label_tag('block-select', l(:label_my_page_block)) %>:
40 <%= select_tag 'block',
40 <%= select_tag 'block',
41 "<option></option>".html_safe + options_for_select(@block_options),
41 "<option></option>".html_safe + options_for_select(@block_options),
42 :id => "block-select" %>
42 :id => "block-select" %>
43 <%= link_to_remote l(:button_add),
43 <%= link_to_remote l(:button_add),
44 {:url => { :action => "add_block" },
44 {:url => { :action => "add_block" },
45 :with => "Form.serialize('block-form')",
45 :with => "Form.serialize('block-form')",
46 :update => "list-top",
46 :update => "list-top",
47 :position => :top,
47 :position => :top,
48 :complete => "afterAddBlock();"
48 :complete => "afterAddBlock();"
49 }, :class => 'icon icon-add'
49 }, :class => 'icon icon-add'
50 %>
50 %>
51 <% end %>
51 <% end %>
52 <%= link_to l(:button_back), {:action => 'page'}, :class => 'icon icon-cancel' %>
52 <%= link_to l(:button_back), {:action => 'page'}, :class => 'icon icon-cancel' %>
53 </div>
53 </div>
54
54
55 <h2><%=l(:label_my_page)%></h2>
55 <h2><%=l(:label_my_page)%></h2>
56
56
57 <div id="list-top" class="block-receiver">
57 <div id="list-top" class="block-receiver">
58 <% @blocks['top'].each do |b|
58 <% @blocks['top'].each do |b|
59 next unless MyController::BLOCKS.keys.include? b %>
59 next unless MyController::BLOCKS.keys.include? b %>
60 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
60 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
61 <% end if @blocks['top'] %>
61 <% end if @blocks['top'] %>
62 </div>
62 </div>
63
63
64 <div id="list-left" class="splitcontentleft block-receiver">
64 <div id="list-left" class="splitcontentleft block-receiver">
65 <% @blocks['left'].each do |b|
65 <% @blocks['left'].each do |b|
66 next unless MyController::BLOCKS.keys.include? b %>
66 next unless MyController::BLOCKS.keys.include? b %>
67 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
67 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
68 <% end if @blocks['left'] %>
68 <% end if @blocks['left'] %>
69 </div>
69 </div>
70
70
71 <div id="list-right" class="splitcontentright block-receiver">
71 <div id="list-right" class="splitcontentright block-receiver">
72 <% @blocks['right'].each do |b|
72 <% @blocks['right'].each do |b|
73 next unless MyController::BLOCKS.keys.include? b %>
73 next unless MyController::BLOCKS.keys.include? b %>
74 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
74 <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %>
75 <% end if @blocks['right'] %>
75 <% end if @blocks['right'] %>
76 </div>
76 </div>
77
77
78 <%= sortable_element 'list-top',
78 <%= sortable_element 'list-top',
79 :tag => 'div',
79 :tag => 'div',
80 :only => 'mypage-box',
80 :only => 'mypage-box',
81 :handle => "handle",
81 :handle => "handle",
82 :dropOnEmpty => true,
82 :dropOnEmpty => true,
83 :containment => ['list-top', 'list-left', 'list-right'],
83 :containment => ['list-top', 'list-left', 'list-right'],
84 :constraint => false,
84 :constraint => false,
85 :url => { :action => "order_blocks", :group => "top" }
85 :url => { :action => "order_blocks", :group => "top" }
86 %>
86 %>
87
87
88 <%= sortable_element 'list-left',
88 <%= sortable_element 'list-left',
89 :tag => 'div',
89 :tag => 'div',
90 :only => 'mypage-box',
90 :only => 'mypage-box',
91 :handle => "handle",
91 :handle => "handle",
92 :dropOnEmpty => true,
92 :dropOnEmpty => true,
93 :containment => ['list-top', 'list-left', 'list-right'],
93 :containment => ['list-top', 'list-left', 'list-right'],
94 :constraint => false,
94 :constraint => false,
95 :url => { :action => "order_blocks", :group => "left" }
95 :url => { :action => "order_blocks", :group => "left" }
96 %>
96 %>
97
97
98 <%= sortable_element 'list-right',
98 <%= sortable_element 'list-right',
99 :tag => 'div',
99 :tag => 'div',
100 :only => 'mypage-box',
100 :only => 'mypage-box',
101 :handle => "handle",
101 :handle => "handle",
102 :dropOnEmpty => true,
102 :dropOnEmpty => true,
103 :containment => ['list-top', 'list-left', 'list-right'],
103 :containment => ['list-top', 'list-left', 'list-right'],
104 :constraint => false,
104 :constraint => false,
105 :url => { :action => "order_blocks", :group => "right" }
105 :url => { :action => "order_blocks", :group => "right" }
106 %>
106 %>
107
107
108 <%= javascript_tag "updateSelect()" %>
108 <%= javascript_tag "updateSelect()" %>
109 <% html_title(l(:label_my_page)) -%>
109 <% html_title(l(:label_my_page)) -%>
@@ -1,22 +1,22
1 <h2><%=l(:button_change_password)%></h2>
1 <h2><%=l(:button_change_password)%></h2>
2
2
3 <%= error_messages_for 'user' %>
3 <%= error_messages_for 'user' %>
4
4
5 <% form_tag({}, :class => "tabular") do %>
5 <%= form_tag({}, :class => "tabular") do %>
6 <div class="box">
6 <div class="box">
7 <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
7 <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
8 <%= password_field_tag 'password', nil, :size => 25 %></p>
8 <%= password_field_tag 'password', nil, :size => 25 %></p>
9
9
10 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
10 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
11 <%= password_field_tag 'new_password', nil, :size => 25 %>
11 <%= password_field_tag 'new_password', nil, :size => 25 %>
12 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
12 <em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
13
13
14 <p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
14 <p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
15 <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
15 <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
16 </div>
16 </div>
17 <%= submit_tag l(:button_apply) %>
17 <%= submit_tag l(:button_apply) %>
18 <% end %>
18 <% end %>
19
19
20 <% content_for :sidebar do %>
20 <% content_for :sidebar do %>
21 <%= render :partial => 'sidebar' %>
21 <%= render :partial => 'sidebar' %>
22 <% end %>
22 <% end %>
@@ -1,17 +1,17
1 <h2><%=l(:label_news)%></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <%= link_to_remote l(:label_preview),
6 <%= link_to_remote l(:label_preview),
7 { :url => preview_news_path(:project_id => @project),
7 { :url => preview_news_path(:project_id => @project),
8 :method => 'get',
8 :method => 'get',
9 :update => 'preview',
9 :update => 'preview',
10 :with => "Form.serialize('news-form')"
10 :with => "Form.serialize('news-form')"
11 }, :accesskey => accesskey(:preview) %>
11 }, :accesskey => accesskey(:preview) %>
12 <% end %>
12 <% end %>
13 <div id="preview" class="wiki"></div>
13 <div id="preview" class="wiki"></div>
14
14
15 <% content_for :header_tags do %>
15 <% content_for :header_tags do %>
16 <%= stylesheet_link_tag 'scm' %>
16 <%= stylesheet_link_tag 'scm' %>
17 <% end %>
17 <% end %>
@@ -1,51 +1,51
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:label_news_new),
2 <%= link_to(l(:label_news_new),
3 new_project_news_path(@project),
3 new_project_news_path(@project),
4 :class => 'icon icon-add',
4 :class => 'icon icon-add',
5 :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project && User.current.allowed_to?(:manage_news, @project) %>
5 :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project && User.current.allowed_to?(:manage_news, @project) %>
6 </div>
6 </div>
7
7
8 <div id="add-news" style="display:none;">
8 <div id="add-news" style="display:none;">
9 <h2><%=l(:label_news_new)%></h2>
9 <h2><%=l(:label_news_new)%></h2>
10 <% labelled_form_for @news, :url => project_news_index_path(@project),
10 <%= labelled_form_for @news, :url => project_news_index_path(@project),
11 :html => { :id => 'news-form', :multipart => true } do |f| %>
11 :html => { :id => 'news-form', :multipart => true } do |f| %>
12 <%= render :partial => 'news/form', :locals => { :f => f } %>
12 <%= render :partial => 'news/form', :locals => { :f => f } %>
13 <%= submit_tag l(:button_create) %>
13 <%= submit_tag l(:button_create) %>
14 <%= link_to_remote l(:label_preview),
14 <%= link_to_remote l(:label_preview),
15 { :url => preview_news_path(:project_id => @project),
15 { :url => preview_news_path(:project_id => @project),
16 :method => 'get',
16 :method => 'get',
17 :update => 'preview',
17 :update => 'preview',
18 :with => "Form.serialize('news-form')"
18 :with => "Form.serialize('news-form')"
19 }, :accesskey => accesskey(:preview) %> |
19 }, :accesskey => accesskey(:preview) %> |
20 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %>
20 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %>
21 <% end if @project %>
21 <% end if @project %>
22 <div id="preview" class="wiki"></div>
22 <div id="preview" class="wiki"></div>
23 </div>
23 </div>
24
24
25 <h2><%=l(:label_news_plural)%></h2>
25 <h2><%=l(:label_news_plural)%></h2>
26
26
27 <% if @newss.empty? %>
27 <% if @newss.empty? %>
28 <p class="nodata"><%= l(:label_no_data) %></p>
28 <p class="nodata"><%= l(:label_no_data) %></p>
29 <% else %>
29 <% else %>
30 <% @newss.each do |news| %>
30 <% @newss.each do |news| %>
31 <h3><%= avatar(news.author, :size => "24") %><%= link_to_project(news.project) + ': ' unless news.project == @project %>
31 <h3><%= avatar(news.author, :size => "24") %><%= link_to_project(news.project) + ': ' unless news.project == @project %>
32 <%= link_to h(news.title), news_path(news) %>
32 <%= link_to h(news.title), news_path(news) %>
33 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
33 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
34 <p class="author"><%= authoring news.created_on, news.author %></p>
34 <p class="author"><%= authoring news.created_on, news.author %></p>
35 <div class="wiki">
35 <div class="wiki">
36 <%= textilizable(news, :description) %>
36 <%= textilizable(news, :description) %>
37 </div>
37 </div>
38 <% end %>
38 <% end %>
39 <% end %>
39 <% end %>
40 <p class="pagination"><%= pagination_links_full @news_pages %></p>
40 <p class="pagination"><%= pagination_links_full @news_pages %></p>
41
41
42 <% other_formats_links do |f| %>
42 <% other_formats_links do |f| %>
43 <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
43 <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
44 <% end %>
44 <% end %>
45
45
46 <% content_for :header_tags do %>
46 <% content_for :header_tags do %>
47 <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
47 <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
48 <%= stylesheet_link_tag 'scm' %>
48 <%= stylesheet_link_tag 'scm' %>
49 <% end %>
49 <% end %>
50
50
51 <% html_title(l(:label_news_plural)) -%>
51 <% html_title(l(:label_news_plural)) -%>
@@ -1,14 +1,14
1 <h2><%=l(:label_news_new)%></h2>
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 :html => { :id => 'news-form', :multipart => true } do |f| %>
4 :html => { :id => 'news-form', :multipart => true } do |f| %>
5 <%= render :partial => 'news/form', :locals => { :f => f } %>
5 <%= render :partial => 'news/form', :locals => { :f => f } %>
6 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create) %>
7 <%= link_to_remote l(:label_preview),
7 <%= link_to_remote l(:label_preview),
8 { :url => preview_news_path(:project_id => @project),
8 { :url => preview_news_path(:project_id => @project),
9 :method => 'get',
9 :method => 'get',
10 :update => 'preview',
10 :update => 'preview',
11 :with => "Form.serialize('news-form')"
11 :with => "Form.serialize('news-form')"
12 }, :accesskey => accesskey(:preview) %>
12 }, :accesskey => accesskey(:preview) %>
13 <% end %>
13 <% end %>
14 <div id="preview" class="wiki"></div>
14 <div id="preview" class="wiki"></div>
@@ -1,71 +1,71
1 <div class="contextual">
1 <div class="contextual">
2 <%= watcher_tag(@news, User.current) %>
2 <%= watcher_tag(@news, User.current) %>
3 <%= link_to(l(:button_edit),
3 <%= link_to(l(:button_edit),
4 edit_news_path(@news),
4 edit_news_path(@news),
5 :class => 'icon icon-edit',
5 :class => 'icon icon-edit',
6 :accesskey => accesskey(:edit),
6 :accesskey => accesskey(:edit),
7 :onclick => 'Element.show("edit-news"); return false;') if User.current.allowed_to?(:manage_news, @project) %>
7 :onclick => 'Element.show("edit-news"); return false;') if User.current.allowed_to?(:manage_news, @project) %>
8 <%= link_to(l(:button_delete),
8 <%= link_to(l(:button_delete),
9 news_path(@news),
9 news_path(@news),
10 :confirm => l(:text_are_you_sure),
10 :confirm => l(:text_are_you_sure),
11 :method => :delete,
11 :method => :delete,
12 :class => 'icon icon-del') if User.current.allowed_to?(:manage_news, @project) %>
12 :class => 'icon icon-del') if User.current.allowed_to?(:manage_news, @project) %>
13 </div>
13 </div>
14
14
15 <h2><%= avatar(@news.author, :size => "24") %><%=h @news.title %></h2>
15 <h2><%= avatar(@news.author, :size => "24") %><%=h @news.title %></h2>
16
16
17 <% if authorize_for('news', 'edit') %>
17 <% if authorize_for('news', 'edit') %>
18 <div id="edit-news" style="display:none;">
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 :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
20 :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
21 <%= render :partial => 'form', :locals => { :f => f } %>
21 <%= render :partial => 'form', :locals => { :f => f } %>
22 <%= submit_tag l(:button_save) %>
22 <%= submit_tag l(:button_save) %>
23 <%= link_to_remote l(:label_preview),
23 <%= link_to_remote l(:label_preview),
24 { :url => preview_news_path(:project_id => @project),
24 { :url => preview_news_path(:project_id => @project),
25 :method => 'get',
25 :method => 'get',
26 :update => 'preview',
26 :update => 'preview',
27 :with => "Form.serialize('news-form')"
27 :with => "Form.serialize('news-form')"
28 }, :accesskey => accesskey(:preview) %> |
28 }, :accesskey => accesskey(:preview) %> |
29 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("edit-news"); return false;' %>
29 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("edit-news"); return false;' %>
30 <% end %>
30 <% end %>
31 <div id="preview" class="wiki"></div>
31 <div id="preview" class="wiki"></div>
32 </div>
32 </div>
33 <% end %>
33 <% end %>
34
34
35 <p><% unless @news.summary.blank? %><em><%=h @news.summary %></em><br /><% end %>
35 <p><% unless @news.summary.blank? %><em><%=h @news.summary %></em><br /><% end %>
36 <span class="author"><%= authoring @news.created_on, @news.author %></span></p>
36 <span class="author"><%= authoring @news.created_on, @news.author %></span></p>
37 <div class="wiki">
37 <div class="wiki">
38 <%= textilizable(@news, :description) %>
38 <%= textilizable(@news, :description) %>
39 </div>
39 </div>
40 <%= link_to_attachments @news %>
40 <%= link_to_attachments @news %>
41 <br />
41 <br />
42
42
43 <div id="comments" style="margin-bottom:16px;">
43 <div id="comments" style="margin-bottom:16px;">
44 <h3 class="comments"><%= l(:label_comment_plural) %></h3>
44 <h3 class="comments"><%= l(:label_comment_plural) %></h3>
45 <% @comments.each do |comment| %>
45 <% @comments.each do |comment| %>
46 <% next if comment.new_record? %>
46 <% next if comment.new_record? %>
47 <div class="contextual">
47 <div class="contextual">
48 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
48 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
49 :confirm => l(:text_are_you_sure), :method => :delete, :title => l(:button_delete) %>
49 :confirm => l(:text_are_you_sure), :method => :delete, :title => l(:button_delete) %>
50 </div>
50 </div>
51 <h4><%= avatar(comment.author, :size => "24") %><%= authoring comment.created_on, comment.author %></h4>
51 <h4><%= avatar(comment.author, :size => "24") %><%= authoring comment.created_on, comment.author %></h4>
52 <%= textilizable(comment.comments) %>
52 <%= textilizable(comment.comments) %>
53 <% end if @comments.any? %>
53 <% end if @comments.any? %>
54 </div>
54 </div>
55
55
56 <% if @news.commentable? %>
56 <% if @news.commentable? %>
57 <p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p>
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 <div class="box">
59 <div class="box">
60 <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
60 <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
61 <%= wikitoolbar_for 'comment_comments' %>
61 <%= wikitoolbar_for 'comment_comments' %>
62 </div>
62 </div>
63 <p><%= submit_tag l(:button_add) %></p>
63 <p><%= submit_tag l(:button_add) %></p>
64 <% end %>
64 <% end %>
65 <% end %>
65 <% end %>
66
66
67 <% html_title @news.title -%>
67 <% html_title @news.title -%>
68
68
69 <% content_for :header_tags do %>
69 <% content_for :header_tags do %>
70 <%= stylesheet_link_tag 'scm' %>
70 <%= stylesheet_link_tag 'scm' %>
71 <% end %>
71 <% end %>
@@ -1,4 +1,4
1 <% labelled_form_for @project do |f| %>
1 <%= labelled_form_for @project do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <%= submit_tag l(:button_save) %>
3 <%= submit_tag l(:button_save) %>
4 <% end %>
4 <% end %>
@@ -1,20 +1,20
1 <h2><%=l(:label_project_new)%></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
5
6 <fieldset class="box tabular"><legend><%= l(:button_copy) %></legend>
6 <fieldset class="box tabular"><legend><%= l(:button_copy) %></legend>
7 <label class="block"><%= check_box_tag 'only[]', 'members', true %> <%= l(:label_member_plural) %> (<%= @source_project.members.count %>)</label>
7 <label class="block"><%= check_box_tag 'only[]', 'members', true %> <%= l(:label_member_plural) %> (<%= @source_project.members.count %>)</label>
8 <label class="block"><%= check_box_tag 'only[]', 'versions', true %> <%= l(:label_version_plural) %> (<%= @source_project.versions.count %>)</label>
8 <label class="block"><%= check_box_tag 'only[]', 'versions', true %> <%= l(:label_version_plural) %> (<%= @source_project.versions.count %>)</label>
9 <label class="block"><%= check_box_tag 'only[]', 'issue_categories', true %> <%= l(:label_issue_category_plural) %> (<%= @source_project.issue_categories.count %>)</label>
9 <label class="block"><%= check_box_tag 'only[]', 'issue_categories', true %> <%= l(:label_issue_category_plural) %> (<%= @source_project.issue_categories.count %>)</label>
10 <label class="block"><%= check_box_tag 'only[]', 'issues', true %> <%= l(:label_issue_plural) %> (<%= @source_project.issues.count %>)</label>
10 <label class="block"><%= check_box_tag 'only[]', 'issues', true %> <%= l(:label_issue_plural) %> (<%= @source_project.issues.count %>)</label>
11 <label class="block"><%= check_box_tag 'only[]', 'queries', true %> <%= l(:label_query_plural) %> (<%= @source_project.queries.count %>)</label>
11 <label class="block"><%= check_box_tag 'only[]', 'queries', true %> <%= l(:label_query_plural) %> (<%= @source_project.queries.count %>)</label>
12 <label class="block"><%= check_box_tag 'only[]', 'boards', true %> <%= l(:label_board_plural) %> (<%= @source_project.boards.count %>)</label>
12 <label class="block"><%= check_box_tag 'only[]', 'boards', true %> <%= l(:label_board_plural) %> (<%= @source_project.boards.count %>)</label>
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>
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 <%= hidden_field_tag 'only[]', '' %>
14 <%= hidden_field_tag 'only[]', '' %>
15 <br />
15 <br />
16 <label class="block"><%= check_box_tag 'notifications', 1, params[:notifications] %> <%= l(:label_project_copy_notifications) %></label>
16 <label class="block"><%= check_box_tag 'notifications', 1, params[:notifications] %> <%= l(:label_project_copy_notifications) %></label>
17 </fieldset>
17 </fieldset>
18
18
19 <%= submit_tag l(:button_copy) %>
19 <%= submit_tag l(:button_copy) %>
20 <% end %>
20 <% end %>
@@ -1,16 +1,16
1 <h2><%=l(:label_confirmation)%></h2>
1 <h2><%=l(:label_confirmation)%></h2>
2 <div class="warning">
2 <div class="warning">
3 <p><strong><%=h @project_to_destroy %></strong><br />
3 <p><strong><%=h @project_to_destroy %></strong><br />
4 <%=l(:text_project_destroy_confirmation)%>
4 <%=l(:text_project_destroy_confirmation)%>
5
5
6 <% if @project_to_destroy.descendants.any? %>
6 <% if @project_to_destroy.descendants.any? %>
7 <br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
7 <br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
8 <% end %>
8 <% end %>
9 </p>
9 </p>
10 <p>
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 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
12 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
13 <%= submit_tag l(:button_delete) %>
13 <%= submit_tag l(:button_delete) %>
14 <% end %>
14 <% end %>
15 </p>
15 </p>
16 </div>
16 </div>
@@ -1,28 +1,28
1 <% content_for :header_tags do %>
1 <% content_for :header_tags do %>
2 <%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
2 <%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
3 <% end %>
3 <% end %>
4
4
5 <div class="contextual">
5 <div class="contextual">
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) %>
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 <%= link_to(l(:label_overall_spent_time), time_entries_path) + ' |' if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
8 <%= link_to(l(:label_overall_spent_time), time_entries_path) + ' |' if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
9 <%= link_to l(:label_overall_activity),
9 <%= link_to l(:label_overall_activity),
10 { :controller => 'activities', :action => 'index',
10 { :controller => 'activities', :action => 'index',
11 :id => nil } %>
11 :id => nil } %>
12 </div>
12 </div>
13
13
14 <h2><%=l(:label_project_plural)%></h2>
14 <h2><%=l(:label_project_plural)%></h2>
15
15
16 <%= render_project_hierarchy(@projects)%>
16 <%= render_project_hierarchy(@projects)%>
17
17
18 <% if User.current.logged? %>
18 <% if User.current.logged? %>
19 <p style="text-align:right;">
19 <p style="text-align:right;">
20 <span class="my-project"><%= l(:label_my_projects) %></span>
20 <span class="my-project"><%= l(:label_my_projects) %></span>
21 </p>
21 </p>
22 <% end %>
22 <% end %>
23
23
24 <% other_formats_links do |f| %>
24 <% other_formats_links do |f| %>
25 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
25 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
26 <% end %>
26 <% end %>
27
27
28 <% html_title(l(:label_project_plural)) -%>
28 <% html_title(l(:label_project_plural)) -%>
@@ -1,8 +1,8
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_form_for @project do |f| %>
3 <%= labelled_form_for @project do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
7 <%= javascript_tag "Form.Element.focus('project_name');" %>
7 <%= javascript_tag "Form.Element.focus('project_name');" %>
8 <% end %>
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 <table class="list">
3 <table class="list">
4 <thead><tr>
4 <thead><tr>
5 <th><%= l(:field_name) %></th>
5 <th><%= l(:field_name) %></th>
6 <th><%= l(:enumeration_system_activity) %></th>
6 <th><%= l(:enumeration_system_activity) %></th>
7 <% TimeEntryActivity.new.available_custom_fields.each do |value| %>
7 <% TimeEntryActivity.new.available_custom_fields.each do |value| %>
8 <th><%= h value.name %></th>
8 <th><%= h value.name %></th>
9 <% end %>
9 <% end %>
10 <th style="width:15%;"><%= l(:field_active) %></th>
10 <th style="width:15%;"><%= l(:field_active) %></th>
11 </tr></thead>
11 </tr></thead>
12
12
13 <% @project.activities(true).each do |enumeration| %>
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 <tr class="<%= cycle('odd', 'even') %>">
15 <tr class="<%= cycle('odd', 'even') %>">
16 <td>
16 <td>
17 <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %>
17 <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %>
18 <%= h(enumeration) %>
18 <%= h(enumeration) %>
19 </td>
19 </td>
20 <td align="center" style="width:15%;"><%= checked_image !enumeration.project %></td>
20 <td align="center" style="width:15%;"><%= checked_image !enumeration.project %></td>
21 <% enumeration.custom_field_values.each do |value| %>
21 <% enumeration.custom_field_values.each do |value| %>
22 <td align="center">
22 <td align="center">
23 <%= custom_field_tag "enumerations[#{enumeration.id}]", value %>
23 <%= custom_field_tag "enumerations[#{enumeration.id}]", value %>
24 </td>
24 </td>
25 <% end %>
25 <% end %>
26 <td align="center" style="width:15%;">
26 <td align="center" style="width:15%;">
27 <%= ff.check_box :active %>
27 <%= ff.check_box :active %>
28 </td>
28 </td>
29 </tr>
29 </tr>
30 <% end %>
30 <% end %>
31 <% end %>
31 <% end %>
32 </table>
32 </table>
33
33
34 <div class="contextual">
34 <div class="contextual">
35 <%= link_to(l(:button_reset), project_enumerations_path(@project),
35 <%= link_to(l(:button_reset), project_enumerations_path(@project),
36 :method => :delete,
36 :method => :delete,
37 :confirm => l(:text_are_you_sure),
37 :confirm => l(:text_are_you_sure),
38 :class => 'icon icon-del') %>
38 :class => 'icon icon-del') %>
39 </div>
39 </div>
40
40
41 <%= submit_tag l(:button_save) %>
41 <%= submit_tag l(:button_save) %>
42 <% end %>
42 <% end %>
@@ -1,20 +1,20
1 <% form_for :project, @project,
1 <%= form_for @project,
2 :url => { :action => 'modules', :id => @project },
2 :url => { :action => 'modules', :id => @project },
3 :html => {:id => 'modules-form',
3 :html => {:id => 'modules-form',
4 :method => :post} do |f| %>
4 :method => :post} do |f| %>
5
5
6 <div class="box">
6 <div class="box">
7 <fieldset>
7 <fieldset>
8 <legend><%= l(:text_select_project_modules) %></legend>
8 <legend><%= l(:text_select_project_modules) %></legend>
9
9
10 <% Redmine::AccessControl.available_project_modules.each do |m| %>
10 <% Redmine::AccessControl.available_project_modules.each do |m| %>
11 <p><label><%= check_box_tag 'enabled_module_names[]', m, @project.module_enabled?(m) -%>
11 <p><label><%= check_box_tag 'enabled_module_names[]', m, @project.module_enabled?(m) -%>
12 <%= l_or_humanize(m, :prefix => "project_module_") %></label></p>
12 <%= l_or_humanize(m, :prefix => "project_module_") %></label></p>
13 <% end %>
13 <% end %>
14 </fieldset>
14 </fieldset>
15 </div>
15 </div>
16
16
17 <p><%= check_all_links 'modules-form' %></p>
17 <p><%= check_all_links 'modules-form' %></p>
18 <p><%= submit_tag l(:button_save) %></p>
18 <p><%= submit_tag l(:button_save) %></p>
19
19
20 <% end %>
20 <% end %>
@@ -1,82 +1,82
1 <div class="contextual">
1 <div class="contextual">
2 <% if User.current.allowed_to?(:add_subprojects, @project) %>
2 <% if User.current.allowed_to?(:add_subprojects, @project) %>
3 <%= link_to l(:label_subproject_new), {:controller => 'projects', :action => 'new', :parent_id => @project}, :class => 'icon icon-add' %>
3 <%= link_to l(:label_subproject_new), {:controller => 'projects', :action => 'new', :parent_id => @project}, :class => 'icon icon-add' %>
4 <% end %>
4 <% end %>
5 </div>
5 </div>
6
6
7 <h2><%=l(:label_overview)%></h2>
7 <h2><%=l(:label_overview)%></h2>
8
8
9 <div class="splitcontentleft">
9 <div class="splitcontentleft">
10 <div class="wiki">
10 <div class="wiki">
11 <%= textilizable @project.description %>
11 <%= textilizable @project.description %>
12 </div>
12 </div>
13 <ul>
13 <ul>
14 <% unless @project.homepage.blank? %>
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 <% end %>
16 <% end %>
17 <% if @subprojects.any? %>
17 <% if @subprojects.any? %>
18 <li><%=l(:label_subproject_plural)%>:
18 <li><%=l(:label_subproject_plural)%>:
19 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ").html_safe %></li>
19 <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ").html_safe %></li>
20 <% end %>
20 <% end %>
21 <% @project.visible_custom_field_values.each do |custom_value| %>
21 <% @project.visible_custom_field_values.each do |custom_value| %>
22 <% if !custom_value.value.blank? %>
22 <% if !custom_value.value.blank? %>
23 <li><%=h custom_value.custom_field.name %>: <%=h show_value(custom_value) %></li>
23 <li><%=h custom_value.custom_field.name %>: <%=h show_value(custom_value) %></li>
24 <% end %>
24 <% end %>
25 <% end %>
25 <% end %>
26 </ul>
26 </ul>
27
27
28 <% if User.current.allowed_to?(:view_issues, @project) %>
28 <% if User.current.allowed_to?(:view_issues, @project) %>
29 <div class="issues box">
29 <div class="issues box">
30 <h3><%=l(:label_issue_tracking)%></h3>
30 <h3><%=l(:label_issue_tracking)%></h3>
31 <ul>
31 <ul>
32 <% for tracker in @trackers %>
32 <% for tracker in @trackers %>
33 <li><%= link_to h(tracker.name), :controller => 'issues', :action => 'index', :project_id => @project,
33 <li><%= link_to h(tracker.name), :controller => 'issues', :action => 'index', :project_id => @project,
34 :set_filter => 1,
34 :set_filter => 1,
35 "tracker_id" => tracker.id %>:
35 "tracker_id" => tracker.id %>:
36 <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
36 <%= l(:label_x_open_issues_abbr_on_total, :count => @open_issues_by_tracker[tracker].to_i,
37 :total => @total_issues_by_tracker[tracker].to_i) %>
37 :total => @total_issues_by_tracker[tracker].to_i) %>
38 </li>
38 </li>
39 <% end %>
39 <% end %>
40 </ul>
40 </ul>
41 <p>
41 <p>
42 <%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %>
42 <%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %>
43 <% if User.current.allowed_to?(:view_calendar, @project, :global => true) %>
43 <% if User.current.allowed_to?(:view_calendar, @project, :global => true) %>
44 | <%= link_to(l(:label_calendar), :controller => 'calendars', :action => 'show', :project_id => @project) %>
44 | <%= link_to(l(:label_calendar), :controller => 'calendars', :action => 'show', :project_id => @project) %>
45 <% end %>
45 <% end %>
46 <% if User.current.allowed_to?(:view_gantt, @project, :global => true) %>
46 <% if User.current.allowed_to?(:view_gantt, @project, :global => true) %>
47 | <%= link_to(l(:label_gantt), :controller => 'gantts', :action => 'show', :project_id => @project) %>
47 | <%= link_to(l(:label_gantt), :controller => 'gantts', :action => 'show', :project_id => @project) %>
48 <% end %>
48 <% end %>
49 </p>
49 </p>
50 </div>
50 </div>
51 <% end %>
51 <% end %>
52 <%= call_hook(:view_projects_show_left, :project => @project) %>
52 <%= call_hook(:view_projects_show_left, :project => @project) %>
53 </div>
53 </div>
54
54
55 <div class="splitcontentright">
55 <div class="splitcontentright">
56 <%= render :partial => 'members_box' %>
56 <%= render :partial => 'members_box' %>
57
57
58 <% if @news.any? && authorize_for('news', 'index') %>
58 <% if @news.any? && authorize_for('news', 'index') %>
59 <div class="news box">
59 <div class="news box">
60 <h3><%=l(:label_news_latest)%></h3>
60 <h3><%=l(:label_news_latest)%></h3>
61 <%= render :partial => 'news/news', :collection => @news %>
61 <%= render :partial => 'news/news', :collection => @news %>
62 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
62 <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p>
63 </div>
63 </div>
64 <% end %>
64 <% end %>
65 <%= call_hook(:view_projects_show_right, :project => @project) %>
65 <%= call_hook(:view_projects_show_right, :project => @project) %>
66 </div>
66 </div>
67
67
68 <% content_for :sidebar do %>
68 <% content_for :sidebar do %>
69 <% if @total_hours.present? %>
69 <% if @total_hours.present? %>
70 <h3><%= l(:label_spent_time) %></h3>
70 <h3><%= l(:label_spent_time) %></h3>
71 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
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}) %> |
72 <p><%= link_to(l(:label_details), project_time_entries_path(@project)) %> |
73 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
73 <%= link_to(l(:label_report), report_project_time_entries_path(@project)) %></p>
74 <% end %>
74 <% end %>
75 <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %>
75 <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %>
76 <% end %>
76 <% end %>
77
77
78 <% content_for :header_tags do %>
78 <% content_for :header_tags do %>
79 <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
79 <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
80 <% end %>
80 <% end %>
81
81
82 <% html_title(l(:label_overview)) -%>
82 <% html_title(l(:label_overview)) -%>
@@ -1,53 +1,53
1 <table style="width:100%">
1 <table style="width:100%">
2 <tr>
2 <tr>
3 <td>
3 <td>
4 <table>
4 <table>
5 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
5 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
6 <% field = filter[0]
6 <% field = filter[0]
7 options = filter[1] %>
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 <td class="field">
9 <td class="field">
10 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
10 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
11 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
11 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
12 </td>
12 </td>
13 <td class="operator">
13 <td class="operator">
14 <%= label_tag "operators_#{field}", l(:description_filter), :class => "hidden-for-sighted" %>
14 <%= label_tag "operators_#{field}", l(:description_filter), :class => "hidden-for-sighted" %>
15 <%= select_tag "op[#{field}]", options_for_select(operators_for_select(options[:type]),
15 <%= select_tag "op[#{field}]", options_for_select(operators_for_select(options[:type]),
16 query.operator_for(field)), :id => "operators_#{field}",
16 query.operator_for(field)), :id => "operators_#{field}",
17 :onchange => "toggle_operator('#{field}');" %>
17 :onchange => "toggle_operator('#{field}');" %>
18 </td>
18 </td>
19 <td class="values">
19 <td class="values">
20 <div id="div_values_<%= field %>" style="display:none;">
20 <div id="div_values_<%= field %>" style="display:none;">
21 <% case options[:type]
21 <% case options[:type]
22 when :list, :list_optional, :list_status, :list_subprojects %>
22 when :list, :list_optional, :list_status, :list_subprojects %>
23 <span class="span_values_<%= field %>">
23 <span class="span_values_<%= field %>">
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) %>
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 <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('values_#{field}_1');" %>
25 <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('values_#{field}_1');" %>
26 </span>
26 </span>
27 <% when :date, :date_past %>
27 <% when :date, :date_past %>
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>
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 <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>
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 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 3, :class => "values_#{field}" %> <%= l(:label_day_plural) %></span>
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 <% when :string, :text %>
31 <% when :string, :text %>
32 <span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}", :size => 30 %></span>
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 <% when :integer, :float %>
33 <% when :integer, :float %>
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>
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 <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>
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 <% end %>
36 <% end %>
37 </div>
37 </div>
38 <script type="text/javascript">toggle_filter('<%= field %>');</script>
38 <script type="text/javascript">toggle_filter('<%= field %>');</script>
39 </td>
39 </td>
40 </tr>
40 </tr>
41 <% end %>
41 <% end %>
42 </table>
42 </table>
43 </td>
43 </td>
44 <td class="add-filter">
44 <td class="add-filter">
45 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
45 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
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),
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 :onchange => "add_filter();",
47 :onchange => "add_filter();",
48 :name => nil %>
48 :name => nil %>
49 </td>
49 </td>
50 </tr>
50 </tr>
51 </table>
51 </table>
52 <%= hidden_field_tag 'f[]', '' %>
52 <%= hidden_field_tag 'f[]', '' %>
53 <%= javascript_tag 'Event.observe(window,"load",apply_filters_observer);' %>
53 <%= javascript_tag 'Event.observe(window,"load",apply_filters_observer);' %>
@@ -1,6 +1,6
1 <h2><%= l(:label_query) %></h2>
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 <%= render :partial => 'form', :locals => {:query => @query} %>
4 <%= render :partial => 'form', :locals => {:query => @query} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= l(:label_query_new) %></h2>
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 <%= render :partial => 'form', :locals => {:query => @query} %>
4 <%= render :partial => 'form', :locals => {:query => @query} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,34 +1,34
1 <% content_for :header_tags do %>
1 <% content_for :header_tags do %>
2 <%= javascript_include_tag 'repository_navigation' %>
2 <%= javascript_include_tag 'repository_navigation' %>
3 <% end %>
3 <% end %>
4
4
5 <%= link_to l(:label_statistics),
5 <%= link_to l(:label_statistics),
6 {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param},
6 {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param},
7 :class => 'icon icon-stats' if @repository.supports_all_revisions? %>
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 :id => @project,
10 :id => @project,
11 :repository_id => @repository.identifier_param,
11 :repository_id => @repository.identifier_param,
12 :path => to_path_param(@path),
12 :path => to_path_param(@path),
13 :rev => nil},
13 :rev => nil},
14 {:method => :get, :id => 'revision_selector'}) do -%>
14 {:method => :get, :id => 'revision_selector'}) do -%>
15 <!-- Branches Dropdown -->
15 <!-- Branches Dropdown -->
16 <% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
16 <% if !@repository.branches.nil? && @repository.branches.length > 0 -%>
17 | <%= l(:label_branch) %>:
17 | <%= l(:label_branch) %>:
18 <%= select_tag :branch,
18 <%= select_tag :branch,
19 options_for_select([''] + @repository.branches, @rev),
19 options_for_select([''] + @repository.branches, @rev),
20 :id => 'branch' %>
20 :id => 'branch' %>
21 <% end -%>
21 <% end -%>
22
22
23 <% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
23 <% if !@repository.tags.nil? && @repository.tags.length > 0 -%>
24 | <%= l(:label_tag) %>:
24 | <%= l(:label_tag) %>:
25 <%= select_tag :tag,
25 <%= select_tag :tag,
26 options_for_select([''] + @repository.tags, @rev),
26 options_for_select([''] + @repository.tags, @rev),
27 :id => 'tag' %>
27 :id => 'tag' %>
28 <% end -%>
28 <% end -%>
29
29
30 <% if @repository.supports_all_revisions? %>
30 <% if @repository.supports_all_revisions? %>
31 | <%= l(:label_revision) %>:
31 | <%= l(:label_revision) %>:
32 <%= text_field_tag 'rev', @rev, :size => 8 %>
32 <%= text_field_tag 'rev', @rev, :size => 8 %>
33 <% end %>
33 <% end %>
34 <% end -%>
34 <% end -%>
@@ -1,48 +1,48
1 <% show_revision_graph = ( @repository.supports_revision_graph? && path.blank? ) %>
1 <% show_revision_graph = ( @repository.supports_revision_graph? && path.blank? ) %>
2 <%= if show_revision_graph && revisions && revisions.any?
2 <%= if show_revision_graph && revisions && revisions.any?
3 indexed_commits, graph_space = index_commits(revisions, @repository.branches) do |scmid|
3 indexed_commits, graph_space = index_commits(revisions, @repository.branches) do |scmid|
4 url_for(
4 url_for(
5 :controller => 'repositories',
5 :controller => 'repositories',
6 :action => 'revision',
6 :action => 'revision',
7 :id => project,
7 :id => project,
8 :repository_id => @repository.identifier_param,
8 :repository_id => @repository.identifier_param,
9 :rev => scmid)
9 :rev => scmid)
10 end
10 end
11 render :partial => 'revision_graph',
11 render :partial => 'revision_graph',
12 :locals => {
12 :locals => {
13 :commits => indexed_commits,
13 :commits => indexed_commits,
14 :space => graph_space
14 :space => graph_space
15 }
15 }
16 end %>
16 end %>
17 <% form_tag(
17 <%= form_tag(
18 {:controller => 'repositories', :action => 'diff', :id => project,
18 {:controller => 'repositories', :action => 'diff', :id => project,
19 :repository_id => @repository.identifier_param, :path => to_path_param(path)},
19 :repository_id => @repository.identifier_param, :path => to_path_param(path)},
20 :method => :get
20 :method => :get
21 ) do %>
21 ) do %>
22 <table class="list changesets">
22 <table class="list changesets">
23 <thead><tr>
23 <thead><tr>
24 <th>#</th>
24 <th>#</th>
25 <th></th>
25 <th></th>
26 <th></th>
26 <th></th>
27 <th><%= l(:label_date) %></th>
27 <th><%= l(:label_date) %></th>
28 <th><%= l(:field_author) %></th>
28 <th><%= l(:field_author) %></th>
29 <th><%= l(:field_comments) %></th>
29 <th><%= l(:field_comments) %></th>
30 </tr></thead>
30 </tr></thead>
31 <tbody>
31 <tbody>
32 <% show_diff = revisions.size > 1 %>
32 <% show_diff = revisions.size > 1 %>
33 <% line_num = 1 %>
33 <% line_num = 1 %>
34 <% revisions.each do |changeset| %>
34 <% revisions.each do |changeset| %>
35 <tr class="changeset <%= cycle 'odd', 'even' %>">
35 <tr class="changeset <%= cycle 'odd', 'even' %>">
36 <td class="id"<%= show_revision_graph ? " style=\"padding-left:#{(graph_space + 1) * 20}px\"" : '' %>><%= link_to_revision(changeset, @repository) %></td>
36 <td class="id"<%= show_revision_graph ? " style=\"padding-left:#{(graph_space + 1) * 20}px\"" : '' %>><%= link_to_revision(changeset, @repository) %></td>
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>
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 <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>
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 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
39 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
40 <td class="author"><%= h truncate(changeset.author.to_s, :length => 30) %></td>
40 <td class="author"><%= h truncate(changeset.author.to_s, :length => 30) %></td>
41 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
41 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
42 </tr>
42 </tr>
43 <% line_num += 1 %>
43 <% line_num += 1 %>
44 <% end %>
44 <% end %>
45 </tbody>
45 </tbody>
46 </table>
46 </table>
47 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
47 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
48 <% end %>
48 <% end %>
@@ -1,42 +1,42
1 <h2><%= l(:label_repository) %></h2>
1 <h2><%= l(:label_repository) %></h2>
2
2
3 <%= simple_format(l(:text_repository_usernames_mapping)) %>
3 <%= simple_format(l(:text_repository_usernames_mapping)) %>
4
4
5 <% if @committers.empty? %>
5 <% if @committers.empty? %>
6 <p class="nodata"><%= l(:label_no_data) %></p>
6 <p class="nodata"><%= l(:label_no_data) %></p>
7 <% else %>
7 <% else %>
8
8
9 <% form_tag({}) do %>
9 <%= form_tag({}) do %>
10 <table class="list">
10 <table class="list">
11 <thead>
11 <thead>
12 <tr>
12 <tr>
13 <th><%= l(:field_login) %></th>
13 <th><%= l(:field_login) %></th>
14 <th><%= l(:label_user) %></th>
14 <th><%= l(:label_user) %></th>
15 </tr>
15 </tr>
16 </thead>
16 </thead>
17 <tbody>
17 <tbody>
18 <% i = 0 -%>
18 <% i = 0 -%>
19 <% @committers.each do |committer, user_id| -%>
19 <% @committers.each do |committer, user_id| -%>
20 <tr class="<%= cycle 'odd', 'even' %>">
20 <tr class="<%= cycle 'odd', 'even' %>">
21 <td><%=h committer %></td>
21 <td><%=h committer %></td>
22 <td>
22 <td>
23 <%= hidden_field_tag "committers[#{i}][]", committer %>
23 <%= hidden_field_tag "committers[#{i}][]", committer %>
24 <%= select_tag "committers[#{i}][]",
24 <%= select_tag "committers[#{i}][]",
25 content_tag(
25 content_tag(
26 'option',
26 'option',
27 "-- #{l :actionview_instancetag_blank_option} --",
27 "-- #{l :actionview_instancetag_blank_option} --",
28 :value => ''
28 :value => ''
29 ) +
29 ) +
30 options_from_collection_for_select(
30 options_from_collection_for_select(
31 @users, 'id', 'name', user_id.to_i
31 @users, 'id', 'name', user_id.to_i
32 ) %>
32 ) %>
33 </td>
33 </td>
34 </tr>
34 </tr>
35 <% i += 1 -%>
35 <% i += 1 -%>
36 <% end -%>
36 <% end -%>
37 </tbody>
37 </tbody>
38 </table>
38 </table>
39 <p><%= submit_tag(l(:button_update)) %></p>
39 <p><%= submit_tag(l(:button_update)) %></p>
40 <% end %>
40 <% end %>
41
41
42 <% end %>
42 <% end %>
@@ -1,28 +1,28
1 <h2><%= l(:label_revision) %> <%= @diff_format_revisions %> <%=h @path %></h2>
1 <h2><%= l(:label_revision) %> <%= @diff_format_revisions %> <%=h @path %></h2>
2
2
3 <!-- Choose view type -->
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 <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
5 <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
6 <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
6 <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
7 <p>
7 <p>
8 <label><%= l(:label_view_diff) %></label>
8 <label><%= l(:label_view_diff) %></label>
9 <%= select_tag 'type',
9 <%= select_tag 'type',
10 options_for_select(
10 options_for_select(
11 [[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
11 [[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
12 :onchange => "if (this.value != '') {this.form.submit()}" %>
12 :onchange => "if (this.value != '') {this.form.submit()}" %>
13 </p>
13 </p>
14 <% end %>
14 <% end %>
15
15
16 <% cache(@cache_key) do -%>
16 <% cache(@cache_key) do -%>
17 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
17 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
18 <% end -%>
18 <% end -%>
19
19
20 <% other_formats_links do |f| %>
20 <% other_formats_links do |f| %>
21 <%= f.link_to 'Diff', :url => params, :caption => 'Unified diff' %>
21 <%= f.link_to 'Diff', :url => params, :caption => 'Unified diff' %>
22 <% end %>
22 <% end %>
23
23
24 <% html_title(with_leading_slash(@path), 'Diff') -%>
24 <% html_title(with_leading_slash(@path), 'Diff') -%>
25
25
26 <% content_for :header_tags do %>
26 <% content_for :header_tags do %>
27 <%= stylesheet_link_tag "scm" %>
27 <%= stylesheet_link_tag "scm" %>
28 <% end %>
28 <% end %>
@@ -1,5 +1,5
1 <h2><%= l(:label_repository) %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <% end %>
5 <% end %>
@@ -1,5 +1,5
1 <h2><%= l(:label_repository_new) %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <% end %>
5 <% end %>
@@ -1,97 +1,97
1 <div class="contextual">
1 <div class="contextual">
2 &#171;
2 &#171;
3 <% unless @changeset.previous.nil? -%>
3 <% unless @changeset.previous.nil? -%>
4 <%= link_to_revision(@changeset.previous, @repository, :text => l(:label_previous)) %>
4 <%= link_to_revision(@changeset.previous, @repository, :text => l(:label_previous)) %>
5 <% else -%>
5 <% else -%>
6 <%= l(:label_previous) %>
6 <%= l(:label_previous) %>
7 <% end -%>
7 <% end -%>
8 |
8 |
9 <% unless @changeset.next.nil? -%>
9 <% unless @changeset.next.nil? -%>
10 <%= link_to_revision(@changeset.next, @repository, :text => l(:label_next)) %>
10 <%= link_to_revision(@changeset.next, @repository, :text => l(:label_next)) %>
11 <% else -%>
11 <% else -%>
12 <%= l(:label_next) %>
12 <%= l(:label_next) %>
13 <% end -%>
13 <% end -%>
14 &#187;&nbsp;
14 &#187;&nbsp;
15
15
16 <% form_tag({:controller => 'repositories',
16 <%= form_tag({:controller => 'repositories',
17 :action => 'revision',
17 :action => 'revision',
18 :id => @project,
18 :id => @project,
19 :repository_id => @repository.identifier_param,
19 :repository_id => @repository.identifier_param,
20 :rev => nil},
20 :rev => nil},
21 :method => :get) do %>
21 :method => :get) do %>
22 <%= text_field_tag 'rev', @rev, :size => 8 %>
22 <%= text_field_tag 'rev', @rev, :size => 8 %>
23 <%= submit_tag 'OK', :name => nil %>
23 <%= submit_tag 'OK', :name => nil %>
24 <% end %>
24 <% end %>
25 </div>
25 </div>
26
26
27 <h2><%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
27 <h2><%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
28
28
29 <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %>
29 <% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %>
30 <table class="revision-info">
30 <table class="revision-info">
31 <% if @changeset.scmid.present? %>
31 <% if @changeset.scmid.present? %>
32 <tr>
32 <tr>
33 <td>ID</td><td><%= h(@changeset.scmid) %></td>
33 <td>ID</td><td><%= h(@changeset.scmid) %></td>
34 </tr>
34 </tr>
35 <% end %>
35 <% end %>
36 <% if @changeset.parents.present? %>
36 <% if @changeset.parents.present? %>
37 <tr>
37 <tr>
38 <td><%= l(:label_parent_revision) %></td>
38 <td><%= l(:label_parent_revision) %></td>
39 <td>
39 <td>
40 <%= @changeset.parents.collect{
40 <%= @changeset.parents.collect{
41 |p| link_to_revision(p, @repository, :text => format_revision(p))
41 |p| link_to_revision(p, @repository, :text => format_revision(p))
42 }.join(", ").html_safe %>
42 }.join(", ").html_safe %>
43 </td>
43 </td>
44 </tr>
44 </tr>
45 <% end %>
45 <% end %>
46 <% if @changeset.children.present? %>
46 <% if @changeset.children.present? %>
47 <tr>
47 <tr>
48 <td><%= l(:label_child_revision) %></td>
48 <td><%= l(:label_child_revision) %></td>
49 <td>
49 <td>
50 <%= @changeset.children.collect{
50 <%= @changeset.children.collect{
51 |p| link_to_revision(p, @repository, :text => format_revision(p))
51 |p| link_to_revision(p, @repository, :text => format_revision(p))
52 }.join(", ").html_safe %>
52 }.join(", ").html_safe %>
53 </td>
53 </td>
54 </tr>
54 </tr>
55 <% end %>
55 <% end %>
56 </table>
56 </table>
57 <% end %>
57 <% end %>
58
58
59 <p>
59 <p>
60 <span class="author">
60 <span class="author">
61 <%= authoring(@changeset.committed_on, @changeset.author) %>
61 <%= authoring(@changeset.committed_on, @changeset.author) %>
62 </span>
62 </span>
63 </p>
63 </p>
64
64
65 <%= textilizable @changeset.comments %>
65 <%= textilizable @changeset.comments %>
66
66
67 <% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %>
67 <% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %>
68 <%= render :partial => 'related_issues' %>
68 <%= render :partial => 'related_issues' %>
69 <% end %>
69 <% end %>
70
70
71 <% if User.current.allowed_to?(:browse_repository, @project) %>
71 <% if User.current.allowed_to?(:browse_repository, @project) %>
72 <h3><%= l(:label_attachment_plural) %></h3>
72 <h3><%= l(:label_attachment_plural) %></h3>
73 <ul id="changes-legend">
73 <ul id="changes-legend">
74 <li class="change change-A"><%= l(:label_added) %></li>
74 <li class="change change-A"><%= l(:label_added) %></li>
75 <li class="change change-M"><%= l(:label_modified) %></li>
75 <li class="change change-M"><%= l(:label_modified) %></li>
76 <li class="change change-C"><%= l(:label_copied) %></li>
76 <li class="change change-C"><%= l(:label_copied) %></li>
77 <li class="change change-R"><%= l(:label_renamed) %></li>
77 <li class="change change-R"><%= l(:label_renamed) %></li>
78 <li class="change change-D"><%= l(:label_deleted) %></li>
78 <li class="change change-D"><%= l(:label_deleted) %></li>
79 </ul>
79 </ul>
80
80
81 <p><%= link_to(l(:label_view_diff),
81 <p><%= link_to(l(:label_view_diff),
82 :action => 'diff',
82 :action => 'diff',
83 :id => @project,
83 :id => @project,
84 :repository_id => @repository.identifier_param,
84 :repository_id => @repository.identifier_param,
85 :path => "",
85 :path => "",
86 :rev => @changeset.identifier) if @changeset.changes.any? %></p>
86 :rev => @changeset.identifier) if @changeset.changes.any? %></p>
87
87
88 <div class="changeset-changes">
88 <div class="changeset-changes">
89 <%= render_changeset_changes %>
89 <%= render_changeset_changes %>
90 </div>
90 </div>
91 <% end %>
91 <% end %>
92
92
93 <% content_for :header_tags do %>
93 <% content_for :header_tags do %>
94 <%= stylesheet_link_tag "scm" %>
94 <%= stylesheet_link_tag "scm" %>
95 <% end %>
95 <% end %>
96
96
97 <% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%>
97 <% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%>
@@ -1,33 +1,33
1 <div class="contextual">
1 <div class="contextual">
2 <% form_tag(
2 <%= form_tag(
3 {:action => 'revision', :id => @project,
3 {:controller => 'repositories', :action => 'revision', :id => @project,
4 :repository_id => @repository.identifier_param}
4 :repository_id => @repository.identifier_param}
5 ) do %>
5 ) do %>
6 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
6 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
7 <%= submit_tag 'OK' %>
7 <%= submit_tag 'OK' %>
8 <% end %>
8 <% end %>
9 </div>
9 </div>
10
10
11 <h2><%= l(:label_revision_plural) %></h2>
11 <h2><%= l(:label_revision_plural) %></h2>
12
12
13 <%= render :partial => 'revisions',
13 <%= render :partial => 'revisions',
14 :locals => {:project => @project,
14 :locals => {:project => @project,
15 :path => '',
15 :path => '',
16 :revisions => @changesets,
16 :revisions => @changesets,
17 :entry => nil } %>
17 :entry => nil } %>
18
18
19 <p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p>
19 <p class="pagination"><%= pagination_links_full @changeset_pages,@changeset_count %></p>
20
20
21 <% content_for :header_tags do %>
21 <% content_for :header_tags do %>
22 <%= stylesheet_link_tag "scm" %>
22 <%= stylesheet_link_tag "scm" %>
23 <%= auto_discovery_link_tag(
23 <%= auto_discovery_link_tag(
24 :atom,
24 :atom,
25 params.merge(
25 params.merge(
26 {:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
26 {:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
27 <% end %>
27 <% end %>
28
28
29 <% other_formats_links do |f| %>
29 <% other_formats_links do |f| %>
30 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
30 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
31 <% end %>
31 <% end %>
32
32
33 <% html_title(l(:label_revision_plural)) -%>
33 <% html_title(l(:label_revision_plural)) -%>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=h @role.name %></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_role_new)%></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -1,55 +1,55
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_permissions_report)%></h2>
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 <%= hidden_field_tag 'permissions[0]', '', :id => nil %>
4 <%= hidden_field_tag 'permissions[0]', '', :id => nil %>
5 <div class="autoscroll">
5 <div class="autoscroll">
6 <table class="list permissions">
6 <table class="list permissions">
7 <thead>
7 <thead>
8 <tr>
8 <tr>
9 <th><%=l(:label_permissions)%></th>
9 <th><%=l(:label_permissions)%></th>
10 <% @roles.each do |role| %>
10 <% @roles.each do |role| %>
11 <th>
11 <th>
12 <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %>
12 <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %>
13 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')",
13 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')",
14 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
14 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
15 </th>
15 </th>
16 <% end %>
16 <% end %>
17 </tr>
17 </tr>
18 </thead>
18 </thead>
19 <tbody>
19 <tbody>
20 <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
20 <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
21 <% perms_by_module.keys.sort.each do |mod| %>
21 <% perms_by_module.keys.sort.each do |mod| %>
22 <% unless mod.blank? %>
22 <% unless mod.blank? %>
23 <tr class="group open">
23 <tr class="group open">
24 <td>
24 <td>
25 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
25 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
26 <%= l_or_humanize(mod, :prefix => 'project_module_') %>
26 <%= l_or_humanize(mod, :prefix => 'project_module_') %>
27 </td>
27 </td>
28 <% @roles.each do |role| %>
28 <% @roles.each do |role| %>
29 <td class="role"><%= h(role.name) %></td>
29 <td class="role"><%= h(role.name) %></td>
30 <% end %>
30 <% end %>
31 </tr>
31 </tr>
32 <% end %>
32 <% end %>
33 <% perms_by_module[mod].each do |permission| %>
33 <% perms_by_module[mod].each do |permission| %>
34 <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>">
34 <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>">
35 <td>
35 <td>
36 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')",
36 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')",
37 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
37 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
38 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
38 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
39 </td>
39 </td>
40 <% @roles.each do |role| %>
40 <% @roles.each do |role| %>
41 <td align="center">
41 <td align="center">
42 <% if role.setable_permissions.include? permission %>
42 <% if role.setable_permissions.include? permission %>
43 <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %>
43 <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %>
44 <% end %>
44 <% end %>
45 </td>
45 </td>
46 <% end %>
46 <% end %>
47 </tr>
47 </tr>
48 <% end %>
48 <% end %>
49 <% end %>
49 <% end %>
50 </tbody>
50 </tbody>
51 </table>
51 </table>
52 </div>
52 </div>
53 <p><%= check_all_links 'permissions_form' %></p>
53 <p><%= check_all_links 'permissions_form' %></p>
54 <p><%= submit_tag l(:button_save) %></p>
54 <p><%= submit_tag l(:button_save) %></p>
55 <% end %>
55 <% end %>
@@ -1,52 +1,52
1 <h2><%= l(:label_search) %></h2>
1 <h2><%= l(:label_search) %></h2>
2
2
3 <div class="box">
3 <div class="box">
4 <% form_tag({}, :method => :get) do %>
4 <%= form_tag({}, :method => :get) do %>
5 <%= label_tag "search-input", l(:description_search), :class => "hidden-for-sighted" %>
5 <%= label_tag "search-input", l(:description_search), :class => "hidden-for-sighted" %>
6 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
6 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
7 <%= javascript_tag "Field.focus('search-input')" %>
7 <%= javascript_tag "Field.focus('search-input')" %>
8 <%= project_select_tag %>
8 <%= project_select_tag %>
9 <%= hidden_field_tag 'all_words', '', :id => nil %>
9 <%= hidden_field_tag 'all_words', '', :id => nil %>
10 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
10 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
11 <%= hidden_field_tag 'titles_only', '', :id => nil %>
11 <%= hidden_field_tag 'titles_only', '', :id => nil %>
12 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
12 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
13 </p>
13 </p>
14 <p>
14 <p>
15 <% @object_types.each do |t| %>
15 <% @object_types.each do |t| %>
16 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
16 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
17 <% end %>
17 <% end %>
18 </p>
18 </p>
19
19
20 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
20 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
21 <% end %>
21 <% end %>
22 </div>
22 </div>
23
23
24 <% if @results %>
24 <% if @results %>
25 <div id="search-results-counts">
25 <div id="search-results-counts">
26 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
26 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
27 </div>
27 </div>
28
28
29 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
29 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
30 <dl id="search-results">
30 <dl id="search-results">
31 <% @results.each do |e| %>
31 <% @results.each do |e| %>
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>
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 <dd><span class="description"><%= highlight_tokens(h(e.event_description), @tokens) %></span>
33 <dd><span class="description"><%= highlight_tokens(h(e.event_description), @tokens) %></span>
34 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
34 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
35 <% end %>
35 <% end %>
36 </dl>
36 </dl>
37 <% end %>
37 <% end %>
38
38
39 <p><center>
39 <p><center>
40 <% if @pagination_previous_date %>
40 <% if @pagination_previous_date %>
41 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
41 <%= link_to_content_update("\xc2\xab " + l(:label_previous),
42 params.merge(:previous => 1,
42 params.merge(:previous => 1,
43 :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
43 :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
44 <% end %>
44 <% end %>
45 <% if @pagination_next_date %>
45 <% if @pagination_next_date %>
46 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
46 <%= link_to_content_update(l(:label_next) + " \xc2\xbb",
47 params.merge(:previous => nil,
47 params.merge(:previous => nil,
48 :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
48 :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
49 <% end %>
49 <% end %>
50 </center></p>
50 </center></p>
51
51
52 <% html_title(l(:label_search)) -%>
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 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :login_required %></p>
4 <p><%= setting_check_box :login_required %></p>
5
5
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>
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 <p><%= setting_select :self_registration, [[l(:label_disabled), "0"],
8 <p><%= setting_select :self_registration, [[l(:label_disabled), "0"],
9 [l(:label_registration_activation_by_email), "1"],
9 [l(:label_registration_activation_by_email), "1"],
10 [l(:label_registration_manual_activation), "2"],
10 [l(:label_registration_manual_activation), "2"],
11 [l(:label_registration_automatic_activation), "3"]] %></p>
11 [l(:label_registration_automatic_activation), "3"]] %></p>
12
12
13 <p><%= setting_check_box :unsubscribe %></p>
13 <p><%= setting_check_box :unsubscribe %></p>
14
14
15 <p><%= setting_text_field :password_min_length, :size => 6 %></p>
15 <p><%= setting_text_field :password_min_length, :size => 6 %></p>
16
16
17 <p><%= setting_check_box :lost_password, :label => :label_password_lost %></p>
17 <p><%= setting_check_box :lost_password, :label => :label_password_lost %></p>
18
18
19 <p><%= setting_check_box :openid, :disabled => !Object.const_defined?(:OpenID) %></p>
19 <p><%= setting_check_box :openid, :disabled => !Object.const_defined?(:OpenID) %></p>
20
20
21 <p><%= setting_check_box :rest_api_enabled %></p>
21 <p><%= setting_check_box :rest_api_enabled %></p>
22 </div>
22 </div>
23
23
24 <%= submit_tag l(:button_save) %>
24 <%= submit_tag l(:button_save) %>
25 <% end %>
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 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_select :ui_theme, Redmine::Themes.themes.collect {|t| [t.name, t.id]}, :blank => :label_default, :label => :label_theme %></p>
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 <p><%= setting_select :default_language, lang_options_for_select(false) %></p>
6 <p><%= setting_select :default_language, lang_options_for_select(false) %></p>
7
7
8 <p><%= setting_select :start_of_week, [[day_name(1),'1'], [day_name(6),'6'], [day_name(7),'7']], :blank => :label_language_based %></p>
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 <p><%= setting_select :date_format, Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, :blank => :label_language_based %></p>
10 <p><%= setting_select :date_format, Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, :blank => :label_language_based %></p>
11
11
12 <p><%= setting_select :time_format, Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, :blank => :label_language_based %></p>
12 <p><%= setting_select :time_format, Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, :blank => :label_language_based %></p>
13
13
14 <p><%= setting_select :user_format, @options[:user_format] %></p>
14 <p><%= setting_select :user_format, @options[:user_format] %></p>
15
15
16 <p><%= setting_check_box :gravatar_enabled %></p>
16 <p><%= setting_check_box :gravatar_enabled %></p>
17
17
18 <p><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></p>
18 <p><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></p>
19 </div>
19 </div>
20
20
21 <%= submit_tag l(:button_save) %>
21 <%= submit_tag l(:button_save) %>
22 <% end %>
22 <% end %>
@@ -1,40 +1,40
1 <% form_tag({:action => 'edit'}) do %>
1 <%= form_tag({:action => 'edit'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_text_field :app_title, :size => 30 %></p>
4 <p><%= setting_text_field :app_title, :size => 30 %></p>
5
5
6 <p><%= setting_text_area :welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit' %></p>
6 <p><%= setting_text_area :welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit' %></p>
7 <%= wikitoolbar_for 'settings_welcome_text' %>
7 <%= wikitoolbar_for 'settings_welcome_text' %>
8
8
9 <p><%= setting_text_field :attachment_max_size, :size => 6 %> <%= l(:"number.human.storage_units.units.kb") %></p>
9 <p><%= setting_text_field :attachment_max_size, :size => 6 %> <%= l(:"number.human.storage_units.units.kb") %></p>
10
10
11 <p><%= setting_text_field :per_page_options, :size => 20 %>
11 <p><%= setting_text_field :per_page_options, :size => 20 %>
12 <em class="info"><%= l(:text_comma_separated) %></em></p>
12 <em class="info"><%= l(:text_comma_separated) %></em></p>
13
13
14 <p><%= setting_text_field :activity_days_default, :size => 6 %> <%= l(:label_day_plural) %></p>
14 <p><%= setting_text_field :activity_days_default, :size => 6 %> <%= l(:label_day_plural) %></p>
15
15
16 <p><%= setting_text_field :host_name, :size => 60 %>
16 <p><%= setting_text_field :host_name, :size => 60 %>
17 <em class="info"><%= l(:label_example) %>: <%= @guessed_host_and_path %></em></p>
17 <em class="info"><%= l(:label_example) %>: <%= @guessed_host_and_path %></em></p>
18
18
19 <p><%= setting_select :protocol, [['HTTP', 'http'], ['HTTPS', 'https']] %></p>
19 <p><%= setting_select :protocol, [['HTTP', 'http'], ['HTTPS', 'https']] %></p>
20
20
21 <p><%= setting_select :text_formatting, Redmine::WikiFormatting.format_names.collect{|name| [name, name.to_s]}, :blank => :label_none %></p>
21 <p><%= setting_select :text_formatting, Redmine::WikiFormatting.format_names.collect{|name| [name, name.to_s]}, :blank => :label_none %></p>
22
22
23 <p><%= setting_check_box :cache_formatted_text %></p>
23 <p><%= setting_check_box :cache_formatted_text %></p>
24
24
25 <p><%= setting_select :wiki_compression, [['Gzip', 'gzip']], :blank => :label_none %></p>
25 <p><%= setting_select :wiki_compression, [['Gzip', 'gzip']], :blank => :label_none %></p>
26
26
27 <p><%= setting_text_field :feeds_limit, :size => 6 %></p>
27 <p><%= setting_text_field :feeds_limit, :size => 6 %></p>
28
28
29 <p><%= setting_text_field :file_max_size_displayed, :size => 6 %> <%= l(:"number.human.storage_units.units.kb") %></p>
29 <p><%= setting_text_field :file_max_size_displayed, :size => 6 %> <%= l(:"number.human.storage_units.units.kb") %></p>
30
30
31 <p><%= setting_text_field :diff_max_lines_displayed, :size => 6 %></p>
31 <p><%= setting_text_field :diff_max_lines_displayed, :size => 6 %></p>
32
32
33 <p><%= setting_text_field :repositories_encodings, :size => 60 %>
33 <p><%= setting_text_field :repositories_encodings, :size => 60 %>
34 <em class="info"><%= l(:text_comma_separated) %></em></p>
34 <em class="info"><%= l(:text_comma_separated) %></em></p>
35
35
36 <%= call_hook(:view_settings_general_form) %>
36 <%= call_hook(:view_settings_general_form) %>
37 </div>
37 </div>
38
38
39 <%= submit_tag l(:button_save) %>
39 <%= submit_tag l(:button_save) %>
40 <% end %>
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 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :cross_project_issue_relations %></p>
4 <p><%= setting_check_box :cross_project_issue_relations %></p>
5
5
6 <p><%= setting_check_box :issue_group_assignment %></p>
6 <p><%= setting_check_box :issue_group_assignment %></p>
7
7
8 <p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
8 <p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
9
9
10 <p><%= setting_check_box :display_subprojects_issues %></p>
10 <p><%= setting_check_box :display_subprojects_issues %></p>
11
11
12 <p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
12 <p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
13
13
14 <p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
14 <p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
15
15
16 <p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
16 <p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
17 </div>
17 </div>
18
18
19 <fieldset class="box settings"><legend><%= l(:setting_issue_list_default_columns) %></legend>
19 <fieldset class="box settings"><legend><%= l(:setting_issue_list_default_columns) %></legend>
20 <%= setting_multiselect(:issue_list_default_columns,
20 <%= setting_multiselect(:issue_list_default_columns,
21 Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label => false) %>
21 Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label => false) %>
22 </fieldset>
22 </fieldset>
23
23
24 <%= submit_tag l(:button_save) %>
24 <%= submit_tag l(:button_save) %>
25 <% end %>
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 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p>
4 <p>
5 <%= setting_text_area :mail_handler_body_delimiters, :rows => 5 %>
5 <%= setting_text_area :mail_handler_body_delimiters, :rows => 5 %>
6 <em class="info"><%= l(:text_line_separated) %></em>
6 <em class="info"><%= l(:text_line_separated) %></em>
7 </p>
7 </p>
8 </div>
8 </div>
9
9
10 <div class="box tabular settings">
10 <div class="box tabular settings">
11 <p><%= setting_check_box :mail_handler_api_enabled,
11 <p><%= setting_check_box :mail_handler_api_enabled,
12 :onclick => "if (this.checked) { Form.Element.enable('settings_mail_handler_api_key'); } else { Form.Element.disable('settings_mail_handler_api_key'); }"%></p>
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 <p><%= setting_text_field :mail_handler_api_key, :size => 30,
14 <p><%= setting_text_field :mail_handler_api_key, :size => 30,
15 :id => 'settings_mail_handler_api_key',
15 :id => 'settings_mail_handler_api_key',
16 :disabled => !Setting.mail_handler_api_enabled? %>
16 :disabled => !Setting.mail_handler_api_enabled? %>
17 <%= link_to_function l(:label_generate_key), "if ($('settings_mail_handler_api_key').disabled == false) { $('settings_mail_handler_api_key').value = randomKey(20) }" %>
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 </p>
18 </p>
19 </div>
19 </div>
20
20
21 <%= submit_tag l(:button_save) %>
21 <%= submit_tag l(:button_save) %>
22
22
23 <% end %>
23 <% end %>
@@ -1,42 +1,42
1 <% if @deliveries %>
1 <% if @deliveries %>
2 <% form_tag({:action => 'edit', :tab => 'notifications'}) do %>
2 <%= form_tag({:action => 'edit', :tab => 'notifications'}) do %>
3
3
4 <div class="box tabular settings">
4 <div class="box tabular settings">
5 <p><%= setting_text_field :mail_from, :size => 60 %></p>
5 <p><%= setting_text_field :mail_from, :size => 60 %></p>
6
6
7 <p><%= setting_check_box :bcc_recipients %></p>
7 <p><%= setting_check_box :bcc_recipients %></p>
8
8
9 <p><%= setting_check_box :plain_text_mail %></p>
9 <p><%= setting_check_box :plain_text_mail %></p>
10
10
11 <p><%= setting_select(:default_notification_option, User.valid_notification_options.collect {|o| [l(o.last), o.first.to_s]}) %></p>
11 <p><%= setting_select(:default_notification_option, User.valid_notification_options.collect {|o| [l(o.last), o.first.to_s]}) %></p>
12
12
13 </div>
13 </div>
14
14
15 <fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend>
15 <fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend>
16 <%= hidden_field_tag 'settings[notified_events][]', '' %>
16 <%= hidden_field_tag 'settings[notified_events][]', '' %>
17 <% @notifiables.each do |notifiable| %>
17 <% @notifiables.each do |notifiable| %>
18 <%= notification_field notifiable %>
18 <%= notification_field notifiable %>
19 <br />
19 <br />
20 <% end %>
20 <% end %>
21 <p><%= check_all_links('notified_events') %></p>
21 <p><%= check_all_links('notified_events') %></p>
22 </fieldset>
22 </fieldset>
23
23
24 <fieldset class="box"><legend><%= l(:setting_emails_header) %></legend>
24 <fieldset class="box"><legend><%= l(:setting_emails_header) %></legend>
25 <%= setting_text_area :emails_header, :label => false, :class => 'wiki-edit', :rows => 5 %>
25 <%= setting_text_area :emails_header, :label => false, :class => 'wiki-edit', :rows => 5 %>
26 </fieldset>
26 </fieldset>
27
27
28 <fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend>
28 <fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend>
29 <%= setting_text_area :emails_footer, :label => false, :class => 'wiki-edit', :rows => 5 %>
29 <%= setting_text_area :emails_footer, :label => false, :class => 'wiki-edit', :rows => 5 %>
30 </fieldset>
30 </fieldset>
31
31
32 <div style="float:right;">
32 <div style="float:right;">
33 <%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %>
33 <%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %>
34 </div>
34 </div>
35
35
36 <%= submit_tag l(:button_save) %>
36 <%= submit_tag l(:button_save) %>
37 <% end %>
37 <% end %>
38 <% else %>
38 <% else %>
39 <div class="nodata">
39 <div class="nodata">
40 <%= simple_format(l(:text_email_delivery_not_configured)) %>
40 <%= simple_format(l(:text_email_delivery_not_configured)) %>
41 </div>
41 </div>
42 <% end %>
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 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :default_projects_public %></p>
4 <p><%= setting_check_box :default_projects_public %></p>
5
5
6 <p><%= setting_multiselect(:default_projects_modules,
6 <p><%= setting_multiselect(:default_projects_modules,
7 Redmine::AccessControl.available_project_modules.collect {|m| [l_or_humanize(m, :prefix => "project_module_"), m.to_s]}) %></p>
7 Redmine::AccessControl.available_project_modules.collect {|m| [l_or_humanize(m, :prefix => "project_module_"), m.to_s]}) %></p>
8
8
9 <p><%= setting_check_box :sequential_project_identifiers %></p>
9 <p><%= setting_check_box :sequential_project_identifiers %></p>
10
10
11 <p><%= setting_select :new_project_user_role_id,
11 <p><%= setting_select :new_project_user_role_id,
12 Role.find_all_givable.collect {|r| [r.name, r.id.to_s]},
12 Role.find_all_givable.collect {|r| [r.name, r.id.to_s]},
13 :blank => "--- #{l(:actionview_instancetag_blank_option)} ---" %></p>
13 :blank => "--- #{l(:actionview_instancetag_blank_option)} ---" %></p>
14 </div>
14 </div>
15
15
16 <%= submit_tag l(:button_save) %>
16 <%= submit_tag l(:button_save) %>
17 <% end %>
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 <fieldset class="box settings enabled_scm">
3 <fieldset class="box settings enabled_scm">
4 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
4 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
5 <legend><%= l(:setting_enabled_scm) %></legend>
5 <legend><%= l(:setting_enabled_scm) %></legend>
6 <table>
6 <table>
7 <tr>
7 <tr>
8 <th></th>
8 <th></th>
9 <th><%= l(:text_scm_command) %></th>
9 <th><%= l(:text_scm_command) %></th>
10 <th><%= l(:text_scm_command_version) %></th>
10 <th><%= l(:text_scm_command_version) %></th>
11 </tr>
11 </tr>
12 <% Redmine::Scm::Base.all.collect do |choice| %>
12 <% Redmine::Scm::Base.all.collect do |choice| %>
13 <% scm_class = "Repository::#{choice}".constantize %>
13 <% scm_class = "Repository::#{choice}".constantize %>
14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
15 <% setting = :enabled_scm %>
15 <% setting = :enabled_scm %>
16 <% enabled = Setting.send(setting).include?(value) %>
16 <% enabled = Setting.send(setting).include?(value) %>
17 <tr>
17 <tr>
18 <td class="scm_name">
18 <td class="scm_name">
19 <%=
19 <%=
20 check_box_tag(
20 check_box_tag(
21 "settings[#{setting}][]",
21 "settings[#{setting}][]",
22 value,
22 value,
23 enabled)
23 enabled)
24 %>
24 %>
25 <%= text.to_s %>
25 <%= text.to_s %>
26 </td>
26 </td>
27 <td>
27 <td>
28 <% if enabled %>
28 <% if enabled %>
29 <%=
29 <%=
30 image_tag(
30 image_tag(
31 (scm_class.scm_available ? 'true.png' : 'exclamation.png'),
31 (scm_class.scm_available ? 'true.png' : 'exclamation.png'),
32 :style => "vertical-align:bottom;"
32 :style => "vertical-align:bottom;"
33 )
33 )
34 %>
34 %>
35 <%= scm_class.scm_command %>
35 <%= scm_class.scm_command %>
36 <% end %>
36 <% end %>
37 </td>
37 </td>
38 <td>
38 <td>
39 <%= scm_class.scm_version_string if enabled %>
39 <%= scm_class.scm_version_string if enabled %>
40 </td>
40 </td>
41 </tr>
41 </tr>
42 <% end %>
42 <% end %>
43 </table>
43 </table>
44 <p><em class="info"><%= l(:text_scm_config) %></em></p>
44 <p><em class="info"><%= l(:text_scm_config) %></em></p>
45 </fieldset>
45 </fieldset>
46
46
47 <div class="box tabular settings">
47 <div class="box tabular settings">
48 <p><%= setting_check_box :autofetch_changesets %></p>
48 <p><%= setting_check_box :autofetch_changesets %></p>
49
49
50 <p><%= setting_check_box :sys_api_enabled,
50 <p><%= setting_check_box :sys_api_enabled,
51 :onclick =>
51 :onclick =>
52 "if (this.checked) { Form.Element.enable('settings_sys_api_key'); } else { Form.Element.disable('settings_sys_api_key'); }" %></p>
52 "if (this.checked) { Form.Element.enable('settings_sys_api_key'); } else { Form.Element.disable('settings_sys_api_key'); }" %></p>
53
53
54 <p><%= setting_text_field :sys_api_key,
54 <p><%= setting_text_field :sys_api_key,
55 :size => 30,
55 :size => 30,
56 :id => 'settings_sys_api_key',
56 :id => 'settings_sys_api_key',
57 :disabled => !Setting.sys_api_enabled?,
57 :disabled => !Setting.sys_api_enabled?,
58 :label => :setting_mail_handler_api_key %>
58 :label => :setting_mail_handler_api_key %>
59 <%= link_to_function l(:label_generate_key),
59 <%= link_to_function l(:label_generate_key),
60 "if ($('settings_sys_api_key').disabled == false) { $('settings_sys_api_key').value = randomKey(20) }" %>
60 "if ($('settings_sys_api_key').disabled == false) { $('settings_sys_api_key').value = randomKey(20) }" %>
61 </p>
61 </p>
62
62
63 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
63 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
64 </div>
64 </div>
65
65
66 <fieldset class="box tabular settings">
66 <fieldset class="box tabular settings">
67 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
67 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
68 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
68 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
69 <em class="info"><%= l(:text_comma_separated) %></em></p>
69 <em class="info"><%= l(:text_comma_separated) %></em></p>
70
70
71 <p><%= setting_text_field :commit_fix_keywords, :size => 30 %>
71 <p><%= setting_text_field :commit_fix_keywords, :size => 30 %>
72 &nbsp;<%= l(:label_applied_status) %>: <%= setting_select :commit_fix_status_id,
72 &nbsp;<%= l(:label_applied_status) %>: <%= setting_select :commit_fix_status_id,
73 [["", 0]] +
73 [["", 0]] +
74 IssueStatus.find(:all).collect{
74 IssueStatus.find(:all).collect{
75 |status| [status.name, status.id.to_s]
75 |status| [status.name, status.id.to_s]
76 },
76 },
77 :label => false %>
77 :label => false %>
78 &nbsp;<%= l(:field_done_ratio) %>: <%= setting_select :commit_fix_done_ratio,
78 &nbsp;<%= l(:field_done_ratio) %>: <%= setting_select :commit_fix_done_ratio,
79 (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
79 (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
80 :blank => :label_no_change_option,
80 :blank => :label_no_change_option,
81 :label => false %>
81 :label => false %>
82 <em class="info"><%= l(:text_comma_separated) %></em></p>
82 <em class="info"><%= l(:text_comma_separated) %></em></p>
83
83
84 <p><%= setting_check_box :commit_cross_project_ref %></p>
84 <p><%= setting_check_box :commit_cross_project_ref %></p>
85
85
86 <p><%= setting_check_box :commit_logtime_enabled,
86 <p><%= setting_check_box :commit_logtime_enabled,
87 :onclick =>
87 :onclick =>
88 "if (this.checked) { Form.Element.enable('settings_commit_logtime_activity_id'); } else { Form.Element.disable('settings_commit_logtime_activity_id'); }"%></p>
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 <p><%= setting_select :commit_logtime_activity_id,
90 <p><%= setting_select :commit_logtime_activity_id,
91 [[l(:label_default), 0]] +
91 [[l(:label_default), 0]] +
92 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
92 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
93 :disabled => !Setting.commit_logtime_enabled?%></p>
93 :disabled => !Setting.commit_logtime_enabled?%></p>
94 </fieldset>
94 </fieldset>
95
95
96 <%= submit_tag l(:button_save) %>
96 <%= submit_tag l(:button_save) %>
97 <% end %>
97 <% end %>
@@ -1,10 +1,10
1 <h2><%= l(:label_settings) %>: <%=h @plugin.name %></h2>
1 <h2><%= l(:label_settings) %>: <%=h @plugin.name %></h2>
2
2
3 <div id="settings">
3 <div id="settings">
4 <% form_tag({:action => 'plugin'}) do %>
4 <%= form_tag({:action => 'plugin'}) do %>
5 <div class="box tabular">
5 <div class="box tabular">
6 <%= render :partial => @partial, :locals => {:settings => @settings}%>
6 <%= render :partial => @partial, :locals => {:settings => @settings}%>
7 </div>
7 </div>
8 <%= submit_tag l(:button_apply) %>
8 <%= submit_tag l(:button_apply) %>
9 <% end %>
9 <% end %>
10 </div>
10 </div>
@@ -1,38 +1,38
1 <fieldset id="date-range" class="collapsible">
1 <fieldset id="date-range" class="collapsible">
2 <legend onclick="toggleFieldset(this);"><%= l(:label_date_range) %></legend>
2 <legend onclick="toggleFieldset(this);"><%= l(:label_date_range) %></legend>
3 <div>
3 <div>
4 <p>
4 <p>
5 <%= label_tag "period_type_list", l(:description_date_range_list), :class => "hidden-for-sighted" %>
5 <%= label_tag "period_type_list", l(:description_date_range_list), :class => "hidden-for-sighted" %>
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"%>
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 <%= select_tag 'period', options_for_period_select(params[:period]),
7 <%= select_tag 'period', options_for_period_select(params[:period]),
8 :onchange => 'this.form.submit();',
8 :onchange => 'this.form.submit();',
9 :onfocus => '$("period_type_1").checked = true;',
9 :onfocus => '$("period_type_1").checked = true;',
10 :disabled => @free_period %>
10 :disabled => @free_period %>
11 </p>
11 </p>
12 <p>
12 <p>
13 <%= label_tag "period_type_interval", l(:description_date_range_interval), :class => "hidden-for-sighted" %>
13 <%= label_tag "period_type_interval", l(:description_date_range_interval), :class => "hidden-for-sighted" %>
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" %>
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 <span onclick="$('period_type_interval').checked = true;Form.Element.enable('from');Form.Element.enable('to');Form.Element.disable('period');">
15 <span onclick="$('period_type_interval').checked = true;Form.Element.enable('from');Form.Element.enable('to');Form.Element.disable('period');">
16 <%= l(:label_date_from_to,
16 <%= l(:label_date_from_to,
17 :start => ((label_tag "from", l(:description_date_from), :class => "hidden-for-sighted") +
17 :start => ((label_tag "from", l(:description_date_from), :class => "hidden-for-sighted") +
18 text_field_tag('from', @from, :size => 10, :disabled => !@free_period) + calendar_for('from')),
18 text_field_tag('from', @from, :size => 10, :disabled => !@free_period) + calendar_for('from')),
19 :end => ((label_tag "to", l(:description_date_to), :class => "hidden-for-sighted") +
19 :end => ((label_tag "to", l(:description_date_to), :class => "hidden-for-sighted") +
20 text_field_tag('to', @to, :size => 10, :disabled => !@free_period) + calendar_for('to'))).html_safe %>
20 text_field_tag('to', @to, :size => 10, :disabled => !@free_period) + calendar_for('to'))).html_safe %>
21 </span>
21 </span>
22 </p>
22 </p>
23 </div>
23 </div>
24 </fieldset>
24 </fieldset>
25 <p class="buttons">
25 <p class="buttons">
26 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
26 <%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
27 <%= link_to l(:button_clear), {:controller => controller_name, :action => action_name, :project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
27 <%= link_to l(:button_clear), {:controller => controller_name, :action => action_name, :project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
28 </p>
28 </p>
29
29
30 <div class="tabs">
30 <div class="tabs">
31 <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
31 <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
32 <ul>
32 <ul>
33 <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }),
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 <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
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 </ul>
37 </ul>
38 </div>
38 </div>
@@ -1,55 +1,55
1 <% form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list time-entries">
4 <table class="list time-entries">
5 <thead>
5 <thead>
6 <tr>
6 <tr>
7 <th class="checkbox hide-when-print">
7 <th class="checkbox hide-when-print">
8 <%= link_to image_tag('toggle_check.png'),
8 <%= link_to image_tag('toggle_check.png'),
9 {},
9 {},
10 :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
10 :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
11 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
11 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
12 </th>
12 </th>
13 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
13 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
14 <%= sort_header_tag('user', :caption => l(:label_member)) %>
14 <%= sort_header_tag('user', :caption => l(:label_member)) %>
15 <%= sort_header_tag('activity', :caption => l(:label_activity)) %>
15 <%= sort_header_tag('activity', :caption => l(:label_activity)) %>
16 <%= sort_header_tag('project', :caption => l(:label_project)) %>
16 <%= sort_header_tag('project', :caption => l(:label_project)) %>
17 <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
17 <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
18 <th><%= l(:field_comments) %></th>
18 <th><%= l(:field_comments) %></th>
19 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
19 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
20 <th></th>
20 <th></th>
21 </tr>
21 </tr>
22 </thead>
22 </thead>
23 <tbody>
23 <tbody>
24 <% entries.each do |entry| -%>
24 <% entries.each do |entry| -%>
25 <tr class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
25 <tr class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
26 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
26 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
27 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
27 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
28 <td class="user"><%= link_to_user(entry.user) %></td>
28 <td class="user"><%= link_to_user(entry.user) %></td>
29 <td class="activity"><%=h entry.activity %></td>
29 <td class="activity"><%=h entry.activity %></td>
30 <td class="project"><%= link_to_project(entry.project) %></td>
30 <td class="project"><%= link_to_project(entry.project) %></td>
31 <td class="subject">
31 <td class="subject">
32 <% if entry.issue -%>
32 <% if entry.issue -%>
33 <%= entry.issue.visible? ? link_to_issue(entry.issue, :truncate => 50) : "##{entry.issue.id}" -%>
33 <%= entry.issue.visible? ? link_to_issue(entry.issue, :truncate => 50) : "##{entry.issue.id}" -%>
34 <% end -%>
34 <% end -%>
35 </td>
35 </td>
36 <td class="comments"><%=h entry.comments %></td>
36 <td class="comments"><%=h entry.comments %></td>
37 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
37 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
38 <td align="center">
38 <td align="center">
39 <% if entry.editable_by?(User.current) -%>
39 <% if entry.editable_by?(User.current) -%>
40 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
40 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
41 :title => l(:button_edit) %>
41 :title => l(:button_edit) %>
42 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
42 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
43 :confirm => l(:text_are_you_sure),
43 :confirm => l(:text_are_you_sure),
44 :method => :delete,
44 :method => :delete,
45 :title => l(:button_delete) %>
45 :title => l(:button_delete) %>
46 <% end -%>
46 <% end -%>
47 </td>
47 </td>
48 </tr>
48 </tr>
49 <% end -%>
49 <% end -%>
50 </tbody>
50 </tbody>
51 </table>
51 </table>
52 </div>
52 </div>
53 <% end -%>
53 <% end -%>
54
54
55 <%= context_menu time_entries_context_menu_path %>
55 <%= context_menu time_entries_context_menu_path %>
@@ -1,50 +1,50
1 <h2><%= l(:label_bulk_edit_selected_time_entries) %></h2>
1 <h2><%= l(:label_bulk_edit_selected_time_entries) %></h2>
2
2
3 <ul>
3 <ul>
4 <%= @time_entries.collect {|i| content_tag('li',
4 <%= @time_entries.collect {|i| content_tag('li',
5 link_to(h("#{i.spent_on.strftime("%Y-%m-%d")} - #{i.project}: #{l(:label_f_hour_plural, :value => i.hours)}"),
5 link_to(h("#{i.spent_on.strftime("%Y-%m-%d")} - #{i.project}: #{l(:label_f_hour_plural, :value => i.hours)}"),
6 { :action => 'edit', :id => i })
6 { :action => 'edit', :id => i })
7 )}.join("\n").html_safe %>
7 )}.join("\n").html_safe %>
8 </ul>
8 </ul>
9
9
10 <% form_tag(:action => 'bulk_update') do %>
10 <%= form_tag(:action => 'bulk_update') do %>
11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
12 <div class="box tabular">
12 <div class="box tabular">
13 <div>
13 <div>
14 <p>
14 <p>
15 <label><%= l(:field_issue) %></label>
15 <label><%= l(:field_issue) %></label>
16 <%= text_field :time_entry, :issue_id, :size => 6 %>
16 <%= text_field :time_entry, :issue_id, :size => 6 %>
17 </p>
17 </p>
18
18
19 <p>
19 <p>
20 <label><%= l(:field_spent_on) %></label>
20 <label><%= l(:field_spent_on) %></label>
21 <%= text_field :time_entry, :spent_on, :size => 10 %><%= calendar_for('time_entry_spent_on') %>
21 <%= text_field :time_entry, :spent_on, :size => 10 %><%= calendar_for('time_entry_spent_on') %>
22 </p>
22 </p>
23
23
24 <p>
24 <p>
25 <label><%= l(:field_hours) %></label>
25 <label><%= l(:field_hours) %></label>
26 <%= text_field :time_entry, :hours, :size => 6 %>
26 <%= text_field :time_entry, :hours, :size => 6 %>
27 </p>
27 </p>
28
28
29 <% if @available_activities.any? %>
29 <% if @available_activities.any? %>
30 <p>
30 <p>
31 <label><%= l(:field_activity) %></label>
31 <label><%= l(:field_activity) %></label>
32 <%= select_tag('time_entry[activity_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_activities, :id, :name)) %>
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 </p>
33 </p>
34 <% end %>
34 <% end %>
35
35
36 <p>
36 <p>
37 <label><%= l(:field_comments) %></label>
37 <label><%= l(:field_comments) %></label>
38 <%= text_field(:time_entry, :comments, :size => 100) %>
38 <%= text_field(:time_entry, :comments, :size => 100) %>
39 </p>
39 </p>
40
40
41 <% @custom_fields.each do |custom_field| %>
41 <% @custom_fields.each do |custom_field| %>
42 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('time_entry', custom_field, @projects) %></p>
42 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('time_entry', custom_field, @projects) %></p>
43 <% end %>
43 <% end %>
44
44
45 <%= call_hook(:view_time_entries_bulk_edit_details_bottom, { :time_entries => @time_entries }) %>
45 <%= call_hook(:view_time_entries_bulk_edit_details_bottom, { :time_entries => @time_entries }) %>
46 </div>
46 </div>
47 </div>
47 </div>
48
48
49 <p><%= submit_tag l(:button_submit) %></p>
49 <p><%= submit_tag l(:button_submit) %></p>
50 <% end %>
50 <% end %>
@@ -1,6 +1,6
1 <h2><%= l(:label_spent_time) %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,33 +1,33
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:button_log_time),
2 <%= link_to l(:button_log_time),
3 {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue},
3 {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue},
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
5 </div>
5 </div>
6
6
7 <%= render_timelog_breadcrumb %>
7 <%= render_timelog_breadcrumb %>
8
8
9 <h2><%= l(:label_spent_time) %></h2>
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 <%= render :partial => 'date_range' %>
12 <%= render :partial => 'date_range' %>
13 <% end %>
13 <% end %>
14
14
15 <div class="total-hours">
15 <div class="total-hours">
16 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
16 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
17 </div>
17 </div>
18
18
19 <% unless @entries.empty? %>
19 <% unless @entries.empty? %>
20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
21 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
21 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
22
22
23 <% other_formats_links do |f| %>
23 <% other_formats_links do |f| %>
24 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
24 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
25 <%= f.link_to 'CSV', :url => params %>
25 <%= f.link_to 'CSV', :url => params %>
26 <% end %>
26 <% end %>
27 <% end %>
27 <% end %>
28
28
29 <% html_title l(:label_spent_time), l(:label_details) %>
29 <% html_title l(:label_spent_time), l(:label_details) %>
30
30
31 <% content_for :header_tags do %>
31 <% content_for :header_tags do %>
32 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
32 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
33 <% end %>
33 <% end %>
@@ -1,7 +1,7
1 <h2><%= l(:label_spent_time) %></h2>
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 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
7 <% end %>
7 <% end %>
@@ -1,72 +1,74
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
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 </div>
3 </div>
4
4
5 <%= render_timelog_breadcrumb %>
5 <%= render_timelog_breadcrumb %>
6
6
7 <h2><%= l(:label_spent_time) %></h2>
7 <h2><%= l(:label_spent_time) %></h2>
8
8
9 <% form_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 <% @report.criteria.each do |criterion| %>
12 <% @report.criteria.each do |criterion| %>
11 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
13 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
12 <% end %>
14 <% end %>
13 <%= render :partial => 'timelog/date_range' %>
15 <%= render :partial => 'timelog/date_range' %>
14
16
15 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
17 <p><label for='columns'><%= l(:label_details) %></label>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
16 [l(:label_month), 'month'],
18 [l(:label_month), 'month'],
17 [l(:label_week), 'week'],
19 [l(:label_week), 'week'],
18 [l(:label_day_plural).titleize, 'day']], @report.columns),
20 [l(:label_day_plural).titleize, 'day']], @report.columns),
19 :onchange => "this.form.submit();" %>
21 :onchange => "this.form.submit();" %>
20
22
21 <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]}),
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 :onchange => "this.form.submit();",
24 :onchange => "this.form.submit();",
23 :style => 'width: 200px',
25 :style => 'width: 200px',
24 :id => nil,
26 :id => nil,
25 :disabled => (@report.criteria.length >= 3), :id => "criterias") %>
27 :disabled => (@report.criteria.length >= 3), :id => "criterias") %>
26 <%= 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>
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 <% end %>
29 <% end %>
28
30
29 <% unless @report.criteria.empty? %>
31 <% unless @report.criteria.empty? %>
30 <div class="total-hours">
32 <div class="total-hours">
31 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
33 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@report.total_hours)) %></p>
32 </div>
34 </div>
33
35
34 <% unless @report.hours.empty? %>
36 <% unless @report.hours.empty? %>
35 <div class="autoscroll">
37 <div class="autoscroll">
36 <table class="list" id="time-report">
38 <table class="list" id="time-report">
37 <thead>
39 <thead>
38 <tr>
40 <tr>
39 <% @report.criteria.each do |criteria| %>
41 <% @report.criteria.each do |criteria| %>
40 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
42 <th><%= l_or_humanize(@report.available_criteria[criteria][:label]) %></th>
41 <% end %>
43 <% end %>
42 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
44 <% columns_width = (40 / (@report.periods.length+1)).to_i %>
43 <% @report.periods.each do |period| %>
45 <% @report.periods.each do |period| %>
44 <th class="period" width="<%= columns_width %>%"><%= period %></th>
46 <th class="period" width="<%= columns_width %>%"><%= period %></th>
45 <% end %>
47 <% end %>
46 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
48 <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
47 </tr>
49 </tr>
48 </thead>
50 </thead>
49 <tbody>
51 <tbody>
50 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
52 <%= render :partial => 'report_criteria', :locals => {:criterias => @report.criteria, :hours => @report.hours, :level => 0} %>
51 <tr class="total">
53 <tr class="total">
52 <td><%= l(:label_total) %></td>
54 <td><%= l(:label_total) %></td>
53 <%= ('<td></td>' * (@report.criteria.size - 1)).html_safe %>
55 <%= ('<td></td>' * (@report.criteria.size - 1)).html_safe %>
54 <% total = 0 -%>
56 <% total = 0 -%>
55 <% @report.periods.each do |period| -%>
57 <% @report.periods.each do |period| -%>
56 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
58 <% sum = sum_hours(select_hours(@report.hours, @report.columns, period.to_s)); total += sum -%>
57 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
59 <td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
58 <% end -%>
60 <% end -%>
59 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
61 <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
60 </tr>
62 </tr>
61 </tbody>
63 </tbody>
62 </table>
64 </table>
63 </div>
65 </div>
64
66
65 <% other_formats_links do |f| %>
67 <% other_formats_links do |f| %>
66 <%= f.link_to 'CSV', :url => params %>
68 <%= f.link_to 'CSV', :url => params %>
67 <% end %>
69 <% end %>
68 <% end %>
70 <% end %>
69 <% end %>
71 <% end %>
70
72
71 <% html_title l(:label_spent_time), l(:label_report) %>
73 <% html_title l(:label_spent_time), l(:label_report) %>
72
74
@@ -1,5 +1,5
1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=h @tracker %></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <% end %>
5 <% end %>
@@ -1,5 +1,5
1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=l(:label_tracker_new)%></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <% end %>
5 <% end %>
@@ -1,7 +1,7
1 <% labelled_form_for @user do |f| %>
1 <%= labelled_form_for @user do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <% if @user.active? && email_delivery_enabled? -%>
3 <% if @user.active? && email_delivery_enabled? -%>
4 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
4 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
5 <% end -%>
5 <% end -%>
6 <p><%= submit_tag l(:button_save) %></p>
6 <p><%= submit_tag l(:button_save) %></p>
7 <% end %>
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 <div class="box">
2 <div class="box">
3 <% Group.all.sort.each do |group| %>
3 <% Group.all.sort.each do |group| %>
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
5 <% end %>
5 <% end %>
6 <%= hidden_field_tag 'user[group_ids][]', '' %>
6 <%= hidden_field_tag 'user[group_ids][]', '' %>
7 </div>
7 </div>
8 <%= submit_tag l(:button_save) %>
8 <%= submit_tag l(:button_save) %>
9 <% end %>
9 <% end %>
@@ -1,70 +1,72
1 <% roles = Role.find_all_givable %>
1 <% roles = Role.find_all_givable %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
3
3
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
5 <% if @user.memberships.any? %>
5 <% if @user.memberships.any? %>
6 <table class="list memberships">
6 <table class="list memberships">
7 <thead><tr>
7 <thead><tr>
8 <th><%= l(:label_project) %></th>
8 <th><%= l(:label_project) %></th>
9 <th><%= l(:label_role_plural) %></th>
9 <th><%= l(:label_role_plural) %></th>
10 <th style="width:15%"></th>
10 <th style="width:15%"></th>
11 <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
11 <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
12 </tr></thead>
12 </tr></thead>
13 <tbody>
13 <tbody>
14 <% @user.memberships.each do |membership| %>
14 <% @user.memberships.each do |membership| %>
15 <% next if membership.new_record? %>
15 <% next if membership.new_record? %>
16 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
17 <td class="project">
17 <td class="project">
18 <%= link_to_project membership.project %>
18 <%= link_to_project membership.project %>
19 </td>
19 </td>
20 <td class="roles">
20 <td class="roles">
21 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
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,
22 <%= form_for(:membership, :remote => true,
23 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
23 :url => user_membership_path(@user, membership), :method => :put,
24 :html => {:id => "member-#{membership.id}-roles-form",
25 :style => 'display:none;'}) do %>
24 <p><% roles.each do |role| %>
26 <p><% roles.each do |role| %>
25 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
27 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
26 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
28 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
27 <% end %></p>
29 <% end %></p>
28 <%= hidden_field_tag 'membership[role_ids][]', '' %>
30 <%= hidden_field_tag 'membership[role_ids][]', '' %>
29 <p><%= submit_tag l(:button_change) %>
31 <p><%= submit_tag l(:button_change) %>
30 <%= link_to_function l(:button_cancel),
32 <%= link_to_function l(:button_cancel),
31 "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;"
33 "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;"
32 %></p>
34 %></p>
33 <% end %>
35 <% end %>
34 </td>
36 </td>
35 <td class="buttons">
37 <td class="buttons">
36 <%= link_to_function l(:button_edit),
38 <%= link_to_function l(:button_edit),
37 "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;",
39 "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;",
38 :class => 'icon icon-edit'
40 :class => 'icon icon-edit'
39 %>
41 %>
40 <%= link_to_remote(
42 <%= link_to_remote(
41 l(:button_delete),
43 l(:button_delete),
42 { :url => user_membership_path(@user, membership),
44 { :url => user_membership_path(@user, membership),
43 :method => :delete },
45 :method => :delete },
44 :class => 'icon icon-del'
46 :class => 'icon icon-del'
45 ) if membership.deletable? %>
47 ) if membership.deletable? %>
46 </td>
48 </td>
47 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
49 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
48 </tr>
50 </tr>
49 <% end; reset_cycle %>
51 <% end; reset_cycle %>
50 </tbody>
52 </tbody>
51 </table>
53 </table>
52 <% else %>
54 <% else %>
53 <p class="nodata"><%= l(:label_no_data) %></p>
55 <p class="nodata"><%= l(:label_no_data) %></p>
54 <% end %>
56 <% end %>
55 </div>
57 </div>
56
58
57 <div class="splitcontentright">
59 <div class="splitcontentright">
58 <% if projects.any? %>
60 <% if projects.any? %>
59 <fieldset><legend><%=l(:label_project_new)%></legend>
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 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
63 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
62 <p><%= l(:label_role_plural) %>:
64 <p><%= l(:label_role_plural) %>:
63 <% roles.each do |role| %>
65 <% roles.each do |role| %>
64 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
66 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
65 <% end %></p>
67 <% end %></p>
66 <p><%= submit_tag l(:button_add) %></p>
68 <p><%= submit_tag l(:button_add) %></p>
67 <% end %>
69 <% end %>
68 </fieldset>
70 </fieldset>
69 <% end %>
71 <% end %>
70 </div>
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 <p><%= pref_fields.check_box :hide_mail %></p>
2 <p><%= pref_fields.check_box :hide_mail %></p>
3 <p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
3 <p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
4 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
4 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
5 <p><%= pref_fields.check_box :warn_on_leaving_unsaved %></p>
5 <p><%= pref_fields.check_box :warn_on_leaving_unsaved %></p>
6 <% end %>
6 <% end %>
7
7
@@ -1,58 +1,58
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_user_plural)%></h2>
5 <h2><%=l(:label_user_plural)%></h2>
6
6
7 <% form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label for='status'><%= l(:field_status) %>:</label>
9 <label for='status'><%= l(:field_status) %>:</label>
10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11
11
12 <% if @groups.present? %>
12 <% if @groups.present? %>
13 <label for='group_id'><%= l(:label_group) %>:</label>
13 <label for='group_id'><%= l(:label_group) %>:</label>
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;" %>
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 <% end %>
15 <% end %>
16
16
17 <label for='name'><%= l(:label_user) %>:</label>
17 <label for='name'><%= l(:label_user) %>:</label>
18 <%= text_field_tag 'name', params[:name], :size => 30 %>
18 <%= text_field_tag 'name', params[:name], :size => 30 %>
19 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
19 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
20 <%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
20 <%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
21 </fieldset>
21 </fieldset>
22 <% end %>
22 <% end %>
23 &nbsp;
23 &nbsp;
24
24
25 <div class="autoscroll">
25 <div class="autoscroll">
26 <table class="list">
26 <table class="list">
27 <thead><tr>
27 <thead><tr>
28 <%= sort_header_tag('login', :caption => l(:field_login)) %>
28 <%= sort_header_tag('login', :caption => l(:field_login)) %>
29 <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
29 <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
30 <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
30 <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
31 <%= sort_header_tag('mail', :caption => l(:field_mail)) %>
31 <%= sort_header_tag('mail', :caption => l(:field_mail)) %>
32 <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
32 <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
33 <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
33 <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
34 <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
34 <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
35 <th></th>
35 <th></th>
36 </tr></thead>
36 </tr></thead>
37 <tbody>
37 <tbody>
38 <% for user in @users -%>
38 <% for user in @users -%>
39 <tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">
39 <tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">
40 <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
40 <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
41 <td class="firstname"><%= h(user.firstname) %></td>
41 <td class="firstname"><%= h(user.firstname) %></td>
42 <td class="lastname"><%= h(user.lastname) %></td>
42 <td class="lastname"><%= h(user.lastname) %></td>
43 <td class="email"><%= mail_to(h(user.mail)) %></td>
43 <td class="email"><%= mail_to(h(user.mail)) %></td>
44 <td align="center"><%= checked_image user.admin? %></td>
44 <td align="center"><%= checked_image user.admin? %></td>
45 <td class="created_on" align="center"><%= format_time(user.created_on) %></td>
45 <td class="created_on" align="center"><%= format_time(user.created_on) %></td>
46 <td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
46 <td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
47 <td class="buttons">
47 <td class="buttons">
48 <%= change_status_link(user) %>
48 <%= change_status_link(user) %>
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 %>
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 </td>
50 </td>
51 </tr>
51 </tr>
52 <% end -%>
52 <% end -%>
53 </tbody>
53 </tbody>
54 </table>
54 </table>
55 </div>
55 </div>
56 <p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p>
56 <p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p>
57
57
58 <% html_title(l(:label_user_plural)) -%>
58 <% html_title(l(:label_user_plural)) -%>
@@ -1,12 +1,12
1 <h2><%= link_to l(:label_user_plural), users_path %> &#187; <%=l(:label_user_new)%></h2>
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 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <% if email_delivery_enabled? %>
5 <% if email_delivery_enabled? %>
6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
7 <% end %>
7 <% end %>
8 <p>
8 <p>
9 <%= submit_tag l(:button_create) %>
9 <%= submit_tag l(:button_create) %>
10 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
10 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
11 </p>
11 </p>
12 <% end %>
12 <% end %>
@@ -1,35 +1,35
1 <% form_tag({}, :id => "status_by_form") do -%>
1 <%= form_tag({}, :id => "status_by_form") do -%>
2 <fieldset>
2 <fieldset>
3 <legend>
3 <legend>
4 <%= l(:label_issues_by,
4 <%= l(:label_issues_by,
5 select_tag('status_by',
5 select_tag('status_by',
6 status_by_options_for_select(criteria),
6 status_by_options_for_select(criteria),
7 :id => 'status_by_select',
7 :id => 'status_by_select',
8 :onchange => remote_function(:url => status_by_version_path(version),
8 :onchange => remote_function(:url => status_by_version_path(version),
9 :with => "Form.serialize('status_by_form')"))).html_safe %>
9 :with => "Form.serialize('status_by_form')"))).html_safe %>
10 </legend>
10 </legend>
11 <% if counts.empty? %>
11 <% if counts.empty? %>
12 <p><em><%= l(:label_no_data) %></em></p>
12 <p><em><%= l(:label_no_data) %></em></p>
13 <% else %>
13 <% else %>
14 <table>
14 <table>
15 <% counts.each do |count| %>
15 <% counts.each do |count| %>
16 <tr>
16 <tr>
17 <td width="130px" align="right" >
17 <td width="130px" align="right" >
18 <%= link_to h(count[:group]), {:controller => 'issues',
18 <%= link_to h(count[:group]), {:controller => 'issues',
19 :action => 'index',
19 :action => 'index',
20 :project_id => version.project,
20 :project_id => version.project,
21 :set_filter => 1,
21 :set_filter => 1,
22 :status_id => '*',
22 :status_id => '*',
23 :fixed_version_id => version}.merge("#{criteria}_id".to_sym => count[:group]) %>
23 :fixed_version_id => version}.merge("#{criteria}_id".to_sym => count[:group]) %>
24 </td>
24 </td>
25 <td width="240px">
25 <td width="240px">
26 <%= progress_bar((count[:closed].to_f / count[:total])*100,
26 <%= progress_bar((count[:closed].to_f / count[:total])*100,
27 :legend => "#{count[:closed]}/#{count[:total]}",
27 :legend => "#{count[:closed]}/#{count[:total]}",
28 :width => "#{(count[:total].to_f / max * 200).floor}px;") %>
28 :width => "#{(count[:total].to_f / max * 200).floor}px;") %>
29 </td>
29 </td>
30 </tr>
30 </tr>
31 <% end %>
31 <% end %>
32 </table>
32 </table>
33 <% end %>
33 <% end %>
34 </fieldset>
34 </fieldset>
35 <% end %>
35 <% end %>
@@ -1,9 +1,9
1 <h3 class="title"><%=l(:label_version_new)%></h3>
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 <%= render :partial => 'versions/form', :locals => { :f => f } %>
4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
5 <p class="buttons">
5 <p class="buttons">
6 <%= submit_tag l(:button_create), :name => nil %>
6 <%= submit_tag l(:button_create), :name => nil %>
7 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
7 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
8 </p>
8 </p>
9 <% end %>
9 <% end %>
@@ -1,7 +1,7
1 <h2><%=l(:label_version)%></h2>
1 <h2><%=l(:label_version)%></h2>
2
2
3 <% labelled_form_for @version do |f| %>
3 <%= labelled_form_for @version do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
7
7
@@ -1,68 +1,68
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_version_new), new_project_version_path(@project), :class => 'icon icon-add' if User.current.allowed_to?(:manage_versions, @project) %>
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 </div>
3 </div>
4
4
5 <h2><%=l(:label_roadmap)%></h2>
5 <h2><%=l(:label_roadmap)%></h2>
6
6
7 <% if @versions.empty? %>
7 <% if @versions.empty? %>
8 <p class="nodata"><%= l(:label_no_data) %></p>
8 <p class="nodata"><%= l(:label_no_data) %></p>
9 <% else %>
9 <% else %>
10 <div id="roadmap">
10 <div id="roadmap">
11 <% @versions.each do |version| %>
11 <% @versions.each do |version| %>
12 <h3 class="version"><%= tag 'a', :name => anchor(version.name) %><%= link_to_version version %></h3>
12 <h3 class="version"><%= tag 'a', :name => anchor(version.name) %><%= link_to_version version %></h3>
13 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
13 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
14 <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
14 <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
15
15
16 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
16 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
17 <% form_tag({}) do -%>
17 <%= form_tag({}) do -%>
18 <table class="list related-issues">
18 <table class="list related-issues">
19 <caption><%= l(:label_related_issues) %></caption>
19 <caption><%= l(:label_related_issues) %></caption>
20 <% issues.each do |issue| -%>
20 <% issues.each do |issue| -%>
21 <tr class="hascontextmenu">
21 <tr class="hascontextmenu">
22 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
22 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
23 <td><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
23 <td><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
24 </tr>
24 </tr>
25 <% end -%>
25 <% end -%>
26 </table>
26 </table>
27 <% end %>
27 <% end %>
28 <% end %>
28 <% end %>
29 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
29 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
30 <% end %>
30 <% end %>
31 </div>
31 </div>
32 <% end %>
32 <% end %>
33
33
34 <% content_for :sidebar do %>
34 <% content_for :sidebar do %>
35 <% form_tag({}, :method => :get) do %>
35 <%= form_tag({}, :method => :get) do %>
36 <h3><%= l(:label_roadmap) %></h3>
36 <h3><%= l(:label_roadmap) %></h3>
37 <% @trackers.each do |tracker| %>
37 <% @trackers.each do |tracker| %>
38 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
38 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
39 <%=h tracker.name %></label><br />
39 <%=h tracker.name %></label><br />
40 <% end %>
40 <% end %>
41 <br />
41 <br />
42 <label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
42 <label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
43 <% if @project.descendants.active.any? %>
43 <% if @project.descendants.active.any? %>
44 <%= hidden_field_tag 'with_subprojects', 0 %>
44 <%= hidden_field_tag 'with_subprojects', 0 %>
45 <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
45 <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
46 <% end %>
46 <% end %>
47 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
47 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
48 <% end %>
48 <% end %>
49
49
50 <h3><%= l(:label_version_plural) %></h3>
50 <h3><%= l(:label_version_plural) %></h3>
51 <% @versions.each do |version| %>
51 <% @versions.each do |version| %>
52 <%= link_to format_version_name(version), "##{anchor(version.name)}" %><br />
52 <%= link_to format_version_name(version), "##{anchor(version.name)}" %><br />
53 <% end %>
53 <% end %>
54 <% if @completed_versions.present? %>
54 <% if @completed_versions.present? %>
55 <p>
55 <p>
56 <%= link_to_function l(:label_completed_versions),
56 <%= link_to_function l(:label_completed_versions),
57 'Element.toggleClassName("toggle-completed-versions", "collapsed"); Element.toggle("completed-versions")',
57 'Element.toggleClassName("toggle-completed-versions", "collapsed"); Element.toggle("completed-versions")',
58 :id => 'toggle-completed-versions', :class => 'collapsible collapsed' %><br />
58 :id => 'toggle-completed-versions', :class => 'collapsible collapsed' %><br />
59 <span id="completed-versions" style="display:none;">
59 <span id="completed-versions" style="display:none;">
60 <%= @completed_versions.map {|version| link_to format_version_name(version), version_path(version)}.join("<br />\n").html_safe %>
60 <%= @completed_versions.map {|version| link_to format_version_name(version), version_path(version)}.join("<br />\n").html_safe %>
61 </span>
61 </span>
62 </p>
62 </p>
63 <% end %>
63 <% end %>
64 <% end %>
64 <% end %>
65
65
66 <% html_title(l(:label_roadmap)) %>
66 <% html_title(l(:label_roadmap)) %>
67
67
68 <%= context_menu issues_context_menu_path %>
68 <%= context_menu issues_context_menu_path %>
@@ -1,6 +1,6
1 <h2><%=l(:label_version_new)%></h2>
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 <%= render :partial => 'versions/form', :locals => { :f => f } %>
4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -1,56 +1,56
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:button_edit), edit_version_path(@version), :class => 'icon icon-edit') if User.current.allowed_to?(:manage_versions, @version.project) %>
2 <%= link_to(l(:button_edit), edit_version_path(@version), :class => 'icon icon-edit') if User.current.allowed_to?(:manage_versions, @version.project) %>
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? %>
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 <%= link_to(l(:button_delete), version_path(@version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)),
4 <%= link_to(l(:button_delete), version_path(@version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)),
5 :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del') if User.current.allowed_to?(:manage_versions, @version.project) %>
5 :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del') if User.current.allowed_to?(:manage_versions, @version.project) %>
6 <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
6 <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
7 </div>
7 </div>
8
8
9 <h2><%= h(@version.name) %></h2>
9 <h2><%= h(@version.name) %></h2>
10
10
11 <div id="roadmap">
11 <div id="roadmap">
12 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
12 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
13 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
13 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
14
14
15 <div id="version-summary">
15 <div id="version-summary">
16 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
16 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
17 <fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
17 <fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
18 <table>
18 <table>
19 <tr>
19 <tr>
20 <th><%= l(:field_estimated_hours) %></th>
20 <th><%= l(:field_estimated_hours) %></th>
21 <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
21 <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
22 </tr>
22 </tr>
23 <% if User.current.allowed_to?(:view_time_entries, @project) %>
23 <% if User.current.allowed_to?(:view_time_entries, @project) %>
24 <tr>
24 <tr>
25 <th><%= l(:label_spent_time) %></th>
25 <th><%= l(:label_spent_time) %></th>
26 <td class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td>
26 <td class="total-hours"><%= html_hours(l_hours(@version.spent_hours)) %></td>
27 </tr>
27 </tr>
28 <% end %>
28 <% end %>
29 </table>
29 </table>
30 </fieldset>
30 </fieldset>
31 <% end %>
31 <% end %>
32
32
33 <div id="status_by">
33 <div id="status_by">
34 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
34 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
35 </div>
35 </div>
36 </div>
36 </div>
37
37
38 <% if @issues.present? %>
38 <% if @issues.present? %>
39 <% form_tag({}) do -%>
39 <%= form_tag({}) do -%>
40 <table class="list related-issues">
40 <table class="list related-issues">
41 <caption><%= l(:label_related_issues) %></caption>
41 <caption><%= l(:label_related_issues) %></caption>
42 <%- @issues.each do |issue| -%>
42 <%- @issues.each do |issue| -%>
43 <tr class="hascontextmenu">
43 <tr class="hascontextmenu">
44 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
44 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
45 <td><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
45 <td><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
46 </tr>
46 </tr>
47 <% end %>
47 <% end %>
48 </table>
48 </table>
49 <% end %>
49 <% end %>
50 <%= context_menu issues_context_menu_path %>
50 <%= context_menu issues_context_menu_path %>
51 <% end %>
51 <% end %>
52 </div>
52 </div>
53
53
54 <%= call_hook :view_versions_show_bottom, :version => @version %>
54 <%= call_hook :view_versions_show_bottom, :version => @version %>
55
55
56 <% html_title @version.name %>
56 <% html_title @version.name %>
@@ -1,32 +1,32
1 <h3 class="title"><%= l(:permission_add_issue_watchers) %></h3>
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 :action => (watched ? 'create' : 'append'),
4 :action => (watched ? 'create' : 'append'),
5 :object_type => watched.class.name.underscore,
5 :object_type => watched.class.name.underscore,
6 :object_id => watched},
6 :object_id => watched},
7 :method => :post,
7 :method => :post,
8 :html => {:id => 'new-watcher-form'} do %>
8 :html => {:id => 'new-watcher-form'} do %>
9
9
10 <p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
10 <p><%= label_tag 'user_search', l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
11 <%= observe_field(:user_search,
11 <%= observe_field(:user_search,
12 :frequency => 0.5,
12 :frequency => 0.5,
13 :update => :users_for_watcher,
13 :update => :users_for_watcher,
14 :method => :get,
14 :method => :get,
15 :before => '$("user_search").addClassName("ajax-loading")',
15 :before => '$("user_search").addClassName("ajax-loading")',
16 :complete => '$("user_search").removeClassName("ajax-loading")',
16 :complete => '$("user_search").removeClassName("ajax-loading")',
17 :url => {
17 :url => {
18 :controller => 'watchers',
18 :controller => 'watchers',
19 :action => 'autocomplete_for_user',
19 :action => 'autocomplete_for_user',
20 :object_type => watched.class.name.underscore,
20 :object_type => watched.class.name.underscore,
21 :object_id => watched},
21 :object_id => watched},
22 :with => 'q') %>
22 :with => 'q') %>
23
23
24 <div id="users_for_watcher">
24 <div id="users_for_watcher">
25 <%= principals_check_box_tags 'watcher[user_ids][]', (watched ? watched.addable_watcher_users : User.active.all(:limit => 100)) %>
25 <%= principals_check_box_tags 'watcher[user_ids][]', (watched ? watched.addable_watcher_users : User.active.all(:limit => 100)) %>
26 </div>
26 </div>
27
27
28 <p class="buttons">
28 <p class="buttons">
29 <%= submit_tag l(:button_add), :name => nil, :onclick => "hideModal(this);" %>
29 <%= submit_tag l(:button_add), :name => nil, :onclick => "hideModal(this);" %>
30 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
30 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
31 </p>
31 </p>
32 <% end %>
32 <% end %>
@@ -1,9 +1,9
1 <% if @wiki && @wiki.sidebar -%>
1 <% if @wiki && @wiki.sidebar -%>
2 <%= textilizable @wiki.sidebar.content, :text %>
2 <%= textilizable @wiki.sidebar.content, :text %>
3 <% end -%>
3 <% end -%>
4
4
5 <h3><%= l(:label_wiki) %></h3>
5 <h3><%= l(:label_wiki) %></h3>
6
6
7 <%= link_to l(:field_start_page), {:action => 'show', :id => nil} %><br />
7 <%= link_to l(:field_start_page), {:action => 'show', :id => nil} %><br />
8 <%= link_to l(:label_index_by_title), {:action => 'index'} %><br />
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 <%= wiki_page_breadcrumb(@page) %>
1 <%= wiki_page_breadcrumb(@page) %>
2
2
3 <h2><%=h @page.pretty_title %></h2>
3 <h2><%=h @page.pretty_title %></h2>
4
4
5 <% form_tag({}, :method => :delete) do %>
5 <%= form_tag({}, :method => :delete) do %>
6 <div class="box">
6 <div class="box">
7 <p><strong><%= l(:text_wiki_page_destroy_question, :descendants => @descendants_count) %></strong></p>
7 <p><strong><%= l(:text_wiki_page_destroy_question, :descendants => @descendants_count) %></strong></p>
8 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_wiki_page_nullify_children) %></label><br />
8 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_wiki_page_nullify_children) %></label><br />
9 <label><%= radio_button_tag 'todo', 'destroy', false %> <%= l(:text_wiki_page_destroy_children) %></label>
9 <label><%= radio_button_tag 'todo', 'destroy', false %> <%= l(:text_wiki_page_destroy_children) %></label>
10 <% if @reassignable_to.any? %>
10 <% if @reassignable_to.any? %>
11 <br />
11 <br />
12 <label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_wiki_page_reassign_children) %></label>:
12 <label><%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_wiki_page_reassign_children) %></label>:
13 <%= label_tag "reassign_to_id", l(:description_wiki_subpages_reassign), :class => "hidden-for-sighted" %>
13 <%= label_tag "reassign_to_id", l(:description_wiki_subpages_reassign), :class => "hidden-for-sighted" %>
14 <%= select_tag 'reassign_to_id', wiki_page_options_for_select(@reassignable_to),
14 <%= select_tag 'reassign_to_id', wiki_page_options_for_select(@reassignable_to),
15 :onclick => "$('todo_reassign').checked = true;" %>
15 :onclick => "$('todo_reassign').checked = true;" %>
16 <% end %>
16 <% end %>
17 </p>
17 </p>
18 </div>
18 </div>
19
19
20 <%= submit_tag l(:button_apply) %>
20 <%= submit_tag l(:button_apply) %>
21 <%= link_to l(:button_cancel), :controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title %>
21 <%= link_to l(:button_cancel), :controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title %>
22 <% end %>
22 <% end %>
@@ -1,49 +1,49
1 <%= wiki_page_breadcrumb(@page) %>
1 <%= wiki_page_breadcrumb(@page) %>
2
2
3 <h2><%= h @page.pretty_title %></h2>
3 <h2><%= h @page.pretty_title %></h2>
4
4
5 <% form_for :content, @content,
5 <%= form_for @content, :as => :content,
6 :url => {:action => 'update', :id => @page.title},
6 :url => {:action => 'update', :id => @page.title},
7 :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %>
7 :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %>
8 <%= f.hidden_field :version %>
8 <%= f.hidden_field :version %>
9 <% if @section %>
9 <% if @section %>
10 <%= hidden_field_tag 'section', @section %>
10 <%= hidden_field_tag 'section', @section %>
11 <%= hidden_field_tag 'section_hash', @section_hash %>
11 <%= hidden_field_tag 'section_hash', @section_hash %>
12 <% end %>
12 <% end %>
13 <%= error_messages_for 'content' %>
13 <%= error_messages_for 'content' %>
14
14
15 <div class="box tabular">
15 <div class="box tabular">
16 <%= text_area_tag 'content[text]', @text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>
16 <%= text_area_tag 'content[text]', @text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>
17
17
18 <% if @page.safe_attribute_names.include?('parent_id') && @wiki.pages.any? %>
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 <p>
20 <p>
21 <label><%= l(:field_parent_title) %></label>
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 </p>
23 </p>
24 <% end %>
24 <% end %>
25 <% end %>
25 <% end %>
26
26
27 <p><label><%= l(:field_comments) %></label><%= f.text_field :comments, :size => 120 %></p>
27 <p><label><%= l(:field_comments) %></label><%= f.text_field :comments, :size => 120 %></p>
28 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
28 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
29 </div>
29 </div>
30
30
31 <p><%= submit_tag l(:button_save) %>
31 <p><%= submit_tag l(:button_save) %>
32 <%= link_to_remote l(:label_preview),
32 <%= link_to_remote l(:label_preview),
33 { :url => { :controller => 'wiki', :action => 'preview', :project_id => @project, :id => @page.title },
33 { :url => { :controller => 'wiki', :action => 'preview', :project_id => @project, :id => @page.title },
34 :method => :post,
34 :method => :post,
35 :update => 'preview',
35 :update => 'preview',
36 :with => "Form.serialize('wiki_form')",
36 :with => "Form.serialize('wiki_form')",
37 :complete => "Element.scrollTo('preview')"
37 :complete => "Element.scrollTo('preview')"
38 }, :accesskey => accesskey(:preview) %></p>
38 }, :accesskey => accesskey(:preview) %></p>
39 <%= wikitoolbar_for 'content_text' %>
39 <%= wikitoolbar_for 'content_text' %>
40 <% end %>
40 <% end %>
41
41
42 <div id="preview" class="wiki"></div>
42 <div id="preview" class="wiki"></div>
43
43
44 <% content_for :header_tags do %>
44 <% content_for :header_tags do %>
45 <%= stylesheet_link_tag 'scm' %>
45 <%= stylesheet_link_tag 'scm' %>
46 <%= robot_exclusion_tag %>
46 <%= robot_exclusion_tag %>
47 <% end %>
47 <% end %>
48
48
49 <% html_title @page.pretty_title %>
49 <% html_title @page.pretty_title %>
@@ -1,37 +1,39
1 <%= wiki_page_breadcrumb(@page) %>
1 <%= wiki_page_breadcrumb(@page) %>
2
2
3 <h2><%= h(@page.pretty_title) %></h2>
3 <h2><%= h(@page.pretty_title) %></h2>
4
4
5 <h3><%= l(:label_history) %></h3>
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 <table class="list wiki-page-versions">
10 <table class="list wiki-page-versions">
9 <thead><tr>
11 <thead><tr>
10 <th>#</th>
12 <th>#</th>
11 <th></th>
13 <th></th>
12 <th></th>
14 <th></th>
13 <th><%= l(:field_updated_on) %></th>
15 <th><%= l(:field_updated_on) %></th>
14 <th><%= l(:field_author) %></th>
16 <th><%= l(:field_author) %></th>
15 <th><%= l(:field_comments) %></th>
17 <th><%= l(:field_comments) %></th>
16 <th></th>
18 <th></th>
17 </tr></thead>
19 </tr></thead>
18 <tbody>
20 <tbody>
19 <% show_diff = @versions.size > 1 %>
21 <% show_diff = @versions.size > 1 %>
20 <% line_num = 1 %>
22 <% line_num = 1 %>
21 <% @versions.each do |ver| %>
23 <% @versions.each do |ver| %>
22 <tr class="wiki-page-version <%= cycle("odd", "even") %>">
24 <tr class="wiki-page-version <%= cycle("odd", "even") %>">
23 <td class="id"><%= link_to h(ver.version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
25 <td class="id"><%= link_to h(ver.version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
24 <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>
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 <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
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 <td class="updated_on"><%= format_time(ver.updated_on) %></td>
28 <td class="updated_on"><%= format_time(ver.updated_on) %></td>
27 <td class="author"><%= link_to_user ver.author %></td>
29 <td class="author"><%= link_to_user ver.author %></td>
28 <td class="comments"><%=h ver.comments %></td>
30 <td class="comments"><%=h ver.comments %></td>
29 <td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
31 <td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
30 </tr>
32 </tr>
31 <% line_num += 1 %>
33 <% line_num += 1 %>
32 <% end %>
34 <% end %>
33 </tbody>
35 </tbody>
34 </table>
36 </table>
35 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
37 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
36 <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
38 <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
37 <% end %>
39 <% end %>
@@ -1,21 +1,21
1 <%= wiki_page_breadcrumb(@page) %>
1 <%= wiki_page_breadcrumb(@page) %>
2
2
3 <h2><%= h @original_title %></h2>
3 <h2><%= h @original_title %></h2>
4
4
5 <%= error_messages_for 'page' %>
5 <%= error_messages_for 'page' %>
6
6
7 <% labelled_form_for :wiki_page, @page,
7 <%= labelled_form_for :wiki_page, @page,
8 :url => { :action => 'rename' },
8 :url => { :action => 'rename' },
9 :html => { :method => :post } do |f| %>
9 :html => { :method => :post } do |f| %>
10 <div class="box tabular">
10 <div class="box tabular">
11 <p><%= f.text_field :title, :required => true, :size => 100 %></p>
11 <p><%= f.text_field :title, :required => true, :size => 100 %></p>
12 <p><%= f.check_box :redirect_existing_links %></p>
12 <p><%= f.check_box :redirect_existing_links %></p>
13 <p><%= f.select :parent_id,
13 <p><%= f.select :parent_id,
14 "<option value=''></option>" +
14 content_tag('option', '', :value => '') +
15 wiki_page_options_for_select(
15 wiki_page_options_for_select(
16 @wiki.pages.all(:include => :parent) - @page.self_and_descendants,
16 @wiki.pages.all(:include => :parent) - @page.self_and_descendants,
17 @page.parent),
17 @page.parent),
18 :label => :field_parent_title %></p>
18 :label => :field_parent_title %></p>
19 </div>
19 </div>
20 <%= submit_tag l(:button_rename) %>
20 <%= submit_tag l(:button_rename) %>
21 <% end %>
21 <% end %>
@@ -1,69 +1,72
1 <div class="contextual">
1 <div class="contextual">
2 <% if @editable %>
2 <% if @editable %>
3 <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :id => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.current_version? %>
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 <%= watcher_tag(@page, User.current) %>
4 <%= watcher_tag(@page, User.current) %>
5 <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :id => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
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 <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :id => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
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 <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :id => @page.title}, :class => 'icon icon-move') if @content.current_version? %>
7 <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :id => @page.title}, :class => 'icon icon-move') if @content.current_version? %>
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') %>
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 <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :id => @page.title, :version => @content.version }, :class => 'icon icon-cancel') unless @content.current_version? %>
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 <% end %>
10 <% end %>
11 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %>
11 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %>
12 </div>
12 </div>
13
13
14 <%= wiki_page_breadcrumb(@page) %>
14 <%= wiki_page_breadcrumb(@page) %>
15
15
16 <% unless @content.current_version? %>
16 <% unless @content.current_version? %>
17 <p>
17 <p>
18 <%= link_to(("\xc2\xab " + l(:label_previous)),
18 <%= link_to(("\xc2\xab " + l(:label_previous)),
19 :action => 'show', :id => @page.title, :project_id => @page.project,
19 :action => 'show', :id => @page.title, :project_id => @page.project,
20 :version => (@content.version - 1)) + " - " if @content.version > 1 %>
20 :version => (@content.version - 1)) + " - " if @content.version > 1 %>
21 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
21 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
22 <%= '('.html_safe + link_to(l(:label_diff), :controller => 'wiki', :action => 'diff',
22 <%= '('.html_safe + link_to(l(:label_diff), :controller => 'wiki', :action => 'diff',
23 :id => @page.title, :project_id => @page.project,
23 :id => @page.title, :project_id => @page.project,
24 :version => @content.version) + ')'.html_safe if @content.version > 1 %> -
24 :version => @content.version) + ')'.html_safe if @content.version > 1 %> -
25 <%= link_to((l(:label_next) + " \xc2\xbb"), :action => 'show',
25 <%= link_to((l(:label_next) + " \xc2\xbb"), :action => 'show',
26 :id => @page.title, :project_id => @page.project,
26 :id => @page.title, :project_id => @page.project,
27 :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
27 :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
28 <%= link_to(l(:label_current_version), :action => 'show', :id => @page.title, :project_id => @page.project) %>
28 <%= link_to(l(:label_current_version), :action => 'show', :id => @page.title, :project_id => @page.project) %>
29 <br />
29 <br />
30 <em><%= @content.author ? link_to_user(@content.author) : l(:label_user_anonymous)
30 <em><%= @content.author ? link_to_user(@content.author) : l(:label_user_anonymous)
31 %>, <%= format_time(@content.updated_on) %> </em><br />
31 %>, <%= format_time(@content.updated_on) %> </em><br />
32 <%=h @content.comments %>
32 <%=h @content.comments %>
33 </p>
33 </p>
34 <hr />
34 <hr />
35 <% end %>
35 <% end %>
36
36
37 <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
37 <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
38
38
39 <%= link_to_attachments @page %>
39 <%= link_to_attachments @page %>
40
40
41 <% if @editable && authorize_for('wiki', 'add_attachment') %>
41 <% if @editable && authorize_for('wiki', 'add_attachment') %>
42 <div id="wiki_add_attachment">
42 <div id="wiki_add_attachment">
43 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
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 :id => 'attach_files_link' %></p>
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 <div class="box">
49 <div class="box">
47 <p><%= render :partial => 'attachments/form' %></p>
50 <p><%= render :partial => 'attachments/form' %></p>
48 </div>
51 </div>
49 <%= submit_tag l(:button_add) %>
52 <%= submit_tag l(:button_add) %>
50 <%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
53 <%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
51 <% end %>
54 <% end %>
52 </div>
55 </div>
53 <% end %>
56 <% end %>
54
57
55 <% other_formats_links do |f| %>
58 <% other_formats_links do |f| %>
56 <%= f.link_to 'PDF', :url => {:id => @page.title, :version => params[:version]} %>
59 <%= f.link_to 'PDF', :url => {:id => @page.title, :version => params[:version]} %>
57 <%= f.link_to 'HTML', :url => {:id => @page.title, :version => params[:version]} %>
60 <%= f.link_to 'HTML', :url => {:id => @page.title, :version => params[:version]} %>
58 <%= f.link_to 'TXT', :url => {:id => @page.title, :version => params[:version]} %>
61 <%= f.link_to 'TXT', :url => {:id => @page.title, :version => params[:version]} %>
59 <% end if User.current.allowed_to?(:export_wiki_pages, @project) %>
62 <% end if User.current.allowed_to?(:export_wiki_pages, @project) %>
60
63
61 <% content_for :header_tags do %>
64 <% content_for :header_tags do %>
62 <%= stylesheet_link_tag 'scm' %>
65 <%= stylesheet_link_tag 'scm' %>
63 <% end %>
66 <% end %>
64
67
65 <% content_for :sidebar do %>
68 <% content_for :sidebar do %>
66 <%= render :partial => 'sidebar' %>
69 <%= render :partial => 'sidebar' %>
67 <% end %>
70 <% end %>
68
71
69 <% html_title @page.pretty_title %>
72 <% html_title @page.pretty_title %>
@@ -1,10 +1,10
1 <h2><%=l(:label_confirmation)%></h2>
1 <h2><%=l(:label_confirmation)%></h2>
2
2
3 <div class="box"><center>
3 <div class="box"><center>
4 <p><strong><%= h(@project.name) %></strong><br /><%=l(:text_wiki_destroy_confirmation)%></p>
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 <%= hidden_field_tag "confirm", 1 %>
7 <%= hidden_field_tag "confirm", 1 %>
8 <%= submit_tag l(:button_delete) %>
8 <%= submit_tag l(:button_delete) %>
9 <% end %>
9 <% end %>
10 </center></div>
10 </center></div>
@@ -1,40 +1,40
1 <%= render :partial => 'action_menu' %>
1 <%= render :partial => 'action_menu' %>
2
2
3 <h2><%=l(:label_workflow)%></h2>
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 <fieldset class="tabular box">
6 <fieldset class="tabular box">
7 <legend><%= l(:label_copy_source) %></legend>
7 <legend><%= l(:label_copy_source) %></legend>
8 <p>
8 <p>
9 <label><%= l(:label_tracker) %></label>
9 <label><%= l(:label_tracker) %></label>
10 <%= select_tag('source_tracker_id',
10 <%= select_tag('source_tracker_id',
11 "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
11 "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
12 "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" +
12 "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" +
13 options_from_collection_for_select(@trackers, 'id', 'name', @source_tracker && @source_tracker.id)) %>
13 options_from_collection_for_select(@trackers, 'id', 'name', @source_tracker && @source_tracker.id)) %>
14 </p>
14 </p>
15 <p>
15 <p>
16 <label><%= l(:label_role) %></label>
16 <label><%= l(:label_role) %></label>
17 <%= select_tag('source_role_id',
17 <%= select_tag('source_role_id',
18 "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
18 "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
19 "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" +
19 "<option value=\"any\">--- #{ l(:label_copy_same_as_target) } ---</option>" +
20 options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %>
20 options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %>
21 </p>
21 </p>
22 </fieldset>
22 </fieldset>
23
23
24 <fieldset class="tabular box">
24 <fieldset class="tabular box">
25 <legend><%= l(:label_copy_target) %></legend>
25 <legend><%= l(:label_copy_target) %></legend>
26 <p>
26 <p>
27 <label><%= l(:label_tracker) %></label>
27 <label><%= l(:label_tracker) %></label>
28 <%= select_tag 'target_tracker_ids',
28 <%= select_tag 'target_tracker_ids',
29 "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
29 "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
30 options_from_collection_for_select(@trackers, 'id', 'name', @target_trackers && @target_trackers.map(&:id)), :multiple => true %>
30 options_from_collection_for_select(@trackers, 'id', 'name', @target_trackers && @target_trackers.map(&:id)), :multiple => true %>
31 </p>
31 </p>
32 <p>
32 <p>
33 <label><%= l(:label_role) %></label>
33 <label><%= l(:label_role) %></label>
34 <%= select_tag 'target_role_ids',
34 <%= select_tag 'target_role_ids',
35 "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
35 "<option value=\"\" disabled=\"disabled\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" +
36 options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %>
36 options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %>
37 </p>
37 </p>
38 </fieldset>
38 </fieldset>
39 <%= submit_tag l(:button_copy) %>
39 <%= submit_tag l(:button_copy) %>
40 <% end %>
40 <% end %>
@@ -1,50 +1,50
1 <%= render :partial => 'action_menu' %>
1 <%= render :partial => 'action_menu' %>
2
2
3 <h2><%=l(:label_workflow)%></h2>
3 <h2><%=l(:label_workflow)%></h2>
4
4
5 <p><%=l(:text_workflow_edit)%>:</p>
5 <p><%=l(:text_workflow_edit)%>:</p>
6
6
7 <% form_tag({}, :method => 'get') do %>
7 <%= form_tag({}, :method => 'get') do %>
8 <p>
8 <p>
9 <label><%=l(:label_role)%>:
9 <label><%=l(:label_role)%>:
10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
11
11
12 <label><%=l(:label_tracker)%>:
12 <label><%=l(:label_tracker)%>:
13 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
13 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
14
14
15 <%= hidden_field_tag 'used_statuses_only', '0' %>
15 <%= hidden_field_tag 'used_statuses_only', '0' %>
16 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
16 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
17 </p>
17 </p>
18 <p>
18 <p>
19 <%= submit_tag l(:button_edit), :name => nil %>
19 <%= submit_tag l(:button_edit), :name => nil %>
20 </p>
20 </p>
21 <% end %>
21 <% end %>
22
22
23 <% if @tracker && @role && @statuses.any? %>
23 <% if @tracker && @role && @statuses.any? %>
24 <% form_tag({}, :id => 'workflow_form' ) do %>
24 <%= form_tag({}, :id => 'workflow_form' ) do %>
25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
26 <%= hidden_field_tag 'role_id', @role.id %>
26 <%= hidden_field_tag 'role_id', @role.id %>
27 <div class="autoscroll">
27 <div class="autoscroll">
28 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
28 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
29
29
30 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
30 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
31 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
31 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
32 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
32 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
33 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
33 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
34 </div>
34 </div>
35 </fieldset>
35 </fieldset>
36 <%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
36 <%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
37
37
38 <fieldset class="collapsible" style="padding: 0;">
38 <fieldset class="collapsible" style="padding: 0;">
39 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
39 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
40 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
40 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
41 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
41 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
42 </div>
42 </div>
43 </fieldset>
43 </fieldset>
44 <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
44 <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
45 </div>
45 </div>
46 <%= submit_tag l(:button_save) %>
46 <%= submit_tag l(:button_save) %>
47 <% end %>
47 <% end %>
48 <% end %>
48 <% end %>
49
49
50 <% html_title(l(:label_workflow)) -%>
50 <% html_title(l(:label_workflow)) -%>
@@ -1,124 +1,6
1 # Don't change this file!
1 require 'rubygems'
2 # Configure your app in config/environment.rb and config/environments/*.rb
3
2
4 if RUBY_VERSION >= '1.9'
3 # Set up gems listed in the Gemfile.
5 require 'yaml'
4 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
6 YAML::ENGINE.yamler = 'syck'
7 end
8
5
9 RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
6 require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
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!
@@ -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 # Initialize the rails application
4 # you don't control web/app server and can't set it the proper way
5 RedmineApp::Application.initialize!
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
@@ -1,16 +1,19
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2 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
8 # Log error messages when you accidentally call methods on nil.
4 # every request. This slows down response time but is perfect for development
9 config.whiny_nils = true
5 # since you don't have to restart the webserver when you make code changes.
6 config.cache_classes = false
7
10
8 # Log error messages when you accidentally call methods on nil.
11 # Show full error reports and disable caching
9 config.whiny_nils = true
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
15 # Don't care if the mailer can't send
12 config.action_controller.consider_all_requests_local = true
16 config.action_mailer.raise_delivery_errors = false
13 config.action_controller.perform_caching = false
14
17
15 # Don't care if the mailer can't send
18 config.active_support.deprecation = :log
16 config.action_mailer.raise_delivery_errors = false
19 end
@@ -1,30 +1,32
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2 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.
7 #####
4 # Code is not reloaded between requests
8 # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html)
5 config.cache_classes = true
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 #####
19 # Full error reports are disabled and caching is turned on
8 # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html)
20 config.action_controller.perform_caching = true
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
18
21
19 # Full error reports are disabled and caching is turned on
22 # Enable serving of images, stylesheets, and javascripts from an asset server
20 config.action_controller.consider_all_requests_local = false
23 # config.action_controller.asset_host = "http://assets.example.com"
21 config.action_controller.perform_caching = true
22
24
23 # Enable serving of images, stylesheets, and javascripts from an asset server
25 # Disable delivery errors if you bad email addresses should just be ignored
24 # config.action_controller.asset_host = "http://assets.example.com"
26 config.action_mailer.raise_delivery_errors = false
25
27
26 # Disable delivery errors if you bad email addresses should just be ignored
28 # No email in production log
27 config.action_mailer.raise_delivery_errors = false
29 config.action_mailer.logger = nil
28
30
29 # No email in production log
31 config.active_support.deprecation = :log
30 config.action_mailer.logger = nil
32 end
@@ -1,25 +1,25
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2 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
9 # Log error messages when you accidentally call methods on nil.
4 # test suite. You never need to work with it otherwise. Remember that
10 config.whiny_nils = true
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
8
11
9 # Log error messages when you accidentally call methods on nil.
12 # Show full error reports and disable caching
10 config.whiny_nils = true
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
16 config.action_mailer.perform_deliveries = true
13 config.action_controller.consider_all_requests_local = true
17 config.action_mailer.delivery_method = :test
14 config.action_controller.perform_caching = false
15
18
16 config.action_mailer.perform_deliveries = true
19 # Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
17 config.action_mailer.delivery_method = :test
20 config.action_controller.allow_forgery_protection = false
18
21
19 config.action_controller.session = {
22 config.active_support.deprecation = :log
20 :key => "_test_session",
21 :secret => "some secret phrase for the tests."
22 }
23
23
24 # Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
24 config.secret_token = 'a secret token for running the tests'
25 config.action_controller.allow_forgery_protection = false
25 end
@@ -1,88 +1,86
1 # Patches active_support/core_ext/load_error.rb to support 1.9.3 LoadError message
1 # Patches active_support/core_ext/load_error.rb to support 1.9.3 LoadError message
2 if RUBY_VERSION >= '1.9.3'
2 if RUBY_VERSION >= '1.9.3'
3 MissingSourceFile::REGEXPS << [/^cannot load such file -- (.+)$/i, 1]
3 MissingSourceFile::REGEXPS << [/^cannot load such file -- (.+)$/i, 1]
4 end
4 end
5
5
6 require 'active_record'
6 require 'active_record'
7
7
8 module ActiveRecord
8 module ActiveRecord
9 class Base
9 class Base
10 include Redmine::I18n
10 include Redmine::I18n
11 def self.named_scope(*args)
12 scope(*args)
13 end
11
14
12 # Translate attribute names for validation errors display
15 # Translate attribute names for validation errors display
13 def self.human_attribute_name(attr, *args)
16 def self.human_attribute_name(attr, *args)
14 l("field_#{attr.to_s.gsub(/_id$/, '')}", :default => attr)
17 l("field_#{attr.to_s.gsub(/_id$/, '')}", :default => attr)
15 end
18 end
16 end
19 end
17 end
20 end
18
21
19 module ActionView
22 module ActionView
20 module Helpers
23 module Helpers
21 module DateHelper
24 module DateHelper
22 # distance_of_time_in_words breaks when difference is greater than 30 years
25 # distance_of_time_in_words breaks when difference is greater than 30 years
23 def distance_of_date_in_words(from_date, to_date = 0, options = {})
26 def distance_of_date_in_words(from_date, to_date = 0, options = {})
24 from_date = from_date.to_date if from_date.respond_to?(:to_date)
27 from_date = from_date.to_date if from_date.respond_to?(:to_date)
25 to_date = to_date.to_date if to_date.respond_to?(:to_date)
28 to_date = to_date.to_date if to_date.respond_to?(:to_date)
26 distance_in_days = (to_date - from_date).abs
29 distance_in_days = (to_date - from_date).abs
27
30
28 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
31 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
29 case distance_in_days
32 case distance_in_days
30 when 0..60 then locale.t :x_days, :count => distance_in_days.round
33 when 0..60 then locale.t :x_days, :count => distance_in_days.round
31 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
34 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
32 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
35 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
33 end
36 end
34 end
37 end
35 end
38 end
36 end
39 end
37 end
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 end
53 end
39
54
40 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
55 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
41
56
42 module AsynchronousMailer
57 module AsynchronousMailer
43 # Adds :async_smtp and :async_sendmail delivery methods
58 # Adds :async_smtp and :async_sendmail delivery methods
44 # to perform email deliveries asynchronously
59 # to perform email deliveries asynchronously
45 %w(smtp sendmail).each do |type|
60 %w(smtp sendmail).each do |type|
46 define_method("perform_delivery_async_#{type}") do |mail|
61 define_method("perform_delivery_async_#{type}") do |mail|
47 Thread.start do
62 Thread.start do
48 send "perform_delivery_#{type}", mail
63 send "perform_delivery_#{type}", mail
49 end
64 end
50 end
65 end
51 end
66 end
52
67
53 # Adds a delivery method that writes emails in tmp/emails for testing purpose
68 # Adds a delivery method that writes emails in tmp/emails for testing purpose
54 def perform_delivery_tmp_file(mail)
69 def perform_delivery_tmp_file(mail)
55 dest_dir = File.join(Rails.root, 'tmp', 'emails')
70 dest_dir = File.join(Rails.root, 'tmp', 'emails')
56 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
71 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
57 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
72 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
58 end
73 end
59 end
74 end
60
75
61 ActionMailer::Base.send :include, AsynchronousMailer
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 module ActionController
78 module ActionController
81 module MimeResponds
79 module MimeResponds
82 class Responder
80 class Collector
83 def api(&block)
81 def api(&block)
84 any(:xml, :json, &block)
82 any(:xml, :json, &block)
85 end
83 end
86 end
84 end
87 end
85 end
88 end
86 end
@@ -1,5 +1,4
1 # Add new mime types for use in respond_to blocks:
1 # Add new mime types for use in respond_to blocks:
2
2
3 Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
3 Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
4 Mime::Type.register 'application/pdf', :pdf
4
5 Mime::Type.register 'image/png', :png
@@ -1,5 +1,7
1 I18n.default_locale = 'en'
1 I18n.default_locale = 'en'
2 # Adds fallback to default locale for untranslated strings
2 # Adds fallback to default locale for untranslated strings
3 I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
3 I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
4
4
5 require 'redmine'
5 require 'redmine'
6
7 Redmine::Plugin.load
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now