Compare commits
989
Commits
2.0.2
...
2.2-stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4cd22bb53a | ||
|
|
7d8db59dae | ||
|
|
d1606fd3e3 | ||
|
|
097d9661c9 | ||
|
|
f8cedf00a8 | ||
|
|
2461d12388 | ||
|
|
bcd8ce2de2 | ||
|
|
0cd9149e7a | ||
|
|
69d0660760 | ||
|
|
3f7a7d9b14 | ||
|
|
66f881aad3 | ||
|
|
d3d719c0e7 | ||
|
|
03aa7037a5 | ||
|
|
ffb981255c | ||
|
|
3bbb2566c8 | ||
|
|
8cfec7ddf5 | ||
|
|
af177c3a59 | ||
|
|
25aeb4c3d3 | ||
|
|
b75afd49de | ||
|
|
0cd08ed08c | ||
|
|
ad71bffe34 | ||
|
|
a2edef9035 | ||
|
|
abcfbd9c75 | ||
|
|
d0ca8eb703 | ||
|
|
e637c15660 | ||
|
|
f9d0b4776f | ||
|
|
3b5fd45fae | ||
|
|
6b0e8fd82a | ||
|
|
972a991a92 | ||
|
|
62d5b4c710 | ||
|
|
197a14a82e | ||
|
|
ff86c37ed3 | ||
|
|
5e9320137b | ||
|
|
18ed4cf373 | ||
|
|
8303e7c168 | ||
|
|
eb58c62f26 | ||
|
|
b72d40a429 | ||
|
|
6a37151b5c | ||
|
|
5003927f13 | ||
|
|
ffcf1925e3 | ||
|
|
ad4ee1c8d8 | ||
|
|
d4fc347a8a | ||
|
|
c25d175623 | ||
|
|
47e375b271 | ||
|
|
eeedd287ad | ||
|
|
2edb8d597a | ||
|
|
23eae0bf3c | ||
|
|
822a627c44 | ||
|
|
3431b860ad | ||
|
|
3483938185 | ||
|
|
2af4bb6a86 | ||
|
|
b9586dd570 | ||
|
|
40df75243b | ||
|
|
0486ce9976 | ||
|
|
621b15c923 | ||
|
|
d1eb07dd8b | ||
|
|
9c5f339efb | ||
|
|
bbed770fa4 | ||
|
|
1f7a944d14 | ||
|
|
ef8c393290 | ||
|
|
bcb4c2cde4 | ||
|
|
3dfef8a00d | ||
|
|
ee2481a3b9 | ||
|
|
99913e80f0 | ||
|
|
48ac467981 | ||
|
|
6e21342c60 | ||
|
|
3f63b5ea93 | ||
|
|
ca7498c2d6 | ||
|
|
1b64e4be5e | ||
|
|
3a32edc3bd | ||
|
|
462c986452 | ||
|
|
c3170e9264 | ||
|
|
2a9c001625 | ||
|
|
94db7c0029 | ||
|
|
e1492f771c | ||
|
|
48b9b805b2 | ||
|
|
8b72710d7e | ||
|
|
265baa1b2c | ||
|
|
139ddd639f | ||
|
|
e4b7b7cc38 | ||
|
|
8eb39d682d | ||
|
|
4bca584024 | ||
|
|
935d8e594b | ||
|
|
374a0cc7cb | ||
|
|
53dd8dc11a | ||
|
|
444987ce91 | ||
|
|
43add0d306 | ||
|
|
3581782ec2 | ||
|
|
8d4c0e5ecd | ||
|
|
9a57cc18e3 | ||
|
|
564de36c08 | ||
|
|
6ea1bc725a | ||
|
|
a7be337a4c | ||
|
|
3a77d543fd | ||
|
|
d33fa1f8c8 | ||
|
|
50506ef621 | ||
|
|
c68ee7f545 | ||
|
|
ebc979e9b1 | ||
|
|
3cc6d5e815 | ||
|
|
a2e59cc956 | ||
|
|
5969df8142 | ||
|
|
2ed4364c8a | ||
|
|
70226f1833 | ||
|
|
a1d8cab6c6 | ||
|
|
3478a21c2f | ||
|
|
44c15c7560 | ||
|
|
d1a52fd301 | ||
|
|
377ef084ad | ||
|
|
665a331cc4 | ||
|
|
185ab2a020 | ||
|
|
02fb4c3b2c | ||
|
|
8376311ee1 | ||
|
|
ac5e3c2036 | ||
|
|
a5c4fcc8f0 | ||
|
|
29d54f5d50 | ||
|
|
fc3a09e49a | ||
|
|
73aece0baf | ||
|
|
af5a814f4c | ||
|
|
91a09bd474 | ||
|
|
3673fbd881 | ||
|
|
d79bcc4369 | ||
|
|
327660eb7f | ||
|
|
49e80c2cea | ||
|
|
3119d1996c | ||
|
|
11d4d8177c | ||
|
|
861ca78179 | ||
|
|
8fb1a7e3cc | ||
|
|
91c875437d | ||
|
|
60574bfb9c | ||
|
|
3e4cbec95e | ||
|
|
dbf41dba68 | ||
|
|
163659d0a5 | ||
|
|
ec02853141 | ||
|
|
2118f3a1fd | ||
|
|
145e5997b2 | ||
|
|
32fcdff69f | ||
|
|
c0c491dd61 | ||
|
|
0a6c1d9c13 | ||
|
|
e52219f09d | ||
|
|
02b2a61e15 | ||
|
|
8b12702ebe | ||
|
|
cbfafcd5e2 | ||
|
|
e06bf0c464 | ||
|
|
f1650b8ff4 | ||
|
|
f82a7a35b0 | ||
|
|
908b960e71 | ||
|
|
0ce0b52342 | ||
|
|
b907398788 | ||
|
|
2e4f3d5a60 | ||
|
|
e0faf5cf84 | ||
|
|
9553325ee3 | ||
|
|
e2d6f0af4e | ||
|
|
405bcc10c0 | ||
|
|
2e536a2c56 | ||
|
|
b6be9bff35 | ||
|
|
b418e27283 | ||
|
|
3676783052 | ||
|
|
599736aca7 | ||
|
|
1749fbf3e6 | ||
|
|
e9de6c1415 | ||
|
|
962134ab4f | ||
|
|
2e35a6800f | ||
|
|
0a5828ee4a | ||
|
|
1949f61d0c | ||
|
|
3f1c35b71e | ||
|
|
1a38434d5f | ||
|
|
f8183429a9 | ||
|
|
2275f46138 | ||
|
|
34cdac1669 | ||
|
|
c7b712067e | ||
|
|
6ca1c117ab | ||
|
|
b603aa74f9 | ||
|
|
c57a2974d1 | ||
|
|
77d94818a4 | ||
|
|
a53894dd22 | ||
|
|
d99dbe9f0c | ||
|
|
3f88fd44ef | ||
|
|
bc153cb61d | ||
|
|
9554f0133f | ||
|
|
f50da1593e | ||
|
|
96306f9dff | ||
|
|
8f0947e578 | ||
|
|
d048d86c50 | ||
|
|
95457c434e | ||
|
|
42570c4426 | ||
|
|
e4518af32a | ||
|
|
08f200c487 | ||
|
|
64f66daedd | ||
|
|
0c72347d61 | ||
|
|
15341bd844 | ||
|
|
7a4c6d2a19 | ||
|
|
1e6938a119 | ||
|
|
9ef719f15e | ||
|
|
2175e4a901 | ||
|
|
3692369584 | ||
|
|
75b0c01431 | ||
|
|
d252848b2e | ||
|
|
3a7207672e | ||
|
|
f8de723c46 | ||
|
|
a0b2ab338e | ||
|
|
308010c7c9 | ||
|
|
6918a8974c | ||
|
|
ba9f1c0388 | ||
|
|
45d9763090 | ||
|
|
62d5b359c0 | ||
|
|
33d6f9e7a1 | ||
|
|
c6106543e1 | ||
|
|
8ee2ae1846 | ||
|
|
a7caf3faf0 | ||
|
|
cc79feabe2 | ||
|
|
71c5d6c8ee | ||
|
|
65524cc1cc | ||
|
|
138f6736b2 | ||
|
|
0880096162 | ||
|
|
571d316b13 | ||
|
|
93d9d459c5 | ||
|
|
c544e9b9c6 | ||
|
|
5565a078f3 | ||
|
|
7306c733bf | ||
|
|
969a5be7da | ||
|
|
6851b0ca81 | ||
|
|
a7a0d47f2e | ||
|
|
93a65c3c82 | ||
|
|
1a088fb31e | ||
|
|
6e7507d580 | ||
|
|
42fdc1d54d | ||
|
|
afc8239bf7 | ||
|
|
8088749f10 | ||
|
|
3c3bdb8bb0 | ||
|
|
9b4d29dc0d | ||
|
|
9eb041fbc7 | ||
|
|
ed165f6716 | ||
|
|
8ed4620bb9 | ||
|
|
8e1f7607ec | ||
|
|
87f284dcb6 | ||
|
|
04f9a321b1 | ||
|
|
f03e21fb45 | ||
|
|
88efd302a7 | ||
|
|
28cdc8adfc | ||
|
|
3209c4c5e4 | ||
|
|
8fa787719a | ||
|
|
bdd29295b4 | ||
|
|
faab8678d4 | ||
|
|
44fcc8919d | ||
|
|
0b31c8ac85 | ||
|
|
46b1a49453 | ||
|
|
cd54e15a8d | ||
|
|
6a2ca5e034 | ||
|
|
387836f8aa | ||
|
|
1f84a8d83e | ||
|
|
dc393cc480 | ||
|
|
d41d493128 | ||
|
|
dadd294a25 | ||
|
|
bd85428b01 | ||
|
|
eb62603144 | ||
|
|
387f09d4f4 | ||
|
|
47e496f049 | ||
|
|
d3bfbb800c | ||
|
|
9c3045eeda | ||
|
|
3eaa998c28 | ||
|
|
e8469e2c5b | ||
|
|
3386008491 | ||
|
|
dee586e9eb | ||
|
|
5b6732cfaf | ||
|
|
b53f9c688b | ||
|
|
87da04c808 | ||
|
|
38060a2cf0 | ||
|
|
c178aded61 | ||
|
|
8553107016 | ||
|
|
21ee2e2cf2 | ||
|
|
035805fbd0 | ||
|
|
51d5a52029 | ||
|
|
6668d7ebd1 | ||
|
|
1c8d03c1e9 | ||
|
|
c790f1ca41 | ||
|
|
d4f7b4af6d | ||
|
|
690b0fb08c | ||
|
|
cc553764ba | ||
|
|
705f96ccde | ||
|
|
cd39b12f2c | ||
|
|
7e5bad993d | ||
|
|
c9a46950de | ||
|
|
ff68fff80e | ||
|
|
bb2af97dcf | ||
|
|
ccc5f64a99 | ||
|
|
6f3c339e5e | ||
|
|
b0757bbdf6 | ||
|
|
a49d50614f | ||
|
|
7b3aaca83c | ||
|
|
922f1d5243 | ||
|
|
ee3478f389 | ||
|
|
07546b4943 | ||
|
|
2b642c0662 | ||
|
|
3e3714c2d6 | ||
|
|
b5a4446f18 | ||
|
|
4fdbeea686 | ||
|
|
bbb14cb57c | ||
|
|
07ce80be2d | ||
|
|
c3c7f1f900 | ||
|
|
fe95692f10 | ||
|
|
537be80be2 | ||
|
|
5c2de4dfc9 | ||
|
|
570e1b1d62 | ||
|
|
7f1fb7ce81 | ||
|
|
5ac4b6670f | ||
|
|
898873a22d | ||
|
|
74ee5c078f | ||
|
|
185175c78e | ||
|
|
ba647689f4 | ||
|
|
471f631dbd | ||
|
|
54d2b07bff | ||
|
|
e23511076c | ||
|
|
072475b8a5 | ||
|
|
071cf55fc8 | ||
|
|
a7bdc5e0af | ||
|
|
712e5be29f | ||
|
|
5097f51784 | ||
|
|
8a3126e175 | ||
|
|
2f55caa1ba | ||
|
|
4c8c5e9187 | ||
|
|
7946f4a696 | ||
|
|
cb09055d60 | ||
|
|
9e06942ef0 | ||
|
|
02e7025685 | ||
|
|
88e1ae892c | ||
|
|
7f0bb136ad | ||
|
|
391d9b14fb | ||
|
|
e0fb60c6b5 | ||
|
|
6c1db9c3a8 | ||
|
|
7c6582009a | ||
|
|
b636695631 | ||
|
|
004a13b75c | ||
|
|
d7b669e50b | ||
|
|
54d55a360a | ||
|
|
18f693f9f7 | ||
|
|
6bf60e8c20 | ||
|
|
823ac010c5 | ||
|
|
0dbe234226 | ||
|
|
9c9b0a4150 | ||
|
|
7502ac484e | ||
|
|
e2851a8b2a | ||
|
|
c929ab5c16 | ||
|
|
165c2be523 | ||
|
|
b9d7a02b51 | ||
|
|
2984dc903c | ||
|
|
5d90228866 | ||
|
|
2dbabde7f4 | ||
|
|
e74d4ecf5f | ||
|
|
23a1ef543f | ||
|
|
7acd04fb87 | ||
|
|
133ca59edb | ||
|
|
1c825df549 | ||
|
|
c11f5a23fe | ||
|
|
ab96a29460 | ||
|
|
c02594af96 | ||
|
|
193b571e67 | ||
|
|
a1d0acd632 | ||
|
|
6adf61fd02 | ||
|
|
986ffb2434 | ||
|
|
efc6abea07 | ||
|
|
851fbaf750 | ||
|
|
7b40767428 | ||
|
|
56666a41b8 | ||
|
|
7a529b24ee | ||
|
|
b379397d95 | ||
|
|
a0c495b953 | ||
|
|
b0bd506201 | ||
|
|
3b207ee77c | ||
|
|
553066e804 | ||
|
|
76a4b81cf3 | ||
|
|
6086aa1be4 | ||
|
|
8a080d8e53 | ||
|
|
6351631ec6 | ||
|
|
5981aee06d | ||
|
|
232edc6237 | ||
|
|
d4dd35a78c | ||
|
|
f4a2d1f342 | ||
|
|
232d57450a | ||
|
|
196f96fff1 | ||
|
|
1d7c0eb7c0 | ||
|
|
bd927eea88 | ||
|
|
a1f17b982c | ||
|
|
cc2ee90f97 | ||
|
|
1eebd030ca | ||
|
|
4cecc1beed | ||
|
|
51a1bf90dd | ||
|
|
ad68830a1b | ||
|
|
1d1aef9d61 | ||
|
|
d00ba6d2bb | ||
|
|
86617fc437 | ||
|
|
65b4515e1a | ||
|
|
a8e5ba0007 | ||
|
|
df41255105 | ||
|
|
bf69f9af9f | ||
|
|
0c435ce630 | ||
|
|
3e5dfed5d6 | ||
|
|
a265e323ad | ||
|
|
56cf381357 | ||
|
|
eb8f455c13 | ||
|
|
6c9401b624 | ||
|
|
f9f5e9e7c6 | ||
|
|
6529035ee1 | ||
|
|
7f6ac407ef | ||
|
|
0ecab46229 | ||
|
|
c38c3c9436 | ||
|
|
2007543539 | ||
|
|
72e5b638e9 | ||
|
|
66390fd0e6 | ||
|
|
21ac4a3d0e | ||
|
|
2ceb6b8230 | ||
|
|
7fb2ddefde | ||
|
|
8334651c4d | ||
|
|
ac56c0c99c | ||
|
|
5961a1e70d | ||
|
|
1221b79e98 | ||
|
|
0cc817fba3 | ||
|
|
7ed2c481d8 | ||
|
|
26e75568cf | ||
|
|
37cbbcea71 | ||
|
|
585d08765e | ||
|
|
3b854bee59 | ||
|
|
6c6aae07a2 | ||
|
|
9c060fe33a | ||
|
|
d3aa9c6a7b | ||
|
|
a787325ed2 | ||
|
|
2311f80c5d | ||
|
|
f62507dae5 | ||
|
|
9e878a9384 | ||
|
|
e3eab40ec4 | ||
|
|
b93e040bb8 | ||
|
|
dbdc9b9da7 | ||
|
|
2c07b478bb | ||
|
|
542b355210 | ||
|
|
897e83a556 | ||
|
|
768e36caf0 | ||
|
|
76eeb64d1a | ||
|
|
c804151780 | ||
|
|
e199c4b823 | ||
|
|
9eef74f09a | ||
|
|
48a557a79e | ||
|
|
4fda1f6cd1 | ||
|
|
12ea96066b | ||
|
|
13dced9d76 | ||
|
|
667693b32c | ||
|
|
965ef33192 | ||
|
|
462d0ddfe8 | ||
|
|
9989ae5a06 | ||
|
|
89feb8a3fb | ||
|
|
fdd36ce713 | ||
|
|
21dac7287b | ||
|
|
74a2c5a521 | ||
|
|
96f5d315ea | ||
|
|
9b63117856 | ||
|
|
888d284136 | ||
|
|
f673027bc1 | ||
|
|
8a491dbae9 | ||
|
|
7c105ec9e9 | ||
|
|
9b60214b3a | ||
|
|
4d12bea397 | ||
|
|
490bfc2934 | ||
|
|
c47293edb1 | ||
|
|
be4ad60058 | ||
|
|
74645eb017 | ||
|
|
26ff9e1c26 | ||
|
|
c73823cd0f | ||
|
|
8d4e528f22 | ||
|
|
898a4164a0 | ||
|
|
d21bacb01d | ||
|
|
8b381e3bd4 | ||
|
|
9f788310b1 | ||
|
|
7fba8bc682 | ||
|
|
2314e41474 | ||
|
|
77bac4b14d | ||
|
|
b7a435f7a0 | ||
|
|
234e5d59ac | ||
|
|
44ea32afba | ||
|
|
c8226509fd | ||
|
|
327d5d2132 | ||
|
|
1c5b214056 | ||
|
|
5bfe80949c | ||
|
|
0523ac387b | ||
|
|
ef01cada8b | ||
|
|
182a215c7b | ||
|
|
2cbf9c9cc4 | ||
|
|
dd9c2cafa7 | ||
|
|
3142183b30 | ||
|
|
ba25d27e3a | ||
|
|
69b46ab1be | ||
|
|
dd1cb4a5bf | ||
|
|
d58e5a0a1e | ||
|
|
9f531a4380 | ||
|
|
733fef458c | ||
|
|
121ce2a390 | ||
|
|
fc7b790081 | ||
|
|
d915a5e36d | ||
|
|
4ee133e77e | ||
|
|
53a0cee57a | ||
|
|
251d62c2bf | ||
|
|
afbff44fb4 | ||
|
|
3ba6718805 | ||
|
|
e1268faa4a | ||
|
|
cfaa0fc23e | ||
|
|
cfddd2ff48 | ||
|
|
87783362cf | ||
|
|
9aef7a3242 | ||
|
|
189be55235 | ||
|
|
84084b0168 | ||
|
|
3a4a708d51 | ||
|
|
e4332ba35f | ||
|
|
d664fdcde9 | ||
|
|
aa18cd54c7 | ||
|
|
59cbc68dde | ||
|
|
221585c7b5 | ||
|
|
e7b9a9c6ff | ||
|
|
472d3a00a3 | ||
|
|
235238b583 | ||
|
|
73500a349b | ||
|
|
b587f693ab | ||
|
|
c77370a9eb | ||
|
|
6206c88dfa | ||
|
|
1ab9e42b9b | ||
|
|
ae8830ed61 | ||
|
|
1996af104d | ||
|
|
e68043e13c | ||
|
|
05481fe96b |
@@ -19,9 +19,11 @@
|
|||||||
/public/plugin_assets
|
/public/plugin_assets
|
||||||
/tmp/*
|
/tmp/*
|
||||||
/tmp/cache/*
|
/tmp/cache/*
|
||||||
|
/tmp/pdf/*
|
||||||
/tmp/sessions/*
|
/tmp/sessions/*
|
||||||
/tmp/sockets/*
|
/tmp/sockets/*
|
||||||
/tmp/test/*
|
/tmp/test/*
|
||||||
|
/tmp/thumbnails/*
|
||||||
/vendor/cache
|
/vendor/cache
|
||||||
/vendor/rails
|
/vendor/rails
|
||||||
*.rbc
|
*.rbc
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ public/dispatch.*
|
|||||||
public/plugin_assets
|
public/plugin_assets
|
||||||
tmp/*
|
tmp/*
|
||||||
tmp/cache/*
|
tmp/cache/*
|
||||||
|
tmp/pdf/*
|
||||||
tmp/sessions/*
|
tmp/sessions/*
|
||||||
tmp/sockets/*
|
tmp/sockets/*
|
||||||
tmp/test/*
|
tmp/test/*
|
||||||
|
tmp/thumbnails/*
|
||||||
vendor/cache
|
vendor/cache
|
||||||
vendor/rails
|
vendor/rails
|
||||||
*.rbc
|
*.rbc
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
source 'http://rubygems.org'
|
source 'http://rubygems.org'
|
||||||
|
|
||||||
gem 'rails', '3.2.3'
|
gem "rails", "3.2.13"
|
||||||
gem 'prototype-rails', '3.2.1'
|
gem "jquery-rails", "~> 2.0.2"
|
||||||
gem "i18n", "~> 0.6.0"
|
gem "i18n", "~> 0.6.0"
|
||||||
gem "coderay", "~> 1.0.6"
|
gem "coderay", "~> 1.0.6"
|
||||||
gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
|
gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
|
||||||
gem "builder"
|
gem "builder", "3.0.0"
|
||||||
|
|
||||||
# Optional gem for LDAP authentication
|
# Optional gem for LDAP authentication
|
||||||
group :ldap do
|
group :ldap do
|
||||||
@@ -41,7 +41,7 @@ end
|
|||||||
|
|
||||||
platforms :mri_18, :mingw_18 do
|
platforms :mri_18, :mingw_18 do
|
||||||
group :mysql do
|
group :mysql do
|
||||||
gem "mysql"
|
gem "mysql", "~> 2.8.1"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -69,11 +69,17 @@ end
|
|||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem "rdoc", ">= 2.4.2"
|
gem "rdoc", ">= 2.4.2"
|
||||||
|
gem "yard"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem "shoulda", "~> 2.11"
|
gem "shoulda", "~> 2.11"
|
||||||
gem "mocha"
|
# 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
|
end
|
||||||
|
|
||||||
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
|
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
|
||||||
|
|||||||
@@ -40,19 +40,26 @@ class AccountController < ApplicationController
|
|||||||
redirect_to home_url
|
redirect_to home_url
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enable user to choose a new password
|
# Lets user choose a new password
|
||||||
def lost_password
|
def lost_password
|
||||||
redirect_to(home_url) && return unless Setting.lost_password?
|
redirect_to(home_url) && return unless Setting.lost_password?
|
||||||
if params[:token]
|
if params[:token]
|
||||||
@token = Token.find_by_action_and_value("recovery", params[:token])
|
@token = Token.find_by_action_and_value("recovery", params[:token].to_s)
|
||||||
redirect_to(home_url) && return unless @token and !@token.expired?
|
if @token.nil? || @token.expired?
|
||||||
|
redirect_to home_url
|
||||||
|
return
|
||||||
|
end
|
||||||
@user = @token.user
|
@user = @token.user
|
||||||
|
unless @user && @user.active?
|
||||||
|
redirect_to home_url
|
||||||
|
return
|
||||||
|
end
|
||||||
if request.post?
|
if request.post?
|
||||||
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
|
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
|
||||||
if @user.save
|
if @user.save
|
||||||
@token.destroy
|
@token.destroy
|
||||||
flash[:notice] = l(:notice_account_password_updated)
|
flash[:notice] = l(:notice_account_password_updated)
|
||||||
redirect_to :action => 'login'
|
redirect_to signin_path
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -60,17 +67,23 @@ class AccountController < ApplicationController
|
|||||||
return
|
return
|
||||||
else
|
else
|
||||||
if request.post?
|
if request.post?
|
||||||
user = User.find_by_mail(params[:mail])
|
user = User.find_by_mail(params[:mail].to_s)
|
||||||
# user not found in db
|
# user not found or not active
|
||||||
(flash.now[:error] = l(:notice_account_unknown_email); return) unless user
|
unless user && user.active?
|
||||||
# user uses an external authentification
|
flash.now[:error] = l(:notice_account_unknown_email)
|
||||||
(flash.now[:error] = l(:notice_can_t_change_password); return) if user.auth_source_id
|
return
|
||||||
|
end
|
||||||
|
# user cannot change its password
|
||||||
|
unless user.change_password_allowed?
|
||||||
|
flash.now[:error] = l(:notice_can_t_change_password)
|
||||||
|
return
|
||||||
|
end
|
||||||
# create a new token for password recovery
|
# create a new token for password recovery
|
||||||
token = Token.new(:user => user, :action => "recovery")
|
token = Token.new(:user => user, :action => "recovery")
|
||||||
if token.save
|
if token.save
|
||||||
Mailer.lost_password(token).deliver
|
Mailer.lost_password(token).deliver
|
||||||
flash[:notice] = l(:notice_account_lost_email_sent)
|
flash[:notice] = l(:notice_account_lost_email_sent)
|
||||||
redirect_to :action => 'login'
|
redirect_to signin_path
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -84,8 +97,9 @@ class AccountController < ApplicationController
|
|||||||
session[:auth_source_registration] = nil
|
session[:auth_source_registration] = nil
|
||||||
@user = User.new(:language => Setting.default_language)
|
@user = User.new(:language => Setting.default_language)
|
||||||
else
|
else
|
||||||
|
user_params = params[:user] || {}
|
||||||
@user = User.new
|
@user = User.new
|
||||||
@user.safe_attributes = params[:user]
|
@user.safe_attributes = user_params
|
||||||
@user.admin = false
|
@user.admin = false
|
||||||
@user.register
|
@user.register
|
||||||
if session[:auth_source_registration]
|
if session[:auth_source_registration]
|
||||||
@@ -100,7 +114,9 @@ class AccountController < ApplicationController
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
@user.login = params[:user][:login]
|
@user.login = params[:user][:login]
|
||||||
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
|
unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank?
|
||||||
|
@user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation]
|
||||||
|
end
|
||||||
|
|
||||||
case Setting.self_registration
|
case Setting.self_registration
|
||||||
when '1'
|
when '1'
|
||||||
@@ -126,7 +142,7 @@ class AccountController < ApplicationController
|
|||||||
token.destroy
|
token.destroy
|
||||||
flash[:notice] = l(:notice_account_activated)
|
flash[:notice] = l(:notice_account_activated)
|
||||||
end
|
end
|
||||||
redirect_to :action => 'login'
|
redirect_to signin_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -194,6 +210,7 @@ class AccountController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def successful_authentication(user)
|
def successful_authentication(user)
|
||||||
|
logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}"
|
||||||
# Valid user
|
# Valid user
|
||||||
self.logged_user = user
|
self.logged_user = user
|
||||||
# generate a key and set cookie if autologin
|
# generate a key and set cookie if autologin
|
||||||
@@ -237,7 +254,7 @@ class AccountController < ApplicationController
|
|||||||
if user.save and token.save
|
if user.save and token.save
|
||||||
Mailer.register(token).deliver
|
Mailer.register(token).deliver
|
||||||
flash[:notice] = l(:notice_account_register_done)
|
flash[:notice] = l(:notice_account_register_done)
|
||||||
redirect_to :action => 'login'
|
redirect_to signin_path
|
||||||
else
|
else
|
||||||
yield if block_given?
|
yield if block_given?
|
||||||
end
|
end
|
||||||
@@ -274,6 +291,6 @@ class AccountController < ApplicationController
|
|||||||
|
|
||||||
def account_pending
|
def account_pending
|
||||||
flash[:notice] = l(:notice_account_pending)
|
flash[:notice] = l(:notice_account_pending)
|
||||||
redirect_to :action => 'login'
|
redirect_to signin_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ class ActivitiesController < ApplicationController
|
|||||||
|
|
||||||
events = @activity.events(@date_from, @date_to)
|
events = @activity.events(@date_from, @date_to)
|
||||||
|
|
||||||
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
|
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
@events_by_day = events.group_by(&:event_date)
|
@events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
|
||||||
render :layout => false if request.xhr?
|
render :layout => false if request.xhr?
|
||||||
}
|
}
|
||||||
format.atom {
|
format.atom {
|
||||||
|
|||||||
@@ -35,46 +35,104 @@ class ApplicationController < ActionController::Base
|
|||||||
cookies.delete(:autologin)
|
cookies.delete(:autologin)
|
||||||
end
|
end
|
||||||
|
|
||||||
before_filter :user_setup, :check_if_login_required, :set_localization
|
before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
|
||||||
|
|
||||||
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
|
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
|
||||||
rescue_from ::Unauthorized, :with => :deny_access
|
rescue_from ::Unauthorized, :with => :deny_access
|
||||||
|
rescue_from ::ActionView::MissingTemplate, :with => :missing_template
|
||||||
|
|
||||||
include Redmine::Search::Controller
|
include Redmine::Search::Controller
|
||||||
include Redmine::MenuManager::MenuController
|
include Redmine::MenuManager::MenuController
|
||||||
helper Redmine::MenuManager::MenuHelper
|
helper Redmine::MenuManager::MenuHelper
|
||||||
|
|
||||||
|
def session_expiration
|
||||||
|
if session[:user_id]
|
||||||
|
if session_expired? && !try_to_autologin
|
||||||
|
reset_session
|
||||||
|
flash[:error] = l(:error_session_expired)
|
||||||
|
redirect_to signin_url
|
||||||
|
else
|
||||||
|
session[:atime] = Time.now.utc.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def session_expired?
|
||||||
|
if Setting.session_lifetime?
|
||||||
|
unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if Setting.session_timeout?
|
||||||
|
unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_user_session(user)
|
||||||
|
session[:user_id] = user.id
|
||||||
|
session[:ctime] = Time.now.utc.to_i
|
||||||
|
session[:atime] = Time.now.utc.to_i
|
||||||
|
end
|
||||||
|
|
||||||
def user_setup
|
def user_setup
|
||||||
# Check the settings cache for each request
|
# Check the settings cache for each request
|
||||||
Setting.check_cache
|
Setting.check_cache
|
||||||
# Find the current user
|
# Find the current user
|
||||||
User.current = find_current_user
|
User.current = find_current_user
|
||||||
|
logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the current user or nil if no user is logged in
|
# Returns the current user or nil if no user is logged in
|
||||||
# and starts a session if needed
|
# and starts a session if needed
|
||||||
def find_current_user
|
def find_current_user
|
||||||
if session[:user_id]
|
user = nil
|
||||||
# existing session
|
unless api_request?
|
||||||
(User.active.find(session[:user_id]) rescue nil)
|
if session[:user_id]
|
||||||
elsif cookies[:autologin] && Setting.autologin?
|
# existing session
|
||||||
# auto-login feature starts a new session
|
user = (User.active.find(session[:user_id]) rescue nil)
|
||||||
user = User.try_to_autologin(cookies[:autologin])
|
elsif autologin_user = try_to_autologin
|
||||||
session[:user_id] = user.id if user
|
user = autologin_user
|
||||||
user
|
elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
|
||||||
elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
|
# RSS key authentication does not start a session
|
||||||
# RSS key authentication does not start a session
|
user = User.find_by_rss_key(params[:key])
|
||||||
User.find_by_rss_key(params[:key])
|
end
|
||||||
elsif Setting.rest_api_enabled? && accept_api_auth?
|
end
|
||||||
|
if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
|
||||||
if (key = api_key_from_request)
|
if (key = api_key_from_request)
|
||||||
# Use API key
|
# Use API key
|
||||||
User.find_by_api_key(key)
|
user = User.find_by_api_key(key)
|
||||||
else
|
else
|
||||||
# HTTP Basic, either username/password or API key/random
|
# HTTP Basic, either username/password or API key/random
|
||||||
authenticate_with_http_basic do |username, password|
|
authenticate_with_http_basic do |username, password|
|
||||||
User.try_to_login(username, password) || User.find_by_api_key(username)
|
user = User.try_to_login(username, password) || User.find_by_api_key(username)
|
||||||
end
|
end
|
||||||
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
|
||||||
|
|
||||||
|
def try_to_autologin
|
||||||
|
if cookies[:autologin] && Setting.autologin?
|
||||||
|
# auto-login feature starts a new session
|
||||||
|
user = User.try_to_autologin(cookies[:autologin])
|
||||||
|
if user
|
||||||
|
reset_session
|
||||||
|
start_user_session(user)
|
||||||
|
end
|
||||||
|
user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -83,7 +141,7 @@ class ApplicationController < ActionController::Base
|
|||||||
reset_session
|
reset_session
|
||||||
if user && user.is_a?(User)
|
if user && user.is_a?(User)
|
||||||
User.current = user
|
User.current = user
|
||||||
session[:user_id] = user.id
|
start_user_session(user)
|
||||||
else
|
else
|
||||||
User.current = User.anonymous
|
User.current = User.anonymous
|
||||||
end
|
end
|
||||||
@@ -218,14 +276,24 @@ class ApplicationController < ActionController::Base
|
|||||||
self.model_object = model
|
self.model_object = model
|
||||||
end
|
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
|
def find_issues
|
||||||
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
|
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
|
||||||
raise ActiveRecord::RecordNotFound if @issues.empty?
|
raise ActiveRecord::RecordNotFound if @issues.empty?
|
||||||
if @issues.detect {|issue| !issue.visible?}
|
raise Unauthorized unless @issues.all?(&:visible?)
|
||||||
deny_access
|
|
||||||
return
|
|
||||||
end
|
|
||||||
@projects = @issues.collect(&:project).compact.uniq
|
@projects = @issues.collect(&:project).compact.uniq
|
||||||
@project = @projects.first if @projects.size == 1
|
@project = @projects.first if @projects.size == 1
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
@@ -235,7 +303,7 @@ class ApplicationController < ActionController::Base
|
|||||||
# make sure that the user is a member of the project (or admin) if project is private
|
# make sure that the user is a member of the project (or admin) if project is private
|
||||||
# used as a before_filter for actions that do not require any particular permission on the project
|
# used as a before_filter for actions that do not require any particular permission on the project
|
||||||
def check_project_privacy
|
def check_project_privacy
|
||||||
if @project && @project.active?
|
if @project && !@project.archived?
|
||||||
if @project.visible?
|
if @project.visible?
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
@@ -249,12 +317,16 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def back_url
|
def back_url
|
||||||
params[:back_url] || request.env['HTTP_REFERER']
|
url = params[:back_url]
|
||||||
|
if url.nil? && referer = request.env['HTTP_REFERER']
|
||||||
|
url = CGI.unescape(referer.to_s)
|
||||||
|
end
|
||||||
|
url
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect_back_or_default(default)
|
def redirect_back_or_default(default)
|
||||||
back_url = CGI.unescape(params[:back_url].to_s)
|
back_url = params[:back_url].to_s
|
||||||
if !back_url.blank?
|
if back_url.present?
|
||||||
begin
|
begin
|
||||||
uri = URI.parse(back_url)
|
uri = URI.parse(back_url)
|
||||||
# do not redirect user to another host or to the login or register page
|
# do not redirect user to another host or to the login or register page
|
||||||
@@ -263,6 +335,7 @@ class ApplicationController < ActionController::Base
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
rescue URI::InvalidURIError
|
rescue URI::InvalidURIError
|
||||||
|
logger.warn("Could not redirect to invalid URL #{back_url}")
|
||||||
# redirect to default
|
# redirect to default
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -306,13 +379,17 @@ class ApplicationController < ActionController::Base
|
|||||||
format.html {
|
format.html {
|
||||||
render :template => 'common/error', :layout => use_layout, :status => @status
|
render :template => 'common/error', :layout => use_layout, :status => @status
|
||||||
}
|
}
|
||||||
format.atom { head @status }
|
format.any { head @status }
|
||||||
format.xml { head @status }
|
|
||||||
format.js { head @status }
|
|
||||||
format.json { head @status }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handler for ActionView::MissingTemplate exception
|
||||||
|
def missing_template
|
||||||
|
logger.warn "Missing template, responding with 404"
|
||||||
|
@project = nil
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
# Filter for actions that provide an API response
|
# Filter for actions that provide an API response
|
||||||
# but have no HTML representation for non admin users
|
# but have no HTML representation for non admin users
|
||||||
def require_admin_or_api_request
|
def require_admin_or_api_request
|
||||||
@@ -345,7 +422,7 @@ class ApplicationController < ActionController::Base
|
|||||||
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
|
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
|
||||||
@items = @items.slice(0, Setting.feeds_limit.to_i)
|
@items = @items.slice(0, Setting.feeds_limit.to_i)
|
||||||
@title = options[:title] || Setting.app_title
|
@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'
|
:content_type => 'application/atom+xml'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -445,12 +522,17 @@ class ApplicationController < ActionController::Base
|
|||||||
# Returns the API key present in the request
|
# Returns the API key present in the request
|
||||||
def api_key_from_request
|
def api_key_from_request
|
||||||
if params[:key].present?
|
if params[:key].present?
|
||||||
params[:key]
|
params[:key].to_s
|
||||||
elsif request.headers["X-Redmine-API-Key"].present?
|
elsif request.headers["X-Redmine-API-Key"].present?
|
||||||
request.headers["X-Redmine-API-Key"]
|
request.headers["X-Redmine-API-Key"].to_s
|
||||||
end
|
end
|
||||||
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
|
# Renders a warning flash if obj has unsaved attachments
|
||||||
def render_attachment_warning_if_needed(obj)
|
def render_attachment_warning_if_needed(obj)
|
||||||
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
|
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
|
||||||
@@ -479,6 +561,17 @@ class ApplicationController < ActionController::Base
|
|||||||
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
|
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Renders a 200 response for successfull updates or deletions via the API
|
||||||
|
def render_api_ok
|
||||||
|
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
|
# Renders API response on validation failure
|
||||||
def render_validation_errors(objects)
|
def render_validation_errors(objects)
|
||||||
if objects.is_a?(Array)
|
if objects.is_a?(Array)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
class AttachmentsController < ApplicationController
|
class AttachmentsController < ApplicationController
|
||||||
before_filter :find_project, :except => :upload
|
before_filter :find_project, :except => :upload
|
||||||
before_filter :file_readable, :read_authorize, :only => [:show, :download]
|
before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
|
||||||
before_filter :delete_authorize, :only => :destroy
|
before_filter :delete_authorize, :only => :destroy
|
||||||
before_filter :authorize_global, :only => :upload
|
before_filter :authorize_global, :only => :upload
|
||||||
|
|
||||||
@@ -52,11 +52,26 @@ class AttachmentsController < ApplicationController
|
|||||||
@attachment.increment_download
|
@attachment.increment_download
|
||||||
end
|
end
|
||||||
|
|
||||||
# images are sent inline
|
if stale?(:etag => @attachment.digest)
|
||||||
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
|
# images are sent inline
|
||||||
:type => detect_content_type(@attachment),
|
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
|
||||||
:disposition => (@attachment.image? ? 'inline' : 'attachment')
|
:type => detect_content_type(@attachment),
|
||||||
|
:disposition => (@attachment.image? ? 'inline' : 'attachment')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def thumbnail
|
||||||
|
if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size])
|
||||||
|
if stale?(:etag => thumbnail)
|
||||||
|
send_file thumbnail,
|
||||||
|
:filename => filename_for_content_disposition(@attachment.filename),
|
||||||
|
:type => detect_content_type(@attachment),
|
||||||
|
:disposition => 'inline'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# No thumbnail for the attachment or thumbnail could not be created
|
||||||
|
render :nothing => true, :status => 404
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload
|
def upload
|
||||||
@@ -69,7 +84,7 @@ class AttachmentsController < ApplicationController
|
|||||||
|
|
||||||
@attachment = Attachment.new(:file => request.raw_post)
|
@attachment = Attachment.new(:file => request.raw_post)
|
||||||
@attachment.author = User.current
|
@attachment.author = User.current
|
||||||
@attachment.filename = Redmine::Utils.random_hex(16)
|
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
|
||||||
|
|
||||||
if @attachment.save
|
if @attachment.save
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|||||||
@@ -20,25 +20,25 @@ class AutoCompletesController < ApplicationController
|
|||||||
|
|
||||||
def issues
|
def issues
|
||||||
@issues = []
|
@issues = []
|
||||||
q = params[:q].to_s
|
q = (params[:q] || params[:term]).to_s.strip
|
||||||
query = (params[:scope] == "all" && Setting.cross_project_issue_relations?) ? Issue : @project.issues
|
if q.present?
|
||||||
if q.match(/^\d+$/)
|
scope = (params[:scope] == "all" || @project.nil? ? Issue : @project.issues).visible
|
||||||
@issues << query.visible.find_by_id(q.to_i)
|
if q.match(/^\d+$/)
|
||||||
|
@issues << scope.find_by_id(q.to_i)
|
||||||
|
end
|
||||||
|
@issues += scope.where("LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%").order("#{Issue.table_name}.id DESC").limit(10).all
|
||||||
|
@issues.compact!
|
||||||
end
|
end
|
||||||
unless q.blank?
|
|
||||||
@issues += query.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
|
|
||||||
end
|
|
||||||
@issues.compact!
|
|
||||||
render :layout => false
|
render :layout => false
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_project
|
def find_project
|
||||||
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
|
if params[:project_id].present?
|
||||||
@project = Project.find(project_id)
|
@project = Project.find(params[:project_id])
|
||||||
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class BoardsController < ApplicationController
|
|||||||
helper :watchers
|
helper :watchers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@boards = @project.boards
|
@boards = @project.boards.includes(:last_message => :author).all
|
||||||
# show the board if there is only one
|
# show the board if there is only one
|
||||||
if @boards.size == 1
|
if @boards.size == 1
|
||||||
@board = @boards.first
|
@board = @boards.first
|
||||||
@@ -39,14 +39,18 @@ class BoardsController < ApplicationController
|
|||||||
sort_init 'updated_on', 'desc'
|
sort_init 'updated_on', 'desc'
|
||||||
sort_update 'created_on' => "#{Message.table_name}.created_on",
|
sort_update 'created_on' => "#{Message.table_name}.created_on",
|
||||||
'replies' => "#{Message.table_name}.replies_count",
|
'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_count = @board.topics.count
|
||||||
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
|
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
|
||||||
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
|
@topics = @board.topics.
|
||||||
:include => [:author, {:last_reply => :author}],
|
reorder("#{Message.table_name}.sticky DESC").
|
||||||
:limit => @topic_pages.items_per_page,
|
includes(:last_reply).
|
||||||
:offset => @topic_pages.current.offset
|
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)
|
@message = Message.new(:board => @board)
|
||||||
render :action => 'show', :layout => !request.xhr?
|
render :action => 'show', :layout => !request.xhr?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ContextMenusController < ApplicationController
|
|||||||
if (@issues.size == 1)
|
if (@issues.size == 1)
|
||||||
@issue = @issues.first
|
@issue = @issues.first
|
||||||
end
|
end
|
||||||
|
@issue_ids = @issues.map(&:id).sort
|
||||||
|
|
||||||
@allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
|
@allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
|
||||||
@projects = @issues.collect(&:project).compact.uniq
|
@projects = @issues.collect(&:project).compact.uniq
|
||||||
@@ -48,6 +49,7 @@ class ContextMenusController < ApplicationController
|
|||||||
@assignables = @projects.map(&:assignable_users).reduce(:&)
|
@assignables = @projects.map(&:assignable_users).reduce(:&)
|
||||||
@trackers = @projects.map(&:trackers).reduce(:&)
|
@trackers = @projects.map(&:trackers).reduce(:&)
|
||||||
end
|
end
|
||||||
|
@versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
|
||||||
|
|
||||||
@priorities = IssuePriority.active.reverse
|
@priorities = IssuePriority.active.reverse
|
||||||
@back = back_url
|
@back = back_url
|
||||||
@@ -65,6 +67,7 @@ class ContextMenusController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
|
||||||
render :layout => false
|
render :layout => false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,26 @@
|
|||||||
class EnumerationsController < ApplicationController
|
class EnumerationsController < ApplicationController
|
||||||
layout 'admin'
|
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 :build_new_enumeration, :only => [:new, :create]
|
||||||
before_filter :find_enumeration, :only => [:edit, :update, :destroy]
|
before_filter :find_enumeration, :only => [:edit, :update, :destroy]
|
||||||
|
accept_api_auth :index
|
||||||
|
|
||||||
helper :custom_fields
|
helper :custom_fields
|
||||||
|
|
||||||
def index
|
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
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@@ -33,7 +46,7 @@ class EnumerationsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
if request.post? && @enumeration.save
|
if request.post? && @enumeration.save
|
||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_to :action => 'index', :type => @enumeration.type
|
redirect_to :action => 'index'
|
||||||
else
|
else
|
||||||
render :action => 'new'
|
render :action => 'new'
|
||||||
end
|
end
|
||||||
@@ -45,7 +58,7 @@ class EnumerationsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
if request.put? && @enumeration.update_attributes(params[:enumeration])
|
if request.put? && @enumeration.update_attributes(params[:enumeration])
|
||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to :action => 'index', :type => @enumeration.type
|
redirect_to :action => 'index'
|
||||||
else
|
else
|
||||||
render :action => 'edit'
|
render :action => 'edit'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,51 +19,34 @@ class GroupsController < ApplicationController
|
|||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_filter :require_admin
|
before_filter :require_admin
|
||||||
|
before_filter :find_group, :except => [:index, :new, :create]
|
||||||
|
accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
|
||||||
|
|
||||||
helper :custom_fields
|
helper :custom_fields
|
||||||
|
|
||||||
# GET /groups
|
|
||||||
# GET /groups.xml
|
|
||||||
def index
|
def index
|
||||||
@groups = Group.find(:all, :order => 'lastname')
|
@groups = Group.sorted.all
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html # index.html.erb
|
format.html
|
||||||
format.xml { render :xml => @groups }
|
format.api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /groups/1
|
|
||||||
# GET /groups/1.xml
|
|
||||||
def show
|
def show
|
||||||
@group = Group.find(params[:id])
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html # show.html.erb
|
format.html
|
||||||
format.xml { render :xml => @group }
|
format.api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /groups/new
|
|
||||||
# GET /groups/new.xml
|
|
||||||
def new
|
def new
|
||||||
@group = Group.new
|
@group = Group.new
|
||||||
|
|
||||||
respond_to do |format|
|
|
||||||
format.html # new.html.erb
|
|
||||||
format.xml { render :xml => @group }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /groups/1/edit
|
|
||||||
def edit
|
|
||||||
@group = Group.find(params[:id], :include => :projects)
|
|
||||||
end
|
|
||||||
|
|
||||||
# POST /groups
|
|
||||||
# POST /groups.xml
|
|
||||||
def create
|
def create
|
||||||
@group = Group.new(params[:group])
|
@group = Group.new
|
||||||
|
@group.safe_attributes = params[:group]
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @group.save
|
if @group.save
|
||||||
@@ -71,102 +54,87 @@ class GroupsController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_to(params[:continue] ? new_group_path : groups_path)
|
redirect_to(params[:continue] ? new_group_path : groups_path)
|
||||||
}
|
}
|
||||||
format.xml { render :xml => @group, :status => :created, :location => @group }
|
format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
|
||||||
else
|
else
|
||||||
format.html { render :action => "new" }
|
format.html { render :action => "new" }
|
||||||
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
|
format.api { render_validation_errors(@group) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# PUT /groups/1
|
def edit
|
||||||
# PUT /groups/1.xml
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@group = Group.find(params[:id])
|
@group.safe_attributes = params[:group]
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @group.update_attributes(params[:group])
|
if @group.save
|
||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
format.html { redirect_to(groups_path) }
|
format.html { redirect_to(groups_path) }
|
||||||
format.xml { head :ok }
|
format.api { render_api_ok }
|
||||||
else
|
else
|
||||||
format.html { render :action => "edit" }
|
format.html { render :action => "edit" }
|
||||||
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
|
format.api { render_validation_errors(@group) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# DELETE /groups/1
|
|
||||||
# DELETE /groups/1.xml
|
|
||||||
def destroy
|
def destroy
|
||||||
@group = Group.find(params[:id])
|
|
||||||
@group.destroy
|
@group.destroy
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to(groups_url) }
|
format.html { redirect_to(groups_url) }
|
||||||
format.xml { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_users
|
def add_users
|
||||||
@group = Group.find(params[:id])
|
@users = User.find_all_by_id(params[:user_id] || params[:user_ids])
|
||||||
users = User.find_all_by_id(params[:user_ids])
|
@group.users << @users if request.post?
|
||||||
@group.users << users if request.post?
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
|
||||||
format.js {
|
format.js
|
||||||
render(:update) {|page|
|
format.api { render_api_ok }
|
||||||
page.replace_html "tab-content-users", :partial => 'groups/users'
|
|
||||||
users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_user
|
def remove_user
|
||||||
@group = Group.find(params[:id])
|
|
||||||
@group.users.delete(User.find(params[:user_id])) if request.delete?
|
@group.users.delete(User.find(params[:user_id])) if request.delete?
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
|
||||||
format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
|
format.js
|
||||||
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def autocomplete_for_user
|
def autocomplete_for_user
|
||||||
@group = Group.find(params[:id])
|
|
||||||
@users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
|
@users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
|
||||||
render :layout => false
|
render :layout => false
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit_membership
|
def edit_membership
|
||||||
@group = Group.find(params[:id])
|
|
||||||
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
|
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
|
||||||
@membership.save if request.post?
|
@membership.save if request.post?
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @membership.valid?
|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
|
||||||
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
|
format.js
|
||||||
format.js {
|
|
||||||
render(:update) {|page|
|
|
||||||
page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
|
|
||||||
page.visual_effect(:highlight, "member-#{@membership.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
format.js {
|
|
||||||
render(:update) {|page|
|
|
||||||
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy_membership
|
def destroy_membership
|
||||||
@group = Group.find(params[:id])
|
|
||||||
Member.find(params[:membership_id]).destroy if request.post?
|
Member.find(params[:membership_id]).destroy if request.post?
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
|
||||||
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
|
format.js
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_group
|
||||||
|
@group = Group.find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class IssueCategoriesController < ApplicationController
|
|||||||
def new
|
def new
|
||||||
@category = @project.issue_categories.build
|
@category = @project.issue_categories.build
|
||||||
@category.safe_attributes = params[:issue_category]
|
@category.safe_attributes = params[:issue_category]
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
format.js
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -52,20 +57,13 @@ class IssueCategoriesController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
|
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
|
||||||
end
|
end
|
||||||
format.js do
|
format.js
|
||||||
# IE doesn't support the replace_html rjs method for select box options
|
|
||||||
render(:update) {|page| page.replace "issue_category_id",
|
|
||||||
content_tag('select', content_tag('option') + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
|
|
||||||
}
|
|
||||||
end
|
|
||||||
format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
|
format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render :action => 'new'}
|
format.html { render :action => 'new'}
|
||||||
format.js do
|
format.js { render :action => 'new'}
|
||||||
render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
|
|
||||||
end
|
|
||||||
format.api { render_validation_errors(@category) }
|
format.api { render_validation_errors(@category) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -82,7 +80,7 @@ class IssueCategoriesController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
|
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
|
||||||
}
|
}
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -102,7 +100,7 @@ class IssueCategoriesController < ApplicationController
|
|||||||
@category.destroy(reassign_to)
|
@category.destroy(reassign_to)
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' }
|
format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' }
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -49,16 +49,9 @@ class IssueRelationsController < ApplicationController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
|
||||||
format.js do
|
format.js {
|
||||||
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
||||||
render :update do |page|
|
}
|
||||||
page.replace_html "relations", :partial => 'issues/relations'
|
|
||||||
if @relation.errors.empty?
|
|
||||||
page << "$('relation_delay').value = ''"
|
|
||||||
page << "$('relation_issue_to_id').value = ''"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
format.api {
|
format.api {
|
||||||
if saved
|
if saved
|
||||||
render :action => 'show', :status => :created, :location => relation_url(@relation)
|
render :action => 'show', :status => :created, :location => relation_url(@relation)
|
||||||
@@ -75,8 +68,8 @@ class IssueRelationsController < ApplicationController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to issue_path } # TODO : does this really work since @issue is always nil? What is it useful to?
|
format.html { redirect_to issue_path } # TODO : does this really work since @issue is always nil? What is it useful to?
|
||||||
format.js { render(:update) {|page| page.remove "relation-#{@relation.id}"} }
|
format.js
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ class IssuesController < ApplicationController
|
|||||||
include SortHelper
|
include SortHelper
|
||||||
include IssuesHelper
|
include IssuesHelper
|
||||||
helper :timelog
|
helper :timelog
|
||||||
helper :gantt
|
|
||||||
include Redmine::Export::PDF
|
include Redmine::Export::PDF
|
||||||
|
|
||||||
def index
|
def index
|
||||||
retrieve_query
|
retrieve_query
|
||||||
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
|
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
|
||||||
sort_update(@query.sortable_columns)
|
sort_update(@query.sortable_columns)
|
||||||
|
@query.sort_criteria = sort_criteria.to_a
|
||||||
|
|
||||||
if @query.valid?
|
if @query.valid?
|
||||||
case params[:format]
|
case params[:format]
|
||||||
@@ -82,7 +82,7 @@ class IssuesController < ApplicationController
|
|||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render :template => 'issues/index', :layout => !request.xhr? }
|
format.html { render :template => 'issues/index', :layout => !request.xhr? }
|
||||||
format.api {
|
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.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') }
|
format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
|
||||||
@@ -100,8 +100,9 @@ class IssuesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
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.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?
|
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
||||||
|
|
||||||
@changesets = @issue.changesets.visible.all
|
@changesets = @issue.changesets.visible.all
|
||||||
@@ -119,7 +120,10 @@ class IssuesController < ApplicationController
|
|||||||
}
|
}
|
||||||
format.api
|
format.api
|
||||||
format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -128,17 +132,7 @@ class IssuesController < ApplicationController
|
|||||||
def new
|
def new
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render :action => 'new', :layout => !request.xhr? }
|
format.html { render :action => 'new', :layout => !request.xhr? }
|
||||||
format.js {
|
format.js { render :partial => 'update_form' }
|
||||||
render(:update) { |page|
|
|
||||||
if params[:project_change]
|
|
||||||
page.replace_html 'all_attributes', :partial => 'form'
|
|
||||||
else
|
|
||||||
page.replace_html 'attributes', :partial => 'attributes'
|
|
||||||
end
|
|
||||||
m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
|
|
||||||
page << "if ($('log_time')) {Element.#{m}('log_time');}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -150,7 +144,7 @@ class IssuesController < ApplicationController
|
|||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
render_attachment_warning_if_needed(@issue)
|
render_attachment_warning_if_needed(@issue)
|
||||||
flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
|
flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
|
||||||
redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
|
redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
|
||||||
{ :action => 'show', :id => @issue })
|
{ :action => 'show', :id => @issue })
|
||||||
}
|
}
|
||||||
@@ -183,12 +177,8 @@ class IssuesController < ApplicationController
|
|||||||
rescue ActiveRecord::StaleObjectError
|
rescue ActiveRecord::StaleObjectError
|
||||||
@conflict = true
|
@conflict = true
|
||||||
if params[:last_journal_id]
|
if params[:last_journal_id]
|
||||||
if params[:last_journal_id].present?
|
@conflict_journals = @issue.journals_after(params[:last_journal_id]).all
|
||||||
last_journal_id = params[:last_journal_id].to_i
|
@conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||||
@conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
|
|
||||||
else
|
|
||||||
@conflict_journals = @issue.journals.all
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -198,7 +188,7 @@ class IssuesController < ApplicationController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
|
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -237,6 +227,7 @@ class IssuesController < ApplicationController
|
|||||||
@categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
|
@categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
|
||||||
if @copy
|
if @copy
|
||||||
@attachments_present = @issues.detect {|i| i.attachments.any?}.present?
|
@attachments_present = @issues.detect {|i| i.attachments.any?}.present?
|
||||||
|
@subtasks_present = @issues.detect {|i| !i.leaf?}.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
|
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
|
||||||
@@ -250,10 +241,20 @@ class IssuesController < ApplicationController
|
|||||||
|
|
||||||
unsaved_issue_ids = []
|
unsaved_issue_ids = []
|
||||||
moved_issues = []
|
moved_issues = []
|
||||||
|
|
||||||
|
if @copy && params[:copy_subtasks].present?
|
||||||
|
# Descendant issues will be copied with the parent task
|
||||||
|
# Don't copy them twice
|
||||||
|
@issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
|
||||||
|
end
|
||||||
|
|
||||||
@issues.each do |issue|
|
@issues.each do |issue|
|
||||||
issue.reload
|
issue.reload
|
||||||
if @copy
|
if @copy
|
||||||
issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
|
issue = issue.copy({},
|
||||||
|
:attachments => params[:copy_attachments].present?,
|
||||||
|
:subtasks => params[:copy_subtasks].present?
|
||||||
|
)
|
||||||
end
|
end
|
||||||
journal = issue.init_journal(User.current, params[:notes])
|
journal = issue.init_journal(User.current, params[:notes])
|
||||||
issue.safe_attributes = attributes
|
issue.safe_attributes = attributes
|
||||||
@@ -308,23 +309,11 @@ class IssuesController < ApplicationController
|
|||||||
end
|
end
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
|
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
|
||||||
|
|
||||||
def find_project
|
def find_project
|
||||||
project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
|
project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
|
||||||
@@ -359,8 +348,7 @@ private
|
|||||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||||
@time_entry.attributes = params[:time_entry]
|
@time_entry.attributes = params[:time_entry]
|
||||||
|
|
||||||
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
|
@issue.init_journal(User.current)
|
||||||
@issue.init_journal(User.current, @notes)
|
|
||||||
|
|
||||||
issue_attributes = params[:issue]
|
issue_attributes = params[:issue]
|
||||||
if issue_attributes && params[:conflict_resolution]
|
if issue_attributes && params[:conflict_resolution]
|
||||||
@@ -369,7 +357,7 @@ private
|
|||||||
issue_attributes = issue_attributes.dup
|
issue_attributes = issue_attributes.dup
|
||||||
issue_attributes.delete(:lock_version)
|
issue_attributes.delete(:lock_version)
|
||||||
when 'add_notes'
|
when 'add_notes'
|
||||||
issue_attributes = {}
|
issue_attributes = issue_attributes.slice(:notes)
|
||||||
when 'cancel'
|
when 'cancel'
|
||||||
redirect_to issue_path(@issue)
|
redirect_to issue_path(@issue)
|
||||||
return false
|
return false
|
||||||
@@ -390,7 +378,8 @@ private
|
|||||||
begin
|
begin
|
||||||
@copy_from = Issue.visible.find(params[:copy_from])
|
@copy_from = Issue.visible.find(params[:copy_from])
|
||||||
@copy_attachments = params[:copy_attachments].present? || request.get?
|
@copy_attachments = params[:copy_attachments].present? || request.get?
|
||||||
@issue.copy_from(@copy_from, :attachments => @copy_attachments)
|
@copy_subtasks = params[:copy_subtasks].present? || request.get?
|
||||||
|
@issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
return
|
return
|
||||||
@@ -402,7 +391,7 @@ private
|
|||||||
end
|
end
|
||||||
|
|
||||||
@issue.project = @project
|
@issue.project = @project
|
||||||
@issue.author = User.current
|
@issue.author ||= User.current
|
||||||
# Tracker must be set before custom field values
|
# Tracker must be set before custom field values
|
||||||
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
|
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
|
||||||
if @issue.tracker.nil?
|
if @issue.tracker.nil?
|
||||||
|
|||||||
@@ -57,26 +57,20 @@ class JournalsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
journal = Journal.find(params[:journal_id]) if params[:journal_id]
|
@journal = Journal.visible.find(params[:journal_id]) if params[:journal_id]
|
||||||
if journal
|
if @journal
|
||||||
user = journal.user
|
user = @journal.user
|
||||||
text = journal.notes
|
text = @journal.notes
|
||||||
else
|
else
|
||||||
user = @issue.author
|
user = @issue.author
|
||||||
text = @issue.description
|
text = @issue.description
|
||||||
end
|
end
|
||||||
# Replaces pre blocks with [...]
|
# Replaces pre blocks with [...]
|
||||||
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
|
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
|
||||||
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
|
@content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
|
||||||
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
|
@content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
render(:update) { |page|
|
render_404
|
||||||
page.<< "$('notes').value = \"#{escape_javascript content}\";"
|
|
||||||
page.show 'update'
|
|
||||||
page << "Form.Element.focus('notes');"
|
|
||||||
page << "Element.scrollTo('update');"
|
|
||||||
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
@@ -103,17 +97,9 @@ class JournalsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def find_journal
|
def find_journal
|
||||||
@journal = Journal.find(params[:id])
|
@journal = Journal.visible.find(params[:id])
|
||||||
@project = @journal.journalized.project
|
@project = @journal.journalized.project
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -63,31 +63,16 @@ class MembersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if members.present? && members.all? {|m| m.valid? }
|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
|
||||||
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
|
format.js { @members = members }
|
||||||
format.js {
|
format.api {
|
||||||
render(:update) {|page|
|
@member = members.first
|
||||||
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
|
if @member.valid?
|
||||||
page << 'hideOnLoad()'
|
|
||||||
members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format.api {
|
|
||||||
@member = members.first
|
|
||||||
render :action => 'show', :status => :created, :location => membership_url(@member)
|
render :action => 'show', :status => :created, :location => membership_url(@member)
|
||||||
}
|
else
|
||||||
else
|
render_validation_errors(@member)
|
||||||
format.js {
|
end
|
||||||
render(:update) {|page|
|
}
|
||||||
errors = members.collect {|m|
|
|
||||||
m.errors.full_messages
|
|
||||||
}.flatten.uniq
|
|
||||||
|
|
||||||
page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', ')))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format.api { render_validation_errors(members.first) }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -98,16 +83,10 @@ class MembersController < ApplicationController
|
|||||||
saved = @member.save
|
saved = @member.save
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
|
||||||
format.js {
|
format.js
|
||||||
render(:update) {|page|
|
|
||||||
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
|
|
||||||
page << 'hideOnLoad()'
|
|
||||||
page.visual_effect(:highlight, "member-#{@member.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format.api {
|
format.api {
|
||||||
if saved
|
if saved
|
||||||
head :ok
|
render_api_ok
|
||||||
else
|
else
|
||||||
render_validation_errors(@member)
|
render_validation_errors(@member)
|
||||||
end
|
end
|
||||||
@@ -121,14 +100,10 @@ class MembersController < ApplicationController
|
|||||||
end
|
end
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
|
||||||
format.js { render(:update) {|page|
|
format.js
|
||||||
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
|
|
||||||
page << 'hideOnLoad()'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format.api {
|
format.api {
|
||||||
if @member.destroyed?
|
if @member.destroyed?
|
||||||
head :ok
|
render_api_ok
|
||||||
else
|
else
|
||||||
head :unprocessable_entity
|
head :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -140,5 +115,4 @@ class MembersController < ApplicationController
|
|||||||
@principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
|
@principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
|
||||||
render :layout => false
|
render :layout => false
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class MessagesController < ApplicationController
|
|||||||
before_filter :find_message, :except => [:new, :preview]
|
before_filter :find_message, :except => [:new, :preview]
|
||||||
before_filter :authorize, :except => [:preview, :edit, :destroy]
|
before_filter :authorize, :except => [:preview, :edit, :destroy]
|
||||||
|
|
||||||
|
helper :boards
|
||||||
helper :watchers
|
helper :watchers
|
||||||
helper :attachments
|
helper :attachments
|
||||||
include AttachmentsHelper
|
include AttachmentsHelper
|
||||||
@@ -59,7 +60,7 @@ class MessagesController < ApplicationController
|
|||||||
if @message.save
|
if @message.save
|
||||||
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
|
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
|
||||||
render_attachment_warning_if_needed(@message)
|
render_attachment_warning_if_needed(@message)
|
||||||
redirect_to :action => 'show', :id => @message
|
redirect_to board_message_path(@board, @message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -76,7 +77,7 @@ class MessagesController < ApplicationController
|
|||||||
attachments = Attachment.attach_files(@reply, params[:attachments])
|
attachments = Attachment.attach_files(@reply, params[:attachments])
|
||||||
render_attachment_warning_if_needed(@reply)
|
render_attachment_warning_if_needed(@reply)
|
||||||
end
|
end
|
||||||
redirect_to :action => 'show', :id => @topic, :r => @reply
|
redirect_to board_message_path(@board, @topic, :r => @reply)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Edit a message
|
# Edit a message
|
||||||
@@ -88,7 +89,7 @@ class MessagesController < ApplicationController
|
|||||||
render_attachment_warning_if_needed(@message)
|
render_attachment_warning_if_needed(@message)
|
||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
@message.reload
|
@message.reload
|
||||||
redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
|
redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,38 +98,32 @@ class MessagesController < ApplicationController
|
|||||||
(render_403; return false) unless @message.destroyable_by?(User.current)
|
(render_403; return false) unless @message.destroyable_by?(User.current)
|
||||||
r = @message.to_param
|
r = @message.to_param
|
||||||
@message.destroy
|
@message.destroy
|
||||||
redirect_to @message.parent.nil? ?
|
if @message.parent
|
||||||
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
|
redirect_to board_message_path(@board, @message.parent, :r => r)
|
||||||
{ :action => 'show', :id => @message.parent, :r => r }
|
else
|
||||||
|
redirect_to project_board_path(@project, @board)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote
|
def quote
|
||||||
user = @message.author
|
@subject = @message.subject
|
||||||
text = @message.content
|
@subject = "RE: #{@subject}" unless @subject.starts_with?('RE:')
|
||||||
subject = @message.subject.gsub('"', '\"')
|
|
||||||
subject = "RE: #{subject}" unless subject.starts_with?('RE:')
|
@content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> "
|
||||||
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
|
@content << @message.content.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
|
||||||
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
|
|
||||||
render(:update) { |page|
|
|
||||||
page << "$('message_subject').value = \"#{subject}\";"
|
|
||||||
page.<< "$('message_content').value = \"#{content}\";"
|
|
||||||
page.show 'reply'
|
|
||||||
page << "Form.Element.focus('message_content');"
|
|
||||||
page << "Element.scrollTo('reply');"
|
|
||||||
page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview
|
def preview
|
||||||
message = @board.messages.find_by_id(params[:id])
|
message = @board.messages.find_by_id(params[:id])
|
||||||
@attachements = message.attachments if message
|
@attachements = message.attachments if message
|
||||||
@text = (params[:message] || params[:reply])[:content]
|
@text = (params[:message] || params[:reply])[:content]
|
||||||
|
@previewed = message
|
||||||
render :partial => 'common/preview'
|
render :partial => 'common/preview'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def find_message
|
def find_message
|
||||||
find_board
|
return unless find_board
|
||||||
@message = @board.messages.find(params[:id], :include => :parent)
|
@message = @board.messages.find(params[:id], :include => :parent)
|
||||||
@topic = @message.root
|
@topic = @message.root
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
@@ -140,5 +135,6 @@ private
|
|||||||
@project = @board.project
|
@project = @board.project
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -135,7 +135,11 @@ class MyController < ApplicationController
|
|||||||
@user = User.current
|
@user = User.current
|
||||||
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
|
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
|
||||||
@block_options = []
|
@block_options = []
|
||||||
BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
|
BLOCKS.each do |k, v|
|
||||||
|
unless %w(top left right).detect {|f| (@blocks[f] ||= []).include?(k)}
|
||||||
|
@block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add a block to user's page
|
# Add a block to user's page
|
||||||
@@ -143,16 +147,17 @@ class MyController < ApplicationController
|
|||||||
# params[:block] : id of the block to add
|
# params[:block] : id of the block to add
|
||||||
def add_block
|
def add_block
|
||||||
block = params[:block].to_s.underscore
|
block = params[:block].to_s.underscore
|
||||||
(render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
|
if block.present? && BLOCKS.key?(block)
|
||||||
@user = User.current
|
@user = User.current
|
||||||
layout = @user.pref[:my_page_layout] || {}
|
layout = @user.pref[:my_page_layout] || {}
|
||||||
# remove if already present in a group
|
# remove if already present in a group
|
||||||
%w(top left right).each {|f| (layout[f] ||= []).delete block }
|
%w(top left right).each {|f| (layout[f] ||= []).delete block }
|
||||||
# add it on top
|
# add it on top
|
||||||
layout['top'].unshift block
|
layout['top'].unshift block
|
||||||
@user.pref[:my_page_layout] = layout
|
@user.pref[:my_page_layout] = layout
|
||||||
@user.pref.save
|
@user.pref.save
|
||||||
render :partial => "block", :locals => {:user => @user, :block_name => block}
|
end
|
||||||
|
redirect_to :action => 'page_layout'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove a block to user's page
|
# Remove a block to user's page
|
||||||
@@ -165,7 +170,7 @@ class MyController < ApplicationController
|
|||||||
%w(top left right).each {|f| (layout[f] ||= []).delete block }
|
%w(top left right).each {|f| (layout[f] ||= []).delete block }
|
||||||
@user.pref[:my_page_layout] = layout
|
@user.pref[:my_page_layout] = layout
|
||||||
@user.pref.save
|
@user.pref.save
|
||||||
render :nothing => true
|
redirect_to :action => 'page_layout'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Change blocks order on user's page
|
# Change blocks order on user's page
|
||||||
@@ -175,7 +180,8 @@ class MyController < ApplicationController
|
|||||||
group = params[:group]
|
group = params[:group]
|
||||||
@user = User.current
|
@user = User.current
|
||||||
if group.is_a?(String)
|
if group.is_a?(String)
|
||||||
group_items = (params["list-#{group}"] || []).collect(&:underscore)
|
group_items = (params["blocks"] || []).collect(&:underscore)
|
||||||
|
group_items.each {|s| s.sub!(/^block_/, '')}
|
||||||
if group_items and group_items.is_a? Array
|
if group_items and group_items.is_a? Array
|
||||||
layout = @user.pref[:my_page_layout] || {}
|
layout = @user.pref[:my_page_layout] || {}
|
||||||
# remove group blocks if they are presents in other groups
|
# remove group blocks if they are presents in other groups
|
||||||
|
|||||||
@@ -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")
|
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
|
||||||
@description = nil
|
@description = nil
|
||||||
end
|
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
|
else
|
||||||
@description = (params[:issue] ? params[:issue][:description] : nil)
|
@description = (params[:issue] ? params[:issue][:description] : nil)
|
||||||
end
|
end
|
||||||
@@ -34,6 +35,10 @@ class PreviewsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def news
|
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)
|
@text = (params[:news] ? params[:news][:description] : nil)
|
||||||
render :partial => 'common/preview'
|
render :partial => 'common/preview'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ class ProjectsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
@projects = Project.visible.find(:all, :order => 'lft')
|
scope = Project
|
||||||
|
unless params[:closed]
|
||||||
|
scope = scope.active
|
||||||
|
end
|
||||||
|
@projects = scope.visible.order('lft').all
|
||||||
}
|
}
|
||||||
format.api {
|
format.api {
|
||||||
@offset, @limit = api_offset_and_limit
|
@offset, @limit = api_offset_and_limit
|
||||||
@@ -65,14 +69,14 @@ class ProjectsController < ApplicationController
|
|||||||
|
|
||||||
def new
|
def new
|
||||||
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
||||||
@trackers = Tracker.all
|
@trackers = Tracker.sorted.all
|
||||||
@project = Project.new
|
@project = Project.new
|
||||||
@project.safe_attributes = params[:project]
|
@project.safe_attributes = params[:project]
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
||||||
@trackers = Tracker.all
|
@trackers = Tracker.sorted.all
|
||||||
@project = Project.new
|
@project = Project.new
|
||||||
@project.safe_attributes = params[:project]
|
@project.safe_attributes = params[:project]
|
||||||
|
|
||||||
@@ -105,18 +109,14 @@ class ProjectsController < ApplicationController
|
|||||||
|
|
||||||
def copy
|
def copy
|
||||||
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
||||||
@trackers = Tracker.all
|
@trackers = Tracker.sorted.all
|
||||||
@root_projects = Project.find(:all,
|
@root_projects = Project.find(:all,
|
||||||
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
|
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
|
||||||
:order => 'name')
|
:order => 'name')
|
||||||
@source_project = Project.find(params[:id])
|
@source_project = Project.find(params[:id])
|
||||||
if request.get?
|
if request.get?
|
||||||
@project = Project.copy_from(@source_project)
|
@project = Project.copy_from(@source_project)
|
||||||
if @project
|
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
|
||||||
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
|
|
||||||
else
|
|
||||||
redirect_to :controller => 'admin', :action => 'projects'
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
Mailer.with_deliveries(params[:notifications] == '1') do
|
Mailer.with_deliveries(params[:notifications] == '1') do
|
||||||
@project = Project.new
|
@project = Project.new
|
||||||
@@ -135,7 +135,8 @@ class ProjectsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to :controller => 'admin', :action => 'projects'
|
# source_project not found
|
||||||
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
# Show @project
|
# Show @project
|
||||||
@@ -152,12 +153,8 @@ class ProjectsController < ApplicationController
|
|||||||
|
|
||||||
cond = @project.project_condition(Setting.display_subprojects_issues?)
|
cond = @project.project_condition(Setting.display_subprojects_issues?)
|
||||||
|
|
||||||
@open_issues_by_tracker = Issue.visible.count(:group => :tracker,
|
@open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker)
|
||||||
:include => [:project, :status, :tracker],
|
@total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
|
||||||
:conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
|
|
||||||
@total_issues_by_tracker = Issue.visible.count(:group => :tracker,
|
|
||||||
:include => [:project, :status, :tracker],
|
|
||||||
:conditions => cond)
|
|
||||||
|
|
||||||
if User.current.allowed_to?(:view_time_entries, @project)
|
if User.current.allowed_to?(:view_time_entries, @project)
|
||||||
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
|
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
|
||||||
@@ -175,7 +172,7 @@ class ProjectsController < ApplicationController
|
|||||||
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
|
||||||
@issue_category ||= IssueCategory.new
|
@issue_category ||= IssueCategory.new
|
||||||
@member ||= @project.members.new
|
@member ||= @project.members.new
|
||||||
@trackers = Tracker.all
|
@trackers = Tracker.sorted.all
|
||||||
@wiki ||= @project.wiki
|
@wiki ||= @project.wiki
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -191,7 +188,7 @@ class ProjectsController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to :action => 'settings', :id => @project
|
redirect_to :action => 'settings', :id => @project
|
||||||
}
|
}
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -224,6 +221,16 @@ class ProjectsController < ApplicationController
|
|||||||
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
|
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
@project.close
|
||||||
|
redirect_to project_path(@project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reopen
|
||||||
|
@project.reopen
|
||||||
|
redirect_to project_path(@project)
|
||||||
|
end
|
||||||
|
|
||||||
# Delete @project
|
# Delete @project
|
||||||
def destroy
|
def destroy
|
||||||
@project_to_destroy = @project
|
@project_to_destroy = @project
|
||||||
@@ -231,7 +238,7 @@ class ProjectsController < ApplicationController
|
|||||||
@project_to_destroy.destroy
|
@project_to_destroy.destroy
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'admin', :action => 'projects' }
|
format.html { redirect_to :controller => 'admin', :action => 'projects' }
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# hide project in layout
|
# hide project in layout
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ReportsController < ApplicationController
|
|||||||
def issue_report
|
def issue_report
|
||||||
@trackers = @project.trackers
|
@trackers = @project.trackers
|
||||||
@versions = @project.shared_versions.sort
|
@versions = @project.shared_versions.sort
|
||||||
@priorities = IssuePriority.all
|
@priorities = IssuePriority.all.reverse
|
||||||
@categories = @project.issue_categories
|
@categories = @project.issue_categories
|
||||||
@assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
|
@assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
|
||||||
@authors = @project.users.sort
|
@authors = @project.users.sort
|
||||||
@@ -53,7 +53,7 @@ class ReportsController < ApplicationController
|
|||||||
@report_title = l(:field_version)
|
@report_title = l(:field_version)
|
||||||
when "priority"
|
when "priority"
|
||||||
@field = "priority_id"
|
@field = "priority_id"
|
||||||
@rows = IssuePriority.all
|
@rows = IssuePriority.all.reverse
|
||||||
@data = Issue.by_priority(@project)
|
@data = Issue.by_priority(@project)
|
||||||
@report_title = l(:field_priority)
|
@report_title = l(:field_priority)
|
||||||
when "category"
|
when "category"
|
||||||
|
|||||||
@@ -42,12 +42,12 @@ class RepositoriesController < ApplicationController
|
|||||||
@repository = Repository.factory(scm)
|
@repository = Repository.factory(scm)
|
||||||
@repository.is_default = @project.repository.nil?
|
@repository.is_default = @project.repository.nil?
|
||||||
@repository.project = @project
|
@repository.project = @project
|
||||||
render :layout => !request.xhr?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
attrs = pickup_extra_info
|
attrs = pickup_extra_info
|
||||||
@repository = Repository.factory(params[:repository_scm], attrs[:attrs])
|
@repository = Repository.factory(params[:repository_scm])
|
||||||
|
@repository.safe_attributes = params[:repository]
|
||||||
if attrs[:attrs_extra].keys.any?
|
if attrs[:attrs_extra].keys.any?
|
||||||
@repository.merge_extra_info(attrs[:attrs_extra])
|
@repository.merge_extra_info(attrs[:attrs_extra])
|
||||||
end
|
end
|
||||||
@@ -64,7 +64,7 @@ class RepositoriesController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
attrs = pickup_extra_info
|
attrs = pickup_extra_info
|
||||||
@repository.attributes = attrs[:attrs]
|
@repository.safe_attributes = attrs[:attrs]
|
||||||
if attrs[:attrs_extra].keys.any?
|
if attrs[:attrs_extra].keys.any?
|
||||||
@repository.merge_extra_info(attrs[:attrs_extra])
|
@repository.merge_extra_info(attrs[:attrs_extra])
|
||||||
end
|
end
|
||||||
@@ -176,6 +176,7 @@ class RepositoriesController < ApplicationController
|
|||||||
send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
|
send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
|
||||||
send_type = Redmine::MimeType.of(@path)
|
send_type = Redmine::MimeType.of(@path)
|
||||||
send_opt[:type] = send_type.to_s if send_type
|
send_opt[:type] = send_type.to_s if send_type
|
||||||
|
send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment')
|
||||||
send_data @content, send_opt
|
send_data @content, send_opt
|
||||||
else
|
else
|
||||||
# Prevent empty lines when displaying a file with Windows style eol
|
# Prevent empty lines when displaying a file with Windows style eol
|
||||||
@@ -234,22 +235,6 @@ class RepositoriesController < ApplicationController
|
|||||||
|
|
||||||
if @issue
|
if @issue
|
||||||
@changeset.issues << @issue
|
@changeset.issues << @issue
|
||||||
respond_to do |format|
|
|
||||||
format.js {
|
|
||||||
render :update do |page|
|
|
||||||
page.replace_html "related-issues", :partial => "related_issues"
|
|
||||||
page.visual_effect :highlight, "related-issue-#{@issue.id}"
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
respond_to do |format|
|
|
||||||
format.js {
|
|
||||||
render :update do |page|
|
|
||||||
page.alert(l(:label_issue) + ' ' + l('activerecord.errors.messages.invalid'))
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -260,14 +245,6 @@ class RepositoriesController < ApplicationController
|
|||||||
if @issue
|
if @issue
|
||||||
@changeset.issues.delete(@issue)
|
@changeset.issues.delete(@issue)
|
||||||
end
|
end
|
||||||
|
|
||||||
respond_to do |format|
|
|
||||||
format.js {
|
|
||||||
render :update do |page|
|
|
||||||
page.remove "related-issue-#{@issue.id}"
|
|
||||||
end if @issue
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff
|
def diff
|
||||||
@@ -454,4 +431,3 @@ class RepositoriesController < ApplicationController
|
|||||||
graph.burn
|
graph.burn
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
class RolesController < ApplicationController
|
class RolesController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_filter :require_admin, :except => :index
|
before_filter :require_admin, :except => [:index, :show]
|
||||||
before_filter :require_admin_or_api_request, :only => :index
|
before_filter :require_admin_or_api_request, :only => [:index, :show]
|
||||||
before_filter :find_role, :only => [:edit, :update, :destroy]
|
before_filter :find_role, :only => [:show, :edit, :update, :destroy]
|
||||||
accept_api_auth :index
|
accept_api_auth :index, :show
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -35,9 +35,18 @@ class RolesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
respond_to do |format|
|
||||||
|
format.api
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
# Prefills the form with 'Non member' role permissions
|
# Prefills the form with 'Non member' role permissions by default
|
||||||
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
|
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
|
||||||
|
if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
|
||||||
|
@role.copy_from(@copy_from)
|
||||||
|
end
|
||||||
@roles = Role.sorted.all
|
@roles = Role.sorted.all
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -46,7 +55,7 @@ class RolesController < ApplicationController
|
|||||||
if request.post? && @role.save
|
if request.post? && @role.save
|
||||||
# workflow copy
|
# workflow copy
|
||||||
if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
|
if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
|
||||||
@role.workflows.copy(copy_from)
|
@role.workflow_rules.copy(copy_from)
|
||||||
end
|
end
|
||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_to :action => 'index'
|
redirect_to :action => 'index'
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class SettingsController < ApplicationController
|
|||||||
redirect_to :action => 'edit', :tab => params[:tab]
|
redirect_to :action => 'edit', :tab => params[:tab]
|
||||||
else
|
else
|
||||||
@options = {}
|
@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
|
@deliveries = ActionMailer::Base.perform_deliveries
|
||||||
|
|
||||||
@guessed_host_and_path = request.host_with_port.dup
|
@guessed_host_and_path = request.host_with_port.dup
|
||||||
|
|||||||
@@ -18,12 +18,13 @@
|
|||||||
class TimelogController < ApplicationController
|
class TimelogController < ApplicationController
|
||||||
menu_item :issues
|
menu_item :issues
|
||||||
|
|
||||||
before_filter :find_project, :only => [:create]
|
before_filter :find_project_for_new_time_entry, :only => [:create]
|
||||||
before_filter :find_time_entry, :only => [:show, :edit, :update]
|
before_filter :find_time_entry, :only => [:show, :edit, :update]
|
||||||
before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
|
before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
|
||||||
before_filter :authorize, :except => [:new, :index, :report]
|
before_filter :authorize, :except => [:new, :index, :report]
|
||||||
|
|
||||||
before_filter :find_optional_project, :only => [:new, :index, :report]
|
before_filter :find_optional_project, :only => [:index, :report]
|
||||||
|
before_filter :find_optional_project_for_new_time_entry, :only => [:new]
|
||||||
before_filter :authorize_global, :only => [:new, :index, :report]
|
before_filter :authorize_global, :only => [:new, :index, :report]
|
||||||
|
|
||||||
accept_rss_auth :index
|
accept_rss_auth :index
|
||||||
@@ -38,7 +39,7 @@ class TimelogController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
sort_init 'spent_on', 'desc'
|
sort_init 'spent_on', 'desc'
|
||||||
sort_update 'spent_on' => 'spent_on',
|
sort_update 'spent_on' => ['spent_on', "#{TimeEntry.table_name}.created_on"],
|
||||||
'user' => 'user_id',
|
'user' => 'user_id',
|
||||||
'activity' => 'activity_id',
|
'activity' => 'activity_id',
|
||||||
'project' => "#{Project.table_name}.name",
|
'project' => "#{Project.table_name}.name",
|
||||||
@@ -133,9 +134,13 @@ class TimelogController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
if params[:continue]
|
if params[:continue]
|
||||||
if params[:project_id]
|
if params[:project_id]
|
||||||
redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue, :back_url => params[:back_url]
|
redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue,
|
||||||
|
:time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
|
||||||
|
:back_url => params[:back_url]
|
||||||
else
|
else
|
||||||
redirect_to :action => 'new', :back_url => params[:back_url]
|
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
|
end
|
||||||
else
|
else
|
||||||
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
|
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
|
||||||
@@ -166,7 +171,7 @@ class TimelogController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
|
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
|
||||||
}
|
}
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -218,7 +223,7 @@ class TimelogController < ApplicationController
|
|||||||
}
|
}
|
||||||
format.api {
|
format.api {
|
||||||
if destroyed
|
if destroyed
|
||||||
head :ok
|
render_api_ok
|
||||||
else
|
else
|
||||||
render_validation_errors(@time_entries)
|
render_validation_errors(@time_entries)
|
||||||
end
|
end
|
||||||
@@ -258,7 +263,7 @@ private
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_project
|
def find_optional_project_for_new_time_entry
|
||||||
if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
|
if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
|
||||||
@project = Project.find(project_id)
|
@project = Project.find(project_id)
|
||||||
end
|
end
|
||||||
@@ -266,14 +271,17 @@ private
|
|||||||
@issue = Issue.find(issue_id)
|
@issue = Issue.find(issue_id)
|
||||||
@project ||= @issue.project
|
@project ||= @issue.project
|
||||||
end
|
end
|
||||||
if @project.nil?
|
|
||||||
render_404
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_project_for_new_time_entry
|
||||||
|
find_optional_project_for_new_time_entry
|
||||||
|
if @project.nil?
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def find_optional_project
|
def find_optional_project
|
||||||
if !params[:issue_id].blank?
|
if !params[:issue_id].blank?
|
||||||
@issue = Issue.find(params[:issue_id])
|
@issue = Issue.find(params[:issue_id])
|
||||||
@@ -300,6 +308,9 @@ private
|
|||||||
when 'last_week'
|
when 'last_week'
|
||||||
@from = Date.today - 7 - (Date.today.cwday - 1)%7
|
@from = Date.today - 7 - (Date.today.cwday - 1)%7
|
||||||
@to = @from + 6
|
@to = @from + 6
|
||||||
|
when 'last_2_weeks'
|
||||||
|
@from = Date.today - 14 - (Date.today.cwday - 1)%7
|
||||||
|
@to = @from + 13
|
||||||
when '7_days'
|
when '7_days'
|
||||||
@from = Date.today - 7
|
@from = Date.today - 7
|
||||||
@to = Date.today
|
@to = Date.today
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class TrackersController < ApplicationController
|
|||||||
render :action => "index", :layout => false if request.xhr?
|
render :action => "index", :layout => false if request.xhr?
|
||||||
}
|
}
|
||||||
format.api {
|
format.api {
|
||||||
@trackers = Tracker.all
|
@trackers = Tracker.sorted.all
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -45,7 +45,7 @@ class TrackersController < ApplicationController
|
|||||||
if request.post? and @tracker.save
|
if request.post? and @tracker.save
|
||||||
# workflow copy
|
# workflow copy
|
||||||
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
|
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
|
||||||
@tracker.workflows.copy(copy_from)
|
@tracker.workflow_rules.copy(copy_from)
|
||||||
end
|
end
|
||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_to :action => 'index'
|
redirect_to :action => 'index'
|
||||||
@@ -80,4 +80,22 @@ class TrackersController < ApplicationController
|
|||||||
end
|
end
|
||||||
redirect_to :action => 'index'
|
redirect_to :action => 'index'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fields
|
||||||
|
if request.post? && params[:trackers]
|
||||||
|
params[:trackers].each do |tracker_id, tracker_params|
|
||||||
|
tracker = Tracker.find_by_id(tracker_id)
|
||||||
|
if tracker
|
||||||
|
tracker.core_fields = tracker_params[:core_fields]
|
||||||
|
tracker.custom_field_ids = tracker_params[:custom_field_ids]
|
||||||
|
tracker.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
flash[:notice] = l(:notice_successful_update)
|
||||||
|
redirect_to :action => 'fields'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
@trackers = Tracker.sorted.all
|
||||||
|
@custom_fields = IssueCustomField.all.sort
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
|
||||||
redirect_to(params[:continue] ?
|
redirect_to(params[:continue] ?
|
||||||
{:controller => 'users', :action => 'new'} :
|
{:controller => 'users', :action => 'new'} :
|
||||||
{:controller => 'users', :action => 'edit', :id => @user}
|
{:controller => 'users', :action => 'edit', :id => @user}
|
||||||
@@ -156,7 +156,7 @@ class UsersController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to_referer_or edit_user_path(@user)
|
redirect_to_referer_or edit_user_path(@user)
|
||||||
}
|
}
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@auth_sources = AuthSource.find(:all)
|
@auth_sources = AuthSource.find(:all)
|
||||||
@@ -174,8 +174,8 @@ class UsersController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
@user.destroy
|
@user.destroy
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to_referer_or(users_url) }
|
format.html { redirect_back_or_default(users_url) }
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -183,21 +183,8 @@ class UsersController < ApplicationController
|
|||||||
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
|
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
|
||||||
@membership.save
|
@membership.save
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @membership.valid?
|
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
|
||||||
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
|
format.js
|
||||||
format.js {
|
|
||||||
render(:update) {|page|
|
|
||||||
page.replace_html "tab-content-memberships", :partial => 'users/memberships'
|
|
||||||
page.visual_effect(:highlight, "member-#{@membership.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
format.js {
|
|
||||||
render(:update) {|page|
|
|
||||||
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -208,7 +195,7 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
|
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
|
||||||
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
|
format.js
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class VersionsController < ApplicationController
|
|||||||
model_object Version
|
model_object Version
|
||||||
before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
|
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_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
|
before_filter :authorize
|
||||||
|
|
||||||
accept_api_auth :index, :show, :create, :update, :destroy
|
accept_api_auth :index, :show, :create, :update, :destroy
|
||||||
@@ -78,13 +78,7 @@ class VersionsController < ApplicationController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
format.js do
|
format.js
|
||||||
render :update do |page|
|
|
||||||
page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
|
|
||||||
page << "showModal('ajax-modal', '600px');"
|
|
||||||
page << "Form.Element.focus('version_name');"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -103,14 +97,7 @@ class VersionsController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
|
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
|
||||||
end
|
end
|
||||||
format.js do
|
format.js
|
||||||
render(:update) {|page|
|
|
||||||
page << 'hideModal();'
|
|
||||||
# IE doesn't support the replace_html rjs method for select box options
|
|
||||||
page.replace "issue_fixed_version_id",
|
|
||||||
content_tag('select', content_tag('option') + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
|
|
||||||
}
|
|
||||||
end
|
|
||||||
format.api do
|
format.api do
|
||||||
render :action => 'show', :status => :created, :location => version_url(@version)
|
render :action => 'show', :status => :created, :location => version_url(@version)
|
||||||
end
|
end
|
||||||
@@ -118,12 +105,7 @@ class VersionsController < ApplicationController
|
|||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render :action => 'new' }
|
format.html { render :action => 'new' }
|
||||||
format.js do
|
format.js { render :action => 'new' }
|
||||||
render :update do |page|
|
|
||||||
page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
|
|
||||||
page << "Form.Element.focus('version_name');"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
format.api { render_validation_errors(@version) }
|
format.api { render_validation_errors(@version) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -144,7 +126,7 @@ class VersionsController < ApplicationController
|
|||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
|
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
|
||||||
}
|
}
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -167,7 +149,7 @@ class VersionsController < ApplicationController
|
|||||||
@version.destroy
|
@version.destroy
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
|
format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
|
||||||
format.api { head :ok }
|
format.api { render_api_ok }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -183,16 +165,11 @@ class VersionsController < ApplicationController
|
|||||||
def status_by
|
def status_by
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render :action => 'show' }
|
format.html { render :action => 'show' }
|
||||||
format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
|
format.js
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def find_project
|
|
||||||
@project = Project.find(params[:project_id])
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
render_404
|
|
||||||
end
|
|
||||||
|
|
||||||
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
|
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
|
||||||
if ids = params[:tracker_ids]
|
if ids = params[:tracker_ids]
|
||||||
@@ -201,5 +178,4 @@ private
|
|||||||
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
|
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,15 +33,6 @@ class WatchersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
respond_to do |format|
|
|
||||||
format.js do
|
|
||||||
render :update do |page|
|
|
||||||
page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
|
|
||||||
page << "showModal('ajax-modal', '400px');"
|
|
||||||
page << "$('ajax-modal').addClassName('new-watcher');"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -53,29 +44,14 @@ class WatchersController < ApplicationController
|
|||||||
end
|
end
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
|
format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
|
||||||
format.js do
|
format.js
|
||||||
render :update do |page|
|
|
||||||
page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
|
|
||||||
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def append
|
def append
|
||||||
if params[:watcher].is_a?(Hash)
|
if params[:watcher].is_a?(Hash)
|
||||||
user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
|
user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
|
||||||
users = User.active.find_all_by_id(user_ids)
|
@users = User.active.find_all_by_id(user_ids)
|
||||||
respond_to do |format|
|
|
||||||
format.js do
|
|
||||||
render :update do |page|
|
|
||||||
users.each do |user|
|
|
||||||
page << %|$$("#issue_watcher_user_ids_#{user.id}").each(function(el){el.remove();});|
|
|
||||||
end
|
|
||||||
page.insert_html :bottom, 'watchers_inputs', :text => watchers_checkboxes(nil, users, true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -83,11 +59,7 @@ class WatchersController < ApplicationController
|
|||||||
@watched.set_watcher(User.find(params[:user_id]), false) if request.post?
|
@watched.set_watcher(User.find(params[:user_id]), false) if request.post?
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :back }
|
format.html { redirect_to :back }
|
||||||
format.js do
|
format.js
|
||||||
render :update do |page|
|
|
||||||
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -117,12 +89,7 @@ private
|
|||||||
@watched.set_watcher(user, watching)
|
@watched.set_watcher(user, watching)
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
|
format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
|
||||||
format.js do
|
format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => @watched} }
|
||||||
render(:update) do |page|
|
|
||||||
c = watcher_css(@watched)
|
|
||||||
page << %|$$(".#{c}").each(function(el){el.innerHTML="#{escape_javascript watcher_link(@watched, user)}"});|
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ class WikiController < ApplicationController
|
|||||||
default_search_scope :wiki_pages
|
default_search_scope :wiki_pages
|
||||||
before_filter :find_wiki, :authorize
|
before_filter :find_wiki, :authorize
|
||||||
before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
|
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
|
helper :attachments
|
||||||
include AttachmentsHelper
|
include AttachmentsHelper
|
||||||
@@ -45,7 +46,13 @@ class WikiController < ApplicationController
|
|||||||
# List of pages, sorted alphabetically and by parent (hierarchy)
|
# List of pages, sorted alphabetically and by parent (hierarchy)
|
||||||
def index
|
def index
|
||||||
load_pages_for_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
|
end
|
||||||
|
|
||||||
# List of page, by last update
|
# List of page, by last update
|
||||||
@@ -57,7 +64,7 @@ class WikiController < ApplicationController
|
|||||||
# display a page (in editing mode if it doesn't exist)
|
# display a page (in editing mode if it doesn't exist)
|
||||||
def show
|
def show
|
||||||
if @page.new_record?
|
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
|
edit
|
||||||
render :action => 'edit'
|
render :action => 'edit'
|
||||||
else
|
else
|
||||||
@@ -66,8 +73,7 @@ class WikiController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
|
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
|
deny_access
|
||||||
redirect_to :version => nil
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@content = @page.content_for_version(params[:version])
|
@content = @page.content_for_version(params[:version])
|
||||||
@@ -89,7 +95,10 @@ class WikiController < ApplicationController
|
|||||||
@content.current_version? &&
|
@content.current_version? &&
|
||||||
Redmine::WikiFormatting.supports_section_edit?
|
Redmine::WikiFormatting.supports_section_edit?
|
||||||
|
|
||||||
render :action => 'show'
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
format.api
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# edit an existing page or a new one
|
# edit an existing page or a new one
|
||||||
@@ -121,50 +130,65 @@ class WikiController < ApplicationController
|
|||||||
# Creates a new page or updates an existing one
|
# Creates a new page or updates an existing one
|
||||||
def update
|
def update
|
||||||
return render_403 unless editable?
|
return render_403 unless editable?
|
||||||
|
was_new_page = @page.new_record?
|
||||||
@page.content = WikiContent.new(:page => @page) if @page.new_record?
|
@page.content = WikiContent.new(:page => @page) if @page.new_record?
|
||||||
@page.safe_attributes = params[:wiki_page]
|
@page.safe_attributes = params[:wiki_page]
|
||||||
|
|
||||||
@content = @page.content_for_version(params[:version])
|
@content = @page.content
|
||||||
@content.text = initial_page_content(@page) if @content.text.blank?
|
content_params = params[:content]
|
||||||
# don't keep previous comment
|
if content_params.nil? && params[:wiki_page].is_a?(Hash)
|
||||||
@content.comments = nil
|
content_params = params[:wiki_page].slice(:text, :comments, :version)
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
content_params ||= {}
|
||||||
|
|
||||||
@content.comments = params[:content][:comments]
|
@content.comments = content_params[:comments]
|
||||||
@text = params[:content][:text]
|
@text = content_params[:text]
|
||||||
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
|
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
|
||||||
@section = params[:section].to_i
|
@section = params[:section].to_i
|
||||||
@section_hash = params[:section_hash]
|
@section_hash = params[:section_hash]
|
||||||
@content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
|
@content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
|
||||||
else
|
else
|
||||||
@content.version = params[:content][:version]
|
@content.version = content_params[:version] if content_params[:version]
|
||||||
@content.text = @text
|
@content.text = @text
|
||||||
end
|
end
|
||||||
@content.author = User.current
|
@content.author = User.current
|
||||||
@page.content = @content
|
|
||||||
if @page.save
|
if @page.save_with_content
|
||||||
attachments = Attachment.attach_files(@page, params[:attachments])
|
attachments = Attachment.attach_files(@page, params[:attachments])
|
||||||
render_attachment_warning_if_needed(@page)
|
render_attachment_warning_if_needed(@page)
|
||||||
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @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
|
else
|
||||||
render :action => 'edit'
|
respond_to do |format|
|
||||||
|
format.html { render :action => 'edit' }
|
||||||
|
format.api { render_validation_errors(@content) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
|
rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
|
||||||
# Optimistic locking exception
|
# Optimistic locking exception
|
||||||
flash.now[:error] = l(:notice_locking_conflict)
|
respond_to do |format|
|
||||||
render :action => 'edit'
|
format.html {
|
||||||
|
flash.now[:error] = l(:notice_locking_conflict)
|
||||||
|
render :action => 'edit'
|
||||||
|
}
|
||||||
|
format.api { render_api_head :conflict }
|
||||||
|
end
|
||||||
rescue ActiveRecord::RecordNotSaved
|
rescue ActiveRecord::RecordNotSaved
|
||||||
render :action => 'edit'
|
respond_to do |format|
|
||||||
|
format.html { render :action => 'edit' }
|
||||||
|
format.api { render_validation_errors(@content) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# rename a page
|
# rename a page
|
||||||
@@ -187,7 +211,7 @@ class WikiController < ApplicationController
|
|||||||
# show page history
|
# show page history
|
||||||
def history
|
def history
|
||||||
@version_count = @page.content.versions.count
|
@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
|
# don't load text
|
||||||
@versions = @page.content.versions.find :all,
|
@versions = @page.content.versions.find :all,
|
||||||
:select => "id, author_id, comments, updated_on, version",
|
:select => "id, author_id, comments, updated_on, version",
|
||||||
@@ -230,16 +254,28 @@ class WikiController < ApplicationController
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
@reassignable_to = @wiki.pages - @page.self_and_descendants
|
@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
|
||||||
end
|
end
|
||||||
@page.destroy
|
@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
|
end
|
||||||
|
|
||||||
# Export wiki to a single pdf or html file
|
# Export wiki to a single pdf or html file
|
||||||
def export
|
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|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
export = render_to_string :action => 'export_multiple', :layout => false
|
export = render_to_string :action => 'export_multiple', :layout => false
|
||||||
@@ -313,6 +349,6 @@ private
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_pages_for_index
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class WikisController < ApplicationController
|
|||||||
@wiki = @project.wiki || Wiki.new(:project => @project)
|
@wiki = @project.wiki || Wiki.new(:project => @project)
|
||||||
@wiki.safe_attributes = params[:wiki]
|
@wiki.safe_attributes = params[:wiki]
|
||||||
@wiki.save if request.post?
|
@wiki.save if request.post?
|
||||||
render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Delete a project's wiki
|
# Delete a project's wiki
|
||||||
|
|||||||
@@ -18,30 +18,27 @@
|
|||||||
class WorkflowsController < ApplicationController
|
class WorkflowsController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_filter :require_admin
|
before_filter :require_admin, :find_roles, :find_trackers
|
||||||
before_filter :find_roles
|
|
||||||
before_filter :find_trackers
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@workflow_counts = Workflow.count_by_tracker_and_role
|
@workflow_counts = WorkflowTransition.count_by_tracker_and_role
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
@role = Role.find_by_id(params[:role_id])
|
@role = Role.find_by_id(params[:role_id]) if params[:role_id]
|
||||||
@tracker = Tracker.find_by_id(params[:tracker_id])
|
@tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
|
||||||
|
|
||||||
if request.post?
|
if request.post?
|
||||||
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
|
WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
|
||||||
(params[:issue_status] || []).each { |status_id, transitions|
|
(params[:issue_status] || []).each { |status_id, transitions|
|
||||||
transitions.each { |new_status_id, options|
|
transitions.each { |new_status_id, options|
|
||||||
author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
|
author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
|
||||||
assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
|
assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
|
||||||
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
|
WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if @role.save
|
if @role.save
|
||||||
flash[:notice] = l(:notice_successful_update)
|
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only]
|
||||||
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -50,10 +47,10 @@ class WorkflowsController < ApplicationController
|
|||||||
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
|
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
|
||||||
@statuses = @tracker.issue_statuses
|
@statuses = @tracker.issue_statuses
|
||||||
end
|
end
|
||||||
@statuses ||= IssueStatus.find(:all, :order => 'position')
|
@statuses ||= IssueStatus.sorted.all
|
||||||
|
|
||||||
if @tracker && @role && @statuses.any?
|
if @tracker && @role && @statuses.any?
|
||||||
workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
|
workflows = WorkflowTransition.where(:role_id => @role.id, :tracker_id => @tracker.id).all
|
||||||
@workflows = {}
|
@workflows = {}
|
||||||
@workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
|
@workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
|
||||||
@workflows['author'] = workflows.select {|w| w.author}
|
@workflows['author'] = workflows.select {|w| w.author}
|
||||||
@@ -61,6 +58,35 @@ class WorkflowsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def permissions
|
||||||
|
@role = Role.find_by_id(params[:role_id]) if params[:role_id]
|
||||||
|
@tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
|
||||||
|
|
||||||
|
if request.post? && @role && @tracker
|
||||||
|
WorkflowPermission.replace_permissions(@tracker, @role, params[:permissions] || {})
|
||||||
|
redirect_to :action => 'permissions', :role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only]
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
|
||||||
|
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
|
||||||
|
@statuses = @tracker.issue_statuses
|
||||||
|
end
|
||||||
|
@statuses ||= IssueStatus.sorted.all
|
||||||
|
|
||||||
|
if @role && @tracker
|
||||||
|
@fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
|
||||||
|
@custom_fields = @tracker.custom_fields
|
||||||
|
|
||||||
|
@permissions = WorkflowPermission.where(:tracker_id => @tracker.id, :role_id => @role.id).all.inject({}) do |h, w|
|
||||||
|
h[w.old_status_id] ||= {}
|
||||||
|
h[w.old_status_id][w.field_name] = w.rule
|
||||||
|
h
|
||||||
|
end
|
||||||
|
@statuses.each {|status| @permissions[status.id] ||= {}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def copy
|
def copy
|
||||||
|
|
||||||
if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
|
if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
|
||||||
@@ -83,7 +109,7 @@ class WorkflowsController < ApplicationController
|
|||||||
elsif @target_trackers.nil? || @target_roles.nil?
|
elsif @target_trackers.nil? || @target_roles.nil?
|
||||||
flash.now[:error] = l(:error_workflow_copy_target)
|
flash.now[:error] = l(:error_workflow_copy_target)
|
||||||
else
|
else
|
||||||
Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
|
WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
|
||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
|
redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
|
||||||
end
|
end
|
||||||
@@ -93,10 +119,10 @@ class WorkflowsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def find_roles
|
def find_roles
|
||||||
@roles = Role.find(:all, :order => 'builtin, position')
|
@roles = Role.sorted.all
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_trackers
|
def find_trackers
|
||||||
@trackers = Tracker.find(:all, :order => 'position')
|
@trackers = Tracker.sorted.all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
module AdminHelper
|
module AdminHelper
|
||||||
def project_status_options_for_select(selected)
|
def project_status_options_for_select(selected)
|
||||||
options_for_select([[l(:label_all), ''],
|
options_for_select([[l(:label_all), ''],
|
||||||
[l(:status_active), '1']], selected.to_s)
|
[l(:project_status_active), '1'],
|
||||||
|
[l(:project_status_closed), '5'],
|
||||||
|
[l(:project_status_archived), '9']], selected.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+203
-123
@@ -43,18 +43,12 @@ module ApplicationHelper
|
|||||||
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
|
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Display a link to remote if user is authorized
|
|
||||||
def link_to_remote_if_authorized(name, options = {}, html_options = nil)
|
|
||||||
url = options[:url] || {}
|
|
||||||
link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Displays a link to user's account page if active
|
# Displays a link to user's account page if active
|
||||||
def link_to_user(user, options={})
|
def link_to_user(user, options={})
|
||||||
if user.is_a?(User)
|
if user.is_a?(User)
|
||||||
name = h(user.name(options[:format]))
|
name = h(user.name(options[:format]))
|
||||||
if user.active?
|
if user.active? || (User.current.admin? && user.logged?)
|
||||||
link_to name, :controller => 'users', :action => 'show', :id => user
|
link_to name, user_path(user), :class => user.css_classes
|
||||||
else
|
else
|
||||||
name
|
name
|
||||||
end
|
end
|
||||||
@@ -70,10 +64,12 @@ module ApplicationHelper
|
|||||||
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
|
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
|
||||||
# link_to_issue(issue, :subject => false) # => Defect #6
|
# link_to_issue(issue, :subject => false) # => Defect #6
|
||||||
# link_to_issue(issue, :project => true) # => Foo - 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={})
|
def link_to_issue(issue, options={})
|
||||||
title = nil
|
title = nil
|
||||||
subject = nil
|
subject = nil
|
||||||
|
text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
|
||||||
if options[:subject] == false
|
if options[:subject] == false
|
||||||
title = truncate(issue.subject, :length => 60)
|
title = truncate(issue.subject, :length => 60)
|
||||||
else
|
else
|
||||||
@@ -82,9 +78,7 @@ module ApplicationHelper
|
|||||||
subject = truncate(subject, :length => options[:truncate])
|
subject = truncate(subject, :length => options[:truncate])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
|
s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
|
||||||
:class => issue.css_classes,
|
|
||||||
:title => title
|
|
||||||
s << h(": #{subject}") if subject
|
s << h(": #{subject}") if subject
|
||||||
s = h("#{issue.project} - ") + s if options[:project]
|
s = h("#{issue.project} - ") + s if options[:project]
|
||||||
s
|
s
|
||||||
@@ -128,7 +122,7 @@ module ApplicationHelper
|
|||||||
h(truncate(message.subject, :length => 60)),
|
h(truncate(message.subject, :length => 60)),
|
||||||
{ :controller => 'messages', :action => 'show',
|
{ :controller => 'messages', :action => 'show',
|
||||||
:board_id => message.board_id,
|
:board_id => message.board_id,
|
||||||
:id => message.root,
|
:id => (message.parent_id || message.id),
|
||||||
:r => (message.parent_id && message.id),
|
:r => (message.parent_id && message.id),
|
||||||
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
|
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
|
||||||
}.merge(options),
|
}.merge(options),
|
||||||
@@ -145,17 +139,27 @@ module ApplicationHelper
|
|||||||
# link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
|
# link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
|
||||||
#
|
#
|
||||||
def link_to_project(project, options={}, html_options = nil)
|
def link_to_project(project, options={}, html_options = nil)
|
||||||
if project.active?
|
if project.archived?
|
||||||
|
h(project)
|
||||||
|
else
|
||||||
url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
|
url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
|
||||||
link_to(h(project), url, html_options)
|
link_to(h(project), url, html_options)
|
||||||
else
|
|
||||||
h(project)
|
|
||||||
end
|
end
|
||||||
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},
|
||||||
|
:title => attachment.filename
|
||||||
|
end
|
||||||
|
|
||||||
def toggle_link(name, id, options={})
|
def toggle_link(name, id, options={})
|
||||||
onclick = "Element.toggle('#{id}'); "
|
onclick = "$('##{id}').toggle(); "
|
||||||
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
|
onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
|
||||||
onclick << "return false;"
|
onclick << "return false;"
|
||||||
link_to(name, "#", :onclick => onclick)
|
link_to(name, "#", :onclick => onclick)
|
||||||
end
|
end
|
||||||
@@ -168,17 +172,12 @@ module ApplicationHelper
|
|||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
def prompt_to_remote(name, text, param, url, html_options = {})
|
|
||||||
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
|
|
||||||
link_to name, {}, html_options
|
|
||||||
end
|
|
||||||
|
|
||||||
def format_activity_title(text)
|
def format_activity_title(text)
|
||||||
h(truncate_single_line(text, :length => 100))
|
h(truncate_single_line(text, :length => 100))
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_activity_day(date)
|
def format_activity_day(date)
|
||||||
date == Date.today ? l(:label_today).titleize : format_date(date)
|
date == User.current.today ? l(:label_today).titleize : format_date(date)
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_activity_description(text)
|
def format_activity_description(text)
|
||||||
@@ -200,13 +199,46 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Renders a tree of projects as a nested set of unordered lists
|
||||||
|
# The given collection may be a subset of the whole project tree
|
||||||
|
# (eg. some intermediate nodes are private and can not be seen)
|
||||||
|
def render_project_nested_lists(projects)
|
||||||
|
s = ''
|
||||||
|
if projects.any?
|
||||||
|
ancestors = []
|
||||||
|
original_project = @project
|
||||||
|
projects.sort_by(&:lft).each do |project|
|
||||||
|
# set the project environment to please macros.
|
||||||
|
@project = project
|
||||||
|
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
|
||||||
|
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
|
||||||
|
else
|
||||||
|
ancestors.pop
|
||||||
|
s << "</li>"
|
||||||
|
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
|
||||||
|
ancestors.pop
|
||||||
|
s << "</ul></li>\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
classes = (ancestors.empty? ? 'root' : 'child')
|
||||||
|
s << "<li class='#{classes}'><div class='#{classes}'>"
|
||||||
|
s << h(block_given? ? yield(project) : project.name)
|
||||||
|
s << "</div>\n"
|
||||||
|
ancestors << project
|
||||||
|
end
|
||||||
|
s << ("</li></ul>\n" * ancestors.size)
|
||||||
|
@project = original_project
|
||||||
|
end
|
||||||
|
s.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
def render_page_hierarchy(pages, node=nil, options={})
|
def render_page_hierarchy(pages, node=nil, options={})
|
||||||
content = ''
|
content = ''
|
||||||
if pages[node]
|
if pages[node]
|
||||||
content << "<ul class=\"pages-hierarchy\">\n"
|
content << "<ul class=\"pages-hierarchy\">\n"
|
||||||
pages[node].each do |page|
|
pages[node].each do |page|
|
||||||
content << "<li>"
|
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))
|
: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 << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
|
||||||
content << "</li>\n"
|
content << "</li>\n"
|
||||||
@@ -237,23 +269,24 @@ module ApplicationHelper
|
|||||||
# Renders the project quick-jump box
|
# Renders the project quick-jump box
|
||||||
def render_project_jump_box
|
def render_project_jump_box
|
||||||
return unless User.current.logged?
|
return unless User.current.logged?
|
||||||
projects = User.current.memberships.collect(&:project).compact.uniq
|
projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
|
||||||
if projects.any?
|
if projects.any?
|
||||||
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
|
options =
|
||||||
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
|
("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
|
||||||
'<option value="" disabled="disabled">---</option>'
|
'<option value="" disabled="disabled">---</option>').html_safe
|
||||||
s << project_tree_options_for_select(projects, :selected => @project) do |p|
|
|
||||||
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
|
options << project_tree_options_for_select(projects, :selected => @project) do |p|
|
||||||
|
{ :value => project_path(:id => p, :jump => current_menu_item) }
|
||||||
end
|
end
|
||||||
s << '</select>'
|
|
||||||
s.html_safe
|
select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_tree_options_for_select(projects, options = {})
|
def project_tree_options_for_select(projects, options = {})
|
||||||
s = ''
|
s = ''
|
||||||
project_tree(projects) do |project, level|
|
project_tree(projects) do |project, level|
|
||||||
name_prefix = (level > 0 ? (' ' * 2 * level + '» ').html_safe : '')
|
name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe
|
||||||
tag_options = {:value => project.id}
|
tag_options = {:value => project.id}
|
||||||
if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
|
if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
|
||||||
tag_options[:selected] = 'selected'
|
tag_options[:selected] = 'selected'
|
||||||
@@ -273,30 +306,6 @@ module ApplicationHelper
|
|||||||
Project.project_tree(projects, &block)
|
Project.project_tree(projects, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_nested_ul(projects, &block)
|
|
||||||
s = ''
|
|
||||||
if projects.any?
|
|
||||||
ancestors = []
|
|
||||||
projects.sort_by(&:lft).each do |project|
|
|
||||||
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
|
|
||||||
s << "<ul>\n"
|
|
||||||
else
|
|
||||||
ancestors.pop
|
|
||||||
s << "</li>"
|
|
||||||
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
|
|
||||||
ancestors.pop
|
|
||||||
s << "</ul></li>\n"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
s << "<li>"
|
|
||||||
s << yield(project).to_s
|
|
||||||
ancestors << project
|
|
||||||
end
|
|
||||||
s << ("</li></ul>\n" * ancestors.size)
|
|
||||||
end
|
|
||||||
s.html_safe
|
|
||||||
end
|
|
||||||
|
|
||||||
def principals_check_box_tags(name, principals)
|
def principals_check_box_tags(name, principals)
|
||||||
s = ''
|
s = ''
|
||||||
principals.sort.each do |principal|
|
principals.sort.each do |principal|
|
||||||
@@ -309,7 +318,7 @@ module ApplicationHelper
|
|||||||
def principals_options_for_select(collection, selected=nil)
|
def principals_options_for_select(collection, selected=nil)
|
||||||
s = ''
|
s = ''
|
||||||
if collection.include?(User.current)
|
if collection.include?(User.current)
|
||||||
s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
|
s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
|
||||||
end
|
end
|
||||||
groups = ''
|
groups = ''
|
||||||
collection.sort.each do |element|
|
collection.sort.each do |element|
|
||||||
@@ -322,6 +331,15 @@ module ApplicationHelper
|
|||||||
s.html_safe
|
s.html_safe
|
||||||
end
|
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
|
# Truncates and returns the string as a single line
|
||||||
def truncate_single_line(string, *args)
|
def truncate_single_line(string, *args)
|
||||||
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
|
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
|
||||||
@@ -352,7 +370,7 @@ module ApplicationHelper
|
|||||||
def time_tag(time)
|
def time_tag(time)
|
||||||
text = distance_of_time_in_words(Time.now, time)
|
text = distance_of_time_in_words(Time.now, time)
|
||||||
if @project
|
if @project
|
||||||
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
|
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
|
||||||
else
|
else
|
||||||
content_tag('acronym', text, :title => format_time(time))
|
content_tag('acronym', text, :title => format_time(time))
|
||||||
end
|
end
|
||||||
@@ -522,6 +540,8 @@ module ApplicationHelper
|
|||||||
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
|
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
|
||||||
only_path = options.delete(:only_path) == false ? false : true
|
only_path = options.delete(:only_path) == false ? false : true
|
||||||
|
|
||||||
|
text = text.dup
|
||||||
|
macros = catch_macros(text)
|
||||||
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
|
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
|
||||||
|
|
||||||
@parsed_headings = []
|
@parsed_headings = []
|
||||||
@@ -529,8 +549,8 @@ module ApplicationHelper
|
|||||||
@current_section = 0 if options[:edit_section_links]
|
@current_section = 0 if options[:edit_section_links]
|
||||||
|
|
||||||
parse_sections(text, project, obj, attr, only_path, options)
|
parse_sections(text, project, obj, attr, only_path, options)
|
||||||
text = parse_non_pre_blocks(text) do |text|
|
text = parse_non_pre_blocks(text, obj, macros) do |text|
|
||||||
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
|
||||||
send method_name, text, project, obj, attr, only_path, options
|
send method_name, text, project, obj, attr, only_path, options
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -543,7 +563,7 @@ module ApplicationHelper
|
|||||||
text.html_safe
|
text.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_non_pre_blocks(text)
|
def parse_non_pre_blocks(text, obj, macros)
|
||||||
s = StringScanner.new(text)
|
s = StringScanner.new(text)
|
||||||
tags = []
|
tags = []
|
||||||
parsed = ''
|
parsed = ''
|
||||||
@@ -552,6 +572,9 @@ module ApplicationHelper
|
|||||||
text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
|
text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
|
||||||
if tags.empty?
|
if tags.empty?
|
||||||
yield text
|
yield text
|
||||||
|
inject_macros(text, obj, macros) if macros.any?
|
||||||
|
else
|
||||||
|
inject_macros(text, obj, macros, false) if macros.any?
|
||||||
end
|
end
|
||||||
parsed << text
|
parsed << text
|
||||||
if tag
|
if tag
|
||||||
@@ -574,8 +597,9 @@ module ApplicationHelper
|
|||||||
|
|
||||||
def parse_inline_attachments(text, project, obj, attr, only_path, options)
|
def parse_inline_attachments(text, project, obj, attr, only_path, options)
|
||||||
# when using an image link, try to use an attachment, if possible
|
# when using an image link, try to use an attachment, if possible
|
||||||
if options[:attachments] || (obj && obj.respond_to?(:attachments))
|
attachments = options[:attachments] || []
|
||||||
attachments = options[:attachments] || obj.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|
|
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
|
||||||
filename, ext, alt, alttext = $1.downcase, $2, $3, $4
|
filename, ext, alt, alttext = $1.downcase, $2, $3, $4
|
||||||
# search for the picture in attachments
|
# search for the picture in attachments
|
||||||
@@ -634,7 +658,7 @@ module ApplicationHelper
|
|||||||
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
|
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
|
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,
|
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
|
||||||
end
|
end
|
||||||
link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
|
link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
|
||||||
@@ -680,10 +704,11 @@ module ApplicationHelper
|
|||||||
# identifier:document:"Some document"
|
# identifier:document:"Some document"
|
||||||
# identifier:version:1.0.0
|
# identifier:version:1.0.0
|
||||||
# identifier:source:some/file
|
# identifier:source:some/file
|
||||||
def parse_redmine_links(text, project, obj, attr, only_path, options)
|
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|
|
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
|
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
|
link = nil
|
||||||
|
project = default_project
|
||||||
if project_identifier
|
if project_identifier
|
||||||
project = Project.visible.find_by_identifier(project_identifier)
|
project = Project.visible.find_by_identifier(project_identifier)
|
||||||
end
|
end
|
||||||
@@ -707,7 +732,7 @@ module ApplicationHelper
|
|||||||
oid = identifier.to_i
|
oid = identifier.to_i
|
||||||
case prefix
|
case prefix
|
||||||
when nil
|
when nil
|
||||||
if issue = Issue.visible.find_by_id(oid, :include => :status)
|
if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
|
||||||
anchor = comment_id ? "note-#{comment_id}" : nil
|
anchor = comment_id ? "note-#{comment_id}" : nil
|
||||||
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
|
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
|
||||||
:class => issue.css_classes,
|
:class => issue.css_classes,
|
||||||
@@ -769,7 +794,7 @@ module ApplicationHelper
|
|||||||
when 'commit', 'source', 'export'
|
when 'commit', 'source', 'export'
|
||||||
if project
|
if project
|
||||||
repository = nil
|
repository = nil
|
||||||
if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
|
if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
|
||||||
repo_prefix, repo_identifier, name = $1, $2, $3
|
repo_prefix, repo_identifier, name = $1, $2, $3
|
||||||
repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
|
repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
|
||||||
else
|
else
|
||||||
@@ -785,11 +810,10 @@ module ApplicationHelper
|
|||||||
if repository && User.current.allowed_to?(:browse_repository, project)
|
if repository && User.current.allowed_to?(:browse_repository, project)
|
||||||
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
|
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
|
||||||
path, rev, anchor = $1, $3, $5
|
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),
|
:path => to_path_param(path),
|
||||||
:rev => rev,
|
:rev => rev,
|
||||||
:anchor => anchor,
|
:anchor => anchor},
|
||||||
:format => (prefix == 'export' ? 'raw' : nil)},
|
|
||||||
:class => (prefix == 'export' ? 'source download' : 'source')
|
:class => (prefix == 'export' ? 'source download' : 'source')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -797,7 +821,7 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
when 'attachment'
|
when 'attachment'
|
||||||
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
|
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},
|
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
|
||||||
:class => 'attachment'
|
:class => 'attachment'
|
||||||
end
|
end
|
||||||
@@ -812,7 +836,7 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
|
HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
|
||||||
|
|
||||||
def parse_sections(text, project, obj, attr, only_path, options)
|
def parse_sections(text, project, obj, attr, only_path, options)
|
||||||
return unless options[:edit_section_links]
|
return unless options[:edit_section_links]
|
||||||
@@ -851,31 +875,57 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
MACROS_RE = /
|
MACROS_RE = /(
|
||||||
(!)? # escaping
|
(!)? # escaping
|
||||||
(
|
(
|
||||||
\{\{ # opening tag
|
\{\{ # opening tag
|
||||||
([\w]+) # macro name
|
([\w]+) # macro name
|
||||||
(\(([^\}]*)\))? # optional arguments
|
(\(([^\n\r]*?)\))? # optional arguments
|
||||||
|
([\n\r].*?[\n\r])? # optional block of text
|
||||||
\}\} # closing tag
|
\}\} # closing tag
|
||||||
)
|
)
|
||||||
/x unless const_defined?(:MACROS_RE)
|
)/mx unless const_defined?(:MACROS_RE)
|
||||||
|
|
||||||
# Macros substitution
|
MACRO_SUB_RE = /(
|
||||||
def parse_macros(text, project, obj, attr, only_path, options)
|
\{\{
|
||||||
|
macro\((\d+)\)
|
||||||
|
\}\}
|
||||||
|
)/x unless const_defined?(:MACRO_SUB_RE)
|
||||||
|
|
||||||
|
# Extracts macros from text
|
||||||
|
def catch_macros(text)
|
||||||
|
macros = {}
|
||||||
text.gsub!(MACROS_RE) do
|
text.gsub!(MACROS_RE) do
|
||||||
esc, all, macro = $1, $2, $3.downcase
|
all, macro = $1, $4.downcase
|
||||||
args = ($5 || '').split(',').each(&:strip)
|
if macro_exists?(macro) || all =~ MACRO_SUB_RE
|
||||||
if esc.nil?
|
index = macros.size
|
||||||
begin
|
macros[index] = all
|
||||||
exec_macro(macro, obj, args)
|
"{{macro(#{index})}}"
|
||||||
rescue => e
|
|
||||||
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
|
|
||||||
end || all
|
|
||||||
else
|
else
|
||||||
all
|
all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
macros
|
||||||
|
end
|
||||||
|
|
||||||
|
# Executes and replaces macros in text
|
||||||
|
def inject_macros(text, obj, macros, execute=true)
|
||||||
|
text.gsub!(MACRO_SUB_RE) do
|
||||||
|
all, index = $1, $2.to_i
|
||||||
|
orig = macros.delete(index)
|
||||||
|
if execute && orig && orig =~ MACROS_RE
|
||||||
|
esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
|
||||||
|
if esc.nil?
|
||||||
|
h(exec_macro(macro, obj, args, block) || all)
|
||||||
|
else
|
||||||
|
h(all)
|
||||||
|
end
|
||||||
|
elsif orig
|
||||||
|
h(orig)
|
||||||
|
else
|
||||||
|
h(all)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
|
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
|
||||||
@@ -883,6 +933,8 @@ module ApplicationHelper
|
|||||||
# Renders the TOC with given headings
|
# Renders the TOC with given headings
|
||||||
def replace_toc(text, headings)
|
def replace_toc(text, headings)
|
||||||
text.gsub!(TOC_RE) do
|
text.gsub!(TOC_RE) do
|
||||||
|
# Keep only the 4 first levels
|
||||||
|
headings = headings.select{|level, anchor, item| level <= 4}
|
||||||
if headings.empty?
|
if headings.empty?
|
||||||
''
|
''
|
||||||
else
|
else
|
||||||
@@ -921,8 +973,7 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def lang_options_for_select(blank=true)
|
def lang_options_for_select(blank=true)
|
||||||
(blank ? [["(auto)", ""]] : []) +
|
(blank ? [["(auto)", ""]] : []) + languages_options
|
||||||
valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def label_tag_for(name, option_tags = nil, options = {})
|
def label_tag_for(name, option_tags = nil, options = {})
|
||||||
@@ -930,16 +981,6 @@ module ApplicationHelper
|
|||||||
content_tag("label", label_text)
|
content_tag("label", label_text)
|
||||||
end
|
end
|
||||||
|
|
||||||
def labelled_tabular_form_for(*args, &proc)
|
|
||||||
ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
|
|
||||||
args << {} unless args.last.is_a?(Hash)
|
|
||||||
options = args.last
|
|
||||||
options[:html] ||= {}
|
|
||||||
options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
|
|
||||||
options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
|
|
||||||
form_for(*args, &proc)
|
|
||||||
end
|
|
||||||
|
|
||||||
def labelled_form_for(*args, &proc)
|
def labelled_form_for(*args, &proc)
|
||||||
args << {} unless args.last.is_a?(Hash)
|
args << {} unless args.last.is_a?(Hash)
|
||||||
options = args.last
|
options = args.last
|
||||||
@@ -958,6 +999,7 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def labelled_remote_form_for(*args, &proc)
|
def labelled_remote_form_for(*args, &proc)
|
||||||
|
ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
|
||||||
args << {} unless args.last.is_a?(Hash)
|
args << {} unless args.last.is_a?(Hash)
|
||||||
options = args.last
|
options = args.last
|
||||||
options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
|
options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
|
||||||
@@ -978,10 +1020,44 @@ module ApplicationHelper
|
|||||||
html.html_safe
|
html.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_link(url, options={})
|
||||||
|
options = {
|
||||||
|
:method => :delete,
|
||||||
|
:data => {:confirm => l(:text_are_you_sure)},
|
||||||
|
:class => 'icon icon-del'
|
||||||
|
}.merge(options)
|
||||||
|
|
||||||
|
link_to l(:button_delete), url, options
|
||||||
|
end
|
||||||
|
|
||||||
|
def preview_link(url, form, target='preview', options={})
|
||||||
|
content_tag 'a', l(:label_preview), {
|
||||||
|
:href => "#",
|
||||||
|
:onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
|
||||||
|
:accesskey => accesskey(:preview)
|
||||||
|
}.merge(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_to_function(name, function, html_options={})
|
||||||
|
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']
|
||||||
|
url = CGI.unescape(referer.to_s)
|
||||||
|
end
|
||||||
|
url
|
||||||
|
end
|
||||||
|
|
||||||
def back_url_hidden_field_tag
|
def back_url_hidden_field_tag
|
||||||
back_url = params[:back_url] || request.env['HTTP_REFERER']
|
url = back_url
|
||||||
back_url = CGI.unescape(back_url.to_s)
|
hidden_field_tag('back_url', url, :id => nil) unless url.blank?
|
||||||
hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_all_links(form_name)
|
def check_all_links(form_name)
|
||||||
@@ -1025,35 +1101,34 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
@context_menu_included = true
|
@context_menu_included = true
|
||||||
end
|
end
|
||||||
javascript_tag "new ContextMenu('#{ url_for(url) }')"
|
javascript_tag "contextMenuInit('#{ url_for(url) }')"
|
||||||
end
|
end
|
||||||
|
|
||||||
def calendar_for(field_id)
|
def calendar_for(field_id)
|
||||||
include_calendar_headers_tags
|
include_calendar_headers_tags
|
||||||
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
|
javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
|
||||||
javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_calendar_headers_tags
|
def include_calendar_headers_tags
|
||||||
unless @calendar_headers_tags_included
|
unless @calendar_headers_tags_included
|
||||||
@calendar_headers_tags_included = true
|
@calendar_headers_tags_included = true
|
||||||
content_for :header_tags do
|
content_for :header_tags do
|
||||||
start_of_week = case Setting.start_of_week.to_i
|
start_of_week = Setting.start_of_week
|
||||||
when 1
|
start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
|
||||||
'Calendar._FD = 1;' # Monday
|
# Redmine uses 1..7 (monday..sunday) in settings and locales
|
||||||
when 7
|
# JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
|
||||||
'Calendar._FD = 0;' # Sunday
|
start_of_week = start_of_week.to_i % 7
|
||||||
when 6
|
|
||||||
'Calendar._FD = 6;' # Saturday
|
|
||||||
else
|
|
||||||
'' # use language
|
|
||||||
end
|
|
||||||
|
|
||||||
javascript_include_tag('calendar/calendar') +
|
tags = javascript_tag(
|
||||||
javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
|
"var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
|
||||||
javascript_tag(start_of_week) +
|
"showOn: 'button', buttonImageOnly: true, buttonImage: '" +
|
||||||
javascript_include_tag('calendar/calendar-setup') +
|
path_to_image('/images/calendar.png') +
|
||||||
stylesheet_link_tag('calendar')
|
"', showButtonPanel: true};")
|
||||||
|
jquery_locale = l('jquery.locale', :default => current_language.to_s)
|
||||||
|
unless jquery_locale == 'en'
|
||||||
|
tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
|
||||||
|
end
|
||||||
|
tags
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1151,14 +1226,19 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sanitize_anchor_name(anchor)
|
def sanitize_anchor_name(anchor)
|
||||||
anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
|
||||||
|
anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
||||||
|
else
|
||||||
|
# TODO: remove when ruby1.8 is no longer supported
|
||||||
|
anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the javascript tags that are included in the html layout head
|
# Returns the javascript tags that are included in the html layout head
|
||||||
def javascript_heads
|
def javascript_heads
|
||||||
tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
|
tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
|
||||||
unless User.current.pref.warn_on_leaving_unsaved == '0'
|
unless User.current.pref.warn_on_leaving_unsaved == '0'
|
||||||
tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
|
tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
|
||||||
end
|
end
|
||||||
tags
|
tags
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ module AttachmentsHelper
|
|||||||
# Displays view/delete links to the attachments of the given object
|
# Displays view/delete links to the attachments of the given object
|
||||||
# Options:
|
# Options:
|
||||||
# :author -- author names are not displayed if set to false
|
# :author -- author names are not displayed if set to false
|
||||||
|
# :thumbails -- display thumbnails if enabled in settings
|
||||||
def link_to_attachments(container, options = {})
|
def link_to_attachments(container, options = {})
|
||||||
options.assert_valid_keys(:author)
|
options.assert_valid_keys(:author, :thumbnails)
|
||||||
|
|
||||||
if container.attachments.any?
|
if container.attachments.any?
|
||||||
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
|
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
|
||||||
render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
|
render :partial => 'attachments/links',
|
||||||
|
:locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,24 @@
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
module BoardsHelper
|
module BoardsHelper
|
||||||
|
def board_breadcrumb(item)
|
||||||
|
board = item.is_a?(Message) ? item.board : item
|
||||||
|
links = [link_to(l(:label_board_plural), project_boards_path(item.project))]
|
||||||
|
boards = board.ancestors.reverse
|
||||||
|
if item.is_a?(Message)
|
||||||
|
boards << board
|
||||||
|
end
|
||||||
|
links += boards.map {|ancestor| link_to(h(ancestor.name), project_board_path(ancestor.project, ancestor))}
|
||||||
|
breadcrumb links
|
||||||
|
end
|
||||||
|
|
||||||
|
def boards_options_for_select(boards)
|
||||||
|
options = []
|
||||||
|
Board.board_tree(boards) do |board, level|
|
||||||
|
label = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe
|
||||||
|
label << board.name
|
||||||
|
options << [label, board.id]
|
||||||
|
end
|
||||||
|
options
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ module ContextMenusHelper
|
|||||||
end
|
end
|
||||||
if options.delete(:disabled)
|
if options.delete(:disabled)
|
||||||
options.delete(:method)
|
options.delete(:method)
|
||||||
options.delete(:confirm)
|
options.delete(:data)
|
||||||
options.delete(:onclick)
|
options[:onclick] = 'return false;'
|
||||||
options[:class] << ' disabled'
|
options[:class] << ' disabled'
|
||||||
url = '#'
|
url = '#'
|
||||||
end
|
end
|
||||||
@@ -36,7 +36,7 @@ module ContextMenusHelper
|
|||||||
|
|
||||||
def bulk_update_custom_field_context_menu_link(field, text, value)
|
def bulk_update_custom_field_context_menu_link(field, text, value)
|
||||||
context_menu_link h(text),
|
context_menu_link h(text),
|
||||||
{:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back},
|
{:controller => 'issues', :action => 'bulk_update', :ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back},
|
||||||
:method => :post,
|
:method => :post,
|
||||||
:selected => (@issue && @issue.custom_field_value(field) == value)
|
:selected => (@issue && @issue.custom_field_value(field) == value)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,16 +20,7 @@
|
|||||||
module CustomFieldsHelper
|
module CustomFieldsHelper
|
||||||
|
|
||||||
def custom_fields_tabs
|
def custom_fields_tabs
|
||||||
tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
|
CustomField::CUSTOM_FIELDS_TABS
|
||||||
{: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}
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return custom field html tag corresponding to its format
|
# Return custom field html tag corresponding to its format
|
||||||
@@ -73,15 +64,17 @@ module CustomFieldsHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Return custom field label tag
|
# Return custom field label tag
|
||||||
def custom_field_label_tag(name, custom_value)
|
def custom_field_label_tag(name, custom_value, options={})
|
||||||
|
required = options[:required] || custom_value.custom_field.is_required?
|
||||||
|
|
||||||
content_tag "label", h(custom_value.custom_field.name) +
|
content_tag "label", h(custom_value.custom_field.name) +
|
||||||
(custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>".html_safe : ""),
|
(required ? " <span class=\"required\">*</span>".html_safe : ""),
|
||||||
:for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
|
:for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return custom field tag with its label tag
|
# Return custom field tag with its label tag
|
||||||
def custom_field_tag_with_label(name, custom_value)
|
def custom_field_tag_with_label(name, custom_value, options={})
|
||||||
custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
|
custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
|
def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
|
||||||
|
|||||||
@@ -18,15 +18,6 @@
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
module GroupsHelper
|
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
|
def group_settings_tabs
|
||||||
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
|
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
|
||||||
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
|
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ module IssuesHelper
|
|||||||
s = ''
|
s = ''
|
||||||
ancestors = issue.root? ? [] : issue.ancestors.visible.all
|
ancestors = issue.root? ? [] : issue.ancestors.visible.all
|
||||||
ancestors.each do |ancestor|
|
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
|
end
|
||||||
s << '<div>'
|
s << '<div>'
|
||||||
subject = h(issue.subject)
|
subject = h(issue.subject)
|
||||||
@@ -80,18 +80,71 @@ module IssuesHelper
|
|||||||
def render_descendants_tree(issue)
|
def render_descendants_tree(issue)
|
||||||
s = '<form><table class="list issues">'
|
s = '<form><table class="list issues">'
|
||||||
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
|
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',
|
s << content_tag('tr',
|
||||||
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
|
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', h(child.status)) +
|
||||||
content_tag('td', link_to_user(child.assigned_to)) +
|
content_tag('td', link_to_user(child.assigned_to)) +
|
||||||
content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
|
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
|
end
|
||||||
s << '</table></form>'
|
s << '</table></form>'
|
||||||
s.html_safe
|
s.html_safe
|
||||||
end
|
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
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@left = []
|
||||||
|
@right = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def left(*args)
|
||||||
|
args.any? ? @left << cells(*args) : @left
|
||||||
|
end
|
||||||
|
|
||||||
|
def right(*args)
|
||||||
|
args.any? ? @right << cells(*args) : @right
|
||||||
|
end
|
||||||
|
|
||||||
|
def size
|
||||||
|
@left.size > @right.size ? @left.size : @right.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_html
|
||||||
|
html = ''.html_safe
|
||||||
|
blank = content_tag('th', '') + content_tag('td', '')
|
||||||
|
size.times do |i|
|
||||||
|
left = @left[i] || blank
|
||||||
|
right = @right[i] || blank
|
||||||
|
html << content_tag('tr', left + right)
|
||||||
|
end
|
||||||
|
html
|
||||||
|
end
|
||||||
|
|
||||||
|
def cells(label, text, options={})
|
||||||
|
content_tag('th', "#{label}:", options) + content_tag('td', text, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue_fields_rows
|
||||||
|
r = IssueFieldsRows.new
|
||||||
|
yield r
|
||||||
|
r.to_html
|
||||||
|
end
|
||||||
|
|
||||||
def render_custom_fields_rows(issue)
|
def render_custom_fields_rows(issue)
|
||||||
return if issue.custom_field_values.empty?
|
return if issue.custom_field_values.empty?
|
||||||
ordered_values = []
|
ordered_values = []
|
||||||
@@ -248,7 +301,7 @@ module IssuesHelper
|
|||||||
unless no_html
|
unless no_html
|
||||||
label = content_tag('strong', label)
|
label = content_tag('strong', label)
|
||||||
old_value = content_tag("i", h(old_value)) if detail.old_value
|
old_value = content_tag("i", h(old_value)) if detail.old_value
|
||||||
old_value = content_tag("strike", old_value) if detail.old_value and detail.value.blank?
|
old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
|
||||||
if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
|
if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
|
||||||
# Link to the attachment if it has not been removed
|
# Link to the attachment if it has not been removed
|
||||||
value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
|
value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
|
||||||
@@ -297,7 +350,10 @@ module IssuesHelper
|
|||||||
association = Issue.reflect_on_association(field.to_sym)
|
association = Issue.reflect_on_association(field.to_sym)
|
||||||
if association
|
if association
|
||||||
record = association.class_name.constantize.find_by_id(id)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -318,12 +374,16 @@ module IssuesHelper
|
|||||||
def issues_to_csv(issues, project, query, options={})
|
def issues_to_csv(issues, project, query, options={})
|
||||||
decimal_separator = l(:general_csv_decimal_separator)
|
decimal_separator = l(:general_csv_decimal_separator)
|
||||||
encoding = l(:general_csv_encoding)
|
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|
|
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
|
||||||
# csv header fields
|
# csv header fields
|
||||||
csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } +
|
csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
|
||||||
(options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : [])
|
|
||||||
|
|
||||||
# csv lines
|
# csv lines
|
||||||
issues.each do |issue|
|
issues.each do |issue|
|
||||||
@@ -345,8 +405,7 @@ module IssuesHelper
|
|||||||
end
|
end
|
||||||
s.to_s
|
s.to_s
|
||||||
end
|
end
|
||||||
csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } +
|
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)] : [])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
export
|
export
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ module JournalsHelper
|
|||||||
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
|
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
|
||||||
links = []
|
links = []
|
||||||
if !journal.notes.blank?
|
if !journal.notes.blank?
|
||||||
links << link_to_remote(image_tag('comment.png'),
|
links << link_to(image_tag('comment.png'),
|
||||||
{ :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} },
|
{:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal},
|
||||||
:title => l(:button_quote)) if options[:reply_links]
|
:remote => true,
|
||||||
|
:method => 'post',
|
||||||
|
:title => l(:button_quote)) if options[:reply_links]
|
||||||
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
|
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
|
||||||
{ :controller => 'journals', :action => 'edit', :id => journal },
|
{ :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
|
||||||
:title => l(:button_edit)) if editable
|
:title => l(:button_edit)) if editable
|
||||||
end
|
end
|
||||||
content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
|
content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
|
||||||
@@ -38,7 +40,7 @@ module JournalsHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def link_to_in_place_notes_editor(text, field_id, url, options={})
|
def link_to_in_place_notes_editor(text, field_id, url, options={})
|
||||||
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
|
onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;"
|
||||||
link_to text, '#', options.merge(:onclick => onclick)
|
link_to text, '#', options.merge(:onclick => onclick)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -51,38 +51,15 @@ module ProjectsHelper
|
|||||||
content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
|
content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Renders a tree of projects as a nested set of unordered lists
|
# Renders the projects index
|
||||||
# The given collection may be a subset of the whole project tree
|
|
||||||
# (eg. some intermediate nodes are private and can not be seen)
|
|
||||||
def render_project_hierarchy(projects)
|
def render_project_hierarchy(projects)
|
||||||
s = ''
|
render_project_nested_lists(projects) do |project|
|
||||||
if projects.any?
|
s = link_to_project(project, {}, :class => "#{project.css_classes} #{User.current.member_of?(project) ? 'my-project' : nil}")
|
||||||
ancestors = []
|
if project.description.present?
|
||||||
original_project = @project
|
s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
|
||||||
projects.each do |project|
|
|
||||||
# set the project environment to please macros.
|
|
||||||
@project = project
|
|
||||||
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
|
|
||||||
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
|
|
||||||
else
|
|
||||||
ancestors.pop
|
|
||||||
s << "</li>"
|
|
||||||
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
|
|
||||||
ancestors.pop
|
|
||||||
s << "</ul></li>\n"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
classes = (ancestors.empty? ? 'root' : 'child')
|
|
||||||
s << "<li class='#{classes}'><div class='#{classes}'>" +
|
|
||||||
link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
|
|
||||||
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
|
|
||||||
s << "</div>\n"
|
|
||||||
ancestors << project
|
|
||||||
end
|
end
|
||||||
s << ("</li></ul>\n" * ancestors.size)
|
s
|
||||||
@project = original_project
|
|
||||||
end
|
end
|
||||||
s.html_safe
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a set of options for a select field, grouped by project.
|
# Returns a set of options for a select field, grouped by project.
|
||||||
@@ -91,10 +68,6 @@ module ProjectsHelper
|
|||||||
versions.each do |version|
|
versions.each do |version|
|
||||||
grouped[version.project.name] << [version.name, version.id]
|
grouped[version.project.name] << [version.name, version.id]
|
||||||
end
|
end
|
||||||
# Add in the selected
|
|
||||||
if selected && !versions.include?(selected)
|
|
||||||
grouped[selected.project.name] << [selected.name, selected.id]
|
|
||||||
end
|
|
||||||
|
|
||||||
if grouped.keys.size > 1
|
if grouped.keys.size > 1
|
||||||
grouped_options_for_select(grouped, selected && selected.id)
|
grouped_options_for_select(grouped, selected && selected.id)
|
||||||
|
|||||||
@@ -18,9 +18,44 @@
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
module QueriesHelper
|
module QueriesHelper
|
||||||
|
def filters_options_for_select(query)
|
||||||
|
options_for_select(filters_options(query))
|
||||||
|
end
|
||||||
|
|
||||||
def operators_for_select(filter_type)
|
def filters_options(query)
|
||||||
Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
|
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
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def column_header(column)
|
def column_header(column)
|
||||||
@@ -32,7 +67,7 @@ module QueriesHelper
|
|||||||
def column_content(column, issue)
|
def column_content(column, issue)
|
||||||
value = column.value(issue)
|
value = column.value(issue)
|
||||||
if value.is_a?(Array)
|
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
|
else
|
||||||
column_value(column, issue, value)
|
column_value(column, issue, value)
|
||||||
end
|
end
|
||||||
@@ -43,6 +78,8 @@ module QueriesHelper
|
|||||||
when 'String'
|
when 'String'
|
||||||
if column.name == :subject
|
if column.name == :subject
|
||||||
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
|
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
|
else
|
||||||
h(value)
|
h(value)
|
||||||
end
|
end
|
||||||
@@ -50,14 +87,14 @@ module QueriesHelper
|
|||||||
format_time(value)
|
format_time(value)
|
||||||
when 'Date'
|
when 'Date'
|
||||||
format_date(value)
|
format_date(value)
|
||||||
when 'Fixnum', 'Float'
|
when 'Fixnum'
|
||||||
if column.name == :done_ratio
|
if column.name == :done_ratio
|
||||||
progress_bar(value, :width => '80px')
|
progress_bar(value, :width => '80px')
|
||||||
elsif column.name == :spent_hours
|
|
||||||
sprintf "%.2f", value
|
|
||||||
else
|
else
|
||||||
h(value.to_s)
|
value.to_s
|
||||||
end
|
end
|
||||||
|
when 'Float'
|
||||||
|
sprintf "%.2f", value
|
||||||
when 'User'
|
when 'User'
|
||||||
link_to_user value
|
link_to_user value
|
||||||
when 'Project'
|
when 'Project'
|
||||||
@@ -70,6 +107,11 @@ module QueriesHelper
|
|||||||
l(:general_text_No)
|
l(:general_text_No)
|
||||||
when 'Issue'
|
when 'Issue'
|
||||||
link_to_issue(value, :subject => false)
|
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
|
else
|
||||||
h(value)
|
h(value)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,9 +17,6 @@
|
|||||||
# along with this program; if not, write to the Free Software
|
# along with this program; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
require 'iconv'
|
|
||||||
require 'redmine/codeset_util'
|
|
||||||
|
|
||||||
module RepositoriesHelper
|
module RepositoriesHelper
|
||||||
def format_revision(revision)
|
def format_revision(revision)
|
||||||
if revision.respond_to? :format_identifier
|
if revision.respond_to? :format_identifier
|
||||||
@@ -46,17 +43,17 @@ module RepositoriesHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_changeset_changes
|
def render_changeset_changes
|
||||||
changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
|
changes = @changeset.filechanges.find(:all, :limit => 1000, :order => 'path').collect do |change|
|
||||||
case change.action
|
case change.action
|
||||||
when 'A'
|
when 'A'
|
||||||
# Detects moved/copied files
|
# Detects moved/copied files
|
||||||
if !change.from_path.blank?
|
if !change.from_path.blank?
|
||||||
change.action =
|
change.action =
|
||||||
@changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
|
@changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
|
||||||
end
|
end
|
||||||
change
|
change
|
||||||
when 'D'
|
when 'D'
|
||||||
@changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
|
@changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
|
||||||
else
|
else
|
||||||
change
|
change
|
||||||
end
|
end
|
||||||
@@ -141,12 +138,7 @@ module RepositoriesHelper
|
|||||||
select_tag('repository_scm',
|
select_tag('repository_scm',
|
||||||
options_for_select(scm_options, repository.class.name.demodulize),
|
options_for_select(scm_options, repository.class.name.demodulize),
|
||||||
:disabled => (repository && !repository.new_record?),
|
:disabled => (repository && !repository.new_record?),
|
||||||
:onchange => remote_function(
|
:data => {:remote => true, :method => 'get'})
|
||||||
:url => new_project_repository_path(@project),
|
|
||||||
:method => :get,
|
|
||||||
:update => 'content',
|
|
||||||
:with => "Form.serialize(this.form)")
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_leading_slash(path)
|
def with_leading_slash(path)
|
||||||
@@ -159,7 +151,7 @@ module RepositoriesHelper
|
|||||||
|
|
||||||
def subversion_field_tags(form, repository)
|
def subversion_field_tags(form, repository)
|
||||||
content_tag('p', form.text_field(:url, :size => 60, :required => true,
|
content_tag('p', form.text_field(:url, :size => 60, :required => true,
|
||||||
:disabled => (repository && !repository.root_url.blank?)) +
|
:disabled => !repository.safe_attribute?('url')) +
|
||||||
'<br />'.html_safe +
|
'<br />'.html_safe +
|
||||||
'(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
|
'(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
|
||||||
content_tag('p', form.text_field(:login, :size => 30)) +
|
content_tag('p', form.text_field(:login, :size => 30)) +
|
||||||
@@ -174,7 +166,7 @@ module RepositoriesHelper
|
|||||||
content_tag('p', form.text_field(
|
content_tag('p', form.text_field(
|
||||||
:url, :label => l(:field_path_to_repository),
|
:url, :label => l(:field_path_to_repository),
|
||||||
:size => 60, :required => true,
|
:size => 60, :required => true,
|
||||||
:disabled => (repository && !repository.new_record?))) +
|
:disabled => !repository.safe_attribute?('url'))) +
|
||||||
content_tag('p', form.select(
|
content_tag('p', form.select(
|
||||||
:log_encoding, [nil] + Setting::ENCODINGS,
|
:log_encoding, [nil] + Setting::ENCODINGS,
|
||||||
:label => l(:field_commit_logs_encoding), :required => true))
|
:label => l(:field_commit_logs_encoding), :required => true))
|
||||||
@@ -184,7 +176,7 @@ module RepositoriesHelper
|
|||||||
content_tag('p', form.text_field(
|
content_tag('p', form.text_field(
|
||||||
:url, :label => l(:field_path_to_repository),
|
:url, :label => l(:field_path_to_repository),
|
||||||
:size => 60, :required => true,
|
:size => 60, :required => true,
|
||||||
:disabled => (repository && !repository.root_url.blank?)
|
:disabled => !repository.safe_attribute?('url')
|
||||||
) +
|
) +
|
||||||
'<br />'.html_safe + l(:text_mercurial_repository_note)) +
|
'<br />'.html_safe + l(:text_mercurial_repository_note)) +
|
||||||
content_tag('p', form.select(
|
content_tag('p', form.select(
|
||||||
@@ -198,7 +190,7 @@ module RepositoriesHelper
|
|||||||
content_tag('p', form.text_field(
|
content_tag('p', form.text_field(
|
||||||
:url, :label => l(:field_path_to_repository),
|
:url, :label => l(:field_path_to_repository),
|
||||||
:size => 60, :required => true,
|
:size => 60, :required => true,
|
||||||
:disabled => (repository && !repository.root_url.blank?)
|
:disabled => !repository.safe_attribute?('url')
|
||||||
) +
|
) +
|
||||||
'<br />'.html_safe +
|
'<br />'.html_safe +
|
||||||
l(:text_git_repository_note)) +
|
l(:text_git_repository_note)) +
|
||||||
@@ -218,12 +210,12 @@ module RepositoriesHelper
|
|||||||
:root_url,
|
:root_url,
|
||||||
:label => l(:field_cvsroot),
|
:label => l(:field_cvsroot),
|
||||||
:size => 60, :required => true,
|
:size => 60, :required => true,
|
||||||
:disabled => !repository.new_record?)) +
|
:disabled => !repository.safe_attribute?('root_url'))) +
|
||||||
content_tag('p', form.text_field(
|
content_tag('p', form.text_field(
|
||||||
:url,
|
:url,
|
||||||
:label => l(:field_cvs_module),
|
:label => l(:field_cvs_module),
|
||||||
:size => 30, :required => true,
|
:size => 30, :required => true,
|
||||||
:disabled => !repository.new_record?)) +
|
:disabled => !repository.safe_attribute?('url'))) +
|
||||||
content_tag('p', form.select(
|
content_tag('p', form.select(
|
||||||
:log_encoding, [nil] + Setting::ENCODINGS,
|
:log_encoding, [nil] + Setting::ENCODINGS,
|
||||||
:label => l(:field_commit_logs_encoding), :required => true)) +
|
:label => l(:field_commit_logs_encoding), :required => true)) +
|
||||||
@@ -238,7 +230,7 @@ module RepositoriesHelper
|
|||||||
content_tag('p', form.text_field(
|
content_tag('p', form.text_field(
|
||||||
:url, :label => l(:field_path_to_repository),
|
:url, :label => l(:field_path_to_repository),
|
||||||
:size => 60, :required => true,
|
:size => 60, :required => true,
|
||||||
:disabled => (repository && !repository.new_record?))) +
|
:disabled => !repository.safe_attribute?('url'))) +
|
||||||
content_tag('p', form.select(
|
content_tag('p', form.select(
|
||||||
:log_encoding, [nil] + Setting::ENCODINGS,
|
:log_encoding, [nil] + Setting::ENCODINGS,
|
||||||
:label => l(:field_commit_logs_encoding), :required => true))
|
:label => l(:field_commit_logs_encoding), :required => true))
|
||||||
@@ -248,7 +240,7 @@ module RepositoriesHelper
|
|||||||
content_tag('p', form.text_field(
|
content_tag('p', form.text_field(
|
||||||
:url, :label => l(:field_root_directory),
|
:url, :label => l(:field_root_directory),
|
||||||
:size => 60, :required => true,
|
:size => 60, :required => true,
|
||||||
:disabled => (repository && !repository.root_url.blank?))) +
|
:disabled => !repository.safe_attribute?('url'))) +
|
||||||
content_tag('p', form.select(
|
content_tag('p', form.select(
|
||||||
:path_encoding, [nil] + Setting::ENCODINGS,
|
:path_encoding, [nil] + Setting::ENCODINGS,
|
||||||
:label => l(:field_scm_path_encoding)
|
:label => l(:field_scm_path_encoding)
|
||||||
@@ -258,16 +250,13 @@ module RepositoriesHelper
|
|||||||
|
|
||||||
def index_commits(commits, heads)
|
def index_commits(commits, heads)
|
||||||
return nil if commits.nil? or commits.first.parents.nil?
|
return nil if commits.nil? or commits.first.parents.nil?
|
||||||
|
|
||||||
refs_map = {}
|
refs_map = {}
|
||||||
heads.each do |head|
|
heads.each do |head|
|
||||||
refs_map[head.scmid] ||= []
|
refs_map[head.scmid] ||= []
|
||||||
refs_map[head.scmid] << head
|
refs_map[head.scmid] << head
|
||||||
end
|
end
|
||||||
|
|
||||||
commits_by_scmid = {}
|
commits_by_scmid = {}
|
||||||
commits.reverse.each_with_index do |commit, commit_index|
|
commits.reverse.each_with_index do |commit, commit_index|
|
||||||
|
|
||||||
commits_by_scmid[commit.scmid] = {
|
commits_by_scmid[commit.scmid] = {
|
||||||
:parent_scmids => commit.parents.collect { |parent| parent.scmid },
|
:parent_scmids => commit.parents.collect { |parent| parent.scmid },
|
||||||
:rdmid => commit_index,
|
:rdmid => commit_index,
|
||||||
@@ -276,38 +265,28 @@ module RepositoriesHelper
|
|||||||
:href => block_given? ? yield(commit.scmid) : commit.scmid
|
:href => block_given? ? yield(commit.scmid) : commit.scmid
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
|
heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
|
||||||
|
|
||||||
space = nil
|
space = nil
|
||||||
heads.each do |head|
|
heads.each do |head|
|
||||||
if commits_by_scmid.include? head.scmid
|
if commits_by_scmid.include? head.scmid
|
||||||
space = index_head((space || -1) + 1, head, commits_by_scmid)
|
space = index_head((space || -1) + 1, head, commits_by_scmid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# when no head matched anything use first commit
|
# when no head matched anything use first commit
|
||||||
space ||= index_head(0, commits.first, commits_by_scmid)
|
space ||= index_head(0, commits.first, commits_by_scmid)
|
||||||
|
|
||||||
return commits_by_scmid, space
|
return commits_by_scmid, space
|
||||||
end
|
end
|
||||||
|
|
||||||
def index_head(space, commit, commits_by_scmid)
|
def index_head(space, commit, commits_by_scmid)
|
||||||
|
|
||||||
stack = [[space, commits_by_scmid[commit.scmid]]]
|
stack = [[space, commits_by_scmid[commit.scmid]]]
|
||||||
max_space = space
|
max_space = space
|
||||||
|
|
||||||
until stack.empty?
|
until stack.empty?
|
||||||
space, commit = stack.pop
|
space, commit = stack.pop
|
||||||
commit[:space] = space if commit[:space].nil?
|
commit[:space] = space if commit[:space].nil?
|
||||||
|
|
||||||
space -= 1
|
space -= 1
|
||||||
commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
|
commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
|
||||||
|
|
||||||
parent_commit = commits_by_scmid[parent_scmid]
|
parent_commit = commits_by_scmid[parent_scmid]
|
||||||
|
|
||||||
if parent_commit and parent_commit[:space].nil?
|
if parent_commit and parent_commit[:space].nil?
|
||||||
|
|
||||||
stack.unshift [space += 1, parent_commit]
|
stack.unshift [space += 1, parent_commit]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ module SettingsHelper
|
|||||||
setting_values = Setting.send(setting)
|
setting_values = Setting.send(setting)
|
||||||
setting_values = [] unless setting_values.is_a?(Array)
|
setting_values = [] unless setting_values.is_a?(Array)
|
||||||
|
|
||||||
setting_label(setting, options).html_safe +
|
content_tag("label", l(options[:label] || "setting_#{setting}")) +
|
||||||
hidden_field_tag("settings[#{setting}][]", '').html_safe +
|
hidden_field_tag("settings[#{setting}][]", '').html_safe +
|
||||||
choices.collect do |choice|
|
choices.collect do |choice|
|
||||||
text, value = (choice.is_a?(Array) ? choice : [choice, choice])
|
text, value = (choice.is_a?(Array) ? choice : [choice, choice])
|
||||||
@@ -53,9 +53,10 @@ module SettingsHelper
|
|||||||
check_box_tag(
|
check_box_tag(
|
||||||
"settings[#{setting}][]",
|
"settings[#{setting}][]",
|
||||||
value,
|
value,
|
||||||
Setting.send(setting).include?(value)
|
Setting.send(setting).include?(value),
|
||||||
|
:id => nil
|
||||||
) + text.to_s,
|
) + text.to_s,
|
||||||
:class => 'block'
|
:class => (options[:inline] ? 'inline' : 'block')
|
||||||
)
|
)
|
||||||
end.join.html_safe
|
end.join.html_safe
|
||||||
end
|
end
|
||||||
@@ -72,13 +73,13 @@ module SettingsHelper
|
|||||||
|
|
||||||
def setting_check_box(setting, options={})
|
def setting_check_box(setting, options={})
|
||||||
setting_label(setting, options).html_safe +
|
setting_label(setting, options).html_safe +
|
||||||
hidden_field_tag("settings[#{setting}]", 0).html_safe +
|
hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
|
||||||
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
|
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
def setting_label(setting, options={})
|
def setting_label(setting, options={})
|
||||||
label = options.delete(:label)
|
label = options.delete(:label)
|
||||||
label != false ? content_tag("label", l(label || "setting_#{setting}")).html_safe : ''
|
label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}")).html_safe : ''
|
||||||
end
|
end
|
||||||
|
|
||||||
# Renders a notification field for a Redmine::Notifiable option
|
# Renders a notification field for a Redmine::Notifiable option
|
||||||
@@ -86,8 +87,20 @@ module SettingsHelper
|
|||||||
return content_tag(:label,
|
return content_tag(:label,
|
||||||
check_box_tag('settings[notified_events][]',
|
check_box_tag('settings[notified_events][]',
|
||||||
notifiable.name,
|
notifiable.name,
|
||||||
Setting.notified_events.include?(notifiable.name)).html_safe +
|
Setting.notified_events.include?(notifiable.name), :id => nil).html_safe +
|
||||||
l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
|
l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
|
||||||
:class => notifiable.parent.present? ? "parent" : '').html_safe
|
:class => notifiable.parent.present? ? "parent" : '').html_safe
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ module SortHelper
|
|||||||
sql.blank? ? nil : sql
|
sql.blank? ? nil : sql
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_a
|
||||||
|
@criteria.dup
|
||||||
|
end
|
||||||
|
|
||||||
def add!(key, asc)
|
def add!(key, asc)
|
||||||
@criteria.delete_if {|k,o| k == key}
|
@criteria.delete_if {|k,o| k == key}
|
||||||
@criteria = [[key, asc]] + @criteria
|
@criteria = [[key, asc]] + @criteria
|
||||||
@@ -182,6 +186,10 @@ module SortHelper
|
|||||||
@sort_criteria.to_sql
|
@sort_criteria.to_sql
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sort_criteria
|
||||||
|
@sort_criteria
|
||||||
|
end
|
||||||
|
|
||||||
# Returns a link which sorts by the named column.
|
# Returns a link which sorts by the named column.
|
||||||
#
|
#
|
||||||
# - column is the name of an attribute in the sorted record collection.
|
# - column is the name of an attribute in the sorted record collection.
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ module TimelogHelper
|
|||||||
[l(:label_yesterday), 'yesterday'],
|
[l(:label_yesterday), 'yesterday'],
|
||||||
[l(:label_this_week), 'current_week'],
|
[l(:label_this_week), 'current_week'],
|
||||||
[l(:label_last_week), 'last_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_last_n_days, 7), '7_days'],
|
||||||
[l(:label_this_month), 'current_month'],
|
[l(:label_this_month), 'current_month'],
|
||||||
[l(:label_last_month), 'last_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)
|
["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
|
||||||
end
|
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)
|
def user_mail_notification_options(user)
|
||||||
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
|
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,10 +19,18 @@
|
|||||||
|
|
||||||
module VersionsHelper
|
module VersionsHelper
|
||||||
|
|
||||||
STATUS_BY_CRITERIAS = %w(category tracker status priority author assigned_to)
|
def version_anchor(version)
|
||||||
|
if @project == version.project
|
||||||
|
anchor version.name
|
||||||
|
else
|
||||||
|
anchor "#{version.project.try(:identifier)}-#{version.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
STATUS_BY_CRITERIAS = %w(tracker status priority author assigned_to category)
|
||||||
|
|
||||||
def render_issue_status_by(version, criteria)
|
def render_issue_status_by(version, criteria)
|
||||||
criteria = 'category' unless STATUS_BY_CRITERIAS.include?(criteria)
|
criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria)
|
||||||
|
|
||||||
h = Hash.new {|k,v| k[v] = [0, 0]}
|
h = Hash.new {|k,v| k[v] = [0, 0]}
|
||||||
begin
|
begin
|
||||||
@@ -36,7 +44,8 @@ module VersionsHelper
|
|||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
# When grouping by an association, Rails throws this exception if there's no result (bug)
|
# When grouping by an association, Rails throws this exception if there's no result (bug)
|
||||||
end
|
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
|
max = counts.collect {|c| c[:total]}.max
|
||||||
|
|
||||||
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
|
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ module WatchersHelper
|
|||||||
:action => (watched ? 'unwatch' : 'watch'),
|
:action => (watched ? 'unwatch' : 'watch'),
|
||||||
:object_type => object.class.to_s.underscore,
|
:object_type => object.class.to_s.underscore,
|
||||||
:object_id => object.id}
|
:object_id => object.id}
|
||||||
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
|
link_to((watched ? l(:button_unwatch) : l(:button_watch)), url,
|
||||||
{:url => url},
|
:remote => true, :method => 'post', :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
|
||||||
:href => url_for(url),
|
|
||||||
:class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,23 +43,24 @@ module WatchersHelper
|
|||||||
# Returns a comma separated list of users watching the given object
|
# Returns a comma separated list of users watching the given object
|
||||||
def watchers_list(object)
|
def watchers_list(object)
|
||||||
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
|
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
|
||||||
|
content = ''.html_safe
|
||||||
lis = object.watcher_users.collect do |user|
|
lis = object.watcher_users.collect do |user|
|
||||||
s = avatar(user, :size => "16").to_s + link_to_user(user, :class => 'user').to_s
|
s = ''.html_safe
|
||||||
|
s << avatar(user, :size => "16").to_s
|
||||||
|
s << link_to_user(user, :class => 'user')
|
||||||
if remove_allowed
|
if remove_allowed
|
||||||
url = {:controller => 'watchers',
|
url = {:controller => 'watchers',
|
||||||
:action => 'destroy',
|
:action => 'destroy',
|
||||||
:object_type => object.class.to_s.underscore,
|
:object_type => object.class.to_s.underscore,
|
||||||
:object_id => object.id,
|
:object_id => object.id,
|
||||||
:user_id => user}
|
:user_id => user}
|
||||||
s += ' ' + link_to_remote(image_tag('delete.png'),
|
s << ' '
|
||||||
{:url => url},
|
s << link_to(image_tag('delete.png'), url,
|
||||||
:href => url_for(url),
|
:remote => true, :method => 'post', :style => "vertical-align: middle", :class => "delete")
|
||||||
:style => "vertical-align: middle",
|
|
||||||
:class => "delete")
|
|
||||||
end
|
end
|
||||||
content_tag :li, s.html_safe
|
content << content_tag('li', s)
|
||||||
end
|
end
|
||||||
(lis.empty? ? "" : "<ul>#{ lis.join("\n") }</ul>").html_safe
|
content.present? ? content_tag('ul', content) : content
|
||||||
end
|
end
|
||||||
|
|
||||||
def watchers_checkboxes(object, users, checked=nil)
|
def watchers_checkboxes(object, users, checked=nil)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ module WikiHelper
|
|||||||
|
|
||||||
def wiki_page_breadcrumb(page)
|
def wiki_page_breadcrumb(page)
|
||||||
breadcrumb(page.ancestors.reverse.collect {|parent|
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,4 +18,15 @@
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
module WorkflowsHelper
|
module WorkflowsHelper
|
||||||
|
def field_required?(field)
|
||||||
|
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
|
||||||
|
end
|
||||||
|
|
||||||
|
def field_permission_tag(permissions, status, field)
|
||||||
|
name = field.is_a?(CustomField) ? field.id.to_s : field
|
||||||
|
options = [["", ""], [l(:label_readonly), "readonly"]]
|
||||||
|
options << [l(:label_required), "required"] unless field_required?(field)
|
||||||
|
|
||||||
|
select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, permissions[status.id][name]))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Attachment < ActiveRecord::Base
|
|||||||
validates_presence_of :filename, :author
|
validates_presence_of :filename, :author
|
||||||
validates_length_of :filename, :maximum => 255
|
validates_length_of :filename, :maximum => 255
|
||||||
validates_length_of :disk_filename, :maximum => 255
|
validates_length_of :disk_filename, :maximum => 255
|
||||||
|
validates_length_of :description, :maximum => 255
|
||||||
validate :validate_max_file_size
|
validate :validate_max_file_size
|
||||||
|
|
||||||
acts_as_event :title => :filename,
|
acts_as_event :title => :filename,
|
||||||
@@ -46,6 +47,9 @@ class Attachment < ActiveRecord::Base
|
|||||||
cattr_accessor :storage_path
|
cattr_accessor :storage_path
|
||||||
@@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
|
@@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
|
||||||
|
|
||||||
|
cattr_accessor :thumbnails_storage_path
|
||||||
|
@@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
|
||||||
|
|
||||||
before_save :files_to_final_location
|
before_save :files_to_final_location
|
||||||
after_destroy :delete_from_disk
|
after_destroy :delete_from_disk
|
||||||
|
|
||||||
@@ -123,7 +127,7 @@ class Attachment < ActiveRecord::Base
|
|||||||
|
|
||||||
# Deletes the file from the file system if it's not referenced by other attachments
|
# Deletes the file from the file system if it's not referenced by other attachments
|
||||||
def delete_from_disk
|
def delete_from_disk
|
||||||
if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
|
if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
|
||||||
delete_from_disk!
|
delete_from_disk!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -133,6 +137,14 @@ class Attachment < ActiveRecord::Base
|
|||||||
File.join(self.class.storage_path, disk_filename.to_s)
|
File.join(self.class.storage_path, disk_filename.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
title = filename.to_s
|
||||||
|
if description.present?
|
||||||
|
title << " (#{description})"
|
||||||
|
end
|
||||||
|
title
|
||||||
|
end
|
||||||
|
|
||||||
def increment_download
|
def increment_download
|
||||||
increment!(:downloads)
|
increment!(:downloads)
|
||||||
end
|
end
|
||||||
@@ -150,7 +162,43 @@ class Attachment < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def image?
|
def image?
|
||||||
self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
|
!!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def thumbnailable?
|
||||||
|
image?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the full path the attachment thumbnail, or nil
|
||||||
|
# if the thumbnail cannot be generated.
|
||||||
|
def thumbnail(options={})
|
||||||
|
if thumbnailable? && readable?
|
||||||
|
size = options[:size].to_i
|
||||||
|
if size > 0
|
||||||
|
# Limit the number of thumbnails per image
|
||||||
|
size = (size / 50) * 50
|
||||||
|
# Maximum thumbnail size
|
||||||
|
size = 800 if size > 800
|
||||||
|
else
|
||||||
|
size = Setting.thumbnails_size.to_i
|
||||||
|
end
|
||||||
|
size = 100 unless size > 0
|
||||||
|
target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
|
||||||
|
|
||||||
|
begin
|
||||||
|
Redmine::Thumbnail.generate(self.diskfile, target, size)
|
||||||
|
rescue => e
|
||||||
|
logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes all thumbnails
|
||||||
|
def self.clear_thumbnails
|
||||||
|
Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
|
||||||
|
File.delete file
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_text?
|
def is_text?
|
||||||
@@ -175,7 +223,7 @@ class Attachment < ActiveRecord::Base
|
|||||||
def self.find_by_token(token)
|
def self.find_by_token(token)
|
||||||
if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
|
if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
|
||||||
attachment_id, attachment_digest = $1, $2
|
attachment_id, attachment_digest = $1, $2
|
||||||
attachment = Attachment.first(:conditions => {:id => attachment_id, :digest => attachment_digest})
|
attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
|
||||||
if attachment && attachment.container.nil?
|
if attachment && attachment.container.nil?
|
||||||
attachment
|
attachment
|
||||||
end
|
end
|
||||||
@@ -200,8 +248,7 @@ class Attachment < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.prune(age=1.day)
|
def self.prune(age=1.day)
|
||||||
attachments = Attachment.all(:conditions => ["created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age])
|
Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
|
||||||
attachments.each(&:destroy)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
# Generic exception for when the AuthSource can not be reached
|
# Generic exception for when the AuthSource can not be reached
|
||||||
# (eg. can not connect to the LDAP)
|
# (eg. can not connect to the LDAP)
|
||||||
class AuthSourceException < Exception; end
|
class AuthSourceException < Exception; end
|
||||||
|
class AuthSourceTimeoutException < AuthSourceException; end
|
||||||
|
|
||||||
class AuthSource < ActiveRecord::Base
|
class AuthSource < ActiveRecord::Base
|
||||||
include Redmine::SubclassFactory
|
include Redmine::SubclassFactory
|
||||||
@@ -58,7 +59,7 @@ class AuthSource < ActiveRecord::Base
|
|||||||
|
|
||||||
# Try to authenticate a user not yet registered against available sources
|
# Try to authenticate a user not yet registered against available sources
|
||||||
def self.authenticate(login, password)
|
def self.authenticate(login, password)
|
||||||
AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
|
AuthSource.where(:onthefly_register => true).all.each do |source|
|
||||||
begin
|
begin
|
||||||
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
|
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
|
||||||
attrs = source.authenticate(login, password)
|
attrs = source.authenticate(login, password)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
require 'iconv'
|
require 'iconv'
|
||||||
require 'net/ldap'
|
require 'net/ldap'
|
||||||
require 'net/ldap/dn'
|
require 'net/ldap/dn'
|
||||||
|
require 'timeout'
|
||||||
|
|
||||||
class AuthSourceLdap < AuthSource
|
class AuthSourceLdap < AuthSource
|
||||||
validates_presence_of :host, :port, :attr_login
|
validates_presence_of :host, :port, :attr_login
|
||||||
@@ -25,18 +26,11 @@ class AuthSourceLdap < AuthSource
|
|||||||
validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
|
validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
|
||||||
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
|
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
|
||||||
validates_numericality_of :port, :only_integer => true
|
validates_numericality_of :port, :only_integer => true
|
||||||
|
validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
|
||||||
validate :validate_filter
|
validate :validate_filter
|
||||||
|
|
||||||
before_validation :strip_ldap_attributes
|
before_validation :strip_ldap_attributes
|
||||||
|
|
||||||
def self.human_attribute_name(attribute_key_name, *args)
|
|
||||||
attr_name = attribute_key_name.to_s
|
|
||||||
if attr_name == "filter"
|
|
||||||
attr_name = "ldap_filter"
|
|
||||||
end
|
|
||||||
super(attr_name, *args)
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(attributes=nil, *args)
|
def initialize(attributes=nil, *args)
|
||||||
super
|
super
|
||||||
self.port = 389 if self.port == 0
|
self.port = 389 if self.port == 0
|
||||||
@@ -44,22 +38,26 @@ class AuthSourceLdap < AuthSource
|
|||||||
|
|
||||||
def authenticate(login, password)
|
def authenticate(login, password)
|
||||||
return nil if login.blank? || password.blank?
|
return nil if login.blank? || password.blank?
|
||||||
attrs = get_user_dn(login, password)
|
|
||||||
|
|
||||||
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
|
with_timeout do
|
||||||
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
|
attrs = get_user_dn(login, password)
|
||||||
return attrs.except(:dn)
|
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
|
||||||
|
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
|
||||||
|
return attrs.except(:dn)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
rescue Net::LDAP::LdapError => e
|
rescue Net::LDAP::LdapError => e
|
||||||
raise AuthSourceException.new(e.message)
|
raise AuthSourceException.new(e.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
# test the connection to the LDAP
|
# test the connection to the LDAP
|
||||||
def test_connection
|
def test_connection
|
||||||
ldap_con = initialize_ldap_con(self.account, self.account_password)
|
with_timeout do
|
||||||
ldap_con.open { }
|
ldap_con = initialize_ldap_con(self.account, self.account_password)
|
||||||
rescue Net::LDAP::LdapError => e
|
ldap_con.open { }
|
||||||
raise "LdapError: " + e.message
|
end
|
||||||
|
rescue Net::LDAP::LdapError => e
|
||||||
|
raise AuthSourceException.new(e.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_method_name
|
def auth_method_name
|
||||||
@@ -68,6 +66,16 @@ class AuthSourceLdap < AuthSource
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def with_timeout(&block)
|
||||||
|
timeout = self.timeout
|
||||||
|
timeout = 20 unless timeout && timeout > 0
|
||||||
|
Timeout.timeout(timeout) do
|
||||||
|
return yield
|
||||||
|
end
|
||||||
|
rescue Timeout::Error => e
|
||||||
|
raise AuthSourceTimeoutException.new(e.message)
|
||||||
|
end
|
||||||
|
|
||||||
def ldap_filter
|
def ldap_filter
|
||||||
if filter.present?
|
if filter.present?
|
||||||
Net::LDAP::Filter.construct(filter)
|
Net::LDAP::Filter.construct(filter)
|
||||||
|
|||||||
+35
-2
@@ -21,26 +21,37 @@ class Board < ActiveRecord::Base
|
|||||||
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
|
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
|
||||||
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
|
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
|
||||||
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
|
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
|
||||||
acts_as_list :scope => :project_id
|
acts_as_tree :dependent => :nullify
|
||||||
|
acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
|
||||||
acts_as_watchable
|
acts_as_watchable
|
||||||
|
|
||||||
validates_presence_of :name, :description
|
validates_presence_of :name, :description
|
||||||
validates_length_of :name, :maximum => 30
|
validates_length_of :name, :maximum => 30
|
||||||
validates_length_of :description, :maximum => 255
|
validates_length_of :description, :maximum => 255
|
||||||
|
validate :validate_board
|
||||||
|
|
||||||
scope :visible, lambda {|*args| { :include => :project,
|
scope :visible, lambda {|*args| { :include => :project,
|
||||||
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
|
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
|
||||||
|
|
||||||
safe_attributes 'name', 'description', 'move_to'
|
safe_attributes 'name', 'description', 'parent_id', 'move_to'
|
||||||
|
|
||||||
def visible?(user=User.current)
|
def visible?(user=User.current)
|
||||||
!user.nil? && user.allowed_to?(:view_messages, project)
|
!user.nil? && user.allowed_to?(:view_messages, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reload(*args)
|
||||||
|
@valid_parents = nil
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
name
|
name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def valid_parents
|
||||||
|
@valid_parents ||= project.boards - self_and_descendants
|
||||||
|
end
|
||||||
|
|
||||||
def reset_counters!
|
def reset_counters!
|
||||||
self.class.reset_counters!(id)
|
self.class.reset_counters!(id)
|
||||||
end
|
end
|
||||||
@@ -53,4 +64,26 @@ class Board < ActiveRecord::Base
|
|||||||
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
|
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
|
||||||
["id = ?", board_id])
|
["id = ?", board_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.board_tree(boards, parent_id=nil, level=0)
|
||||||
|
tree = []
|
||||||
|
boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
|
||||||
|
tree << [board, level]
|
||||||
|
tree += board_tree(boards, board.id, level+1)
|
||||||
|
end
|
||||||
|
if block_given?
|
||||||
|
tree.each do |board, level|
|
||||||
|
yield board, level
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tree
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def validate_board
|
||||||
|
if parent_id && parent_id_changed?
|
||||||
|
errors.add(:parent_id, :invalid) unless valid_parents.include?(parent)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ require 'iconv'
|
|||||||
class Changeset < ActiveRecord::Base
|
class Changeset < ActiveRecord::Base
|
||||||
belongs_to :repository
|
belongs_to :repository
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :changes, :dependent => :delete_all
|
has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
|
||||||
has_and_belongs_to_many :issues
|
has_and_belongs_to_many :issues
|
||||||
has_and_belongs_to_many :parents,
|
has_and_belongs_to_many :parents,
|
||||||
:class_name => "Changeset",
|
:class_name => "Changeset",
|
||||||
@@ -176,18 +176,12 @@ class Changeset < ActiveRecord::Base
|
|||||||
|
|
||||||
# Returns the previous changeset
|
# Returns the previous changeset
|
||||||
def previous
|
def previous
|
||||||
@previous ||= Changeset.find(:first,
|
@previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first
|
||||||
:conditions => ['id < ? AND repository_id = ?',
|
|
||||||
self.id, self.repository_id],
|
|
||||||
:order => 'id DESC')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the next changeset
|
# Returns the next changeset
|
||||||
def next
|
def next
|
||||||
@next ||= Changeset.find(:first,
|
@next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first
|
||||||
:conditions => ['id > ? AND repository_id = ?',
|
|
||||||
self.id, self.repository_id],
|
|
||||||
:order => 'id ASC')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates a new Change from it's common parameters
|
# Creates a new Change from it's common parameters
|
||||||
|
|||||||
+106
-7
@@ -30,9 +30,32 @@ class CustomField < ActiveRecord::Base
|
|||||||
validate :validate_custom_field
|
validate :validate_custom_field
|
||||||
before_validation :set_searchable
|
before_validation :set_searchable
|
||||||
|
|
||||||
def initialize(attributes=nil, *args)
|
CUSTOM_FIELDS_TABS = [
|
||||||
super
|
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
|
||||||
self.possible_values ||= []
|
: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
|
end
|
||||||
|
|
||||||
def set_searchable
|
def set_searchable
|
||||||
@@ -97,7 +120,7 @@ class CustomField < ActiveRecord::Base
|
|||||||
value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
|
value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
values
|
values || []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -131,16 +154,32 @@ class CustomField < ActiveRecord::Base
|
|||||||
casted
|
casted
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def value_from_keyword(keyword, customized)
|
||||||
|
possible_values_options = possible_values_options(customized)
|
||||||
|
if possible_values_options.present?
|
||||||
|
keyword = keyword.to_s.downcase
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
# Returns a ORDER BY clause that can used to sort customized
|
# Returns a ORDER BY clause that can used to sort customized
|
||||||
# objects by their value of the custom field.
|
# objects by their value of the custom field.
|
||||||
# Returns false, if the custom field can not be used for sorting.
|
# Returns nil if the custom field can not be used for sorting.
|
||||||
def order_statement
|
def order_statement
|
||||||
return nil if multiple?
|
return nil if multiple?
|
||||||
case field_format
|
case field_format
|
||||||
when 'string', 'text', 'list', 'date', 'bool'
|
when 'string', 'text', 'list', 'date', 'bool'
|
||||||
# COALESCE is here to make sure that blank and NULL values are sorted equally
|
# COALESCE is here to make sure that blank and NULL values are sorted equally
|
||||||
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
|
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
|
||||||
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
|
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
|
||||||
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
||||||
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
|
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
|
||||||
when 'int', 'float'
|
when 'int', 'float'
|
||||||
@@ -148,18 +187,74 @@ class CustomField < ActiveRecord::Base
|
|||||||
# Postgresql will raise an error if a value can not be casted!
|
# Postgresql will raise an error if a value can not be casted!
|
||||||
# CustomValue validations should ensure that it doesn't occur
|
# CustomValue validations should ensure that it doesn't occur
|
||||||
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
|
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
|
||||||
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
|
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
|
||||||
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
||||||
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
|
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
|
||||||
|
when 'user', 'version'
|
||||||
|
value_class.fields_for_order_statement(value_join_alias)
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns a GROUP BY clause that can used to group by custom value
|
||||||
|
# Returns nil if the custom field can not be used for grouping.
|
||||||
|
def group_statement
|
||||||
|
return nil if multiple?
|
||||||
|
case field_format
|
||||||
|
when 'list', 'date', 'bool', 'int'
|
||||||
|
order_statement
|
||||||
|
when 'user', 'version'
|
||||||
|
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
|
||||||
|
" WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
|
||||||
|
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
|
||||||
|
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def join_for_order_statement
|
||||||
|
case field_format
|
||||||
|
when 'user', 'version'
|
||||||
|
"LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
|
||||||
|
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
||||||
|
" AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
|
||||||
|
" AND #{join_alias}.custom_field_id = #{id}" +
|
||||||
|
" AND #{join_alias}.value <> ''" +
|
||||||
|
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
||||||
|
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
||||||
|
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
||||||
|
" AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
|
||||||
|
" LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
|
||||||
|
" ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def join_alias
|
||||||
|
"cf_#{id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_join_alias
|
||||||
|
join_alias + "_" + field_format
|
||||||
|
end
|
||||||
|
|
||||||
def <=>(field)
|
def <=>(field)
|
||||||
position <=> field.position
|
position <=> field.position
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns the class that values represent
|
||||||
|
def value_class
|
||||||
|
case field_format
|
||||||
|
when 'user', 'version'
|
||||||
|
field_format.classify.constantize
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.customized_class
|
def self.customized_class
|
||||||
self.name =~ /^(.+)CustomField$/
|
self.name =~ /^(.+)CustomField$/
|
||||||
begin; $1.constantize; rescue nil; end
|
begin; $1.constantize; rescue nil; end
|
||||||
@@ -200,6 +295,10 @@ class CustomField < ActiveRecord::Base
|
|||||||
validate_field_value(value).empty?
|
validate_field_value(value).empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_in?(*args)
|
||||||
|
args.include?(field_format)
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# Returns the error message for the given value regarding its format
|
# Returns the error message for the given value regarding its format
|
||||||
|
|||||||
@@ -31,4 +31,10 @@ class DocumentCategory < Enumeration
|
|||||||
def transfer_relations(to)
|
def transfer_relations(to)
|
||||||
documents.update_all("category_id = #{to.id}")
|
documents.update_all("category_id = #{to.id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.default
|
||||||
|
d = super
|
||||||
|
d = first if d.nil?
|
||||||
|
d
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,19 +35,20 @@ class Enumeration < ActiveRecord::Base
|
|||||||
validates_uniqueness_of :name, :scope => [:type, :project_id]
|
validates_uniqueness_of :name, :scope => [:type, :project_id]
|
||||||
validates_length_of :name, :maximum => 30
|
validates_length_of :name, :maximum => 30
|
||||||
|
|
||||||
scope :shared, :conditions => { :project_id => nil }
|
scope :shared, where(:project_id => nil)
|
||||||
scope :active, :conditions => { :active => true }
|
scope :sorted, order("#{table_name}.position ASC")
|
||||||
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
|
scope :active, where(:active => true)
|
||||||
|
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||||
|
|
||||||
def self.default
|
def self.default
|
||||||
# Creates a fake default scope so Enumeration.default will check
|
# Creates a fake default scope so Enumeration.default will check
|
||||||
# it's type. STI subclasses will automatically add their own
|
# it's type. STI subclasses will automatically add their own
|
||||||
# types to the finder.
|
# types to the finder.
|
||||||
if self.descends_from_active_record?
|
if self.descends_from_active_record?
|
||||||
find(:first, :conditions => { :is_default => true, :type => 'Enumeration' })
|
where(:is_default => true, :type => 'Enumeration').first
|
||||||
else
|
else
|
||||||
# STI classes are
|
# STI classes are
|
||||||
find(:first, :conditions => { :is_default => true })
|
where(:is_default => true).first
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ class Enumeration < ActiveRecord::Base
|
|||||||
|
|
||||||
def check_default
|
def check_default
|
||||||
if is_default? && is_default_changed?
|
if is_default? && is_default_changed?
|
||||||
Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
|
Enumeration.update_all({:is_default => false}, {:type => type})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -16,6 +16,8 @@
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
class Group < Principal
|
class Group < Principal
|
||||||
|
include Redmine::SafeAttributes
|
||||||
|
|
||||||
has_and_belongs_to_many :users, :after_add => :user_added,
|
has_and_belongs_to_many :users, :after_add => :user_added,
|
||||||
:after_remove => :user_removed
|
:after_remove => :user_removed
|
||||||
|
|
||||||
@@ -27,11 +29,25 @@ class Group < Principal
|
|||||||
|
|
||||||
before_destroy :remove_references_before_destroy
|
before_destroy :remove_references_before_destroy
|
||||||
|
|
||||||
|
scope :sorted, order("#{table_name}.lastname ASC")
|
||||||
|
|
||||||
|
safe_attributes 'name',
|
||||||
|
'user_ids',
|
||||||
|
'custom_field_values',
|
||||||
|
'custom_fields',
|
||||||
|
:if => lambda {|group, user| user.admin?}
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
lastname.to_s
|
lastname.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
alias :name :to_s
|
def name
|
||||||
|
lastname
|
||||||
|
end
|
||||||
|
|
||||||
|
def name=(arg)
|
||||||
|
self.lastname = arg
|
||||||
|
end
|
||||||
|
|
||||||
def user_added(user)
|
def user_added(user)
|
||||||
members.each do |member|
|
members.each do |member|
|
||||||
|
|||||||
+377
-61
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
class Issue < ActiveRecord::Base
|
class Issue < ActiveRecord::Base
|
||||||
include Redmine::SafeAttributes
|
include Redmine::SafeAttributes
|
||||||
|
include Redmine::Utils::DateCalculation
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :tracker
|
belongs_to :tracker
|
||||||
@@ -28,6 +29,14 @@ class Issue < ActiveRecord::Base
|
|||||||
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
|
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
|
||||||
|
|
||||||
has_many :journals, :as => :journalized, :dependent => :destroy
|
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_many :time_entries, :dependent => :delete_all
|
||||||
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
|
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_customizable
|
||||||
acts_as_watchable
|
acts_as_watchable
|
||||||
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
|
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
|
# sort by id so that limited eager loading doesn't break with postgresql
|
||||||
:order_column => "#{table_name}.id"
|
:order_column => "#{table_name}.id"
|
||||||
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
|
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
|
||||||
@@ -52,13 +61,14 @@ class Issue < ActiveRecord::Base
|
|||||||
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
|
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
|
||||||
|
|
||||||
attr_reader :current_journal
|
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
|
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
|
||||||
|
|
||||||
validates_length_of :subject, :maximum => 255
|
validates_length_of :subject, :maximum => 255
|
||||||
validates_inclusion_of :done_ratio, :in => 0..100
|
validates_inclusion_of :done_ratio, :in => 0..100
|
||||||
validates_numericality_of :estimated_hours, :allow_nil => true
|
validates_numericality_of :estimated_hours, :allow_nil => true
|
||||||
validate :validate_issue
|
validate :validate_issue, :validate_required_fields
|
||||||
|
|
||||||
scope :visible,
|
scope :visible,
|
||||||
lambda {|*args| { :include => :project,
|
lambda {|*args| { :include => :project,
|
||||||
@@ -70,30 +80,35 @@ class Issue < ActiveRecord::Base
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
|
scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
|
||||||
scope :with_limit, lambda { |limit| { :limit => limit} }
|
|
||||||
scope :on_active_project, :include => [:status, :project, :tracker],
|
scope :on_active_project, :include => [:status, :project, :tracker],
|
||||||
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
|
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
|
||||||
|
|
||||||
before_create :default_assign
|
before_create :default_assign
|
||||||
before_save :close_duplicates, :update_done_ratio_from_issue_status
|
before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
|
||||||
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
|
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
|
||||||
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
|
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
|
||||||
|
# Should be after_create but would be called before previous after_save callbacks
|
||||||
|
after_save :after_create_from_copy
|
||||||
after_destroy :update_parent_attributes
|
after_destroy :update_parent_attributes
|
||||||
|
|
||||||
# Returns a SQL conditions string used to find all issues visible by the specified user
|
# Returns a SQL conditions string used to find all issues visible by the specified user
|
||||||
def self.visible_condition(user, options={})
|
def self.visible_condition(user, options={})
|
||||||
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
|
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
|
||||||
case role.issues_visibility
|
if user.logged?
|
||||||
when 'all'
|
case role.issues_visibility
|
||||||
nil
|
when 'all'
|
||||||
when 'default'
|
nil
|
||||||
user_ids = [user.id] + user.groups.map(&:id)
|
when 'default'
|
||||||
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
user_ids = [user.id] + user.groups.map(&:id)
|
||||||
when 'own'
|
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
||||||
user_ids = [user.id] + user.groups.map(&:id)
|
when 'own'
|
||||||
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
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
|
else
|
||||||
'1=0'
|
"(#{table_name}.is_private = #{connection.quoted_false})"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -101,15 +116,19 @@ class Issue < ActiveRecord::Base
|
|||||||
# Returns true if usr or current user is allowed to view the issue
|
# Returns true if usr or current user is allowed to view the issue
|
||||||
def visible?(usr=nil)
|
def visible?(usr=nil)
|
||||||
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
|
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
|
||||||
case role.issues_visibility
|
if user.logged?
|
||||||
when 'all'
|
case role.issues_visibility
|
||||||
true
|
when 'all'
|
||||||
when 'default'
|
true
|
||||||
!self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
|
when 'default'
|
||||||
when 'own'
|
!self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
|
||||||
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
|
else
|
||||||
false
|
!self.is_private?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -124,6 +143,34 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# AR#Persistence#destroy would raise and RecordNotFound exception
|
||||||
|
# if the issue was already deleted or updated (non matching lock_version).
|
||||||
|
# This is a problem when bulk deleting issues or deleting a project
|
||||||
|
# (because an issue may already be deleted if its parent was deleted
|
||||||
|
# first).
|
||||||
|
# The issue is reloaded by the nested_set before being deleted so
|
||||||
|
# the lock_version condition should not be an issue but we handle it.
|
||||||
|
def destroy
|
||||||
|
super
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
# Stale or already deleted
|
||||||
|
begin
|
||||||
|
reload
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
# The issue was actually already deleted
|
||||||
|
@destroyed = true
|
||||||
|
return freeze
|
||||||
|
end
|
||||||
|
# The issue was stale, retry to destroy
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def reload(*args)
|
||||||
|
@workflow_rule_by_attribute = nil
|
||||||
|
@assignable_versions = nil
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
|
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
|
||||||
def available_custom_fields
|
def available_custom_fields
|
||||||
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
|
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
|
||||||
@@ -142,6 +189,7 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
@copied_from = issue
|
@copied_from = issue
|
||||||
|
@copy_options = options
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -186,7 +234,9 @@ class Issue < ActiveRecord::Base
|
|||||||
|
|
||||||
def status_id=(sid)
|
def status_id=(sid)
|
||||||
self.status = nil
|
self.status = nil
|
||||||
write_attribute(:status_id, sid)
|
result = write_attribute(:status_id, sid)
|
||||||
|
@workflow_rule_by_attribute = nil
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def priority_id=(pid)
|
def priority_id=(pid)
|
||||||
@@ -208,6 +258,7 @@ class Issue < ActiveRecord::Base
|
|||||||
self.tracker = nil
|
self.tracker = nil
|
||||||
result = write_attribute(:tracker_id, tid)
|
result = write_attribute(:tracker_id, tid)
|
||||||
@custom_field_values = nil
|
@custom_field_values = nil
|
||||||
|
@workflow_rule_by_attribute = nil
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -222,6 +273,8 @@ class Issue < ActiveRecord::Base
|
|||||||
write_attribute(:project_id, project ? project.id : nil)
|
write_attribute(:project_id, project ? project.id : nil)
|
||||||
association_instance_set('project', project)
|
association_instance_set('project', project)
|
||||||
if project_was && project && project_was != project
|
if project_was && project && project_was != project
|
||||||
|
@assignable_versions = nil
|
||||||
|
|
||||||
unless keep_tracker || project.trackers.include?(tracker)
|
unless keep_tracker || project.trackers.include?(tracker)
|
||||||
self.tracker = project.trackers.first
|
self.tracker = project.trackers.first
|
||||||
end
|
end
|
||||||
@@ -233,7 +286,8 @@ class Issue < ActiveRecord::Base
|
|||||||
if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
|
if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
|
||||||
self.fixed_version = nil
|
self.fixed_version = nil
|
||||||
end
|
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
|
self.parent_issue_id = nil
|
||||||
end
|
end
|
||||||
@custom_field_values = nil
|
@custom_field_values = nil
|
||||||
@@ -292,6 +346,7 @@ class Issue < ActiveRecord::Base
|
|||||||
'custom_field_values',
|
'custom_field_values',
|
||||||
'custom_fields',
|
'custom_fields',
|
||||||
'lock_version',
|
'lock_version',
|
||||||
|
'notes',
|
||||||
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
|
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
|
||||||
|
|
||||||
safe_attributes 'status_id',
|
safe_attributes 'status_id',
|
||||||
@@ -299,8 +354,15 @@ class Issue < ActiveRecord::Base
|
|||||||
'fixed_version_id',
|
'fixed_version_id',
|
||||||
'done_ratio',
|
'done_ratio',
|
||||||
'lock_version',
|
'lock_version',
|
||||||
|
'notes',
|
||||||
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
|
: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',
|
safe_attributes 'watcher_user_ids',
|
||||||
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
|
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
|
||||||
|
|
||||||
@@ -314,6 +376,13 @@ class Issue < ActiveRecord::Base
|
|||||||
:if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
|
:if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
|
||||||
user.allowed_to?(:manage_subtasks, issue.project)}
|
user.allowed_to?(:manage_subtasks, issue.project)}
|
||||||
|
|
||||||
|
def safe_attribute_names(user=nil)
|
||||||
|
names = super
|
||||||
|
names -= disabled_core_fields
|
||||||
|
names -= read_only_attribute_names(user)
|
||||||
|
names
|
||||||
|
end
|
||||||
|
|
||||||
# Safely sets attributes
|
# Safely sets attributes
|
||||||
# Should be called from controllers instead of #attributes=
|
# Should be called from controllers instead of #attributes=
|
||||||
# attr_accessible is too rough because we still want things like
|
# attr_accessible is too rough because we still want things like
|
||||||
@@ -321,39 +390,125 @@ class Issue < ActiveRecord::Base
|
|||||||
def safe_attributes=(attrs, user=User.current)
|
def safe_attributes=(attrs, user=User.current)
|
||||||
return unless attrs.is_a?(Hash)
|
return unless attrs.is_a?(Hash)
|
||||||
|
|
||||||
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
|
attrs = attrs.dup
|
||||||
attrs = delete_unsafe_attributes(attrs, user)
|
|
||||||
return if attrs.empty?
|
|
||||||
|
|
||||||
# Project and Tracker must be set before since new_statuses_allowed_to depends on it.
|
# Project and Tracker must be set before since new_statuses_allowed_to depends on it.
|
||||||
if p = attrs.delete('project_id')
|
if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
|
||||||
if allowed_target_projects(user).collect(&:id).include?(p.to_i)
|
if allowed_target_projects(user).collect(&:id).include?(p.to_i)
|
||||||
self.project_id = p
|
self.project_id = p
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if t = attrs.delete('tracker_id')
|
if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
|
||||||
self.tracker_id = t
|
self.tracker_id = t
|
||||||
end
|
end
|
||||||
|
|
||||||
if attrs['status_id']
|
if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
|
||||||
unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
|
if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
|
||||||
attrs.delete('status_id')
|
self.status_id = s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attrs = delete_unsafe_attributes(attrs, user)
|
||||||
|
return if attrs.empty?
|
||||||
|
|
||||||
unless leaf?
|
unless leaf?
|
||||||
attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
|
attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
|
||||||
end
|
end
|
||||||
|
|
||||||
if attrs['parent_issue_id'].present?
|
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?
|
||||||
|
attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
|
||||||
|
end
|
||||||
|
|
||||||
|
if attrs['custom_fields'].present?
|
||||||
|
attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
|
||||||
end
|
end
|
||||||
|
|
||||||
# mass-assignment security bypass
|
# mass-assignment security bypass
|
||||||
assign_attributes attrs, :without_protection => true
|
assign_attributes attrs, :without_protection => true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def disabled_core_fields
|
||||||
|
tracker ? tracker.disabled_core_fields : []
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the custom_field_values that can be edited by the given user
|
||||||
|
def editable_custom_field_values(user=nil)
|
||||||
|
custom_field_values.reject do |value|
|
||||||
|
read_only_attribute_names(user).include?(value.custom_field_id.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the names of attributes that are read-only for user or the current user
|
||||||
|
# For users with multiple roles, the read-only fields are the intersection of
|
||||||
|
# read-only fields of each role
|
||||||
|
# The result is an array of strings where sustom fields are represented with their ids
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# issue.read_only_attribute_names # => ['due_date', '2']
|
||||||
|
# issue.read_only_attribute_names(user) # => []
|
||||||
|
def read_only_attribute_names(user=nil)
|
||||||
|
workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the names of required attributes for user or the current user
|
||||||
|
# For users with multiple roles, the required fields are the intersection of
|
||||||
|
# required fields of each role
|
||||||
|
# The result is an array of strings where sustom fields are represented with their ids
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# issue.required_attribute_names # => ['due_date', '2']
|
||||||
|
# issue.required_attribute_names(user) # => []
|
||||||
|
def required_attribute_names(user=nil)
|
||||||
|
workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the attribute is required for user
|
||||||
|
def required_attribute?(name, user=nil)
|
||||||
|
required_attribute_names(user).include?(name.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a hash of the workflow rule by attribute for the given user
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
|
||||||
|
def workflow_rule_by_attribute(user=nil)
|
||||||
|
return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
|
||||||
|
|
||||||
|
user_real = user || User.current
|
||||||
|
roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
|
||||||
|
return {} if roles.empty?
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
|
||||||
|
if workflow_permissions.any?
|
||||||
|
workflow_rules = workflow_permissions.inject({}) do |h, wp|
|
||||||
|
h[wp.field_name] ||= []
|
||||||
|
h[wp.field_name] << wp.rule
|
||||||
|
h
|
||||||
|
end
|
||||||
|
workflow_rules.each do |attr, rules|
|
||||||
|
next if rules.size < roles.size
|
||||||
|
uniq_rules = rules.uniq
|
||||||
|
if uniq_rules.size == 1
|
||||||
|
result[attr] = uniq_rules.first
|
||||||
|
else
|
||||||
|
result[attr] = 'required'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@workflow_rule_by_attribute = result if user.nil?
|
||||||
|
result
|
||||||
|
end
|
||||||
|
private :workflow_rule_by_attribute
|
||||||
|
|
||||||
def done_ratio
|
def done_ratio
|
||||||
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
|
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
|
||||||
status.default_done_ratio
|
status.default_done_ratio
|
||||||
@@ -371,11 +526,15 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_issue
|
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
|
errors.add :due_date, :not_a_date
|
||||||
end
|
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
|
errors.add :due_date, :greater_than_start_date
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -399,9 +558,11 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Checks parent issue assignment
|
# Checks parent issue assignment
|
||||||
if @parent_issue
|
if @invalid_parent_issue_id.present?
|
||||||
if @parent_issue.project_id != project_id
|
errors.add :parent_issue_id, :invalid
|
||||||
errors.add :parent_issue_id, :not_same_project
|
elsif @parent_issue
|
||||||
|
if !valid_parent_project?(@parent_issue)
|
||||||
|
errors.add :parent_issue_id, :invalid
|
||||||
elsif !new_record?
|
elsif !new_record?
|
||||||
# moving an existing issue
|
# moving an existing issue
|
||||||
if @parent_issue.root_id != root_id
|
if @parent_issue.root_id != root_id
|
||||||
@@ -409,7 +570,26 @@ class Issue < ActiveRecord::Base
|
|||||||
elsif move_possible?(@parent_issue)
|
elsif move_possible?(@parent_issue)
|
||||||
# move accepted inside tree
|
# move accepted inside tree
|
||||||
else
|
else
|
||||||
errors.add :parent_issue_id, :not_a_valid_parent
|
errors.add :parent_issue_id, :invalid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validates the issue against additional workflow requirements
|
||||||
|
def validate_required_fields
|
||||||
|
user = new_record? ? author : current_journal.try(:user)
|
||||||
|
|
||||||
|
required_attribute_names(user).each do |attribute|
|
||||||
|
if attribute =~ /^\d+$/
|
||||||
|
attribute = attribute.to_i
|
||||||
|
v = custom_field_values.detect {|v| v.custom_field_id == attribute }
|
||||||
|
if v && v.value.blank?
|
||||||
|
errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if respond_to?(attribute) && send(attribute).blank?
|
||||||
|
errors.add attribute, :blank
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -432,8 +612,6 @@ class Issue < ActiveRecord::Base
|
|||||||
@custom_values_before_change = {}
|
@custom_values_before_change = {}
|
||||||
self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
|
self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
|
||||||
end
|
end
|
||||||
# Make sure updated_on is updated when adding a note.
|
|
||||||
updated_on_will_change!
|
|
||||||
@current_journal
|
@current_journal
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -442,10 +620,19 @@ class Issue < ActiveRecord::Base
|
|||||||
if new_record?
|
if new_record?
|
||||||
nil
|
nil
|
||||||
else
|
else
|
||||||
journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
|
journals.maximum(:id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns a scope for journals that have an id greater than journal_id
|
||||||
|
def journals_after(journal_id)
|
||||||
|
scope = journals.reorder("#{Journal.table_name}.id ASC")
|
||||||
|
if journal_id.present?
|
||||||
|
scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
|
||||||
|
end
|
||||||
|
scope
|
||||||
|
end
|
||||||
|
|
||||||
# Return true if the issue is closed, otherwise false
|
# Return true if the issue is closed, otherwise false
|
||||||
def closed?
|
def closed?
|
||||||
self.status.is_closed?
|
self.status.is_closed?
|
||||||
@@ -502,7 +689,21 @@ class Issue < ActiveRecord::Base
|
|||||||
|
|
||||||
# Versions that the issue can be assigned to
|
# Versions that the issue can be assigned to
|
||||||
def assignable_versions
|
def assignable_versions
|
||||||
@assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
|
return @assignable_versions if @assignable_versions
|
||||||
|
|
||||||
|
versions = project.shared_versions.open.all
|
||||||
|
if fixed_version
|
||||||
|
if fixed_version_id_changed?
|
||||||
|
# nothing to do
|
||||||
|
elsif project_id_changed?
|
||||||
|
if project.shared_versions.include?(fixed_version)
|
||||||
|
versions << fixed_version
|
||||||
|
end
|
||||||
|
else
|
||||||
|
versions << fixed_version
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@assignable_versions = versions.uniq.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if this issue is blocked by another issue that is still open
|
# Returns true if this issue is blocked by another issue that is still open
|
||||||
@@ -542,8 +743,8 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the mail adresses of users that should be notified
|
# Returns the users that should be notified
|
||||||
def recipients
|
def notified_users
|
||||||
notified = []
|
notified = []
|
||||||
# Author and assignee are always notified unless they have been
|
# Author and assignee are always notified unless they have been
|
||||||
# locked or don't want to be notified
|
# locked or don't want to be notified
|
||||||
@@ -560,7 +761,12 @@ class Issue < ActiveRecord::Base
|
|||||||
notified.uniq!
|
notified.uniq!
|
||||||
# Remove users that can not view the issue
|
# Remove users that can not view the issue
|
||||||
notified.reject! {|user| !visible?(user)}
|
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
|
end
|
||||||
|
|
||||||
# Returns the number of hours spent on this issue
|
# Returns the number of hours spent on this issue
|
||||||
@@ -579,7 +785,7 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def relations
|
def relations
|
||||||
@relations ||= (relations_from + relations_to).sort
|
@relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Preloads relations for a collection of issues
|
# Preloads relations for a collection of issues
|
||||||
@@ -602,6 +808,25 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
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.
|
# Finds an issue relation given its id.
|
||||||
def find_relation(relation_id)
|
def find_relation(relation_id)
|
||||||
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
|
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
|
||||||
@@ -639,29 +864,58 @@ class Issue < ActiveRecord::Base
|
|||||||
(start_date && due_date) ? due_date - start_date : 0
|
(start_date && due_date) ? due_date - start_date : 0
|
||||||
end
|
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 ||= (
|
@soonest_start ||= (
|
||||||
relations_to.collect{|relation| relation.successor_soonest_start} +
|
relations_to(reload).collect{|relation| relation.successor_soonest_start} +
|
||||||
ancestors.collect(&:soonest_start)
|
ancestors.collect(&:soonest_start)
|
||||||
).compact.max
|
).compact.max
|
||||||
end
|
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?
|
return if date.nil?
|
||||||
if leaf?
|
if leaf?
|
||||||
if start_date.nil? || start_date < date
|
if start_date.nil? || start_date != date
|
||||||
self.start_date, self.due_date = date, date + duration
|
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
|
begin
|
||||||
save
|
save
|
||||||
rescue ActiveRecord::StaleObjectError
|
rescue ActiveRecord::StaleObjectError
|
||||||
reload
|
reload
|
||||||
self.start_date, self.due_date = date, date + duration
|
reschedule_on(date)
|
||||||
save
|
save
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
leaves.each do |leaf|
|
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
|
end
|
||||||
end
|
end
|
||||||
@@ -682,7 +936,7 @@ class Issue < ActiveRecord::Base
|
|||||||
|
|
||||||
# Returns a string of css classes that apply to the issue
|
# Returns a string of css classes that apply to the issue
|
||||||
def css_classes
|
def css_classes
|
||||||
s = "issue status-#{status.position} priority-#{priority.position}"
|
s = "issue status-#{status_id} #{priority.try(:css_classes)}"
|
||||||
s << ' closed' if closed?
|
s << ' closed' if closed?
|
||||||
s << ' overdue' if overdue?
|
s << ' overdue' if overdue?
|
||||||
s << ' child' if child?
|
s << ' child' if child?
|
||||||
@@ -732,23 +986,44 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def parent_issue_id=(arg)
|
def parent_issue_id=(arg)
|
||||||
parent_issue_id = arg.blank? ? nil : arg.to_i
|
s = arg.to_s.strip.presence
|
||||||
if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
|
if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
|
||||||
@parent_issue.id
|
@parent_issue.id
|
||||||
else
|
else
|
||||||
@parent_issue = nil
|
@parent_issue = nil
|
||||||
nil
|
@invalid_parent_issue_id = arg
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parent_issue_id
|
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
|
@parent_issue.nil? ? nil : @parent_issue.id
|
||||||
else
|
else
|
||||||
parent_id
|
parent_id
|
||||||
end
|
end
|
||||||
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.
|
# Extracted from the ReportsController.
|
||||||
def self.by_tracker(project)
|
def self.by_tracker(project)
|
||||||
count_and_group_by(:project => project,
|
count_and_group_by(:project => project,
|
||||||
@@ -828,8 +1103,9 @@ class Issue < ActiveRecord::Base
|
|||||||
relations_to.clear
|
relations_to.clear
|
||||||
end
|
end
|
||||||
|
|
||||||
# Move subtasks
|
# Move subtasks that were in the same project
|
||||||
children.each do |child|
|
children.each do |child|
|
||||||
|
next unless child.project_id == project_id_was
|
||||||
# Change project and keep project
|
# Change project and keep project
|
||||||
child.send :project=, project, true
|
child.send :project=, project, true
|
||||||
unless child.save
|
unless child.save
|
||||||
@@ -838,6 +1114,39 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# 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? && !@after_create_from_copy_handled
|
||||||
|
|
||||||
|
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
|
||||||
|
logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
|
||||||
|
next
|
||||||
|
end
|
||||||
|
copy = Issue.new.copy_from(child, @copy_options)
|
||||||
|
copy.author = author
|
||||||
|
copy.project = project
|
||||||
|
copy.parent_issue_id = id
|
||||||
|
# Children subtasks are copied recursively
|
||||||
|
unless copy.save
|
||||||
|
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
|
||||||
|
end
|
||||||
|
@after_create_from_copy_handled = true
|
||||||
|
end
|
||||||
|
|
||||||
def update_nested_set_attributes
|
def update_nested_set_attributes
|
||||||
if root_id.nil?
|
if root_id.nil?
|
||||||
# issue was just created
|
# issue was just created
|
||||||
@@ -946,7 +1255,7 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Callback on attachment deletion
|
# Callback on file attachment
|
||||||
def attachment_added(obj)
|
def attachment_added(obj)
|
||||||
if @current_journal && !obj.new_record?
|
if @current_journal && !obj.new_record?
|
||||||
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
|
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
|
||||||
@@ -994,6 +1303,13 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Make sure updated_on is updated when adding a note
|
||||||
|
def force_updated_on_change
|
||||||
|
if @current_journal
|
||||||
|
self.updated_on = current_time_from_proper_timezone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Saves the changes in a Journal
|
# Saves the changes in a Journal
|
||||||
# Called after_save
|
# Called after_save
|
||||||
def create_journal
|
def create_journal
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class IssueCategory < ActiveRecord::Base
|
|||||||
|
|
||||||
safe_attributes 'name', 'assigned_to_id'
|
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
|
alias :destroy_without_reassign :destroy
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
class IssuePriority < Enumeration
|
class IssuePriority < Enumeration
|
||||||
has_many :issues, :foreign_key => 'priority_id'
|
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
|
OptionName = :enumeration_issue_priorities
|
||||||
|
|
||||||
def option_name
|
def option_name
|
||||||
@@ -31,4 +34,35 @@ class IssuePriority < Enumeration
|
|||||||
def transfer_relations(to)
|
def transfer_relations(to)
|
||||||
issues.update_all("priority_id = #{to.id}")
|
issues.update_all("priority_id = #{to.id}")
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -15,6 +15,20 @@
|
|||||||
# along with this program; if not, write to the Free Software
|
# along with this program; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 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
|
class IssueRelation < ActiveRecord::Base
|
||||||
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
|
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
|
||||||
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_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_BLOCKED = "blocked"
|
||||||
TYPE_PRECEDES = "precedes"
|
TYPE_PRECEDES = "precedes"
|
||||||
TYPE_FOLLOWS = "follows"
|
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 },
|
TYPES = {
|
||||||
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
|
TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
|
||||||
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
|
:order => 1, :sym => TYPE_RELATES },
|
||||||
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED },
|
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
|
||||||
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
|
:order => 2, :sym => TYPE_DUPLICATED },
|
||||||
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
|
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
|
||||||
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }
|
:order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
|
||||||
}.freeze
|
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_presence_of :issue_from, :issue_to, :relation_type
|
||||||
validates_inclusion_of :relation_type, :in => TYPES.keys
|
validates_inclusion_of :relation_type, :in => TYPES.keys
|
||||||
validates_numericality_of :delay, :allow_nil => true
|
validates_numericality_of :delay, :allow_nil => true
|
||||||
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
|
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
|
||||||
|
|
||||||
validate :validate_issue_relation
|
validate :validate_issue_relation
|
||||||
|
|
||||||
attr_protected :issue_from_id, :issue_to_id
|
attr_protected :issue_from_id, :issue_to_id
|
||||||
|
|
||||||
before_save :handle_issue_order
|
before_save :handle_issue_order
|
||||||
|
|
||||||
def visible?(user=User.current)
|
def visible?(user=User.current)
|
||||||
@@ -69,14 +95,19 @@ class IssueRelation < ActiveRecord::Base
|
|||||||
def validate_issue_relation
|
def validate_issue_relation
|
||||||
if issue_from && issue_to
|
if issue_from && issue_to
|
||||||
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
|
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?
|
unless issue_from.project_id == issue_to.project_id ||
|
||||||
#detect circular dependencies depending wether the relation should be reversed
|
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]
|
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
|
||||||
errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
|
errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
|
||||||
else
|
else
|
||||||
errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
|
errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -96,7 +127,13 @@ class IssueRelation < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def label_for(issue)
|
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
|
end
|
||||||
|
|
||||||
def handle_issue_order
|
def handle_issue_order
|
||||||
@@ -113,18 +150,20 @@ class IssueRelation < ActiveRecord::Base
|
|||||||
def set_issue_to_dates
|
def set_issue_to_dates
|
||||||
soonest_start = self.successor_soonest_start
|
soonest_start = self.successor_soonest_start
|
||||||
if soonest_start && issue_to
|
if soonest_start && issue_to
|
||||||
issue_to.reschedule_after(soonest_start)
|
issue_to.reschedule_on!(soonest_start)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def successor_soonest_start
|
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
|
(issue_from.due_date || issue_from.start_date) + 1 + delay
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def <=>(relation)
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
+19
-18
@@ -17,10 +17,10 @@
|
|||||||
|
|
||||||
class IssueStatus < ActiveRecord::Base
|
class IssueStatus < ActiveRecord::Base
|
||||||
before_destroy :check_integrity
|
before_destroy :check_integrity
|
||||||
has_many :workflows, :foreign_key => "old_status_id"
|
has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
|
||||||
acts_as_list
|
acts_as_list
|
||||||
|
|
||||||
before_destroy :delete_workflows
|
before_destroy :delete_workflow_rules
|
||||||
after_save :update_default
|
after_save :update_default
|
||||||
|
|
||||||
validates_presence_of :name
|
validates_presence_of :name
|
||||||
@@ -28,23 +28,23 @@ class IssueStatus < ActiveRecord::Base
|
|||||||
validates_length_of :name, :maximum => 30
|
validates_length_of :name, :maximum => 30
|
||||||
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
|
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
|
||||||
|
|
||||||
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
|
scope :sorted, order("#{table_name}.position ASC")
|
||||||
|
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||||
|
|
||||||
def update_default
|
def update_default
|
||||||
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
|
IssueStatus.update_all({:is_default => false}, ['id <> ?', id]) if self.is_default?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the default status for new issues
|
# Returns the default status for new issues
|
||||||
def self.default
|
def self.default
|
||||||
find(:first, :conditions =>["is_default=?", true])
|
where(:is_default => true).first
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
|
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
|
||||||
def self.update_issue_done_ratios
|
def self.update_issue_done_ratios
|
||||||
if Issue.use_status_for_done_ratio?
|
if Issue.use_status_for_done_ratio?
|
||||||
IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
|
IssueStatus.where("default_done_ratio >= 0").all.each do |status|
|
||||||
Issue.update_all(["done_ratio = ?", status.default_done_ratio],
|
Issue.update_all({:done_ratio => status.default_done_ratio}, {:status_id => status.id})
|
||||||
["status_id = ?", status.id])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ class IssueStatus < ActiveRecord::Base
|
|||||||
w.tracker_id == tracker.id &&
|
w.tracker_id == tracker.id &&
|
||||||
((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
|
((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
|
||||||
end
|
end
|
||||||
transitions.collect{|w| w.new_status}.compact.sort
|
transitions.map(&:new_status).compact.sort
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
@@ -75,12 +75,12 @@ class IssueStatus < ActiveRecord::Base
|
|||||||
conditions << " OR author = :true" if author
|
conditions << " OR author = :true" if author
|
||||||
conditions << " OR assignee = :true" if assignee
|
conditions << " OR assignee = :true" if assignee
|
||||||
|
|
||||||
workflows.find(:all,
|
workflows.
|
||||||
:include => :new_status,
|
includes(:new_status).
|
||||||
:conditions => ["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})",
|
where(["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})",
|
||||||
{:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false}
|
{:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false}
|
||||||
]
|
]).all.
|
||||||
).collect{|w| w.new_status}.compact.sort
|
map(&:new_status).compact.sort
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
@@ -92,13 +92,14 @@ class IssueStatus < ActiveRecord::Base
|
|||||||
|
|
||||||
def to_s; name end
|
def to_s; name end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_integrity
|
def check_integrity
|
||||||
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
|
raise "Can't delete status" if Issue.where(:status_id => id).any?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Deletes associated workflows
|
# Deletes associated workflows
|
||||||
def delete_workflows
|
def delete_workflow_rules
|
||||||
Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
|
WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+47
-4
@@ -37,10 +37,15 @@ class Journal < ActiveRecord::Base
|
|||||||
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
|
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
|
||||||
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
|
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
|
||||||
|
|
||||||
scope :visible, lambda {|*args| {
|
before_create :split_private_notes
|
||||||
:include => {:issue => :project},
|
|
||||||
:conditions => Issue.visible_condition(args.shift || User.current, *args)
|
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)
|
def save(*args)
|
||||||
# Do not save an empty journal
|
# Do not save an empty journal
|
||||||
@@ -75,6 +80,7 @@ class Journal < ActiveRecord::Base
|
|||||||
s = 'journal'
|
s = 'journal'
|
||||||
s << ' has-notes' unless notes.blank?
|
s << ' has-notes' unless notes.blank?
|
||||||
s << ' has-details' unless details.blank?
|
s << ' has-details' unless details.blank?
|
||||||
|
s << ' private-notes' if private_notes?
|
||||||
s
|
s
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -85,4 +91,41 @@ class Journal < ActiveRecord::Base
|
|||||||
def notify=(arg)
|
def notify=(arg)
|
||||||
@notify = arg
|
@notify = arg
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
+28
-15
@@ -50,8 +50,8 @@ class MailHandler < ActionMailer::Base
|
|||||||
|
|
||||||
cattr_accessor :ignored_emails_headers
|
cattr_accessor :ignored_emails_headers
|
||||||
@@ignored_emails_headers = {
|
@@ignored_emails_headers = {
|
||||||
'X-Auto-Response-Suppress' => 'OOF',
|
'X-Auto-Response-Suppress' => 'oof',
|
||||||
'Auto-Submitted' => 'auto-replied'
|
'Auto-Submitted' => /^auto-/
|
||||||
}
|
}
|
||||||
|
|
||||||
# Processes incoming emails
|
# Processes incoming emails
|
||||||
@@ -69,11 +69,14 @@ class MailHandler < ActionMailer::Base
|
|||||||
# Ignore auto generated emails
|
# Ignore auto generated emails
|
||||||
self.class.ignored_emails_headers.each do |key, ignored_value|
|
self.class.ignored_emails_headers.each do |key, ignored_value|
|
||||||
value = email.header[key]
|
value = email.header[key]
|
||||||
if value && value.to_s.downcase == ignored_value.downcase
|
if value
|
||||||
if logger && logger.info
|
value = value.to_s.downcase
|
||||||
logger.info "MailHandler: ignoring email with #{key}:#{value} header"
|
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
|
||||||
|
if logger && logger.info
|
||||||
|
logger.info "MailHandler: ignoring email with #{key}:#{value} header"
|
||||||
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@user = User.find_by_mail(sender_email) if sender_email.present?
|
@user = User.find_by_mail(sender_email) if sender_email.present?
|
||||||
@@ -121,6 +124,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
|
|
||||||
def dispatch
|
def dispatch
|
||||||
headers = [email.in_reply_to, email.references].flatten.compact
|
headers = [email.in_reply_to, email.references].flatten.compact
|
||||||
|
subject = email.subject.to_s
|
||||||
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
|
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
|
||||||
klass, object_id = $1, $2.to_i
|
klass, object_id = $1, $2.to_i
|
||||||
method_name = "receive_#{klass}_reply"
|
method_name = "receive_#{klass}_reply"
|
||||||
@@ -129,9 +133,9 @@ class MailHandler < ActionMailer::Base
|
|||||||
else
|
else
|
||||||
# ignoring it
|
# ignoring it
|
||||||
end
|
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)
|
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)
|
receive_message_reply(m[1].to_i)
|
||||||
else
|
else
|
||||||
dispatch_to_default
|
dispatch_to_default
|
||||||
@@ -163,7 +167,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
issue = Issue.new(:author => user, :project => project)
|
issue = Issue.new(:author => user, :project => project)
|
||||||
issue.safe_attributes = issue_attributes_from_keywords(issue)
|
issue.safe_attributes = issue_attributes_from_keywords(issue)
|
||||||
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
|
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
|
||||||
issue.subject = email.subject.to_s.chomp[0,255]
|
issue.subject = cleaned_up_subject
|
||||||
if issue.subject.blank?
|
if issue.subject.blank?
|
||||||
issue.subject = '(no subject)'
|
issue.subject = '(no subject)'
|
||||||
end
|
end
|
||||||
@@ -178,7 +182,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Adds a note to an existing issue
|
# 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)
|
issue = Issue.find_by_id(issue_id)
|
||||||
return unless issue
|
return unless issue
|
||||||
# check permission
|
# check permission
|
||||||
@@ -193,6 +197,10 @@ class MailHandler < ActionMailer::Base
|
|||||||
@@handler_options[:issue].clear
|
@@handler_options[:issue].clear
|
||||||
|
|
||||||
journal = issue.init_journal(user)
|
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 = issue_attributes_from_keywords(issue)
|
||||||
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
|
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
|
||||||
journal.notes = cleaned_up_text_body
|
journal.notes = cleaned_up_text_body
|
||||||
@@ -208,7 +216,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
def receive_journal_reply(journal_id)
|
def receive_journal_reply(journal_id)
|
||||||
journal = Journal.find_by_id(journal_id)
|
journal = Journal.find_by_id(journal_id)
|
||||||
if journal && journal.journalized_type == 'Issue'
|
if journal && journal.journalized_type == 'Issue'
|
||||||
receive_issue_reply(journal.journalized_id)
|
receive_issue_reply(journal.journalized_id, journal)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -223,7 +231,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
if !message.locked?
|
if !message.locked?
|
||||||
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
|
reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
|
||||||
:content => cleaned_up_text_body)
|
:content => cleaned_up_text_body)
|
||||||
reply.author = user
|
reply.author = user
|
||||||
reply.board = message.board
|
reply.board = message.board
|
||||||
@@ -339,8 +347,8 @@ class MailHandler < ActionMailer::Base
|
|||||||
# Returns a Hash of issue custom field values extracted from keywords in the email body
|
# Returns a Hash of issue custom field values extracted from keywords in the email body
|
||||||
def custom_field_values_from_keywords(customized)
|
def custom_field_values_from_keywords(customized)
|
||||||
customized.custom_field_values.inject({}) do |h, v|
|
customized.custom_field_values.inject({}) do |h, v|
|
||||||
if value = get_keyword(v.custom_field.name, :override => true)
|
if keyword = get_keyword(v.custom_field.name, :override => true)
|
||||||
h[v.custom_field.id.to_s] = value
|
h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
|
||||||
end
|
end
|
||||||
h
|
h
|
||||||
end
|
end
|
||||||
@@ -364,6 +372,11 @@ class MailHandler < ActionMailer::Base
|
|||||||
cleanup_body(plain_text_body)
|
cleanup_body(plain_text_body)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cleaned_up_subject
|
||||||
|
subject = email.subject.to_s
|
||||||
|
subject.strip[0,255]
|
||||||
|
end
|
||||||
|
|
||||||
def self.full_sanitizer
|
def self.full_sanitizer
|
||||||
@full_sanitizer ||= HTML::FullSanitizer.new
|
@full_sanitizer ||= HTML::FullSanitizer.new
|
||||||
end
|
end
|
||||||
@@ -448,7 +461,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
if assignee.nil?
|
if assignee.nil?
|
||||||
assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
|
assignee ||= assignable.detect {|a| a.name.downcase == keyword}
|
||||||
end
|
end
|
||||||
assignee
|
assignee
|
||||||
end
|
end
|
||||||
|
|||||||
+21
-10
@@ -62,9 +62,9 @@ class Mailer < ActionMailer::Base
|
|||||||
message_id journal
|
message_id journal
|
||||||
references issue
|
references issue
|
||||||
@author = journal.user
|
@author = journal.user
|
||||||
recipients = issue.recipients
|
recipients = journal.recipients
|
||||||
# Watchers in cc
|
# Watchers in cc
|
||||||
cc = issue.watcher_recipients - recipients
|
cc = journal.watcher_recipients - recipients
|
||||||
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
|
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
|
||||||
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
|
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
|
||||||
s << issue.subject
|
s << issue.subject
|
||||||
@@ -327,22 +327,31 @@ class Mailer < ActionMailer::Base
|
|||||||
# * :days => how many days in the future to remind about (defaults to 7)
|
# * :days => how many days in the future to remind about (defaults to 7)
|
||||||
# * :tracker => id of tracker for filtering issues (defaults to all trackers)
|
# * :tracker => id of tracker for filtering issues (defaults to all trackers)
|
||||||
# * :project => id or identifier of project to process (defaults to all projects)
|
# * :project => id or identifier of project to process (defaults to all projects)
|
||||||
# * :users => array of user ids who should be reminded
|
# * :users => array of user/group ids who should be reminded
|
||||||
def self.reminders(options={})
|
def self.reminders(options={})
|
||||||
days = options[:days] || 7
|
days = options[:days] || 7
|
||||||
project = options[:project] ? Project.find(options[:project]) : nil
|
project = options[:project] ? Project.find(options[:project]) : nil
|
||||||
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
|
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
|
||||||
user_ids = options[:users]
|
user_ids = options[:users]
|
||||||
|
|
||||||
scope = Issue.open.scoped(:conditions => ["#{Issue.table_name}.assigned_to_id IS NOT NULL" +
|
scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" +
|
||||||
" AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
|
" AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
|
||||||
" AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date]
|
" AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date
|
||||||
)
|
)
|
||||||
scope = scope.scoped(:conditions => {:assigned_to_id => user_ids}) if user_ids.present?
|
scope = scope.where(:assigned_to_id => user_ids) if user_ids.present?
|
||||||
scope = scope.scoped(:conditions => {:project_id => project.id}) if project
|
scope = scope.where(:project_id => project.id) if project
|
||||||
scope = scope.scoped(:conditions => {:tracker_id => tracker.id}) if tracker
|
scope = scope.where(:tracker_id => tracker.id) if tracker
|
||||||
|
|
||||||
|
issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).all.group_by(&:assigned_to)
|
||||||
|
issues_by_assignee.keys.each do |assignee|
|
||||||
|
if assignee.is_a?(Group)
|
||||||
|
assignee.users.each do |user|
|
||||||
|
issues_by_assignee[user] ||= []
|
||||||
|
issues_by_assignee[user] += issues_by_assignee[assignee]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
issues_by_assignee = scope.all(:include => [:status, :assigned_to, :project, :tracker]).group_by(&:assigned_to)
|
|
||||||
issues_by_assignee.each do |assignee, issues|
|
issues_by_assignee.each do |assignee, issues|
|
||||||
reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
|
reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
|
||||||
end
|
end
|
||||||
@@ -361,7 +370,9 @@ class Mailer < ActionMailer::Base
|
|||||||
def self.with_synched_deliveries(&block)
|
def self.with_synched_deliveries(&block)
|
||||||
saved_method = ActionMailer::Base.delivery_method
|
saved_method = ActionMailer::Base.delivery_method
|
||||||
if m = saved_method.to_s.match(%r{^async_(.+)$})
|
if m = saved_method.to_s.match(%r{^async_(.+)$})
|
||||||
ActionMailer::Base.delivery_method = m[1].to_sym
|
synched_method = m[1]
|
||||||
|
ActionMailer::Base.delivery_method = synched_method.to_sym
|
||||||
|
ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings")
|
||||||
end
|
end
|
||||||
yield
|
yield
|
||||||
ensure
|
ensure
|
||||||
|
|||||||
+6
-10
@@ -41,9 +41,9 @@ class Message < ActiveRecord::Base
|
|||||||
validates_length_of :subject, :maximum => 255
|
validates_length_of :subject, :maximum => 255
|
||||||
validate :cannot_reply_to_locked_topic, :on => :create
|
validate :cannot_reply_to_locked_topic, :on => :create
|
||||||
|
|
||||||
after_create :add_author_as_watcher, :update_parent_last_reply
|
after_create :add_author_as_watcher, :reset_counters!
|
||||||
after_update :update_messages_board
|
after_update :update_messages_board
|
||||||
after_destroy :reset_board_counters
|
after_destroy :reset_counters!
|
||||||
|
|
||||||
scope :visible, lambda {|*args| { :include => {:board => :project},
|
scope :visible, lambda {|*args| { :include => {:board => :project},
|
||||||
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
|
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
|
||||||
@@ -63,13 +63,6 @@ class Message < ActiveRecord::Base
|
|||||||
errors.add :base, 'Topic is locked' if root.locked? && self != root
|
errors.add :base, 'Topic is locked' if root.locked? && self != root
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_parent_last_reply
|
|
||||||
if parent
|
|
||||||
parent.reload.update_attribute(:last_reply_id, self.id)
|
|
||||||
end
|
|
||||||
board.reset_counters!
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_messages_board
|
def update_messages_board
|
||||||
if board_id_changed?
|
if board_id_changed?
|
||||||
Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
|
Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
|
||||||
@@ -78,7 +71,10 @@ class Message < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_board_counters
|
def reset_counters!
|
||||||
|
if parent && parent.id
|
||||||
|
Message.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id})
|
||||||
|
end
|
||||||
board.reset_counters!
|
board.reset_counters!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -19,7 +19,7 @@ class Principal < ActiveRecord::Base
|
|||||||
self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
|
self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
|
||||||
|
|
||||||
has_many :members, :foreign_key => 'user_id', :dependent => :destroy
|
has_many :members, :foreign_key => 'user_id', :dependent => :destroy
|
||||||
has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
|
has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name"
|
||||||
has_many :projects, :through => :memberships
|
has_many :projects, :through => :memberships
|
||||||
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
|
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
|
||||||
|
|
||||||
@@ -27,19 +27,20 @@ class Principal < ActiveRecord::Base
|
|||||||
scope :active, :conditions => "#{Principal.table_name}.status = 1"
|
scope :active, :conditions => "#{Principal.table_name}.status = 1"
|
||||||
|
|
||||||
scope :like, lambda {|q|
|
scope :like, lambda {|q|
|
||||||
|
q = q.to_s
|
||||||
if q.blank?
|
if q.blank?
|
||||||
{}
|
where({})
|
||||||
else
|
else
|
||||||
q = q.to_s.downcase
|
|
||||||
pattern = "%#{q}%"
|
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}
|
params = {:p => pattern}
|
||||||
if q =~ /^(.+)\s+(.+)$/
|
if q =~ /^(.+)\s+(.+)$/
|
||||||
a, b = "#{$1}%", "#{$2}%"
|
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)
|
params.merge!(:a => a, :b => b)
|
||||||
end
|
end
|
||||||
{:conditions => [sql, params]}
|
where(sql, params)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,20 +48,20 @@ class Principal < ActiveRecord::Base
|
|||||||
scope :member_of, lambda {|projects|
|
scope :member_of, lambda {|projects|
|
||||||
projects = [projects] unless projects.is_a?(Array)
|
projects = [projects] unless projects.is_a?(Array)
|
||||||
if projects.empty?
|
if projects.empty?
|
||||||
{:conditions => "1=0"}
|
where("1=0")
|
||||||
else
|
else
|
||||||
ids = projects.map(&:id)
|
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
|
end
|
||||||
}
|
}
|
||||||
# Principals that are not members of projects
|
# Principals that are not members of projects
|
||||||
scope :not_member_of, lambda {|projects|
|
scope :not_member_of, lambda {|projects|
|
||||||
projects = [projects] unless projects.is_a?(Array)
|
projects = [projects] unless projects.is_a?(Array)
|
||||||
if projects.empty?
|
if projects.empty?
|
||||||
{:conditions => "1=0"}
|
where("1=0")
|
||||||
else
|
else
|
||||||
ids = projects.map(&:id)
|
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
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+96
-42
@@ -20,6 +20,7 @@ class Project < ActiveRecord::Base
|
|||||||
|
|
||||||
# Project statuses
|
# Project statuses
|
||||||
STATUS_ACTIVE = 1
|
STATUS_ACTIVE = 1
|
||||||
|
STATUS_CLOSED = 5
|
||||||
STATUS_ARCHIVED = 9
|
STATUS_ARCHIVED = 9
|
||||||
|
|
||||||
# Maximum length for project identifiers
|
# Maximum length for project identifiers
|
||||||
@@ -27,7 +28,7 @@ class Project < ActiveRecord::Base
|
|||||||
|
|
||||||
# Specific overidden Activities
|
# Specific overidden Activities
|
||||||
has_many :time_entry_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 :memberships, :class_name => 'Member'
|
||||||
has_many :member_principals, :class_name => 'Member',
|
has_many :member_principals, :class_name => 'Member',
|
||||||
:include => :principal,
|
:include => :principal,
|
||||||
@@ -80,6 +81,7 @@ class Project < ActiveRecord::Base
|
|||||||
# reserved words
|
# reserved words
|
||||||
validates_exclusion_of :identifier, :in => %w( new )
|
validates_exclusion_of :identifier, :in => %w( new )
|
||||||
|
|
||||||
|
after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
|
||||||
before_destroy :delete_all_members
|
before_destroy :delete_all_members
|
||||||
|
|
||||||
scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
|
scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
|
||||||
@@ -121,7 +123,7 @@ class Project < ActiveRecord::Base
|
|||||||
self.enabled_module_names = Setting.default_projects_modules
|
self.enabled_module_names = Setting.default_projects_modules
|
||||||
end
|
end
|
||||||
if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
|
if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
|
||||||
self.trackers = Tracker.all
|
self.trackers = Tracker.sorted.all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -130,7 +132,7 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def identifier_frozen?
|
def identifier_frozen?
|
||||||
errors[:identifier].nil? && !(new_record? || identifier.blank?)
|
errors[:identifier].blank? && !(new_record? || identifier.blank?)
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns latest created projects
|
# returns latest created projects
|
||||||
@@ -161,12 +163,11 @@ class Project < ActiveRecord::Base
|
|||||||
# * :with_subprojects => limit the condition to project and its subprojects
|
# * :with_subprojects => limit the condition to project and its subprojects
|
||||||
# * :member => limit the condition to the user projects
|
# * :member => limit the condition to the user projects
|
||||||
def self.allowed_to_condition(user, permission, options={})
|
def self.allowed_to_condition(user, permission, options={})
|
||||||
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
|
perm = Redmine::AccessControl.permission(permission)
|
||||||
if perm = Redmine::AccessControl.permission(permission)
|
base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
|
||||||
unless perm.project_module.nil?
|
if perm && perm.project_module
|
||||||
# If the permission belongs to a project module, make sure the module is enabled
|
# If the permission belongs to a project module, make sure the module is enabled
|
||||||
base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
|
base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
|
||||||
end
|
|
||||||
end
|
end
|
||||||
if options[:project]
|
if options[:project]
|
||||||
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
|
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
|
||||||
@@ -186,7 +187,7 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
if user.logged?
|
if user.logged?
|
||||||
user.projects_by_role.each do |role, projects|
|
user.projects_by_role.each do |role, projects|
|
||||||
if role.allowed_to?(permission)
|
if role.allowed_to?(permission) && projects.any?
|
||||||
statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
|
statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -325,6 +326,14 @@ class Project < ActiveRecord::Base
|
|||||||
update_attribute :status, STATUS_ACTIVE
|
update_attribute :status, STATUS_ACTIVE
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
|
||||||
|
end
|
||||||
|
|
||||||
|
def reopen
|
||||||
|
self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
|
||||||
|
end
|
||||||
|
|
||||||
# Returns an array of projects the project can be moved to
|
# Returns an array of projects the project can be moved to
|
||||||
# by the current user
|
# by the current user
|
||||||
def allowed_parents
|
def allowed_parents
|
||||||
@@ -375,22 +384,7 @@ class Project < ActiveRecord::Base
|
|||||||
# Nothing to do
|
# Nothing to do
|
||||||
true
|
true
|
||||||
elsif p.nil? || (p.active? && move_possible?(p))
|
elsif p.nil? || (p.active? && move_possible?(p))
|
||||||
# Insert the project so that target's children or root projects stay alphabetically sorted
|
set_or_update_position_under(p)
|
||||||
sibs = (p.nil? ? self.class.roots : p.children)
|
|
||||||
to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
|
|
||||||
if to_be_inserted_before
|
|
||||||
move_to_left_of(to_be_inserted_before)
|
|
||||||
elsif p.nil?
|
|
||||||
if sibs.empty?
|
|
||||||
# move_to_root adds the project in first (ie. left) position
|
|
||||||
move_to_root
|
|
||||||
else
|
|
||||||
move_to_right_of(sibs.last) unless self == sibs.last
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# move_to_child_of adds the project in last (ie.right) position
|
|
||||||
move_to_child_of(p)
|
|
||||||
end
|
|
||||||
Issue.update_versions_from_hierarchy_change(self)
|
Issue.update_versions_from_hierarchy_change(self)
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
@@ -399,12 +393,22 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
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
|
# Returns an array of the trackers used by the project and its active sub projects
|
||||||
def rolled_up_trackers
|
def rolled_up_trackers
|
||||||
@rolled_up_trackers ||=
|
@rolled_up_trackers ||=
|
||||||
Tracker.find(:all, :joins => :projects,
|
Tracker.find(:all, :joins => :projects,
|
||||||
:select => "DISTINCT #{Tracker.table_name}.*",
|
:select => "DISTINCT #{Tracker.table_name}.*",
|
||||||
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
|
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
|
||||||
:order => "#{Tracker.table_name}.position")
|
:order => "#{Tracker.table_name}.position")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -423,20 +427,20 @@ class Project < ActiveRecord::Base
|
|||||||
def rolled_up_versions
|
def rolled_up_versions
|
||||||
@rolled_up_versions ||=
|
@rolled_up_versions ||=
|
||||||
Version.scoped(:include => :project,
|
Version.scoped(:include => :project,
|
||||||
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
|
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a scope of the Versions used by the project
|
# Returns a scope of the Versions used by the project
|
||||||
def shared_versions
|
def shared_versions
|
||||||
if new_record?
|
if new_record?
|
||||||
Version.scoped(:include => :project,
|
Version.scoped(:include => :project,
|
||||||
:conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
|
:conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
|
||||||
else
|
else
|
||||||
@shared_versions ||= begin
|
@shared_versions ||= begin
|
||||||
r = root? ? self : root
|
r = root? ? self : root
|
||||||
Version.scoped(:include => :project,
|
Version.scoped(:include => :project,
|
||||||
:conditions => "#{Project.table_name}.id = #{id}" +
|
:conditions => "#{Project.table_name}.id = #{id}" +
|
||||||
" OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
|
" OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
|
||||||
" #{Version.table_name}.sharing = 'system'" +
|
" #{Version.table_name}.sharing = 'system'" +
|
||||||
" OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
|
" OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
|
||||||
" OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
|
" OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
|
||||||
@@ -478,7 +482,7 @@ class Project < ActiveRecord::Base
|
|||||||
# Returns the users that should be notified on project events
|
# Returns the users that should be notified on project events
|
||||||
def notified_users
|
def notified_users
|
||||||
# TODO: User part should be extracted to User#notify_about?
|
# 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
|
end
|
||||||
|
|
||||||
# Returns an array of all custom fields enabled for project issues
|
# Returns an array of all custom fields enabled for project issues
|
||||||
@@ -515,6 +519,13 @@ class Project < ActiveRecord::Base
|
|||||||
s << ' root' if root?
|
s << ' root' if root?
|
||||||
s << ' child' if child?
|
s << ' child' if child?
|
||||||
s << (leaf? ? ' leaf' : ' parent')
|
s << (leaf? ? ' leaf' : ' parent')
|
||||||
|
unless active?
|
||||||
|
if archived?
|
||||||
|
s << ' archived'
|
||||||
|
else
|
||||||
|
s << ' closed'
|
||||||
|
end
|
||||||
|
end
|
||||||
s
|
s
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -558,11 +569,20 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return true if this project is allowed to do the specified action.
|
# Return true if this project allows to do the specified action.
|
||||||
# action can be:
|
# action can be:
|
||||||
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
||||||
# * a permission Symbol (eg. :edit_project)
|
# * a permission Symbol (eg. :edit_project)
|
||||||
def allows_to?(action)
|
def allows_to?(action)
|
||||||
|
if archived?
|
||||||
|
# No action allowed on archived projects
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
unless active? || Redmine::AccessControl.read_action?(action)
|
||||||
|
# No write action allowed on closed projects
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
# No action allowed on disabled modules
|
||||||
if action.is_a? Hash
|
if action.is_a? Hash
|
||||||
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
|
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
|
||||||
else
|
else
|
||||||
@@ -711,7 +731,7 @@ class Project < ActiveRecord::Base
|
|||||||
def copy_wiki(project)
|
def copy_wiki(project)
|
||||||
# Check that the source project has a wiki first
|
# Check that the source project has a wiki first
|
||||||
unless project.wiki.nil?
|
unless project.wiki.nil?
|
||||||
self.wiki ||= Wiki.new
|
wiki = self.wiki || Wiki.new
|
||||||
wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
|
wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
|
||||||
wiki_pages_map = {}
|
wiki_pages_map = {}
|
||||||
project.wiki.pages.each do |page|
|
project.wiki.pages.each do |page|
|
||||||
@@ -723,6 +743,8 @@ class Project < ActiveRecord::Base
|
|||||||
wiki.pages << new_wiki_page
|
wiki.pages << new_wiki_page
|
||||||
wiki_pages_map[page.id] = new_wiki_page
|
wiki_pages_map[page.id] = new_wiki_page
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self.wiki = wiki
|
||||||
wiki.save
|
wiki.save
|
||||||
# Reproduce page hierarchy
|
# Reproduce page hierarchy
|
||||||
project.wiki.pages.each do |page|
|
project.wiki.pages.each do |page|
|
||||||
@@ -753,27 +775,30 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Copies issues from +project+
|
# Copies issues from +project+
|
||||||
# Note: issues assigned to a closed version won't be copied due to validation rules
|
|
||||||
def copy_issues(project)
|
def copy_issues(project)
|
||||||
# Stores the source issue id as a key and the copied issues as the
|
# Stores the source issue id as a key and the copied issues as the
|
||||||
# value. Used to map the two togeather for issue relations.
|
# value. Used to map the two togeather for issue relations.
|
||||||
issues_map = {}
|
issues_map = {}
|
||||||
|
|
||||||
|
# Store status and reopen locked/closed versions
|
||||||
|
version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
|
||||||
|
version_statuses.each do |version, status|
|
||||||
|
version.update_attribute :status, 'open'
|
||||||
|
end
|
||||||
|
|
||||||
# Get issues sorted by root_id, lft so that parent issues
|
# Get issues sorted by root_id, lft so that parent issues
|
||||||
# get copied before their children
|
# get copied before their children
|
||||||
project.issues.find(:all, :order => 'root_id, lft').each do |issue|
|
project.issues.find(:all, :order => 'root_id, lft').each do |issue|
|
||||||
new_issue = Issue.new
|
new_issue = Issue.new
|
||||||
new_issue.copy_from(issue)
|
new_issue.copy_from(issue, :subtasks => false, :link => false)
|
||||||
new_issue.project = self
|
new_issue.project = self
|
||||||
# Reassign fixed_versions by name, since names are unique per
|
# Reassign fixed_versions by name, since names are unique per project
|
||||||
# project and the versions for self are not yet saved
|
if issue.fixed_version && issue.fixed_version.project == project
|
||||||
if issue.fixed_version
|
new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
|
||||||
new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
|
|
||||||
end
|
end
|
||||||
# Reassign the category by name, since names are unique per
|
# Reassign the category by name, since names are unique per project
|
||||||
# project and the categories for self are not yet saved
|
|
||||||
if issue.category
|
if issue.category
|
||||||
new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
|
new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
|
||||||
end
|
end
|
||||||
# Parent issue
|
# Parent issue
|
||||||
if issue.parent_id
|
if issue.parent_id
|
||||||
@@ -790,6 +815,11 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Restore locked/closed version statuses
|
||||||
|
version_statuses.each do |version, status|
|
||||||
|
version.update_attribute :status, status
|
||||||
|
end
|
||||||
|
|
||||||
# Relations after in case issues related each other
|
# Relations after in case issues related each other
|
||||||
project.issues.each do |issue|
|
project.issues.each do |issue|
|
||||||
new_issue = issues_map[issue.id]
|
new_issue = issues_map[issue.id]
|
||||||
@@ -919,4 +949,28 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
update_attribute :status, STATUS_ARCHIVED
|
update_attribute :status, STATUS_ARCHIVED
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_position_under_parent
|
||||||
|
set_or_update_position_under(parent)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Inserts/moves the project so that target's children or root projects stay alphabetically sorted
|
||||||
|
def set_or_update_position_under(target_parent)
|
||||||
|
sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
|
||||||
|
to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
|
||||||
|
|
||||||
|
if to_be_inserted_before
|
||||||
|
move_to_left_of(to_be_inserted_before)
|
||||||
|
elsif target_parent.nil?
|
||||||
|
if sibs.empty?
|
||||||
|
# move_to_root adds the project in first (ie. left) position
|
||||||
|
move_to_root
|
||||||
|
else
|
||||||
|
move_to_right_of(sibs.last) unless self == sibs.last
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# move_to_child_of adds the project in last (ie.right) position
|
||||||
|
move_to_child_of(target_parent)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+308
-66
@@ -27,6 +27,7 @@ class QueryColumn
|
|||||||
self.groupable = name.to_s
|
self.groupable = name.to_s
|
||||||
end
|
end
|
||||||
self.default_order = options[:default_order]
|
self.default_order = options[:default_order]
|
||||||
|
@inline = options.key?(:inline) ? options[:inline] : true
|
||||||
@caption_key = options[:caption] || "field_#{name}"
|
@caption_key = options[:caption] || "field_#{name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43,6 +44,10 @@ class QueryColumn
|
|||||||
@sortable.is_a?(Proc) ? @sortable.call : @sortable
|
@sortable.is_a?(Proc) ? @sortable.call : @sortable
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inline?
|
||||||
|
@inline
|
||||||
|
end
|
||||||
|
|
||||||
def value(issue)
|
def value(issue)
|
||||||
issue.send name
|
issue.send name
|
||||||
end
|
end
|
||||||
@@ -57,10 +62,8 @@ class QueryCustomFieldColumn < QueryColumn
|
|||||||
def initialize(custom_field)
|
def initialize(custom_field)
|
||||||
self.name = "cf_#{custom_field.id}".to_sym
|
self.name = "cf_#{custom_field.id}".to_sym
|
||||||
self.sortable = custom_field.order_statement || false
|
self.sortable = custom_field.order_statement || false
|
||||||
if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
|
self.groupable = custom_field.group_statement || false
|
||||||
self.groupable = custom_field.order_statement
|
@inline = true
|
||||||
end
|
|
||||||
self.groupable ||= false
|
|
||||||
@cf = custom_field
|
@cf = custom_field
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -74,7 +77,7 @@ class QueryCustomFieldColumn < QueryColumn
|
|||||||
|
|
||||||
def value(issue)
|
def value(issue)
|
||||||
cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
|
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
|
end
|
||||||
|
|
||||||
def css_classes
|
def css_classes
|
||||||
@@ -103,20 +106,25 @@ class Query < ActiveRecord::Base
|
|||||||
"o" => :label_open_issues,
|
"o" => :label_open_issues,
|
||||||
"c" => :label_closed_issues,
|
"c" => :label_closed_issues,
|
||||||
"!*" => :label_none,
|
"!*" => :label_none,
|
||||||
"*" => :label_all,
|
"*" => :label_any,
|
||||||
">=" => :label_greater_or_equal,
|
">=" => :label_greater_or_equal,
|
||||||
"<=" => :label_less_or_equal,
|
"<=" => :label_less_or_equal,
|
||||||
"><" => :label_between,
|
"><" => :label_between,
|
||||||
"<t+" => :label_in_less_than,
|
"<t+" => :label_in_less_than,
|
||||||
">t+" => :label_in_more_than,
|
">t+" => :label_in_more_than,
|
||||||
|
"><t+"=> :label_in_the_next_days,
|
||||||
"t+" => :label_in,
|
"t+" => :label_in,
|
||||||
"t" => :label_today,
|
"t" => :label_today,
|
||||||
"w" => :label_this_week,
|
"w" => :label_this_week,
|
||||||
">t-" => :label_less_than_ago,
|
">t-" => :label_less_than_ago,
|
||||||
"<t-" => :label_more_than_ago,
|
"<t-" => :label_more_than_ago,
|
||||||
|
"><t-"=> :label_in_the_past_days,
|
||||||
"t-" => :label_ago,
|
"t-" => :label_ago,
|
||||||
"~" => :label_contains,
|
"~" => :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
|
cattr_reader :operators
|
||||||
|
|
||||||
@@ -124,12 +132,13 @@ class Query < ActiveRecord::Base
|
|||||||
:list_status => [ "o", "=", "!", "c", "*" ],
|
:list_status => [ "o", "=", "!", "c", "*" ],
|
||||||
:list_optional => [ "=", "!", "!*", "*" ],
|
:list_optional => [ "=", "!", "!*", "*" ],
|
||||||
:list_subprojects => [ "*", "!*", "=" ],
|
:list_subprojects => [ "*", "!*", "=" ],
|
||||||
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
|
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
|
||||||
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
|
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
|
||||||
:string => [ "=", "~", "!", "!~", "!*", "*" ],
|
:string => [ "=", "~", "!", "!~", "!*", "*" ],
|
||||||
:text => [ "~", "!~", "!*", "*" ],
|
:text => [ "~", "!~", "!*", "*" ],
|
||||||
:integer => [ "=", ">=", "<=", "><", "!*", "*" ],
|
:integer => [ "=", ">=", "<=", "><", "!*", "*" ],
|
||||||
:float => [ "=", ">=", "<=", "><", "!*", "*" ] }
|
:float => [ "=", ">=", "<=", "><", "!*", "*" ],
|
||||||
|
:relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
|
||||||
|
|
||||||
cattr_reader :operators_by_filter_type
|
cattr_reader :operators_by_filter_type
|
||||||
|
|
||||||
@@ -144,12 +153,14 @@ class Query < ActiveRecord::Base
|
|||||||
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
|
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
|
||||||
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
|
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
|
||||||
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
|
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
|
||||||
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
|
QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
|
||||||
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
|
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
|
||||||
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
|
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
|
||||||
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
|
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
|
||||||
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
|
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(: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
|
cattr_reader :available_columns
|
||||||
|
|
||||||
@@ -174,14 +185,14 @@ class Query < ActiveRecord::Base
|
|||||||
if values_for(field)
|
if values_for(field)
|
||||||
case type_for(field)
|
case type_for(field)
|
||||||
when :integer
|
when :integer
|
||||||
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
|
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
|
||||||
when :float
|
when :float
|
||||||
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
|
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
|
||||||
when :date, :date_past
|
when :date, :date_past
|
||||||
case operator_for(field)
|
case operator_for(field)
|
||||||
when "=", ">=", "<=", "><"
|
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?) }
|
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+$/) }
|
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -213,48 +224,68 @@ class Query < ActiveRecord::Base
|
|||||||
is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
|
is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def trackers
|
||||||
|
@trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a hash of localized labels for all filter operators
|
||||||
|
def self.operators_labels
|
||||||
|
operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
|
||||||
|
end
|
||||||
|
|
||||||
def available_filters
|
def available_filters
|
||||||
return @available_filters if @available_filters
|
return @available_filters if @available_filters
|
||||||
|
@available_filters = {
|
||||||
trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
|
"status_id" => {
|
||||||
|
:type => :list_status, :order => 0,
|
||||||
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
|
: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] } },
|
"tracker_id" => {
|
||||||
"subject" => { :type => :text, :order => 8 },
|
:type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
|
||||||
"created_on" => { :type => :date_past, :order => 9 },
|
},
|
||||||
"updated_on" => { :type => :date_past, :order => 10 },
|
"priority_id" => {
|
||||||
"start_date" => { :type => :date, :order => 11 },
|
:type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
|
||||||
"due_date" => { :type => :date, :order => 12 },
|
},
|
||||||
"estimated_hours" => { :type => :float, :order => 13 },
|
"subject" => { :type => :text, :order => 8 },
|
||||||
"done_ratio" => { :type => :integer, :order => 14 }}
|
"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 = []
|
principals = []
|
||||||
if project
|
if project
|
||||||
principals += project.principals.sort
|
principals += project.principals.sort
|
||||||
unless project.leaf?
|
unless project.leaf?
|
||||||
subprojects = project.descendants.visible.all
|
subprojects = project.descendants.visible.all
|
||||||
if subprojects.any?
|
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)
|
principals += Principal.member_of(subprojects)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
all_projects = Project.visible.all
|
|
||||||
if all_projects.any?
|
if all_projects.any?
|
||||||
# members of visible projects
|
# members of visible projects
|
||||||
principals += Principal.member_of(all_projects)
|
principals += Principal.member_of(all_projects)
|
||||||
|
|
||||||
# project filter
|
# project filter
|
||||||
project_values = []
|
project_values = []
|
||||||
if User.current.logged? && User.current.memberships.any?
|
if User.current.logged? && User.current.memberships.any?
|
||||||
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
|
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
|
||||||
end
|
end
|
||||||
Project.project_tree(all_projects) do |p, level|
|
project_values += all_projects_values
|
||||||
prefix = (level > 0 ? ('--' * level + ' ') : '')
|
@available_filters["project_id"] = {
|
||||||
project_values << ["#{prefix}#{p.name}", p.id.to_s]
|
:type => :list, :order => 1, :values => project_values
|
||||||
end
|
} unless project_values.empty?
|
||||||
@available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
principals.uniq!
|
principals.uniq!
|
||||||
@@ -263,46 +294,111 @@ class Query < ActiveRecord::Base
|
|||||||
|
|
||||||
assigned_to_values = []
|
assigned_to_values = []
|
||||||
assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
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] }
|
assigned_to_values += (Setting.issue_group_assignment? ?
|
||||||
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
|
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 = []
|
||||||
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
|
||||||
author_values += users.collect{|s| [s.name, s.id.to_s] }
|
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] }
|
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] }
|
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?
|
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
|
end
|
||||||
|
|
||||||
if project
|
if project
|
||||||
# project specific filters
|
# project specific filters
|
||||||
categories = project.issue_categories.all
|
categories = project.issue_categories.all
|
||||||
unless categories.empty?
|
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
|
end
|
||||||
versions = project.shared_versions.all
|
versions = project.shared_versions.all
|
||||||
unless versions.empty?
|
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
|
end
|
||||||
add_custom_fields_filters(project.all_issue_custom_fields)
|
add_custom_fields_filters(project.all_issue_custom_fields)
|
||||||
else
|
else
|
||||||
# global filters for cross project issue list
|
# global filters for cross project issue list
|
||||||
system_shared_versions = Version.visible.find_all_by_sharing('system')
|
system_shared_versions = Version.visible.find_all_by_sharing('system')
|
||||||
unless system_shared_versions.empty?
|
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
|
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 => 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(options[:label] || "field_#{field}".gsub(/_id$/, ''))
|
||||||
end
|
end
|
||||||
@available_filters
|
@available_filters
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns a representation of the available filters for JSON serialization
|
||||||
|
def available_filters_as_json
|
||||||
|
json = {}
|
||||||
|
available_filters.each do |field, options|
|
||||||
|
json[field] = options.slice(:type, :name, :values).stringify_keys
|
||||||
|
end
|
||||||
|
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)
|
def add_filter(field, operator, values)
|
||||||
# values must be an array
|
# values must be an array
|
||||||
return unless values.nil? || values.is_a?(Array)
|
return unless values.nil? || values.is_a?(Array)
|
||||||
@@ -380,6 +476,17 @@ class Query < ActiveRecord::Base
|
|||||||
:caption => :label_spent_time
|
:caption => :label_spent_time
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
|
||||||
|
User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
|
||||||
|
@available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
|
||||||
|
end
|
||||||
|
|
||||||
|
disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
|
||||||
|
@available_columns.reject! {|column|
|
||||||
|
disabled_fields.include?(column.name.to_s)
|
||||||
|
}
|
||||||
|
|
||||||
@available_columns
|
@available_columns
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -411,6 +518,22 @@ class Query < ActiveRecord::Base
|
|||||||
end.compact
|
end.compact
|
||||||
end
|
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
|
def default_columns_names
|
||||||
@default_columns_names ||= begin
|
@default_columns_names ||= begin
|
||||||
default_columns = Setting.issue_list_default_columns.map(&:to_sym)
|
default_columns = Setting.issue_list_default_columns.map(&:to_sym)
|
||||||
@@ -444,7 +567,7 @@ class Query < ActiveRecord::Base
|
|||||||
if arg.is_a?(Hash)
|
if arg.is_a?(Hash)
|
||||||
arg = arg.keys.sort.collect {|k| arg[k]}
|
arg = arg.keys.sort.collect {|k| arg[k]}
|
||||||
end
|
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)
|
write_attribute(:sort_criteria, c)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -460,12 +583,17 @@ class Query < ActiveRecord::Base
|
|||||||
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
|
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
|
||||||
end
|
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
|
# Returns the SQL sort order that should be prepended for grouping
|
||||||
def group_by_sort_order
|
def group_by_sort_order
|
||||||
if grouped? && (column = group_by_column)
|
if grouped? && (column = group_by_column)
|
||||||
|
order = sort_criteria_order_for(column.name) || column.default_order
|
||||||
column.sortable.is_a?(Array) ?
|
column.sortable.is_a?(Array) ?
|
||||||
column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
|
column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
|
||||||
"#{column.sortable} #{column.default_order}"
|
"#{column.sortable} #{order}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -534,7 +662,7 @@ class Query < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if field =~ /^cf_(\d+)$/
|
if field =~ /cf_(\d+)$/
|
||||||
# custom field
|
# custom field
|
||||||
filters_clauses << sql_for_custom_field(field, operator, v, $1)
|
filters_clauses << sql_for_custom_field(field, operator, v, $1)
|
||||||
elsif respond_to?("sql_for_#{field}_field")
|
elsif respond_to?("sql_for_#{field}_field")
|
||||||
@@ -585,18 +713,19 @@ class Query < ActiveRecord::Base
|
|||||||
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
|
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
|
||||||
order_option = nil if order_option.blank?
|
order_option = nil if order_option.blank?
|
||||||
|
|
||||||
joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
|
|
||||||
|
|
||||||
issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
|
issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
|
||||||
:conditions => statement,
|
:conditions => statement,
|
||||||
:order => order_option,
|
:order => order_option,
|
||||||
:joins => joins,
|
:joins => joins_for_order_statement(order_option),
|
||||||
:limit => options[:limit],
|
:limit => options[:limit],
|
||||||
:offset => options[:offset]
|
:offset => options[:offset]
|
||||||
|
|
||||||
if has_column?(:spent_hours)
|
if has_column?(:spent_hours)
|
||||||
Issue.load_visible_spent_hours(issues)
|
Issue.load_visible_spent_hours(issues)
|
||||||
end
|
end
|
||||||
|
if has_column?(:relations)
|
||||||
|
Issue.load_visible_relations(issues)
|
||||||
|
end
|
||||||
issues
|
issues
|
||||||
rescue ::ActiveRecord::StatementInvalid => e
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
raise StatementInvalid.new(e.message)
|
raise StatementInvalid.new(e.message)
|
||||||
@@ -607,12 +736,10 @@ class Query < ActiveRecord::Base
|
|||||||
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
|
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
|
||||||
order_option = nil if order_option.blank?
|
order_option = nil if order_option.blank?
|
||||||
|
|
||||||
joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
|
|
||||||
|
|
||||||
Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
|
Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
|
||||||
:conditions => statement,
|
:conditions => statement,
|
||||||
:order => order_option,
|
:order => order_option,
|
||||||
:joins => joins,
|
:joins => joins_for_order_statement(order_option),
|
||||||
:limit => options[:limit],
|
:limit => options[:limit],
|
||||||
:offset => options[:offset]).find_ids
|
:offset => options[:offset]).find_ids
|
||||||
rescue ::ActiveRecord::StatementInvalid => e
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
@@ -686,13 +813,57 @@ class Query < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sql_for_is_private_field(field, operator, value)
|
||||||
|
op = (operator == "=" ? 'IN' : 'NOT IN')
|
||||||
|
va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
|
||||||
|
|
||||||
|
"#{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
|
private
|
||||||
|
|
||||||
def sql_for_custom_field(field, operator, value, custom_field_id)
|
def sql_for_custom_field(field, operator, value, custom_field_id)
|
||||||
db_table = CustomValue.table_name
|
db_table = CustomValue.table_name
|
||||||
db_field = 'value'
|
db_field = 'value'
|
||||||
filter = @available_filters[field]
|
filter = @available_filters[field]
|
||||||
if filter && filter[:format] == 'user'
|
return nil unless filter
|
||||||
|
if filter[:format] == 'user'
|
||||||
if value.delete('me')
|
if value.delete('me')
|
||||||
value.push User.current.id.to_s
|
value.push User.current.id.to_s
|
||||||
end
|
end
|
||||||
@@ -703,7 +874,15 @@ class Query < ActiveRecord::Base
|
|||||||
operator = '='
|
operator = '='
|
||||||
not_in = 'NOT'
|
not_in = 'NOT'
|
||||||
end
|
end
|
||||||
"#{Issue.table_name}.id #{not_in} IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
|
customized_key = "id"
|
||||||
|
customized_class = Issue
|
||||||
|
if field =~ /^(.+)\.cf_/
|
||||||
|
assoc = $1
|
||||||
|
customized_key = "#{assoc}_id"
|
||||||
|
customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
|
||||||
|
raise "Unknown Issue association #{assoc}" unless customized_class
|
||||||
|
end
|
||||||
|
"#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
|
||||||
sql_for_field(field, operator, value, db_table, db_field, true) + ')'
|
sql_for_field(field, operator, value, db_table, db_field, true) + ')'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -782,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"
|
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"
|
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"
|
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)
|
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-"
|
when "<t-"
|
||||||
|
# <= today - n days
|
||||||
sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
|
sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
|
||||||
when "t-"
|
when "t-"
|
||||||
|
# = n days in past
|
||||||
sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
|
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+"
|
when ">t+"
|
||||||
|
# >= today + n days
|
||||||
sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
|
sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
|
||||||
when "<t+"
|
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+"
|
when "t+"
|
||||||
|
# = today + n days
|
||||||
sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
|
sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
|
||||||
when "t"
|
when "t"
|
||||||
|
# = today
|
||||||
sql = relative_date_clause(db_table, db_field, 0, 0)
|
sql = relative_date_clause(db_table, db_field, 0, 0)
|
||||||
when "w"
|
when "w"
|
||||||
|
# = this week
|
||||||
first_day_of_week = l(:general_first_day_of_week).to_i
|
first_day_of_week = l(:general_first_day_of_week).to_i
|
||||||
day_of_week = Date.today.cwday
|
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)
|
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
|
||||||
@@ -812,7 +1005,8 @@ class Query < ActiveRecord::Base
|
|||||||
return sql
|
return sql
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_custom_fields_filters(custom_fields)
|
def add_custom_fields_filters(custom_fields, assoc=nil)
|
||||||
|
return unless custom_fields.present?
|
||||||
@available_filters ||= {}
|
@available_filters ||= {}
|
||||||
|
|
||||||
custom_fields.select(&:is_filter?).each do |field|
|
custom_fields.select(&:is_filter?).each do |field|
|
||||||
@@ -839,7 +1033,29 @@ class Query < ActiveRecord::Base
|
|||||||
else
|
else
|
||||||
options = { :type => :string, :order => 20 }
|
options = { :type => :string, :order => 20 }
|
||||||
end
|
end
|
||||||
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
|
filter_id = "cf_#{field.id}"
|
||||||
|
filter_name = field.name
|
||||||
|
if assoc.present?
|
||||||
|
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,
|
||||||
|
:field => field
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_associations_custom_fields_filters(*associations)
|
||||||
|
fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
|
||||||
|
associations.each do |assoc|
|
||||||
|
association_klass = Issue.reflect_on_association(assoc).klass
|
||||||
|
fields_by_class.each do |field_class, fields|
|
||||||
|
if field_class.customized_class <= association_klass
|
||||||
|
add_custom_fields_filters(fields, assoc)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -848,12 +1064,18 @@ class Query < ActiveRecord::Base
|
|||||||
s = []
|
s = []
|
||||||
if from
|
if from
|
||||||
from_yesterday = from - 1
|
from_yesterday = from - 1
|
||||||
from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
|
from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
|
||||||
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
|
if self.class.default_timezone == :utc
|
||||||
|
from_yesterday_time = from_yesterday_time.utc
|
||||||
|
end
|
||||||
|
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
|
||||||
end
|
end
|
||||||
if to
|
if to
|
||||||
to_utc = Time.gm(to.year, to.month, to.day)
|
to_time = Time.local(to.year, to.month, to.day)
|
||||||
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
|
if self.class.default_timezone == :utc
|
||||||
|
to_time = to_time.utc
|
||||||
|
end
|
||||||
|
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
|
||||||
end
|
end
|
||||||
s.join(' AND ')
|
s.join(' AND ')
|
||||||
end
|
end
|
||||||
@@ -862,4 +1084,24 @@ class Query < ActiveRecord::Base
|
|||||||
def relative_date_clause(table, field, days_from, days_to)
|
def relative_date_clause(table, field, days_from, days_to)
|
||||||
date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
|
date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Additional joins required for the given sort options
|
||||||
|
def joins_for_order_statement(order_options)
|
||||||
|
joins = []
|
||||||
|
|
||||||
|
if order_options
|
||||||
|
if order_options.include?('authors')
|
||||||
|
joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
|
||||||
|
end
|
||||||
|
order_options.scan(/cf_\d+/).uniq.each do |name|
|
||||||
|
column = available_columns.detect {|c| c.name.to_s == name}
|
||||||
|
join = column && column.custom_field.join_for_order_statement
|
||||||
|
if join
|
||||||
|
joins << join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
joins.any? ? joins.join(' ') : nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ class ScmFetchError < Exception; end
|
|||||||
|
|
||||||
class Repository < ActiveRecord::Base
|
class Repository < ActiveRecord::Base
|
||||||
include Redmine::Ciphering
|
include Redmine::Ciphering
|
||||||
|
include Redmine::SafeAttributes
|
||||||
|
|
||||||
|
# Maximum length for repository identifiers
|
||||||
|
IDENTIFIER_MAX_LENGTH = 255
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
|
has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
|
||||||
has_many :changes, :through => :changesets
|
has_many :filechanges, :class_name => 'Change', :through => :changesets
|
||||||
|
|
||||||
serialize :extra_info
|
serialize :extra_info
|
||||||
|
|
||||||
@@ -33,15 +37,25 @@ class Repository < ActiveRecord::Base
|
|||||||
before_destroy :clear_changesets
|
before_destroy :clear_changesets
|
||||||
|
|
||||||
validates_length_of :password, :maximum => 255, :allow_nil => true
|
validates_length_of :password, :maximum => 255, :allow_nil => true
|
||||||
validates_length_of :identifier, :maximum => 255, :allow_blank => true
|
validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
|
||||||
validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
|
validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
|
||||||
validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
|
validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
|
||||||
validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
|
validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
|
||||||
# donwcase letters, digits, dashes but not digits only
|
# donwcase letters, digits, dashes, underscores but not digits only
|
||||||
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
|
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true
|
||||||
# Checks if the SCM is enabled when creating a repository
|
# Checks if the SCM is enabled when creating a repository
|
||||||
validate :repo_create_validation, :on => :create
|
validate :repo_create_validation, :on => :create
|
||||||
|
|
||||||
|
safe_attributes 'identifier',
|
||||||
|
'login',
|
||||||
|
'password',
|
||||||
|
'path_encoding',
|
||||||
|
'log_encoding',
|
||||||
|
'is_default'
|
||||||
|
|
||||||
|
safe_attributes 'url',
|
||||||
|
:if => lambda {|repository, user| repository.new_record?}
|
||||||
|
|
||||||
def repo_create_validation
|
def repo_create_validation
|
||||||
unless Setting.enabled_scm.include?(self.class.name.demodulize)
|
unless Setting.enabled_scm.include?(self.class.name.demodulize)
|
||||||
errors.add(:type, :invalid)
|
errors.add(:type, :invalid)
|
||||||
@@ -103,6 +117,14 @@ class Repository < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def identifier=(identifier)
|
||||||
|
super unless identifier_frozen?
|
||||||
|
end
|
||||||
|
|
||||||
|
def identifier_frozen?
|
||||||
|
errors[:identifier].blank? && !(new_record? || identifier.blank?)
|
||||||
|
end
|
||||||
|
|
||||||
def identifier_param
|
def identifier_param
|
||||||
if is_default?
|
if is_default?
|
||||||
nil
|
nil
|
||||||
@@ -167,7 +189,9 @@ class Repository < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def entries(path=nil, identifier=nil)
|
def entries(path=nil, identifier=nil)
|
||||||
scm.entries(path, identifier)
|
entries = scm.entries(path, identifier)
|
||||||
|
load_entries_changesets(entries)
|
||||||
|
entries
|
||||||
end
|
end
|
||||||
|
|
||||||
def branches
|
def branches
|
||||||
@@ -228,7 +252,7 @@ class Repository < ActiveRecord::Base
|
|||||||
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
|
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
|
||||||
:limit => limit)
|
:limit => limit)
|
||||||
else
|
else
|
||||||
changes.find(
|
filechanges.find(
|
||||||
:all,
|
:all,
|
||||||
:include => {:changeset => :user},
|
:include => {:changeset => :user},
|
||||||
:conditions => ["path = ?", path.with_leading_slash],
|
:conditions => ["path = ?", path.with_leading_slash],
|
||||||
@@ -380,6 +404,16 @@ class Repository < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_entries_changesets(entries)
|
||||||
|
if entries
|
||||||
|
entries.each do |entry|
|
||||||
|
if entry.lastrev && entry.lastrev.identifier
|
||||||
|
entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Deletes repository data
|
# Deletes repository data
|
||||||
@@ -393,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 #{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 #{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}")
|
connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
|
||||||
|
clear_extra_info_of_changesets
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_extra_info_of_changesets
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,7 +37,28 @@ class Repository::Bazaar < Repository
|
|||||||
'Bazaar'
|
'Bazaar'
|
||||||
end
|
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)
|
def entries(path=nil, identifier=nil)
|
||||||
|
scm.bzr_path_encodig = log_encoding
|
||||||
entries = scm.entries(path, identifier)
|
entries = scm.entries(path, identifier)
|
||||||
if entries
|
if entries
|
||||||
entries.each do |e|
|
entries.each do |e|
|
||||||
@@ -63,9 +84,12 @@ class Repository::Bazaar < Repository
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
load_entries_changesets(entries)
|
||||||
|
entries
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_changesets
|
def fetch_changesets
|
||||||
|
scm.bzr_path_encodig = log_encoding
|
||||||
scm_info = scm.info
|
scm_info = scm.info
|
||||||
if scm_info
|
if scm_info
|
||||||
# latest revision found in database
|
# latest revision found in database
|
||||||
@@ -78,7 +102,7 @@ class Repository::Bazaar < Repository
|
|||||||
while (identifier_from <= scm_revision)
|
while (identifier_from <= scm_revision)
|
||||||
# loads changesets by batches of 200
|
# loads changesets by batches of 200
|
||||||
identifier_to = [identifier_from + 199, scm_revision].min
|
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
|
transaction do
|
||||||
revisions.reverse_each do |revision|
|
revisions.reverse_each do |revision|
|
||||||
changeset = Changeset.create(:repository => self,
|
changeset = Changeset.create(:repository => self,
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ require 'digest/sha1'
|
|||||||
class Repository::Cvs < Repository
|
class Repository::Cvs < Repository
|
||||||
validates_presence_of :url, :root_url, :log_encoding
|
validates_presence_of :url, :root_url, :log_encoding
|
||||||
|
|
||||||
|
safe_attributes 'root_url',
|
||||||
|
:if => lambda {|repository, user| repository.new_record?}
|
||||||
|
|
||||||
def self.human_attribute_name(attribute_key_name, *args)
|
def self.human_attribute_name(attribute_key_name, *args)
|
||||||
attr_name = attribute_key_name.to_s
|
attr_name = attribute_key_name.to_s
|
||||||
if attr_name == "root_url"
|
if attr_name == "root_url"
|
||||||
@@ -54,7 +57,7 @@ class Repository::Cvs < Repository
|
|||||||
if entries
|
if entries
|
||||||
entries.each() do |entry|
|
entries.each() do |entry|
|
||||||
if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
|
if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
|
||||||
change=changes.find_by_revision_and_path(
|
change = filechanges.find_by_revision_and_path(
|
||||||
entry.lastrev.revision,
|
entry.lastrev.revision,
|
||||||
scm.with_leading_slash(entry.path) )
|
scm.with_leading_slash(entry.path) )
|
||||||
if change
|
if change
|
||||||
@@ -66,6 +69,7 @@ class Repository::Cvs < Repository
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
load_entries_changesets(entries)
|
||||||
entries
|
entries
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -94,7 +98,7 @@ class Repository::Cvs < Repository
|
|||||||
if rev_to.to_i > 0
|
if rev_to.to_i > 0
|
||||||
changeset_to = changesets.find_by_revision(rev_to)
|
changeset_to = changesets.find_by_revision(rev_to)
|
||||||
end
|
end
|
||||||
changeset_from.changes.each() do |change_from|
|
changeset_from.filechanges.each() do |change_from|
|
||||||
revision_from = nil
|
revision_from = nil
|
||||||
revision_to = nil
|
revision_to = nil
|
||||||
if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
|
if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
|
||||||
@@ -102,7 +106,7 @@ class Repository::Cvs < Repository
|
|||||||
end
|
end
|
||||||
if revision_from
|
if revision_from
|
||||||
if changeset_to
|
if changeset_to
|
||||||
changeset_to.changes.each() do |change_to|
|
changeset_to.filechanges.each() do |change_to|
|
||||||
revision_to = change_to.revision if change_to.path == change_from.path
|
revision_to = change_to.revision if change_to.path == change_from.path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -133,7 +137,7 @@ class Repository::Cvs < Repository
|
|||||||
# only add the change to the database, if it doen't exists. the cvs log
|
# only add the change to the database, if it doen't exists. the cvs log
|
||||||
# is not exclusive at all.
|
# is not exclusive at all.
|
||||||
tmp_time = revision.time.clone
|
tmp_time = revision.time.clone
|
||||||
unless changes.find_by_path_and_revision(
|
unless filechanges.find_by_path_and_revision(
|
||||||
scm.with_leading_slash(revision.paths[0][:path]),
|
scm.with_leading_slash(revision.paths[0][:path]),
|
||||||
revision.paths[0][:revision]
|
revision.paths[0][:revision]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class Repository::Darcs < Repository
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
load_entries_changesets(entries)
|
||||||
entries
|
entries
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ class Repository::Darcs < Repository
|
|||||||
return nil if patch_from.nil?
|
return nil if patch_from.nil?
|
||||||
patch_to = changesets.find_by_revision(rev_to) if rev_to
|
patch_to = changesets.find_by_revision(rev_to) if rev_to
|
||||||
if path.blank?
|
if path.blank?
|
||||||
path = patch_from.changes.collect{|change| change.path}.join(' ')
|
path = patch_from.filechanges.collect{|change| change.path}.join(' ')
|
||||||
end
|
end
|
||||||
patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
|
patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ class Repository::Filesystem < Repository
|
|||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def entries(path=nil, identifier=nil)
|
|
||||||
scm.entries(path, identifier)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_changesets
|
def fetch_changesets
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -87,16 +87,16 @@ class Repository::Git < Repository
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_changeset_by_name(name)
|
def find_changeset_by_name(name)
|
||||||
return nil if name.nil? || name.empty?
|
if name.present?
|
||||||
e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
|
changesets.where(:revision => name.to_s).first ||
|
||||||
return e if e
|
changesets.where('scmid LIKE ?', "#{name}%").first
|
||||||
changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def entries(path=nil, identifier=nil)
|
def entries(path=nil, identifier=nil)
|
||||||
scm.entries(path,
|
entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
|
||||||
identifier,
|
load_entries_changesets(entries)
|
||||||
options = {:report_last_commit => extra_report_last_commit})
|
entries
|
||||||
end
|
end
|
||||||
|
|
||||||
# With SCMs that have a sequential commit numbering,
|
# With SCMs that have a sequential commit numbering,
|
||||||
@@ -255,4 +255,15 @@ class Repository::Git < Repository
|
|||||||
:order => 'committed_on DESC'
|
:order => 'committed_on DESC'
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -76,12 +76,12 @@ class Repository::Mercurial < Repository
|
|||||||
return nil if name.blank?
|
return nil if name.blank?
|
||||||
s = name.to_s
|
s = name.to_s
|
||||||
if /[^\d]/ =~ s or s.size > 8
|
if /[^\d]/ =~ s or s.size > 8
|
||||||
e = changesets.find(:first, :conditions => ['scmid = ?', s])
|
cs = changesets.where(:scmid => s).first
|
||||||
else
|
else
|
||||||
e = changesets.find(:first, :conditions => ['revision = ?', s])
|
cs = changesets.where(:revision => s).first
|
||||||
end
|
end
|
||||||
return e if e
|
return cs if cs
|
||||||
changesets.find(:first, :conditions => ['scmid LIKE ?', "#{s}%"]) # last ditch
|
changesets.where('scmid LIKE ?', "#{s}%").first
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the latest changesets for +path+; sorted by revision number
|
# Returns the latest changesets for +path+; sorted by revision number
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ class Repository::Subversion < Repository
|
|||||||
|
|
||||||
def latest_changesets(path, rev, limit=10)
|
def latest_changesets(path, rev, limit=10)
|
||||||
revisions = scm.revisions(path, rev, nil, :limit => limit)
|
revisions = scm.revisions(path, rev, nil, :limit => limit)
|
||||||
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
|
if revisions
|
||||||
|
identifiers = revisions.collect(&:identifier).compact
|
||||||
|
changesets.where(:revision => identifiers).reorder("committed_on DESC").includes(:repository, :user).all
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a path relative to the url of the repository
|
# Returns a path relative to the url of the repository
|
||||||
@@ -81,6 +86,24 @@ class Repository::Subversion < Repository
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def load_entries_changesets(entries)
|
||||||
|
return unless entries
|
||||||
|
|
||||||
|
entries_with_identifier = entries.select {|entry| entry.lastrev && entry.lastrev.identifier.present?}
|
||||||
|
identifiers = entries_with_identifier.map {|entry| entry.lastrev.identifier}.compact.uniq
|
||||||
|
|
||||||
|
if identifiers.any?
|
||||||
|
changesets_by_identifier = changesets.where(:revision => identifiers).includes(:user, :repository).all.group_by(&:revision)
|
||||||
|
entries_with_identifier.each do |entry|
|
||||||
|
if m = changesets_by_identifier[entry.lastrev.identifier]
|
||||||
|
entry.changeset = m.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Returns the relative url of the repository
|
# Returns the relative url of the repository
|
||||||
|
|||||||
+43
-12
@@ -16,6 +16,19 @@
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
class Role < ActiveRecord::Base
|
class Role < ActiveRecord::Base
|
||||||
|
# Custom coder for the permissions attribute that should be an
|
||||||
|
# array of symbols. Rails 3 uses Psych which can be *unbelievably*
|
||||||
|
# slow on some platforms (eg. mingw32).
|
||||||
|
class PermissionsAttributeCoder
|
||||||
|
def self.load(str)
|
||||||
|
str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.dump(value)
|
||||||
|
YAML.dump(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Built-in roles
|
# Built-in roles
|
||||||
BUILTIN_NON_MEMBER = 1
|
BUILTIN_NON_MEMBER = 1
|
||||||
BUILTIN_ANONYMOUS = 2
|
BUILTIN_ANONYMOUS = 2
|
||||||
@@ -26,17 +39,17 @@ class Role < ActiveRecord::Base
|
|||||||
['own', :label_issues_visibility_own]
|
['own', :label_issues_visibility_own]
|
||||||
]
|
]
|
||||||
|
|
||||||
scope :sorted, {:order => 'builtin, position'}
|
scope :sorted, order("#{table_name}.builtin ASC, #{table_name}.position ASC")
|
||||||
scope :givable, { :conditions => "builtin = 0", :order => 'position' }
|
scope :givable, order("#{table_name}.position ASC").where(:builtin => 0)
|
||||||
scope :builtin, lambda { |*args|
|
scope :builtin, lambda { |*args|
|
||||||
compare = 'not' if args.first == true
|
compare = (args.first == true ? 'not' : '')
|
||||||
{ :conditions => "#{compare} builtin = 0" }
|
where("#{compare} builtin = 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
before_destroy :check_deletable
|
before_destroy :check_deletable
|
||||||
has_many :workflows, :dependent => :delete_all do
|
has_many :workflow_rules, :dependent => :delete_all do
|
||||||
def copy(source_role)
|
def copy(source_role)
|
||||||
Workflow.copy(nil, source_role, nil, proxy_association.owner)
|
WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -44,7 +57,7 @@ class Role < ActiveRecord::Base
|
|||||||
has_many :members, :through => :member_roles
|
has_many :members, :through => :member_roles
|
||||||
acts_as_list
|
acts_as_list
|
||||||
|
|
||||||
serialize :permissions, Array
|
serialize :permissions, ::Role::PermissionsAttributeCoder
|
||||||
attr_protected :builtin
|
attr_protected :builtin
|
||||||
|
|
||||||
validates_presence_of :name
|
validates_presence_of :name
|
||||||
@@ -54,8 +67,13 @@ class Role < ActiveRecord::Base
|
|||||||
:in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
|
:in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
|
||||||
:if => lambda {|role| role.respond_to?(:issues_visibility)}
|
:if => lambda {|role| role.respond_to?(:issues_visibility)}
|
||||||
|
|
||||||
def permissions
|
# Copies attributes from another role, arg can be an id or a Role
|
||||||
read_attribute(:permissions) || []
|
def copy_from(arg, options={})
|
||||||
|
return unless arg.present?
|
||||||
|
role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
|
||||||
|
self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
|
||||||
|
self.permissions = role.permissions.dup
|
||||||
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def permissions=(perms)
|
def permissions=(perms)
|
||||||
@@ -87,7 +105,15 @@ class Role < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def <=>(role)
|
def <=>(role)
|
||||||
role ? position <=> role.position : -1
|
if role
|
||||||
|
if builtin == role.builtin
|
||||||
|
position <=> role.position
|
||||||
|
else
|
||||||
|
builtin <=> role.builtin
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
@@ -107,6 +133,11 @@ class Role < ActiveRecord::Base
|
|||||||
self.builtin != 0
|
self.builtin != 0
|
||||||
end
|
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
|
# Return true if the role is a project member role
|
||||||
def member?
|
def member?
|
||||||
!self.builtin?
|
!self.builtin?
|
||||||
@@ -134,7 +165,7 @@ class Role < ActiveRecord::Base
|
|||||||
|
|
||||||
# Find all the roles that can be given to a project member
|
# Find all the roles that can be given to a project member
|
||||||
def self.find_all_givable
|
def self.find_all_givable
|
||||||
find(:all, :conditions => {:builtin => 0}, :order => 'position')
|
Role.givable.all
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return the builtin 'non member' role. If the role doesn't exist,
|
# Return the builtin 'non member' role. If the role doesn't exist,
|
||||||
@@ -165,7 +196,7 @@ private
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.find_or_create_system_role(builtin, name)
|
def self.find_or_create_system_role(builtin, name)
|
||||||
role = first(:conditions => {:builtin => builtin})
|
role = where(:builtin => builtin).first
|
||||||
if role.nil?
|
if role.nil?
|
||||||
role = create(:name => name, :position => 0) do |r|
|
role = create(:name => name, :position => 0) do |r|
|
||||||
r.builtin = builtin
|
r.builtin = builtin
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class TimeEntry < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values'
|
safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields'
|
||||||
|
|
||||||
def initialize(attributes=nil, *args)
|
def initialize(attributes=nil, *args)
|
||||||
super
|
super
|
||||||
|
|||||||
+57
-10
@@ -16,11 +16,18 @@
|
|||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
class Tracker < ActiveRecord::Base
|
class Tracker < ActiveRecord::Base
|
||||||
|
|
||||||
|
CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze
|
||||||
|
# Fields that can be disabled
|
||||||
|
# Other (future) fields should be appended, not inserted!
|
||||||
|
CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze
|
||||||
|
CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze
|
||||||
|
|
||||||
before_destroy :check_integrity
|
before_destroy :check_integrity
|
||||||
has_many :issues
|
has_many :issues
|
||||||
has_many :workflows, :dependent => :delete_all do
|
has_many :workflow_rules, :dependent => :delete_all do
|
||||||
def copy(source_tracker)
|
def copy(source_tracker)
|
||||||
Workflow.copy(source_tracker, nil, proxy_association.owner, nil)
|
WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,11 +35,14 @@ class Tracker < ActiveRecord::Base
|
|||||||
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
|
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
|
||||||
acts_as_list
|
acts_as_list
|
||||||
|
|
||||||
|
attr_protected :field_bits
|
||||||
|
|
||||||
validates_presence_of :name
|
validates_presence_of :name
|
||||||
validates_uniqueness_of :name
|
validates_uniqueness_of :name
|
||||||
validates_length_of :name, :maximum => 30
|
validates_length_of :name, :maximum => 30
|
||||||
|
|
||||||
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
|
scope :sorted, order("#{table_name}.position ASC")
|
||||||
|
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||||
|
|
||||||
def to_s; name end
|
def to_s; name end
|
||||||
|
|
||||||
@@ -40,10 +50,6 @@ class Tracker < ActiveRecord::Base
|
|||||||
position <=> tracker.position
|
position <=> tracker.position
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.all
|
|
||||||
find(:all, :order => 'position')
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns an array of IssueStatus that are used
|
# Returns an array of IssueStatus that are used
|
||||||
# in the tracker's workflows
|
# in the tracker's workflows
|
||||||
def issue_statuses
|
def issue_statuses
|
||||||
@@ -53,16 +59,57 @@ class Tracker < ActiveRecord::Base
|
|||||||
return []
|
return []
|
||||||
end
|
end
|
||||||
|
|
||||||
ids = Workflow.
|
ids = WorkflowTransition.
|
||||||
connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
|
connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'").
|
||||||
flatten.
|
flatten.
|
||||||
uniq
|
uniq
|
||||||
|
|
||||||
@issue_statuses = IssueStatus.find_all_by_id(ids).sort
|
@issue_statuses = IssueStatus.find_all_by_id(ids).sort
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def disabled_core_fields
|
||||||
|
i = -1
|
||||||
|
@disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
def core_fields
|
||||||
|
CORE_FIELDS - disabled_core_fields
|
||||||
|
end
|
||||||
|
|
||||||
|
def core_fields=(fields)
|
||||||
|
raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array)
|
||||||
|
|
||||||
|
bits = 0
|
||||||
|
CORE_FIELDS.each_with_index do |field, i|
|
||||||
|
unless fields.include?(field)
|
||||||
|
bits |= 2 ** i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.fields_bits = bits
|
||||||
|
@disabled_core_fields = nil
|
||||||
|
core_fields
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the fields that are disabled for all the given trackers
|
||||||
|
def self.disabled_core_fields(trackers)
|
||||||
|
if trackers.present?
|
||||||
|
trackers.uniq.map(&:disabled_core_fields).reduce(:&)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the fields that are enabled for one tracker at least
|
||||||
|
def self.core_fields(trackers)
|
||||||
|
if trackers.present?
|
||||||
|
trackers.uniq.map(&:core_fields).reduce(:|)
|
||||||
|
else
|
||||||
|
CORE_FIELDS.dup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def check_integrity
|
def check_integrity
|
||||||
raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
|
raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+90
-35
@@ -28,11 +28,41 @@ class User < Principal
|
|||||||
|
|
||||||
# Different ways of displaying/sorting users
|
# Different ways of displaying/sorting users
|
||||||
USER_FORMATS = {
|
USER_FORMATS = {
|
||||||
:firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
|
:firstname_lastname => {
|
||||||
:firstname => {:string => '#{firstname}', :order => %w(firstname id)},
|
:string => '#{firstname} #{lastname}',
|
||||||
:lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
|
:order => %w(firstname lastname id),
|
||||||
:lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
|
:setting_order => 1
|
||||||
:username => {:string => '#{login}', :order => %w(login id)},
|
},
|
||||||
|
: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 = [
|
MAIL_NOTIFICATION_OPTIONS = [
|
||||||
@@ -52,8 +82,6 @@ class User < Principal
|
|||||||
has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
|
has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
|
||||||
belongs_to :auth_source
|
belongs_to :auth_source
|
||||||
|
|
||||||
# Active non-anonymous users scope
|
|
||||||
scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
|
|
||||||
scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
|
scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
|
||||||
scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
|
scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
|
||||||
|
|
||||||
@@ -69,7 +97,7 @@ class User < Principal
|
|||||||
|
|
||||||
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
|
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 :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
|
# Login must contain lettres, numbers, underscores only
|
||||||
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
|
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
|
||||||
validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
|
validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
|
||||||
@@ -86,11 +114,11 @@ class User < Principal
|
|||||||
|
|
||||||
scope :in_group, lambda {|group|
|
scope :in_group, lambda {|group|
|
||||||
group_id = group.is_a?(Group) ? group.id : group.to_i
|
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|
|
scope :not_in_group, lambda {|group|
|
||||||
group_id = group.is_a?(Group) ? group.id : group.to_i
|
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
|
def set_mail_notification
|
||||||
@@ -130,8 +158,11 @@ class User < Principal
|
|||||||
|
|
||||||
# Returns the user that matches provided login and password, or nil
|
# Returns the user that matches provided login and password, or nil
|
||||||
def self.try_to_login(login, password)
|
def self.try_to_login(login, password)
|
||||||
|
login = login.to_s
|
||||||
|
password = password.to_s
|
||||||
|
|
||||||
# Make sure no one can sign in with an empty password
|
# Make sure no one can sign in with an empty password
|
||||||
return nil if password.to_s.empty?
|
return nil if password.empty?
|
||||||
user = find_by_login(login)
|
user = find_by_login(login)
|
||||||
if user
|
if user
|
||||||
# user is already in local database
|
# user is already in local database
|
||||||
@@ -164,7 +195,7 @@ class User < Principal
|
|||||||
|
|
||||||
# Returns the user who matches the given autologin +key+ or nil
|
# Returns the user who matches the given autologin +key+ or nil
|
||||||
def self.try_to_autologin(key)
|
def self.try_to_autologin(key)
|
||||||
tokens = Token.find_all_by_action_and_value('autologin', key)
|
tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
|
||||||
# Make sure there's only 1 token that matches the key
|
# Make sure there's only 1 token that matches the key
|
||||||
if tokens.size == 1
|
if tokens.size == 1
|
||||||
token = tokens.first
|
token = tokens.first
|
||||||
@@ -329,27 +360,27 @@ class User < Principal
|
|||||||
# version. Exact matches will be given priority.
|
# version. Exact matches will be given priority.
|
||||||
def self.find_by_login(login)
|
def self.find_by_login(login)
|
||||||
# First look for an exact match
|
# 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
|
unless user
|
||||||
# Fail over to case-insensitive if none was found
|
# 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
|
end
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_by_rss_key(key)
|
def self.find_by_rss_key(key)
|
||||||
token = Token.find_by_value(key)
|
token = Token.find_by_action_and_value('feeds', key.to_s)
|
||||||
token && token.user.active? ? token.user : nil
|
token && token.user.active? ? token.user : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_by_api_key(key)
|
def self.find_by_api_key(key)
|
||||||
token = Token.find_by_action_and_value('api', key)
|
token = Token.find_by_action_and_value('api', key.to_s)
|
||||||
token && token.user.active? ? token.user : nil
|
token && token.user.active? ? token.user : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Makes find_by_mail case-insensitive
|
# Makes find_by_mail case-insensitive
|
||||||
def self.find_by_mail(mail)
|
def self.find_by_mail(mail)
|
||||||
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
|
where("LOWER(mail) = ?", mail.to_s.downcase).first
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if the default admin account can no longer be used
|
# Returns true if the default admin account can no longer be used
|
||||||
@@ -361,6 +392,17 @@ class User < Principal
|
|||||||
name
|
name
|
||||||
end
|
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
|
# Returns the current day according to user's time zone
|
||||||
def today
|
def today
|
||||||
if time_zone.nil?
|
if time_zone.nil?
|
||||||
@@ -370,6 +412,15 @@ class User < Principal
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns the day of +time+ according to user's time zone
|
||||||
|
def time_to_date(time)
|
||||||
|
if time_zone.nil?
|
||||||
|
time.to_date
|
||||||
|
else
|
||||||
|
time.in_time_zone(time_zone).to_date
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def logged?
|
def logged?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
@@ -382,7 +433,7 @@ class User < Principal
|
|||||||
def roles_for_project(project)
|
def roles_for_project(project)
|
||||||
roles = []
|
roles = []
|
||||||
# No role on archived projects
|
# No role on archived projects
|
||||||
return roles unless project && project.active?
|
return roles if project.nil? || project.archived?
|
||||||
if logged?
|
if logged?
|
||||||
# Find project membership
|
# Find project membership
|
||||||
membership = memberships.detect {|m| m.project_id == project.id}
|
membership = memberships.detect {|m| m.project_id == project.id}
|
||||||
@@ -408,10 +459,13 @@ class User < Principal
|
|||||||
def projects_by_role
|
def projects_by_role
|
||||||
return @projects_by_role if @projects_by_role
|
return @projects_by_role if @projects_by_role
|
||||||
|
|
||||||
@projects_by_role = Hash.new {|h,k| h[k]=[]}
|
@projects_by_role = Hash.new([])
|
||||||
memberships.each do |membership|
|
memberships.each do |membership|
|
||||||
membership.roles.each do |role|
|
if membership.project
|
||||||
@projects_by_role[role] << membership.project if membership.project
|
membership.roles.each do |role|
|
||||||
|
@projects_by_role[role] = [] unless @projects_by_role.key?(role)
|
||||||
|
@projects_by_role[role] << membership.project
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@projects_by_role.each do |role, projects|
|
@projects_by_role.each do |role, projects|
|
||||||
@@ -443,26 +497,23 @@ class User < Principal
|
|||||||
# or falls back to Non Member / Anonymous permissions depending if the user is logged
|
# or falls back to Non Member / Anonymous permissions depending if the user is logged
|
||||||
def allowed_to?(action, context, options={}, &block)
|
def allowed_to?(action, context, options={}, &block)
|
||||||
if context && context.is_a?(Project)
|
if context && context.is_a?(Project)
|
||||||
# No action allowed on archived projects
|
|
||||||
return false unless context.active?
|
|
||||||
# No action allowed on disabled modules
|
|
||||||
return false unless context.allows_to?(action)
|
return false unless context.allows_to?(action)
|
||||||
# Admin users are authorized for anything else
|
# Admin users are authorized for anything else
|
||||||
return true if admin?
|
return true if admin?
|
||||||
|
|
||||||
roles = roles_for_project(context)
|
roles = roles_for_project(context)
|
||||||
return false unless roles
|
return false unless roles
|
||||||
roles.detect {|role|
|
roles.any? {|role|
|
||||||
(context.is_public? || role.member?) &&
|
(context.is_public? || role.member?) &&
|
||||||
role.allowed_to?(action) &&
|
role.allowed_to?(action) &&
|
||||||
(block_given? ? yield(role, self) : true)
|
(block_given? ? yield(role, self) : true)
|
||||||
}
|
}
|
||||||
elsif context && context.is_a?(Array)
|
elsif context && context.is_a?(Array)
|
||||||
# Authorize if user is authorized on every element of the array
|
if context.empty?
|
||||||
context.map do |project|
|
false
|
||||||
allowed_to?(action, project, options, &block)
|
else
|
||||||
end.inject do |memo,allowed|
|
# Authorize if user is authorized on every element of the array
|
||||||
memo && allowed
|
context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
|
||||||
end
|
end
|
||||||
elsif options[:global]
|
elsif options[:global]
|
||||||
# Admin users are always authorized
|
# Admin users are always authorized
|
||||||
@@ -471,7 +522,7 @@ class User < Principal
|
|||||||
# authorize if user has at least one role that has this permission
|
# authorize if user has at least one role that has this permission
|
||||||
roles = memberships.collect {|m| m.roles}.flatten.uniq
|
roles = memberships.collect {|m| m.roles}.flatten.uniq
|
||||||
roles << (self.logged? ? Role.non_member : Role.anonymous)
|
roles << (self.logged? ? Role.non_member : Role.anonymous)
|
||||||
roles.detect {|role|
|
roles.any? {|role|
|
||||||
role.allowed_to?(action) &&
|
role.allowed_to?(action) &&
|
||||||
(block_given? ? yield(role, self) : true)
|
(block_given? ? yield(role, self) : true)
|
||||||
}
|
}
|
||||||
@@ -489,7 +540,7 @@ class User < Principal
|
|||||||
# Returns true if the user is allowed to delete his own account
|
# Returns true if the user is allowed to delete his own account
|
||||||
def own_account_deletable?
|
def own_account_deletable?
|
||||||
Setting.unsubscribe? &&
|
Setting.unsubscribe? &&
|
||||||
(!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
|
(!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
|
||||||
end
|
end
|
||||||
|
|
||||||
safe_attributes 'login',
|
safe_attributes 'login',
|
||||||
@@ -560,7 +611,7 @@ class User < Principal
|
|||||||
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
|
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
|
||||||
# one anonymous user per database.
|
# one anonymous user per database.
|
||||||
def self.anonymous
|
def self.anonymous
|
||||||
anonymous_user = AnonymousUser.find(:first)
|
anonymous_user = AnonymousUser.first
|
||||||
if anonymous_user.nil?
|
if anonymous_user.nil?
|
||||||
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
|
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
|
||||||
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
|
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
|
||||||
@@ -573,11 +624,11 @@ class User < Principal
|
|||||||
# This method is used in the SaltPasswords migration and is to be kept as is
|
# This method is used in the SaltPasswords migration and is to be kept as is
|
||||||
def self.salt_unsalted_passwords!
|
def self.salt_unsalted_passwords!
|
||||||
transaction do
|
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?
|
next if user.hashed_password.blank?
|
||||||
salt = User.generate_salt
|
salt = User.generate_salt
|
||||||
hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
|
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
|
end
|
||||||
end
|
end
|
||||||
@@ -650,6 +701,10 @@ class AnonymousUser < User
|
|||||||
def time_zone; nil end
|
def time_zone; nil end
|
||||||
def rss_key; nil end
|
def rss_key; nil end
|
||||||
|
|
||||||
|
def pref
|
||||||
|
UserPreference.new(:user => self)
|
||||||
|
end
|
||||||
|
|
||||||
# Anonymous user can not be destroyed
|
# Anonymous user can not be destroyed
|
||||||
def destroy
|
def destroy
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class UserPreference < ActiveRecord::Base
|
|||||||
if attribute_present? attr_name
|
if attribute_present? attr_name
|
||||||
super
|
super
|
||||||
else
|
else
|
||||||
h = read_attribute(:others).dup || {}
|
h = (read_attribute(:others) || {}).dup
|
||||||
h.update(attr_name => value)
|
h.update(attr_name => value)
|
||||||
write_attribute(:others, h)
|
write_attribute(:others, h)
|
||||||
value
|
value
|
||||||
|
|||||||
+26
-13
@@ -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_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 :status, :in => VERSION_STATUSES
|
||||||
validates_inclusion_of :sharing, :in => VERSION_SHARINGS
|
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 :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||||
scope :open, :conditions => {:status => 'open'}
|
scope :open, where(:status => 'open')
|
||||||
scope :visible, lambda {|*args| { :include => :project,
|
scope :visible, lambda {|*args|
|
||||||
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
|
includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues))
|
||||||
|
}
|
||||||
|
|
||||||
safe_attributes 'name',
|
safe_attributes 'name',
|
||||||
'description',
|
'description',
|
||||||
@@ -78,7 +80,7 @@ class Version < ActiveRecord::Base
|
|||||||
|
|
||||||
# Returns the total reported time for this version
|
# Returns the total reported time for this version
|
||||||
def spent_hours
|
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
|
end
|
||||||
|
|
||||||
def closed?
|
def closed?
|
||||||
@@ -91,7 +93,7 @@ class Version < ActiveRecord::Base
|
|||||||
|
|
||||||
# Returns true if the version is completed: due date reached and no open issues
|
# Returns true if the version is completed: due date reached and no open issues
|
||||||
def completed?
|
def completed?
|
||||||
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
|
effective_date && (effective_date < Date.today) && (open_issues_count == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
def behind_schedule?
|
def behind_schedule?
|
||||||
@@ -162,13 +164,13 @@ class Version < ActiveRecord::Base
|
|||||||
"#{project} - #{name}"
|
"#{project} - #{name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Versions are sorted by effective_date and "Project Name - Version name"
|
# Versions are sorted by effective_date and name
|
||||||
# Those with no effective_date are at the end, sorted by "Project Name - Version name"
|
# Those with no effective_date are at the end, sorted by name
|
||||||
def <=>(version)
|
def <=>(version)
|
||||||
if self.effective_date
|
if self.effective_date
|
||||||
if version.effective_date
|
if version.effective_date
|
||||||
if self.effective_date == version.effective_date
|
if self.effective_date == version.effective_date
|
||||||
"#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
|
name == version.name ? id <=> version.id : name <=> version.name
|
||||||
else
|
else
|
||||||
self.effective_date <=> version.effective_date
|
self.effective_date <=> version.effective_date
|
||||||
end
|
end
|
||||||
@@ -179,11 +181,18 @@ class Version < ActiveRecord::Base
|
|||||||
if version.effective_date
|
if version.effective_date
|
||||||
1
|
1
|
||||||
else
|
else
|
||||||
"#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
|
name == version.name ? id <=> version.id : name <=> version.name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.fields_for_order_statement(table=nil)
|
||||||
|
table ||= table_name
|
||||||
|
["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
|
||||||
|
end
|
||||||
|
|
||||||
|
scope :sorted, order(fields_for_order_statement)
|
||||||
|
|
||||||
# Returns the sharings that +user+ can set the version to
|
# Returns the sharings that +user+ can set the version to
|
||||||
def allowed_sharings(user = User.current)
|
def allowed_sharings(user = User.current)
|
||||||
VERSION_SHARINGS.select do |s|
|
VERSION_SHARINGS.select do |s|
|
||||||
@@ -260,12 +269,16 @@ class Version < ActiveRecord::Base
|
|||||||
if issues_count > 0
|
if issues_count > 0
|
||||||
ratio = open ? 'done_ratio' : 100
|
ratio = open ? 'done_ratio' : 100
|
||||||
|
|
||||||
done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
|
done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
|
||||||
:joins => :status,
|
|
||||||
:conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
|
|
||||||
progress = done / (estimated_average * issues_count)
|
progress = done / (estimated_average * issues_count)
|
||||||
end
|
end
|
||||||
progress
|
progress
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_version
|
||||||
|
if effective_date.nil? && @attributes['effective_date'].present?
|
||||||
|
errors.add :effective_date, :not_a_date
|
||||||
|
end
|
||||||
|
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 #{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"}
|
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
|
||||||
|
|
||||||
|
after_destroy :page_update_after_destroy
|
||||||
|
|
||||||
def text=(plain)
|
def text=(plain)
|
||||||
case Setting.wiki_compression
|
case Setting.wiki_compression
|
||||||
when 'gzip'
|
when 'gzip'
|
||||||
@@ -115,10 +117,31 @@ class WikiContent < ActiveRecord::Base
|
|||||||
|
|
||||||
# Returns the previous version or nil
|
# Returns the previous version or nil
|
||||||
def previous
|
def previous
|
||||||
@previous ||= WikiContent::Version.find(:first,
|
@previous ||= WikiContent::Version.
|
||||||
:order => 'version DESC',
|
reorder('version DESC').
|
||||||
:include => :author,
|
includes(:author).
|
||||||
:conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+24
-7
@@ -50,14 +50,14 @@ class WikiPage < ActiveRecord::Base
|
|||||||
|
|
||||||
# eager load information about last updates, without loading text
|
# eager load information about last updates, without loading text
|
||||||
scope :with_updated_on, {
|
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"
|
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Wiki pages that are protected by default
|
# Wiki pages that are protected by default
|
||||||
DEFAULT_PROTECTED_PAGES = %w(sidebar)
|
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)}
|
:if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
|
||||||
|
|
||||||
def initialize(attributes=nil, *args)
|
def initialize(attributes=nil, *args)
|
||||||
@@ -111,11 +111,13 @@ class WikiPage < ActiveRecord::Base
|
|||||||
|
|
||||||
def diff(version_to=nil, version_from=nil)
|
def diff(version_to=nil, version_from=nil)
|
||||||
version_to = version_to ? version_to.to_i : self.content.version
|
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_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
|
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
|
||||||
end
|
end
|
||||||
@@ -143,7 +145,7 @@ class WikiPage < ActiveRecord::Base
|
|||||||
if time = read_attribute(:updated_on)
|
if time = read_attribute(:updated_on)
|
||||||
# content updated_on was eager loaded with the page
|
# content updated_on was eager loaded with the page
|
||||||
begin
|
begin
|
||||||
@updated_on = Time.zone ? Time.zone.parse(time.to_s) : Time.parse(time.to_s)
|
@updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
|
||||||
rescue
|
rescue
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -172,6 +174,21 @@ class WikiPage < ActiveRecord::Base
|
|||||||
self.parent = parent_page
|
self.parent = parent_page
|
||||||
end
|
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
|
protected
|
||||||
|
|
||||||
def validate_parent_title
|
def validate_parent_title
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Redmine - project management software
|
||||||
|
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
class WorkflowPermission < WorkflowRule
|
||||||
|
validates_inclusion_of :rule, :in => %w(readonly required)
|
||||||
|
validate :validate_field_name
|
||||||
|
|
||||||
|
# Replaces the workflow permissions for the given tracker and role
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}}
|
||||||
|
def self.replace_permissions(tracker, role, permissions)
|
||||||
|
destroy_all(:tracker_id => tracker.id, :role_id => role.id)
|
||||||
|
|
||||||
|
permissions.each { |field, rule_by_status_id|
|
||||||
|
rule_by_status_id.each { |status_id, rule|
|
||||||
|
if rule.present?
|
||||||
|
WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def validate_field_name
|
||||||
|
unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/)
|
||||||
|
errors.add :field_name, :invalid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,31 +15,15 @@
|
|||||||
# along with this program; if not, write to the Free Software
|
# along with this program; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
class Workflow < ActiveRecord::Base
|
class WorkflowRule < ActiveRecord::Base
|
||||||
|
self.table_name = "#{table_name_prefix}workflows#{table_name_suffix}"
|
||||||
|
|
||||||
belongs_to :role
|
belongs_to :role
|
||||||
|
belongs_to :tracker
|
||||||
belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id'
|
belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id'
|
||||||
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
|
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
|
||||||
|
|
||||||
validates_presence_of :role, :old_status, :new_status
|
validates_presence_of :role, :tracker, :old_status
|
||||||
|
|
||||||
# Returns workflow transitions count by tracker and role
|
|
||||||
def self.count_by_tracker_and_role
|
|
||||||
counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id")
|
|
||||||
roles = Role.find(:all, :order => 'builtin, position')
|
|
||||||
trackers = Tracker.find(:all, :order => 'position')
|
|
||||||
|
|
||||||
result = []
|
|
||||||
trackers.each do |tracker|
|
|
||||||
t = []
|
|
||||||
roles.each do |role|
|
|
||||||
row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s}
|
|
||||||
t << [role, (row.nil? ? 0 : row['c'].to_i)]
|
|
||||||
end
|
|
||||||
result << [tracker, t]
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
# Copies workflows from source to targets
|
# Copies workflows from source to targets
|
||||||
def self.copy(source_tracker, source_role, target_trackers, target_roles)
|
def self.copy(source_tracker, source_role, target_trackers, target_roles)
|
||||||
@@ -50,7 +34,7 @@ class Workflow < ActiveRecord::Base
|
|||||||
target_trackers = [target_trackers].flatten.compact
|
target_trackers = [target_trackers].flatten.compact
|
||||||
target_roles = [target_roles].flatten.compact
|
target_roles = [target_roles].flatten.compact
|
||||||
|
|
||||||
target_trackers = Tracker.all if target_trackers.empty?
|
target_trackers = Tracker.sorted.all if target_trackers.empty?
|
||||||
target_roles = Role.all if target_roles.empty?
|
target_roles = Role.all if target_roles.empty?
|
||||||
|
|
||||||
target_trackers.each do |target_tracker|
|
target_trackers.each do |target_tracker|
|
||||||
@@ -78,9 +62,9 @@ class Workflow < ActiveRecord::Base
|
|||||||
else
|
else
|
||||||
transaction do
|
transaction do
|
||||||
delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
|
delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
|
||||||
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee)" +
|
connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, rule, type)" +
|
||||||
" SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee" +
|
" SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, rule, type" +
|
||||||
" FROM #{Workflow.table_name}" +
|
" FROM #{WorkflowRule.table_name}" +
|
||||||
" WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
|
" WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
|
||||||
end
|
end
|
||||||
true
|
true
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Redmine - project management software
|
||||||
|
# Copyright (C) 2006-2012 Jean-Philippe Lang
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
class WorkflowTransition < WorkflowRule
|
||||||
|
validates_presence_of :new_status
|
||||||
|
|
||||||
|
# Returns workflow transitions count by tracker and role
|
||||||
|
def self.count_by_tracker_and_role
|
||||||
|
counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id")
|
||||||
|
roles = Role.sorted.all
|
||||||
|
trackers = Tracker.sorted.all
|
||||||
|
|
||||||
|
result = []
|
||||||
|
trackers.each do |tracker|
|
||||||
|
t = []
|
||||||
|
roles.each do |role|
|
||||||
|
row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s}
|
||||||
|
t << [role, (row.nil? ? 0 : row['c'].to_i)]
|
||||||
|
end
|
||||||
|
result << [tracker, t]
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<%= call_hook :view_account_login_top %>
|
<%= call_hook :view_account_login_top %>
|
||||||
<div id="login-form">
|
<div id="login-form">
|
||||||
<%= form_tag({:action=> "login"}) do %>
|
<%= form_tag(signin_path) do %>
|
||||||
<%= back_url_hidden_field_tag %>
|
<%= back_url_hidden_field_tag %>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
|
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td align="left">
|
<td align="left">
|
||||||
<% if Setting.lost_password? %>
|
<% if Setting.lost_password? %>
|
||||||
<%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %>
|
<%= link_to l(:label_password_lost), lost_password_path %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td align="right">
|
<td align="right">
|
||||||
@@ -36,7 +36,12 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<%= javascript_tag "Form.Element.focus('username');" %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= call_hook :view_account_login_bottom %>
|
<%= call_hook :view_account_login_bottom %>
|
||||||
|
|
||||||
|
<% if params[:username].present? %>
|
||||||
|
<%= javascript_tag "$('#password').focus();" %>
|
||||||
|
<% else %>
|
||||||
|
<%= javascript_tag "$('#username').focus();" %>
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<h2><%=l(:label_password_lost)%></h2>
|
<h2><%=l(:label_password_lost)%></h2>
|
||||||
|
|
||||||
<div class="box">
|
<%= form_tag(lost_password_path) do %>
|
||||||
<%= form_tag({:action=> "lost_password"}, :class => "tabular") do %>
|
<div class="box tabular">
|
||||||
|
<p>
|
||||||
<p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
|
<label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
|
||||||
<%= text_field_tag 'mail', nil, :size => 40 %>
|
<%= text_field_tag 'mail', nil, :size => 40 %>
|
||||||
<%= submit_tag l(:button_submit) %></p>
|
<%= submit_tag l(:button_submit) %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -2,14 +2,19 @@
|
|||||||
|
|
||||||
<%= error_messages_for 'user' %>
|
<%= error_messages_for 'user' %>
|
||||||
|
|
||||||
<%= form_tag({:token => @token.value}) do %>
|
<%= form_tag(lost_password_path) do %>
|
||||||
<div class="box tabular">
|
<%= hidden_field_tag 'token', @token.value %>
|
||||||
<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
|
<div class="box tabular">
|
||||||
<%= password_field_tag 'new_password', nil, :size => 25 %>
|
<p>
|
||||||
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em></p>
|
<label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
|
||||||
|
<%= password_field_tag 'new_password', nil, :size => 25 %>
|
||||||
|
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
|
<p>
|
||||||
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
|
<label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
|
||||||
</div>
|
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>
|
||||||
<p><%= submit_tag l(:button_save) %></p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<p><%= submit_tag l(:button_save) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
|
<h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
|
||||||
|
|
||||||
<%= labelled_form_for @user, :url => {:action => 'register'} do |f| %>
|
<%= labelled_form_for @user, :url => register_path do |f| %>
|
||||||
<%= error_messages_for 'user' %>
|
<%= error_messages_for 'user' %>
|
||||||
|
|
||||||
<div class="box tabular">
|
<div class="box tabular">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
|
<h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)).html_safe %></h2>
|
||||||
<p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
|
<p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
|
||||||
|
|
||||||
<div id="activity">
|
<div id="activity">
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
<div id="admin-menu">
|
<div id="admin-menu">
|
||||||
<ul>
|
<%= render_menu :admin_menu %>
|
||||||
<%= render_menu :admin_menu %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,12 +27,12 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% project_tree(@projects) do |project, level| %>
|
<% project_tree(@projects) do |project, level| %>
|
||||||
<tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
|
<tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
|
||||||
<td class="name"><span><%= link_to_project(project, {:action => 'settings'}, :title => project.short_description) %></span></td>
|
<td class="name"><span><%= link_to_project(project, {:action => (project.active? ? 'settings' : 'show')}, :title => project.short_description) %></span></td>
|
||||||
<td align="center"><%= checked_image project.is_public? %></td>
|
<td align="center"><%= checked_image project.is_public? %></td>
|
||||||
<td align="center"><%= format_date(project.created_on) %></td>
|
<td align="center"><%= format_date(project.created_on) %></td>
|
||||||
<td class="buttons">
|
<td class="buttons">
|
||||||
<%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
|
<%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %>
|
||||||
<%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
|
<%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %>
|
||||||
<%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
|
<%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
|
||||||
<%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
|
<%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user