Compare commits
465 Commits
2.1-stable
...
2.2.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c30292e85 | ||
|
|
c4164a0a7f | ||
|
|
504a767028 | ||
|
|
0eaba11e20 | ||
|
|
c2eb28b901 | ||
|
|
f09ea7b65a | ||
|
|
878e85848f | ||
|
|
4b010fb653 | ||
|
|
5f96e64cd7 | ||
|
|
ec1cb4632c | ||
|
|
e4a1b1324a | ||
|
|
6991874911 | ||
|
|
17bd53ae43 | ||
|
|
559dc4f523 | ||
|
|
6dcd50047d | ||
|
|
252df8dfc0 | ||
|
|
cbd3b057e0 | ||
|
|
d094dea0ec | ||
|
|
3a98a14250 | ||
|
|
ecef96a6b9 | ||
|
|
0951522247 | ||
|
|
42cc2d322a | ||
|
|
0367c323de | ||
|
|
80be82ae50 | ||
|
|
ebed927de5 | ||
|
|
cbd069118a | ||
|
|
a211d1be7d | ||
|
|
0a8e0a3bed | ||
|
|
58cf34dea5 | ||
|
|
594a7bde95 | ||
|
|
c5e257d82b | ||
|
|
56b12b289f | ||
|
|
c99eef1aff | ||
|
|
ed891e2733 | ||
|
|
4620b8b8db | ||
|
|
98eb2edd25 | ||
|
|
ed7318fb8d | ||
|
|
5a1a2f5855 | ||
|
|
f1314278d5 | ||
|
|
3a8b872a09 | ||
|
|
56290979fc | ||
|
|
6e0fb415a7 | ||
|
|
f26654b29f | ||
|
|
5c62c1cf95 | ||
|
|
2d6adbd7ff | ||
|
|
b0ccaffe1b | ||
|
|
2c37617973 | ||
|
|
567eb70fdb | ||
|
|
de63102925 | ||
|
|
a8fcf9389e | ||
|
|
3e4b36de83 | ||
|
|
edd584d59e | ||
|
|
e2b27ab696 | ||
|
|
1968d039ee | ||
|
|
fddc33cd45 | ||
|
|
29518ba0cf | ||
|
|
acd27b9eb1 | ||
|
|
6445b5997c | ||
|
|
416d33973f | ||
|
|
82c7dc11d2 | ||
|
|
d22f782d3f | ||
|
|
3717fdfa79 | ||
|
|
9d3d932703 | ||
|
|
3a379ce4b9 | ||
|
|
daa77f0109 | ||
|
|
c0d66d626b | ||
|
|
bf02b76ca3 | ||
|
|
75e02ca486 | ||
|
|
ee3d6dbdbb | ||
|
|
e97a3ab97b | ||
|
|
6e1ff5bba6 | ||
|
|
88d5663ad2 | ||
|
|
16441b4938 | ||
|
|
65d3bffad7 | ||
|
|
4d71446791 | ||
|
|
0ef82b3dcf | ||
|
|
a1d156d794 | ||
|
|
a1119624c0 | ||
|
|
0a675c7c77 | ||
|
|
c7ee26b144 | ||
|
|
f4cf7bc6f5 | ||
|
|
c3817e620f | ||
|
|
9a270c46c0 | ||
|
|
e43448f6ca | ||
|
|
14e56006a1 | ||
|
|
bf5b6014f7 | ||
|
|
6c0f3cd5d4 | ||
|
|
04dcdb3290 | ||
|
|
457f0bb337 | ||
|
|
96e01c6d4a | ||
|
|
fb9a2901e9 | ||
|
|
b94c971755 | ||
|
|
29fc292b18 | ||
|
|
845d7bc645 | ||
|
|
cb24cafedc | ||
|
|
dc68be983a | ||
|
|
25c63ec747 | ||
|
|
13f5301c09 | ||
|
|
be7895e04e | ||
|
|
921ad01bde | ||
|
|
d00cc4d9b5 | ||
|
|
f6367b8ba7 | ||
|
|
5d8d38aa60 | ||
|
|
7048f66619 | ||
|
|
6740f441c4 | ||
|
|
adcf54a92b | ||
|
|
a829bfa755 | ||
|
|
f232f56d97 | ||
|
|
26b3e48545 | ||
|
|
2d1e7ff829 | ||
|
|
a49de1c39d | ||
|
|
3739810afa | ||
|
|
f36b3fff60 | ||
|
|
46d27348fd | ||
|
|
1ff8bbf080 | ||
|
|
628b3cde71 | ||
|
|
5f36e1fff2 | ||
|
|
aa45516a02 | ||
|
|
3cf6a56b97 | ||
|
|
98f225f6cd | ||
|
|
72ea1851b9 | ||
|
|
1c14dd35cf | ||
|
|
4bd1e887d9 | ||
|
|
b3cf67cad4 | ||
|
|
19bd857790 | ||
|
|
dd96ef4248 | ||
|
|
70bcbd1404 | ||
|
|
9f6cf296d1 | ||
|
|
617cb7ac53 | ||
|
|
1da74009cd | ||
|
|
8b527ce247 | ||
|
|
e9e5d2e072 | ||
|
|
fd41840434 | ||
|
|
b880020527 | ||
|
|
547b85c4ef | ||
|
|
d21cfe6bb4 | ||
|
|
ce04c41830 | ||
|
|
37c590bfef | ||
|
|
b0b8352bc5 | ||
|
|
37a600475c | ||
|
|
cb68851eac | ||
|
|
d6b1ad38a7 | ||
|
|
a17f4c8375 | ||
|
|
00927f58af | ||
|
|
4ef90b6096 | ||
|
|
d6e8a2d4e6 | ||
|
|
c8b722456c | ||
|
|
7f3cc6e38b | ||
|
|
7d9c45fb8a | ||
|
|
0beef88850 | ||
|
|
ff9b3d3a06 | ||
|
|
3ac94be04b | ||
|
|
9280182bde | ||
|
|
68cd78f7cc | ||
|
|
45e7c63fc4 | ||
|
|
0eabfc5e72 | ||
|
|
6e190a76fb | ||
|
|
e00b4acf1e | ||
|
|
511e46e978 | ||
|
|
8a66522314 | ||
|
|
b49948d880 | ||
|
|
fdff662f4e | ||
|
|
7158a51add | ||
|
|
b5f8290f4c | ||
|
|
f909a8c006 | ||
|
|
399223daba | ||
|
|
cdc8944a89 | ||
|
|
d62ef6b9b1 | ||
|
|
b9c72bf590 | ||
|
|
cb7ab9bc0c | ||
|
|
9f148e098b | ||
|
|
1fc324b74c | ||
|
|
69b5e929af | ||
|
|
ea0e3849e3 | ||
|
|
96b7c041c8 | ||
|
|
e74986391a | ||
|
|
c00084b80b | ||
|
|
1f55907f83 | ||
|
|
5fe4aae7ba | ||
|
|
6b2e0333ca | ||
|
|
b71355f10b | ||
|
|
48d83884c3 | ||
|
|
fe690dcf20 | ||
|
|
beb2610650 | ||
|
|
625eebb720 | ||
|
|
123d2b318f | ||
|
|
5a2c80cb48 | ||
|
|
a71ff93f8f | ||
|
|
49f2d4729d | ||
|
|
d840837563 | ||
|
|
ac75aad581 | ||
|
|
176e239e23 | ||
|
|
0932689ef6 | ||
|
|
00736951cf | ||
|
|
29a474b641 | ||
|
|
8a2786c91e | ||
|
|
fb68f4703b | ||
|
|
2af516bd05 | ||
|
|
166cbfdb3c | ||
|
|
113f8b5cf2 | ||
|
|
a679d19709 | ||
|
|
03062394fc | ||
|
|
eff874b29a | ||
|
|
9e31308720 | ||
|
|
b2e035fa96 | ||
|
|
1c7e4bb7f0 | ||
|
|
e0811bc80d | ||
|
|
60afbcd37f | ||
|
|
c4ce048970 | ||
|
|
2beae77c58 | ||
|
|
440232a54d | ||
|
|
881a22d248 | ||
|
|
6cccdce06e | ||
|
|
9e7f71080f | ||
|
|
ff4a92a782 | ||
|
|
a829e381be | ||
|
|
1db6a0f89c | ||
|
|
8975f9c235 | ||
|
|
4ff3c9fdb5 | ||
|
|
f188277ab2 | ||
|
|
d66b3452db | ||
|
|
9ec142713d | ||
|
|
61d8e8f1f4 | ||
|
|
9796d18609 | ||
|
|
f09be68323 | ||
|
|
82eab51203 | ||
|
|
d22b57a27d | ||
|
|
d29fa4735b | ||
|
|
0faab70be5 | ||
|
|
251f263abd | ||
|
|
79c1ec7adc | ||
|
|
e7dfc30c2f | ||
|
|
9b702971bf | ||
|
|
2b4389466f | ||
|
|
7729178d9d | ||
|
|
50037b18c4 | ||
|
|
1a9d482fc9 | ||
|
|
1737552eca | ||
|
|
9ff16dd411 | ||
|
|
59cf6dea86 | ||
|
|
f293f82c3f | ||
|
|
e764349a1b | ||
|
|
c3788674ff | ||
|
|
d62b90db73 | ||
|
|
fcb22595d0 | ||
|
|
ef1f14905b | ||
|
|
b8fbb41d5f | ||
|
|
0a000921f8 | ||
|
|
c945fe82b2 | ||
|
|
4b6b568635 | ||
|
|
5aaf9c734c | ||
|
|
ff2d374a14 | ||
|
|
9581afe060 | ||
|
|
6ebfe54e6e | ||
|
|
02c770968f | ||
|
|
5ae73e3784 | ||
|
|
d5abb11c0b | ||
|
|
04ac60d891 | ||
|
|
eedad03141 | ||
|
|
8487a3e0e6 | ||
|
|
a1b82c7650 | ||
|
|
93daf6865e | ||
|
|
0df2c4a975 | ||
|
|
0cf44b976f | ||
|
|
f76de605a0 | ||
|
|
7a162b7255 | ||
|
|
9da555fcb7 | ||
|
|
ed9f954c6c | ||
|
|
0f6be546b8 | ||
|
|
a66a50eda2 | ||
|
|
69f863b3c8 | ||
|
|
db67eff913 | ||
|
|
756adebb96 | ||
|
|
0bcc6115c3 | ||
|
|
be79553f33 | ||
|
|
dd887891f7 | ||
|
|
55dbd8886f | ||
|
|
5d9bdf888a | ||
|
|
43da05f3cb | ||
|
|
6c3e38c4c0 | ||
|
|
a88782bdec | ||
|
|
31ee1d22eb | ||
|
|
9af6141c91 | ||
|
|
8a6ce3a646 | ||
|
|
e04bd297e0 | ||
|
|
3bda16f825 | ||
|
|
7b5d15eca8 | ||
|
|
ad94777d9c | ||
|
|
90e0d681dc | ||
|
|
924e5ad61b | ||
|
|
b537a2f1de | ||
|
|
7af1504672 | ||
|
|
2b797fa82f | ||
|
|
8358dc1cc5 | ||
|
|
92391abd1e | ||
|
|
51818cbbb6 | ||
|
|
8bde60dc56 | ||
|
|
d8f039f489 | ||
|
|
3e706adddd | ||
|
|
5344a35f72 | ||
|
|
2ac9f7d6ac | ||
|
|
f9b073f9d0 | ||
|
|
ee3ef85157 | ||
|
|
3ba648e2dc | ||
|
|
fd79c1b5b4 | ||
|
|
3a683c8ef3 | ||
|
|
2f555a3183 | ||
|
|
da85ca221a | ||
|
|
54f7fe5bd3 | ||
|
|
f683f956bb | ||
|
|
667ad23f9c | ||
|
|
578d15ed00 | ||
|
|
70d75ad222 | ||
|
|
9a3ab9c704 | ||
|
|
00100a15eb | ||
|
|
6c692d9715 | ||
|
|
b15bd840ec | ||
|
|
d0e090eff9 | ||
|
|
83bcc1f043 | ||
|
|
b0013d9f68 | ||
|
|
5a02dcf0a9 | ||
|
|
c4e040fa88 | ||
|
|
3a178a42cf | ||
|
|
340b4a30d0 | ||
|
|
7263862ce3 | ||
|
|
7626b21a1e | ||
|
|
16befaf2e9 | ||
|
|
92f9280352 | ||
|
|
178f64950f | ||
|
|
727aad50b7 | ||
|
|
3599cb15a2 | ||
|
|
fa237901b0 | ||
|
|
6f13f2b842 | ||
|
|
f26662f31f | ||
|
|
4df979fdc2 | ||
|
|
6d6f42c6a9 | ||
|
|
615f8957c5 | ||
|
|
e6596dbc43 | ||
|
|
9d8c0f9a63 | ||
|
|
5411f93a9a | ||
|
|
cbe8226759 | ||
|
|
b9d7c22297 | ||
|
|
44137cb1d4 | ||
|
|
1503c6cce7 | ||
|
|
8162d86192 | ||
|
|
88e1587b08 | ||
|
|
fb4210b6d9 | ||
|
|
65a045e207 | ||
|
|
7c0c699f95 | ||
|
|
eaf46c5512 | ||
|
|
b571c3dbc9 | ||
|
|
5ddd6e7d1f | ||
|
|
0e87672c49 | ||
|
|
0178b5a2fe | ||
|
|
bb1563f23f | ||
|
|
1d4c8abe96 | ||
|
|
ef988ba17c | ||
|
|
a29854707b | ||
|
|
de0e0f09a3 | ||
|
|
5f794b5af5 | ||
|
|
df44af819c | ||
|
|
b04f44f944 | ||
|
|
b49abf0c22 | ||
|
|
757073edef | ||
|
|
ecdd77c582 | ||
|
|
ba2330d0a0 | ||
|
|
8cf8f5bdbf | ||
|
|
81782f2408 | ||
|
|
99662fa18e | ||
|
|
61248d1dbc | ||
|
|
d60b8c927c | ||
|
|
1d7f5e34c5 | ||
|
|
c292971390 | ||
|
|
1b6da80e16 | ||
|
|
69b8931e92 | ||
|
|
69a3941ac8 | ||
|
|
086478c5a6 | ||
|
|
1efd303b4a | ||
|
|
b6be866151 | ||
|
|
3e7bb3c632 | ||
|
|
f1fdbf8d80 | ||
|
|
f58ed6c206 | ||
|
|
d255f7f5b0 | ||
|
|
c6ea8fbf21 | ||
|
|
cc4cff9f11 | ||
|
|
c3e055bd70 | ||
|
|
fe832071d4 | ||
|
|
9c3543ac3f | ||
|
|
7690aff8bd | ||
|
|
455738cbe9 | ||
|
|
cf52a6ccb4 | ||
|
|
c91ce512a9 | ||
|
|
b7c71fe427 | ||
|
|
29292a3faf | ||
|
|
d1d8640822 | ||
|
|
49c2f316c2 | ||
|
|
95fe4d61c9 | ||
|
|
917a89fbf7 | ||
|
|
18d1c62ca8 | ||
|
|
5ce2f4f81c | ||
|
|
7c8ac2eecc | ||
|
|
3bde603029 | ||
|
|
838025372d | ||
|
|
5328c4adcb | ||
|
|
30b3e796ff | ||
|
|
c1b71e84c6 | ||
|
|
59d8ae61ef | ||
|
|
fa2fe3e1e8 | ||
|
|
cb01d87ddd | ||
|
|
37351ddecc | ||
|
|
551c024b45 | ||
|
|
f18edc4e1c | ||
|
|
0af04b8ae0 | ||
|
|
3586e44c1d | ||
|
|
d277d14432 | ||
|
|
12de6a177a | ||
|
|
74f7fc38f0 | ||
|
|
1f58b94bd8 | ||
|
|
1fcd3d956f | ||
|
|
f9208d7c5c | ||
|
|
cbe28d75d0 | ||
|
|
a701bd5fbd | ||
|
|
a3ae06921d | ||
|
|
a6368733ad | ||
|
|
f230ffbd54 | ||
|
|
51e6c7589e | ||
|
|
581058f663 | ||
|
|
187d5db6b4 | ||
|
|
01e2472c92 | ||
|
|
2da70de1a7 | ||
|
|
dd89ce4594 | ||
|
|
55748b6fc0 | ||
|
|
e05e9179fd | ||
|
|
f825167003 | ||
|
|
57c38a33e5 | ||
|
|
b81a578eb6 | ||
|
|
8593e34722 | ||
|
|
34b64d646f | ||
|
|
747e4ecd3a | ||
|
|
ca4f2c59b6 | ||
|
|
2058c66d73 | ||
|
|
53e2eb1867 | ||
|
|
a81da3491e | ||
|
|
ff0989a702 | ||
|
|
49d93a5cab | ||
|
|
17db2dca3d | ||
|
|
3cd15102c6 | ||
|
|
dd5844b86c | ||
|
|
ee0fc2e1d5 | ||
|
|
4a7e148aff | ||
|
|
c2b12d853f | ||
|
|
67cdaabe6f | ||
|
|
72af2730ff | ||
|
|
5441f8de4d | ||
|
|
e659aff468 | ||
|
|
2566aef8ab | ||
|
|
c70bc540dd | ||
|
|
597fbf0e04 | ||
|
|
338d81d6e3 | ||
|
|
40302566a8 | ||
|
|
6d0aed1f44 | ||
|
|
a3d0e82552 | ||
|
|
818f3564fa | ||
|
|
745c5d053e | ||
|
|
049aa3971d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,9 +19,11 @@
|
||||
/public/plugin_assets
|
||||
/tmp/*
|
||||
/tmp/cache/*
|
||||
/tmp/pdf/*
|
||||
/tmp/sessions/*
|
||||
/tmp/sockets/*
|
||||
/tmp/test/*
|
||||
/tmp/thumbnails/*
|
||||
/vendor/cache
|
||||
/vendor/rails
|
||||
*.rbc
|
||||
|
||||
@@ -21,9 +21,11 @@ public/dispatch.*
|
||||
public/plugin_assets
|
||||
tmp/*
|
||||
tmp/cache/*
|
||||
tmp/pdf/*
|
||||
tmp/sessions/*
|
||||
tmp/sockets/*
|
||||
tmp/test/*
|
||||
tmp/thumbnails/*
|
||||
vendor/cache
|
||||
vendor/rails
|
||||
*.rbc
|
||||
|
||||
13
Gemfile
13
Gemfile
@@ -1,6 +1,6 @@
|
||||
source 'http://rubygems.org'
|
||||
|
||||
gem 'rails', '3.2.8'
|
||||
gem "rails", "3.2.13"
|
||||
gem "jquery-rails", "~> 2.0.2"
|
||||
gem "i18n", "~> 0.6.0"
|
||||
gem "coderay", "~> 1.0.6"
|
||||
@@ -41,7 +41,7 @@ end
|
||||
|
||||
platforms :mri_18, :mingw_18 do
|
||||
group :mysql do
|
||||
gem "mysql"
|
||||
gem "mysql", "~> 2.8.1"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -74,9 +74,12 @@ end
|
||||
|
||||
group :test do
|
||||
gem "shoulda", "~> 2.11"
|
||||
# Shoulda does not work nice on Ruby 1.9.3 and seems to need test-unit explicitely.
|
||||
gem "test-unit", :platforms => [:mri_19]
|
||||
gem "mocha", "0.12.3"
|
||||
# Shoulda does not work nice on Ruby 1.9.3 and JRuby 1.7.
|
||||
# It seems to need test-unit explicitely.
|
||||
platforms = [:mri_19]
|
||||
platforms << :jruby if defined?(JRUBY_VERSION) && JRUBY_VERSION >= "1.7"
|
||||
gem "test-unit", :platforms => platforms
|
||||
gem "mocha", "~> 0.13.3"
|
||||
end
|
||||
|
||||
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
|
||||
|
||||
@@ -238,7 +238,7 @@ class AccountController < ApplicationController
|
||||
def onthefly_creation_failed(user, auth_source_options = { })
|
||||
@user = user
|
||||
session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
|
||||
render register_path
|
||||
render :action => 'register'
|
||||
end
|
||||
|
||||
def invalid_credentials
|
||||
|
||||
@@ -22,7 +22,7 @@ class Unauthorized < Exception; end
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include Redmine::I18n
|
||||
|
||||
|
||||
class_attribute :accept_api_auth_actions
|
||||
class_attribute :accept_rss_auth_actions
|
||||
class_attribute :model_object
|
||||
@@ -90,7 +90,7 @@ class ApplicationController < ActionController::Base
|
||||
def find_current_user
|
||||
user = nil
|
||||
unless api_request?
|
||||
if session[:user_id]
|
||||
if session[:user_id]
|
||||
# existing session
|
||||
user = (User.active.find(session[:user_id]) rescue nil)
|
||||
elsif autologin_user = try_to_autologin
|
||||
@@ -110,6 +110,16 @@ class ApplicationController < ActionController::Base
|
||||
user = User.try_to_login(username, password) || User.find_by_api_key(username)
|
||||
end
|
||||
end
|
||||
# Switch user if requested by an admin user
|
||||
if user && user.admin? && (username = api_switch_user_from_request)
|
||||
su = User.find_by_login(username)
|
||||
if su && su.active?
|
||||
logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
|
||||
user = su
|
||||
else
|
||||
render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
|
||||
end
|
||||
end
|
||||
end
|
||||
user
|
||||
end
|
||||
@@ -266,14 +276,24 @@ class ApplicationController < ActionController::Base
|
||||
self.model_object = model
|
||||
end
|
||||
|
||||
# Filter for bulk issue operations
|
||||
# Find the issue whose id is the :id parameter
|
||||
# Raises a Unauthorized exception if the issue is not visible
|
||||
def find_issue
|
||||
# Issue.visible.find(...) can not be used to redirect user to the login form
|
||||
# if the issue actually exists but requires authentication
|
||||
@issue = Issue.find(params[:id])
|
||||
raise Unauthorized unless @issue.visible?
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
# Find issues with a single :id param or :ids array param
|
||||
# Raises a Unauthorized exception if one of the issues is not visible
|
||||
def find_issues
|
||||
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
|
||||
raise ActiveRecord::RecordNotFound if @issues.empty?
|
||||
if @issues.detect {|issue| !issue.visible?}
|
||||
deny_access
|
||||
return
|
||||
end
|
||||
raise Unauthorized unless @issues.all?(&:visible?)
|
||||
@projects = @issues.collect(&:project).compact.uniq
|
||||
@project = @projects.first if @projects.size == 1
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
@@ -402,7 +422,7 @@ class ApplicationController < ActionController::Base
|
||||
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
|
||||
@items = @items.slice(0, Setting.feeds_limit.to_i)
|
||||
@title = options[:title] || Setting.app_title
|
||||
render :template => "common/feed.atom", :layout => false,
|
||||
render :template => "common/feed", :formats => [:atom], :layout => false,
|
||||
:content_type => 'application/atom+xml'
|
||||
end
|
||||
|
||||
@@ -508,6 +528,11 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the API 'switch user' value if present
|
||||
def api_switch_user_from_request
|
||||
request.headers["X-Redmine-Switch-User"].to_s.presence
|
||||
end
|
||||
|
||||
# Renders a warning flash if obj has unsaved attachments
|
||||
def render_attachment_warning_if_needed(obj)
|
||||
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
|
||||
@@ -538,8 +563,13 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
# Renders a 200 response for successfull updates or deletions via the API
|
||||
def render_api_ok
|
||||
# head :ok would return a response body with one space
|
||||
render :text => '', :status => :ok, :layout => nil
|
||||
render_api_head :ok
|
||||
end
|
||||
|
||||
# Renders a head API response
|
||||
def render_api_head(status)
|
||||
# #head would return a response body with one space
|
||||
render :text => '', :status => status, :layout => nil
|
||||
end
|
||||
|
||||
# Renders API response on validation failure
|
||||
|
||||
@@ -84,7 +84,7 @@ class AttachmentsController < ApplicationController
|
||||
|
||||
@attachment = Attachment.new(:file => request.raw_post)
|
||||
@attachment.author = User.current
|
||||
@attachment.filename = Redmine::Utils.random_hex(16)
|
||||
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
|
||||
|
||||
if @attachment.save
|
||||
respond_to do |format|
|
||||
|
||||
@@ -39,14 +39,18 @@ class BoardsController < ApplicationController
|
||||
sort_init 'updated_on', 'desc'
|
||||
sort_update 'created_on' => "#{Message.table_name}.created_on",
|
||||
'replies' => "#{Message.table_name}.replies_count",
|
||||
'updated_on' => "#{Message.table_name}.updated_on"
|
||||
'updated_on' => "COALESCE(last_replies_messages.created_on, #{Message.table_name}.created_on)"
|
||||
|
||||
@topic_count = @board.topics.count
|
||||
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
|
||||
@topics = @board.topics.reorder("#{Message.table_name}.sticky DESC").order(sort_clause).all(
|
||||
:include => [:author, {:last_reply => :author}],
|
||||
:limit => @topic_pages.items_per_page,
|
||||
:offset => @topic_pages.current.offset)
|
||||
@topics = @board.topics.
|
||||
reorder("#{Message.table_name}.sticky DESC").
|
||||
includes(:last_reply).
|
||||
limit(@topic_pages.items_per_page).
|
||||
offset(@topic_pages.current.offset).
|
||||
order(sort_clause).
|
||||
preload(:author, {:last_reply => :author}).
|
||||
all
|
||||
@message = Message.new(:board => @board)
|
||||
render :action => 'show', :layout => !request.xhr?
|
||||
}
|
||||
|
||||
@@ -18,13 +18,26 @@
|
||||
class EnumerationsController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_filter :require_admin
|
||||
before_filter :require_admin, :except => :index
|
||||
before_filter :require_admin_or_api_request, :only => :index
|
||||
before_filter :build_new_enumeration, :only => [:new, :create]
|
||||
before_filter :find_enumeration, :only => [:edit, :update, :destroy]
|
||||
accept_api_auth :index
|
||||
|
||||
helper :custom_fields
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.api {
|
||||
@klass = Enumeration.get_subclass(params[:type])
|
||||
if @klass
|
||||
@enumerations = @klass.shared.sorted.all
|
||||
else
|
||||
render_404
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@@ -33,7 +46,7 @@ class EnumerationsController < ApplicationController
|
||||
def create
|
||||
if request.post? && @enumeration.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
redirect_to :action => 'index', :type => @enumeration.type
|
||||
redirect_to :action => 'index'
|
||||
else
|
||||
render :action => 'new'
|
||||
end
|
||||
@@ -45,7 +58,7 @@ class EnumerationsController < ApplicationController
|
||||
def update
|
||||
if request.put? && @enumeration.update_attributes(params[:enumeration])
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
redirect_to :action => 'index', :type => @enumeration.type
|
||||
redirect_to :action => 'index'
|
||||
else
|
||||
render :action => 'edit'
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class IssueCategoriesController < ApplicationController
|
||||
before_filter :find_project_by_project_id, :only => [:index, :new, :create]
|
||||
before_filter :authorize
|
||||
accept_api_auth :index, :show, :create, :update, :destroy
|
||||
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
|
||||
@@ -92,7 +92,7 @@ class IssueCategoriesController < ApplicationController
|
||||
|
||||
def destroy
|
||||
@issue_count = @category.issues.size
|
||||
if @issue_count == 0 || params[:todo] || api_request?
|
||||
if @issue_count == 0 || params[:todo] || api_request?
|
||||
reassign_to = nil
|
||||
if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
|
||||
reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
|
||||
|
||||
@@ -56,6 +56,7 @@ class IssuesController < ApplicationController
|
||||
retrieve_query
|
||||
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
|
||||
sort_update(@query.sortable_columns)
|
||||
@query.sort_criteria = sort_criteria.to_a
|
||||
|
||||
if @query.valid?
|
||||
case params[:format]
|
||||
@@ -81,7 +82,7 @@ class IssuesController < ApplicationController
|
||||
respond_to do |format|
|
||||
format.html { render :template => 'issues/index', :layout => !request.xhr? }
|
||||
format.api {
|
||||
Issue.load_relations(@issues) if include_in_api_response?('relations')
|
||||
Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
|
||||
}
|
||||
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
|
||||
format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
|
||||
@@ -99,8 +100,9 @@ class IssuesController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
|
||||
@journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
|
||||
@journals.each_with_index {|j,i| j.indice = i+1}
|
||||
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
||||
|
||||
@changesets = @issue.changesets.visible.all
|
||||
@@ -118,7 +120,10 @@ class IssuesController < ApplicationController
|
||||
}
|
||||
format.api
|
||||
format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
|
||||
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
|
||||
format.pdf {
|
||||
pdf = issue_to_pdf(@issue, :journals => @journals)
|
||||
send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -173,6 +178,7 @@ class IssuesController < ApplicationController
|
||||
@conflict = true
|
||||
if params[:last_journal_id]
|
||||
@conflict_journals = @issue.journals_after(params[:last_journal_id]).all
|
||||
@conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -307,19 +313,7 @@ class IssuesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_issue
|
||||
# Issue.visible.find(...) can not be used to redirect user to the login form
|
||||
# if the issue actually exists but requires authentication
|
||||
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
|
||||
unless @issue.visible?
|
||||
deny_access
|
||||
return
|
||||
end
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
private
|
||||
|
||||
def find_project
|
||||
project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
|
||||
@@ -354,8 +348,7 @@ private
|
||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||
@time_entry.attributes = params[:time_entry]
|
||||
|
||||
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
|
||||
@issue.init_journal(User.current, @notes)
|
||||
@issue.init_journal(User.current)
|
||||
|
||||
issue_attributes = params[:issue]
|
||||
if issue_attributes && params[:conflict_resolution]
|
||||
@@ -364,7 +357,7 @@ private
|
||||
issue_attributes = issue_attributes.dup
|
||||
issue_attributes.delete(:lock_version)
|
||||
when 'add_notes'
|
||||
issue_attributes = {}
|
||||
issue_attributes = issue_attributes.slice(:notes)
|
||||
when 'cancel'
|
||||
redirect_to issue_path(@issue)
|
||||
return false
|
||||
@@ -398,7 +391,7 @@ private
|
||||
end
|
||||
|
||||
@issue.project = @project
|
||||
@issue.author = User.current
|
||||
@issue.author ||= User.current
|
||||
# Tracker must be set before custom field values
|
||||
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
|
||||
if @issue.tracker.nil?
|
||||
|
||||
@@ -57,10 +57,10 @@ class JournalsController < ApplicationController
|
||||
end
|
||||
|
||||
def new
|
||||
journal = Journal.find(params[:journal_id]) if params[:journal_id]
|
||||
if journal
|
||||
user = journal.user
|
||||
text = journal.notes
|
||||
@journal = Journal.visible.find(params[:journal_id]) if params[:journal_id]
|
||||
if @journal
|
||||
user = @journal.user
|
||||
text = @journal.notes
|
||||
else
|
||||
user = @issue.author
|
||||
text = @issue.description
|
||||
@@ -69,6 +69,8 @@ class JournalsController < ApplicationController
|
||||
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
|
||||
@content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
|
||||
@content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -95,17 +97,9 @@ class JournalsController < ApplicationController
|
||||
private
|
||||
|
||||
def find_journal
|
||||
@journal = Journal.find(params[:id])
|
||||
@journal = Journal.visible.find(params[:id])
|
||||
@project = @journal.journalized.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
# TODO: duplicated in IssuesController
|
||||
def find_issue
|
||||
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
end
|
||||
|
||||
@@ -123,7 +123,7 @@ class MessagesController < ApplicationController
|
||||
|
||||
private
|
||||
def find_message
|
||||
find_board
|
||||
return unless find_board
|
||||
@message = @board.messages.find(params[:id], :include => :parent)
|
||||
@topic = @message.root
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
@@ -135,5 +135,6 @@ private
|
||||
@project = @board.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -147,15 +147,16 @@ class MyController < ApplicationController
|
||||
# params[:block] : id of the block to add
|
||||
def add_block
|
||||
block = params[:block].to_s.underscore
|
||||
(render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
|
||||
@user = User.current
|
||||
layout = @user.pref[:my_page_layout] || {}
|
||||
# remove if already present in a group
|
||||
%w(top left right).each {|f| (layout[f] ||= []).delete block }
|
||||
# add it on top
|
||||
layout['top'].unshift block
|
||||
@user.pref[:my_page_layout] = layout
|
||||
@user.pref.save
|
||||
if block.present? && BLOCKS.key?(block)
|
||||
@user = User.current
|
||||
layout = @user.pref[:my_page_layout] || {}
|
||||
# remove if already present in a group
|
||||
%w(top left right).each {|f| (layout[f] ||= []).delete block }
|
||||
# add it on top
|
||||
layout['top'].unshift block
|
||||
@user.pref[:my_page_layout] = layout
|
||||
@user.pref.save
|
||||
end
|
||||
redirect_to :action => 'page_layout'
|
||||
end
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ class PreviewsController < ApplicationController
|
||||
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
|
||||
@description = nil
|
||||
end
|
||||
@notes = params[:notes]
|
||||
# params[:notes] is useful for preview of notes in issue history
|
||||
@notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil)
|
||||
else
|
||||
@description = (params[:issue] ? params[:issue][:description] : nil)
|
||||
end
|
||||
@@ -34,6 +35,10 @@ class PreviewsController < ApplicationController
|
||||
end
|
||||
|
||||
def news
|
||||
if params[:id].present? && news = News.visible.find_by_id(params[:id])
|
||||
@previewed = news
|
||||
@attachments = news.attachments
|
||||
end
|
||||
@text = (params[:news] ? params[:news][:description] : nil)
|
||||
render :partial => 'common/preview'
|
||||
end
|
||||
|
||||
@@ -116,11 +116,7 @@ class ProjectsController < ApplicationController
|
||||
@source_project = Project.find(params[:id])
|
||||
if request.get?
|
||||
@project = Project.copy_from(@source_project)
|
||||
if @project
|
||||
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
|
||||
else
|
||||
redirect_to :controller => 'admin', :action => 'projects'
|
||||
end
|
||||
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
|
||||
else
|
||||
Mailer.with_deliveries(params[:notifications] == '1') do
|
||||
@project = Project.new
|
||||
@@ -139,7 +135,8 @@ class ProjectsController < ApplicationController
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to :controller => 'admin', :action => 'projects'
|
||||
# source_project not found
|
||||
render_404
|
||||
end
|
||||
|
||||
# Show @project
|
||||
|
||||
@@ -242,7 +242,7 @@ class RepositoriesController < ApplicationController
|
||||
# DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
|
||||
def remove_related_issue
|
||||
@issue = Issue.visible.find_by_id(params[:issue_id])
|
||||
if @issue
|
||||
if @issue
|
||||
@changeset.issues.delete(@issue)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
class RolesController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_filter :require_admin, :except => :index
|
||||
before_filter :require_admin_or_api_request, :only => :index
|
||||
before_filter :find_role, :only => [:edit, :update, :destroy]
|
||||
accept_api_auth :index
|
||||
before_filter :require_admin, :except => [:index, :show]
|
||||
before_filter :require_admin_or_api_request, :only => [:index, :show]
|
||||
before_filter :find_role, :only => [:show, :edit, :update, :destroy]
|
||||
accept_api_auth :index, :show
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
@@ -35,6 +35,12 @@ class RolesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.api
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
# Prefills the form with 'Non member' role permissions by default
|
||||
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
|
||||
|
||||
@@ -39,7 +39,8 @@ class SettingsController < ApplicationController
|
||||
redirect_to :action => 'edit', :tab => params[:tab]
|
||||
else
|
||||
@options = {}
|
||||
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
|
||||
user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}
|
||||
@options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]}
|
||||
@deliveries = ActionMailer::Base.perform_deliveries
|
||||
|
||||
@guessed_host_and_path = request.host_with_port.dup
|
||||
|
||||
@@ -138,7 +138,7 @@ class TimelogController < ApplicationController
|
||||
:time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
|
||||
:back_url => params[:back_url]
|
||||
else
|
||||
redirect_to :action => 'new',
|
||||
redirect_to :action => 'new',
|
||||
:time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
|
||||
:back_url => params[:back_url]
|
||||
end
|
||||
@@ -308,6 +308,9 @@ private
|
||||
when 'last_week'
|
||||
@from = Date.today - 7 - (Date.today.cwday - 1)%7
|
||||
@to = @from + 6
|
||||
when 'last_2_weeks'
|
||||
@from = Date.today - 14 - (Date.today.cwday - 1)%7
|
||||
@to = @from + 13
|
||||
when '7_days'
|
||||
@from = Date.today - 7
|
||||
@to = Date.today
|
||||
|
||||
@@ -59,7 +59,7 @@ class TrackersController < ApplicationController
|
||||
@tracker ||= Tracker.find(params[:id])
|
||||
@projects = Project.find(:all)
|
||||
end
|
||||
|
||||
|
||||
def update
|
||||
@tracker = Tracker.find(params[:id])
|
||||
if request.put? and @tracker.update_attributes(params[:tracker])
|
||||
|
||||
@@ -20,7 +20,7 @@ class VersionsController < ApplicationController
|
||||
model_object Version
|
||||
before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
|
||||
before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
|
||||
before_filter :find_project, :only => [:index, :new, :create, :close_completed]
|
||||
before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
|
||||
before_filter :authorize
|
||||
|
||||
accept_api_auth :index, :show, :create, :update, :destroy
|
||||
@@ -169,12 +169,7 @@ class VersionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_project
|
||||
@project = Project.find(params[:project_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
private
|
||||
|
||||
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
|
||||
if ids = params[:tracker_ids]
|
||||
@@ -183,5 +178,4 @@ private
|
||||
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -35,7 +35,8 @@ class WikiController < ApplicationController
|
||||
default_search_scope :wiki_pages
|
||||
before_filter :find_wiki, :authorize
|
||||
before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
|
||||
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
|
||||
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
|
||||
accept_api_auth :index, :show, :update, :destroy
|
||||
|
||||
helper :attachments
|
||||
include AttachmentsHelper
|
||||
@@ -45,7 +46,13 @@ class WikiController < ApplicationController
|
||||
# List of pages, sorted alphabetically and by parent (hierarchy)
|
||||
def index
|
||||
load_pages_for_index
|
||||
@pages_by_parent_id = @pages.group_by(&:parent_id)
|
||||
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
@pages_by_parent_id = @pages.group_by(&:parent_id)
|
||||
}
|
||||
format.api
|
||||
end
|
||||
end
|
||||
|
||||
# List of page, by last update
|
||||
@@ -57,7 +64,7 @@ class WikiController < ApplicationController
|
||||
# display a page (in editing mode if it doesn't exist)
|
||||
def show
|
||||
if @page.new_record?
|
||||
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
|
||||
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
|
||||
edit
|
||||
render :action => 'edit'
|
||||
else
|
||||
@@ -66,8 +73,7 @@ class WikiController < ApplicationController
|
||||
return
|
||||
end
|
||||
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
|
||||
# Redirects user to the current version if he's not allowed to view previous versions
|
||||
redirect_to :version => nil
|
||||
deny_access
|
||||
return
|
||||
end
|
||||
@content = @page.content_for_version(params[:version])
|
||||
@@ -89,7 +95,10 @@ class WikiController < ApplicationController
|
||||
@content.current_version? &&
|
||||
Redmine::WikiFormatting.supports_section_edit?
|
||||
|
||||
render :action => 'show'
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.api
|
||||
end
|
||||
end
|
||||
|
||||
# edit an existing page or a new one
|
||||
@@ -121,50 +130,65 @@ class WikiController < ApplicationController
|
||||
# Creates a new page or updates an existing one
|
||||
def update
|
||||
return render_403 unless editable?
|
||||
was_new_page = @page.new_record?
|
||||
@page.content = WikiContent.new(:page => @page) if @page.new_record?
|
||||
@page.safe_attributes = params[:wiki_page]
|
||||
|
||||
@content = @page.content_for_version(params[:version])
|
||||
@content.text = initial_page_content(@page) if @content.text.blank?
|
||||
# don't keep previous comment
|
||||
@content.comments = nil
|
||||
|
||||
if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
|
||||
attachments = Attachment.attach_files(@page, params[:attachments])
|
||||
render_attachment_warning_if_needed(@page)
|
||||
# don't save content if text wasn't changed
|
||||
@page.save
|
||||
redirect_to :action => 'show', :project_id => @project, :id => @page.title
|
||||
return
|
||||
@content = @page.content
|
||||
content_params = params[:content]
|
||||
if content_params.nil? && params[:wiki_page].is_a?(Hash)
|
||||
content_params = params[:wiki_page].slice(:text, :comments, :version)
|
||||
end
|
||||
content_params ||= {}
|
||||
|
||||
@content.comments = params[:content][:comments]
|
||||
@text = params[:content][:text]
|
||||
@content.comments = content_params[:comments]
|
||||
@text = content_params[:text]
|
||||
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
|
||||
@section = params[:section].to_i
|
||||
@section_hash = params[:section_hash]
|
||||
@content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
|
||||
else
|
||||
@content.version = params[:content][:version]
|
||||
@content.version = content_params[:version] if content_params[:version]
|
||||
@content.text = @text
|
||||
end
|
||||
@content.author = User.current
|
||||
@page.content = @content
|
||||
if @page.save
|
||||
|
||||
if @page.save_with_content
|
||||
attachments = Attachment.attach_files(@page, params[:attachments])
|
||||
render_attachment_warning_if_needed(@page)
|
||||
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
|
||||
redirect_to :action => 'show', :project_id => @project, :id => @page.title
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title }
|
||||
format.api {
|
||||
if was_new_page
|
||||
render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title)
|
||||
else
|
||||
render_api_ok
|
||||
end
|
||||
}
|
||||
end
|
||||
else
|
||||
render :action => 'edit'
|
||||
respond_to do |format|
|
||||
format.html { render :action => 'edit' }
|
||||
format.api { render_validation_errors(@content) }
|
||||
end
|
||||
end
|
||||
|
||||
rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
|
||||
# Optimistic locking exception
|
||||
flash.now[:error] = l(:notice_locking_conflict)
|
||||
render :action => 'edit'
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
flash.now[:error] = l(:notice_locking_conflict)
|
||||
render :action => 'edit'
|
||||
}
|
||||
format.api { render_api_head :conflict }
|
||||
end
|
||||
rescue ActiveRecord::RecordNotSaved
|
||||
render :action => 'edit'
|
||||
respond_to do |format|
|
||||
format.html { render :action => 'edit' }
|
||||
format.api { render_validation_errors(@content) }
|
||||
end
|
||||
end
|
||||
|
||||
# rename a page
|
||||
@@ -187,7 +211,7 @@ class WikiController < ApplicationController
|
||||
# show page history
|
||||
def history
|
||||
@version_count = @page.content.versions.count
|
||||
@version_pages = Paginator.new self, @version_count, per_page_option, params['p']
|
||||
@version_pages = Paginator.new self, @version_count, per_page_option, params['page']
|
||||
# don't load text
|
||||
@versions = @page.content.versions.find :all,
|
||||
:select => "id, author_id, comments, updated_on, version",
|
||||
@@ -230,16 +254,28 @@ class WikiController < ApplicationController
|
||||
end
|
||||
else
|
||||
@reassignable_to = @wiki.pages - @page.self_and_descendants
|
||||
return
|
||||
# display the destroy form if it's a user request
|
||||
return unless api_request?
|
||||
end
|
||||
end
|
||||
@page.destroy
|
||||
redirect_to :action => 'index', :project_id => @project
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :action => 'index', :project_id => @project }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_version
|
||||
return render_403 unless editable?
|
||||
|
||||
@content = @page.content_for_version(params[:version])
|
||||
@content.destroy
|
||||
redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project
|
||||
end
|
||||
|
||||
# Export wiki to a single pdf or html file
|
||||
def export
|
||||
@pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
|
||||
@pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
export = render_to_string :action => 'export_multiple', :layout => false
|
||||
@@ -313,6 +349,6 @@ private
|
||||
end
|
||||
|
||||
def load_pages_for_index
|
||||
@pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
|
||||
@pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,8 +47,8 @@ module ApplicationHelper
|
||||
def link_to_user(user, options={})
|
||||
if user.is_a?(User)
|
||||
name = h(user.name(options[:format]))
|
||||
if user.active?
|
||||
link_to name, :controller => 'users', :action => 'show', :id => user
|
||||
if user.active? || (User.current.admin? && user.logged?)
|
||||
link_to name, user_path(user), :class => user.css_classes
|
||||
else
|
||||
name
|
||||
end
|
||||
@@ -64,10 +64,12 @@ module ApplicationHelper
|
||||
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
|
||||
# link_to_issue(issue, :subject => false) # => Defect #6
|
||||
# link_to_issue(issue, :project => true) # => Foo - Defect #6
|
||||
# link_to_issue(issue, :subject => false, :tracker => false) # => #6
|
||||
#
|
||||
def link_to_issue(issue, options={})
|
||||
title = nil
|
||||
subject = nil
|
||||
text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
|
||||
if options[:subject] == false
|
||||
title = truncate(issue.subject, :length => 60)
|
||||
else
|
||||
@@ -76,9 +78,7 @@ module ApplicationHelper
|
||||
subject = truncate(subject, :length => options[:truncate])
|
||||
end
|
||||
end
|
||||
s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
|
||||
:class => issue.css_classes,
|
||||
:title => title
|
||||
s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
|
||||
s << h(": #{subject}") if subject
|
||||
s = h("#{issue.project} - ") + s if options[:project]
|
||||
s
|
||||
@@ -147,6 +147,10 @@ module ApplicationHelper
|
||||
end
|
||||
end
|
||||
|
||||
def wiki_page_path(page, options={})
|
||||
url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
|
||||
end
|
||||
|
||||
def thumbnail_tag(attachment)
|
||||
link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
|
||||
{:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
|
||||
@@ -234,7 +238,7 @@ module ApplicationHelper
|
||||
content << "<ul class=\"pages-hierarchy\">\n"
|
||||
pages[node].each do |page|
|
||||
content << "<li>"
|
||||
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
|
||||
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
|
||||
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
|
||||
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
|
||||
content << "</li>\n"
|
||||
@@ -327,6 +331,15 @@ module ApplicationHelper
|
||||
s.html_safe
|
||||
end
|
||||
|
||||
# Options for the new membership projects combo-box
|
||||
def options_for_membership_project_select(principal, projects)
|
||||
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
|
||||
options << project_tree_options_for_select(projects) do |p|
|
||||
{:disabled => principal.projects.include?(p)}
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
# Truncates and returns the string as a single line
|
||||
def truncate_single_line(string, *args)
|
||||
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
|
||||
@@ -584,8 +597,9 @@ module ApplicationHelper
|
||||
|
||||
def parse_inline_attachments(text, project, obj, attr, only_path, options)
|
||||
# when using an image link, try to use an attachment, if possible
|
||||
if options[:attachments] || (obj && obj.respond_to?(:attachments))
|
||||
attachments = options[:attachments] || obj.attachments
|
||||
attachments = options[:attachments] || []
|
||||
attachments += obj.attachments if obj.respond_to?(:attachments)
|
||||
if attachments.present?
|
||||
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
|
||||
filename, ext, alt, alttext = $1.downcase, $2, $3, $4
|
||||
# search for the picture in attachments
|
||||
@@ -644,7 +658,7 @@ module ApplicationHelper
|
||||
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
|
||||
parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
|
||||
url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
|
||||
:id => wiki_page_id, :anchor => anchor, :parent => parent)
|
||||
:id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
|
||||
end
|
||||
end
|
||||
link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
|
||||
@@ -690,10 +704,11 @@ module ApplicationHelper
|
||||
# identifier:document:"Some document"
|
||||
# identifier:version:1.0.0
|
||||
# identifier:source:some/file
|
||||
def parse_redmine_links(text, project, obj, attr, only_path, options)
|
||||
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|
|
||||
def parse_redmine_links(text, default_project, obj, attr, only_path, options)
|
||||
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|
|
||||
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
|
||||
link = nil
|
||||
project = default_project
|
||||
if project_identifier
|
||||
project = Project.visible.find_by_identifier(project_identifier)
|
||||
end
|
||||
@@ -779,7 +794,7 @@ module ApplicationHelper
|
||||
when 'commit', 'source', 'export'
|
||||
if project
|
||||
repository = nil
|
||||
if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
|
||||
if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
|
||||
repo_prefix, repo_identifier, name = $1, $2, $3
|
||||
repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
|
||||
else
|
||||
@@ -795,11 +810,10 @@ module ApplicationHelper
|
||||
if repository && User.current.allowed_to?(:browse_repository, project)
|
||||
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
|
||||
path, rev, anchor = $1, $3, $5
|
||||
link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
|
||||
link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
|
||||
:path => to_path_param(path),
|
||||
:rev => rev,
|
||||
:anchor => anchor,
|
||||
:format => (prefix == 'export' ? 'raw' : nil)},
|
||||
:anchor => anchor},
|
||||
:class => (prefix == 'export' ? 'source download' : 'source')
|
||||
end
|
||||
end
|
||||
@@ -807,7 +821,7 @@ module ApplicationHelper
|
||||
end
|
||||
when 'attachment'
|
||||
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
|
||||
if attachments && attachment = attachments.detect {|a| a.filename == name }
|
||||
if attachments && attachment = Attachment.latest_attach(attachments, name)
|
||||
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
|
||||
:class => 'attachment'
|
||||
end
|
||||
@@ -959,8 +973,7 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def lang_options_for_select(blank=true)
|
||||
(blank ? [["(auto)", ""]] : []) +
|
||||
valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
|
||||
(blank ? [["(auto)", ""]] : []) + languages_options
|
||||
end
|
||||
|
||||
def label_tag_for(name, option_tags = nil, options = {})
|
||||
@@ -1029,6 +1042,11 @@ module ApplicationHelper
|
||||
content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
|
||||
end
|
||||
|
||||
# Helper to render JSON in views
|
||||
def raw_json(arg)
|
||||
arg.to_json.to_s.gsub('/', '\/').html_safe
|
||||
end
|
||||
|
||||
def back_url
|
||||
url = params[:back_url]
|
||||
if url.nil? && referer = request.env['HTTP_REFERER']
|
||||
@@ -1095,8 +1113,14 @@ module ApplicationHelper
|
||||
unless @calendar_headers_tags_included
|
||||
@calendar_headers_tags_included = true
|
||||
content_for :header_tags do
|
||||
start_of_week = Setting.start_of_week
|
||||
start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
|
||||
# Redmine uses 1..7 (monday..sunday) in settings and locales
|
||||
# JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
|
||||
start_of_week = start_of_week.to_i % 7
|
||||
|
||||
tags = javascript_tag(
|
||||
"var datepickerOptions={dateFormat: 'yy-mm-dd', " +
|
||||
"var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
|
||||
"showOn: 'button', buttonImageOnly: true, buttonImage: '" +
|
||||
path_to_image('/images/calendar.png') +
|
||||
"', showButtonPanel: true};")
|
||||
|
||||
@@ -26,8 +26,8 @@ module ContextMenusHelper
|
||||
end
|
||||
if options.delete(:disabled)
|
||||
options.delete(:method)
|
||||
options.delete(:confirm)
|
||||
options.delete(:onclick)
|
||||
options.delete(:data)
|
||||
options[:onclick] = 'return false;'
|
||||
options[:class] << ' disabled'
|
||||
url = '#'
|
||||
end
|
||||
|
||||
@@ -20,16 +20,7 @@
|
||||
module CustomFieldsHelper
|
||||
|
||||
def custom_fields_tabs
|
||||
tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
|
||||
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
|
||||
{:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
|
||||
{:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
|
||||
{:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
|
||||
{:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
|
||||
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
|
||||
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
|
||||
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
|
||||
]
|
||||
CustomField::CUSTOM_FIELDS_TABS
|
||||
end
|
||||
|
||||
# Return custom field html tag corresponding to its format
|
||||
|
||||
@@ -18,15 +18,6 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module GroupsHelper
|
||||
# Options for the new membership projects combo-box
|
||||
def options_for_membership_project_select(user, projects)
|
||||
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
|
||||
options << project_tree_options_for_select(projects) do |p|
|
||||
{:disabled => (user.projects.include?(p))}
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def group_settings_tabs
|
||||
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
|
||||
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
|
||||
|
||||
@@ -65,7 +65,7 @@ module IssuesHelper
|
||||
s = ''
|
||||
ancestors = issue.root? ? [] : issue.ancestors.visible.all
|
||||
ancestors.each do |ancestor|
|
||||
s << '<div>' + content_tag('p', link_to_issue(ancestor))
|
||||
s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
|
||||
end
|
||||
s << '<div>'
|
||||
subject = h(issue.subject)
|
||||
@@ -80,18 +80,29 @@ module IssuesHelper
|
||||
def render_descendants_tree(issue)
|
||||
s = '<form><table class="list issues">'
|
||||
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
|
||||
css = "issue issue-#{child.id} hascontextmenu"
|
||||
css << " idnt idnt-#{level}" if level > 0
|
||||
s << content_tag('tr',
|
||||
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
|
||||
content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
|
||||
content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
|
||||
content_tag('td', h(child.status)) +
|
||||
content_tag('td', link_to_user(child.assigned_to)) +
|
||||
content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
|
||||
:class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
|
||||
:class => css)
|
||||
end
|
||||
s << '</table></form>'
|
||||
s.html_safe
|
||||
end
|
||||
|
||||
# Returns a link for adding a new subtask to the given issue
|
||||
def link_to_new_subtask(issue)
|
||||
attrs = {
|
||||
:tracker_id => issue.tracker,
|
||||
:parent_issue_id => issue
|
||||
}
|
||||
link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
|
||||
end
|
||||
|
||||
class IssueFieldsRows
|
||||
include ActionView::Helpers::TagHelper
|
||||
|
||||
@@ -339,7 +350,10 @@ module IssuesHelper
|
||||
association = Issue.reflect_on_association(field.to_sym)
|
||||
if association
|
||||
record = association.class_name.constantize.find_by_id(id)
|
||||
return record.name if record
|
||||
if record
|
||||
record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding)
|
||||
return record.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -360,12 +374,16 @@ module IssuesHelper
|
||||
def issues_to_csv(issues, project, query, options={})
|
||||
decimal_separator = l(:general_csv_decimal_separator)
|
||||
encoding = l(:general_csv_encoding)
|
||||
columns = (options[:columns] == 'all' ? query.available_columns : query.columns)
|
||||
columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
|
||||
if options[:description]
|
||||
if description = query.available_columns.detect {|q| q.name == :description}
|
||||
columns << description
|
||||
end
|
||||
end
|
||||
|
||||
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
|
||||
# csv header fields
|
||||
csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } +
|
||||
(options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : [])
|
||||
csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
|
||||
|
||||
# csv lines
|
||||
issues.each do |issue|
|
||||
@@ -387,8 +405,7 @@ module IssuesHelper
|
||||
end
|
||||
s.to_s
|
||||
end
|
||||
csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } +
|
||||
(options[:description] ? [Redmine::CodesetUtil.from_utf8(issue.description, encoding)] : [])
|
||||
csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
|
||||
end
|
||||
end
|
||||
export
|
||||
|
||||
@@ -19,11 +19,43 @@
|
||||
|
||||
module QueriesHelper
|
||||
def filters_options_for_select(query)
|
||||
options_for_select(filters_options(query))
|
||||
end
|
||||
|
||||
def filters_options(query)
|
||||
options = [[]]
|
||||
options += query.available_filters.sort {|a,b| a[1][:order] <=> b[1][:order]}.map do |field, field_options|
|
||||
sorted_options = query.available_filters.sort do |a, b|
|
||||
ord = 0
|
||||
if !(a[1][:order] == 20 && b[1][:order] == 20)
|
||||
ord = a[1][:order] <=> b[1][:order]
|
||||
else
|
||||
cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
|
||||
CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
|
||||
if cn != 0
|
||||
ord = cn
|
||||
else
|
||||
f = (a[1][:field] <=> b[1][:field])
|
||||
if f != 0
|
||||
ord = f
|
||||
else
|
||||
# assigned_to or author
|
||||
ord = (a[0] <=> b[0])
|
||||
end
|
||||
end
|
||||
end
|
||||
ord
|
||||
end
|
||||
options += sorted_options.map do |field, field_options|
|
||||
[field_options[:name], field]
|
||||
end
|
||||
options_for_select(options)
|
||||
end
|
||||
|
||||
def available_block_columns_tags(query)
|
||||
tags = ''.html_safe
|
||||
query.available_block_columns.each do |column|
|
||||
tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
|
||||
end
|
||||
tags
|
||||
end
|
||||
|
||||
def column_header(column)
|
||||
@@ -35,7 +67,7 @@ module QueriesHelper
|
||||
def column_content(column, issue)
|
||||
value = column.value(issue)
|
||||
if value.is_a?(Array)
|
||||
value.collect {|v| column_value(column, issue, v)}.compact.sort.join(', ').html_safe
|
||||
value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
|
||||
else
|
||||
column_value(column, issue, value)
|
||||
end
|
||||
@@ -46,6 +78,8 @@ module QueriesHelper
|
||||
when 'String'
|
||||
if column.name == :subject
|
||||
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
|
||||
elsif column.name == :description
|
||||
issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
|
||||
else
|
||||
h(value)
|
||||
end
|
||||
@@ -53,14 +87,14 @@ module QueriesHelper
|
||||
format_time(value)
|
||||
when 'Date'
|
||||
format_date(value)
|
||||
when 'Fixnum', 'Float'
|
||||
when 'Fixnum'
|
||||
if column.name == :done_ratio
|
||||
progress_bar(value, :width => '80px')
|
||||
elsif column.name == :spent_hours
|
||||
sprintf "%.2f", value
|
||||
else
|
||||
h(value.to_s)
|
||||
value.to_s
|
||||
end
|
||||
when 'Float'
|
||||
sprintf "%.2f", value
|
||||
when 'User'
|
||||
link_to_user value
|
||||
when 'Project'
|
||||
@@ -73,6 +107,11 @@ module QueriesHelper
|
||||
l(:general_text_No)
|
||||
when 'Issue'
|
||||
link_to_issue(value, :subject => false)
|
||||
when 'IssueRelation'
|
||||
other = value.other_issue(issue)
|
||||
content_tag('span',
|
||||
(l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
|
||||
:class => value.css_classes_for(issue))
|
||||
else
|
||||
h(value)
|
||||
end
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
require 'iconv'
|
||||
require 'redmine/codeset_util'
|
||||
|
||||
module RepositoriesHelper
|
||||
def format_revision(revision)
|
||||
if revision.respond_to? :format_identifier
|
||||
@@ -253,16 +250,13 @@ module RepositoriesHelper
|
||||
|
||||
def index_commits(commits, heads)
|
||||
return nil if commits.nil? or commits.first.parents.nil?
|
||||
|
||||
refs_map = {}
|
||||
heads.each do |head|
|
||||
refs_map[head.scmid] ||= []
|
||||
refs_map[head.scmid] << head
|
||||
end
|
||||
|
||||
commits_by_scmid = {}
|
||||
commits.reverse.each_with_index do |commit, commit_index|
|
||||
|
||||
commits_by_scmid[commit.scmid] = {
|
||||
:parent_scmids => commit.parents.collect { |parent| parent.scmid },
|
||||
:rdmid => commit_index,
|
||||
@@ -271,38 +265,28 @@ module RepositoriesHelper
|
||||
:href => block_given? ? yield(commit.scmid) : commit.scmid
|
||||
}
|
||||
end
|
||||
|
||||
heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
|
||||
|
||||
space = nil
|
||||
heads.each do |head|
|
||||
if commits_by_scmid.include? head.scmid
|
||||
space = index_head((space || -1) + 1, head, commits_by_scmid)
|
||||
end
|
||||
end
|
||||
|
||||
# when no head matched anything use first commit
|
||||
space ||= index_head(0, commits.first, commits_by_scmid)
|
||||
|
||||
return commits_by_scmid, space
|
||||
end
|
||||
|
||||
def index_head(space, commit, commits_by_scmid)
|
||||
|
||||
stack = [[space, commits_by_scmid[commit.scmid]]]
|
||||
max_space = space
|
||||
|
||||
until stack.empty?
|
||||
space, commit = stack.pop
|
||||
commit[:space] = space if commit[:space].nil?
|
||||
|
||||
space -= 1
|
||||
commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
|
||||
|
||||
parent_commit = commits_by_scmid[parent_scmid]
|
||||
|
||||
if parent_commit and parent_commit[:space].nil?
|
||||
|
||||
stack.unshift [space += 1, parent_commit]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,7 +56,7 @@ module SettingsHelper
|
||||
Setting.send(setting).include?(value),
|
||||
:id => nil
|
||||
) + text.to_s,
|
||||
:class => 'block'
|
||||
:class => (options[:inline] ? 'inline' : 'block')
|
||||
)
|
||||
end.join.html_safe
|
||||
end
|
||||
@@ -91,4 +91,16 @@ module SettingsHelper
|
||||
l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
|
||||
:class => notifiable.parent.present? ? "parent" : '').html_safe
|
||||
end
|
||||
|
||||
def cross_project_subtasks_options
|
||||
options = [
|
||||
[:label_disabled, ''],
|
||||
[:label_cross_project_system, 'system'],
|
||||
[:label_cross_project_tree, 'tree'],
|
||||
[:label_cross_project_hierarchy, 'hierarchy'],
|
||||
[:label_cross_project_descendants, 'descendants']
|
||||
]
|
||||
|
||||
options.map {|label, value| [l(label), value.to_s]}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -89,6 +89,10 @@ module SortHelper
|
||||
sql.blank? ? nil : sql
|
||||
end
|
||||
|
||||
def to_a
|
||||
@criteria.dup
|
||||
end
|
||||
|
||||
def add!(key, asc)
|
||||
@criteria.delete_if {|k,o| k == key}
|
||||
@criteria = [[key, asc]] + @criteria
|
||||
@@ -182,6 +186,10 @@ module SortHelper
|
||||
@sort_criteria.to_sql
|
||||
end
|
||||
|
||||
def sort_criteria
|
||||
@sort_criteria
|
||||
end
|
||||
|
||||
# Returns a link which sorts by the named column.
|
||||
#
|
||||
# - column is the name of an attribute in the sorted record collection.
|
||||
|
||||
@@ -77,6 +77,7 @@ module TimelogHelper
|
||||
[l(:label_yesterday), 'yesterday'],
|
||||
[l(:label_this_week), 'current_week'],
|
||||
[l(:label_last_week), 'last_week'],
|
||||
[l(:label_last_n_weeks, 2), 'last_2_weeks'],
|
||||
[l(:label_last_n_days, 7), '7_days'],
|
||||
[l(:label_this_month), 'current_month'],
|
||||
[l(:label_last_month), 'last_month'],
|
||||
|
||||
@@ -26,15 +26,6 @@ module UsersHelper
|
||||
["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
|
||||
end
|
||||
|
||||
# Options for the new membership projects combo-box
|
||||
def options_for_membership_project_select(user, projects)
|
||||
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
|
||||
options << project_tree_options_for_select(projects) do |p|
|
||||
{:disabled => (user.projects.include?(p))}
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def user_mail_notification_options(user)
|
||||
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
|
||||
end
|
||||
|
||||
@@ -44,7 +44,8 @@ module VersionsHelper
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# When grouping by an association, Rails throws this exception if there's no result (bug)
|
||||
end
|
||||
counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
|
||||
# Sort with nil keys in last position
|
||||
counts = h.keys.sort {|a,b| a.nil? ? 1 : (b.nil? ? -1 : a <=> b)}.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
|
||||
max = counts.collect {|c| c[:total]}.max
|
||||
|
||||
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
|
||||
|
||||
@@ -37,7 +37,7 @@ module WikiHelper
|
||||
|
||||
def wiki_page_breadcrumb(page)
|
||||
breadcrumb(page.ancestors.reverse.collect {|parent|
|
||||
link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project})
|
||||
link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil})
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +30,34 @@ class CustomField < ActiveRecord::Base
|
||||
validate :validate_custom_field
|
||||
before_validation :set_searchable
|
||||
|
||||
CUSTOM_FIELDS_TABS = [
|
||||
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
|
||||
:label => :label_issue_plural},
|
||||
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
|
||||
:label => :label_spent_time},
|
||||
{:name => 'ProjectCustomField', :partial => 'custom_fields/index',
|
||||
:label => :label_project_plural},
|
||||
{:name => 'VersionCustomField', :partial => 'custom_fields/index',
|
||||
:label => :label_version_plural},
|
||||
{:name => 'UserCustomField', :partial => 'custom_fields/index',
|
||||
:label => :label_user_plural},
|
||||
{:name => 'GroupCustomField', :partial => 'custom_fields/index',
|
||||
:label => :label_group_plural},
|
||||
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
|
||||
:label => TimeEntryActivity::OptionName},
|
||||
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
|
||||
:label => IssuePriority::OptionName},
|
||||
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
|
||||
:label => DocumentCategory::OptionName}
|
||||
]
|
||||
|
||||
CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
|
||||
|
||||
def field_format=(arg)
|
||||
# cannot change format of a saved custom field
|
||||
super if new_record?
|
||||
end
|
||||
|
||||
def set_searchable
|
||||
# make sure these fields are not searchable
|
||||
self.searchable = false if %w(int float date bool).include?(field_format)
|
||||
@@ -130,7 +158,13 @@ class CustomField < ActiveRecord::Base
|
||||
possible_values_options = possible_values_options(customized)
|
||||
if possible_values_options.present?
|
||||
keyword = keyword.to_s.downcase
|
||||
possible_values_options.detect {|text, id| text.downcase == keyword}.try(:last)
|
||||
if v = possible_values_options.detect {|text, id| text.downcase == keyword}
|
||||
if v.is_a?(Array)
|
||||
v.last
|
||||
else
|
||||
v
|
||||
end
|
||||
end
|
||||
else
|
||||
keyword
|
||||
end
|
||||
|
||||
@@ -36,6 +36,7 @@ class Enumeration < ActiveRecord::Base
|
||||
validates_length_of :name, :maximum => 30
|
||||
|
||||
scope :shared, where(:project_id => nil)
|
||||
scope :sorted, order("#{table_name}.position ASC")
|
||||
scope :active, where(:active => true)
|
||||
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
class Issue < ActiveRecord::Base
|
||||
include Redmine::SafeAttributes
|
||||
include Redmine::Utils::DateCalculation
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :tracker
|
||||
@@ -28,6 +29,14 @@ class Issue < ActiveRecord::Base
|
||||
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
|
||||
|
||||
has_many :journals, :as => :journalized, :dependent => :destroy
|
||||
has_many :visible_journals,
|
||||
:class_name => 'Journal',
|
||||
:as => :journalized,
|
||||
:conditions => Proc.new {
|
||||
["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
|
||||
},
|
||||
:readonly => true
|
||||
|
||||
has_many :time_entries, :dependent => :delete_all
|
||||
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
|
||||
|
||||
@@ -39,7 +48,7 @@ class Issue < ActiveRecord::Base
|
||||
acts_as_customizable
|
||||
acts_as_watchable
|
||||
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
|
||||
:include => [:project, :journals],
|
||||
:include => [:project, :visible_journals],
|
||||
# sort by id so that limited eager loading doesn't break with postgresql
|
||||
:order_column => "#{table_name}.id"
|
||||
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
|
||||
@@ -52,6 +61,7 @@ class Issue < ActiveRecord::Base
|
||||
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
|
||||
|
||||
attr_reader :current_journal
|
||||
delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
|
||||
|
||||
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
|
||||
|
||||
@@ -84,17 +94,21 @@ class Issue < ActiveRecord::Base
|
||||
# Returns a SQL conditions string used to find all issues visible by the specified user
|
||||
def self.visible_condition(user, options={})
|
||||
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
|
||||
case role.issues_visibility
|
||||
when 'all'
|
||||
nil
|
||||
when 'default'
|
||||
user_ids = [user.id] + user.groups.map(&:id)
|
||||
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
||||
when 'own'
|
||||
user_ids = [user.id] + user.groups.map(&:id)
|
||||
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
||||
if user.logged?
|
||||
case role.issues_visibility
|
||||
when 'all'
|
||||
nil
|
||||
when 'default'
|
||||
user_ids = [user.id] + user.groups.map(&:id)
|
||||
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
||||
when 'own'
|
||||
user_ids = [user.id] + user.groups.map(&:id)
|
||||
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
||||
else
|
||||
'1=0'
|
||||
end
|
||||
else
|
||||
'1=0'
|
||||
"(#{table_name}.is_private = #{connection.quoted_false})"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -102,15 +116,19 @@ class Issue < ActiveRecord::Base
|
||||
# Returns true if usr or current user is allowed to view the issue
|
||||
def visible?(usr=nil)
|
||||
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
|
||||
case role.issues_visibility
|
||||
when 'all'
|
||||
true
|
||||
when 'default'
|
||||
!self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
|
||||
when 'own'
|
||||
self.author == user || user.is_or_belongs_to?(assigned_to)
|
||||
if user.logged?
|
||||
case role.issues_visibility
|
||||
when 'all'
|
||||
true
|
||||
when 'default'
|
||||
!self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
|
||||
when 'own'
|
||||
self.author == user || user.is_or_belongs_to?(assigned_to)
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
false
|
||||
!self.is_private?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -268,7 +286,8 @@ class Issue < ActiveRecord::Base
|
||||
if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
|
||||
self.fixed_version = nil
|
||||
end
|
||||
if parent && parent.project_id != project_id
|
||||
# Clear the parent task if it's no longer valid
|
||||
unless valid_parent_project?
|
||||
self.parent_issue_id = nil
|
||||
end
|
||||
@custom_field_values = nil
|
||||
@@ -327,6 +346,7 @@ class Issue < ActiveRecord::Base
|
||||
'custom_field_values',
|
||||
'custom_fields',
|
||||
'lock_version',
|
||||
'notes',
|
||||
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
|
||||
|
||||
safe_attributes 'status_id',
|
||||
@@ -334,8 +354,15 @@ class Issue < ActiveRecord::Base
|
||||
'fixed_version_id',
|
||||
'done_ratio',
|
||||
'lock_version',
|
||||
'notes',
|
||||
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
|
||||
|
||||
safe_attributes 'notes',
|
||||
:if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
|
||||
|
||||
safe_attributes 'private_notes',
|
||||
:if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
|
||||
|
||||
safe_attributes 'watcher_user_ids',
|
||||
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
|
||||
|
||||
@@ -390,7 +417,10 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
|
||||
if attrs['parent_issue_id'].present?
|
||||
attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
|
||||
s = attrs['parent_issue_id'].to_s
|
||||
unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
|
||||
@invalid_parent_issue_id = attrs.delete('parent_issue_id')
|
||||
end
|
||||
end
|
||||
|
||||
if attrs['custom_field_values'].present?
|
||||
@@ -496,11 +526,15 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def validate_issue
|
||||
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
|
||||
if due_date.nil? && @attributes['due_date'].present?
|
||||
errors.add :due_date, :not_a_date
|
||||
end
|
||||
|
||||
if self.due_date and self.start_date and self.due_date < self.start_date
|
||||
if start_date.nil? && @attributes['start_date'].present?
|
||||
errors.add :start_date, :not_a_date
|
||||
end
|
||||
|
||||
if due_date && start_date && due_date < start_date
|
||||
errors.add :due_date, :greater_than_start_date
|
||||
end
|
||||
|
||||
@@ -524,9 +558,11 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
|
||||
# Checks parent issue assignment
|
||||
if @parent_issue
|
||||
if @parent_issue.project_id != project_id
|
||||
errors.add :parent_issue_id, :not_same_project
|
||||
if @invalid_parent_issue_id.present?
|
||||
errors.add :parent_issue_id, :invalid
|
||||
elsif @parent_issue
|
||||
if !valid_parent_project?(@parent_issue)
|
||||
errors.add :parent_issue_id, :invalid
|
||||
elsif !new_record?
|
||||
# moving an existing issue
|
||||
if @parent_issue.root_id != root_id
|
||||
@@ -534,7 +570,7 @@ class Issue < ActiveRecord::Base
|
||||
elsif move_possible?(@parent_issue)
|
||||
# move accepted inside tree
|
||||
else
|
||||
errors.add :parent_issue_id, :not_a_valid_parent
|
||||
errors.add :parent_issue_id, :invalid
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -707,8 +743,8 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the mail adresses of users that should be notified
|
||||
def recipients
|
||||
# Returns the users that should be notified
|
||||
def notified_users
|
||||
notified = []
|
||||
# Author and assignee are always notified unless they have been
|
||||
# locked or don't want to be notified
|
||||
@@ -725,7 +761,12 @@ class Issue < ActiveRecord::Base
|
||||
notified.uniq!
|
||||
# Remove users that can not view the issue
|
||||
notified.reject! {|user| !visible?(user)}
|
||||
notified.collect(&:mail)
|
||||
notified
|
||||
end
|
||||
|
||||
# Returns the email addresses that should be notified
|
||||
def recipients
|
||||
notified_users.collect(&:mail)
|
||||
end
|
||||
|
||||
# Returns the number of hours spent on this issue
|
||||
@@ -744,7 +785,7 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def relations
|
||||
@relations ||= (relations_from + relations_to).sort
|
||||
@relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
|
||||
end
|
||||
|
||||
# Preloads relations for a collection of issues
|
||||
@@ -767,6 +808,25 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Preloads visible relations for a collection of issues
|
||||
def self.load_visible_relations(issues, user=User.current)
|
||||
if issues.any?
|
||||
issue_ids = issues.map(&:id)
|
||||
# Relations with issue_from in given issues and visible issue_to
|
||||
relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
|
||||
# Relations with issue_to in given issues and visible issue_from
|
||||
relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
|
||||
|
||||
issues.each do |issue|
|
||||
relations =
|
||||
relations_from.select {|relation| relation.issue_from_id == issue.id} +
|
||||
relations_to.select {|relation| relation.issue_to_id == issue.id}
|
||||
|
||||
issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Finds an issue relation given its id.
|
||||
def find_relation(relation_id)
|
||||
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
|
||||
@@ -804,29 +864,58 @@ class Issue < ActiveRecord::Base
|
||||
(start_date && due_date) ? due_date - start_date : 0
|
||||
end
|
||||
|
||||
def soonest_start
|
||||
# Returns the duration in working days
|
||||
def working_duration
|
||||
(start_date && due_date) ? working_days(start_date, due_date) : 0
|
||||
end
|
||||
|
||||
def soonest_start(reload=false)
|
||||
@soonest_start = nil if reload
|
||||
@soonest_start ||= (
|
||||
relations_to.collect{|relation| relation.successor_soonest_start} +
|
||||
relations_to(reload).collect{|relation| relation.successor_soonest_start} +
|
||||
ancestors.collect(&:soonest_start)
|
||||
).compact.max
|
||||
end
|
||||
|
||||
def reschedule_after(date)
|
||||
# Sets start_date on the given date or the next working day
|
||||
# and changes due_date to keep the same working duration.
|
||||
def reschedule_on(date)
|
||||
wd = working_duration
|
||||
date = next_working_date(date)
|
||||
self.start_date = date
|
||||
self.due_date = add_working_days(date, wd)
|
||||
end
|
||||
|
||||
# Reschedules the issue on the given date or the next working day and saves the record.
|
||||
# If the issue is a parent task, this is done by rescheduling its subtasks.
|
||||
def reschedule_on!(date)
|
||||
return if date.nil?
|
||||
if leaf?
|
||||
if start_date.nil? || start_date < date
|
||||
self.start_date, self.due_date = date, date + duration
|
||||
if start_date.nil? || start_date != date
|
||||
if start_date && start_date > date
|
||||
# Issue can not be moved earlier than its soonest start date
|
||||
date = [soonest_start(true), date].compact.max
|
||||
end
|
||||
reschedule_on(date)
|
||||
begin
|
||||
save
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
reload
|
||||
self.start_date, self.due_date = date, date + duration
|
||||
reschedule_on(date)
|
||||
save
|
||||
end
|
||||
end
|
||||
else
|
||||
leaves.each do |leaf|
|
||||
leaf.reschedule_after(date)
|
||||
if leaf.start_date
|
||||
# Only move subtask if it starts at the same date as the parent
|
||||
# or if it starts before the given date
|
||||
if start_date == leaf.start_date || date > leaf.start_date
|
||||
leaf.reschedule_on!(date)
|
||||
end
|
||||
else
|
||||
leaf.reschedule_on!(date)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -847,7 +936,7 @@ class Issue < ActiveRecord::Base
|
||||
|
||||
# Returns a string of css classes that apply to the issue
|
||||
def css_classes
|
||||
s = "issue status-#{status_id} priority-#{priority_id}"
|
||||
s = "issue status-#{status_id} #{priority.try(:css_classes)}"
|
||||
s << ' closed' if closed?
|
||||
s << ' overdue' if overdue?
|
||||
s << ' child' if child?
|
||||
@@ -897,23 +986,44 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def parent_issue_id=(arg)
|
||||
parent_issue_id = arg.blank? ? nil : arg.to_i
|
||||
if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
|
||||
s = arg.to_s.strip.presence
|
||||
if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
|
||||
@parent_issue.id
|
||||
else
|
||||
@parent_issue = nil
|
||||
nil
|
||||
@invalid_parent_issue_id = arg
|
||||
end
|
||||
end
|
||||
|
||||
def parent_issue_id
|
||||
if instance_variable_defined? :@parent_issue
|
||||
if @invalid_parent_issue_id
|
||||
@invalid_parent_issue_id
|
||||
elsif instance_variable_defined? :@parent_issue
|
||||
@parent_issue.nil? ? nil : @parent_issue.id
|
||||
else
|
||||
parent_id
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if issue's project is a valid
|
||||
# parent issue project
|
||||
def valid_parent_project?(issue=parent)
|
||||
return true if issue.nil? || issue.project_id == project_id
|
||||
|
||||
case Setting.cross_project_subtasks
|
||||
when 'system'
|
||||
true
|
||||
when 'tree'
|
||||
issue.project.root == project.root
|
||||
when 'hierarchy'
|
||||
issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
|
||||
when 'descendants'
|
||||
issue.project.is_or_is_ancestor_of?(project)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Extracted from the ReportsController.
|
||||
def self.by_tracker(project)
|
||||
count_and_group_by(:project => project,
|
||||
@@ -993,8 +1103,9 @@ class Issue < ActiveRecord::Base
|
||||
relations_to.clear
|
||||
end
|
||||
|
||||
# Move subtasks
|
||||
# Move subtasks that were in the same project
|
||||
children.each do |child|
|
||||
next unless child.project_id == project_id_was
|
||||
# Change project and keep project
|
||||
child.send :project=, project, true
|
||||
unless child.save
|
||||
@@ -1003,11 +1114,20 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Copies subtasks from the copied issue
|
||||
# Callback for after the creation of an issue by copy
|
||||
# * adds a "copied to" relation with the copied issue
|
||||
# * copies subtasks from the copied issue
|
||||
def after_create_from_copy
|
||||
return unless copy?
|
||||
return unless copy? && !@after_create_from_copy_handled
|
||||
|
||||
unless @copied_from.leaf? || @copy_options[:subtasks] == false || @subtasks_copied
|
||||
if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
|
||||
relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
|
||||
unless relation.save
|
||||
logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
|
||||
end
|
||||
end
|
||||
|
||||
unless @copied_from.leaf? || @copy_options[:subtasks] == false
|
||||
@copied_from.children.each do |child|
|
||||
unless child.visible?
|
||||
# Do not copy subtasks that are not visible to avoid potential disclosure of private data
|
||||
@@ -1023,8 +1143,8 @@ class Issue < ActiveRecord::Base
|
||||
logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
|
||||
end
|
||||
end
|
||||
@subtasks_copied = true
|
||||
end
|
||||
@after_create_from_copy_handled = true
|
||||
end
|
||||
|
||||
def update_nested_set_attributes
|
||||
@@ -1135,7 +1255,7 @@ class Issue < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Callback on attachment deletion
|
||||
# Callback on file attachment
|
||||
def attachment_added(obj)
|
||||
if @current_journal && !obj.new_record?
|
||||
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
|
||||
|
||||
@@ -27,7 +27,7 @@ class IssueCategory < ActiveRecord::Base
|
||||
|
||||
safe_attributes 'name', 'assigned_to_id'
|
||||
|
||||
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
|
||||
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||
|
||||
alias :destroy_without_reassign :destroy
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
class IssuePriority < Enumeration
|
||||
has_many :issues, :foreign_key => 'priority_id'
|
||||
|
||||
after_destroy {|priority| priority.class.compute_position_names}
|
||||
after_save {|priority| priority.class.compute_position_names if priority.position_changed? && priority.position}
|
||||
|
||||
OptionName = :enumeration_issue_priorities
|
||||
|
||||
def option_name
|
||||
@@ -31,4 +34,35 @@ class IssuePriority < Enumeration
|
||||
def transfer_relations(to)
|
||||
issues.update_all("priority_id = #{to.id}")
|
||||
end
|
||||
|
||||
def css_classes
|
||||
"priority-#{id} priority-#{position_name}"
|
||||
end
|
||||
|
||||
# Clears position_name for all priorities
|
||||
# Called from migration 20121026003537_populate_enumerations_position_name
|
||||
def self.clear_position_names
|
||||
update_all :position_name => nil
|
||||
end
|
||||
|
||||
# Updates position_name for active priorities
|
||||
# Called from migration 20121026003537_populate_enumerations_position_name
|
||||
def self.compute_position_names
|
||||
priorities = where(:active => true).all.sort_by(&:position)
|
||||
if priorities.any?
|
||||
default = priorities.detect(&:is_default?) || priorities[(priorities.size - 1) / 2]
|
||||
priorities.each_with_index do |priority, index|
|
||||
name = case
|
||||
when priority.position == default.position
|
||||
"default"
|
||||
when priority.position < default.position
|
||||
index == 0 ? "lowest" : "low#{index+1}"
|
||||
else
|
||||
index == (priorities.size - 1) ? "highest" : "high#{priorities.size - index}"
|
||||
end
|
||||
|
||||
update_all({:position_name => name}, :id => priority.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,20 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Class used to represent the relations of an issue
|
||||
class IssueRelations < Array
|
||||
include Redmine::I18n
|
||||
|
||||
def initialize(issue, *args)
|
||||
@issue = issue
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def to_s(*args)
|
||||
map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
|
||||
end
|
||||
end
|
||||
|
||||
class IssueRelation < ActiveRecord::Base
|
||||
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
|
||||
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
|
||||
@@ -26,25 +40,37 @@ class IssueRelation < ActiveRecord::Base
|
||||
TYPE_BLOCKED = "blocked"
|
||||
TYPE_PRECEDES = "precedes"
|
||||
TYPE_FOLLOWS = "follows"
|
||||
TYPE_COPIED_TO = "copied_to"
|
||||
TYPE_COPIED_FROM = "copied_from"
|
||||
|
||||
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES },
|
||||
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
|
||||
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
|
||||
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED },
|
||||
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
|
||||
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
|
||||
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }
|
||||
}.freeze
|
||||
TYPES = {
|
||||
TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
|
||||
:order => 1, :sym => TYPE_RELATES },
|
||||
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
|
||||
:order => 2, :sym => TYPE_DUPLICATED },
|
||||
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
|
||||
:order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
|
||||
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by,
|
||||
:order => 4, :sym => TYPE_BLOCKED },
|
||||
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks,
|
||||
:order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
|
||||
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows,
|
||||
:order => 6, :sym => TYPE_FOLLOWS },
|
||||
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes,
|
||||
:order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
|
||||
TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from,
|
||||
:order => 8, :sym => TYPE_COPIED_FROM },
|
||||
TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to,
|
||||
:order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
|
||||
}.freeze
|
||||
|
||||
validates_presence_of :issue_from, :issue_to, :relation_type
|
||||
validates_inclusion_of :relation_type, :in => TYPES.keys
|
||||
validates_numericality_of :delay, :allow_nil => true
|
||||
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
|
||||
|
||||
validate :validate_issue_relation
|
||||
|
||||
attr_protected :issue_from_id, :issue_to_id
|
||||
|
||||
before_save :handle_issue_order
|
||||
|
||||
def visible?(user=User.current)
|
||||
@@ -69,14 +95,19 @@ class IssueRelation < ActiveRecord::Base
|
||||
def validate_issue_relation
|
||||
if issue_from && issue_to
|
||||
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
|
||||
errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
|
||||
#detect circular dependencies depending wether the relation should be reversed
|
||||
unless issue_from.project_id == issue_to.project_id ||
|
||||
Setting.cross_project_issue_relations?
|
||||
errors.add :issue_to_id, :not_same_project
|
||||
end
|
||||
# detect circular dependencies depending wether the relation should be reversed
|
||||
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
|
||||
errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
|
||||
else
|
||||
errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
|
||||
end
|
||||
errors.add :base, :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
|
||||
if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
|
||||
errors.add :base, :cant_link_an_issue_with_a_descendant
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -96,7 +127,13 @@ class IssueRelation < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def label_for(issue)
|
||||
TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
|
||||
TYPES[relation_type] ?
|
||||
TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] :
|
||||
:unknow
|
||||
end
|
||||
|
||||
def css_classes_for(issue)
|
||||
"rel-#{relation_type_for(issue)}"
|
||||
end
|
||||
|
||||
def handle_issue_order
|
||||
@@ -113,18 +150,20 @@ class IssueRelation < ActiveRecord::Base
|
||||
def set_issue_to_dates
|
||||
soonest_start = self.successor_soonest_start
|
||||
if soonest_start && issue_to
|
||||
issue_to.reschedule_after(soonest_start)
|
||||
issue_to.reschedule_on!(soonest_start)
|
||||
end
|
||||
end
|
||||
|
||||
def successor_soonest_start
|
||||
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date)
|
||||
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
|
||||
(issue_from.start_date || issue_from.due_date)
|
||||
(issue_from.due_date || issue_from.start_date) + 1 + delay
|
||||
end
|
||||
end
|
||||
|
||||
def <=>(relation)
|
||||
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
|
||||
r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
|
||||
r == 0 ? id <=> relation.id : r
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -29,7 +29,7 @@ class IssueStatus < ActiveRecord::Base
|
||||
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
|
||||
|
||||
scope :sorted, order("#{table_name}.position ASC")
|
||||
scope :named, lambda {|arg| where(["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip])}
|
||||
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||
|
||||
def update_default
|
||||
IssueStatus.update_all({:is_default => false}, ['id <> ?', id]) if self.is_default?
|
||||
|
||||
@@ -37,10 +37,15 @@ class Journal < ActiveRecord::Base
|
||||
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
|
||||
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
|
||||
|
||||
scope :visible, lambda {|*args| {
|
||||
:include => {:issue => :project},
|
||||
:conditions => Issue.visible_condition(args.shift || User.current, *args)
|
||||
}}
|
||||
before_create :split_private_notes
|
||||
|
||||
scope :visible, lambda {|*args|
|
||||
user = args.shift || User.current
|
||||
|
||||
includes(:issue => :project).
|
||||
where(Issue.visible_condition(user, *args)).
|
||||
where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false)
|
||||
}
|
||||
|
||||
def save(*args)
|
||||
# Do not save an empty journal
|
||||
@@ -75,6 +80,7 @@ class Journal < ActiveRecord::Base
|
||||
s = 'journal'
|
||||
s << ' has-notes' unless notes.blank?
|
||||
s << ' has-details' unless details.blank?
|
||||
s << ' private-notes' if private_notes?
|
||||
s
|
||||
end
|
||||
|
||||
@@ -85,4 +91,41 @@ class Journal < ActiveRecord::Base
|
||||
def notify=(arg)
|
||||
@notify = arg
|
||||
end
|
||||
|
||||
def recipients
|
||||
notified = journalized.notified_users
|
||||
if private_notes?
|
||||
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
||||
end
|
||||
notified.map(&:mail)
|
||||
end
|
||||
|
||||
def watcher_recipients
|
||||
notified = journalized.notified_watchers
|
||||
if private_notes?
|
||||
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
||||
end
|
||||
notified.map(&:mail)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def split_private_notes
|
||||
if private_notes?
|
||||
if notes.present?
|
||||
if details.any?
|
||||
# Split the journal (notes/changes) so we don't have half-private journals
|
||||
journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false)
|
||||
journal.details = details
|
||||
journal.save
|
||||
self.details = []
|
||||
self.created_on = journal.created_on
|
||||
end
|
||||
else
|
||||
# Blank notes should not be private
|
||||
self.private_notes = false
|
||||
end
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -124,6 +124,7 @@ class MailHandler < ActionMailer::Base
|
||||
|
||||
def dispatch
|
||||
headers = [email.in_reply_to, email.references].flatten.compact
|
||||
subject = email.subject.to_s
|
||||
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
|
||||
klass, object_id = $1, $2.to_i
|
||||
method_name = "receive_#{klass}_reply"
|
||||
@@ -132,9 +133,9 @@ class MailHandler < ActionMailer::Base
|
||||
else
|
||||
# ignoring it
|
||||
end
|
||||
elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
|
||||
elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
|
||||
receive_issue_reply(m[1].to_i)
|
||||
elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
|
||||
elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
|
||||
receive_message_reply(m[1].to_i)
|
||||
else
|
||||
dispatch_to_default
|
||||
@@ -181,7 +182,7 @@ class MailHandler < ActionMailer::Base
|
||||
end
|
||||
|
||||
# Adds a note to an existing issue
|
||||
def receive_issue_reply(issue_id)
|
||||
def receive_issue_reply(issue_id, from_journal=nil)
|
||||
issue = Issue.find_by_id(issue_id)
|
||||
return unless issue
|
||||
# check permission
|
||||
@@ -196,6 +197,10 @@ class MailHandler < ActionMailer::Base
|
||||
@@handler_options[:issue].clear
|
||||
|
||||
journal = issue.init_journal(user)
|
||||
if from_journal && from_journal.private_notes?
|
||||
# If the received email was a reply to a private note, make the added note private
|
||||
issue.private_notes = true
|
||||
end
|
||||
issue.safe_attributes = issue_attributes_from_keywords(issue)
|
||||
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
|
||||
journal.notes = cleaned_up_text_body
|
||||
@@ -211,7 +216,7 @@ class MailHandler < ActionMailer::Base
|
||||
def receive_journal_reply(journal_id)
|
||||
journal = Journal.find_by_id(journal_id)
|
||||
if journal && journal.journalized_type == 'Issue'
|
||||
receive_issue_reply(journal.journalized_id)
|
||||
receive_issue_reply(journal.journalized_id, journal)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -369,18 +374,6 @@ class MailHandler < ActionMailer::Base
|
||||
|
||||
def cleaned_up_subject
|
||||
subject = email.subject.to_s
|
||||
unless subject.respond_to?(:encoding)
|
||||
# try to reencode to utf8 manually with ruby1.8
|
||||
begin
|
||||
if h = email.header[:subject]
|
||||
if m = h.value.match(/^=\?([^\?]+)\?/)
|
||||
subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# nop
|
||||
end
|
||||
end
|
||||
subject.strip[0,255]
|
||||
end
|
||||
|
||||
|
||||
@@ -62,9 +62,9 @@ class Mailer < ActionMailer::Base
|
||||
message_id journal
|
||||
references issue
|
||||
@author = journal.user
|
||||
recipients = issue.recipients
|
||||
recipients = journal.recipients
|
||||
# Watchers in cc
|
||||
cc = issue.watcher_recipients - recipients
|
||||
cc = journal.watcher_recipients - recipients
|
||||
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
|
||||
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
|
||||
s << issue.subject
|
||||
|
||||
@@ -27,19 +27,20 @@ class Principal < ActiveRecord::Base
|
||||
scope :active, :conditions => "#{Principal.table_name}.status = 1"
|
||||
|
||||
scope :like, lambda {|q|
|
||||
q = q.to_s
|
||||
if q.blank?
|
||||
{}
|
||||
where({})
|
||||
else
|
||||
q = q.to_s.downcase
|
||||
pattern = "%#{q}%"
|
||||
sql = "LOWER(login) LIKE :p OR LOWER(firstname) LIKE :p OR LOWER(lastname) LIKE :p OR LOWER(mail) LIKE :p"
|
||||
sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
|
||||
params = {:p => pattern}
|
||||
if q =~ /^(.+)\s+(.+)$/
|
||||
a, b = "#{$1}%", "#{$2}%"
|
||||
sql << " OR (LOWER(firstname) LIKE :a AND LOWER(lastname) LIKE :b) OR (LOWER(firstname) LIKE :b AND LOWER(lastname) LIKE :a)"
|
||||
sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))"
|
||||
sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))"
|
||||
params.merge!(:a => a, :b => b)
|
||||
end
|
||||
{:conditions => [sql, params]}
|
||||
where(sql, params)
|
||||
end
|
||||
}
|
||||
|
||||
@@ -47,20 +48,20 @@ class Principal < ActiveRecord::Base
|
||||
scope :member_of, lambda {|projects|
|
||||
projects = [projects] unless projects.is_a?(Array)
|
||||
if projects.empty?
|
||||
{:conditions => "1=0"}
|
||||
where("1=0")
|
||||
else
|
||||
ids = projects.map(&:id)
|
||||
{:conditions => ["#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
|
||||
where("#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
|
||||
end
|
||||
}
|
||||
# Principals that are not members of projects
|
||||
scope :not_member_of, lambda {|projects|
|
||||
projects = [projects] unless projects.is_a?(Array)
|
||||
if projects.empty?
|
||||
{:conditions => "1=0"}
|
||||
where("1=0")
|
||||
else
|
||||
ids = projects.map(&:id)
|
||||
{:conditions => ["#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
|
||||
where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids)
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class Project < ActiveRecord::Base
|
||||
|
||||
# Specific overidden Activities
|
||||
has_many :time_entry_activities
|
||||
has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
|
||||
has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
|
||||
has_many :memberships, :class_name => 'Member'
|
||||
has_many :member_principals, :class_name => 'Member',
|
||||
:include => :principal,
|
||||
@@ -393,6 +393,16 @@ class Project < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Recalculates all lft and rgt values based on project names
|
||||
# Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
|
||||
# Used in BuildProjectsTree migration
|
||||
def self.rebuild_tree!
|
||||
transaction do
|
||||
update_all "lft = NULL, rgt = NULL"
|
||||
rebuild!(false)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an array of the trackers used by the project and its active sub projects
|
||||
def rolled_up_trackers
|
||||
@rolled_up_trackers ||=
|
||||
@@ -472,7 +482,7 @@ class Project < ActiveRecord::Base
|
||||
# Returns the users that should be notified on project events
|
||||
def notified_users
|
||||
# TODO: User part should be extracted to User#notify_about?
|
||||
members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
|
||||
members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
|
||||
end
|
||||
|
||||
# Returns an array of all custom fields enabled for project issues
|
||||
@@ -721,7 +731,7 @@ class Project < ActiveRecord::Base
|
||||
def copy_wiki(project)
|
||||
# Check that the source project has a wiki first
|
||||
unless project.wiki.nil?
|
||||
self.wiki ||= Wiki.new
|
||||
wiki = self.wiki || Wiki.new
|
||||
wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
|
||||
wiki_pages_map = {}
|
||||
project.wiki.pages.each do |page|
|
||||
@@ -733,6 +743,8 @@ class Project < ActiveRecord::Base
|
||||
wiki.pages << new_wiki_page
|
||||
wiki_pages_map[page.id] = new_wiki_page
|
||||
end
|
||||
|
||||
self.wiki = wiki
|
||||
wiki.save
|
||||
# Reproduce page hierarchy
|
||||
project.wiki.pages.each do |page|
|
||||
@@ -778,7 +790,7 @@ class Project < ActiveRecord::Base
|
||||
# get copied before their children
|
||||
project.issues.find(:all, :order => 'root_id, lft').each do |issue|
|
||||
new_issue = Issue.new
|
||||
new_issue.copy_from(issue, :subtasks => false)
|
||||
new_issue.copy_from(issue, :subtasks => false, :link => false)
|
||||
new_issue.project = self
|
||||
# Reassign fixed_versions by name, since names are unique per project
|
||||
if issue.fixed_version && issue.fixed_version.project == project
|
||||
|
||||
@@ -27,6 +27,7 @@ class QueryColumn
|
||||
self.groupable = name.to_s
|
||||
end
|
||||
self.default_order = options[:default_order]
|
||||
@inline = options.key?(:inline) ? options[:inline] : true
|
||||
@caption_key = options[:caption] || "field_#{name}"
|
||||
end
|
||||
|
||||
@@ -38,11 +39,15 @@ class QueryColumn
|
||||
def sortable?
|
||||
!@sortable.nil?
|
||||
end
|
||||
|
||||
|
||||
def sortable
|
||||
@sortable.is_a?(Proc) ? @sortable.call : @sortable
|
||||
end
|
||||
|
||||
def inline?
|
||||
@inline
|
||||
end
|
||||
|
||||
def value(issue)
|
||||
issue.send name
|
||||
end
|
||||
@@ -58,6 +63,7 @@ class QueryCustomFieldColumn < QueryColumn
|
||||
self.name = "cf_#{custom_field.id}".to_sym
|
||||
self.sortable = custom_field.order_statement || false
|
||||
self.groupable = custom_field.group_statement || false
|
||||
@inline = true
|
||||
@cf = custom_field
|
||||
end
|
||||
|
||||
@@ -71,7 +77,7 @@ class QueryCustomFieldColumn < QueryColumn
|
||||
|
||||
def value(issue)
|
||||
cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
|
||||
cv.size > 1 ? cv : cv.first
|
||||
cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
|
||||
end
|
||||
|
||||
def css_classes
|
||||
@@ -100,20 +106,25 @@ class Query < ActiveRecord::Base
|
||||
"o" => :label_open_issues,
|
||||
"c" => :label_closed_issues,
|
||||
"!*" => :label_none,
|
||||
"*" => :label_all,
|
||||
"*" => :label_any,
|
||||
">=" => :label_greater_or_equal,
|
||||
"<=" => :label_less_or_equal,
|
||||
"><" => :label_between,
|
||||
"<t+" => :label_in_less_than,
|
||||
">t+" => :label_in_more_than,
|
||||
"><t+"=> :label_in_the_next_days,
|
||||
"t+" => :label_in,
|
||||
"t" => :label_today,
|
||||
"w" => :label_this_week,
|
||||
">t-" => :label_less_than_ago,
|
||||
"<t-" => :label_more_than_ago,
|
||||
"><t-"=> :label_in_the_past_days,
|
||||
"t-" => :label_ago,
|
||||
"~" => :label_contains,
|
||||
"!~" => :label_not_contains }
|
||||
"!~" => :label_not_contains,
|
||||
"=p" => :label_any_issues_in_project,
|
||||
"=!p" => :label_any_issues_not_in_project,
|
||||
"!p" => :label_no_issues_in_project}
|
||||
|
||||
cattr_reader :operators
|
||||
|
||||
@@ -121,12 +132,13 @@ class Query < ActiveRecord::Base
|
||||
:list_status => [ "o", "=", "!", "c", "*" ],
|
||||
:list_optional => [ "=", "!", "!*", "*" ],
|
||||
:list_subprojects => [ "*", "!*", "=" ],
|
||||
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
|
||||
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
|
||||
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
|
||||
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
|
||||
:string => [ "=", "~", "!", "!~", "!*", "*" ],
|
||||
:text => [ "~", "!~", "!*", "*" ],
|
||||
:integer => [ "=", ">=", "<=", "><", "!*", "*" ],
|
||||
:float => [ "=", ">=", "<=", "><", "!*", "*" ] }
|
||||
:float => [ "=", ">=", "<=", "><", "!*", "*" ],
|
||||
:relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
|
||||
|
||||
cattr_reader :operators_by_filter_type
|
||||
|
||||
@@ -147,6 +159,8 @@ class Query < ActiveRecord::Base
|
||||
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
|
||||
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
|
||||
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
|
||||
QueryColumn.new(:relations, :caption => :label_related_issues),
|
||||
QueryColumn.new(:description, :inline => false)
|
||||
]
|
||||
cattr_reader :available_columns
|
||||
|
||||
@@ -178,7 +192,7 @@ class Query < ActiveRecord::Base
|
||||
case operator_for(field)
|
||||
when "=", ">=", "<=", "><"
|
||||
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
|
||||
when ">t-", "<t-", "t-"
|
||||
when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
|
||||
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
|
||||
end
|
||||
end
|
||||
@@ -221,44 +235,57 @@ class Query < ActiveRecord::Base
|
||||
|
||||
def available_filters
|
||||
return @available_filters if @available_filters
|
||||
|
||||
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
|
||||
"tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
|
||||
"priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
|
||||
"subject" => { :type => :text, :order => 8 },
|
||||
"created_on" => { :type => :date_past, :order => 9 },
|
||||
"updated_on" => { :type => :date_past, :order => 10 },
|
||||
"start_date" => { :type => :date, :order => 11 },
|
||||
"due_date" => { :type => :date, :order => 12 },
|
||||
"estimated_hours" => { :type => :float, :order => 13 },
|
||||
"done_ratio" => { :type => :integer, :order => 14 }}
|
||||
|
||||
@available_filters = {
|
||||
"status_id" => {
|
||||
:type => :list_status, :order => 0,
|
||||
:values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }
|
||||
},
|
||||
"tracker_id" => {
|
||||
:type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
|
||||
},
|
||||
"priority_id" => {
|
||||
:type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
|
||||
},
|
||||
"subject" => { :type => :text, :order => 8 },
|
||||
"created_on" => { :type => :date_past, :order => 9 },
|
||||
"updated_on" => { :type => :date_past, :order => 10 },
|
||||
"start_date" => { :type => :date, :order => 11 },
|
||||
"due_date" => { :type => :date, :order => 12 },
|
||||
"estimated_hours" => { :type => :float, :order => 13 },
|
||||
"done_ratio" => { :type => :integer, :order => 14 }
|
||||
}
|
||||
IssueRelation::TYPES.each do |relation_type, options|
|
||||
@available_filters[relation_type] = {
|
||||
:type => :relation, :order => @available_filters.size + 100,
|
||||
:label => options[:name]
|
||||
}
|
||||
end
|
||||
principals = []
|
||||
if project
|
||||
principals += project.principals.sort
|
||||
unless project.leaf?
|
||||
subprojects = project.descendants.visible.all
|
||||
if subprojects.any?
|
||||
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
|
||||
@available_filters["subproject_id"] = {
|
||||
:type => :list_subprojects, :order => 13,
|
||||
:values => subprojects.collect{|s| [s.name, s.id.to_s] }
|
||||
}
|
||||
principals += Principal.member_of(subprojects)
|
||||
end
|
||||
end
|
||||
else
|
||||
all_projects = Project.visible.all
|
||||
if all_projects.any?
|
||||
# members of visible projects
|
||||
principals += Principal.member_of(all_projects)
|
||||
|
||||
# project filter
|
||||
project_values = []
|
||||
if User.current.logged? && User.current.memberships.any?
|
||||
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
|
||||
end
|
||||
Project.project_tree(all_projects) do |p, level|
|
||||
prefix = (level > 0 ? ('--' * level + ' ') : '')
|
||||
project_values << ["#{prefix}#{p.name}", p.id.to_s]
|
||||
end
|
||||
@available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
|
||||
project_values += all_projects_values
|
||||
@available_filters["project_id"] = {
|
||||
:type => :list, :order => 1, :values => project_values
|
||||
} unless project_values.empty?
|
||||
end
|
||||
end
|
||||
principals.uniq!
|
||||
@@ -267,63 +294,88 @@ class Query < ActiveRecord::Base
|
||||
|
||||
assigned_to_values = []
|
||||
assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||
assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
|
||||
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
|
||||
assigned_to_values += (Setting.issue_group_assignment? ?
|
||||
principals : users).collect{|s| [s.name, s.id.to_s] }
|
||||
@available_filters["assigned_to_id"] = {
|
||||
:type => :list_optional, :order => 4, :values => assigned_to_values
|
||||
} unless assigned_to_values.empty?
|
||||
|
||||
author_values = []
|
||||
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||
author_values += users.collect{|s| [s.name, s.id.to_s] }
|
||||
@available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
|
||||
@available_filters["author_id"] = {
|
||||
:type => :list, :order => 5, :values => author_values
|
||||
} unless author_values.empty?
|
||||
|
||||
group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
|
||||
@available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
|
||||
@available_filters["member_of_group"] = {
|
||||
:type => :list_optional, :order => 6, :values => group_values
|
||||
} unless group_values.empty?
|
||||
|
||||
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
|
||||
@available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
|
||||
@available_filters["assigned_to_role"] = {
|
||||
:type => :list_optional, :order => 7, :values => role_values
|
||||
} unless role_values.empty?
|
||||
|
||||
if User.current.logged?
|
||||
@available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
|
||||
@available_filters["watcher_id"] = {
|
||||
:type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
|
||||
}
|
||||
end
|
||||
|
||||
if project
|
||||
# project specific filters
|
||||
categories = project.issue_categories.all
|
||||
unless categories.empty?
|
||||
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
|
||||
@available_filters["category_id"] = {
|
||||
:type => :list_optional, :order => 6,
|
||||
:values => categories.collect{|s| [s.name, s.id.to_s] }
|
||||
}
|
||||
end
|
||||
versions = project.shared_versions.all
|
||||
unless versions.empty?
|
||||
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
|
||||
@available_filters["fixed_version_id"] = {
|
||||
:type => :list_optional, :order => 7,
|
||||
:values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
|
||||
}
|
||||
end
|
||||
add_custom_fields_filters(project.all_issue_custom_fields)
|
||||
else
|
||||
# global filters for cross project issue list
|
||||
system_shared_versions = Version.visible.find_all_by_sharing('system')
|
||||
unless system_shared_versions.empty?
|
||||
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
|
||||
@available_filters["fixed_version_id"] = {
|
||||
:type => :list_optional, :order => 7,
|
||||
:values => system_shared_versions.sort.collect{|s|
|
||||
["#{s.project.name} - #{s.name}", s.id.to_s]
|
||||
}
|
||||
}
|
||||
end
|
||||
add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
|
||||
add_custom_fields_filters(
|
||||
IssueCustomField.find(:all,
|
||||
:conditions => {
|
||||
:is_filter => true,
|
||||
:is_for_all => true
|
||||
}))
|
||||
end
|
||||
|
||||
add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
|
||||
|
||||
if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
|
||||
User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
|
||||
@available_filters["is_private"] = { :type => :list, :order => 15, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
|
||||
@available_filters["is_private"] = {
|
||||
:type => :list, :order => 16,
|
||||
:values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
|
||||
}
|
||||
end
|
||||
|
||||
Tracker.disabled_core_fields(trackers).each {|field|
|
||||
@available_filters.delete field
|
||||
}
|
||||
|
||||
@available_filters.each do |field, options|
|
||||
options[:name] ||= l("field_#{field}".gsub(/_id$/, ''))
|
||||
options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
|
||||
end
|
||||
|
||||
@available_filters
|
||||
end
|
||||
|
||||
# Returns a representation of the available filters for JSON serialization
|
||||
# Returns a representation of the available filters for JSON serialization
|
||||
def available_filters_as_json
|
||||
json = {}
|
||||
available_filters.each do |field, options|
|
||||
@@ -332,6 +384,21 @@ class Query < ActiveRecord::Base
|
||||
json
|
||||
end
|
||||
|
||||
def all_projects
|
||||
@all_projects ||= Project.visible.all
|
||||
end
|
||||
|
||||
def all_projects_values
|
||||
return @all_projects_values if @all_projects_values
|
||||
|
||||
values = []
|
||||
Project.project_tree(all_projects) do |p, level|
|
||||
prefix = (level > 0 ? ('--' * level + ' ') : '')
|
||||
values << ["#{prefix}#{p.name}", p.id.to_s]
|
||||
end
|
||||
@all_projects_values = values
|
||||
end
|
||||
|
||||
def add_filter(field, operator, values)
|
||||
# values must be an array
|
||||
return unless values.nil? || values.is_a?(Array)
|
||||
@@ -451,6 +518,22 @@ class Query < ActiveRecord::Base
|
||||
end.compact
|
||||
end
|
||||
|
||||
def inline_columns
|
||||
columns.select(&:inline?)
|
||||
end
|
||||
|
||||
def block_columns
|
||||
columns.reject(&:inline?)
|
||||
end
|
||||
|
||||
def available_inline_columns
|
||||
available_columns.select(&:inline?)
|
||||
end
|
||||
|
||||
def available_block_columns
|
||||
available_columns.reject(&:inline?)
|
||||
end
|
||||
|
||||
def default_columns_names
|
||||
@default_columns_names ||= begin
|
||||
default_columns = Setting.issue_list_default_columns.map(&:to_sym)
|
||||
@@ -484,7 +567,7 @@ class Query < ActiveRecord::Base
|
||||
if arg.is_a?(Hash)
|
||||
arg = arg.keys.sort.collect {|k| arg[k]}
|
||||
end
|
||||
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
|
||||
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
|
||||
write_attribute(:sort_criteria, c)
|
||||
end
|
||||
|
||||
@@ -500,12 +583,17 @@ class Query < ActiveRecord::Base
|
||||
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
|
||||
end
|
||||
|
||||
def sort_criteria_order_for(key)
|
||||
sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
|
||||
end
|
||||
|
||||
# Returns the SQL sort order that should be prepended for grouping
|
||||
def group_by_sort_order
|
||||
if grouped? && (column = group_by_column)
|
||||
order = sort_criteria_order_for(column.name) || column.default_order
|
||||
column.sortable.is_a?(Array) ?
|
||||
column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
|
||||
"#{column.sortable} #{column.default_order}"
|
||||
column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
|
||||
"#{column.sortable} #{order}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -635,6 +723,9 @@ class Query < ActiveRecord::Base
|
||||
if has_column?(:spent_hours)
|
||||
Issue.load_visible_spent_hours(issues)
|
||||
end
|
||||
if has_column?(:relations)
|
||||
Issue.load_visible_relations(issues)
|
||||
end
|
||||
issues
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
@@ -711,10 +802,10 @@ class Query < ActiveRecord::Base
|
||||
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
|
||||
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
|
||||
when "=", "!"
|
||||
role_cond = value.any? ?
|
||||
role_cond = value.any? ?
|
||||
"#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
|
||||
"1=0"
|
||||
|
||||
|
||||
sw = operator == "!" ? 'NOT' : ''
|
||||
nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
|
||||
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
|
||||
@@ -729,6 +820,42 @@ class Query < ActiveRecord::Base
|
||||
"#{Issue.table_name}.is_private #{op} (#{va})"
|
||||
end
|
||||
|
||||
def sql_for_relations(field, operator, value, options={})
|
||||
relation_options = IssueRelation::TYPES[field]
|
||||
return relation_options unless relation_options
|
||||
|
||||
relation_type = field
|
||||
join_column, target_join_column = "issue_from_id", "issue_to_id"
|
||||
if relation_options[:reverse] || options[:reverse]
|
||||
relation_type = relation_options[:reverse] || relation_type
|
||||
join_column, target_join_column = target_join_column, join_column
|
||||
end
|
||||
|
||||
sql = case operator
|
||||
when "*", "!*"
|
||||
op = (operator == "*" ? 'IN' : 'NOT IN')
|
||||
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
|
||||
when "=", "!"
|
||||
op = (operator == "=" ? 'IN' : 'NOT IN')
|
||||
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
|
||||
when "=p", "=!p", "!p"
|
||||
op = (operator == "!p" ? 'NOT IN' : 'IN')
|
||||
comp = (operator == "=!p" ? '<>' : '=')
|
||||
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
|
||||
end
|
||||
|
||||
if relation_options[:sym] == field && !options[:reverse]
|
||||
sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
|
||||
sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
|
||||
else
|
||||
sql
|
||||
end
|
||||
end
|
||||
|
||||
IssueRelation::TYPES.keys.each do |relation_type|
|
||||
alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sql_for_custom_field(field, operator, value, custom_field_id)
|
||||
@@ -834,21 +961,35 @@ class Query < ActiveRecord::Base
|
||||
sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
|
||||
when "c"
|
||||
sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
|
||||
when ">t-"
|
||||
when "><t-"
|
||||
# between today - n days and today
|
||||
sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
|
||||
when ">t-"
|
||||
# >= today - n days
|
||||
sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
|
||||
when "<t-"
|
||||
# <= today - n days
|
||||
sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
|
||||
when "t-"
|
||||
# = n days in past
|
||||
sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
|
||||
when "><t+"
|
||||
# between today and today + n days
|
||||
sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
|
||||
when ">t+"
|
||||
# >= today + n days
|
||||
sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
|
||||
when "<t+"
|
||||
sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
|
||||
# <= today + n days
|
||||
sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
|
||||
when "t+"
|
||||
# = today + n days
|
||||
sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
|
||||
when "t"
|
||||
# = today
|
||||
sql = relative_date_clause(db_table, db_field, 0, 0)
|
||||
when "w"
|
||||
# = this week
|
||||
first_day_of_week = l(:general_first_day_of_week).to_i
|
||||
day_of_week = Date.today.cwday
|
||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
|
||||
@@ -898,7 +1039,11 @@ class Query < ActiveRecord::Base
|
||||
filter_id = "#{assoc}.#{filter_id}"
|
||||
filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
|
||||
end
|
||||
@available_filters[filter_id] = options.merge({ :name => filter_name, :format => field.field_format })
|
||||
@available_filters[filter_id] = options.merge({
|
||||
:name => filter_name,
|
||||
:format => field.field_format,
|
||||
:field => field
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class ScmFetchError < Exception; end
|
||||
class Repository < ActiveRecord::Base
|
||||
include Redmine::Ciphering
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
|
||||
# Maximum length for repository identifiers
|
||||
IDENTIFIER_MAX_LENGTH = 255
|
||||
|
||||
@@ -418,7 +418,7 @@ class Repository < ActiveRecord::Base
|
||||
|
||||
# Deletes repository data
|
||||
def clear_changesets
|
||||
cs = Changeset.table_name
|
||||
cs = Changeset.table_name
|
||||
ch = Change.table_name
|
||||
ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
|
||||
cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
|
||||
@@ -427,5 +427,9 @@ class Repository < ActiveRecord::Base
|
||||
connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
|
||||
connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
|
||||
connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
|
||||
clear_extra_info_of_changesets
|
||||
end
|
||||
|
||||
def clear_extra_info_of_changesets
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,7 +37,28 @@ class Repository::Bazaar < Repository
|
||||
'Bazaar'
|
||||
end
|
||||
|
||||
def entry(path=nil, identifier=nil)
|
||||
scm.bzr_path_encodig = log_encoding
|
||||
scm.entry(path, identifier)
|
||||
end
|
||||
|
||||
def cat(path, identifier=nil)
|
||||
scm.bzr_path_encodig = log_encoding
|
||||
scm.cat(path, identifier)
|
||||
end
|
||||
|
||||
def annotate(path, identifier=nil)
|
||||
scm.bzr_path_encodig = log_encoding
|
||||
scm.annotate(path, identifier)
|
||||
end
|
||||
|
||||
def diff(path, rev, rev_to)
|
||||
scm.bzr_path_encodig = log_encoding
|
||||
scm.diff(path, rev, rev_to)
|
||||
end
|
||||
|
||||
def entries(path=nil, identifier=nil)
|
||||
scm.bzr_path_encodig = log_encoding
|
||||
entries = scm.entries(path, identifier)
|
||||
if entries
|
||||
entries.each do |e|
|
||||
@@ -68,6 +89,7 @@ class Repository::Bazaar < Repository
|
||||
end
|
||||
|
||||
def fetch_changesets
|
||||
scm.bzr_path_encodig = log_encoding
|
||||
scm_info = scm.info
|
||||
if scm_info
|
||||
# latest revision found in database
|
||||
@@ -80,7 +102,7 @@ class Repository::Bazaar < Repository
|
||||
while (identifier_from <= scm_revision)
|
||||
# loads changesets by batches of 200
|
||||
identifier_to = [identifier_from + 199, scm_revision].min
|
||||
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
|
||||
revisions = scm.revisions('', identifier_to, identifier_from)
|
||||
transaction do
|
||||
revisions.reverse_each do |revision|
|
||||
changeset = Changeset.create(:repository => self,
|
||||
|
||||
@@ -255,4 +255,15 @@ class Repository::Git < Repository
|
||||
:order => 'committed_on DESC'
|
||||
)
|
||||
end
|
||||
|
||||
def clear_extra_info_of_changesets
|
||||
return if extra_info.nil?
|
||||
v = extra_info["extra_report_last_commit"]
|
||||
write_attribute(:extra_info, nil)
|
||||
h = {}
|
||||
h["extra_report_last_commit"] = v
|
||||
merge_extra_info(h)
|
||||
self.save
|
||||
end
|
||||
private :clear_extra_info_of_changesets
|
||||
end
|
||||
|
||||
@@ -133,6 +133,11 @@ class Role < ActiveRecord::Base
|
||||
self.builtin != 0
|
||||
end
|
||||
|
||||
# Return true if the role is the anonymous role
|
||||
def anonymous?
|
||||
builtin == 2
|
||||
end
|
||||
|
||||
# Return true if the role is a project member role
|
||||
def member?
|
||||
!self.builtin?
|
||||
|
||||
@@ -28,11 +28,41 @@ class User < Principal
|
||||
|
||||
# Different ways of displaying/sorting users
|
||||
USER_FORMATS = {
|
||||
:firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
|
||||
:firstname => {:string => '#{firstname}', :order => %w(firstname id)},
|
||||
:lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
|
||||
:lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
|
||||
:username => {:string => '#{login}', :order => %w(login id)},
|
||||
:firstname_lastname => {
|
||||
:string => '#{firstname} #{lastname}',
|
||||
:order => %w(firstname lastname id),
|
||||
:setting_order => 1
|
||||
},
|
||||
:firstname_lastinitial => {
|
||||
:string => '#{firstname} #{lastname.to_s.chars.first}.',
|
||||
:order => %w(firstname lastname id),
|
||||
:setting_order => 2
|
||||
},
|
||||
:firstname => {
|
||||
:string => '#{firstname}',
|
||||
:order => %w(firstname id),
|
||||
:setting_order => 3
|
||||
},
|
||||
:lastname_firstname => {
|
||||
:string => '#{lastname} #{firstname}',
|
||||
:order => %w(lastname firstname id),
|
||||
:setting_order => 4
|
||||
},
|
||||
:lastname_coma_firstname => {
|
||||
:string => '#{lastname}, #{firstname}',
|
||||
:order => %w(lastname firstname id),
|
||||
:setting_order => 5
|
||||
},
|
||||
:lastname => {
|
||||
:string => '#{lastname}',
|
||||
:order => %w(lastname id),
|
||||
:setting_order => 6
|
||||
},
|
||||
:username => {
|
||||
:string => '#{login}',
|
||||
:order => %w(login id),
|
||||
:setting_order => 7
|
||||
},
|
||||
}
|
||||
|
||||
MAIL_NOTIFICATION_OPTIONS = [
|
||||
@@ -67,7 +97,7 @@ class User < Principal
|
||||
|
||||
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
|
||||
validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
|
||||
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
|
||||
validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
|
||||
# Login must contain lettres, numbers, underscores only
|
||||
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
|
||||
validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
|
||||
@@ -84,11 +114,11 @@ class User < Principal
|
||||
|
||||
scope :in_group, lambda {|group|
|
||||
group_id = group.is_a?(Group) ? group.id : group.to_i
|
||||
{ :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
|
||||
where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
|
||||
}
|
||||
scope :not_in_group, lambda {|group|
|
||||
group_id = group.is_a?(Group) ? group.id : group.to_i
|
||||
{ :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
|
||||
where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
|
||||
}
|
||||
|
||||
def set_mail_notification
|
||||
@@ -330,10 +360,10 @@ class User < Principal
|
||||
# version. Exact matches will be given priority.
|
||||
def self.find_by_login(login)
|
||||
# First look for an exact match
|
||||
user = all(:conditions => {:login => login}).detect {|u| u.login == login}
|
||||
user = where(:login => login).all.detect {|u| u.login == login}
|
||||
unless user
|
||||
# Fail over to case-insensitive if none was found
|
||||
user = first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
|
||||
user = where("LOWER(login) = ?", login.to_s.downcase).first
|
||||
end
|
||||
user
|
||||
end
|
||||
@@ -350,7 +380,7 @@ class User < Principal
|
||||
|
||||
# Makes find_by_mail case-insensitive
|
||||
def self.find_by_mail(mail)
|
||||
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
|
||||
where("LOWER(mail) = ?", mail.to_s.downcase).first
|
||||
end
|
||||
|
||||
# Returns true if the default admin account can no longer be used
|
||||
@@ -362,6 +392,17 @@ class User < Principal
|
||||
name
|
||||
end
|
||||
|
||||
CSS_CLASS_BY_STATUS = {
|
||||
STATUS_ANONYMOUS => 'anon',
|
||||
STATUS_ACTIVE => 'active',
|
||||
STATUS_REGISTERED => 'registered',
|
||||
STATUS_LOCKED => 'locked'
|
||||
}
|
||||
|
||||
def css_classes
|
||||
"user #{CSS_CLASS_BY_STATUS[status]}"
|
||||
end
|
||||
|
||||
# Returns the current day according to user's time zone
|
||||
def today
|
||||
if time_zone.nil?
|
||||
@@ -462,17 +503,17 @@ class User < Principal
|
||||
|
||||
roles = roles_for_project(context)
|
||||
return false unless roles
|
||||
roles.detect {|role|
|
||||
roles.any? {|role|
|
||||
(context.is_public? || role.member?) &&
|
||||
role.allowed_to?(action) &&
|
||||
(block_given? ? yield(role, self) : true)
|
||||
}
|
||||
elsif context && context.is_a?(Array)
|
||||
# Authorize if user is authorized on every element of the array
|
||||
context.map do |project|
|
||||
allowed_to?(action, project, options, &block)
|
||||
end.inject do |memo,allowed|
|
||||
memo && allowed
|
||||
if context.empty?
|
||||
false
|
||||
else
|
||||
# Authorize if user is authorized on every element of the array
|
||||
context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
|
||||
end
|
||||
elsif options[:global]
|
||||
# Admin users are always authorized
|
||||
@@ -481,7 +522,7 @@ class User < Principal
|
||||
# authorize if user has at least one role that has this permission
|
||||
roles = memberships.collect {|m| m.roles}.flatten.uniq
|
||||
roles << (self.logged? ? Role.non_member : Role.anonymous)
|
||||
roles.detect {|role|
|
||||
roles.any? {|role|
|
||||
role.allowed_to?(action) &&
|
||||
(block_given? ? yield(role, self) : true)
|
||||
}
|
||||
@@ -499,7 +540,7 @@ class User < Principal
|
||||
# Returns true if the user is allowed to delete his own account
|
||||
def own_account_deletable?
|
||||
Setting.unsubscribe? &&
|
||||
(!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
|
||||
(!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
|
||||
end
|
||||
|
||||
safe_attributes 'login',
|
||||
@@ -570,7 +611,7 @@ class User < Principal
|
||||
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
|
||||
# one anonymous user per database.
|
||||
def self.anonymous
|
||||
anonymous_user = AnonymousUser.find(:first)
|
||||
anonymous_user = AnonymousUser.first
|
||||
if anonymous_user.nil?
|
||||
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
|
||||
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
|
||||
@@ -583,11 +624,11 @@ class User < Principal
|
||||
# This method is used in the SaltPasswords migration and is to be kept as is
|
||||
def self.salt_unsalted_passwords!
|
||||
transaction do
|
||||
User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
|
||||
User.where("salt IS NULL OR salt = ''").find_each do |user|
|
||||
next if user.hashed_password.blank?
|
||||
salt = User.generate_salt
|
||||
hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
|
||||
User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
|
||||
User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,7 +44,7 @@ class UserPreference < ActiveRecord::Base
|
||||
if attribute_present? attr_name
|
||||
super
|
||||
else
|
||||
h = read_attribute(:others).dup || {}
|
||||
h = (read_attribute(:others) || {}).dup
|
||||
h.update(attr_name => value)
|
||||
write_attribute(:others, h)
|
||||
value
|
||||
|
||||
@@ -33,11 +33,13 @@ class Version < ActiveRecord::Base
|
||||
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
|
||||
validates_inclusion_of :status, :in => VERSION_STATUSES
|
||||
validates_inclusion_of :sharing, :in => VERSION_SHARINGS
|
||||
validate :validate_version
|
||||
|
||||
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
|
||||
scope :open, :conditions => {:status => 'open'}
|
||||
scope :visible, lambda {|*args| { :include => :project,
|
||||
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
|
||||
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||
scope :open, where(:status => 'open')
|
||||
scope :visible, lambda {|*args|
|
||||
includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues))
|
||||
}
|
||||
|
||||
safe_attributes 'name',
|
||||
'description',
|
||||
@@ -78,7 +80,7 @@ class Version < ActiveRecord::Base
|
||||
|
||||
# Returns the total reported time for this version
|
||||
def spent_hours
|
||||
@spent_hours ||= TimeEntry.sum(:hours, :joins => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
|
||||
@spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
|
||||
end
|
||||
|
||||
def closed?
|
||||
@@ -91,7 +93,7 @@ class Version < ActiveRecord::Base
|
||||
|
||||
# Returns true if the version is completed: due date reached and no open issues
|
||||
def completed?
|
||||
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
|
||||
effective_date && (effective_date < Date.today) && (open_issues_count == 0)
|
||||
end
|
||||
|
||||
def behind_schedule?
|
||||
@@ -267,12 +269,16 @@ class Version < ActiveRecord::Base
|
||||
if issues_count > 0
|
||||
ratio = open ? 'done_ratio' : 100
|
||||
|
||||
done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
|
||||
:joins => :status,
|
||||
:conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
|
||||
done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
|
||||
progress = done / (estimated_average * issues_count)
|
||||
end
|
||||
progress
|
||||
end
|
||||
end
|
||||
|
||||
def validate_version
|
||||
if effective_date.nil? && @attributes['effective_date'].present?
|
||||
errors.add :effective_date, :not_a_date
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,6 +73,8 @@ class WikiContent < ActiveRecord::Base
|
||||
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
|
||||
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
|
||||
|
||||
after_destroy :page_update_after_destroy
|
||||
|
||||
def text=(plain)
|
||||
case Setting.wiki_compression
|
||||
when 'gzip'
|
||||
@@ -115,10 +117,31 @@ class WikiContent < ActiveRecord::Base
|
||||
|
||||
# Returns the previous version or nil
|
||||
def previous
|
||||
@previous ||= WikiContent::Version.find(:first,
|
||||
:order => 'version DESC',
|
||||
:include => :author,
|
||||
:conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
|
||||
@previous ||= WikiContent::Version.
|
||||
reorder('version DESC').
|
||||
includes(:author).
|
||||
where("wiki_content_id = ? AND version < ?", wiki_content_id, version).first
|
||||
end
|
||||
|
||||
# Returns the next version or nil
|
||||
def next
|
||||
@next ||= WikiContent::Version.
|
||||
reorder('version ASC').
|
||||
includes(:author).
|
||||
where("wiki_content_id = ? AND version > ?", wiki_content_id, version).first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Updates page's content if the latest version is removed
|
||||
# or destroys the page if it was the only version
|
||||
def page_update_after_destroy
|
||||
latest = page.content.versions.reorder("#{self.class.table_name}.version DESC").first
|
||||
if latest && page.content.version != latest.version
|
||||
raise ActiveRecord::Rollback unless page.content.revert_to!(latest)
|
||||
elsif latest.nil?
|
||||
raise ActiveRecord::Rollback unless page.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,14 +50,14 @@ class WikiPage < ActiveRecord::Base
|
||||
|
||||
# eager load information about last updates, without loading text
|
||||
scope :with_updated_on, {
|
||||
:select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
|
||||
:select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on, #{WikiContent.table_name}.version",
|
||||
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
|
||||
}
|
||||
|
||||
# Wiki pages that are protected by default
|
||||
DEFAULT_PROTECTED_PAGES = %w(sidebar)
|
||||
|
||||
safe_attributes 'parent_id',
|
||||
safe_attributes 'parent_id', 'parent_title',
|
||||
:if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
|
||||
|
||||
def initialize(attributes=nil, *args)
|
||||
@@ -111,11 +111,13 @@ class WikiPage < ActiveRecord::Base
|
||||
|
||||
def diff(version_to=nil, version_from=nil)
|
||||
version_to = version_to ? version_to.to_i : self.content.version
|
||||
version_from = version_from ? version_from.to_i : version_to - 1
|
||||
version_to, version_from = version_from, version_to unless version_from < version_to
|
||||
|
||||
content_to = content.versions.find_by_version(version_to)
|
||||
content_from = content.versions.find_by_version(version_from)
|
||||
content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.try(:previous)
|
||||
return nil unless content_to && content_from
|
||||
|
||||
if content_from.version > content_to.version
|
||||
content_to, content_from = content_from, content_to
|
||||
end
|
||||
|
||||
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
|
||||
end
|
||||
@@ -172,6 +174,21 @@ class WikiPage < ActiveRecord::Base
|
||||
self.parent = parent_page
|
||||
end
|
||||
|
||||
# Saves the page and its content if text was changed
|
||||
def save_with_content
|
||||
ret = nil
|
||||
transaction do
|
||||
if new_record?
|
||||
# Rails automatically saves associated content
|
||||
ret = save
|
||||
else
|
||||
ret = save && (content.text_changed? ? content.save : true)
|
||||
end
|
||||
raise ActiveRecord::Rollback unless ret
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def validate_parent_title
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
|
||||
<td align="left"><%= text_field_tag 'username', nil, :tabindex => '1' %></td>
|
||||
<td align="left"><%= text_field_tag 'username', params[:username], :tabindex => '1' %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
|
||||
@@ -36,7 +36,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<%= javascript_tag "$('#username').focus();" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= call_hook :view_account_login_bottom %>
|
||||
|
||||
<% if params[:username].present? %>
|
||||
<%= javascript_tag "$('#password').focus();" %>
|
||||
<% else %>
|
||||
<%= javascript_tag "$('#username').focus();" %>
|
||||
<% end %>
|
||||
|
||||
@@ -6,16 +6,14 @@
|
||||
<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
|
||||
<span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
|
||||
</div>
|
||||
<p>
|
||||
<%= form_tag({}, :method => 'get') do %>
|
||||
<label><%= l(:label_view_diff) %></label>
|
||||
<%= select_tag 'type',
|
||||
options_for_select(
|
||||
[[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
|
||||
:onchange => "if (this.value != '') {this.form.submit()}" %>
|
||||
<p>
|
||||
<%= l(:label_view_diff) %>:
|
||||
<label><%= radio_button_tag 'type', 'inline', @diff_type != 'sbs', :onchange => "this.form.submit()" %> <%= l(:label_diff_inline) %></label>
|
||||
<label><%= radio_button_tag 'type', 'sbs', @diff_type == 'sbs', :onchange => "this.form.submit()" %> <%= l(:label_diff_side_by_side) %></label>
|
||||
</p>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
|
||||
<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type, :diff_style => nil} %>
|
||||
|
||||
<% html_title @attachment.filename %>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<% diff = Redmine::UnifiedDiff.new(
|
||||
diff, :type => diff_type,
|
||||
:max_lines => Setting.diff_max_lines_displayed.to_i) -%>
|
||||
:max_lines => Setting.diff_max_lines_displayed.to_i,
|
||||
:style => diff_style) -%>
|
||||
|
||||
<% diff.each do |table_file| -%>
|
||||
<div class="autoscroll">
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<% if @message.present? %>
|
||||
<p id="errorExplanation"><%=h @message %></p>
|
||||
<% end %>
|
||||
<p><a href="javascript:history.back()">Back</a></p>
|
||||
<p><a href="javascript:history.back()"><%= l(:button_back) %></a></p>
|
||||
|
||||
<% html_title @status %>
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
<li><%= bulk_update_custom_field_context_menu_link(field, text, value || text) %></li>
|
||||
<% end %>
|
||||
<% unless field.is_required? %>
|
||||
<li><%= bulk_update_custom_field_context_menu_link(field, l(:label_none), '') %></li>
|
||||
<li><%= bulk_update_custom_field_context_menu_link(field, l(:label_none), '__none__') %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
9
app/views/enumerations/index.api.rsb
Normal file
9
app/views/enumerations/index.api.rsb
Normal file
@@ -0,0 +1,9 @@
|
||||
api.array @klass.name.underscore.pluralize do
|
||||
@enumerations.each do |enumeration|
|
||||
api.__send__ @klass.name.underscore do
|
||||
api.id enumeration.id
|
||||
api.name enumeration.name
|
||||
api.is_default enumeration.is_default
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -201,9 +201,10 @@
|
||||
style += "width: #{width}px;"
|
||||
style += "height: #{height}px;"
|
||||
style += "font-size:0.7em;"
|
||||
style += 'background:#f1f1f1;' if wday > 5
|
||||
clss = "gantt_hdr"
|
||||
clss << " nwday" if @gantt.non_working_week_days.include?(wday)
|
||||
%>
|
||||
<%= content_tag(:div, :style => style, :class => "gantt_hdr") do %>
|
||||
<%= content_tag(:div, :style => style, :class => clss) do %>
|
||||
<%= day_letter(wday) %>
|
||||
<% end %>
|
||||
<%
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<%= l(:field_delay) %>: <%= f.text_field :delay, :size => 3 %> <%= l(:label_day_plural) %>
|
||||
</span>
|
||||
<%= submit_tag l(:button_add) %>
|
||||
<%= toggle_link l(:button_cancel), 'new-relation-form'%>
|
||||
<%= link_to_function l(:button_cancel), '$("#new-relation-form").hide();'%>
|
||||
</p>
|
||||
|
||||
<%= javascript_tag "observeAutocompleteField('relation_issue_to_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (Setting.cross_project_issue_relations? ? 'all' : nil))}')" %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="contextual">
|
||||
<%= 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)) %>
|
||||
<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "issue_notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
|
||||
<%= 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) %>
|
||||
<%= watcher_tag(@issue, User.current) %>
|
||||
<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon icon-copy' %>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="splitcontent">
|
||||
<div class="splitcontentleft">
|
||||
<% if @issue.safe_attribute? 'status_id' %>
|
||||
<% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %>
|
||||
<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true},
|
||||
:onchange => "updateIssueFrom('#{escape_javascript project_issue_form_path(@project, :id => @issue, :format => 'js')}')" %></p>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="splitcontentright">
|
||||
<% if @issue.safe_attribute? 'parent_issue_id' %>
|
||||
<p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
|
||||
<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project)}')" %>
|
||||
<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path}')" %>
|
||||
<% end %>
|
||||
|
||||
<% if @issue.safe_attribute? 'start_date' %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<p>
|
||||
<label><%= radio_button_tag 'conflict_resolution', 'overwrite' %> <%= l(:text_issue_conflict_resolution_overwrite) %></label><br />
|
||||
<% if @notes.present? %>
|
||||
<% if @issue.notes.present? %>
|
||||
<label><%= radio_button_tag 'conflict_resolution', 'add_notes' %> <%= l(:text_issue_conflict_resolution_add_notes) %></label><br />
|
||||
<% end %>
|
||||
<label><%= radio_button_tag 'conflict_resolution', 'cancel' %> <%= l(:text_issue_conflict_resolution_cancel, :link => link_to_issue(@issue, :subject => false)).html_safe %></label>
|
||||
|
||||
@@ -27,11 +27,18 @@
|
||||
<% end %>
|
||||
|
||||
<fieldset><legend><%= l(:field_notes) %></legend>
|
||||
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
|
||||
<%= wikitoolbar_for 'notes' %>
|
||||
<%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
|
||||
<%= f.text_area :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', :no_label => true %>
|
||||
<%= wikitoolbar_for 'issue_notes' %>
|
||||
|
||||
<p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
|
||||
<% if @issue.safe_attribute? 'private_notes' %>
|
||||
<label for="issue_private_notes"><%= f.check_box :private_notes, :no_label => true %> <%= l(:field_private_notes) %></label>
|
||||
<% end %>
|
||||
|
||||
<%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
|
||||
</fieldset>
|
||||
|
||||
<fieldset><legend><%= l(:label_attachment_plural) %></legend>
|
||||
<p><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<% end %>
|
||||
|
||||
<% if @issue.safe_attribute? 'subject' %>
|
||||
<p><%= f.text_field :subject, :size => 80, :required => true %></p>
|
||||
<p><%= f.text_field :subject, :size => 80, :maxlength => 255, :required => true %></p>
|
||||
<% end %>
|
||||
|
||||
<% if @issue.safe_attribute? 'description' %>
|
||||
|
||||
@@ -2,25 +2,30 @@
|
||||
<%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
|
||||
<div class="autoscroll">
|
||||
<table class="list issues">
|
||||
<thead><tr>
|
||||
<th class="checkbox hide-when-print"><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(this); return false;',
|
||||
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
|
||||
</th>
|
||||
<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
|
||||
<% query.columns.each do |column| %>
|
||||
<%= column_header(column) %>
|
||||
<% end %>
|
||||
</tr></thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="checkbox hide-when-print">
|
||||
<%= link_to image_tag('toggle_check.png'), {},
|
||||
:onclick => 'toggleIssuesSelection(this); return false;',
|
||||
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
|
||||
</th>
|
||||
<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
|
||||
<% query.inline_columns.each do |column| %>
|
||||
<%= column_header(column) %>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<% previous_group = false %>
|
||||
<tbody>
|
||||
<% issue_list(issues) do |issue, level| -%>
|
||||
<% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
|
||||
<% reset_cycle %>
|
||||
<tr class="group open">
|
||||
<td colspan="<%= query.columns.size + 2 %>">
|
||||
<td colspan="<%= query.inline_columns.size + 2 %>">
|
||||
<span class="expander" onclick="toggleRowGroup(this);"> </span>
|
||||
<%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
|
||||
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
|
||||
<%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span>
|
||||
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
|
||||
"toggleAllRowGroups(this)", :class => 'toggle-all') %>
|
||||
</td>
|
||||
</tr>
|
||||
<% previous_group = group %>
|
||||
@@ -28,8 +33,15 @@
|
||||
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
|
||||
<td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
|
||||
<td class="id"><%= link_to issue.id, issue_path(issue) %></td>
|
||||
<%= raw query.columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
|
||||
<%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
|
||||
</tr>
|
||||
<% @query.block_columns.each do |column|
|
||||
if (text = column_content(column, issue)) && text.present? -%>
|
||||
<tr class="<%= current_cycle %>">
|
||||
<td colspan="<%= @query.inline_columns.size + 2 %>" class="<%= column.css_classes %>"><%= text %></td>
|
||||
</tr>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1 +1 @@
|
||||
$('#content').html('<%= escape_javascript(render :template => 'issues/bulk_edit.html') %>');
|
||||
$('#content').html('<%= escape_javascript(render :template => 'issues/bulk_edit', :formats => [:html]) %>');
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
@query.group_by)
|
||||
) %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%= l(:button_show) %></td>
|
||||
<td><%= available_block_columns_tags(@query) %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -73,7 +77,7 @@
|
||||
<label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= check_box_tag 'description', '1' %> <%= l(:field_description) %></label>
|
||||
<label><%= check_box_tag 'description', '1', @query.has_column?(:description) %> <%= l(:field_description) %></label>
|
||||
</p>
|
||||
<p class="buttons">
|
||||
<%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
|
||||
|
||||
@@ -48,7 +48,7 @@ api.issue do
|
||||
end if include_in_api_response?('changesets') && User.current.allowed_to?(:view_changesets, @project)
|
||||
|
||||
api.array :journals do
|
||||
@issue.journals.each do |journal|
|
||||
@journals.each do |journal|
|
||||
api.journal :id => journal.id do
|
||||
api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
|
||||
api.notes journal.notes
|
||||
|
||||
@@ -71,6 +71,7 @@ end %>
|
||||
<% if @issue.description? || @issue.attachments.any? -%>
|
||||
<hr />
|
||||
<% if @issue.description? %>
|
||||
<div class="description">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_quote),
|
||||
{:controller => 'journals', :action => 'new', :id => @issue},
|
||||
@@ -83,6 +84,7 @@ end %>
|
||||
<div class="wiki">
|
||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to_attachments @issue, :thumbnails => true %>
|
||||
<% end -%>
|
||||
@@ -93,7 +95,7 @@ end %>
|
||||
<hr />
|
||||
<div id="issue_tree">
|
||||
<div class="contextual">
|
||||
<%= link_to(l(:button_add), {:controller => 'issues', :action => 'new', :project_id => @project, :issue => {:parent_issue_id => @issue}}) if User.current.allowed_to?(:manage_subtasks, @project) %>
|
||||
<%= link_to_new_subtask(@issue) if User.current.allowed_to?(:manage_subtasks, @project) %>
|
||||
</div>
|
||||
<p><strong><%=l(:label_subtask_plural)%></strong></p>
|
||||
<%= render_descendants_tree(@issue) unless @issue.leaf? %>
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
$("#journal-<%= @journal.id %>-notes").hide();
|
||||
$("#journal-<%= @journal.id %>-notes").after('<%= escape_javascript(render :partial => 'notes_form') %>');
|
||||
|
||||
if ($("form#journal-<%= @journal.id %>-form").length > 0) {
|
||||
// journal edit form already loaded
|
||||
$("#journal-<%= @journal.id %>-form").show();
|
||||
} else {
|
||||
$("#journal-<%= @journal.id %>-notes").after('<%= escape_javascript(render :partial => 'notes_form') %>');
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
$('#notes').val("<%= raw escape_javascript(@content) %>");
|
||||
$('#issue_notes').val("<%= raw escape_javascript(@content) %>");
|
||||
<%
|
||||
# when quoting a private journal, check the private checkbox
|
||||
if @journal && @journal.private_notes?
|
||||
%>
|
||||
$('#issue_private_notes').attr('checked', true);
|
||||
<% end %>
|
||||
|
||||
showAndScrollTo("update", "notes");
|
||||
$('#notes').scrollTop = $('#notes').scrollHeight - $('#notes').clientHeight;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<body class="<%=h body_css_classes %>">
|
||||
<div id="wrapper">
|
||||
<div id="wrapper2">
|
||||
<div id="wrapper3">
|
||||
<div id="top-menu">
|
||||
<div id="account">
|
||||
<%= render_menu :account_menu -%>
|
||||
@@ -62,6 +63,7 @@
|
||||
<div style="clear:both;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
|
||||
<div id="ajax-modal" style="display:none;"></div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%= labelled_form_for @news, :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
|
||||
<%= render :partial => 'form', :locals => { :f => f } %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<%= preview_link preview_news_path(:project_id => @project), 'news-form' %>
|
||||
<%= preview_link preview_news_path(:project_id => @project, :id => @news), 'news-form' %>
|
||||
<% end %>
|
||||
<div id="preview" class="wiki"></div>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
:html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
|
||||
<%= render :partial => 'form', :locals => { :f => f } %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<%= preview_link preview_news_path(:project_id => @project), 'news-form' %> |
|
||||
<%= preview_link preview_news_path(:project_id => @project, :id => @news), 'news-form' %> |
|
||||
<%= link_to l(:button_cancel), "#", :onclick => '$("#edit-news").hide(); return false;' %>
|
||||
<% end %>
|
||||
<div id="preview" class="wiki"></div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<tr class="<%= cycle 'odd', 'even' %>">
|
||||
<td>
|
||||
<%= link_to repository.identifier,
|
||||
{:controller => 'repositories', :action => 'show',:id => @project, :repository_id => repository.identifier_param} if repository.identifier_param.present? %>
|
||||
{:controller => 'repositories', :action => 'show',:id => @project, :repository_id => repository.identifier_param} if repository.identifier.present? %>
|
||||
</td>
|
||||
<td align="center"><%= checked_image repository.is_default? %></td>
|
||||
<td><%=h repository.scm_name %></td>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<%= label_tag "available_columns", l(:description_available_columns) %>
|
||||
<br />
|
||||
<%= select_tag 'available_columns',
|
||||
options_for_select((query.available_columns - query.columns).collect {|column| [column.caption, column.name]}),
|
||||
options_for_select((query.available_inline_columns - query.columns).collect {|column| [column.caption, column.name]}),
|
||||
:multiple => true, :size => 10, :style => "width:150px",
|
||||
:ondblclick => "moveOptions(this.form.available_columns, this.form.selected_columns);" %>
|
||||
</td>
|
||||
@@ -18,7 +18,7 @@
|
||||
<%= label_tag "selected_columns", l(:description_selected_columns) %>
|
||||
<br />
|
||||
<%= select_tag((defined?(tag_name) ? tag_name : 'c[]'),
|
||||
options_for_select(query.columns.collect {|column| [column.caption, column.name]}),
|
||||
options_for_select(query.inline_columns.collect {|column| [column.caption, column.name]}),
|
||||
:id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px",
|
||||
:ondblclick => "moveOptions(this.form.selected_columns, this.form.available_columns);") %>
|
||||
</td>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<%= javascript_tag do %>
|
||||
var operatorLabels = <%= raw Query.operators_labels.to_json %>;
|
||||
var operatorByType = <%= raw Query.operators_by_filter_type.to_json %>;
|
||||
var availableFilters = <%= raw query.available_filters_as_json.to_json %>;
|
||||
var labelDayPlural = "<%= raw escape_javascript(l(:label_day_plural)) %>";
|
||||
var operatorLabels = <%= raw_json Query.operators_labels %>;
|
||||
var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
|
||||
var availableFilters = <%= raw_json query.available_filters_as_json %>;
|
||||
var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
|
||||
var allProjects = <%= raw query.all_projects_values.to_json %>;
|
||||
$(document).ready(function(){
|
||||
initFilters();
|
||||
<% query.filters.each do |field, options| %>
|
||||
addFilter("<%= field %>", <%= raw query.operator_for(field).to_json %>, <%= raw query.values_for(field).to_json %>);
|
||||
addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>);
|
||||
<% end %>
|
||||
});
|
||||
<% end %>
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
|
||||
<p><label for="query_group_by"><%= l(:field_group_by) %></label>
|
||||
<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
|
||||
|
||||
<p><label><%= l(:button_show) %></label>
|
||||
<%= available_block_columns_tags(@query) %></p>
|
||||
</div>
|
||||
|
||||
<fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<%= javascript_include_tag 'raphael.js' %>
|
||||
<%= javascript_include_tag 'revision_graph.js' %>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
<%= javascript_tag do %>
|
||||
function revisionGraphHandler(){
|
||||
drawRevisionGraph(
|
||||
document.getElementById('holder'),
|
||||
@@ -10,5 +8,11 @@ function revisionGraphHandler(){
|
||||
}
|
||||
$(document).ready(revisionGraphHandler);
|
||||
$(window).resize(revisionGraphHandler);
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
<div id="holder" class="revision-graph"></div>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascript_include_tag 'raphael' %>
|
||||
<%= javascript_include_tag 'revision_graph' %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<h2><%= l(:label_revision) %> <%= @diff_format_revisions %> <%=h @path %></h2>
|
||||
|
||||
<!-- Choose view type -->
|
||||
<%= form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
|
||||
<%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
|
||||
<%= form_tag({:action => 'diff', :id => @project,
|
||||
:repository_id => @repository.identifier_param,
|
||||
:path => to_path_param(@path), :rev=> @rev}, :method => 'get') do %>
|
||||
<%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
|
||||
<p>
|
||||
<label><%= l(:label_view_diff) %></label>
|
||||
<%= select_tag 'type',
|
||||
options_for_select(
|
||||
[[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type),
|
||||
:onchange => "if (this.value != '') {this.form.submit()}" %>
|
||||
<%= l(:label_view_diff) %>:
|
||||
<label><%= radio_button_tag 'type', 'inline', @diff_type != 'sbs', :onchange => "this.form.submit()" %> <%= l(:label_diff_inline) %></label>
|
||||
<label><%= radio_button_tag 'type', 'sbs', @diff_type == 'sbs', :onchange => "this.form.submit()" %> <%= l(:label_diff_side_by_side) %></label>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% cache(@cache_key) do -%>
|
||||
<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
|
||||
<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type, :diff_style => @repository.class.scm_name} %>
|
||||
<% end -%>
|
||||
|
||||
<% other_formats_links do |f| %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
$('#content').html('<%= escape_javascript(render :template => 'repositories/new.html') %>');
|
||||
$('#content').html('<%= escape_javascript(render :template => 'repositories/new', :formats => [:html]) %>');
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<div class="contextual">
|
||||
<%= form_tag(
|
||||
{:controller => 'repositories', :action => 'revision', :id => @project,
|
||||
:repository_id => @repository.identifier_param}
|
||||
:repository_id => @repository.identifier_param},
|
||||
:method => :get
|
||||
) do %>
|
||||
<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
|
||||
<%= l(:label_revision) %>: <%= text_field_tag 'rev', nil, :size => 8 %>
|
||||
<%= submit_tag 'OK' %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<%= error_messages_for 'role' %>
|
||||
|
||||
<% unless @role.anonymous? %>
|
||||
<div class="box tabular">
|
||||
<% unless @role.builtin? %>
|
||||
<p><%= f.text_field :name, :required => true %></p>
|
||||
@@ -11,6 +12,7 @@
|
||||
<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<h3><%= l(:label_permissions) %></h3>
|
||||
<div class="box tabular" id="permissions">
|
||||
|
||||
9
app/views/roles/show.api.rsb
Normal file
9
app/views/roles/show.api.rsb
Normal file
@@ -0,0 +1,9 @@
|
||||
api.role do
|
||||
api.id @role.id
|
||||
api.name @role.name
|
||||
api.array :permissions do
|
||||
@role.permissions.each do |perm|
|
||||
api.permission(perm.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,8 @@
|
||||
<div class="box tabular settings">
|
||||
<p><%= setting_check_box :cross_project_issue_relations %></p>
|
||||
|
||||
<p><%= setting_select :cross_project_subtasks, cross_project_subtasks_options %></p>
|
||||
|
||||
<p><%= setting_check_box :issue_group_assignment %></p>
|
||||
|
||||
<p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
|
||||
@@ -11,6 +13,8 @@
|
||||
|
||||
<p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
|
||||
|
||||
<p><%= setting_multiselect :non_working_week_days, (1..7).map {|d| [day_name(d), d.to_s]}, :inline => true %></p>
|
||||
|
||||
<p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
|
||||
|
||||
<p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
|
||||
<p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
|
||||
<p><%= f.text_field :hours, :size => 6, :required => true %></p>
|
||||
<p><%= f.text_field :comments, :size => 100 %></p>
|
||||
<p><%= f.text_field :comments, :size => 100, :maxlength => 255 %></p>
|
||||
<p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
|
||||
<% @time_entry.custom_field_values.each do |value| %>
|
||||
<p><%= custom_field_tag_with_label :time_entry, value %></p>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% for user in @users -%>
|
||||
<tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">
|
||||
<tr class="<%= user.css_classes %> <%= cycle("odd", "even") %>">
|
||||
<td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
|
||||
<td class="firstname"><%= h(user.firstname) %></td>
|
||||
<td class="lastname"><%= h(user.lastname) %></td>
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
<% counts.each do |count| %>
|
||||
<tr>
|
||||
<td width="130px" align="right" >
|
||||
<%= link_to h(count[:group]), {:controller => 'issues',
|
||||
:action => 'index',
|
||||
:project_id => version.project,
|
||||
:set_filter => 1,
|
||||
:status_id => '*',
|
||||
:fixed_version_id => version}.merge("#{criteria}_id".to_sym => count[:group]) %>
|
||||
<% if count[:group] -%>
|
||||
<%= link_to(h(count[:group]), project_issues_path(version.project, :set_filter => 1, :status_id => '*', :fixed_version_id => version, "#{criteria}_id" => count[:group])) %>
|
||||
<% else -%>
|
||||
<%= link_to(l(:label_none), project_issues_path(version.project, :set_filter => 1, :status_id => '*', :fixed_version_id => version, "#{criteria}_id" => "!*")) %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td width="240px">
|
||||
<%= progress_bar((count[:closed].to_f / count[:total])*100,
|
||||
|
||||
@@ -28,12 +28,15 @@
|
||||
<td class="updated_on"><%= format_time(ver.updated_on) %></td>
|
||||
<td class="author"><%= link_to_user ver.author %></td>
|
||||
<td class="comments"><%=h ver.comments %></td>
|
||||
<td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %>
|
||||
<%= delete_link wiki_page_path(@page, :version => ver.version) if User.current.allowed_to?(:delete_wiki_pages, @page.project) && @version_count > 1 %>
|
||||
</td>
|
||||
</tr>
|
||||
<% line_num += 1 %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
|
||||
<span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
|
||||
<span class="pagination"><%= pagination_links_full @version_pages, @version_count %></span>
|
||||
<% end %>
|
||||
|
||||
13
app/views/wiki/index.api.rsb
Normal file
13
app/views/wiki/index.api.rsb
Normal file
@@ -0,0 +1,13 @@
|
||||
api.array :wiki_pages do
|
||||
@pages.each do |page|
|
||||
api.wiki_page do
|
||||
api.title page.title
|
||||
if page.parent
|
||||
api.parent :title => page.parent.title
|
||||
end
|
||||
api.version page.version
|
||||
api.created_on page.created_on
|
||||
api.updated_on page.updated_on
|
||||
end
|
||||
end
|
||||
end
|
||||
18
app/views/wiki/show.api.rsb
Normal file
18
app/views/wiki/show.api.rsb
Normal file
@@ -0,0 +1,18 @@
|
||||
api.wiki_page do
|
||||
api.title @page.title
|
||||
if @page.parent
|
||||
api.parent :title => @page.parent.title
|
||||
end
|
||||
api.text @content.text
|
||||
api.version @content.version
|
||||
api.author(:id => @content.author_id, :name => @content.author.name)
|
||||
api.comments @page.content.comments
|
||||
api.created_on @page.created_on
|
||||
api.updated_on @content.updated_on
|
||||
|
||||
api.array :attachments do
|
||||
@page.attachments.each do |attachment|
|
||||
render_api_attachment(attachment, api)
|
||||
end
|
||||
end if include_in_api_response?('attachments')
|
||||
end
|
||||
@@ -1,12 +1,15 @@
|
||||
<div class="contextual">
|
||||
<% if @editable %>
|
||||
<%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :id => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.current_version? %>
|
||||
<%= watcher_tag(@page, User.current) %>
|
||||
<%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :id => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
|
||||
<%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :id => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
|
||||
<%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :id => @page.title}, :class => 'icon icon-move') if @content.current_version? %>
|
||||
<%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :id => @page.title}, :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del') %>
|
||||
<%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :id => @page.title, :version => @content.version }, :class => 'icon icon-cancel') unless @content.current_version? %>
|
||||
<% if @content.current_version? %>
|
||||
<%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :id => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
|
||||
<%= watcher_tag(@page, User.current) %>
|
||||
<%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :id => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
|
||||
<%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :id => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
|
||||
<%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :id => @page.title}, :class => 'icon icon-move') %>
|
||||
<%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :id => @page.title}, :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon icon-del') %>
|
||||
<% else %>
|
||||
<%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :id => @page.title, :version => @content.version }, :class => 'icon icon-cancel') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= link_to_if_authorized(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %>
|
||||
</div>
|
||||
@@ -17,15 +20,15 @@
|
||||
<p>
|
||||
<%= link_to(("\xc2\xab " + l(:label_previous)),
|
||||
:action => 'show', :id => @page.title, :project_id => @page.project,
|
||||
:version => (@content.version - 1)) + " - " if @content.version > 1 %>
|
||||
:version => @content.previous.version) + " - " if @content.previous %>
|
||||
<%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
|
||||
<%= '('.html_safe + link_to(l(:label_diff), :controller => 'wiki', :action => 'diff',
|
||||
:id => @page.title, :project_id => @page.project,
|
||||
:version => @content.version) + ')'.html_safe if @content.version > 1 %> -
|
||||
:version => @content.version) + ')'.html_safe if @content.previous %> -
|
||||
<%= link_to((l(:label_next) + " \xc2\xbb"), :action => 'show',
|
||||
:id => @page.title, :project_id => @page.project,
|
||||
:version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
|
||||
<%= link_to(l(:label_current_version), :action => 'show', :id => @page.title, :project_id => @page.project) %>
|
||||
:version => @content.next.version) + " - " if @content.next %>
|
||||
<%= link_to(l(:label_current_version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => nil) %>
|
||||
<br />
|
||||
<em><%= @content.author ? link_to_user(@content.author) : l(:label_user_anonymous)
|
||||
%>, <%= format_time(@content.updated_on) %> </em><br />
|
||||
|
||||
@@ -50,6 +50,9 @@ module RedmineApp
|
||||
|
||||
config.action_mailer.perform_deliveries = false
|
||||
|
||||
# Do not include all helpers
|
||||
config.action_controller.include_all_helpers = false
|
||||
|
||||
config.session_store :cookie_store, :key => '_redmine_session'
|
||||
|
||||
if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
|
||||
|
||||
@@ -166,6 +166,28 @@ default:
|
||||
# the ImageMagick's `convert` binary. Used to generate attachment thumbnails.
|
||||
#imagemagick_convert_command:
|
||||
|
||||
# Configuration of RMagcik font.
|
||||
#
|
||||
# Redmine uses RMagcik in order to export gantt png.
|
||||
# You don't need this setting if you don't install RMagcik.
|
||||
#
|
||||
# In CJK (Chinese, Japanese and Korean),
|
||||
# in order to show CJK characters correctly,
|
||||
# you need to set this configuration.
|
||||
#
|
||||
# Because there is no standard font across platforms in CJK,
|
||||
# you need to set a font installed in your server.
|
||||
#
|
||||
# This setting is not necessary in non CJK.
|
||||
#
|
||||
# Examples for Japanese:
|
||||
# Windows:
|
||||
# rmagick_font_path: C:\windows\fonts\msgothic.ttc
|
||||
# Linux:
|
||||
# rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf
|
||||
#
|
||||
rmagick_font_path:
|
||||
|
||||
# specific configuration options for production environment
|
||||
# that overrides the default ones
|
||||
production:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user