Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
918ec276f1 | ||
|
|
ba083225b7 | ||
|
|
7f22cc6113 | ||
|
|
e162f87964 | ||
|
|
711982b7b3 | ||
|
|
3578cf4d9a | ||
|
|
ec5dbdb0bd | ||
|
|
41ade07171 | ||
|
|
3109ed9320 | ||
|
|
16b94e126b | ||
|
|
0552655ff5 | ||
|
|
b48fcdaa97 | ||
|
|
99bf8c95ab | ||
|
|
13e381d31d | ||
|
|
4eea64d38f | ||
|
|
3172359f97 | ||
|
|
758cc2f2e6 | ||
|
|
699317e12e | ||
|
|
5f747faa58 | ||
|
|
94e7df78ca | ||
|
|
58c69b8123 | ||
|
|
7bea176cdb | ||
|
|
60d6e16978 | ||
|
|
b071a937b3 | ||
|
|
176ce78574 | ||
|
|
c6c0491ad8 | ||
|
|
b6cb7aa8e3 | ||
|
|
b8aa4da28a | ||
|
|
461342dd2e | ||
|
|
e7bc6364e1 | ||
|
|
53d749eb79 | ||
|
|
a9892b5f0e | ||
|
|
cfc65d9397 | ||
|
|
1b63553dd4 | ||
|
|
3f566d6042 | ||
|
|
7b1b605ae8 | ||
|
|
0444ecca3c | ||
|
|
a77b462a53 | ||
|
|
b59d109680 | ||
|
|
6ed3730c46 | ||
|
|
0c0b2f817f | ||
|
|
f399b31eff | ||
|
|
e35640f3e8 | ||
|
|
285baaff94 | ||
|
|
38e8703ed4 | ||
|
|
d8ddb2cee4 | ||
|
|
31ccc32016 | ||
|
|
77c497daa0 | ||
|
|
cbe75f3b9f | ||
|
|
9b00bba5b5 | ||
|
|
166d67bf87 | ||
|
|
c72baca17c | ||
|
|
b6f2b982ab | ||
|
|
13f6cd6b71 | ||
|
|
7c2f62c798 | ||
|
|
3e90b6bcbf | ||
|
|
5259da5e80 | ||
|
|
3bc1dd573a | ||
|
|
c136e6bc60 | ||
|
|
a1addba26c | ||
|
|
481a1311b7 | ||
|
|
2012e635b0 | ||
|
|
30eaf86f29 | ||
|
|
cfc05d310e | ||
|
|
2c97f9ecde | ||
|
|
886284b33f | ||
|
|
4a36e09d49 | ||
|
|
5e2eedf2c5 | ||
|
|
62b66e9b38 | ||
|
|
3ace406bba | ||
|
|
11ae71c5b9 | ||
|
|
5aa8358f97 | ||
|
|
120689e7cb | ||
|
|
bf0676e17a | ||
|
|
9c9b5e471c | ||
|
|
d604725b40 | ||
|
|
8b794b5e83 | ||
|
|
ef87b1cee4 | ||
|
|
23c56501de | ||
|
|
9002477006 | ||
|
|
da87e4b1ef | ||
|
|
eaf05a87b4 | ||
|
|
72e48e6a68 | ||
|
|
435df86757 | ||
|
|
12e1499b06 | ||
|
|
4db3e0be80 | ||
|
|
59b935aa46 | ||
|
|
ae7304dd00 | ||
|
|
cb747a34ef | ||
|
|
e7931941a7 | ||
|
|
9149425750 | ||
|
|
02fca76c13 | ||
|
|
06efcaddfa | ||
|
|
15ab64488a | ||
|
|
0fc21282d5 | ||
|
|
4212654b64 | ||
|
|
81ad6501ef | ||
|
|
1f196787c8 | ||
|
|
4a2f9685bb | ||
|
|
69714f4a07 | ||
|
|
b188a05713 | ||
|
|
e574aad6d7 | ||
|
|
bda60769ea | ||
|
|
e4fc36fa1b | ||
|
|
a3c30bf234 | ||
|
|
98ac4c0baf | ||
|
|
556cbad5c0 | ||
|
|
d769b8080d | ||
|
|
816a6e4b1f | ||
|
|
2aa4f77fb0 | ||
|
|
b5b0d7396b | ||
|
|
bdf6bd3355 | ||
|
|
8254cd2381 | ||
|
|
adebdc835f | ||
|
|
3acefa8923 | ||
|
|
4a11737d1c | ||
|
|
0be69b8b04 | ||
|
|
43da3859bc | ||
|
|
e4c13efed7 | ||
|
|
7394aeaa32 | ||
|
|
9fc4efcdd9 | ||
|
|
62bbd52452 | ||
|
|
c93dd877e9 | ||
|
|
e05f54819b | ||
|
|
33ef601b12 | ||
|
|
ae2b47afd6 | ||
|
|
85d4ac372a | ||
|
|
936ef250b4 | ||
|
|
cb90cdcc15 | ||
|
|
fbffe332e2 | ||
|
|
fa51c8601d | ||
|
|
b764e39847 | ||
|
|
bd4fba08e5 | ||
|
|
fd1b060705 | ||
|
|
b19b902345 | ||
|
|
b900ea8e2f | ||
|
|
e13814a59c | ||
|
|
60fa32d51a | ||
|
|
7711e37da4 | ||
|
|
f5658df8db | ||
|
|
c49451eb10 | ||
|
|
c20b1d64a7 | ||
|
|
3df08b02b4 | ||
|
|
1502da5726 | ||
|
|
27c3299f32 | ||
|
|
df7e56b13c | ||
|
|
4a70a319c5 | ||
|
|
76e48c3914 | ||
|
|
474453d2b0 | ||
|
|
9a0db9cb88 | ||
|
|
642a892c05 | ||
|
|
f3241385cd | ||
|
|
1e738fbaca | ||
|
|
2f9050115b | ||
|
|
3700378764 | ||
|
|
670d2b6e2b | ||
|
|
0ac3afebc6 | ||
|
|
ef8dd0f64b | ||
|
|
21fc903c04 | ||
|
|
dc5e5eca6b | ||
|
|
4069f95fd1 | ||
|
|
26c30a04ba | ||
|
|
053c15df8a | ||
|
|
b6e6fb1fac | ||
|
|
a43eb91f45 | ||
|
|
76ab4710a8 | ||
|
|
b6dd64aac1 | ||
|
|
7bb6dabf23 | ||
|
|
3c4f47acc9 | ||
|
|
01a9e17c49 | ||
|
|
9ca8b20d55 | ||
|
|
0e3dfa8de4 | ||
|
|
085417bdbb | ||
|
|
205eda8b33 | ||
|
|
010bfc56e1 | ||
|
|
3c9263221d | ||
|
|
13bf8dc8bf | ||
|
|
01f259be0d | ||
|
|
594589e0ec | ||
|
|
0087d237f7 | ||
|
|
7509dda1ff | ||
|
|
5fbc7f8f1f | ||
|
|
628d05629b | ||
|
|
a74d55edd9 | ||
|
|
8ff33ac9f7 | ||
|
|
6ecd90e7ff | ||
|
|
0a4d2affd8 | ||
|
|
9cb68f7b29 | ||
|
|
80b1a73ccf | ||
|
|
f3d1aa5359 | ||
|
|
f5768cc99b | ||
|
|
bc4785ca60 | ||
|
|
888c3581eb | ||
|
|
4545b906b4 | ||
|
|
b38de1f5e2 | ||
|
|
7ee06dfe08 | ||
|
|
c5ca4e2622 | ||
|
|
edb6e245cf | ||
|
|
e1ca4a8663 | ||
|
|
b1b721ff6d | ||
|
|
de6ff11164 | ||
|
|
6db84ab0c1 | ||
|
|
bc18590da3 | ||
|
|
e4e1c7a0f2 | ||
|
|
7dfc6545ff | ||
|
|
9bf4288af9 | ||
|
|
56f9354998 | ||
|
|
484a3ffc8a | ||
|
|
c17347b7f7 | ||
|
|
9b549fdfe9 | ||
|
|
17378152bf | ||
|
|
b29288c50a | ||
|
|
784440691c | ||
|
|
6da348c3e6 | ||
|
|
6ccb68bd46 | ||
|
|
eeb290d650 | ||
|
|
0396c99945 | ||
|
|
2f53246c13 | ||
|
|
07d88851bf | ||
|
|
e978b3ace0 | ||
|
|
cd716e311a | ||
|
|
f9ddb562d5 | ||
|
|
136cdc765a | ||
|
|
60d2a5e322 | ||
|
|
7371077fa2 | ||
|
|
e0c7eb25a4 | ||
|
|
2113b88db3 | ||
|
|
3e9aeea75d | ||
|
|
32464f4912 | ||
|
|
0ba6ece2fd | ||
|
|
cb140248ea | ||
|
|
ebb73fa258 | ||
|
|
5a7f656303 | ||
|
|
2d02a2692a | ||
|
|
52af9a1766 | ||
|
|
86cfa025ed | ||
|
|
f64dc99f8a | ||
|
|
4bd874ab46 | ||
|
|
ec46d4315c | ||
|
|
597b2ad2ea | ||
|
|
a0423f4eb1 | ||
|
|
2a00c33a61 | ||
|
|
d90f46a5da | ||
|
|
019f57e5c7 | ||
|
|
60a8230209 | ||
|
|
346bbcb80a | ||
|
|
bbd0cb269d | ||
|
|
9aad7c3907 | ||
|
|
442532e316 | ||
|
|
6ab02e535f | ||
|
|
8cea7d8cf2 | ||
|
|
6327bdc6f8 | ||
|
|
4c2776c634 | ||
|
|
b0c7884976 | ||
|
|
46085644ed | ||
|
|
01411ab567 | ||
|
|
7e4d0209bb | ||
|
|
1fd7540b85 | ||
|
|
7bc0c26198 | ||
|
|
f41d917141 | ||
|
|
29c3b34641 | ||
|
|
e27deb1ece | ||
|
|
bf76b3b286 | ||
|
|
5a90b6d0b5 | ||
|
|
bc75b15546 | ||
|
|
2ca2634138 | ||
|
|
0163ef8bfb | ||
|
|
57c347d8f8 | ||
|
|
a4394e877e | ||
|
|
d2192bba02 | ||
|
|
71afcaf43b | ||
|
|
73aadc4900 | ||
|
|
131dc3a8fc | ||
|
|
69496179d4 | ||
|
|
4dbe2e7062 | ||
|
|
672331d7e4 | ||
|
|
1e18c23095 | ||
|
|
54478d1e08 | ||
|
|
f085508121 | ||
|
|
8ea4e79964 | ||
|
|
596366b062 | ||
|
|
f4637c38b4 | ||
|
|
9d62a564df | ||
|
|
83d148242a | ||
|
|
38f6c588ca | ||
|
|
7def5b551d | ||
|
|
a968c1196e | ||
|
|
e3d47acb0b | ||
|
|
098a38bb3a | ||
|
|
dbc7369d9d | ||
|
|
b3a6555b30 | ||
|
|
1f9e1ca318 | ||
|
|
e5e73a56a7 | ||
|
|
0e20ec697b | ||
|
|
cbb8b12ba4 | ||
|
|
eeeada0773 | ||
|
|
bbca694881 | ||
|
|
160f6cd361 | ||
|
|
11b24ab64b | ||
|
|
360bda7e68 | ||
|
|
0c6e2feeaa | ||
|
|
bd8b509986 | ||
|
|
2e67ffe769 | ||
|
|
ed364c9114 | ||
|
|
1093454193 | ||
|
|
de394f5f74 | ||
|
|
23527da802 | ||
|
|
79c8fea12f | ||
|
|
61dfab12fd | ||
|
|
474c010746 | ||
|
|
8f7b69f77e | ||
|
|
0d4bb7558f | ||
|
|
5b29964512 | ||
|
|
2c67e9e0d7 | ||
|
|
6b03c741ac | ||
|
|
45a9b9954a | ||
|
|
6e35d2f0e9 | ||
|
|
997bfbe78f | ||
|
|
6aa63659aa | ||
|
|
c90bf645f5 | ||
|
|
d678959359 | ||
|
|
8bbc6d251e | ||
|
|
eab2c74e10 | ||
|
|
35ca873269 | ||
|
|
a4391db7cd | ||
|
|
f2a609c358 | ||
|
|
559aa626cd | ||
|
|
5ef5b3202d | ||
|
|
b3d80d50a3 | ||
|
|
28ca9d240a | ||
|
|
ece5232110 | ||
|
|
7e41730293 | ||
|
|
288c3b863d | ||
|
|
4a59b869c0 | ||
|
|
10ed306b19 | ||
|
|
0dce4761a8 | ||
|
|
9b7d312a0e | ||
|
|
76a3298306 | ||
|
|
922acdbcc2 | ||
|
|
a7102db8c4 | ||
|
|
a2b6088772 | ||
|
|
cf30a68312 | ||
|
|
8c9bba1cbf | ||
|
|
78dc37d8af | ||
|
|
ef153a5bca | ||
|
|
f9e6414de6 | ||
|
|
b49ea330d1 | ||
|
|
8a87ff96f9 | ||
|
|
72a46e53ed | ||
|
|
ed956ba5d4 | ||
|
|
e1ba9012c6 | ||
|
|
a3766c5281 | ||
|
|
af2053fe13 | ||
|
|
68ef3aeaa8 | ||
|
|
d65c0e8ed7 | ||
|
|
644455d56b | ||
|
|
4b5677101c | ||
|
|
47235f7b23 | ||
|
|
2542afbc3b | ||
|
|
83d3aaf170 | ||
|
|
cd2776a96f | ||
|
|
e038d630d5 | ||
|
|
6d4e43f3fb | ||
|
|
aaa37be223 | ||
|
|
2b022ac097 | ||
|
|
00f15828a2 | ||
|
|
9f41c7283c | ||
|
|
1e7c26d9a5 | ||
|
|
fe0293a110 | ||
|
|
79e394a0c5 | ||
|
|
5b95b6a6aa | ||
|
|
d994ff79f4 | ||
|
|
0d2fb4503b | ||
|
|
b4c3bb0558 | ||
|
|
2b3111122e | ||
|
|
db3b3d3a1c | ||
|
|
be0da7aaa8 | ||
|
|
2183c673d1 | ||
|
|
ec4dbbced5 | ||
|
|
afa9100453 | ||
|
|
d69de691a7 | ||
|
|
70bdb86c53 | ||
|
|
1269e6c7d3 | ||
|
|
ed18b3359b | ||
|
|
33afeea87a | ||
|
|
452c71c9dc | ||
|
|
42753001ea | ||
|
|
8d824f5088 | ||
|
|
bee45bac8f | ||
|
|
5aca038b7c | ||
|
|
22a7885605 | ||
|
|
7bd4ea835e | ||
|
|
012dc74eea | ||
|
|
eb8770899c | ||
|
|
784f33daa7 | ||
|
|
59b1a6695e | ||
|
|
953e4b4abf | ||
|
|
208d6d881c | ||
|
|
bc68c1c039 | ||
|
|
84e455b583 | ||
|
|
e6ab36d205 | ||
|
|
e771d68214 | ||
|
|
501007f01d | ||
|
|
8aa1bdc031 | ||
|
|
c5ac2d3966 | ||
|
|
6c39a260a3 | ||
|
|
5cc597e394 | ||
|
|
5a3b903733 | ||
|
|
92dcaeb472 | ||
|
|
9d190c922a | ||
|
|
e1d47931f1 | ||
|
|
5d09c730d4 | ||
|
|
89847f9269 | ||
|
|
2ad0465864 | ||
|
|
f8b756627c | ||
|
|
af7540a23a | ||
|
|
b54496e61f | ||
|
|
86442e60ed | ||
|
|
932380b284 | ||
|
|
8387c628cd | ||
|
|
e59814f981 | ||
|
|
fe903be480 | ||
|
|
885b67fcaa | ||
|
|
22a69cc9ae | ||
|
|
de26e3fb85 | ||
|
|
485ce41523 | ||
|
|
98bb49382b | ||
|
|
b31d1329ca | ||
|
|
f7f6b7ba5b | ||
|
|
eae3a82067 | ||
|
|
7ab18f6aa1 | ||
|
|
6c182d0a8a | ||
|
|
21b1783da3 | ||
|
|
ba11020fdf | ||
|
|
1bdd37f3e0 | ||
|
|
19f70c95d1 | ||
|
|
9fea2ab6d2 | ||
|
|
c709c5120c | ||
|
|
32ee6b3275 | ||
|
|
7a18920728 | ||
|
|
5d05de4b09 | ||
|
|
36120d17e7 | ||
|
|
691add1aff | ||
|
|
c745acf4c2 | ||
|
|
6869e652c4 | ||
|
|
bd0e728352 | ||
|
|
760d397363 | ||
|
|
6469e9a5a3 | ||
|
|
0d02931759 | ||
|
|
ed0a8413a2 | ||
|
|
d42c3dbf18 | ||
|
|
09d1f5dd2e | ||
|
|
d5004b607b | ||
|
|
c961fe8f2b | ||
|
|
633e4a00e6 | ||
|
|
25209273d3 | ||
|
|
4224a0c3fe | ||
|
|
4a3864a180 | ||
|
|
ed5b14884c | ||
|
|
de68231632 | ||
|
|
bc555048a5 | ||
|
|
8045ef9d3d | ||
|
|
213e8facee | ||
|
|
3f2982ede6 | ||
|
|
1ef354f75c | ||
|
|
2adc469439 | ||
|
|
1b9faa1aaf | ||
|
|
d8769cb27c | ||
|
|
c20a13f8ee | ||
|
|
3f277b6b65 | ||
|
|
700e9c8aa0 | ||
|
|
919d4589b9 | ||
|
|
1e5fa50a80 | ||
|
|
31590e307a | ||
|
|
d7cb14773b | ||
|
|
b26f1c55cc | ||
|
|
75050f767a | ||
|
|
8cbd91214d | ||
|
|
a9ea20e818 | ||
|
|
b3c5ef8afe | ||
|
|
9c1f943a17 | ||
|
|
6bc4a110e4 | ||
|
|
064bb49b55 | ||
|
|
067aebab76 | ||
|
|
aa583b4144 | ||
|
|
32391269ae | ||
|
|
cc5f59abd4 | ||
|
|
cf1cc08c49 | ||
|
|
e41b90c71c | ||
|
|
134aeb2ca8 | ||
|
|
c616f21d1f | ||
|
|
b8a70f2e24 | ||
|
|
5d600587a1 | ||
|
|
24e99f8b22 | ||
|
|
38b3e045cf | ||
|
|
b1afd75620 | ||
|
|
628f9dc4cf | ||
|
|
7040a1212f | ||
|
|
1ada210da1 | ||
|
|
1dc94c65cb | ||
|
|
3749e4eb98 | ||
|
|
544bbba046 | ||
|
|
a97a2fec3f | ||
|
|
4e62001154 | ||
|
|
ab9f201302 | ||
|
|
a1b24e0f65 | ||
|
|
eaa016bbc3 | ||
|
|
e6f7e47547 | ||
|
|
d78d83f553 | ||
|
|
1f4e3af192 | ||
|
|
e74734f57c | ||
|
|
71b3b5d1b5 | ||
|
|
51631e54f8 | ||
|
|
0dac2e8c59 | ||
|
|
c950adfe82 | ||
|
|
61396f68d0 | ||
|
|
1d48ab201b | ||
|
|
83d7761998 | ||
|
|
6d20cfe831 | ||
|
|
3528343863 | ||
|
|
248d983a7d | ||
|
|
447e15d6cb | ||
|
|
734188c0dc | ||
|
|
999c9762e6 | ||
|
|
5e4060b15c | ||
|
|
121df3d4a7 | ||
|
|
f83c0a81fe | ||
|
|
de6e4ad431 | ||
|
|
bca9a8431e | ||
|
|
7cecd72faa | ||
|
|
d85cc5f561 | ||
|
|
fe8fb875c2 | ||
|
|
06a92fe0d0 | ||
|
|
3126ff8388 | ||
|
|
9c2561b59d | ||
|
|
bb80baf9a5 | ||
|
|
ba797909b0 | ||
|
|
5416c6e3ef | ||
|
|
9d406189bd | ||
|
|
df3adb9a90 | ||
|
|
d40ad599ed | ||
|
|
dac066ea0a | ||
|
|
d3de07a17c | ||
|
|
72539451aa | ||
|
|
0d141eef32 | ||
|
|
b19a6d06d9 | ||
|
|
8e92ffc191 | ||
|
|
b53b7b1acd | ||
|
|
39b9599360 | ||
|
|
5984adc3df | ||
|
|
6a4966fe10 | ||
|
|
419b18b4a9 | ||
|
|
7d3f29ea3f | ||
|
|
9286ddd655 | ||
|
|
3357850013 | ||
|
|
68e98fa37d | ||
|
|
0225030551 | ||
|
|
f77ed4a7c3 | ||
|
|
6cffab9919 | ||
|
|
e7bf31d162 | ||
|
|
c4cf97dea5 | ||
|
|
b25d496c24 | ||
|
|
d5ca514688 | ||
|
|
a010372458 | ||
|
|
08ef201cec | ||
|
|
80807a8c49 | ||
|
|
caf61dc923 | ||
|
|
4185a4ae3b | ||
|
|
5a8b369754 | ||
|
|
cfcc9069e5 | ||
|
|
a140a03a35 | ||
|
|
6803d95a32 | ||
|
|
69657be534 | ||
|
|
5f0509435a | ||
|
|
b2f2c58f3d | ||
|
|
466a7f2293 | ||
|
|
5f4e940a09 | ||
|
|
2f9c6b38cd | ||
|
|
5c1e1ee4bb | ||
|
|
f544dd682b | ||
|
|
5bf647f290 | ||
|
|
953b3619d4 | ||
|
|
a5c05435ab | ||
|
|
9a8f430db6 | ||
|
|
23941adee1 | ||
|
|
bf917a61e7 | ||
|
|
2ffbfacf5f | ||
|
|
3f67ec4436 | ||
|
|
36da6d0f80 | ||
|
|
7d49d8b20e | ||
|
|
fb0303ac1d | ||
|
|
223f892742 | ||
|
|
b32056b79d | ||
|
|
92532fb7b8 | ||
|
|
c22810ed51 | ||
|
|
870d9bad86 | ||
|
|
5b4a9ac3b3 | ||
|
|
dfca67d844 | ||
|
|
458800c1d5 | ||
|
|
0a6a7f0ed7 | ||
|
|
e37ee0d21b | ||
|
|
de716bd024 | ||
|
|
cd9de0837f | ||
|
|
0082e36b60 | ||
|
|
ace19cc20f | ||
|
|
0cefb7716e | ||
|
|
6f21796c34 | ||
|
|
8e17c2aeac | ||
|
|
85b872f340 | ||
|
|
3269fab34e | ||
|
|
e14caf8e33 | ||
|
|
fac4a79d4c |
@@ -1,5 +1,7 @@
|
|||||||
/.project
|
/.project
|
||||||
/.loadpath
|
/.loadpath
|
||||||
|
/.powrc
|
||||||
|
/.rvmrc
|
||||||
/config/additional_environment.rb
|
/config/additional_environment.rb
|
||||||
/config/configuration.yml
|
/config/configuration.yml
|
||||||
/config/database.yml
|
/config/database.yml
|
||||||
@@ -15,8 +17,14 @@
|
|||||||
/lib/redmine/scm/adapters/mercurial/redminehelper.pyo
|
/lib/redmine/scm/adapters/mercurial/redminehelper.pyo
|
||||||
/log/*.log*
|
/log/*.log*
|
||||||
/log/mongrel_debug
|
/log/mongrel_debug
|
||||||
|
/plugins/*
|
||||||
|
!/plugins/README
|
||||||
/public/dispatch.*
|
/public/dispatch.*
|
||||||
/public/plugin_assets
|
/public/plugin_assets
|
||||||
|
/public/themes/*
|
||||||
|
!/public/themes/alternate
|
||||||
|
!/public/themes/classic
|
||||||
|
!/public/themes/README
|
||||||
/tmp/*
|
/tmp/*
|
||||||
/tmp/cache/*
|
/tmp/cache/*
|
||||||
/tmp/pdf/*
|
/tmp/pdf/*
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ syntax: glob
|
|||||||
|
|
||||||
.project
|
.project
|
||||||
.loadpath
|
.loadpath
|
||||||
|
.powrc
|
||||||
|
.rvmrc
|
||||||
config/additional_environment.rb
|
config/additional_environment.rb
|
||||||
config/configuration.yml
|
config/configuration.yml
|
||||||
config/database.yml
|
config/database.yml
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
**Do not send a pull requst to this github repository**.
|
||||||
|
|
||||||
|
For more detail, please see [official website] wiki [Contribute].
|
||||||
|
|
||||||
|
[official website]: http://www.redmine.org
|
||||||
|
[Contribute]: http://www.redmine.org/projects/redmine/wiki/Contribute
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
source 'http://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
gem 'rails', '3.2.12'
|
gem "rails", "3.2.13"
|
||||||
gem "jquery-rails", "~> 2.0.2"
|
gem "jquery-rails", "~> 2.0.2"
|
||||||
gem "i18n", "~> 0.6.0"
|
gem "i18n", "~> 0.6.0"
|
||||||
gem "coderay", "~> 1.0.6"
|
gem "coderay", "~> 1.1.0"
|
||||||
gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
|
gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
|
||||||
gem "builder", "3.0.0"
|
gem "builder", "3.0.0"
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ end
|
|||||||
|
|
||||||
# Optional gem for OpenID authentication
|
# Optional gem for OpenID authentication
|
||||||
group :openid do
|
group :openid do
|
||||||
gem "ruby-openid", "~> 2.1.4", :require => "openid"
|
gem "ruby-openid", "~> 2.3.0", :require => "openid"
|
||||||
gem "rack-openid"
|
gem "rack-openid"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,12 +31,13 @@ end
|
|||||||
platforms :jruby do
|
platforms :jruby do
|
||||||
# jruby-openssl is bundled with JRuby 1.7.0
|
# jruby-openssl is bundled with JRuby 1.7.0
|
||||||
gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0'
|
gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0'
|
||||||
gem "activerecord-jdbc-adapter", "1.2.5"
|
gem "activerecord-jdbc-adapter", "~> 1.2.6"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Include database gems for the adapters found in the database
|
# Include database gems for the adapters found in the database
|
||||||
# configuration file
|
# configuration file
|
||||||
require 'erb'
|
require 'erb'
|
||||||
|
require 'yaml'
|
||||||
database_file = File.join(File.dirname(__FILE__), "config/database.yml")
|
database_file = File.join(File.dirname(__FILE__), "config/database.yml")
|
||||||
if File.exist?(database_file)
|
if File.exist?(database_file)
|
||||||
database_config = YAML::load(ERB.new(IO.read(database_file)).result)
|
database_config = YAML::load(ERB.new(IO.read(database_file)).result)
|
||||||
@@ -44,9 +45,11 @@ if File.exist?(database_file)
|
|||||||
if adapters.any?
|
if adapters.any?
|
||||||
adapters.each do |adapter|
|
adapters.each do |adapter|
|
||||||
case adapter
|
case adapter
|
||||||
when /mysql/
|
when 'mysql2'
|
||||||
gem "mysql", "~> 2.8.1", :platforms => [:mri_18, :mingw_18]
|
gem "mysql2", "~> 0.3.11", :platforms => [:mri, :mingw]
|
||||||
gem "mysql2", "~> 0.3.11", :platforms => [:mri_19, :mingw_19]
|
gem "activerecord-jdbcmysql-adapter", :platforms => :jruby
|
||||||
|
when 'mysql'
|
||||||
|
gem "mysql", "~> 2.8.1", :platforms => [:mri, :mingw]
|
||||||
gem "activerecord-jdbcmysql-adapter", :platforms => :jruby
|
gem "activerecord-jdbcmysql-adapter", :platforms => :jruby
|
||||||
when /postgresql/
|
when /postgresql/
|
||||||
gem "pg", ">= 0.11.0", :platforms => [:mri, :mingw]
|
gem "pg", ">= 0.11.0", :platforms => [:mri, :mingw]
|
||||||
@@ -75,8 +78,12 @@ end
|
|||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem "shoulda", "~> 3.3.2"
|
gem "shoulda", "~> 3.3.2"
|
||||||
gem "mocha"
|
gem "mocha", ">= 0.14", :require => 'mocha/api'
|
||||||
gem 'capybara', '~> 2.0.0'
|
if RUBY_VERSION >= '1.9.3'
|
||||||
|
gem "capybara", "~> 2.1.0"
|
||||||
|
gem "selenium-webdriver"
|
||||||
|
gem "database_cleaner"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
|
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
|
||||||
@@ -88,5 +95,6 @@ end
|
|||||||
# Load plugins' Gemfiles
|
# Load plugins' Gemfiles
|
||||||
Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file|
|
Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file|
|
||||||
puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
|
puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
|
||||||
instance_eval File.read(file)
|
#TODO: switch to "eval_gemfile file" when bundler >= 1.2.0 will be required (rails 4)
|
||||||
|
instance_eval File.read(file), file
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class AccountController < ApplicationController
|
|||||||
include CustomFieldsHelper
|
include CustomFieldsHelper
|
||||||
|
|
||||||
# prevents login action to be filtered by check_if_login_required application scope filter
|
# prevents login action to be filtered by check_if_login_required application scope filter
|
||||||
skip_before_filter :check_if_login_required
|
skip_before_filter :check_if_login_required, :check_password_change
|
||||||
|
|
||||||
# Login request and validation
|
# Login request and validation
|
||||||
def login
|
def login
|
||||||
@@ -75,11 +75,15 @@ class AccountController < ApplicationController
|
|||||||
else
|
else
|
||||||
if request.post?
|
if request.post?
|
||||||
user = User.find_by_mail(params[:mail].to_s)
|
user = User.find_by_mail(params[:mail].to_s)
|
||||||
# user not found or not active
|
# user not found
|
||||||
unless user && user.active?
|
unless user
|
||||||
flash.now[:error] = l(:notice_account_unknown_email)
|
flash.now[:error] = l(:notice_account_unknown_email)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
unless user.active?
|
||||||
|
handle_inactive_user(user, lost_password_path)
|
||||||
|
return
|
||||||
|
end
|
||||||
# user cannot change its password
|
# user cannot change its password
|
||||||
unless user.change_password_allowed?
|
unless user.change_password_allowed?
|
||||||
flash.now[:error] = l(:notice_can_t_change_password)
|
flash.now[:error] = l(:notice_can_t_change_password)
|
||||||
@@ -152,6 +156,19 @@ class AccountController < ApplicationController
|
|||||||
redirect_to signin_path
|
redirect_to signin_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Sends a new account activation email
|
||||||
|
def activation_email
|
||||||
|
if session[:registered_user_id] && Setting.self_registration == '1'
|
||||||
|
user_id = session.delete(:registered_user_id).to_i
|
||||||
|
user = User.find_by_id(user_id)
|
||||||
|
if user && user.registered?
|
||||||
|
register_by_email_activation(user)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redirect_to(home_url)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authenticate_user
|
def authenticate_user
|
||||||
@@ -163,7 +180,7 @@ class AccountController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def password_authentication
|
def password_authentication
|
||||||
user = User.try_to_login(params[:username], params[:password])
|
user = User.try_to_login(params[:username], params[:password], false)
|
||||||
|
|
||||||
if user.nil?
|
if user.nil?
|
||||||
invalid_credentials
|
invalid_credentials
|
||||||
@@ -171,25 +188,31 @@ class AccountController < ApplicationController
|
|||||||
onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
|
onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
|
||||||
else
|
else
|
||||||
# Valid user
|
# Valid user
|
||||||
successful_authentication(user)
|
if user.active?
|
||||||
|
successful_authentication(user)
|
||||||
|
else
|
||||||
|
handle_inactive_user(user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_id_authenticate(openid_url)
|
def open_id_authenticate(openid_url)
|
||||||
authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url, :method => :post) do |result, identity_url, registration|
|
back_url = signin_url(:autologin => params[:autologin])
|
||||||
|
authenticate_with_open_id(
|
||||||
|
openid_url, :required => [:nickname, :fullname, :email],
|
||||||
|
:return_to => back_url, :method => :post
|
||||||
|
) do |result, identity_url, registration|
|
||||||
if result.successful?
|
if result.successful?
|
||||||
user = User.find_or_initialize_by_identity_url(identity_url)
|
user = User.find_or_initialize_by_identity_url(identity_url)
|
||||||
if user.new_record?
|
if user.new_record?
|
||||||
# Self-registration off
|
# Self-registration off
|
||||||
(redirect_to(home_url); return) unless Setting.self_registration?
|
(redirect_to(home_url); return) unless Setting.self_registration?
|
||||||
|
|
||||||
# Create on the fly
|
# Create on the fly
|
||||||
user.login = registration['nickname'] unless registration['nickname'].nil?
|
user.login = registration['nickname'] unless registration['nickname'].nil?
|
||||||
user.mail = registration['email'] unless registration['email'].nil?
|
user.mail = registration['email'] unless registration['email'].nil?
|
||||||
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
|
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
|
||||||
user.random_password
|
user.random_password
|
||||||
user.register
|
user.register
|
||||||
|
|
||||||
case Setting.self_registration
|
case Setting.self_registration
|
||||||
when '1'
|
when '1'
|
||||||
register_by_email_activation(user) do
|
register_by_email_activation(user) do
|
||||||
@@ -209,7 +232,7 @@ class AccountController < ApplicationController
|
|||||||
if user.active?
|
if user.active?
|
||||||
successful_authentication(user)
|
successful_authentication(user)
|
||||||
else
|
else
|
||||||
account_pending
|
handle_inactive_user(user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -230,7 +253,6 @@ class AccountController < ApplicationController
|
|||||||
|
|
||||||
def set_autologin_cookie(user)
|
def set_autologin_cookie(user)
|
||||||
token = Token.create(:user => user, :action => 'autologin')
|
token = Token.create(:user => user, :action => 'autologin')
|
||||||
cookie_name = Redmine::Configuration['autologin_cookie_name'] || 'autologin'
|
|
||||||
cookie_options = {
|
cookie_options = {
|
||||||
:value => token.value,
|
:value => token.value,
|
||||||
:expires => 1.year.from_now,
|
:expires => 1.year.from_now,
|
||||||
@@ -238,7 +260,7 @@ class AccountController < ApplicationController
|
|||||||
:secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false),
|
:secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false),
|
||||||
:httponly => true
|
:httponly => true
|
||||||
}
|
}
|
||||||
cookies[cookie_name] = cookie_options
|
cookies[autologin_cookie_name] = cookie_options
|
||||||
end
|
end
|
||||||
|
|
||||||
# Onthefly creation failed, display the registration form to fill/fix attributes
|
# Onthefly creation failed, display the registration form to fill/fix attributes
|
||||||
@@ -260,7 +282,7 @@ class AccountController < ApplicationController
|
|||||||
token = Token.new(:user => user, :action => "register")
|
token = Token.new(:user => user, :action => "register")
|
||||||
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, :email => user.mail)
|
||||||
redirect_to signin_path
|
redirect_to signin_path
|
||||||
else
|
else
|
||||||
yield if block_given?
|
yield if block_given?
|
||||||
@@ -290,14 +312,32 @@ class AccountController < ApplicationController
|
|||||||
if user.save
|
if user.save
|
||||||
# Sends an email to the administrators
|
# Sends an email to the administrators
|
||||||
Mailer.account_activation_request(user).deliver
|
Mailer.account_activation_request(user).deliver
|
||||||
account_pending
|
account_pending(user)
|
||||||
else
|
else
|
||||||
yield if block_given?
|
yield if block_given?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_pending
|
def handle_inactive_user(user, redirect_path=signin_path)
|
||||||
flash[:notice] = l(:notice_account_pending)
|
if user.registered?
|
||||||
redirect_to signin_path
|
account_pending(user, redirect_path)
|
||||||
|
else
|
||||||
|
account_locked(user, redirect_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_pending(user, redirect_path=signin_path)
|
||||||
|
if Setting.self_registration == '1'
|
||||||
|
flash[:error] = l(:notice_account_not_activated_yet, :url => activation_email_path)
|
||||||
|
session[:registered_user_id] = user.id
|
||||||
|
else
|
||||||
|
flash[:error] = l(:notice_account_pending)
|
||||||
|
end
|
||||||
|
redirect_to redirect_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_locked(user, redirect_path=signin_path)
|
||||||
|
flash[:error] = l(:notice_account_locked)
|
||||||
|
redirect_to redirect_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class AdminController < ApplicationController
|
|||||||
@test = Mailer.test_email(User.current).deliver
|
@test = Mailer.test_email(User.current).deliver
|
||||||
flash[:notice] = l(:notice_email_sent, User.current.mail)
|
flash[:notice] = l(:notice_email_sent, User.current.mail)
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
flash[:error] = l(:notice_email_error, e.message)
|
flash[:error] = l(:notice_email_error, Redmine::CodesetUtil.replace_invalid_utf8(e.message))
|
||||||
end
|
end
|
||||||
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
|
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
|
||||||
redirect_to settings_path(:tab => 'notifications')
|
redirect_to settings_path(:tab => 'notifications')
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ class ApplicationController < ActionController::Base
|
|||||||
protect_from_forgery
|
protect_from_forgery
|
||||||
def handle_unverified_request
|
def handle_unverified_request
|
||||||
super
|
super
|
||||||
cookies.delete(:autologin)
|
cookies.delete(autologin_cookie_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
|
before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :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
|
||||||
@@ -78,6 +78,9 @@ class ApplicationController < ActionController::Base
|
|||||||
session[:user_id] = user.id
|
session[:user_id] = user.id
|
||||||
session[:ctime] = Time.now.utc.to_i
|
session[:ctime] = Time.now.utc.to_i
|
||||||
session[:atime] = Time.now.utc.to_i
|
session[:atime] = Time.now.utc.to_i
|
||||||
|
if user.must_change_password?
|
||||||
|
session[:pwd] = '1'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_setup
|
def user_setup
|
||||||
@@ -112,6 +115,10 @@ class ApplicationController < ActionController::Base
|
|||||||
authenticate_with_http_basic do |username, password|
|
authenticate_with_http_basic do |username, password|
|
||||||
user = 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
|
||||||
|
if user && user.must_change_password?
|
||||||
|
render_error :message => 'You must change your password', :status => 403
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
# Switch user if requested by an admin user
|
# Switch user if requested by an admin user
|
||||||
if user && user.admin? && (username = api_switch_user_from_request)
|
if user && user.admin? && (username = api_switch_user_from_request)
|
||||||
@@ -127,10 +134,14 @@ class ApplicationController < ActionController::Base
|
|||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def autologin_cookie_name
|
||||||
|
Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
|
||||||
|
end
|
||||||
|
|
||||||
def try_to_autologin
|
def try_to_autologin
|
||||||
if cookies[:autologin] && Setting.autologin?
|
if cookies[autologin_cookie_name] && Setting.autologin?
|
||||||
# auto-login feature starts a new session
|
# auto-login feature starts a new session
|
||||||
user = User.try_to_autologin(cookies[:autologin])
|
user = User.try_to_autologin(cookies[autologin_cookie_name])
|
||||||
if user
|
if user
|
||||||
reset_session
|
reset_session
|
||||||
start_user_session(user)
|
start_user_session(user)
|
||||||
@@ -153,7 +164,7 @@ class ApplicationController < ActionController::Base
|
|||||||
# Logs out current user
|
# Logs out current user
|
||||||
def logout_user
|
def logout_user
|
||||||
if User.current.logged?
|
if User.current.logged?
|
||||||
cookies.delete :autologin
|
cookies.delete(autologin_cookie_name)
|
||||||
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
|
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
|
||||||
self.logged_user = nil
|
self.logged_user = nil
|
||||||
end
|
end
|
||||||
@@ -166,6 +177,16 @@ class ApplicationController < ActionController::Base
|
|||||||
require_login if Setting.login_required?
|
require_login if Setting.login_required?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_password_change
|
||||||
|
if session[:pwd]
|
||||||
|
if User.current.must_change_password?
|
||||||
|
redirect_to my_password_path
|
||||||
|
else
|
||||||
|
session.delete(:pwd)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_localization
|
def set_localization
|
||||||
lang = nil
|
lang = nil
|
||||||
if User.current.logged?
|
if User.current.logged?
|
||||||
@@ -191,7 +212,13 @@ class ApplicationController < ActionController::Base
|
|||||||
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
|
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
|
||||||
end
|
end
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
|
format.html {
|
||||||
|
if request.xhr?
|
||||||
|
head :unauthorized
|
||||||
|
else
|
||||||
|
redirect_to :controller => "account", :action => "login", :back_url => url
|
||||||
|
end
|
||||||
|
}
|
||||||
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
|
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
|
||||||
format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
|
format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
|
||||||
format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
|
format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
|
||||||
@@ -294,7 +321,7 @@ class ApplicationController < ActionController::Base
|
|||||||
# Find issues with a single :id param or :ids array param
|
# Find issues with a single :id param or :ids array param
|
||||||
# Raises a Unauthorized exception if one of the issues is not visible
|
# 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.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
|
||||||
raise ActiveRecord::RecordNotFound if @issues.empty?
|
raise ActiveRecord::RecordNotFound if @issues.empty?
|
||||||
raise Unauthorized unless @issues.all?(&:visible?)
|
raise Unauthorized unless @issues.all?(&:visible?)
|
||||||
@projects = @issues.collect(&:project).compact.uniq
|
@projects = @issues.collect(&:project).compact.uniq
|
||||||
@@ -551,21 +578,6 @@ class ApplicationController < ActionController::Base
|
|||||||
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?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sets the `flash` notice or error based the number of issues that did not save
|
|
||||||
#
|
|
||||||
# @param [Array, Issue] issues all of the saved and unsaved Issues
|
|
||||||
# @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
|
|
||||||
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
|
|
||||||
if unsaved_issue_ids.empty?
|
|
||||||
flash[:notice] = l(:notice_successful_update) unless issues.empty?
|
|
||||||
else
|
|
||||||
flash[:error] = l(:notice_failed_to_save_issues,
|
|
||||||
:count => unsaved_issue_ids.size,
|
|
||||||
:total => issues.size,
|
|
||||||
:ids => '#' + unsaved_issue_ids.join(', #'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Rescues an invalid query statement. Just in case...
|
# Rescues an invalid query statement. Just in case...
|
||||||
def query_statement_invalid(exception)
|
def query_statement_invalid(exception)
|
||||||
logger.error "Query::StatementInvalid: #{exception.message}" if logger
|
logger.error "Query::StatementInvalid: #{exception.message}" if logger
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class BoardsController < ApplicationController
|
|||||||
helper :watchers
|
helper :watchers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@boards = @project.boards.includes(:last_message => :author).all
|
@boards = @project.boards.includes(:project, :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
|
||||||
|
|||||||
@@ -19,17 +19,15 @@ class ContextMenusController < ApplicationController
|
|||||||
helper :watchers
|
helper :watchers
|
||||||
helper :issues
|
helper :issues
|
||||||
|
|
||||||
|
before_filter :find_issues, :only => :issues
|
||||||
|
|
||||||
def issues
|
def issues
|
||||||
@issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
|
|
||||||
(render_404; return) unless @issues.present?
|
|
||||||
if (@issues.size == 1)
|
if (@issues.size == 1)
|
||||||
@issue = @issues.first
|
@issue = @issues.first
|
||||||
end
|
end
|
||||||
@issue_ids = @issues.map(&:id).sort
|
@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
|
|
||||||
@project = @projects.first if @projects.size == 1
|
|
||||||
|
|
||||||
@can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
|
@can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
|
||||||
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
|
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
|
||||||
@@ -73,8 +71,7 @@ class ContextMenusController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def time_entries
|
def time_entries
|
||||||
@time_entries = TimeEntry.all(
|
@time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
|
||||||
:conditions => {:id => params[:ids]}, :include => :project)
|
|
||||||
(render_404; return) unless @time_entries.present?
|
(render_404; return) unless @time_entries.present?
|
||||||
|
|
||||||
@projects = @time_entries.collect(&:project).compact.uniq
|
@projects = @time_entries.collect(&:project).compact.uniq
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ class CustomFieldsController < ApplicationController
|
|||||||
before_filter :require_admin
|
before_filter :require_admin
|
||||||
before_filter :build_new_custom_field, :only => [:new, :create]
|
before_filter :build_new_custom_field, :only => [:new, :create]
|
||||||
before_filter :find_custom_field, :only => [:edit, :update, :destroy]
|
before_filter :find_custom_field, :only => [:edit, :update, :destroy]
|
||||||
|
accept_api_auth :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
|
respond_to do |format|
|
||||||
@tab = params[:tab] || 'IssueCustomField'
|
format.html {
|
||||||
|
@custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
|
||||||
|
@tab = params[:tab] || 'IssueCustomField'
|
||||||
|
}
|
||||||
|
format.api {
|
||||||
|
@custom_fields = CustomField.all
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
|||||||
@@ -70,14 +70,12 @@ class EnumerationsController < ApplicationController
|
|||||||
@enumeration.destroy
|
@enumeration.destroy
|
||||||
redirect_to enumerations_path
|
redirect_to enumerations_path
|
||||||
return
|
return
|
||||||
elsif params[:reassign_to_id]
|
elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i))
|
||||||
if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id])
|
@enumeration.destroy(reassign_to)
|
||||||
@enumeration.destroy(reassign_to)
|
redirect_to enumerations_path
|
||||||
redirect_to enumerations_path
|
return
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@enumerations = @enumeration.class.all - [@enumeration]
|
@enumerations = @enumeration.class.system.all - [@enumeration]
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class IssueRelationsController < ApplicationController
|
|||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to issue_path(@issue) }
|
format.html { redirect_to issue_path(@issue) }
|
||||||
format.js {
|
format.js {
|
||||||
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
@relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
||||||
}
|
}
|
||||||
format.api {
|
format.api {
|
||||||
if saved
|
if saved
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class IssueStatusesController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
@issue_status = IssueStatus.new(params[:issue_status])
|
@issue_status = IssueStatus.new(params[:issue_status])
|
||||||
if request.post? && @issue_status.save
|
if @issue_status.save
|
||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_to issue_statuses_path
|
redirect_to issue_statuses_path
|
||||||
else
|
else
|
||||||
@@ -54,7 +54,7 @@ class IssueStatusesController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
@issue_status = IssueStatus.find(params[:id])
|
@issue_status = IssueStatus.find(params[:id])
|
||||||
if request.put? && @issue_status.update_attributes(params[:issue_status])
|
if @issue_status.update_attributes(params[:issue_status])
|
||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to issue_statuses_path
|
redirect_to issue_statuses_path
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ class IssuesController < ApplicationController
|
|||||||
@journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
|
@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.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||||
|
Journal.preload_journals_details_custom_fields(@journals)
|
||||||
|
# TODO: use #select! when ruby1.8 support is dropped
|
||||||
|
@journals.reject! {|journal| !journal.notes? && journal.visible_details.empty?}
|
||||||
@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
|
||||||
@@ -113,6 +116,8 @@ class IssuesController < ApplicationController
|
|||||||
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
|
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
|
||||||
@priorities = IssuePriority.active
|
@priorities = IssuePriority.active
|
||||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||||
|
@relation = IssueRelation.new
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
retrieve_previous_and_next_issue_ids
|
retrieve_previous_and_next_issue_ids
|
||||||
@@ -176,7 +181,7 @@ class IssuesController < ApplicationController
|
|||||||
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
|
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
|
||||||
saved = false
|
saved = false
|
||||||
begin
|
begin
|
||||||
saved = @issue.save_issue_with_child_records(params, @time_entry)
|
saved = save_issue_with_child_records
|
||||||
rescue ActiveRecord::StaleObjectError
|
rescue ActiveRecord::StaleObjectError
|
||||||
@conflict = true
|
@conflict = true
|
||||||
if params[:last_journal_id]
|
if params[:last_journal_id]
|
||||||
@@ -228,7 +233,7 @@ class IssuesController < ApplicationController
|
|||||||
else
|
else
|
||||||
@available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
|
@available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
|
||||||
end
|
end
|
||||||
@custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
|
@custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
|
||||||
@assignables = target_projects.map(&:assignable_users).reduce(:&)
|
@assignables = target_projects.map(&:assignable_users).reduce(:&)
|
||||||
@trackers = target_projects.map(&:trackers).reduce(:&)
|
@trackers = target_projects.map(&:trackers).reduce(:&)
|
||||||
@versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
|
@versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
|
||||||
@@ -239,7 +244,9 @@ class IssuesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
|
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
|
||||||
render :layout => false if request.xhr?
|
|
||||||
|
@issue_params = params[:issue] || {}
|
||||||
|
@issue_params[:custom_field_values] ||= {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def bulk_update
|
def bulk_update
|
||||||
@@ -247,8 +254,8 @@ class IssuesController < ApplicationController
|
|||||||
@copy = params[:copy].present?
|
@copy = params[:copy].present?
|
||||||
attributes = parse_params_for_bulk_issue_attributes(params)
|
attributes = parse_params_for_bulk_issue_attributes(params)
|
||||||
|
|
||||||
unsaved_issue_ids = []
|
unsaved_issues = []
|
||||||
moved_issues = []
|
saved_issues = []
|
||||||
|
|
||||||
if @copy && params[:copy_subtasks].present?
|
if @copy && params[:copy_subtasks].present?
|
||||||
# Descendant issues will be copied with the parent task
|
# Descendant issues will be copied with the parent task
|
||||||
@@ -256,39 +263,48 @@ class IssuesController < ApplicationController
|
|||||||
@issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
|
@issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@issues.each do |issue|
|
@issues.each do |orig_issue|
|
||||||
issue.reload
|
orig_issue.reload
|
||||||
if @copy
|
if @copy
|
||||||
issue = issue.copy({},
|
issue = orig_issue.copy({},
|
||||||
:attachments => params[:copy_attachments].present?,
|
:attachments => params[:copy_attachments].present?,
|
||||||
:subtasks => params[:copy_subtasks].present?
|
:subtasks => params[:copy_subtasks].present?
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
issue = orig_issue
|
||||||
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
|
||||||
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
|
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
|
||||||
if issue.save
|
if issue.save
|
||||||
moved_issues << issue
|
saved_issues << issue
|
||||||
else
|
else
|
||||||
# Keep unsaved issue ids to display them in flash error
|
unsaved_issues << orig_issue
|
||||||
unsaved_issue_ids << issue.id
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
|
|
||||||
|
|
||||||
if params[:follow]
|
if unsaved_issues.empty?
|
||||||
if @issues.size == 1 && moved_issues.size == 1
|
flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
|
||||||
redirect_to issue_path(moved_issues.first)
|
if params[:follow]
|
||||||
elsif moved_issues.map(&:project).uniq.size == 1
|
if @issues.size == 1 && saved_issues.size == 1
|
||||||
redirect_to project_issues_path(moved_issues.map(&:project).first)
|
redirect_to issue_path(saved_issues.first)
|
||||||
|
elsif saved_issues.map(&:project).uniq.size == 1
|
||||||
|
redirect_to project_issues_path(saved_issues.map(&:project).first)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
redirect_back_or_default _project_issues_path(@project)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
redirect_back_or_default _project_issues_path(@project)
|
@saved_issues = @issues
|
||||||
|
@unsaved_issues = unsaved_issues
|
||||||
|
@issues = Issue.visible.find_all_by_id(@unsaved_issues.map(&:id))
|
||||||
|
bulk_edit
|
||||||
|
render :action => 'bulk_edit'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
|
@hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
|
||||||
if @hours > 0
|
if @hours > 0
|
||||||
case params[:todo]
|
case params[:todo]
|
||||||
when 'destroy'
|
when 'destroy'
|
||||||
@@ -436,4 +452,26 @@ class IssuesController < ApplicationController
|
|||||||
end
|
end
|
||||||
attributes
|
attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Saves @issue and a time_entry from the parameters
|
||||||
|
def save_issue_with_child_records
|
||||||
|
Issue.transaction do
|
||||||
|
if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
|
||||||
|
time_entry = @time_entry || TimeEntry.new
|
||||||
|
time_entry.project = @issue.project
|
||||||
|
time_entry.issue = @issue
|
||||||
|
time_entry.user = User.current
|
||||||
|
time_entry.spent_on = User.current.today
|
||||||
|
time_entry.attributes = params[:time_entry]
|
||||||
|
@issue.time_entries << time_entry
|
||||||
|
end
|
||||||
|
|
||||||
|
call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
|
||||||
|
if @issue.save
|
||||||
|
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
|
||||||
|
else
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class MessagesController < ApplicationController
|
|||||||
page = params[:page]
|
page = params[:page]
|
||||||
# Find the page of the requested reply
|
# Find the page of the requested reply
|
||||||
if params[:r] && page.nil?
|
if params[:r] && page.nil?
|
||||||
offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
|
offset = @topic.children.where("#{Message.table_name}.id < ?", params[:r].to_i).count
|
||||||
page = 1 + offset / REPLIES_PER_PAGE
|
page = 1 + offset / REPLIES_PER_PAGE
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
class MyController < ApplicationController
|
class MyController < ApplicationController
|
||||||
before_filter :require_login
|
before_filter :require_login
|
||||||
|
# let user change user's password when user has to
|
||||||
|
skip_before_filter :check_password_change, :only => :password
|
||||||
|
|
||||||
helper :issues
|
helper :issues
|
||||||
helper :users
|
helper :users
|
||||||
@@ -53,10 +55,8 @@ class MyController < ApplicationController
|
|||||||
if request.post?
|
if request.post?
|
||||||
@user.safe_attributes = params[:user]
|
@user.safe_attributes = params[:user]
|
||||||
@user.pref.attributes = params[:pref]
|
@user.pref.attributes = params[:pref]
|
||||||
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
|
|
||||||
if @user.save
|
if @user.save
|
||||||
@user.pref.save
|
@user.pref.save
|
||||||
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
|
|
||||||
set_language_if_valid @user.language
|
set_language_if_valid @user.language
|
||||||
flash[:notice] = l(:notice_account_updated)
|
flash[:notice] = l(:notice_account_updated)
|
||||||
redirect_to my_account_path
|
redirect_to my_account_path
|
||||||
@@ -92,14 +92,17 @@ class MyController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if request.post?
|
if request.post?
|
||||||
if @user.check_password?(params[:password])
|
if !@user.check_password?(params[:password])
|
||||||
|
flash.now[:error] = l(:notice_account_wrong_password)
|
||||||
|
elsif params[:password] == params[:new_password]
|
||||||
|
flash.now[:error] = l(:notice_new_password_must_be_different)
|
||||||
|
else
|
||||||
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
|
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
|
||||||
|
@user.must_change_passwd = false
|
||||||
if @user.save
|
if @user.save
|
||||||
flash[:notice] = l(:notice_account_password_updated)
|
flash[:notice] = l(:notice_account_password_updated)
|
||||||
redirect_to my_account_path
|
redirect_to my_account_path
|
||||||
end
|
end
|
||||||
else
|
|
||||||
flash[:error] = l(:notice_account_wrong_password)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class ProjectsController < ApplicationController
|
|||||||
|
|
||||||
if validate_parent_id && @project.save
|
if validate_parent_id && @project.save
|
||||||
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
|
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
|
||||||
# Add current user as a project member if he is not admin
|
# Add current user as a project member if current user is not admin
|
||||||
unless User.current.admin?
|
unless User.current.admin?
|
||||||
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
|
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
|
||||||
m = Member.new(:user => User.current, :roles => [r])
|
m = Member.new(:user => User.current, :roles => [r])
|
||||||
@@ -155,7 +155,7 @@ class ProjectsController < ApplicationController
|
|||||||
@total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
|
@total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
|
||||||
|
|
||||||
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.where(cond).sum(:hours).to_f
|
||||||
end
|
end
|
||||||
|
|
||||||
@key = User.current.rss_key
|
@key = User.current.rss_key
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class QueriesController < ApplicationController
|
|||||||
@query = IssueQuery.new
|
@query = IssueQuery.new
|
||||||
@query.user = User.current
|
@query.user = User.current
|
||||||
@query.project = @project
|
@query.project = @project
|
||||||
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
|
@query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
|
||||||
@query.build_from_params(params)
|
@query.build_from_params(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -53,13 +53,13 @@ class QueriesController < ApplicationController
|
|||||||
@query = IssueQuery.new(params[:query])
|
@query = IssueQuery.new(params[:query])
|
||||||
@query.user = User.current
|
@query.user = User.current
|
||||||
@query.project = params[:query_is_for_all] ? nil : @project
|
@query.project = params[:query_is_for_all] ? nil : @project
|
||||||
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
|
@query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
|
||||||
@query.build_from_params(params)
|
@query.build_from_params(params)
|
||||||
@query.column_names = nil if params[:default_columns]
|
@query.column_names = nil if params[:default_columns]
|
||||||
|
|
||||||
if @query.save
|
if @query.save
|
||||||
flash[:notice] = l(:notice_successful_create)
|
flash[:notice] = l(:notice_successful_create)
|
||||||
redirect_to _project_issues_path(@project, :query_id => @query)
|
redirect_to_issues(:query_id => @query)
|
||||||
else
|
else
|
||||||
render :action => 'new', :layout => !request.xhr?
|
render :action => 'new', :layout => !request.xhr?
|
||||||
end
|
end
|
||||||
@@ -71,13 +71,13 @@ class QueriesController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@query.attributes = params[:query]
|
@query.attributes = params[:query]
|
||||||
@query.project = nil if params[:query_is_for_all]
|
@query.project = nil if params[:query_is_for_all]
|
||||||
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
|
@query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
|
||||||
@query.build_from_params(params)
|
@query.build_from_params(params)
|
||||||
@query.column_names = nil if params[:default_columns]
|
@query.column_names = nil if params[:default_columns]
|
||||||
|
|
||||||
if @query.save
|
if @query.save
|
||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to _project_issues_path(@project, :query_id => @query)
|
redirect_to_issues(:query_id => @query)
|
||||||
else
|
else
|
||||||
render :action => 'edit'
|
render :action => 'edit'
|
||||||
end
|
end
|
||||||
@@ -85,7 +85,7 @@ class QueriesController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@query.destroy
|
@query.destroy
|
||||||
redirect_to _project_issues_path(@project, :set_filter => 1)
|
redirect_to_issues(:set_filter => 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -103,4 +103,16 @@ private
|
|||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def redirect_to_issues(options)
|
||||||
|
if params[:gantt]
|
||||||
|
if @project
|
||||||
|
redirect_to project_gantt_path(@project, options)
|
||||||
|
else
|
||||||
|
redirect_to issues_gantt_path(options)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
redirect_to _project_issues_path(@project, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class RepositoriesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
|
@repository.fetch_changesets if @project.active? && Setting.autofetch_changesets? && @path.empty?
|
||||||
|
|
||||||
@entries = @repository.entries(@path, @rev)
|
@entries = @repository.entries(@path, @rev)
|
||||||
@changeset = @repository.find_changeset_by_name(@rev)
|
@changeset = @repository.find_changeset_by_name(@rev)
|
||||||
@@ -352,15 +352,18 @@ class RepositoriesController < ApplicationController
|
|||||||
@date_to = Date.today
|
@date_to = Date.today
|
||||||
@date_from = @date_to << 11
|
@date_from = @date_to << 11
|
||||||
@date_from = Date.civil(@date_from.year, @date_from.month, 1)
|
@date_from = Date.civil(@date_from.year, @date_from.month, 1)
|
||||||
commits_by_day = Changeset.count(
|
commits_by_day = Changeset.
|
||||||
:all, :group => :commit_date,
|
where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
|
||||||
:conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
|
group(:commit_date).
|
||||||
|
count
|
||||||
commits_by_month = [0] * 12
|
commits_by_month = [0] * 12
|
||||||
commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
|
commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
|
||||||
|
|
||||||
changes_by_day = Change.count(
|
changes_by_day = Change.
|
||||||
:all, :group => :commit_date, :include => :changeset,
|
joins(:changeset).
|
||||||
:conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
|
where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
|
||||||
|
group(:commit_date).
|
||||||
|
count
|
||||||
changes_by_month = [0] * 12
|
changes_by_month = [0] * 12
|
||||||
changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
|
changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
|
||||||
|
|
||||||
@@ -393,10 +396,10 @@ class RepositoriesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def graph_commits_per_author(repository)
|
def graph_commits_per_author(repository)
|
||||||
commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id])
|
commits_by_author = Changeset.where("repository_id = ?", repository.id).group(:committer).count
|
||||||
commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
|
commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
|
||||||
|
|
||||||
changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id])
|
changes_by_author = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", repository.id).group(:committer).count
|
||||||
h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
|
h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
|
||||||
|
|
||||||
fields = commits_by_author.collect {|r| r.first}
|
fields = commits_by_author.collect {|r| r.first}
|
||||||
@@ -411,7 +414,7 @@ class RepositoriesController < ApplicationController
|
|||||||
fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
|
fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
|
||||||
|
|
||||||
graph = SVG::Graph::BarHorizontal.new(
|
graph = SVG::Graph::BarHorizontal.new(
|
||||||
:height => 400,
|
:height => 30 * commits_data.length,
|
||||||
:width => 800,
|
:width => 800,
|
||||||
:fields => fields,
|
:fields => fields,
|
||||||
:stack => :side,
|
:stack => :side,
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ class SettingsController < ApplicationController
|
|||||||
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
|
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
|
||||||
settings = (params[:settings] || {}).dup.symbolize_keys
|
settings = (params[:settings] || {}).dup.symbolize_keys
|
||||||
settings.each do |name, value|
|
settings.each do |name, value|
|
||||||
# remove blank values in array settings
|
Setting.set_from_params name, value
|
||||||
value.delete_if {|v| v.blank? } if value.is_a?(Array)
|
|
||||||
Setting[name] = value
|
|
||||||
end
|
end
|
||||||
flash[:notice] = l(:notice_successful_update)
|
flash[:notice] = l(:notice_successful_update)
|
||||||
redirect_to settings_path(:tab => params[:tab])
|
redirect_to settings_path(:tab => params[:tab])
|
||||||
@@ -48,6 +46,9 @@ class SettingsController < ApplicationController
|
|||||||
@guessed_host_and_path = request.host_with_port.dup
|
@guessed_host_and_path = request.host_with_port.dup
|
||||||
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
|
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
|
||||||
|
|
||||||
|
@commit_update_keywords = Setting.commit_update_keywords.dup
|
||||||
|
@commit_update_keywords = [{}] unless @commit_update_keywords.is_a?(Array) && @commit_update_keywords.any?
|
||||||
|
|
||||||
Redmine::Themes.rescan
|
Redmine::Themes.rescan
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ class SysController < ActionController::Base
|
|||||||
before_filter :check_enabled
|
before_filter :check_enabled
|
||||||
|
|
||||||
def projects
|
def projects
|
||||||
p = Project.active.has_module(:repository).find(
|
p = Project.active.has_module(:repository).order("#{Project.table_name}.identifier").preload(:repository).all
|
||||||
:all,
|
|
||||||
:include => :repository,
|
|
||||||
:order => "#{Project.table_name}.identifier"
|
|
||||||
)
|
|
||||||
# extra_info attribute from repository breaks activeresource client
|
# extra_info attribute from repository breaks activeresource client
|
||||||
render :xml => p.to_xml(
|
render :xml => p.to_xml(
|
||||||
:only => [:id, :identifier, :name, :is_public, :status],
|
:only => [:id, :identifier, :name, :is_public, :status],
|
||||||
|
|||||||
@@ -43,22 +43,18 @@ class TimelogController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
|
@query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
|
||||||
scope = time_entry_scope
|
|
||||||
|
|
||||||
sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
|
sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
|
||||||
sort_update(@query.sortable_columns)
|
sort_update(@query.sortable_columns)
|
||||||
|
scope = time_entry_scope(:order => sort_clause).
|
||||||
|
includes(:project, :activity, :user, :issue).
|
||||||
|
preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
# Paginate results
|
|
||||||
@entry_count = scope.count
|
@entry_count = scope.count
|
||||||
@entry_pages = Paginator.new @entry_count, per_page_option, params['page']
|
@entry_pages = Paginator.new @entry_count, per_page_option, params['page']
|
||||||
@entries = scope.all(
|
@entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).all
|
||||||
:include => [:project, :activity, :user, {:issue => :tracker}],
|
|
||||||
:order => sort_clause,
|
|
||||||
:limit => @entry_pages.per_page,
|
|
||||||
:offset => @entry_pages.offset
|
|
||||||
)
|
|
||||||
@total_hours = scope.sum(:hours).to_f
|
@total_hours = scope.sum(:hours).to_f
|
||||||
|
|
||||||
render :layout => !request.xhr?
|
render :layout => !request.xhr?
|
||||||
@@ -66,27 +62,15 @@ class TimelogController < ApplicationController
|
|||||||
format.api {
|
format.api {
|
||||||
@entry_count = scope.count
|
@entry_count = scope.count
|
||||||
@offset, @limit = api_offset_and_limit
|
@offset, @limit = api_offset_and_limit
|
||||||
@entries = scope.all(
|
@entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).all
|
||||||
:include => [:project, :activity, :user, {:issue => :tracker}],
|
|
||||||
:order => sort_clause,
|
|
||||||
:limit => @limit,
|
|
||||||
:offset => @offset
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
format.atom {
|
format.atom {
|
||||||
entries = scope.all(
|
entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").all
|
||||||
:include => [:project, :activity, :user, {:issue => :tracker}],
|
|
||||||
:order => "#{TimeEntry.table_name}.created_on DESC",
|
|
||||||
:limit => Setting.feeds_limit.to_i
|
|
||||||
)
|
|
||||||
render_feed(entries, :title => l(:label_spent_time))
|
render_feed(entries, :title => l(:label_spent_time))
|
||||||
}
|
}
|
||||||
format.csv {
|
format.csv {
|
||||||
# Export all entries
|
# Export all entries
|
||||||
@entries = scope.all(
|
@entries = scope.all
|
||||||
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
|
|
||||||
:order => sort_clause
|
|
||||||
)
|
|
||||||
send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
|
send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -198,6 +182,7 @@ class TimelogController < ApplicationController
|
|||||||
time_entry.safe_attributes = attributes
|
time_entry.safe_attributes = attributes
|
||||||
call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
|
call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
|
||||||
unless time_entry.save
|
unless time_entry.save
|
||||||
|
logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info
|
||||||
# Keep unsaved time_entry ids to display them in flash error
|
# Keep unsaved time_entry ids to display them in flash error
|
||||||
unsaved_time_entry_ids << time_entry.id
|
unsaved_time_entry_ids << time_entry.id
|
||||||
end
|
end
|
||||||
@@ -295,12 +280,10 @@ private
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Returns the TimeEntry scope for index and report actions
|
# Returns the TimeEntry scope for index and report actions
|
||||||
def time_entry_scope
|
def time_entry_scope(options={})
|
||||||
scope = TimeEntry.visible.where(@query.statement)
|
scope = @query.results_scope(options)
|
||||||
if @issue
|
if @issue
|
||||||
scope = scope.on_issue(@issue)
|
scope = scope.on_issue(@issue)
|
||||||
elsif @project
|
|
||||||
scope = scope.on_project(@project, Setting.display_subprojects_issues?)
|
|
||||||
end
|
end
|
||||||
scope
|
scope
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
# show projects based on current user visibility
|
# show projects based on current user visibility
|
||||||
@memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
|
@memberships = @user.memberships.where(Project.visible_condition(User.current)).all
|
||||||
|
|
||||||
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
|
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
|
||||||
@events_by_day = events.group_by(&:event_date)
|
@events_by_day = events.group_by(&:event_date)
|
||||||
@@ -80,6 +80,7 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
def new
|
def new
|
||||||
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
|
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
|
||||||
|
@user.safe_attributes = params[:user]
|
||||||
@auth_sources = AuthSource.all
|
@auth_sources = AuthSource.all
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -92,17 +93,16 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
@user.pref.attributes = params[:pref]
|
@user.pref.attributes = params[:pref]
|
||||||
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
|
|
||||||
@user.pref.save
|
@user.pref.save
|
||||||
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
|
|
||||||
|
|
||||||
Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
|
Mailer.account_information(@user, @user.password).deliver if params[:send_information]
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html {
|
format.html {
|
||||||
flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
|
flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
|
||||||
if params[:continue]
|
if params[:continue]
|
||||||
redirect_to new_user_path
|
attrs = params[:user].slice(:generate_password)
|
||||||
|
redirect_to new_user_path(:user => attrs)
|
||||||
else
|
else
|
||||||
redirect_to edit_user_path(@user)
|
redirect_to edit_user_path(@user)
|
||||||
end
|
end
|
||||||
@@ -137,16 +137,14 @@ class UsersController < ApplicationController
|
|||||||
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
|
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
|
||||||
# TODO: Similar to My#account
|
# TODO: Similar to My#account
|
||||||
@user.pref.attributes = params[:pref]
|
@user.pref.attributes = params[:pref]
|
||||||
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
|
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
@user.pref.save
|
@user.pref.save
|
||||||
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
|
|
||||||
|
|
||||||
if was_activated
|
if was_activated
|
||||||
Mailer.account_activated(@user).deliver
|
Mailer.account_activated(@user).deliver
|
||||||
elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
|
elsif @user.active? && params[:send_information] && @user.password.present? && @user.auth_source_id.nil?
|
||||||
Mailer.account_information(@user, params[:user][:password]).deliver
|
Mailer.account_information(@user, @user.password).deliver
|
||||||
end
|
end
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ class VersionsController < ApplicationController
|
|||||||
|
|
||||||
@issues_by_version = {}
|
@issues_by_version = {}
|
||||||
if @selected_tracker_ids.any? && @versions.any?
|
if @selected_tracker_ids.any? && @versions.any?
|
||||||
issues = Issue.visible.all(
|
issues = Issue.visible.
|
||||||
:include => [:project, :status, :tracker, :priority, :fixed_version],
|
includes(:project, :tracker).
|
||||||
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)},
|
preload(:status, :priority, :fixed_version).
|
||||||
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id"
|
where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)).
|
||||||
)
|
order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
|
||||||
@issues_by_version = issues.group_by(&:fixed_version)
|
@issues_by_version = issues.group_by(&:fixed_version)
|
||||||
end
|
end
|
||||||
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
|
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
|
||||||
|
|||||||
@@ -15,8 +15,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 'diff'
|
|
||||||
|
|
||||||
# The WikiController follows the Rails REST controller pattern but with
|
# The WikiController follows the Rails REST controller pattern but with
|
||||||
# a few differences
|
# a few differences
|
||||||
#
|
#
|
||||||
@@ -64,7 +62,12 @@ 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 params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
|
||||||
|
deny_access
|
||||||
|
return
|
||||||
|
end
|
||||||
|
@content = @page.content_for_version(params[:version])
|
||||||
|
if @content.nil?
|
||||||
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
|
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
|
||||||
edit
|
edit
|
||||||
render :action => 'edit'
|
render :action => 'edit'
|
||||||
@@ -73,11 +76,6 @@ class WikiController < ApplicationController
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
|
|
||||||
deny_access
|
|
||||||
return
|
|
||||||
end
|
|
||||||
@content = @page.content_for_version(params[:version])
|
|
||||||
if User.current.allowed_to?(:export_wiki_pages, @project)
|
if User.current.allowed_to?(:export_wiki_pages, @project)
|
||||||
if params[:format] == 'pdf'
|
if params[:format] == 'pdf'
|
||||||
send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
|
send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
|
||||||
@@ -106,19 +104,19 @@ class WikiController < ApplicationController
|
|||||||
def edit
|
def edit
|
||||||
return render_403 unless editable?
|
return render_403 unless editable?
|
||||||
if @page.new_record?
|
if @page.new_record?
|
||||||
@page.content = WikiContent.new(:page => @page)
|
|
||||||
if params[:parent].present?
|
if params[:parent].present?
|
||||||
@page.parent = @page.wiki.find_page(params[:parent].to_s)
|
@page.parent = @page.wiki.find_page(params[:parent].to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@content = @page.content_for_version(params[:version])
|
@content = @page.content_for_version(params[:version])
|
||||||
|
@content ||= WikiContent.new(:page => @page)
|
||||||
@content.text = initial_page_content(@page) if @content.text.blank?
|
@content.text = initial_page_content(@page) if @content.text.blank?
|
||||||
# don't keep previous comment
|
# don't keep previous comment
|
||||||
@content.comments = nil
|
@content.comments = nil
|
||||||
|
|
||||||
# To prevent StaleObjectError exception when reverting to a previous version
|
# To prevent StaleObjectError exception when reverting to a previous version
|
||||||
@content.version = @page.content.version
|
@content.version = @page.content.version if @page.content
|
||||||
|
|
||||||
@text = @content.text
|
@text = @content.text
|
||||||
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
|
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
|
||||||
@@ -132,10 +130,9 @@ class WikiController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return render_403 unless editable?
|
return render_403 unless editable?
|
||||||
was_new_page = @page.new_record?
|
was_new_page = @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
|
@content = @page.content || WikiContent.new(:page => @page)
|
||||||
content_params = params[:content]
|
content_params = params[:content]
|
||||||
if content_params.nil? && params[:wiki_page].is_a?(Hash)
|
if content_params.nil? && params[:wiki_page].is_a?(Hash)
|
||||||
content_params = params[:wiki_page].slice(:text, :comments, :version)
|
content_params = params[:wiki_page].slice(:text, :comments, :version)
|
||||||
@@ -147,20 +144,23 @@ class WikiController < ApplicationController
|
|||||||
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(@section, @text, @section_hash)
|
||||||
else
|
else
|
||||||
@content.version = content_params[:version] if content_params[: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
|
||||||
|
|
||||||
if @page.save_with_content
|
if @page.save_with_content(@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})
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to project_wiki_page_path(@project, @page.title) }
|
format.html {
|
||||||
|
anchor = @section ? "section-#{@section}" : nil
|
||||||
|
redirect_to project_wiki_page_path(@project, @page.title, :anchor => anchor)
|
||||||
|
}
|
||||||
format.api {
|
format.api {
|
||||||
if was_new_page
|
if was_new_page
|
||||||
render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
|
render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
groups = ''
|
groups = ''
|
||||||
collection.sort.each do |element|
|
collection.sort.each do |element|
|
||||||
selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
|
selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
|
||||||
(element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
|
(element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
|
||||||
end
|
end
|
||||||
unless groups.empty?
|
unless groups.empty?
|
||||||
@@ -343,11 +343,15 @@ module ApplicationHelper
|
|||||||
def options_for_membership_project_select(principal, projects)
|
def options_for_membership_project_select(principal, projects)
|
||||||
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
|
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
|
||||||
options << project_tree_options_for_select(projects) do |p|
|
options << project_tree_options_for_select(projects) do |p|
|
||||||
{:disabled => principal.projects.include?(p)}
|
{:disabled => principal.projects.to_a.include?(p)}
|
||||||
end
|
end
|
||||||
options
|
options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def option_tag(name, text, value, selected=nil, options={})
|
||||||
|
content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
|
||||||
|
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, ' ')
|
||||||
@@ -380,7 +384,7 @@ module ApplicationHelper
|
|||||||
if @project
|
if @project
|
||||||
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :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('abbr', text, :title => format_time(time))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -445,12 +449,31 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns a h2 tag and sets the html title with the given arguments
|
||||||
|
def title(*args)
|
||||||
|
strings = args.map do |arg|
|
||||||
|
if arg.is_a?(Array) && arg.size >= 2
|
||||||
|
link_to(*arg)
|
||||||
|
else
|
||||||
|
h(arg.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
|
||||||
|
content_tag('h2', strings.join(' » ').html_safe)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sets the html title
|
||||||
|
# Returns the html title when called without arguments
|
||||||
|
# Current project name and app_title and automatically appended
|
||||||
|
# Exemples:
|
||||||
|
# html_title 'Foo', 'Bar'
|
||||||
|
# html_title # => 'Foo - Bar - My Project - Redmine'
|
||||||
def html_title(*args)
|
def html_title(*args)
|
||||||
if args.empty?
|
if args.empty?
|
||||||
title = @html_title || []
|
title = @html_title || []
|
||||||
title << @project.name if @project
|
title << @project.name if @project
|
||||||
title << Setting.app_title unless Setting.app_title == title.last
|
title << Setting.app_title unless Setting.app_title == title.last
|
||||||
title.select {|t| !t.blank? }.join(' - ')
|
title.reject(&:blank?).join(' - ')
|
||||||
else
|
else
|
||||||
@html_title ||= []
|
@html_title ||= []
|
||||||
@html_title += args
|
@html_title += args
|
||||||
@@ -465,13 +488,18 @@ module ApplicationHelper
|
|||||||
css << 'theme-' + theme.name
|
css << 'theme-' + theme.name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
css << 'project-' + @project.identifier if @project && @project.identifier.present?
|
||||||
css << 'controller-' + controller_name
|
css << 'controller-' + controller_name
|
||||||
css << 'action-' + action_name
|
css << 'action-' + action_name
|
||||||
css.join(' ')
|
css.join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
def accesskey(s)
|
def accesskey(s)
|
||||||
Redmine::AccessKeys.key_for s
|
@used_accesskeys ||= []
|
||||||
|
key = Redmine::AccessKeys.key_for(s)
|
||||||
|
return nil if @used_accesskeys.include?(key)
|
||||||
|
@used_accesskeys << key
|
||||||
|
key
|
||||||
end
|
end
|
||||||
|
|
||||||
# Formats text according to system settings.
|
# Formats text according to system settings.
|
||||||
@@ -611,7 +639,7 @@ module ApplicationHelper
|
|||||||
else
|
else
|
||||||
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, :version => nil, :anchor => anchor, :parent => parent)
|
:id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -652,6 +680,9 @@ module ApplicationHelper
|
|||||||
# export:some/file -> Force the download of the file
|
# export:some/file -> Force the download of the file
|
||||||
# Forum messages:
|
# Forum messages:
|
||||||
# message#1218 -> Link to message with id 1218
|
# message#1218 -> Link to message with id 1218
|
||||||
|
# Projects:
|
||||||
|
# project:someproject -> Link to project named "someproject"
|
||||||
|
# project#3 -> Link to project with id 3
|
||||||
#
|
#
|
||||||
# Links can refer other objects from other projects, using project identifier:
|
# Links can refer other objects from other projects, using project identifier:
|
||||||
# identifier:r52
|
# identifier:r52
|
||||||
@@ -688,7 +719,7 @@ module ApplicationHelper
|
|||||||
when nil
|
when nil
|
||||||
if oid.to_s == identifier && 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(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
|
||||||
:class => issue.css_classes,
|
:class => issue.css_classes,
|
||||||
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
|
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
|
||||||
end
|
end
|
||||||
@@ -758,7 +789,7 @@ module ApplicationHelper
|
|||||||
if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
|
if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
|
||||||
link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
|
link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
|
||||||
:class => 'changeset',
|
:class => 'changeset',
|
||||||
:title => truncate_single_line(h(changeset.comments), :length => 100)
|
:title => truncate_single_line(changeset.comments, :length => 100)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if repository && User.current.allowed_to?(:browse_repository, project)
|
if repository && User.current.allowed_to?(:browse_repository, project)
|
||||||
@@ -800,7 +831,8 @@ module ApplicationHelper
|
|||||||
content_tag('div',
|
content_tag('div',
|
||||||
link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
|
link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
|
||||||
:class => 'contextual',
|
:class => 'contextual',
|
||||||
:title => l(:button_edit_section)) + heading.html_safe
|
:title => l(:button_edit_section),
|
||||||
|
:id => "section-#{@current_section}") + heading.html_safe
|
||||||
else
|
else
|
||||||
heading
|
heading
|
||||||
end
|
end
|
||||||
@@ -971,7 +1003,7 @@ module ApplicationHelper
|
|||||||
html << "</ul></div>\n"
|
html << "</ul></div>\n"
|
||||||
end
|
end
|
||||||
html.html_safe
|
html.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_link(url, options={})
|
def delete_link(url, options={})
|
||||||
options = {
|
options = {
|
||||||
@@ -985,8 +1017,8 @@ module ApplicationHelper
|
|||||||
|
|
||||||
def preview_link(url, form, target='preview', options={})
|
def preview_link(url, form, target='preview', options={})
|
||||||
content_tag 'a', l(:label_preview), {
|
content_tag 'a', l(:label_preview), {
|
||||||
:href => "#",
|
:href => "#",
|
||||||
:onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
|
:onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
|
||||||
:accesskey => accesskey(:preview)
|
:accesskey => accesskey(:preview)
|
||||||
}.merge(options)
|
}.merge(options)
|
||||||
end
|
end
|
||||||
@@ -1031,7 +1063,7 @@ module ApplicationHelper
|
|||||||
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
|
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
|
||||||
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
|
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
|
||||||
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
|
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
|
||||||
), :class => 'progress', :style => "width: #{width};").html_safe +
|
), :class => 'progress progress-#{pcts[0]}', :style => "width: #{width};").html_safe +
|
||||||
content_tag('p', legend, :class => 'percent').html_safe
|
content_tag('p', legend, :class => 'percent').html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1064,6 +1096,7 @@ module ApplicationHelper
|
|||||||
|
|
||||||
def include_calendar_headers_tags
|
def include_calendar_headers_tags
|
||||||
unless @calendar_headers_tags_included
|
unless @calendar_headers_tags_included
|
||||||
|
tags = javascript_include_tag("datepicker")
|
||||||
@calendar_headers_tags_included = true
|
@calendar_headers_tags_included = true
|
||||||
content_for :header_tags do
|
content_for :header_tags do
|
||||||
start_of_week = Setting.start_of_week
|
start_of_week = Setting.start_of_week
|
||||||
@@ -1071,15 +1104,16 @@ module ApplicationHelper
|
|||||||
# Redmine uses 1..7 (monday..sunday) in settings and locales
|
# Redmine uses 1..7 (monday..sunday) in settings and locales
|
||||||
# JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
|
# JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
|
||||||
start_of_week = start_of_week.to_i % 7
|
start_of_week = start_of_week.to_i % 7
|
||||||
|
tags << javascript_tag(
|
||||||
tags = javascript_tag(
|
|
||||||
"var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
|
"var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
|
||||||
"showOn: 'button', buttonImageOnly: true, buttonImage: '" +
|
"showOn: 'button', buttonImageOnly: true, buttonImage: '" +
|
||||||
path_to_image('/images/calendar.png') +
|
path_to_image('/images/calendar.png') +
|
||||||
"', showButtonPanel: true, showWeek: true, showOtherMonths: true, selectOtherMonths: true};")
|
"', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
|
||||||
|
"selectOtherMonths: true, changeMonth: true, changeYear: true, " +
|
||||||
|
"beforeShow: beforeShowDatePicker};")
|
||||||
jquery_locale = l('jquery.locale', :default => current_language.to_s)
|
jquery_locale = l('jquery.locale', :default => current_language.to_s)
|
||||||
unless jquery_locale == 'en'
|
unless jquery_locale == 'en'
|
||||||
tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
|
tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
|
||||||
end
|
end
|
||||||
tags
|
tags
|
||||||
end
|
end
|
||||||
@@ -1180,7 +1214,7 @@ module ApplicationHelper
|
|||||||
|
|
||||||
def sanitize_anchor_name(anchor)
|
def sanitize_anchor_name(anchor)
|
||||||
if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
|
if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
|
||||||
anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
||||||
else
|
else
|
||||||
# TODO: remove when ruby1.8 is no longer supported
|
# TODO: remove when ruby1.8 is no longer supported
|
||||||
anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
|
||||||
|
|||||||
@@ -19,8 +19,29 @@
|
|||||||
|
|
||||||
module CustomFieldsHelper
|
module CustomFieldsHelper
|
||||||
|
|
||||||
|
CUSTOM_FIELDS_TABS = [
|
||||||
|
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => :label_issue_plural},
|
||||||
|
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => :label_spent_time},
|
||||||
|
{:name => 'ProjectCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => :label_project_plural},
|
||||||
|
{:name => 'VersionCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => :label_version_plural},
|
||||||
|
{:name => 'UserCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => :label_user_plural},
|
||||||
|
{:name => 'GroupCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => :label_group_plural},
|
||||||
|
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => TimeEntryActivity::OptionName},
|
||||||
|
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => IssuePriority::OptionName},
|
||||||
|
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
|
||||||
|
:label => DocumentCategory::OptionName}
|
||||||
|
]
|
||||||
|
|
||||||
def custom_fields_tabs
|
def custom_fields_tabs
|
||||||
CustomField::CUSTOM_FIELDS_TABS
|
CUSTOM_FIELDS_TABS
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return custom field html tag corresponding to its format
|
# Return custom field html tag corresponding to its format
|
||||||
@@ -77,32 +98,44 @@ module CustomFieldsHelper
|
|||||||
custom_field_label_tag(name, custom_value, options) + 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, value='')
|
||||||
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
|
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
|
||||||
field_name << "[]" if custom_field.multiple?
|
field_name << "[]" if custom_field.multiple?
|
||||||
field_id = "#{name}_custom_field_values_#{custom_field.id}"
|
field_id = "#{name}_custom_field_values_#{custom_field.id}"
|
||||||
|
|
||||||
tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
|
tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
|
||||||
|
|
||||||
|
unset_tag = ''
|
||||||
|
unless custom_field.is_required?
|
||||||
|
unset_tag = content_tag('label',
|
||||||
|
check_box_tag(field_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{field_id}"}) + l(:button_clear),
|
||||||
|
:class => 'inline'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
|
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
|
||||||
case field_format.try(:edit_as)
|
case field_format.try(:edit_as)
|
||||||
when "date"
|
when "date"
|
||||||
text_field_tag(field_name, '', tag_options.merge(:size => 10)) +
|
text_field_tag(field_name, value, tag_options.merge(:size => 10)) +
|
||||||
calendar_for(field_id)
|
calendar_for(field_id) +
|
||||||
|
unset_tag
|
||||||
when "text"
|
when "text"
|
||||||
text_area_tag(field_name, '', tag_options.merge(:rows => 3))
|
text_area_tag(field_name, value, tag_options.merge(:rows => 3)) +
|
||||||
|
'<br />'.html_safe +
|
||||||
|
unset_tag
|
||||||
when "bool"
|
when "bool"
|
||||||
select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
|
select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
|
||||||
[l(:general_text_yes), '1'],
|
[l(:general_text_yes), '1'],
|
||||||
[l(:general_text_no), '0']]), tag_options)
|
[l(:general_text_no), '0']], value), tag_options)
|
||||||
when "list"
|
when "list"
|
||||||
options = []
|
options = []
|
||||||
options << [l(:label_no_change_option), ''] unless custom_field.multiple?
|
options << [l(:label_no_change_option), ''] unless custom_field.multiple?
|
||||||
options << [l(:label_none), '__none__'] unless custom_field.is_required?
|
options << [l(:label_none), '__none__'] unless custom_field.is_required?
|
||||||
options += custom_field.possible_values_options(projects)
|
options += custom_field.possible_values_options(projects)
|
||||||
select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?))
|
select_tag(field_name, options_for_select(options, value), tag_options.merge(:multiple => custom_field.multiple?))
|
||||||
else
|
else
|
||||||
text_field_tag(field_name, '', tag_options)
|
text_field_tag(field_name, value, tag_options) +
|
||||||
|
unset_tag
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,20 @@ module IssuesHelper
|
|||||||
s.html_safe
|
s.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns an array of error messages for bulk edited issues
|
||||||
|
def bulk_edit_error_messages(issues)
|
||||||
|
messages = {}
|
||||||
|
issues.each do |issue|
|
||||||
|
issue.errors.full_messages.each do |message|
|
||||||
|
messages[message] ||= []
|
||||||
|
messages[message] << issue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
messages.map { |message, issues|
|
||||||
|
"#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Returns a link for adding a new subtask to the given issue
|
# Returns a link for adding a new subtask to the given issue
|
||||||
def link_to_new_subtask(issue)
|
def link_to_new_subtask(issue)
|
||||||
attrs = {
|
attrs = {
|
||||||
@@ -146,12 +160,13 @@ module IssuesHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_custom_fields_rows(issue)
|
def render_custom_fields_rows(issue)
|
||||||
return if issue.custom_field_values.empty?
|
values = issue.visible_custom_field_values
|
||||||
|
return if values.empty?
|
||||||
ordered_values = []
|
ordered_values = []
|
||||||
half = (issue.custom_field_values.size / 2.0).ceil
|
half = (values.size / 2.0).ceil
|
||||||
half.times do |i|
|
half.times do |i|
|
||||||
ordered_values << issue.custom_field_values[i]
|
ordered_values << values[i]
|
||||||
ordered_values << issue.custom_field_values[i + half]
|
ordered_values << values[i + half]
|
||||||
end
|
end
|
||||||
s = "<tr>\n"
|
s = "<tr>\n"
|
||||||
n = 0
|
n = 0
|
||||||
@@ -184,36 +199,60 @@ module IssuesHelper
|
|||||||
|
|
||||||
def sidebar_queries
|
def sidebar_queries
|
||||||
unless @sidebar_queries
|
unless @sidebar_queries
|
||||||
@sidebar_queries = IssueQuery.visible.all(
|
@sidebar_queries = IssueQuery.visible.
|
||||||
:order => "#{Query.table_name}.name ASC",
|
order("#{Query.table_name}.name ASC").
|
||||||
# Project specific queries and global queries
|
# Project specific queries and global queries
|
||||||
:conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
|
where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
|
||||||
)
|
all
|
||||||
end
|
end
|
||||||
@sidebar_queries
|
@sidebar_queries
|
||||||
end
|
end
|
||||||
|
|
||||||
def query_links(title, queries)
|
def query_links(title, queries)
|
||||||
|
return '' if queries.empty?
|
||||||
# links to #index on issues/show
|
# links to #index on issues/show
|
||||||
url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
|
url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
|
||||||
|
|
||||||
content_tag('h3', h(title)) +
|
content_tag('h3', title) + "\n" +
|
||||||
queries.collect {|query|
|
content_tag('ul',
|
||||||
css = 'query'
|
queries.collect {|query|
|
||||||
css << ' selected' if query == @query
|
css = 'query'
|
||||||
link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
|
css << ' selected' if query == @query
|
||||||
}.join('<br />').html_safe
|
content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
|
||||||
|
}.join("\n").html_safe,
|
||||||
|
:class => 'queries'
|
||||||
|
) + "\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_sidebar_queries
|
def render_sidebar_queries
|
||||||
out = ''.html_safe
|
out = ''.html_safe
|
||||||
queries = sidebar_queries.select {|q| !q.is_public?}
|
out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
|
||||||
out << query_links(l(:label_my_queries), queries) if queries.any?
|
out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
|
||||||
queries = sidebar_queries.select {|q| q.is_public?}
|
|
||||||
out << query_links(l(:label_query_plural), queries) if queries.any?
|
|
||||||
out
|
out
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_issue_attributes(issue, user)
|
||||||
|
items = []
|
||||||
|
%w(author status priority assigned_to category fixed_version).each do |attribute|
|
||||||
|
unless issue.disabled_core_fields.include?(attribute+"_id")
|
||||||
|
items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
issue.visible_custom_field_values(user).each do |value|
|
||||||
|
items << "#{value.custom_field.name}: #{show_value(value)}"
|
||||||
|
end
|
||||||
|
items
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_email_issue_attributes(issue, user, html=false)
|
||||||
|
items = email_issue_attributes(issue, user)
|
||||||
|
if html
|
||||||
|
content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
|
||||||
|
else
|
||||||
|
items.map{|s| "* #{s}"}.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the textual representation of a journal details
|
# Returns the textual representation of a journal details
|
||||||
# as an array of strings
|
# as an array of strings
|
||||||
def details_to_strings(details, no_html=false, options={})
|
def details_to_strings(details, no_html=false, options={})
|
||||||
@@ -222,23 +261,23 @@ module IssuesHelper
|
|||||||
values_by_field = {}
|
values_by_field = {}
|
||||||
details.each do |detail|
|
details.each do |detail|
|
||||||
if detail.property == 'cf'
|
if detail.property == 'cf'
|
||||||
field_id = detail.prop_key
|
field = detail.custom_field
|
||||||
field = CustomField.find_by_id(field_id)
|
|
||||||
if field && field.multiple?
|
if field && field.multiple?
|
||||||
values_by_field[field_id] ||= {:added => [], :deleted => []}
|
values_by_field[field] ||= {:added => [], :deleted => []}
|
||||||
if detail.old_value
|
if detail.old_value
|
||||||
values_by_field[field_id][:deleted] << detail.old_value
|
values_by_field[field][:deleted] << detail.old_value
|
||||||
end
|
end
|
||||||
if detail.value
|
if detail.value
|
||||||
values_by_field[field_id][:added] << detail.value
|
values_by_field[field][:added] << detail.value
|
||||||
end
|
end
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
strings << show_detail(detail, no_html, options)
|
strings << show_detail(detail, no_html, options)
|
||||||
end
|
end
|
||||||
values_by_field.each do |field_id, changes|
|
values_by_field.each do |field, changes|
|
||||||
detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
|
detail = JournalDetail.new(:property => 'cf', :prop_key => field.id.to_s)
|
||||||
|
detail.instance_variable_set "@custom_field", field
|
||||||
if changes[:added].any?
|
if changes[:added].any?
|
||||||
detail.value = changes[:added]
|
detail.value = changes[:added]
|
||||||
strings << show_detail(detail, no_html, options)
|
strings << show_detail(detail, no_html, options)
|
||||||
@@ -281,7 +320,7 @@ module IssuesHelper
|
|||||||
old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
|
old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
|
||||||
end
|
end
|
||||||
when 'cf'
|
when 'cf'
|
||||||
custom_field = CustomField.find_by_id(detail.prop_key)
|
custom_field = detail.custom_field
|
||||||
if custom_field
|
if custom_field
|
||||||
multiple = custom_field.multiple?
|
multiple = custom_field.multiple?
|
||||||
label = custom_field.name
|
label = custom_field.name
|
||||||
@@ -290,6 +329,17 @@ module IssuesHelper
|
|||||||
end
|
end
|
||||||
when 'attachment'
|
when 'attachment'
|
||||||
label = l(:label_attachment)
|
label = l(:label_attachment)
|
||||||
|
when 'relation'
|
||||||
|
if detail.value && !detail.old_value
|
||||||
|
rel_issue = Issue.visible.find_by_id(detail.value)
|
||||||
|
value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
|
||||||
|
(no_html ? rel_issue : link_to_issue(rel_issue))
|
||||||
|
elsif detail.old_value && !detail.value
|
||||||
|
rel_issue = Issue.visible.find_by_id(detail.old_value)
|
||||||
|
old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
|
||||||
|
(no_html ? rel_issue : link_to_issue(rel_issue))
|
||||||
|
end
|
||||||
|
label = l(detail.prop_key.to_sym)
|
||||||
end
|
end
|
||||||
call_hook(:helper_issues_show_detail_after_setting,
|
call_hook(:helper_issues_show_detail_after_setting,
|
||||||
{:detail => detail, :label => label, :value => value, :old_value => old_value })
|
{:detail => detail, :label => label, :value => value, :old_value => old_value })
|
||||||
@@ -301,7 +351,9 @@ 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("del", old_value) if detail.old_value and detail.value.blank?
|
if detail.old_value && detail.value.blank? && detail.property != 'relation'
|
||||||
|
old_value = content_tag("del", old_value)
|
||||||
|
end
|
||||||
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])
|
||||||
@@ -337,7 +389,7 @@ module IssuesHelper
|
|||||||
else
|
else
|
||||||
l(:text_journal_set_to, :label => label, :value => value).html_safe
|
l(:text_journal_set_to, :label => label, :value => value).html_safe
|
||||||
end
|
end
|
||||||
when 'attachment'
|
when 'attachment', 'relation'
|
||||||
l(:text_journal_added, :label => label, :value => value).html_safe
|
l(:text_journal_added, :label => label, :value => value).html_safe
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -353,7 +405,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
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ module ProjectsHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
options = ''
|
options = ''
|
||||||
options << "<option value=''></option>" if project.allowed_parents.include?(nil)
|
options << "<option value=''> </option>" if project.allowed_parents.include?(nil)
|
||||||
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
|
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
|
||||||
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
|
||||||
@@ -69,10 +69,11 @@ module ProjectsHelper
|
|||||||
grouped[version.project.name] << [version.name, version.id]
|
grouped[version.project.name] << [version.name, version.id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
selected = selected.is_a?(Version) ? selected.id : selected
|
||||||
if grouped.keys.size > 1
|
if grouped.keys.size > 1
|
||||||
grouped_options_for_select(grouped, selected && selected.id)
|
grouped_options_for_select(grouped, selected)
|
||||||
else
|
else
|
||||||
options_for_select((grouped.values.first || []), selected && selected.id)
|
options_for_select((grouped.values.first || []), selected)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,30 @@ module QueriesHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def query_filters_hidden_tags(query)
|
||||||
|
tags = ''.html_safe
|
||||||
|
query.filters.each do |field, options|
|
||||||
|
tags << hidden_field_tag("f[]", field, :id => nil)
|
||||||
|
tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
|
||||||
|
options[:values].each do |value|
|
||||||
|
tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tags
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_columns_hidden_tags(query)
|
||||||
|
tags = ''.html_safe
|
||||||
|
query.columns.each do |column|
|
||||||
|
tags << hidden_field_tag("c[]", column.name, :id => nil)
|
||||||
|
end
|
||||||
|
tags
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_hidden_tags(query)
|
||||||
|
query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
|
||||||
|
end
|
||||||
|
|
||||||
def available_block_columns_tags(query)
|
def available_block_columns_tags(query)
|
||||||
tags = ''.html_safe
|
tags = ''.html_safe
|
||||||
query.available_block_columns.each do |column|
|
query.available_block_columns.each do |column|
|
||||||
@@ -161,7 +185,7 @@ module QueriesHelper
|
|||||||
if !params[:query_id].blank?
|
if !params[:query_id].blank?
|
||||||
cond = "project_id IS NULL"
|
cond = "project_id IS NULL"
|
||||||
cond << " OR project_id = #{@project.id}" if @project
|
cond << " OR project_id = #{@project.id}" if @project
|
||||||
@query = IssueQuery.find(params[:query_id], :conditions => cond)
|
@query = IssueQuery.where(cond).find(params[:query_id])
|
||||||
raise ::Unauthorized unless @query.visible?
|
raise ::Unauthorized unless @query.visible?
|
||||||
@query.project = @project
|
@query.project = @project
|
||||||
session[:query] = {:id => @query.id, :project_id => @query.project_id}
|
session[:query] = {:id => @query.id, :project_id => @query.project_id}
|
||||||
@@ -174,6 +198,7 @@ module QueriesHelper
|
|||||||
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
|
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
|
||||||
else
|
else
|
||||||
# retrieve from session
|
# retrieve from session
|
||||||
|
@query = nil
|
||||||
@query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
|
@query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
|
||||||
@query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
|
@query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
|
||||||
@query.project = @project
|
@query.project = @project
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ module ReportsHelper
|
|||||||
data.each { |row|
|
data.each { |row|
|
||||||
match = 1
|
match = 1
|
||||||
criteria.each { |k, v|
|
criteria.each { |k, v|
|
||||||
match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && row[k] == (v == 0 ? "f" : "t"))
|
match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && (v == 0 ? ['f', false] : ['t', true]).include?(row[k]))
|
||||||
} unless criteria.nil?
|
} unless criteria.nil?
|
||||||
a = a + row["total"].to_i if match == 1
|
a = a + row["total"].to_i if match == 1
|
||||||
} unless data.nil?
|
} unless data.nil?
|
||||||
|
|||||||
@@ -107,14 +107,14 @@ module TimelogHelper
|
|||||||
# Column headers
|
# Column headers
|
||||||
headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
|
headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
|
||||||
headers += report.periods
|
headers += report.periods
|
||||||
headers << l(:label_total)
|
headers << l(:label_total_time)
|
||||||
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
|
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
|
||||||
c.to_s,
|
c.to_s,
|
||||||
l(:general_csv_encoding) ) }
|
l(:general_csv_encoding) ) }
|
||||||
# Content
|
# Content
|
||||||
report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
|
report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
|
||||||
# Total row
|
# Total row
|
||||||
str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
|
str_total = Redmine::CodesetUtil.from_utf8(l(:label_total_time), l(:general_csv_encoding))
|
||||||
row = [ str_total ] + [''] * (report.criteria.size - 1)
|
row = [ str_total ] + [''] * (report.criteria.size - 1)
|
||||||
total = 0
|
total = 0
|
||||||
report.periods.each do |period|
|
report.periods.each do |period|
|
||||||
|
|||||||
@@ -35,12 +35,9 @@ module VersionsHelper
|
|||||||
h = Hash.new {|k,v| k[v] = [0, 0]}
|
h = Hash.new {|k,v| k[v] = [0, 0]}
|
||||||
begin
|
begin
|
||||||
# Total issue count
|
# Total issue count
|
||||||
Issue.count(:group => criteria,
|
Issue.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][0] = s}
|
||||||
:conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
|
|
||||||
# Open issues count
|
# Open issues count
|
||||||
Issue.count(:group => criteria,
|
Issue.open.where(:fixed_version_id => version.id).group(criteria).count.each {|c,s| h[c][1] = s}
|
||||||
:include => :status,
|
|
||||||
:conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s}
|
|
||||||
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
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ module WatchersHelper
|
|||||||
return '' unless user && user.logged?
|
return '' unless user && user.logged?
|
||||||
objects = Array.wrap(objects)
|
objects = Array.wrap(objects)
|
||||||
|
|
||||||
watched = objects.any? {|object| object.watched_by?(user)}
|
watched = Watcher.any_watched?(objects, user)
|
||||||
css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
|
css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
|
||||||
text = watched ? l(:button_unwatch) : l(:button_watch)
|
text = watched ? l(:button_unwatch) : l(:button_watch)
|
||||||
url = watch_path(
|
url = watch_path(
|
||||||
|
|||||||
@@ -22,11 +22,20 @@ module WorkflowsHelper
|
|||||||
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
|
field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
|
||||||
end
|
end
|
||||||
|
|
||||||
def field_permission_tag(permissions, status, field)
|
def field_permission_tag(permissions, status, field, role)
|
||||||
name = field.is_a?(CustomField) ? field.id.to_s : field
|
name = field.is_a?(CustomField) ? field.id.to_s : field
|
||||||
options = [["", ""], [l(:label_readonly), "readonly"]]
|
options = [["", ""], [l(:label_readonly), "readonly"]]
|
||||||
options << [l(:label_required), "required"] unless field_required?(field)
|
options << [l(:label_required), "required"] unless field_required?(field)
|
||||||
|
html_options = {}
|
||||||
|
selected = permissions[status.id][name]
|
||||||
|
|
||||||
select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, permissions[status.id][name]))
|
hidden = field.is_a?(CustomField) && !field.visible? && !role.custom_fields.to_a.include?(field)
|
||||||
|
if hidden
|
||||||
|
options[0][0] = l(:label_hidden)
|
||||||
|
selected = ''
|
||||||
|
html_options[:disabled] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, selected), html_options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class Attachment < ActiveRecord::Base
|
|||||||
if @temp_file && (@temp_file.size > 0)
|
if @temp_file && (@temp_file.size > 0)
|
||||||
self.disk_directory = target_directory
|
self.disk_directory = target_directory
|
||||||
self.disk_filename = Attachment.disk_filename(filename, disk_directory)
|
self.disk_filename = Attachment.disk_filename(filename, disk_directory)
|
||||||
logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
|
logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
|
||||||
path = File.dirname(diskfile)
|
path = File.dirname(diskfile)
|
||||||
unless File.directory?(path)
|
unless File.directory?(path)
|
||||||
FileUtils.mkdir_p(path)
|
FileUtils.mkdir_p(path)
|
||||||
@@ -294,10 +294,10 @@ class Attachment < ActiveRecord::Base
|
|||||||
|
|
||||||
def sanitize_filename(value)
|
def sanitize_filename(value)
|
||||||
# get only the filename, not the whole path
|
# get only the filename, not the whole path
|
||||||
just_filename = value.gsub(/^.*(\\|\/)/, '')
|
just_filename = value.gsub(/\A.*(\\|\/)/m, '')
|
||||||
|
|
||||||
# Finally, replace invalid characters with underscore
|
# Finally, replace invalid characters with underscore
|
||||||
@filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
|
@filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the subdirectory in which the attachment will be saved
|
# Returns the subdirectory in which the attachment will be saved
|
||||||
|
|||||||
+20
-16
@@ -118,22 +118,25 @@ class Changeset < ActiveRecord::Base
|
|||||||
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
|
||||||
ref_keywords_any = ref_keywords.delete('*')
|
ref_keywords_any = ref_keywords.delete('*')
|
||||||
# keywords used to fix issues
|
# keywords used to fix issues
|
||||||
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
|
fix_keywords = Setting.commit_update_keywords_array.map {|r| r['keywords']}.flatten.compact
|
||||||
|
|
||||||
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
||||||
|
|
||||||
referenced_issues = []
|
referenced_issues = []
|
||||||
|
|
||||||
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
|
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
|
||||||
action, refs = match[2], match[3]
|
action, refs = match[2].to_s.downcase, match[3]
|
||||||
next unless action.present? || ref_keywords_any
|
next unless action.present? || ref_keywords_any
|
||||||
|
|
||||||
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
|
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
|
||||||
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
|
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
|
||||||
if issue
|
if issue
|
||||||
referenced_issues << issue
|
referenced_issues << issue
|
||||||
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
|
# Don't update issues or log time when importing old commits
|
||||||
log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
|
unless repository.created_on && committed_on && committed_on < repository.created_on
|
||||||
|
fix_issue(issue, action) if fix_keywords.include?(action)
|
||||||
|
log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -210,25 +213,26 @@ class Changeset < ActiveRecord::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fix_issue(issue)
|
# Updates the +issue+ according to +action+
|
||||||
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
|
def fix_issue(issue, action)
|
||||||
if status.nil?
|
|
||||||
logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
|
|
||||||
return issue
|
|
||||||
end
|
|
||||||
|
|
||||||
# the issue may have been updated by the closure of another one (eg. duplicate)
|
# the issue may have been updated by the closure of another one (eg. duplicate)
|
||||||
issue.reload
|
issue.reload
|
||||||
# don't change the status is the issue is closed
|
# don't change the status is the issue is closed
|
||||||
return if issue.status && issue.status.is_closed?
|
return if issue.status && issue.status.is_closed?
|
||||||
|
|
||||||
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
|
journal = issue.init_journal(user || User.anonymous,
|
||||||
issue.status = status
|
ll(Setting.default_language,
|
||||||
unless Setting.commit_fix_done_ratio.blank?
|
:text_status_changed_by_changeset,
|
||||||
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
|
text_tag(issue.project)))
|
||||||
|
rule = Setting.commit_update_keywords_array.detect do |rule|
|
||||||
|
rule['keywords'].include?(action) &&
|
||||||
|
(rule['if_tracker_id'].blank? || rule['if_tracker_id'] == issue.tracker_id.to_s)
|
||||||
|
end
|
||||||
|
if rule
|
||||||
|
issue.assign_attributes rule.slice(*Issue.attribute_names)
|
||||||
end
|
end
|
||||||
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
|
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
|
||||||
{ :changeset => self, :issue => issue })
|
{ :changeset => self, :issue => issue, :action => action })
|
||||||
unless issue.save
|
unless issue.save
|
||||||
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
|
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,5 +22,16 @@ class Comment < ActiveRecord::Base
|
|||||||
|
|
||||||
validates_presence_of :commented, :author, :comments
|
validates_presence_of :commented, :author, :comments
|
||||||
|
|
||||||
|
after_create :send_notification
|
||||||
|
|
||||||
safe_attributes 'comments'
|
safe_attributes 'comments'
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
mailer_method = "#{commented.class.name.underscore}_comment_added"
|
||||||
|
if Setting.notified_events.include?(mailer_method)
|
||||||
|
Mailer.send(mailer_method, self).deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Redmine - project management software
|
|
||||||
# Copyright (C) 2006-2013 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 CommentObserver < ActiveRecord::Observer
|
|
||||||
def after_create(comment)
|
|
||||||
if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added')
|
|
||||||
Mailer.news_comment_added(comment).deliver
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+58
-25
@@ -19,6 +19,7 @@ class CustomField < ActiveRecord::Base
|
|||||||
include Redmine::SubclassFactory
|
include Redmine::SubclassFactory
|
||||||
|
|
||||||
has_many :custom_values, :dependent => :delete_all
|
has_many :custom_values, :dependent => :delete_all
|
||||||
|
has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
|
||||||
acts_as_list :scope => 'type = \'#{self.class}\''
|
acts_as_list :scope => 'type = \'#{self.class}\''
|
||||||
serialize :possible_values
|
serialize :possible_values
|
||||||
|
|
||||||
@@ -26,35 +27,35 @@ class CustomField < ActiveRecord::Base
|
|||||||
validates_uniqueness_of :name, :scope => :type
|
validates_uniqueness_of :name, :scope => :type
|
||||||
validates_length_of :name, :maximum => 30
|
validates_length_of :name, :maximum => 30
|
||||||
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
|
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
|
||||||
|
|
||||||
validate :validate_custom_field
|
validate :validate_custom_field
|
||||||
|
|
||||||
before_validation :set_searchable
|
before_validation :set_searchable
|
||||||
after_save :handle_multiplicity_change
|
after_save :handle_multiplicity_change
|
||||||
|
after_save do |field|
|
||||||
|
if field.visible_changed? && field.visible
|
||||||
|
field.roles.clear
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
||||||
|
scope :visible, lambda {|*args|
|
||||||
|
user = args.shift || User.current
|
||||||
|
if user.admin?
|
||||||
|
# nop
|
||||||
|
elsif user.memberships.any?
|
||||||
|
where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
|
||||||
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
|
||||||
|
" INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
|
||||||
|
" WHERE m.user_id = ?)",
|
||||||
|
true, user.id)
|
||||||
|
else
|
||||||
|
where(:visible => true)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
CUSTOM_FIELDS_TABS = [
|
def visible_by?(project, user=User.current)
|
||||||
{:name => 'IssueCustomField', :partial => 'custom_fields/index',
|
visible? || user.admin?
|
||||||
:label => :label_issue_plural},
|
end
|
||||||
{: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)
|
def field_format=(arg)
|
||||||
# cannot change format of a saved custom field
|
# cannot change format of a saved custom field
|
||||||
@@ -122,8 +123,10 @@ class CustomField < ActiveRecord::Base
|
|||||||
values.each do |value|
|
values.each do |value|
|
||||||
value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
|
value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
|
||||||
end
|
end
|
||||||
|
values
|
||||||
|
else
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
values || []
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -215,6 +218,7 @@ class CustomField < ActiveRecord::Base
|
|||||||
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
" 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}.customized_id = #{self.class.customized_class.table_name}.id" +
|
||||||
" AND #{join_alias}.custom_field_id = #{id}" +
|
" AND #{join_alias}.custom_field_id = #{id}" +
|
||||||
|
" AND (#{visibility_by_project_condition})" +
|
||||||
" AND #{join_alias}.value <> ''" +
|
" AND #{join_alias}.value <> ''" +
|
||||||
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
" 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" +
|
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
||||||
@@ -227,6 +231,7 @@ class CustomField < ActiveRecord::Base
|
|||||||
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
" 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}.customized_id = #{self.class.customized_class.table_name}.id" +
|
||||||
" AND #{join_alias}.custom_field_id = #{id}" +
|
" AND #{join_alias}.custom_field_id = #{id}" +
|
||||||
|
" AND (#{visibility_by_project_condition})" +
|
||||||
" AND #{join_alias}.value <> ''" +
|
" AND #{join_alias}.value <> ''" +
|
||||||
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
" 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" +
|
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
||||||
@@ -237,6 +242,7 @@ class CustomField < ActiveRecord::Base
|
|||||||
" ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
|
" 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}.customized_id = #{self.class.customized_class.table_name}.id" +
|
||||||
" AND #{join_alias}.custom_field_id = #{id}" +
|
" AND #{join_alias}.custom_field_id = #{id}" +
|
||||||
|
" AND (#{visibility_by_project_condition})" +
|
||||||
" AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
|
" 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" +
|
" WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
|
||||||
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
" AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
|
||||||
@@ -254,6 +260,33 @@ class CustomField < ActiveRecord::Base
|
|||||||
join_alias + "_" + field_format
|
join_alias + "_" + field_format
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visibility_by_project_condition(project_key=nil, user=User.current)
|
||||||
|
if visible? || user.admin?
|
||||||
|
"1=1"
|
||||||
|
elsif user.anonymous?
|
||||||
|
"1=0"
|
||||||
|
else
|
||||||
|
project_key ||= "#{self.class.customized_class.table_name}.project_id"
|
||||||
|
"#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
|
||||||
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
|
||||||
|
" INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
|
||||||
|
" WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.visibility_condition
|
||||||
|
if user.admin?
|
||||||
|
"1=1"
|
||||||
|
elsif user.anonymous?
|
||||||
|
"#{table_name}.visible"
|
||||||
|
else
|
||||||
|
"#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
|
||||||
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
|
||||||
|
" INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
|
||||||
|
" WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def <=>(field)
|
def <=>(field)
|
||||||
position <=> field.position
|
position <=> field.position
|
||||||
end
|
end
|
||||||
@@ -270,7 +303,7 @@ class CustomField < ActiveRecord::Base
|
|||||||
|
|
||||||
def self.customized_class
|
def self.customized_class
|
||||||
self.name =~ /^(.+)CustomField$/
|
self.name =~ /^(.+)CustomField$/
|
||||||
begin; $1.constantize; rescue nil; end
|
$1.constantize rescue nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# to move in project_custom_field
|
# to move in project_custom_field
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class Document < ActiveRecord::Base
|
|||||||
validates_presence_of :project, :title, :category
|
validates_presence_of :project, :title, :category
|
||||||
validates_length_of :title, :maximum => 60
|
validates_length_of :title, :maximum => 60
|
||||||
|
|
||||||
|
after_create :send_notification
|
||||||
|
|
||||||
scope :visible, lambda {|*args|
|
scope :visible, lambda {|*args|
|
||||||
includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
|
includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
|
||||||
}
|
}
|
||||||
@@ -54,4 +56,12 @@ class Document < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
@updated_on
|
@updated_on
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
if Setting.notified_events.include?('document_added')
|
||||||
|
Mailer.document_added(self).deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class Enumeration < ActiveRecord::Base
|
|||||||
scope :shared, lambda { where(:project_id => nil) }
|
scope :shared, lambda { where(:project_id => nil) }
|
||||||
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
scope :sorted, lambda { order("#{table_name}.position ASC") }
|
||||||
scope :active, lambda { where(:active => true) }
|
scope :active, lambda { where(:active => true) }
|
||||||
|
scope :system, lambda { where(:project_id => nil) }
|
||||||
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
|
||||||
|
|
||||||
def self.default
|
def self.default
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class Group < Principal
|
|||||||
before_destroy :remove_references_before_destroy
|
before_destroy :remove_references_before_destroy
|
||||||
|
|
||||||
scope :sorted, lambda { order("#{table_name}.lastname ASC") }
|
scope :sorted, lambda { order("#{table_name}.lastname ASC") }
|
||||||
|
scope :named, lambda {|arg| where("LOWER(#{table_name}.lastname) = LOWER(?)", arg.to_s.strip)}
|
||||||
|
|
||||||
safe_attributes 'name',
|
safe_attributes 'name',
|
||||||
'user_ids',
|
'user_ids',
|
||||||
|
|||||||
+224
-104
@@ -18,6 +18,7 @@
|
|||||||
class Issue < ActiveRecord::Base
|
class Issue < ActiveRecord::Base
|
||||||
include Redmine::SafeAttributes
|
include Redmine::SafeAttributes
|
||||||
include Redmine::Utils::DateCalculation
|
include Redmine::Utils::DateCalculation
|
||||||
|
include Redmine::I18n
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :tracker
|
belongs_to :tracker
|
||||||
@@ -91,12 +92,15 @@ class Issue < ActiveRecord::Base
|
|||||||
}
|
}
|
||||||
|
|
||||||
before_create :default_assign
|
before_create :default_assign
|
||||||
before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
|
before_save :close_duplicates, :update_done_ratio_from_issue_status,
|
||||||
|
:force_updated_on_change, :update_closed_on
|
||||||
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
|
# Should be after_create but would be called before previous after_save callbacks
|
||||||
after_save :after_create_from_copy
|
after_save :after_create_from_copy
|
||||||
after_destroy :update_parent_attributes
|
after_destroy :update_parent_attributes
|
||||||
|
after_create :send_notification
|
||||||
|
|
||||||
# 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={})
|
||||||
@@ -106,10 +110,10 @@ class Issue < ActiveRecord::Base
|
|||||||
when 'all'
|
when 'all'
|
||||||
nil
|
nil
|
||||||
when 'default'
|
when 'default'
|
||||||
user_ids = [user.id] + user.groups.map(&:id)
|
user_ids = [user.id] + user.groups.map(&:id).compact
|
||||||
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
||||||
when 'own'
|
when 'own'
|
||||||
user_ids = [user.id] + user.groups.map(&:id)
|
user_ids = [user.id] + user.groups.map(&:id).compact
|
||||||
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
|
||||||
else
|
else
|
||||||
'1=0'
|
'1=0'
|
||||||
@@ -184,10 +188,12 @@ class Issue < ActiveRecord::Base
|
|||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias :base_reload :reload
|
||||||
def reload(*args)
|
def reload(*args)
|
||||||
@workflow_rule_by_attribute = nil
|
@workflow_rule_by_attribute = nil
|
||||||
@assignable_versions = nil
|
@assignable_versions = nil
|
||||||
super
|
@relations = nil
|
||||||
|
base_reload(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
|
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
|
||||||
@@ -195,6 +201,13 @@ class Issue < ActiveRecord::Base
|
|||||||
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
|
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visible_custom_field_values(user=nil)
|
||||||
|
user_real = user || User.current
|
||||||
|
custom_field_values.select do |value|
|
||||||
|
value.custom_field.visible_by?(project, user_real)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Copies attributes from another issue, arg can be an id or an Issue
|
# Copies attributes from another issue, arg can be an id or an Issue
|
||||||
def copy_from(arg, options={})
|
def copy_from(arg, options={})
|
||||||
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
|
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
|
||||||
@@ -345,8 +358,7 @@ class Issue < ActiveRecord::Base
|
|||||||
if issue.new_record?
|
if issue.new_record?
|
||||||
issue.copy?
|
issue.copy?
|
||||||
elsif user.allowed_to?(:move_issues, issue.project)
|
elsif user.allowed_to?(:move_issues, issue.project)
|
||||||
projects = Issue.allowed_target_projects_on_move(user)
|
Issue.allowed_target_projects_on_move.count > 1
|
||||||
projects.include?(issue.project) && projects.size > 1
|
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +425,7 @@ class Issue < ActiveRecord::Base
|
|||||||
|
|
||||||
# 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')) && safe_attribute?('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).where(:id => p.to_i).exists?
|
||||||
self.project_id = p
|
self.project_id = p
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -443,11 +455,15 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
if attrs['custom_field_values'].present?
|
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}
|
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
|
||||||
|
# TODO: use #select when ruby1.8 support is dropped
|
||||||
|
attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| !editable_custom_field_ids.include?(k.to_s)}
|
||||||
end
|
end
|
||||||
|
|
||||||
if attrs['custom_fields'].present?
|
if attrs['custom_fields'].present?
|
||||||
attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
|
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
|
||||||
|
# TODO: use #select when ruby1.8 support is dropped
|
||||||
|
attrs['custom_fields'] = attrs['custom_fields'].reject {|c| !editable_custom_field_ids.include?(c['id'].to_s)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# mass-assignment security bypass
|
# mass-assignment security bypass
|
||||||
@@ -460,7 +476,7 @@ class Issue < ActiveRecord::Base
|
|||||||
|
|
||||||
# Returns the custom_field_values that can be edited by the given user
|
# Returns the custom_field_values that can be edited by the given user
|
||||||
def editable_custom_field_values(user=nil)
|
def editable_custom_field_values(user=nil)
|
||||||
custom_field_values.reject do |value|
|
visible_custom_field_values(user).reject do |value|
|
||||||
read_only_attribute_names(user).include?(value.custom_field_id.to_s)
|
read_only_attribute_names(user).include?(value.custom_field_id.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -545,12 +561,12 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_issue
|
def validate_issue
|
||||||
if due_date && start_date && due_date < start_date
|
if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
|
||||||
errors.add :due_date, :greater_than_start_date
|
errors.add :due_date, :greater_than_start_date
|
||||||
end
|
end
|
||||||
|
|
||||||
if start_date && soonest_start && start_date < soonest_start
|
if start_date && start_date_changed? && soonest_start && start_date < soonest_start
|
||||||
errors.add :start_date, :invalid
|
errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
|
||||||
end
|
end
|
||||||
|
|
||||||
if fixed_version
|
if fixed_version
|
||||||
@@ -574,6 +590,8 @@ class Issue < ActiveRecord::Base
|
|||||||
elsif @parent_issue
|
elsif @parent_issue
|
||||||
if !valid_parent_project?(@parent_issue)
|
if !valid_parent_project?(@parent_issue)
|
||||||
errors.add :parent_issue_id, :invalid
|
errors.add :parent_issue_id, :invalid
|
||||||
|
elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
|
||||||
|
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
|
||||||
@@ -687,7 +705,7 @@ class Issue < ActiveRecord::Base
|
|||||||
# Is the amount of work done less than it should for the due date
|
# Is the amount of work done less than it should for the due date
|
||||||
def behind_schedule?
|
def behind_schedule?
|
||||||
return false if start_date.nil? || due_date.nil?
|
return false if start_date.nil? || due_date.nil?
|
||||||
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
|
done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
|
||||||
return done_date <= Date.today
|
return done_date <= Date.today
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -740,12 +758,16 @@ class Issue < ActiveRecord::Base
|
|||||||
initial_status = IssueStatus.find_by_id(status_id_was)
|
initial_status = IssueStatus.find_by_id(status_id_was)
|
||||||
end
|
end
|
||||||
initial_status ||= status
|
initial_status ||= status
|
||||||
|
|
||||||
|
initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
|
||||||
|
assignee_transitions_allowed = initial_assigned_to_id.present? &&
|
||||||
|
(user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
|
||||||
|
|
||||||
statuses = initial_status.find_new_statuses_allowed_to(
|
statuses = initial_status.find_new_statuses_allowed_to(
|
||||||
user.admin ? Role.all : user.roles_for_project(project),
|
user.admin ? Role.all : user.roles_for_project(project),
|
||||||
tracker,
|
tracker,
|
||||||
author == user,
|
author == user,
|
||||||
assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
|
assignee_transitions_allowed
|
||||||
)
|
)
|
||||||
statuses << initial_status unless statuses.empty?
|
statuses << initial_status unless statuses.empty?
|
||||||
statuses << IssueStatus.default if include_default
|
statuses << IssueStatus.default if include_default
|
||||||
@@ -786,6 +808,21 @@ class Issue < ActiveRecord::Base
|
|||||||
notified_users.collect(&:mail)
|
notified_users.collect(&:mail)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def each_notification(users, &block)
|
||||||
|
if users.any?
|
||||||
|
if custom_field_values.detect {|value| !value.custom_field.visible?}
|
||||||
|
users_by_custom_field_visibility = users.group_by do |user|
|
||||||
|
visible_custom_field_values(user).map(&:custom_field_id).sort
|
||||||
|
end
|
||||||
|
users_by_custom_field_visibility.values.each do |users|
|
||||||
|
yield(users)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
yield(users)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the number of hours spent on this issue
|
# Returns the number of hours spent on this issue
|
||||||
def spent_hours
|
def spent_hours
|
||||||
@spent_hours ||= time_entries.sum(:hours) || 0
|
@spent_hours ||= time_entries.sum(:hours) || 0
|
||||||
@@ -808,7 +845,7 @@ class Issue < ActiveRecord::Base
|
|||||||
# Preloads relations for a collection of issues
|
# Preloads relations for a collection of issues
|
||||||
def self.load_relations(issues)
|
def self.load_relations(issues)
|
||||||
if issues.any?
|
if issues.any?
|
||||||
relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
|
relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
|
||||||
issues.each do |issue|
|
issues.each do |issue|
|
||||||
issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
|
issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
|
||||||
end
|
end
|
||||||
@@ -818,7 +855,7 @@ class Issue < ActiveRecord::Base
|
|||||||
# Preloads visible spent time for a collection of issues
|
# Preloads visible spent time for a collection of issues
|
||||||
def self.load_visible_spent_hours(issues, user=User.current)
|
def self.load_visible_spent_hours(issues, user=User.current)
|
||||||
if issues.any?
|
if issues.any?
|
||||||
hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
|
hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
|
||||||
issues.each do |issue|
|
issues.each do |issue|
|
||||||
issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
|
issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
|
||||||
end
|
end
|
||||||
@@ -846,18 +883,103 @@ class Issue < ActiveRecord::Base
|
|||||||
|
|
||||||
# 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.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns all the other issues that depend on the issue
|
||||||
|
# The algorithm is a modified breadth first search (bfs)
|
||||||
def all_dependent_issues(except=[])
|
def all_dependent_issues(except=[])
|
||||||
except << self
|
# The found dependencies
|
||||||
dependencies = []
|
dependencies = []
|
||||||
relations_from.each do |relation|
|
|
||||||
if relation.issue_to && !except.include?(relation.issue_to)
|
# The visited flag for every node (issue) used by the breadth first search
|
||||||
dependencies << relation.issue_to
|
eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
|
||||||
dependencies += relation.issue_to.all_dependent_issues(except)
|
|
||||||
|
ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
|
||||||
|
# the issue when it is processed.
|
||||||
|
|
||||||
|
ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
|
||||||
|
# but its children will not be added to the queue when it is processed.
|
||||||
|
|
||||||
|
eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
|
||||||
|
# the queue, but its children have not been added.
|
||||||
|
|
||||||
|
ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
|
||||||
|
# the children still need to be processed.
|
||||||
|
|
||||||
|
eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
|
||||||
|
# added as dependent issues. It needs no further processing.
|
||||||
|
|
||||||
|
issue_status = Hash.new(eNOT_DISCOVERED)
|
||||||
|
|
||||||
|
# The queue
|
||||||
|
queue = []
|
||||||
|
|
||||||
|
# Initialize the bfs, add start node (self) to the queue
|
||||||
|
queue << self
|
||||||
|
issue_status[self] = ePROCESS_ALL
|
||||||
|
|
||||||
|
while (!queue.empty?) do
|
||||||
|
current_issue = queue.shift
|
||||||
|
current_issue_status = issue_status[current_issue]
|
||||||
|
dependencies << current_issue
|
||||||
|
|
||||||
|
# Add parent to queue, if not already in it.
|
||||||
|
parent = current_issue.parent
|
||||||
|
parent_status = issue_status[parent]
|
||||||
|
|
||||||
|
if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
|
||||||
|
queue << parent
|
||||||
|
issue_status[parent] = ePROCESS_RELATIONS_ONLY
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
# Add children to queue, but only if they are not already in it and
|
||||||
|
# the children of the current node need to be processed.
|
||||||
|
if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
|
||||||
|
current_issue.children.each do |child|
|
||||||
|
next if except.include?(child)
|
||||||
|
|
||||||
|
if (issue_status[child] == eNOT_DISCOVERED)
|
||||||
|
queue << child
|
||||||
|
issue_status[child] = ePROCESS_ALL
|
||||||
|
elsif (issue_status[child] == eRELATIONS_PROCESSED)
|
||||||
|
queue << child
|
||||||
|
issue_status[child] = ePROCESS_CHILDREN_ONLY
|
||||||
|
elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
|
||||||
|
queue << child
|
||||||
|
issue_status[child] = ePROCESS_ALL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add related issues to the queue, if they are not already in it.
|
||||||
|
current_issue.relations_from.map(&:issue_to).each do |related_issue|
|
||||||
|
next if except.include?(related_issue)
|
||||||
|
|
||||||
|
if (issue_status[related_issue] == eNOT_DISCOVERED)
|
||||||
|
queue << related_issue
|
||||||
|
issue_status[related_issue] = ePROCESS_ALL
|
||||||
|
elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
|
||||||
|
queue << related_issue
|
||||||
|
issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
|
||||||
|
elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
|
||||||
|
queue << related_issue
|
||||||
|
issue_status[related_issue] = ePROCESS_ALL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set new status for current issue
|
||||||
|
if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
|
||||||
|
issue_status[current_issue] = eALL_PROCESSED
|
||||||
|
elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
|
||||||
|
issue_status[current_issue] = eRELATIONS_PROCESSED
|
||||||
|
end
|
||||||
|
end # while
|
||||||
|
|
||||||
|
# Remove the issues from the "except" parameter from the result array
|
||||||
|
dependencies -= except
|
||||||
|
dependencies.delete(self)
|
||||||
|
|
||||||
dependencies
|
dependencies
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -890,7 +1012,7 @@ class Issue < ActiveRecord::Base
|
|||||||
@soonest_start = nil if reload
|
@soonest_start = nil if reload
|
||||||
@soonest_start ||= (
|
@soonest_start ||= (
|
||||||
relations_to(reload).collect{|relation| relation.successor_soonest_start} +
|
relations_to(reload).collect{|relation| relation.successor_soonest_start} +
|
||||||
ancestors.collect(&:soonest_start)
|
[(@parent_issue || parent).try(:soonest_start)]
|
||||||
).compact.max
|
).compact.max
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -952,40 +1074,19 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# 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(user=User.current)
|
||||||
s = "issue status-#{status_id} #{priority.try(:css_classes)}"
|
s = "issue tracker-#{tracker_id} 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?
|
||||||
s << ' parent' unless leaf?
|
s << ' parent' unless leaf?
|
||||||
s << ' private' if is_private?
|
s << ' private' if is_private?
|
||||||
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
|
if user.logged?
|
||||||
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
|
s << ' created-by-me' if author_id == user.id
|
||||||
s
|
s << ' assigned-to-me' if assigned_to_id == user.id
|
||||||
end
|
s << ' assigned-to-my-group' if user.groups.any? {|g| g.id = assigned_to_id}
|
||||||
|
|
||||||
# Saves an issue and a time_entry from the parameters
|
|
||||||
def save_issue_with_child_records(params, existing_time_entry=nil)
|
|
||||||
Issue.transaction do
|
|
||||||
if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
|
|
||||||
@time_entry = existing_time_entry || TimeEntry.new
|
|
||||||
@time_entry.project = project
|
|
||||||
@time_entry.issue = self
|
|
||||||
@time_entry.user = User.current
|
|
||||||
@time_entry.spent_on = User.current.today
|
|
||||||
@time_entry.attributes = params[:time_entry]
|
|
||||||
self.time_entries << @time_entry
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: Rename hook
|
|
||||||
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
|
|
||||||
if save
|
|
||||||
# TODO: Rename hook
|
|
||||||
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
|
|
||||||
else
|
|
||||||
raise ActiveRecord::Rollback
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
s
|
||||||
end
|
end
|
||||||
|
|
||||||
# Unassigns issues from +version+ if it's no longer shared with issue's project
|
# Unassigns issues from +version+ if it's no longer shared with issue's project
|
||||||
@@ -1006,6 +1107,10 @@ class Issue < ActiveRecord::Base
|
|||||||
s = arg.to_s.strip.presence
|
s = arg.to_s.strip.presence
|
||||||
if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
|
if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
|
||||||
@parent_issue.id
|
@parent_issue.id
|
||||||
|
@invalid_parent_issue_id = nil
|
||||||
|
elsif s.blank?
|
||||||
|
@parent_issue = nil
|
||||||
|
@invalid_parent_issue_id = nil
|
||||||
else
|
else
|
||||||
@parent_issue = nil
|
@parent_issue = nil
|
||||||
@invalid_parent_issue_id = arg
|
@invalid_parent_issue_id = arg
|
||||||
@@ -1094,18 +1199,18 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
# End ReportsController extraction
|
# End ReportsController extraction
|
||||||
|
|
||||||
# Returns an array of projects that user can assign the issue to
|
# Returns a scope of projects that user can assign the issue to
|
||||||
def allowed_target_projects(user=User.current)
|
def allowed_target_projects(user=User.current)
|
||||||
if new_record?
|
if new_record?
|
||||||
Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
|
Project.where(Project.allowed_to_condition(user, :add_issues))
|
||||||
else
|
else
|
||||||
self.class.allowed_target_projects_on_move(user)
|
self.class.allowed_target_projects_on_move(user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns an array of projects that user can move issues to
|
# Returns a scope of projects that user can move issues to
|
||||||
def self.allowed_target_projects_on_move(user=User.current)
|
def self.allowed_target_projects_on_move(user=User.current)
|
||||||
Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
|
Project.where(Project.allowed_to_condition(user, :move_issues))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -1145,20 +1250,27 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
unless @copied_from.leaf? || @copy_options[:subtasks] == false
|
unless @copied_from.leaf? || @copy_options[:subtasks] == false
|
||||||
@copied_from.children.each do |child|
|
copy_options = (@copy_options || {}).merge(:subtasks => false)
|
||||||
|
copied_issue_ids = {@copied_from.id => self.id}
|
||||||
|
@copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
|
||||||
|
# Do not copy self when copying an issue as a descendant of the copied issue
|
||||||
|
next if child == self
|
||||||
|
# Do not copy subtasks of issues that were not copied
|
||||||
|
next unless copied_issue_ids[child.parent_id]
|
||||||
|
# Do not copy subtasks that are not visible to avoid potential disclosure of private data
|
||||||
unless child.visible?
|
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
|
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
|
next
|
||||||
end
|
end
|
||||||
copy = Issue.new.copy_from(child, @copy_options)
|
copy = Issue.new.copy_from(child, copy_options)
|
||||||
copy.author = author
|
copy.author = author
|
||||||
copy.project = project
|
copy.project = project
|
||||||
copy.parent_issue_id = id
|
copy.parent_issue_id = copied_issue_ids[child.parent_id]
|
||||||
# Children subtasks are copied recursively
|
|
||||||
unless copy.save
|
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
|
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
|
||||||
|
next
|
||||||
end
|
end
|
||||||
|
copied_issue_ids[child.id] = copy.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@after_create_from_copy_handled = true
|
@after_create_from_copy_handled = true
|
||||||
@@ -1169,48 +1281,50 @@ class Issue < ActiveRecord::Base
|
|||||||
# issue was just created
|
# issue was just created
|
||||||
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
|
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
|
||||||
set_default_left_and_right
|
set_default_left_and_right
|
||||||
Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
|
Issue.update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt], ["id = ?", id])
|
||||||
if @parent_issue
|
if @parent_issue
|
||||||
move_to_child_of(@parent_issue)
|
move_to_child_of(@parent_issue)
|
||||||
end
|
end
|
||||||
reload
|
|
||||||
elsif parent_issue_id != parent_id
|
elsif parent_issue_id != parent_id
|
||||||
former_parent_id = parent_id
|
update_nested_set_attributes_on_parent_change
|
||||||
# moving an existing issue
|
|
||||||
if @parent_issue && @parent_issue.root_id == root_id
|
|
||||||
# inside the same tree
|
|
||||||
move_to_child_of(@parent_issue)
|
|
||||||
else
|
|
||||||
# to another tree
|
|
||||||
unless root?
|
|
||||||
move_to_right_of(root)
|
|
||||||
reload
|
|
||||||
end
|
|
||||||
old_root_id = root_id
|
|
||||||
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
|
|
||||||
target_maxright = nested_set_scope.maximum(right_column_name) || 0
|
|
||||||
offset = target_maxright + 1 - lft
|
|
||||||
Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
|
|
||||||
["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
|
|
||||||
self[left_column_name] = lft + offset
|
|
||||||
self[right_column_name] = rgt + offset
|
|
||||||
if @parent_issue
|
|
||||||
move_to_child_of(@parent_issue)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
reload
|
|
||||||
# delete invalid relations of all descendants
|
|
||||||
self_and_descendants.each do |issue|
|
|
||||||
issue.relations.each do |relation|
|
|
||||||
relation.destroy unless relation.valid?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# update former parent
|
|
||||||
recalculate_attributes_for(former_parent_id) if former_parent_id
|
|
||||||
end
|
end
|
||||||
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
|
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Updates the nested set for when an existing issue is moved
|
||||||
|
def update_nested_set_attributes_on_parent_change
|
||||||
|
former_parent_id = parent_id
|
||||||
|
# moving an existing issue
|
||||||
|
if @parent_issue && @parent_issue.root_id == root_id
|
||||||
|
# inside the same tree
|
||||||
|
move_to_child_of(@parent_issue)
|
||||||
|
else
|
||||||
|
# to another tree
|
||||||
|
unless root?
|
||||||
|
move_to_right_of(root)
|
||||||
|
end
|
||||||
|
old_root_id = root_id
|
||||||
|
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
|
||||||
|
target_maxright = nested_set_scope.maximum(right_column_name) || 0
|
||||||
|
offset = target_maxright + 1 - lft
|
||||||
|
Issue.update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset],
|
||||||
|
["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
|
||||||
|
self[left_column_name] = lft + offset
|
||||||
|
self[right_column_name] = rgt + offset
|
||||||
|
if @parent_issue
|
||||||
|
move_to_child_of(@parent_issue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# delete invalid relations of all descendants
|
||||||
|
self_and_descendants.each do |issue|
|
||||||
|
issue.relations.each do |relation|
|
||||||
|
relation.destroy unless relation.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# update former parent
|
||||||
|
recalculate_attributes_for(former_parent_id) if former_parent_id
|
||||||
|
end
|
||||||
|
|
||||||
def update_parent_attributes
|
def update_parent_attributes
|
||||||
recalculate_attributes_for(parent_id) if parent_id
|
recalculate_attributes_for(parent_id) if parent_id
|
||||||
end
|
end
|
||||||
@@ -1237,7 +1351,8 @@ class Issue < ActiveRecord::Base
|
|||||||
if average == 0
|
if average == 0
|
||||||
average = 1
|
average = 1
|
||||||
end
|
end
|
||||||
done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
|
done = p.leaves.sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
|
||||||
|
"* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
|
||||||
progress = done / (average * leaves_count)
|
progress = done / (average * leaves_count)
|
||||||
p.done_ratio = progress.round
|
p.done_ratio = progress.round
|
||||||
end
|
end
|
||||||
@@ -1257,12 +1372,11 @@ class Issue < ActiveRecord::Base
|
|||||||
def self.update_versions(conditions=nil)
|
def self.update_versions(conditions=nil)
|
||||||
# Only need to update issues with a fixed_version from
|
# Only need to update issues with a fixed_version from
|
||||||
# a different project and that is not systemwide shared
|
# a different project and that is not systemwide shared
|
||||||
Issue.scoped(:conditions => conditions).all(
|
Issue.includes(:project, :fixed_version).
|
||||||
:conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
|
where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
|
||||||
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
|
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
|
||||||
" AND #{Version.table_name}.sharing <> 'system'",
|
" AND #{Version.table_name}.sharing <> 'system'").
|
||||||
:include => [:project, :fixed_version]
|
where(conditions).each do |issue|
|
||||||
).each do |issue|
|
|
||||||
next if issue.project.nil? || issue.fixed_version.nil?
|
next if issue.project.nil? || issue.fixed_version.nil?
|
||||||
unless issue.project.shared_versions.include?(issue.fixed_version)
|
unless issue.project.shared_versions.include?(issue.fixed_version)
|
||||||
issue.init_journal(User.current)
|
issue.init_journal(User.current)
|
||||||
@@ -1395,6 +1509,12 @@ class Issue < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
if Setting.notified_events.include?('issue_added')
|
||||||
|
Mailer.deliver_issue_add(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Query generator for selecting groups of issue counts for a project
|
# Query generator for selecting groups of issue counts for a project
|
||||||
# based on specific criteria
|
# based on specific criteria
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -23,5 +23,22 @@ class IssueCustomField < CustomField
|
|||||||
def type_name
|
def type_name
|
||||||
:label_issue_plural
|
:label_issue_plural
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
def visible_by?(project, user=User.current)
|
||||||
|
super || (roles & user.roles_for_project(project)).present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def visibility_by_project_condition(*args)
|
||||||
|
sql = super
|
||||||
|
additional_sql = "#{Issue.table_name}.tracker_id IN (SELECT tracker_id FROM #{table_name_prefix}custom_fields_trackers#{table_name_suffix} WHERE custom_field_id = #{id})"
|
||||||
|
unless is_for_all?
|
||||||
|
additional_sql << " AND #{Issue.table_name}.project_id IN (SELECT project_id FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} WHERE custom_field_id = #{id})"
|
||||||
|
end
|
||||||
|
"((#{sql}) AND (#{additional_sql}))"
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_custom_field
|
||||||
|
super
|
||||||
|
errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) unless visible? || roles.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Redmine - project management software
|
|
||||||
# Copyright (C) 2006-2013 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 IssueObserver < ActiveRecord::Observer
|
|
||||||
def after_create(issue)
|
|
||||||
Mailer.issue_add(issue).deliver if Setting.notified_events.include?('issue_added')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+116
-38
@@ -45,9 +45,25 @@ class IssueQuery < Query
|
|||||||
scope :visible, lambda {|*args|
|
scope :visible, lambda {|*args|
|
||||||
user = args.shift || User.current
|
user = args.shift || User.current
|
||||||
base = Project.allowed_to_condition(user, :view_issues, *args)
|
base = Project.allowed_to_condition(user, :view_issues, *args)
|
||||||
user_id = user.logged? ? user.id : 0
|
scope = includes(:project).where("#{table_name}.project_id IS NULL OR (#{base})")
|
||||||
|
|
||||||
includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
|
if user.admin?
|
||||||
|
scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
|
||||||
|
elsif user.memberships.any?
|
||||||
|
scope.where("#{table_name}.visibility = ?" +
|
||||||
|
" OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
|
||||||
|
"SELECT DISTINCT q.id FROM #{table_name} q" +
|
||||||
|
" INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
|
||||||
|
" INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
|
||||||
|
" INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
|
||||||
|
" WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
|
||||||
|
" OR #{table_name}.user_id = ?",
|
||||||
|
VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
|
||||||
|
elsif user.logged?
|
||||||
|
scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
|
||||||
|
else
|
||||||
|
scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
|
||||||
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
def initialize(attributes=nil, *args)
|
def initialize(attributes=nil, *args)
|
||||||
@@ -57,7 +73,53 @@ class IssueQuery < Query
|
|||||||
|
|
||||||
# Returns true if the query is visible to +user+ or the current user.
|
# Returns true if the query is visible to +user+ or the current user.
|
||||||
def visible?(user=User.current)
|
def visible?(user=User.current)
|
||||||
(project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
|
return true if user.admin?
|
||||||
|
return false unless project.nil? || user.allowed_to?(:view_issues, project)
|
||||||
|
case visibility
|
||||||
|
when VISIBILITY_PUBLIC
|
||||||
|
true
|
||||||
|
when VISIBILITY_ROLES
|
||||||
|
if project
|
||||||
|
(user.roles_for_project(project) & roles).any?
|
||||||
|
else
|
||||||
|
Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
|
||||||
|
end
|
||||||
|
else
|
||||||
|
user == self.user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_private?
|
||||||
|
visibility == VISIBILITY_PRIVATE
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_public?
|
||||||
|
!is_private?
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_relations
|
||||||
|
r = options[:draw_relations]
|
||||||
|
r.nil? || r == '1'
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_relations=(arg)
|
||||||
|
options[:draw_relations] = (arg == '0' ? '0' : nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_progress_line
|
||||||
|
r = options[:draw_progress_line]
|
||||||
|
r == '1'
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_progress_line=(arg)
|
||||||
|
options[:draw_progress_line] = (arg == '1' ? '1' : nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_from_params(params)
|
||||||
|
super
|
||||||
|
self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
|
||||||
|
self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
|
||||||
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize_available_filters
|
def initialize_available_filters
|
||||||
@@ -66,7 +128,7 @@ class IssueQuery < Query
|
|||||||
versions = []
|
versions = []
|
||||||
categories = []
|
categories = []
|
||||||
issue_custom_fields = []
|
issue_custom_fields = []
|
||||||
|
|
||||||
if project
|
if project
|
||||||
principals += project.principals.sort
|
principals += project.principals.sort
|
||||||
unless project.leaf?
|
unless project.leaf?
|
||||||
@@ -81,13 +143,12 @@ class IssueQuery < Query
|
|||||||
principals += Principal.member_of(all_projects)
|
principals += Principal.member_of(all_projects)
|
||||||
end
|
end
|
||||||
versions = Version.visible.find_all_by_sharing('system')
|
versions = Version.visible.find_all_by_sharing('system')
|
||||||
issue_custom_fields = IssueCustomField.where(:is_filter => true, :is_for_all => true).all
|
issue_custom_fields = IssueCustomField.where(:is_for_all => true)
|
||||||
end
|
end
|
||||||
principals.uniq!
|
principals.uniq!
|
||||||
principals.sort!
|
principals.sort!
|
||||||
users = principals.select {|p| p.is_a?(User)}
|
users = principals.select {|p| p.is_a?(User)}
|
||||||
|
|
||||||
|
|
||||||
add_available_filter "status_id",
|
add_available_filter "status_id",
|
||||||
:type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
|
:type => :list_status, :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
|
||||||
|
|
||||||
@@ -189,8 +250,8 @@ class IssueQuery < Query
|
|||||||
@available_columns = self.class.available_columns.dup
|
@available_columns = self.class.available_columns.dup
|
||||||
@available_columns += (project ?
|
@available_columns += (project ?
|
||||||
project.all_issue_custom_fields :
|
project.all_issue_custom_fields :
|
||||||
IssueCustomField.all
|
IssueCustomField
|
||||||
).collect {|cf| QueryCustomFieldColumn.new(cf) }
|
).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
|
||||||
|
|
||||||
if User.current.allowed_to?(:view_time_entries, project, :global => true)
|
if User.current.allowed_to?(:view_time_entries, project, :global => true)
|
||||||
index = nil
|
index = nil
|
||||||
@@ -227,7 +288,7 @@ class IssueQuery < Query
|
|||||||
|
|
||||||
# Returns the issue count
|
# Returns the issue count
|
||||||
def issue_count
|
def issue_count
|
||||||
Issue.visible.count(:include => [:status, :project], :conditions => statement)
|
Issue.visible.joins(:status, :project).where(statement).count
|
||||||
rescue ::ActiveRecord::StatementInvalid => e
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
raise StatementInvalid.new(e.message)
|
raise StatementInvalid.new(e.message)
|
||||||
end
|
end
|
||||||
@@ -238,7 +299,12 @@ class IssueQuery < Query
|
|||||||
if grouped?
|
if grouped?
|
||||||
begin
|
begin
|
||||||
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
|
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
|
||||||
r = Issue.visible.count(:joins => joins_for_order_statement(group_by_statement), :group => group_by_statement, :include => [:status, :project], :conditions => statement)
|
r = Issue.visible.
|
||||||
|
joins(:status, :project).
|
||||||
|
where(statement).
|
||||||
|
joins(joins_for_order_statement(group_by_statement)).
|
||||||
|
group(group_by_statement).
|
||||||
|
count
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
r = {nil => issue_count}
|
r = {nil => issue_count}
|
||||||
end
|
end
|
||||||
@@ -257,14 +323,21 @@ class IssueQuery < Query
|
|||||||
def issues(options={})
|
def issues(options={})
|
||||||
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
||||||
|
|
||||||
issues = Issue.visible.where(options[:conditions]).all(
|
scope = Issue.visible.
|
||||||
:include => ([:status, :project] + (options[:include] || [])).uniq,
|
joins(:status, :project).
|
||||||
:conditions => statement,
|
where(statement).
|
||||||
:order => order_option,
|
includes(([:status, :project] + (options[:include] || [])).uniq).
|
||||||
:joins => joins_for_order_statement(order_option.join(',')),
|
where(options[:conditions]).
|
||||||
:limit => options[:limit],
|
order(order_option).
|
||||||
:offset => options[:offset]
|
joins(joins_for_order_statement(order_option.join(','))).
|
||||||
)
|
limit(options[:limit]).
|
||||||
|
offset(options[:offset])
|
||||||
|
|
||||||
|
if has_custom_field_column?
|
||||||
|
scope = scope.preload(:custom_values)
|
||||||
|
end
|
||||||
|
|
||||||
|
issues = scope.all
|
||||||
|
|
||||||
if has_column?(:spent_hours)
|
if has_column?(:spent_hours)
|
||||||
Issue.load_visible_spent_hours(issues)
|
Issue.load_visible_spent_hours(issues)
|
||||||
@@ -281,12 +354,16 @@ class IssueQuery < Query
|
|||||||
def issue_ids(options={})
|
def issue_ids(options={})
|
||||||
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
||||||
|
|
||||||
Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
|
Issue.visible.
|
||||||
:conditions => statement,
|
joins(:status, :project).
|
||||||
:order => order_option,
|
where(statement).
|
||||||
:joins => joins_for_order_statement(order_option.join(',')),
|
includes(([:status, :project] + (options[:include] || [])).uniq).
|
||||||
:limit => options[:limit],
|
where(options[:conditions]).
|
||||||
:offset => options[:offset]).find_ids
|
order(order_option).
|
||||||
|
joins(joins_for_order_statement(order_option.join(','))).
|
||||||
|
limit(options[:limit]).
|
||||||
|
offset(options[:offset]).
|
||||||
|
find_ids
|
||||||
rescue ::ActiveRecord::StatementInvalid => e
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
raise StatementInvalid.new(e.message)
|
raise StatementInvalid.new(e.message)
|
||||||
end
|
end
|
||||||
@@ -294,13 +371,14 @@ class IssueQuery < Query
|
|||||||
# Returns the journals
|
# Returns the journals
|
||||||
# Valid options are :order, :offset, :limit
|
# Valid options are :order, :offset, :limit
|
||||||
def journals(options={})
|
def journals(options={})
|
||||||
Journal.visible.all(
|
Journal.visible.
|
||||||
:include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
|
joins(:issue => [:project, :status]).
|
||||||
:conditions => statement,
|
where(statement).
|
||||||
:order => options[:order],
|
order(options[:order]).
|
||||||
:limit => options[:limit],
|
limit(options[:limit]).
|
||||||
:offset => options[:offset]
|
offset(options[:offset]).
|
||||||
)
|
preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
|
||||||
|
all
|
||||||
rescue ::ActiveRecord::StatementInvalid => e
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
raise StatementInvalid.new(e.message)
|
raise StatementInvalid.new(e.message)
|
||||||
end
|
end
|
||||||
@@ -308,10 +386,11 @@ class IssueQuery < Query
|
|||||||
# Returns the versions
|
# Returns the versions
|
||||||
# Valid options are :conditions
|
# Valid options are :conditions
|
||||||
def versions(options={})
|
def versions(options={})
|
||||||
Version.visible.where(options[:conditions]).all(
|
Version.visible.
|
||||||
:include => :project,
|
where(project_statement).
|
||||||
:conditions => project_statement
|
where(options[:conditions]).
|
||||||
)
|
includes(:project).
|
||||||
|
all
|
||||||
rescue ::ActiveRecord::StatementInvalid => e
|
rescue ::ActiveRecord::StatementInvalid => e
|
||||||
raise StatementInvalid.new(e.message)
|
raise StatementInvalid.new(e.message)
|
||||||
end
|
end
|
||||||
@@ -393,10 +472,9 @@ class IssueQuery < Query
|
|||||||
|
|
||||||
if relation_options[:sym] == field && !options[:reverse]
|
if relation_options[:sym] == field && !options[:reverse]
|
||||||
sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
|
sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
|
||||||
sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
|
sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
|
||||||
else
|
|
||||||
sql
|
|
||||||
end
|
end
|
||||||
|
"(#{sql})"
|
||||||
end
|
end
|
||||||
|
|
||||||
IssueRelation::TYPES.keys.each do |relation_type|
|
IssueRelation::TYPES.keys.each do |relation_type|
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ class IssueRelation < ActiveRecord::Base
|
|||||||
|
|
||||||
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
|
||||||
|
after_create :create_journal_after_create
|
||||||
|
after_destroy :create_journal_after_delete
|
||||||
|
|
||||||
def visible?(user=User.current)
|
def visible?(user=User.current)
|
||||||
(issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
|
(issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
|
||||||
@@ -179,4 +181,30 @@ class IssueRelation < ActiveRecord::Base
|
|||||||
self.relation_type = TYPES[relation_type][:reverse]
|
self.relation_type = TYPES[relation_type][:reverse]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_journal_after_create
|
||||||
|
journal = issue_from.init_journal(User.current)
|
||||||
|
journal.details << JournalDetail.new(:property => 'relation',
|
||||||
|
:prop_key => label_for(issue_from).to_s,
|
||||||
|
:value => issue_to.id)
|
||||||
|
journal.save
|
||||||
|
journal = issue_to.init_journal(User.current)
|
||||||
|
journal.details << JournalDetail.new(:property => 'relation',
|
||||||
|
:prop_key => label_for(issue_to).to_s,
|
||||||
|
:value => issue_from.id)
|
||||||
|
journal.save
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_journal_after_delete
|
||||||
|
journal = issue_from.init_journal(User.current)
|
||||||
|
journal.details << JournalDetail.new(:property => 'relation',
|
||||||
|
:prop_key => label_for(issue_from).to_s,
|
||||||
|
:old_value => issue_to.id)
|
||||||
|
journal.save
|
||||||
|
journal = issue_to.init_journal(User.current)
|
||||||
|
journal.details << JournalDetail.new(:property => 'relation',
|
||||||
|
:prop_key => label_for(issue_to).to_s,
|
||||||
|
:old_value => issue_from.id)
|
||||||
|
journal.save
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+65
-4
@@ -39,6 +39,7 @@ class Journal < ActiveRecord::Base
|
|||||||
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
|
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
|
||||||
|
|
||||||
before_create :split_private_notes
|
before_create :split_private_notes
|
||||||
|
after_create :send_notification
|
||||||
|
|
||||||
scope :visible, lambda {|*args|
|
scope :visible, lambda {|*args|
|
||||||
user = args.shift || User.current
|
user = args.shift || User.current
|
||||||
@@ -53,6 +54,32 @@ class Journal < ActiveRecord::Base
|
|||||||
(details.empty? && notes.blank?) ? false : super
|
(details.empty? && notes.blank?) ? false : super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns journal details that are visible to user
|
||||||
|
def visible_details(user=User.current)
|
||||||
|
details.select do |detail|
|
||||||
|
if detail.property == 'cf'
|
||||||
|
detail.custom_field && detail.custom_field.visible_by?(project, user)
|
||||||
|
elsif detail.property == 'relation'
|
||||||
|
Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def each_notification(users, &block)
|
||||||
|
if users.any?
|
||||||
|
users_by_details_visibility = users.group_by do |user|
|
||||||
|
visible_details(user)
|
||||||
|
end
|
||||||
|
users_by_details_visibility.each do |visible_details, users|
|
||||||
|
if notes? || visible_details.any?
|
||||||
|
yield(users)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the new status if the journal contains a status change, otherwise nil
|
# Returns the new status if the journal contains a status change, otherwise nil
|
||||||
def new_status
|
def new_status
|
||||||
c = details.detect {|detail| detail.prop_key == 'status_id'}
|
c = details.detect {|detail| detail.prop_key == 'status_id'}
|
||||||
@@ -93,20 +120,44 @@ class Journal < ActiveRecord::Base
|
|||||||
@notify = arg
|
@notify = arg
|
||||||
end
|
end
|
||||||
|
|
||||||
def recipients
|
def notified_users
|
||||||
notified = journalized.notified_users
|
notified = journalized.notified_users
|
||||||
if private_notes?
|
if private_notes?
|
||||||
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
||||||
end
|
end
|
||||||
notified.map(&:mail)
|
notified
|
||||||
end
|
end
|
||||||
|
|
||||||
def watcher_recipients
|
def recipients
|
||||||
|
notified_users.map(&:mail)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notified_watchers
|
||||||
notified = journalized.notified_watchers
|
notified = journalized.notified_watchers
|
||||||
if private_notes?
|
if private_notes?
|
||||||
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
||||||
end
|
end
|
||||||
notified.map(&:mail)
|
notified
|
||||||
|
end
|
||||||
|
|
||||||
|
def watcher_recipients
|
||||||
|
notified_watchers.map(&:mail)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sets @custom_field instance variable on journals details using a single query
|
||||||
|
def self.preload_journals_details_custom_fields(journals)
|
||||||
|
field_ids = journals.map(&:details).flatten.select {|d| d.property == 'cf'}.map(&:prop_key).uniq
|
||||||
|
if field_ids.any?
|
||||||
|
fields_by_id = CustomField.find_all_by_id(field_ids).inject({}) {|h, f| h[f.id] = f; h}
|
||||||
|
journals.each do |journal|
|
||||||
|
journal.details.each do |detail|
|
||||||
|
if detail.property == 'cf'
|
||||||
|
detail.instance_variable_set "@custom_field", fields_by_id[detail.prop_key.to_i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
journals
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -129,4 +180,14 @@ class Journal < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
if notify? && (Setting.notified_events.include?('issue_updated') ||
|
||||||
|
(Setting.notified_events.include?('issue_note_added') && notes.present?) ||
|
||||||
|
(Setting.notified_events.include?('issue_status_updated') && new_status.present?) ||
|
||||||
|
(Setting.notified_events.include?('issue_priority_updated') && new_value_for('priority_id').present?)
|
||||||
|
)
|
||||||
|
Mailer.deliver_issue_edit(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ class JournalDetail < ActiveRecord::Base
|
|||||||
belongs_to :journal
|
belongs_to :journal
|
||||||
before_save :normalize_values
|
before_save :normalize_values
|
||||||
|
|
||||||
|
def custom_field
|
||||||
|
if property == 'cf'
|
||||||
|
@custom_field ||= CustomField.find_by_id(prop_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def normalize_values
|
def normalize_values
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# Redmine - project management software
|
|
||||||
# Copyright (C) 2006-2013 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 JournalObserver < ActiveRecord::Observer
|
|
||||||
def after_create(journal)
|
|
||||||
if journal.notify? &&
|
|
||||||
(Setting.notified_events.include?('issue_updated') ||
|
|
||||||
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
|
|
||||||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
|
|
||||||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
|
|
||||||
)
|
|
||||||
Mailer.issue_edit(journal).deliver
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+83
-50
@@ -38,12 +38,27 @@ class MailHandler < ActionMailer::Base
|
|||||||
# Status overridable by default
|
# Status overridable by default
|
||||||
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
|
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
|
||||||
|
|
||||||
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
|
@@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
|
||||||
|
@@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
|
||||||
|
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
|
||||||
|
|
||||||
email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
|
email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
|
||||||
super(email)
|
super(email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Extracts MailHandler options from environment variables
|
||||||
|
# Use when receiving emails with rake tasks
|
||||||
|
def self.extract_options_from_env(env)
|
||||||
|
options = {:issue => {}}
|
||||||
|
%w(project status tracker category priority).each do |option|
|
||||||
|
options[:issue][option.to_sym] = env[option] if env[option]
|
||||||
|
end
|
||||||
|
%w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
|
||||||
|
options[option.to_sym] = env[option] if env[option]
|
||||||
|
end
|
||||||
|
options
|
||||||
|
end
|
||||||
|
|
||||||
def logger
|
def logger
|
||||||
Rails.logger
|
Rails.logger
|
||||||
end
|
end
|
||||||
@@ -61,7 +76,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
sender_email = email.from.to_a.first.to_s.strip
|
sender_email = email.from.to_a.first.to_s.strip
|
||||||
# Ignore emails received from the application emission address to avoid hell cycles
|
# Ignore emails received from the application emission address to avoid hell cycles
|
||||||
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
|
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
|
||||||
if logger && logger.info
|
if logger
|
||||||
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
|
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -72,7 +87,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
if value
|
if value
|
||||||
value = value.to_s.downcase
|
value = value.to_s.downcase
|
||||||
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
|
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
|
||||||
if logger && logger.info
|
if logger
|
||||||
logger.info "MailHandler: ignoring email with #{key}:#{value} header"
|
logger.info "MailHandler: ignoring email with #{key}:#{value} header"
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -81,7 +96,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
end
|
end
|
||||||
@user = User.find_by_mail(sender_email) if sender_email.present?
|
@user = User.find_by_mail(sender_email) if sender_email.present?
|
||||||
if @user && !@user.active?
|
if @user && !@user.active?
|
||||||
if logger && logger.info
|
if logger
|
||||||
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
|
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -94,19 +109,22 @@ class MailHandler < ActionMailer::Base
|
|||||||
when 'create'
|
when 'create'
|
||||||
@user = create_user_from_email
|
@user = create_user_from_email
|
||||||
if @user
|
if @user
|
||||||
if logger && logger.info
|
if logger
|
||||||
logger.info "MailHandler: [#{@user.login}] account created"
|
logger.info "MailHandler: [#{@user.login}] account created"
|
||||||
end
|
end
|
||||||
Mailer.account_information(@user, @user.password).deliver
|
add_user_to_group(@@handler_options[:default_group])
|
||||||
|
unless @@handler_options[:no_account_notice]
|
||||||
|
Mailer.account_information(@user, @user.password).deliver
|
||||||
|
end
|
||||||
else
|
else
|
||||||
if logger && logger.error
|
if logger
|
||||||
logger.error "MailHandler: could not create account for [#{sender_email}]"
|
logger.error "MailHandler: could not create account for [#{sender_email}]"
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# Default behaviour, emails from unknown users are ignored
|
# Default behaviour, emails from unknown users are ignored
|
||||||
if logger && logger.info
|
if logger
|
||||||
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
|
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -118,7 +136,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
|
MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
|
||||||
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
|
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
|
||||||
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
|
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
|
||||||
|
|
||||||
@@ -177,7 +195,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
add_watchers(issue)
|
add_watchers(issue)
|
||||||
issue.save!
|
issue.save!
|
||||||
add_attachments(issue)
|
add_attachments(issue)
|
||||||
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
|
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
|
||||||
issue
|
issue
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -206,7 +224,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
journal.notes = cleaned_up_text_body
|
journal.notes = cleaned_up_text_body
|
||||||
add_attachments(issue)
|
add_attachments(issue)
|
||||||
issue.save!
|
issue.save!
|
||||||
if logger && logger.info
|
if logger
|
||||||
logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
|
logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
|
||||||
end
|
end
|
||||||
journal
|
journal
|
||||||
@@ -239,7 +257,7 @@ class MailHandler < ActionMailer::Base
|
|||||||
add_attachments(reply)
|
add_attachments(reply)
|
||||||
reply
|
reply
|
||||||
else
|
else
|
||||||
if logger && logger.info
|
if logger
|
||||||
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
|
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -249,32 +267,29 @@ class MailHandler < ActionMailer::Base
|
|||||||
def add_attachments(obj)
|
def add_attachments(obj)
|
||||||
if email.attachments && email.attachments.any?
|
if email.attachments && email.attachments.any?
|
||||||
email.attachments.each do |attachment|
|
email.attachments.each do |attachment|
|
||||||
filename = attachment.filename
|
next unless accept_attachment?(attachment)
|
||||||
unless filename.respond_to?(:encoding)
|
|
||||||
# try to reencode to utf8 manually with ruby1.8
|
|
||||||
h = attachment.header['Content-Disposition']
|
|
||||||
unless h.nil?
|
|
||||||
begin
|
|
||||||
if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
|
|
||||||
filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
|
|
||||||
elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
|
|
||||||
# http://tools.ietf.org/html/rfc2047#section-4
|
|
||||||
filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
# nop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
obj.attachments << Attachment.create(:container => obj,
|
obj.attachments << Attachment.create(:container => obj,
|
||||||
:file => attachment.decoded,
|
:file => attachment.decoded,
|
||||||
:filename => filename,
|
:filename => attachment.filename,
|
||||||
:author => user,
|
:author => user,
|
||||||
:content_type => attachment.mime_type)
|
:content_type => attachment.mime_type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns false if the +attachment+ of the incoming email should be ignored
|
||||||
|
def accept_attachment?(attachment)
|
||||||
|
@excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
|
||||||
|
@excluded.each do |pattern|
|
||||||
|
regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
|
||||||
|
if attachment.filename.to_s =~ regexp
|
||||||
|
logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
# Adds To and Cc as watchers of the given object if the sender has the
|
# Adds To and Cc as watchers of the given object if the sender has the
|
||||||
# appropriate permission
|
# appropriate permission
|
||||||
def add_watchers(obj)
|
def add_watchers(obj)
|
||||||
@@ -332,6 +347,13 @@ class MailHandler < ActionMailer::Base
|
|||||||
# * parse the email To field
|
# * parse the email To field
|
||||||
# * specific project (eg. Setting.mail_handler_target_project)
|
# * specific project (eg. Setting.mail_handler_target_project)
|
||||||
target = Project.find_by_identifier(get_keyword(:project))
|
target = Project.find_by_identifier(get_keyword(:project))
|
||||||
|
if target.nil?
|
||||||
|
# Invalid project keyword, use the project specified as the default one
|
||||||
|
default_project = @@handler_options[:issue][:project]
|
||||||
|
if default_project.present?
|
||||||
|
target = Project.find_by_identifier(default_project)
|
||||||
|
end
|
||||||
|
end
|
||||||
raise MissingInformation.new('Unable to determine target project') if target.nil?
|
raise MissingInformation.new('Unable to determine target project') if target.nil?
|
||||||
target
|
target
|
||||||
end
|
end
|
||||||
@@ -376,12 +398,21 @@ class MailHandler < ActionMailer::Base
|
|||||||
def plain_text_body
|
def plain_text_body
|
||||||
return @plain_text_body unless @plain_text_body.nil?
|
return @plain_text_body unless @plain_text_body.nil?
|
||||||
|
|
||||||
part = email.text_part || email.html_part || email
|
parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
|
||||||
@plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
|
text_parts
|
||||||
|
elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
|
||||||
|
html_parts
|
||||||
|
else
|
||||||
|
[email]
|
||||||
|
end
|
||||||
|
@plain_text_body = parts.map {|p| Redmine::CodesetUtil.to_utf8(p.body.decoded, p.charset)}.join("\r\n")
|
||||||
|
|
||||||
# strip html tags and remove doctype directive
|
# strip html tags and remove doctype directive
|
||||||
@plain_text_body = strip_tags(@plain_text_body.strip)
|
if parts.any? {|p| p.mime_type == 'text/html'}
|
||||||
@plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
|
@plain_text_body = strip_tags(@plain_text_body.strip)
|
||||||
|
@plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
|
||||||
|
end
|
||||||
|
|
||||||
@plain_text_body
|
@plain_text_body
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -391,19 +422,6 @@ class MailHandler < ActionMailer::Base
|
|||||||
|
|
||||||
def cleaned_up_subject
|
def cleaned_up_subject
|
||||||
subject = email.subject.to_s
|
subject = email.subject.to_s
|
||||||
unless subject.respond_to?(:encoding)
|
|
||||||
# try to reencode to utf8 manually with ruby1.8
|
|
||||||
begin
|
|
||||||
if h = email.header[:subject]
|
|
||||||
# http://tools.ietf.org/html/rfc2047#section-4
|
|
||||||
if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
|
|
||||||
subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
# nop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
subject.strip[0,255]
|
subject.strip[0,255]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -429,10 +447,9 @@ class MailHandler < ActionMailer::Base
|
|||||||
assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
|
assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
|
||||||
assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
|
assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
|
||||||
user.lastname = '-' if user.lastname.blank?
|
user.lastname = '-' if user.lastname.blank?
|
||||||
|
|
||||||
password_length = [Setting.password_min_length.to_i, 10].max
|
|
||||||
user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
|
|
||||||
user.language = Setting.default_language
|
user.language = Setting.default_language
|
||||||
|
user.generate_password = true
|
||||||
|
user.mail_notification = 'only_my_events'
|
||||||
|
|
||||||
unless user.valid?
|
unless user.valid?
|
||||||
user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
|
user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
|
||||||
@@ -453,6 +470,9 @@ class MailHandler < ActionMailer::Base
|
|||||||
end
|
end
|
||||||
if addr.present?
|
if addr.present?
|
||||||
user = self.class.new_user_from_attributes(addr, name)
|
user = self.class.new_user_from_attributes(addr, name)
|
||||||
|
if @@handler_options[:no_notification]
|
||||||
|
user.mail_notification = 'none'
|
||||||
|
end
|
||||||
if user.save
|
if user.save
|
||||||
user
|
user
|
||||||
else
|
else
|
||||||
@@ -465,6 +485,19 @@ class MailHandler < ActionMailer::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Adds the newly created user to default group
|
||||||
|
def add_user_to_group(default_group)
|
||||||
|
if default_group.present?
|
||||||
|
default_group.split(',').each do |group_name|
|
||||||
|
if group = Group.named(group_name).first
|
||||||
|
group.users << @user
|
||||||
|
elsif logger
|
||||||
|
logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Removes the email body of text after the truncation configurations.
|
# Removes the email body of text after the truncation configurations.
|
||||||
def cleanup_body(body)
|
def cleanup_body(body)
|
||||||
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
|
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
|
||||||
|
|||||||
+84
-64
@@ -27,34 +27,35 @@ class Mailer < ActionMailer::Base
|
|||||||
{ :host => Setting.host_name, :protocol => Setting.protocol }
|
{ :host => Setting.host_name, :protocol => Setting.protocol }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Builds a Mail::Message object used to email recipients of the added issue.
|
# Builds a mail for notifying to_users and cc_users about a new issue
|
||||||
#
|
def issue_add(issue, to_users, cc_users)
|
||||||
# Example:
|
|
||||||
# issue_add(issue) => Mail::Message object
|
|
||||||
# Mailer.issue_add(issue).deliver => sends an email to issue recipients
|
|
||||||
def issue_add(issue)
|
|
||||||
redmine_headers 'Project' => issue.project.identifier,
|
redmine_headers 'Project' => issue.project.identifier,
|
||||||
'Issue-Id' => issue.id,
|
'Issue-Id' => issue.id,
|
||||||
'Issue-Author' => issue.author.login
|
'Issue-Author' => issue.author.login
|
||||||
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
|
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
|
||||||
message_id issue
|
message_id issue
|
||||||
|
references issue
|
||||||
@author = issue.author
|
@author = issue.author
|
||||||
@issue = issue
|
@issue = issue
|
||||||
|
@users = to_users + cc_users
|
||||||
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
|
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
|
||||||
recipients = issue.recipients
|
mail :to => to_users.map(&:mail),
|
||||||
cc = issue.watcher_recipients - recipients
|
:cc => cc_users.map(&:mail),
|
||||||
mail :to => recipients,
|
|
||||||
:cc => cc,
|
|
||||||
:subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
|
:subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Builds a Mail::Message object used to email recipients of the edited issue.
|
# Notifies users about a new issue
|
||||||
#
|
def self.deliver_issue_add(issue)
|
||||||
# Example:
|
to = issue.notified_users
|
||||||
# issue_edit(journal) => Mail::Message object
|
cc = issue.notified_watchers - to
|
||||||
# Mailer.issue_edit(journal).deliver => sends an email to issue recipients
|
issue.each_notification(to + cc) do |users|
|
||||||
def issue_edit(journal)
|
Mailer.issue_add(issue, to & users, cc & users).deliver
|
||||||
issue = journal.journalized.reload
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds a mail for notifying to_users and cc_users about an issue update
|
||||||
|
def issue_edit(journal, to_users, cc_users)
|
||||||
|
issue = journal.journalized
|
||||||
redmine_headers 'Project' => issue.project.identifier,
|
redmine_headers 'Project' => issue.project.identifier,
|
||||||
'Issue-Id' => issue.id,
|
'Issue-Id' => issue.id,
|
||||||
'Issue-Author' => issue.author.login
|
'Issue-Author' => issue.author.login
|
||||||
@@ -62,20 +63,31 @@ class Mailer < ActionMailer::Base
|
|||||||
message_id journal
|
message_id journal
|
||||||
references issue
|
references issue
|
||||||
@author = journal.user
|
@author = journal.user
|
||||||
recipients = journal.recipients
|
|
||||||
# Watchers in cc
|
|
||||||
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
|
||||||
@issue = issue
|
@issue = issue
|
||||||
|
@users = to_users + cc_users
|
||||||
@journal = journal
|
@journal = journal
|
||||||
|
@journal_details = journal.visible_details(@users.first)
|
||||||
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
|
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
|
||||||
mail :to => recipients,
|
mail :to => to_users.map(&:mail),
|
||||||
:cc => cc,
|
:cc => cc_users.map(&:mail),
|
||||||
:subject => s
|
:subject => s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Notifies users about an issue update
|
||||||
|
def self.deliver_issue_edit(journal)
|
||||||
|
issue = journal.journalized.reload
|
||||||
|
to = journal.notified_users
|
||||||
|
cc = journal.notified_watchers
|
||||||
|
journal.each_notification(to + cc) do |users|
|
||||||
|
issue.each_notification(users) do |users2|
|
||||||
|
Mailer.issue_edit(journal, to & users2, cc & users2).deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def reminder(user, issues, days)
|
def reminder(user, issues, days)
|
||||||
set_language_if_valid user.language
|
set_language_if_valid user.language
|
||||||
@issues = issues
|
@issues = issues
|
||||||
@@ -142,6 +154,7 @@ class Mailer < ActionMailer::Base
|
|||||||
redmine_headers 'Project' => news.project.identifier
|
redmine_headers 'Project' => news.project.identifier
|
||||||
@author = news.author
|
@author = news.author
|
||||||
message_id news
|
message_id news
|
||||||
|
references news
|
||||||
@news = news
|
@news = news
|
||||||
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
||||||
mail :to => news.recipients,
|
mail :to => news.recipients,
|
||||||
@@ -158,6 +171,7 @@ class Mailer < ActionMailer::Base
|
|||||||
redmine_headers 'Project' => news.project.identifier
|
redmine_headers 'Project' => news.project.identifier
|
||||||
@author = comment.author
|
@author = comment.author
|
||||||
message_id comment
|
message_id comment
|
||||||
|
references news
|
||||||
@news = news
|
@news = news
|
||||||
@comment = comment
|
@comment = comment
|
||||||
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
|
||||||
@@ -176,7 +190,7 @@ class Mailer < ActionMailer::Base
|
|||||||
'Topic-Id' => (message.parent_id || message.id)
|
'Topic-Id' => (message.parent_id || message.id)
|
||||||
@author = message.author
|
@author = message.author
|
||||||
message_id message
|
message_id message
|
||||||
references message.parent unless message.parent.nil?
|
references message.root
|
||||||
recipients = message.recipients
|
recipients = message.recipients
|
||||||
cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
|
cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
|
||||||
@message = message
|
@message = message
|
||||||
@@ -297,31 +311,6 @@ class Mailer < ActionMailer::Base
|
|||||||
:subject => 'Redmine test'
|
:subject => 'Redmine test'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Overrides default deliver! method to prevent from sending an email
|
|
||||||
# with no recipient, cc or bcc
|
|
||||||
def deliver!(mail = @mail)
|
|
||||||
set_language_if_valid @initial_language
|
|
||||||
return false if (recipients.nil? || recipients.empty?) &&
|
|
||||||
(cc.nil? || cc.empty?) &&
|
|
||||||
(bcc.nil? || bcc.empty?)
|
|
||||||
|
|
||||||
|
|
||||||
# Log errors when raise_delivery_errors is set to false, Rails does not
|
|
||||||
raise_errors = self.class.raise_delivery_errors
|
|
||||||
self.class.raise_delivery_errors = true
|
|
||||||
begin
|
|
||||||
return super(mail)
|
|
||||||
rescue Exception => e
|
|
||||||
if raise_errors
|
|
||||||
raise e
|
|
||||||
elsif mylogger
|
|
||||||
mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
self.class.raise_delivery_errors = raise_errors
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sends reminders to issue assignees
|
# Sends reminders to issue assignees
|
||||||
# Available options:
|
# Available options:
|
||||||
# * :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)
|
||||||
@@ -379,7 +368,7 @@ class Mailer < ActionMailer::Base
|
|||||||
ActionMailer::Base.delivery_method = saved_method
|
ActionMailer::Base.delivery_method = saved_method
|
||||||
end
|
end
|
||||||
|
|
||||||
def mail(headers={})
|
def mail(headers={}, &block)
|
||||||
headers.merge! 'X-Mailer' => 'Redmine',
|
headers.merge! 'X-Mailer' => 'Redmine',
|
||||||
'X-Redmine-Host' => Setting.host_name,
|
'X-Redmine-Host' => Setting.host_name,
|
||||||
'X-Redmine-Site' => Setting.app_title,
|
'X-Redmine-Site' => Setting.app_title,
|
||||||
@@ -389,8 +378,9 @@ class Mailer < ActionMailer::Base
|
|||||||
'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
|
'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
|
||||||
|
|
||||||
# Removes the author from the recipients and cc
|
# Removes the author from the recipients and cc
|
||||||
# if he doesn't want to receive notifications about what he does
|
# if the author does not want to receive notifications
|
||||||
if @author && @author.logged? && @author.pref[:no_self_notified]
|
# about what the author do
|
||||||
|
if @author && @author.logged? && @author.pref.no_self_notified
|
||||||
headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
|
headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
|
||||||
headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
|
headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
|
||||||
end
|
end
|
||||||
@@ -410,15 +400,20 @@ class Mailer < ActionMailer::Base
|
|||||||
headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
|
headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
|
||||||
end
|
end
|
||||||
if @references_objects
|
if @references_objects
|
||||||
headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
|
headers[:references] = @references_objects.collect {|o| "<#{self.class.references_for(o)}>"}.join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
super headers do |format|
|
m = if block_given?
|
||||||
format.text
|
super headers, &block
|
||||||
format.html unless Setting.plain_text_mail?
|
else
|
||||||
|
super headers do |format|
|
||||||
|
format.text
|
||||||
|
format.html unless Setting.plain_text_mail?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
set_language_if_valid @initial_language
|
set_language_if_valid @initial_language
|
||||||
|
|
||||||
|
m
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(*args)
|
def initialize(*args)
|
||||||
@@ -426,10 +421,20 @@ class Mailer < ActionMailer::Base
|
|||||||
set_language_if_valid Setting.default_language
|
set_language_if_valid Setting.default_language
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.deliver_mail(mail)
|
def self.deliver_mail(mail)
|
||||||
return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
|
return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
|
||||||
super
|
begin
|
||||||
|
# Log errors when raise_delivery_errors is set to false, Rails does not
|
||||||
|
mail.raise_delivery_errors = true
|
||||||
|
super
|
||||||
|
rescue Exception => e
|
||||||
|
if ActionMailer::Base.raise_delivery_errors
|
||||||
|
raise e
|
||||||
|
else
|
||||||
|
Rails.logger.error "Email delivery error: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.method_missing(method, *args, &block)
|
def self.method_missing(method, *args, &block)
|
||||||
@@ -448,15 +453,30 @@ class Mailer < ActionMailer::Base
|
|||||||
h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
|
h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a predictable Message-Id for the given object
|
def self.token_for(object, rand=true)
|
||||||
def self.message_id_for(object)
|
|
||||||
# id + timestamp should reduce the odds of a collision
|
|
||||||
# as far as we don't send multiple emails for the same object
|
|
||||||
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
|
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
|
||||||
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
|
hash = [
|
||||||
|
"redmine",
|
||||||
|
"#{object.class.name.demodulize.underscore}-#{object.id}",
|
||||||
|
timestamp.strftime("%Y%m%d%H%M%S")
|
||||||
|
]
|
||||||
|
if rand
|
||||||
|
hash << Redmine::Utils.random_hex(8)
|
||||||
|
end
|
||||||
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
|
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
|
||||||
host = "#{::Socket.gethostname}.redmine" if host.empty?
|
host = "#{::Socket.gethostname}.redmine" if host.empty?
|
||||||
"#{hash}@#{host}"
|
"#{hash.join('.')}@#{host}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a Message-Id for the given object
|
||||||
|
def self.message_id_for(object)
|
||||||
|
token_for(object, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a uniq token for a given object referenced by all notifications
|
||||||
|
# related to this object
|
||||||
|
def self.references_for(object)
|
||||||
|
token_for(object, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_id(object)
|
def message_id(object)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class Message < ActiveRecord::Base
|
|||||||
after_create :add_author_as_watcher, :reset_counters!
|
after_create :add_author_as_watcher, :reset_counters!
|
||||||
after_update :update_messages_board
|
after_update :update_messages_board
|
||||||
after_destroy :reset_counters!
|
after_destroy :reset_counters!
|
||||||
|
after_create :send_notification
|
||||||
|
|
||||||
scope :visible, lambda {|*args|
|
scope :visible, lambda {|*args|
|
||||||
includes(:board => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
|
includes(:board => :project).where(Project.allowed_to_condition(args.shift || User.current, :view_messages, *args))
|
||||||
@@ -105,4 +106,10 @@ class Message < ActiveRecord::Base
|
|||||||
def add_author_as_watcher
|
def add_author_as_watcher
|
||||||
Watcher.create(:watchable => self.root, :user => author)
|
Watcher.create(:watchable => self.root, :user => author)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
if Setting.notified_events.include?('message_posted')
|
||||||
|
Mailer.message_posted(self).deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Redmine - project management software
|
|
||||||
# Copyright (C) 2006-2013 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 MessageObserver < ActiveRecord::Observer
|
|
||||||
def after_create(message)
|
|
||||||
Mailer.message_posted(message).deliver if Setting.notified_events.include?('message_posted')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -33,6 +33,7 @@ class News < ActiveRecord::Base
|
|||||||
acts_as_watchable
|
acts_as_watchable
|
||||||
|
|
||||||
after_create :add_author_as_watcher
|
after_create :add_author_as_watcher
|
||||||
|
after_create :send_notification
|
||||||
|
|
||||||
scope :visible, lambda {|*args|
|
scope :visible, lambda {|*args|
|
||||||
includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_news, *args))
|
includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_news, *args))
|
||||||
@@ -63,4 +64,10 @@ class News < ActiveRecord::Base
|
|||||||
def add_author_as_watcher
|
def add_author_as_watcher
|
||||||
Watcher.create(:watchable => self, :user => author)
|
Watcher.create(:watchable => self, :user => author)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
if Setting.notified_events.include?('news_added')
|
||||||
|
Mailer.news_added(self).deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+40
-21
@@ -33,8 +33,6 @@ class Project < ActiveRecord::Base
|
|||||||
has_many :member_principals, :class_name => 'Member',
|
has_many :member_principals, :class_name => 'Member',
|
||||||
:include => :principal,
|
:include => :principal,
|
||||||
:conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
|
:conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
|
||||||
has_many :users, :through => :members
|
|
||||||
has_many :principals, :through => :member_principals, :source => :principal
|
|
||||||
|
|
||||||
has_many :enabled_modules, :dependent => :delete_all
|
has_many :enabled_modules, :dependent => :delete_all
|
||||||
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
|
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
|
||||||
@@ -92,7 +90,7 @@ class Project < ActiveRecord::Base
|
|||||||
scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
|
scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
|
||||||
scope :all_public, lambda { where(:is_public => true) }
|
scope :all_public, lambda { where(:is_public => true) }
|
||||||
scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
|
scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
|
||||||
scope :allowed_to, lambda {|*args|
|
scope :allowed_to, lambda {|*args|
|
||||||
user = User.current
|
user = User.current
|
||||||
permission = nil
|
permission = nil
|
||||||
if args.first.is_a?(Symbol)
|
if args.first.is_a?(Symbol)
|
||||||
@@ -188,7 +186,7 @@ class Project < ActiveRecord::Base
|
|||||||
else
|
else
|
||||||
statement_by_role = {}
|
statement_by_role = {}
|
||||||
unless options[:member]
|
unless options[:member]
|
||||||
role = user.logged? ? Role.non_member : Role.anonymous
|
role = user.builtin_role
|
||||||
if role.allowed_to?(permission)
|
if role.allowed_to?(permission)
|
||||||
statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
|
statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
|
||||||
end
|
end
|
||||||
@@ -215,6 +213,14 @@ class Project < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def principals
|
||||||
|
@principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def users
|
||||||
|
@users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
|
||||||
|
end
|
||||||
|
|
||||||
# Returns the Systemwide and project specific activities
|
# Returns the Systemwide and project specific activities
|
||||||
def activities(include_inactive=false)
|
def activities(include_inactive=false)
|
||||||
if include_inactive
|
if include_inactive
|
||||||
@@ -285,7 +291,10 @@ class Project < ActiveRecord::Base
|
|||||||
self.find(*args)
|
self.find(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias :base_reload :reload
|
||||||
def reload(*args)
|
def reload(*args)
|
||||||
|
@principals = nil
|
||||||
|
@users = nil
|
||||||
@shared_versions = nil
|
@shared_versions = nil
|
||||||
@rolled_up_versions = nil
|
@rolled_up_versions = nil
|
||||||
@rolled_up_trackers = nil
|
@rolled_up_trackers = nil
|
||||||
@@ -297,7 +306,7 @@ class Project < ActiveRecord::Base
|
|||||||
@actions_allowed = nil
|
@actions_allowed = nil
|
||||||
@start_date = nil
|
@start_date = nil
|
||||||
@due_date = nil
|
@due_date = nil
|
||||||
super
|
base_reload(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
@@ -442,26 +451,29 @@ class Project < ActiveRecord::Base
|
|||||||
# Returns a scope of the Versions on subprojects
|
# Returns a scope of the Versions on subprojects
|
||||||
def rolled_up_versions
|
def rolled_up_versions
|
||||||
@rolled_up_versions ||=
|
@rolled_up_versions ||=
|
||||||
Version.scoped(:include => :project,
|
Version.
|
||||||
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
|
includes(:project).
|
||||||
|
where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
|
||||||
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.
|
||||||
:conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
|
includes(:project).
|
||||||
|
where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
|
||||||
else
|
else
|
||||||
@shared_versions ||= begin
|
@shared_versions ||= begin
|
||||||
r = root? ? self : root
|
r = root? ? self : root
|
||||||
Version.scoped(:include => :project,
|
Version.
|
||||||
:conditions => "#{Project.table_name}.id = #{id}" +
|
includes(:project).
|
||||||
" OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
|
where("#{Project.table_name}.id = #{id}" +
|
||||||
" #{Version.table_name}.sharing = 'system'" +
|
" OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
|
||||||
" OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
|
" #{Version.table_name}.sharing = 'system'" +
|
||||||
" 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 >= #{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 = 'hierarchy')" +
|
" 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 = 'hierarchy')" +
|
||||||
|
"))")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -501,10 +513,14 @@ class Project < ActiveRecord::Base
|
|||||||
members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
|
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 a scope of all custom fields enabled for project issues
|
||||||
# (explictly associated custom fields and custom fields enabled for all projects)
|
# (explictly associated custom fields and custom fields enabled for all projects)
|
||||||
def all_issue_custom_fields
|
def all_issue_custom_fields
|
||||||
@all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
|
@all_issue_custom_fields ||= IssueCustomField.
|
||||||
|
sorted.
|
||||||
|
where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
|
||||||
|
" FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
|
||||||
|
" WHERE cfp.project_id = ?)", true, id)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns an array of all custom fields enabled for project time entries
|
# Returns an array of all custom fields enabled for project time entries
|
||||||
@@ -672,7 +688,7 @@ class Project < ActiveRecord::Base
|
|||||||
|
|
||||||
# Returns an auto-generated project identifier based on the last identifier used
|
# Returns an auto-generated project identifier based on the last identifier used
|
||||||
def self.next_identifier
|
def self.next_identifier
|
||||||
p = Project.order('created_on DESC').first
|
p = Project.order('id DESC').first
|
||||||
p.nil? ? nil : p.identifier.to_s.succ
|
p.nil? ? nil : p.identifier.to_s.succ
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -839,6 +855,9 @@ class Project < ActiveRecord::Base
|
|||||||
new_issue = Issue.new
|
new_issue = Issue.new
|
||||||
new_issue.copy_from(issue, :subtasks => false, :link => false)
|
new_issue.copy_from(issue, :subtasks => false, :link => false)
|
||||||
new_issue.project = self
|
new_issue.project = self
|
||||||
|
# Changing project resets the custom field values
|
||||||
|
# TODO: handle this in Issue#project=
|
||||||
|
new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
|
||||||
# Reassign fixed_versions by name, since names are unique per project
|
# Reassign fixed_versions by name, since names are unique per project
|
||||||
if issue.fixed_version && issue.fixed_version.project == project
|
if issue.fixed_version && issue.fixed_version.project == project
|
||||||
new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
|
new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
|
||||||
@@ -942,7 +961,7 @@ class Project < ActiveRecord::Base
|
|||||||
|
|
||||||
def allowed_permissions
|
def allowed_permissions
|
||||||
@allowed_permissions ||= begin
|
@allowed_permissions ||= begin
|
||||||
module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
|
module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
|
||||||
Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
|
Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+87
-45
@@ -81,8 +81,12 @@ class QueryCustomFieldColumn < QueryColumn
|
|||||||
end
|
end
|
||||||
|
|
||||||
def value(object)
|
def value(object)
|
||||||
cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
|
if custom_field.visible_by?(object.project, User.current)
|
||||||
cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
|
cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
|
||||||
|
cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def css_classes
|
def css_classes
|
||||||
@@ -116,17 +120,33 @@ class Query < ActiveRecord::Base
|
|||||||
class StatementInvalid < ::ActiveRecord::StatementInvalid
|
class StatementInvalid < ::ActiveRecord::StatementInvalid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
VISIBILITY_PRIVATE = 0
|
||||||
|
VISIBILITY_ROLES = 1
|
||||||
|
VISIBILITY_PUBLIC = 2
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
|
||||||
serialize :filters
|
serialize :filters
|
||||||
serialize :column_names
|
serialize :column_names
|
||||||
serialize :sort_criteria, Array
|
serialize :sort_criteria, Array
|
||||||
|
serialize :options, Hash
|
||||||
|
|
||||||
attr_protected :project_id, :user_id
|
attr_protected :project_id, :user_id
|
||||||
|
|
||||||
validates_presence_of :name
|
validates_presence_of :name
|
||||||
validates_length_of :name, :maximum => 255
|
validates_length_of :name, :maximum => 255
|
||||||
|
validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
|
||||||
validate :validate_query_filters
|
validate :validate_query_filters
|
||||||
|
validate do |query|
|
||||||
|
errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
after_save do |query|
|
||||||
|
if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
|
||||||
|
query.roles.clear
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class_attribute :operators
|
class_attribute :operators
|
||||||
self.operators = {
|
self.operators = {
|
||||||
@@ -245,9 +265,9 @@ class Query < ActiveRecord::Base
|
|||||||
def editable_by?(user)
|
def editable_by?(user)
|
||||||
return false unless user
|
return false unless user
|
||||||
# Admin can edit them all and regular users can edit their private queries
|
# Admin can edit them all and regular users can edit their private queries
|
||||||
return true if user.admin? || (!is_public && self.user_id == user.id)
|
return true if user.admin? || (is_private? && self.user_id == user.id)
|
||||||
# Members can not edit public queries that are for all project (only admin is allowed to)
|
# Members can not edit public queries that are for all project (only admin is allowed to)
|
||||||
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
|
def trackers
|
||||||
@@ -429,6 +449,10 @@ class Query < ActiveRecord::Base
|
|||||||
column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
|
column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_custom_field_column?
|
||||||
|
columns.any? {|column| column.is_a? QueryCustomFieldColumn}
|
||||||
|
end
|
||||||
|
|
||||||
def has_default_columns?
|
def has_default_columns?
|
||||||
column_names.nil? || column_names.empty?
|
column_names.nil? || column_names.empty?
|
||||||
end
|
end
|
||||||
@@ -545,6 +569,11 @@ class Query < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end if filters and valid?
|
end if filters and valid?
|
||||||
|
|
||||||
|
if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
|
||||||
|
# Excludes results for which the grouped custom field is not visible
|
||||||
|
filters_clauses << c.custom_field.visibility_by_project_condition
|
||||||
|
end
|
||||||
|
|
||||||
filters_clauses << project_statement
|
filters_clauses << project_statement
|
||||||
filters_clauses.reject!(&:blank?)
|
filters_clauses.reject!(&:blank?)
|
||||||
|
|
||||||
@@ -577,8 +606,14 @@ class Query < ActiveRecord::Base
|
|||||||
customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
|
customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
|
||||||
raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
|
raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
|
||||||
end
|
end
|
||||||
"#{queried_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 " +
|
where = sql_for_field(field, operator, value, db_table, db_field, true)
|
||||||
sql_for_field(field, operator, value, db_table, db_field, true) + ')'
|
if operator =~ /[<>]/
|
||||||
|
where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
|
||||||
|
end
|
||||||
|
"#{queried_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 (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
|
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
|
||||||
@@ -727,54 +762,61 @@ class Query < ActiveRecord::Base
|
|||||||
return sql
|
return sql
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_custom_fields_filters(custom_fields, assoc=nil)
|
# Adds a filter for the given custom field
|
||||||
return unless custom_fields.present?
|
def add_custom_field_filter(field, assoc=nil)
|
||||||
|
case field.field_format
|
||||||
|
when "text"
|
||||||
|
options = { :type => :text }
|
||||||
|
when "list"
|
||||||
|
options = { :type => :list_optional, :values => field.possible_values }
|
||||||
|
when "date"
|
||||||
|
options = { :type => :date }
|
||||||
|
when "bool"
|
||||||
|
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
|
||||||
|
when "int"
|
||||||
|
options = { :type => :integer }
|
||||||
|
when "float"
|
||||||
|
options = { :type => :float }
|
||||||
|
when "user", "version"
|
||||||
|
return unless project
|
||||||
|
values = field.possible_values_options(project)
|
||||||
|
if User.current.logged? && field.field_format == 'user'
|
||||||
|
values.unshift ["<< #{l(:label_me)} >>", "me"]
|
||||||
|
end
|
||||||
|
options = { :type => :list_optional, :values => values }
|
||||||
|
else
|
||||||
|
options = { :type => :string }
|
||||||
|
end
|
||||||
|
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
|
||||||
|
add_available_filter filter_id, options.merge({
|
||||||
|
:name => filter_name,
|
||||||
|
:format => field.field_format,
|
||||||
|
:field => field
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
custom_fields.select(&:is_filter?).sort.each do |field|
|
# Adds filters for the given custom fields scope
|
||||||
case field.field_format
|
def add_custom_fields_filters(scope, assoc=nil)
|
||||||
when "text"
|
scope.visible.where(:is_filter => true).sorted.each do |field|
|
||||||
options = { :type => :text }
|
add_custom_field_filter(field, assoc)
|
||||||
when "list"
|
|
||||||
options = { :type => :list_optional, :values => field.possible_values }
|
|
||||||
when "date"
|
|
||||||
options = { :type => :date }
|
|
||||||
when "bool"
|
|
||||||
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
|
|
||||||
when "int"
|
|
||||||
options = { :type => :integer }
|
|
||||||
when "float"
|
|
||||||
options = { :type => :float }
|
|
||||||
when "user", "version"
|
|
||||||
next unless project
|
|
||||||
values = field.possible_values_options(project)
|
|
||||||
if User.current.logged? && field.field_format == 'user'
|
|
||||||
values.unshift ["<< #{l(:label_me)} >>", "me"]
|
|
||||||
end
|
|
||||||
options = { :type => :list_optional, :values => values }
|
|
||||||
else
|
|
||||||
options = { :type => :string }
|
|
||||||
end
|
|
||||||
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
|
|
||||||
add_available_filter filter_id, options.merge({
|
|
||||||
:name => filter_name,
|
|
||||||
:format => field.field_format,
|
|
||||||
:field => field
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Adds filters for the given associations custom fields
|
||||||
def add_associations_custom_fields_filters(*associations)
|
def add_associations_custom_fields_filters(*associations)
|
||||||
fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
|
fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
|
||||||
associations.each do |assoc|
|
associations.each do |assoc|
|
||||||
association_klass = queried_class.reflect_on_association(assoc).klass
|
association_klass = queried_class.reflect_on_association(assoc).klass
|
||||||
fields_by_class.each do |field_class, fields|
|
fields_by_class.each do |field_class, fields|
|
||||||
if field_class.customized_class <= association_klass
|
if field_class.customized_class <= association_klass
|
||||||
add_custom_fields_filters(fields, assoc)
|
fields.sort.each do |field|
|
||||||
|
add_custom_field_filter(field, assoc)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+12
-13
@@ -249,19 +249,18 @@ class Repository < ActiveRecord::Base
|
|||||||
# Default behaviour is to search in cached changesets
|
# Default behaviour is to search in cached changesets
|
||||||
def latest_changesets(path, rev, limit=10)
|
def latest_changesets(path, rev, limit=10)
|
||||||
if path.blank?
|
if path.blank?
|
||||||
changesets.find(
|
changesets.
|
||||||
:all,
|
reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
|
||||||
:include => :user,
|
limit(limit).
|
||||||
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
|
preload(:user).
|
||||||
:limit => limit)
|
all
|
||||||
else
|
else
|
||||||
filechanges.find(
|
filechanges.
|
||||||
:all,
|
where("path = ?", path.with_leading_slash).
|
||||||
:include => {:changeset => :user},
|
reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
|
||||||
:conditions => ["path = ?", path.with_leading_slash],
|
limit(limit).
|
||||||
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
|
preload(:changeset => :user).
|
||||||
:limit => limit
|
collect(&:changeset)
|
||||||
).collect(&:changeset)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -393,7 +392,7 @@ class Repository < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_as_default?
|
def set_as_default?
|
||||||
new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
|
new_record? && project && Repository.where(:project_id => project.id).empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|||||||
@@ -68,15 +68,11 @@ class Repository::Bazaar < Repository
|
|||||||
full_path = File.join(root_url, e.path)
|
full_path = File.join(root_url, e.path)
|
||||||
e.size = File.stat(full_path).size if File.file?(full_path)
|
e.size = File.stat(full_path).size if File.file?(full_path)
|
||||||
end
|
end
|
||||||
c = Change.find(
|
c = Change.
|
||||||
:first,
|
includes(:changeset).
|
||||||
:include => :changeset,
|
where("#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id).
|
||||||
:conditions => [
|
order("#{Changeset.table_name}.revision DESC").
|
||||||
"#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?",
|
first
|
||||||
e.lastrev.revision,
|
|
||||||
id
|
|
||||||
],
|
|
||||||
:order => "#{Changeset.table_name}.revision DESC")
|
|
||||||
if c
|
if c
|
||||||
e.lastrev.identifier = c.changeset.revision
|
e.lastrev.identifier = c.changeset.revision
|
||||||
e.lastrev.name = c.changeset.revision
|
e.lastrev.name = c.changeset.revision
|
||||||
|
|||||||
@@ -143,14 +143,11 @@ class Repository::Cvs < Repository
|
|||||||
)
|
)
|
||||||
cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
|
cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
|
||||||
author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
|
author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
|
||||||
cs = changesets.find(
|
cs = changesets.where(
|
||||||
:first,
|
:committed_on => tmp_time - time_delta .. tmp_time + time_delta,
|
||||||
:conditions => {
|
:committer => author_utf8,
|
||||||
:committed_on => tmp_time - time_delta .. tmp_time + time_delta,
|
:comments => cmt
|
||||||
:committer => author_utf8,
|
).first
|
||||||
:comments => cmt
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# create a new changeset....
|
# create a new changeset....
|
||||||
unless cs
|
unless cs
|
||||||
# we use a temporaray revision number here (just for inserting)
|
# we use a temporaray revision number here (just for inserting)
|
||||||
@@ -185,10 +182,10 @@ class Repository::Cvs < Repository
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Renumber new changesets in chronological order
|
# Renumber new changesets in chronological order
|
||||||
Changeset.all(
|
Changeset.
|
||||||
:order => 'committed_on ASC, id ASC',
|
order('committed_on ASC, id ASC').
|
||||||
:conditions => ["repository_id = ? AND revision LIKE 'tmp%'", id]
|
where("repository_id = ? AND revision LIKE 'tmp%'", id).
|
||||||
).each do |changeset|
|
each do |changeset|
|
||||||
changeset.update_attribute :revision, next_revision_number
|
changeset.update_attribute :revision, next_revision_number
|
||||||
end
|
end
|
||||||
end # transaction
|
end # transaction
|
||||||
|
|||||||
@@ -191,13 +191,8 @@ class Repository::Git < Repository
|
|||||||
offset = 0
|
offset = 0
|
||||||
revisions_copy = revisions.clone # revisions will change
|
revisions_copy = revisions.clone # revisions will change
|
||||||
while offset < revisions_copy.size
|
while offset < revisions_copy.size
|
||||||
recent_changesets_slice = changesets.find(
|
scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid}
|
||||||
:all,
|
recent_changesets_slice = changesets.where(:scmid => scmids).all
|
||||||
:conditions => [
|
|
||||||
'scmid IN (?)',
|
|
||||||
revisions_copy.slice(offset, limit).map{|x| x.scmid}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# Subtract revisions that redmine already knows about
|
# Subtract revisions that redmine already knows about
|
||||||
recent_revisions = recent_changesets_slice.map{|c| c.scmid}
|
recent_revisions = recent_changesets_slice.map{|c| c.scmid}
|
||||||
revisions.reject!{|r| recent_revisions.include?(r.scmid)}
|
revisions.reject!{|r| recent_revisions.include?(r.scmid)}
|
||||||
@@ -246,13 +241,7 @@ class Repository::Git < Repository
|
|||||||
revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
|
revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
|
||||||
return [] if revisions.nil? || revisions.empty?
|
return [] if revisions.nil? || revisions.empty?
|
||||||
|
|
||||||
changesets.find(
|
changesets.where(:scmid => revisions.map {|c| c.scmid}).all
|
||||||
:all,
|
|
||||||
:conditions => [
|
|
||||||
"scmid IN (?)",
|
|
||||||
revisions.map!{|c| c.scmid}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_extra_info_of_changesets
|
def clear_extra_info_of_changesets
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ require 'redmine/scm/adapters/subversion_adapter'
|
|||||||
class Repository::Subversion < Repository
|
class Repository::Subversion < Repository
|
||||||
attr_protected :root_url
|
attr_protected :root_url
|
||||||
validates_presence_of :url
|
validates_presence_of :url
|
||||||
validates_format_of :url, :with => /\A(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
|
validates_format_of :url, :with => %r{\A(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+}i
|
||||||
|
|
||||||
def self.scm_adapter_class
|
def self.scm_adapter_class
|
||||||
Redmine::Scm::Adapters::SubversionAdapter
|
Redmine::Scm::Adapters::SubversionAdapter
|
||||||
|
|||||||
+2
-1
@@ -52,6 +52,7 @@ class Role < ActiveRecord::Base
|
|||||||
WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
|
WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
|
||||||
|
|
||||||
has_many :member_roles, :dependent => :destroy
|
has_many :member_roles, :dependent => :destroy
|
||||||
has_many :members, :through => :member_roles
|
has_many :members, :through => :member_roles
|
||||||
@@ -137,7 +138,7 @@ class Role < ActiveRecord::Base
|
|||||||
def anonymous?
|
def anonymous?
|
||||||
builtin == 2
|
builtin == 2
|
||||||
end
|
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?
|
||||||
|
|||||||
+74
-2
@@ -132,15 +132,87 @@ class Setting < ActiveRecord::Base
|
|||||||
def self.#{name}=(value)
|
def self.#{name}=(value)
|
||||||
self[:#{name}] = value
|
self[:#{name}] = value
|
||||||
end
|
end
|
||||||
END_SRC
|
END_SRC
|
||||||
class_eval src, __FILE__, __LINE__
|
class_eval src, __FILE__, __LINE__
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Sets a setting value from params
|
||||||
|
def self.set_from_params(name, params)
|
||||||
|
params = params.dup
|
||||||
|
params.delete_if {|v| v.blank? } if params.is_a?(Array)
|
||||||
|
|
||||||
|
m = "#{name}_from_params"
|
||||||
|
if respond_to? m
|
||||||
|
self[name.to_sym] = send m, params
|
||||||
|
else
|
||||||
|
self[name.to_sym] = params
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a hash suitable for commit_update_keywords setting
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# params = {:keywords => ['fixes', 'closes'], :status_id => ["3", "5"], :done_ratio => ["", "100"]}
|
||||||
|
# Setting.commit_update_keywords_from_params(params)
|
||||||
|
# # => [{'keywords => 'fixes', 'status_id' => "3"}, {'keywords => 'closes', 'status_id' => "5", 'done_ratio' => "100"}]
|
||||||
|
def self.commit_update_keywords_from_params(params)
|
||||||
|
s = []
|
||||||
|
if params.is_a?(Hash) && params.key?(:keywords) && params.values.all? {|v| v.is_a? Array}
|
||||||
|
attributes = params.except(:keywords).keys
|
||||||
|
params[:keywords].each_with_index do |keywords, i|
|
||||||
|
next if keywords.blank?
|
||||||
|
s << attributes.inject({}) {|h, a|
|
||||||
|
value = params[a][i].to_s
|
||||||
|
h[a.to_s] = value if value.present?
|
||||||
|
h
|
||||||
|
}.merge('keywords' => keywords)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
# Helper that returns an array based on per_page_options setting
|
# Helper that returns an array based on per_page_options setting
|
||||||
def self.per_page_options_array
|
def self.per_page_options_array
|
||||||
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
|
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Helper that returns a Hash with single update keywords as keys
|
||||||
|
def self.commit_update_keywords_array
|
||||||
|
a = []
|
||||||
|
if commit_update_keywords.is_a?(Array)
|
||||||
|
commit_update_keywords.each do |rule|
|
||||||
|
next unless rule.is_a?(Hash)
|
||||||
|
rule = rule.dup
|
||||||
|
rule.delete_if {|k, v| v.blank?}
|
||||||
|
keywords = rule['keywords'].to_s.downcase.split(",").map(&:strip).reject(&:blank?)
|
||||||
|
next if keywords.empty?
|
||||||
|
a << rule.merge('keywords' => keywords)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
a
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.commit_fix_keywords
|
||||||
|
ActiveSupport::Deprecation.warn "Setting.commit_fix_keywords is deprecated and will be removed in Redmine 3"
|
||||||
|
if commit_update_keywords.is_a?(Array)
|
||||||
|
commit_update_keywords.first && commit_update_keywords.first['keywords']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.commit_fix_status_id
|
||||||
|
ActiveSupport::Deprecation.warn "Setting.commit_fix_status_id is deprecated and will be removed in Redmine 3"
|
||||||
|
if commit_update_keywords.is_a?(Array)
|
||||||
|
commit_update_keywords.first && commit_update_keywords.first['status_id']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.commit_fix_done_ratio
|
||||||
|
ActiveSupport::Deprecation.warn "Setting.commit_fix_done_ratio is deprecated and will be removed in Redmine 3"
|
||||||
|
if commit_update_keywords.is_a?(Array)
|
||||||
|
commit_update_keywords.first && commit_update_keywords.first['done_ratio']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.openid?
|
def self.openid?
|
||||||
Object.const_defined?(:OpenID) && self[:openid].to_i > 0
|
Object.const_defined?(:OpenID) && self[:openid].to_i > 0
|
||||||
end
|
end
|
||||||
@@ -154,7 +226,7 @@ class Setting < ActiveRecord::Base
|
|||||||
clear_cache
|
clear_cache
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Clears the settings cache
|
# Clears the settings cache
|
||||||
def self.clear_cache
|
def self.clear_cache
|
||||||
@cached_settings.clear
|
@cached_settings.clear
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class TimeEntry < ActiveRecord::Base
|
|||||||
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
|
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
|
||||||
validates_numericality_of :hours, :allow_nil => true, :message => :invalid
|
validates_numericality_of :hours, :allow_nil => true, :message => :invalid
|
||||||
validates_length_of :comments, :maximum => 255, :allow_nil => true
|
validates_length_of :comments, :maximum => 255, :allow_nil => true
|
||||||
|
validates :spent_on, :date => true
|
||||||
before_validation :set_project_if_nil
|
before_validation :set_project_if_nil
|
||||||
validate :validate_time_entry
|
validate :validate_time_entry
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,15 @@ class TimeEntryActivity < Enumeration
|
|||||||
OptionName
|
OptionName
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def objects
|
||||||
|
TimeEntry.where(:activity_id => self_and_descendants(1).map(&:id))
|
||||||
|
end
|
||||||
|
|
||||||
def objects_count
|
def objects_count
|
||||||
time_entries.count
|
objects.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def transfer_relations(to)
|
def transfer_relations(to)
|
||||||
time_entries.update_all("activity_id = #{to.id}")
|
objects.update_all(:activity_id => to.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -84,15 +84,15 @@ class TimeEntryQuery < Query
|
|||||||
add_available_filter "comments", :type => :text
|
add_available_filter "comments", :type => :text
|
||||||
add_available_filter "hours", :type => :float
|
add_available_filter "hours", :type => :float
|
||||||
|
|
||||||
add_custom_fields_filters(TimeEntryCustomField.where(:is_filter => true).all)
|
add_custom_fields_filters(TimeEntryCustomField)
|
||||||
add_associations_custom_fields_filters :project, :issue, :user
|
add_associations_custom_fields_filters :project, :issue, :user
|
||||||
end
|
end
|
||||||
|
|
||||||
def available_columns
|
def available_columns
|
||||||
return @available_columns if @available_columns
|
return @available_columns if @available_columns
|
||||||
@available_columns = self.class.available_columns.dup
|
@available_columns = self.class.available_columns.dup
|
||||||
@available_columns += TimeEntryCustomField.all.map {|cf| QueryCustomFieldColumn.new(cf) }
|
@available_columns += TimeEntryCustomField.visible.all.map {|cf| QueryCustomFieldColumn.new(cf) }
|
||||||
@available_columns += IssueCustomField.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
|
@available_columns += IssueCustomField.visible.all.map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf) }
|
||||||
@available_columns
|
@available_columns
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -100,6 +100,15 @@ class TimeEntryQuery < Query
|
|||||||
@default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
|
@default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def results_scope(options={})
|
||||||
|
order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
|
||||||
|
|
||||||
|
TimeEntry.visible.
|
||||||
|
where(statement).
|
||||||
|
order(order_option).
|
||||||
|
joins(joins_for_order_statement(order_option.join(',')))
|
||||||
|
end
|
||||||
|
|
||||||
# Accepts :from/:to params as shortcut filters
|
# Accepts :from/:to params as shortcut filters
|
||||||
def build_from_params(params)
|
def build_from_params(params)
|
||||||
super
|
super
|
||||||
|
|||||||
+82
-29
@@ -81,7 +81,7 @@ class User < Principal
|
|||||||
|
|
||||||
acts_as_customizable
|
acts_as_customizable
|
||||||
|
|
||||||
attr_accessor :password, :password_confirmation
|
attr_accessor :password, :password_confirmation, :generate_password
|
||||||
attr_accessor :last_before_login_on
|
attr_accessor :last_before_login_on
|
||||||
# Prevents unauthorized assignments
|
# Prevents unauthorized assignments
|
||||||
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
|
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
|
||||||
@@ -103,8 +103,9 @@ class User < Principal
|
|||||||
validate :validate_password_length
|
validate :validate_password_length
|
||||||
|
|
||||||
before_create :set_mail_notification
|
before_create :set_mail_notification
|
||||||
before_save :update_hashed_password
|
before_save :generate_password_if_needed, :update_hashed_password
|
||||||
before_destroy :remove_references_before_destroy
|
before_destroy :remove_references_before_destroy
|
||||||
|
after_save :update_notified_project_ids
|
||||||
|
|
||||||
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
|
||||||
@@ -128,10 +129,15 @@ class User < Principal
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias :base_reload :reload
|
||||||
def reload(*args)
|
def reload(*args)
|
||||||
@name = nil
|
@name = nil
|
||||||
@projects_by_role = nil
|
@projects_by_role = nil
|
||||||
super
|
@membership_by_project_id = nil
|
||||||
|
@notified_projects_ids = nil
|
||||||
|
@notified_projects_ids_changed = false
|
||||||
|
@builtin_role = nil
|
||||||
|
base_reload(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mail=(arg)
|
def mail=(arg)
|
||||||
@@ -152,7 +158,7 @@ class User < Principal
|
|||||||
end
|
end
|
||||||
|
|
||||||
# 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, active_only=true)
|
||||||
login = login.to_s
|
login = login.to_s
|
||||||
password = password.to_s
|
password = password.to_s
|
||||||
|
|
||||||
@@ -161,8 +167,8 @@ class User < Principal
|
|||||||
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
|
||||||
return nil unless user.active?
|
|
||||||
return nil unless user.check_password?(password)
|
return nil unless user.check_password?(password)
|
||||||
|
return nil if !user.active? && active_only
|
||||||
else
|
else
|
||||||
# user is not yet registered, try to authenticate with available sources
|
# user is not yet registered, try to authenticate with available sources
|
||||||
attrs = AuthSource.authenticate(login, password)
|
attrs = AuthSource.authenticate(login, password)
|
||||||
@@ -176,7 +182,7 @@ class User < Principal
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
user.update_column(:last_login_on, Time.now) if user && !user.new_record?
|
user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
|
||||||
user
|
user
|
||||||
rescue => text
|
rescue => text
|
||||||
raise text
|
raise text
|
||||||
@@ -274,13 +280,20 @@ class User < Principal
|
|||||||
return auth_source.allow_password_changes?
|
return auth_source.allow_password_changes?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate and set a random password. Useful for automated user creation
|
def must_change_password?
|
||||||
# Based on Token#generate_token_value
|
must_change_passwd? && change_password_allowed?
|
||||||
#
|
end
|
||||||
def random_password
|
|
||||||
|
def generate_password?
|
||||||
|
generate_password == '1' || generate_password == true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate and set a random password on given length
|
||||||
|
def random_password(length=40)
|
||||||
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
||||||
|
chars -= %w(0 O 1 l)
|
||||||
password = ''
|
password = ''
|
||||||
40.times { |i| password << chars[rand(chars.size-1)] }
|
length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
|
||||||
self.password = password
|
self.password = password
|
||||||
self.password_confirmation = password
|
self.password_confirmation = password
|
||||||
self
|
self
|
||||||
@@ -320,12 +333,20 @@ class User < Principal
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notified_project_ids=(ids)
|
def notified_project_ids=(ids)
|
||||||
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
|
@notified_projects_ids_changed = true
|
||||||
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
|
@notified_projects_ids = ids
|
||||||
@notified_projects_ids = nil
|
|
||||||
notified_projects_ids
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Updates per project notifications (after_save callback)
|
||||||
|
def update_notified_project_ids
|
||||||
|
if @notified_projects_ids_changed
|
||||||
|
ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
|
||||||
|
members.update_all(:mail_notification => false)
|
||||||
|
members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
private :update_notified_project_ids
|
||||||
|
|
||||||
def valid_notification_options
|
def valid_notification_options
|
||||||
self.class.valid_notification_options(self)
|
self.class.valid_notification_options(self)
|
||||||
end
|
end
|
||||||
@@ -415,30 +436,38 @@ class User < Principal
|
|||||||
!logged?
|
!logged?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns user's membership for the given project
|
||||||
|
# or nil if the user is not a member of project
|
||||||
|
def membership(project)
|
||||||
|
project_id = project.is_a?(Project) ? project.id : project
|
||||||
|
|
||||||
|
@membership_by_project_id ||= Hash.new {|h, project_id|
|
||||||
|
h[project_id] = memberships.where(:project_id => project_id).first
|
||||||
|
}
|
||||||
|
@membership_by_project_id[project_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the user's bult-in role
|
||||||
|
def builtin_role
|
||||||
|
@builtin_role ||= Role.non_member
|
||||||
|
end
|
||||||
|
|
||||||
# Return user's roles for project
|
# Return user's roles for project
|
||||||
def roles_for_project(project)
|
def roles_for_project(project)
|
||||||
roles = []
|
roles = []
|
||||||
# No role on archived projects
|
# No role on archived projects
|
||||||
return roles if project.nil? || project.archived?
|
return roles if project.nil? || project.archived?
|
||||||
if logged?
|
if membership = membership(project)
|
||||||
# Find project membership
|
roles = membership.roles
|
||||||
membership = memberships.detect {|m| m.project_id == project.id}
|
|
||||||
if membership
|
|
||||||
roles = membership.roles
|
|
||||||
else
|
|
||||||
@role_non_member ||= Role.non_member
|
|
||||||
roles << @role_non_member
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
@role_anonymous ||= Role.anonymous
|
roles << builtin_role
|
||||||
roles << @role_anonymous
|
|
||||||
end
|
end
|
||||||
roles
|
roles
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return true if the user is a member of project
|
# Return true if the user is a member of project
|
||||||
def member_of?(project)
|
def member_of?(project)
|
||||||
roles_for_project(project).any? {|role| role.member?}
|
projects.to_a.include?(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a hash of user's projects grouped by roles
|
# Returns a hash of user's projects grouped by roles
|
||||||
@@ -523,7 +552,7 @@ class User < Principal
|
|||||||
allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
|
allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if the user is allowed to delete his own account
|
# Returns true if the user is allowed to delete the user's own account
|
||||||
def own_account_deletable?
|
def own_account_deletable?
|
||||||
Setting.unsubscribe? &&
|
Setting.unsubscribe? &&
|
||||||
(!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
|
(!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
|
||||||
@@ -534,6 +563,7 @@ class User < Principal
|
|||||||
'lastname',
|
'lastname',
|
||||||
'mail',
|
'mail',
|
||||||
'mail_notification',
|
'mail_notification',
|
||||||
|
'notified_project_ids',
|
||||||
'language',
|
'language',
|
||||||
'custom_field_values',
|
'custom_field_values',
|
||||||
'custom_fields',
|
'custom_fields',
|
||||||
@@ -541,6 +571,8 @@ class User < Principal
|
|||||||
|
|
||||||
safe_attributes 'status',
|
safe_attributes 'status',
|
||||||
'auth_source_id',
|
'auth_source_id',
|
||||||
|
'generate_password',
|
||||||
|
'must_change_passwd',
|
||||||
:if => lambda {|user, current_user| current_user.admin?}
|
:if => lambda {|user, current_user| current_user.admin?}
|
||||||
|
|
||||||
safe_attributes 'group_ids',
|
safe_attributes 'group_ids',
|
||||||
@@ -610,6 +642,7 @@ class User < Principal
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def validate_password_length
|
def validate_password_length
|
||||||
|
return if password.blank? && generate_password?
|
||||||
# Password length validation based on setting
|
# Password length validation based on setting
|
||||||
if !password.nil? && password.size < Setting.password_min_length.to_i
|
if !password.nil? && password.size < Setting.password_min_length.to_i
|
||||||
errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
|
errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
|
||||||
@@ -618,6 +651,13 @@ class User < Principal
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def generate_password_if_needed
|
||||||
|
if generate_password? && auth_source.nil?
|
||||||
|
length = [Setting.password_min_length.to_i + 2, 10].max
|
||||||
|
random_password(length)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Removes references that are not handled by associations
|
# Removes references that are not handled by associations
|
||||||
# Things that are not deleted are reassociated with the anonymous user
|
# Things that are not deleted are reassociated with the anonymous user
|
||||||
def remove_references_before_destroy
|
def remove_references_before_destroy
|
||||||
@@ -634,7 +674,7 @@ class User < Principal
|
|||||||
Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
||||||
News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
||||||
# Remove private queries and keep public ones
|
# Remove private queries and keep public ones
|
||||||
::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
|
::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
|
||||||
::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
|
::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
|
||||||
TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
|
TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
|
||||||
Token.delete_all ['user_id = ?', id]
|
Token.delete_all ['user_id = ?', id]
|
||||||
@@ -679,6 +719,19 @@ class AnonymousUser < User
|
|||||||
UserPreference.new(:user => self)
|
UserPreference.new(:user => self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns the user's bult-in role
|
||||||
|
def builtin_role
|
||||||
|
@builtin_role ||= Role.anonymous
|
||||||
|
end
|
||||||
|
|
||||||
|
def membership(*args)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def member_of?(*args)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
# Anonymous user can not be destroyed
|
# Anonymous user can not be destroyed
|
||||||
def destroy
|
def destroy
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class UserPreference < ActiveRecord::Base
|
|||||||
attr_protected :others, :user_id
|
attr_protected :others, :user_id
|
||||||
|
|
||||||
before_save :set_others_hash
|
before_save :set_others_hash
|
||||||
|
|
||||||
def initialize(attributes=nil, *args)
|
def initialize(attributes=nil, *args)
|
||||||
super
|
super
|
||||||
self.others ||= {}
|
self.others ||= {}
|
||||||
@@ -33,7 +33,7 @@ class UserPreference < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def [](attr_name)
|
def [](attr_name)
|
||||||
if attribute_present? attr_name
|
if has_attribute? attr_name
|
||||||
super
|
super
|
||||||
else
|
else
|
||||||
others ? others[attr_name] : nil
|
others ? others[attr_name] : nil
|
||||||
@@ -41,7 +41,7 @@ class UserPreference < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def []=(attr_name, value)
|
def []=(attr_name, value)
|
||||||
if attribute_present? attr_name
|
if has_attribute? attr_name
|
||||||
super
|
super
|
||||||
else
|
else
|
||||||
h = (read_attribute(:others) || {}).dup
|
h = (read_attribute(:others) || {}).dup
|
||||||
@@ -56,4 +56,7 @@ class UserPreference < ActiveRecord::Base
|
|||||||
|
|
||||||
def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
|
def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
|
||||||
def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
|
def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
|
||||||
|
|
||||||
|
def no_self_notified; (self[:no_self_notified] == true || self[:no_self_notified] == '1'); end
|
||||||
|
def no_self_notified=(value); self[:no_self_notified]=value; end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,14 +40,15 @@ class Version < ActiveRecord::Base
|
|||||||
includes(:project).where(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',
|
||||||
'effective_date',
|
'effective_date',
|
||||||
'due_date',
|
'due_date',
|
||||||
'wiki_page_title',
|
'wiki_page_title',
|
||||||
'status',
|
'status',
|
||||||
'sharing',
|
'sharing',
|
||||||
'custom_field_values'
|
'custom_field_values',
|
||||||
|
'custom_fields'
|
||||||
|
|
||||||
# Returns true if +user+ or current user is allowed to view the version
|
# Returns true if +user+ or current user is allowed to view the version
|
||||||
def visible?(user=User.current)
|
def visible?(user=User.current)
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ class Watcher < ActiveRecord::Base
|
|||||||
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
|
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
|
||||||
validate :validate_user
|
validate :validate_user
|
||||||
|
|
||||||
|
# Returns true if at least one object among objects is watched by user
|
||||||
|
def self.any_watched?(objects, user)
|
||||||
|
objects = objects.reject(&:new_record?)
|
||||||
|
if objects.any?
|
||||||
|
objects.group_by {|object| object.class.base_class}.each do |base_class, objects|
|
||||||
|
if Watcher.where(:watchable_type => base_class.name, :watchable_id => objects.map(&:id), :user_id => user.id).exists?
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
# Unwatch things that users are no longer allowed to view
|
# Unwatch things that users are no longer allowed to view
|
||||||
def self.prune(options={})
|
def self.prune(options={})
|
||||||
if options.has_key?(:user)
|
if options.has_key?(:user)
|
||||||
|
|||||||
+2
-2
@@ -50,10 +50,10 @@ class Wiki < ActiveRecord::Base
|
|||||||
@page_found_with_redirect = false
|
@page_found_with_redirect = false
|
||||||
title = start_page if title.blank?
|
title = start_page if title.blank?
|
||||||
title = Wiki.titleize(title)
|
title = Wiki.titleize(title)
|
||||||
page = pages.first(:conditions => ["LOWER(title) = LOWER(?)", title])
|
page = pages.where("LOWER(title) = LOWER(?)", title).first
|
||||||
if !page && !(options[:with_redirect] == false)
|
if !page && !(options[:with_redirect] == false)
|
||||||
# search for a redirect
|
# search for a redirect
|
||||||
redirect = redirects.first(:conditions => ["LOWER(title) = LOWER(?)", title])
|
redirect = redirects.where("LOWER(title) = LOWER(?)", title).first
|
||||||
if redirect
|
if redirect
|
||||||
page = find_page(redirect.redirects_to, :with_redirect => false)
|
page = find_page(redirect.redirects_to, :with_redirect => false)
|
||||||
@page_found_with_redirect = true
|
@page_found_with_redirect = true
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class WikiContent < ActiveRecord::Base
|
|||||||
|
|
||||||
acts_as_versioned
|
acts_as_versioned
|
||||||
|
|
||||||
|
after_save :send_notification
|
||||||
|
|
||||||
def visible?(user=User.current)
|
def visible?(user=User.current)
|
||||||
page.visible?(user)
|
page.visible?(user)
|
||||||
end
|
end
|
||||||
@@ -145,4 +147,19 @@ class WikiContent < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def send_notification
|
||||||
|
# new_record? returns false in after_save callbacks
|
||||||
|
if id_changed?
|
||||||
|
if Setting.notified_events.include?('wiki_content_added')
|
||||||
|
Mailer.wiki_content_added(self).deliver
|
||||||
|
end
|
||||||
|
elsif text_changed?
|
||||||
|
if Setting.notified_events.include?('wiki_content_updated')
|
||||||
|
Mailer.wiki_content_updated(self).deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -175,9 +175,10 @@ class WikiPage < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Saves the page and its content if text was changed
|
# Saves the page and its content if text was changed
|
||||||
def save_with_content
|
def save_with_content(content)
|
||||||
ret = nil
|
ret = nil
|
||||||
transaction do
|
transaction do
|
||||||
|
self.content = content
|
||||||
if new_record?
|
if new_record?
|
||||||
# Rails automatically saves associated content
|
# Rails automatically saves associated content
|
||||||
ret = save
|
ret = save
|
||||||
|
|||||||
@@ -43,11 +43,17 @@
|
|||||||
<% content_for :sidebar do %>
|
<% content_for :sidebar do %>
|
||||||
<%= form_tag({}, :method => :get) do %>
|
<%= form_tag({}, :method => :get) do %>
|
||||||
<h3><%= l(:label_activity) %></h3>
|
<h3><%= l(:label_activity) %></h3>
|
||||||
<p><% @activity.event_types.each do |t| %>
|
<ul>
|
||||||
<%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
|
<% @activity.event_types.each do |t| %>
|
||||||
<label for="show_<%=t%>"><%= link_to(l("label_#{t.singularize}_plural"), {"show_#{t}" => 1, :user_id => params[:user_id], :from => params[:from]})%></label>
|
<li>
|
||||||
<br />
|
<%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
|
||||||
<% end %></p>
|
<label for="show_<%=t%>">
|
||||||
|
<%= link_to(l("label_#{t.singularize}_plural"),
|
||||||
|
{"show_#{t}" => 1, :user_id => params[:user_id], :from => params[:from]})%>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
<% if @project && @project.descendants.active.any? %>
|
<% if @project && @project.descendants.active.any? %>
|
||||||
<%= hidden_field_tag 'with_subprojects', 0 %>
|
<%= hidden_field_tag 'with_subprojects', 0 %>
|
||||||
<p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
|
<p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h2><%= l(:label_plugins) %></h2>
|
<%= title l(:label_plugins) %>
|
||||||
|
|
||||||
<% if @plugins.any? %>
|
<% if @plugins.any? %>
|
||||||
<table class="list plugins">
|
<table class="list plugins">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
|
<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2><%=l(:label_project_plural)%></h2>
|
<%= title l(:label_project_plural) %>
|
||||||
|
|
||||||
<%= form_tag({}, :method => :get) do %>
|
<%= form_tag({}, :method => :get) do %>
|
||||||
<fieldset><legend><%= l(:label_filter_plural) %></legend>
|
<fieldset><legend><%= l(:label_filter_plural) %></legend>
|
||||||
@@ -41,5 +41,3 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% html_title(l(:label_project_plural)) -%>
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
var fileSpan = $('#attachments_<%= j params[:attachment_id] %>');
|
var fileSpan = $('#attachments_<%= j params[:attachment_id] %>');
|
||||||
|
<% if @attachment.new_record? %>
|
||||||
|
fileSpan.hide();
|
||||||
|
alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
|
||||||
|
<% else %>
|
||||||
$('<input>', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan);
|
$('<input>', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan);
|
||||||
fileSpan.find('a.remove-upload')
|
fileSpan.find('a.remove-upload')
|
||||||
.attr({
|
.attr({
|
||||||
@@ -7,3 +11,4 @@ fileSpan.find('a.remove-upload')
|
|||||||
href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>'
|
href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>'
|
||||||
})
|
})
|
||||||
.off('click');
|
.off('click');
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h2><%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
|
<%= title [l(:label_auth_source_plural), auth_sources_path], @auth_source.name %>
|
||||||
|
|
||||||
<%= labelled_form_for @auth_source, :as => :auth_source, :url => auth_source_path(@auth_source), :html => {:id => 'auth_source_form'} do |f| %>
|
<%= labelled_form_for @auth_source, :as => :auth_source, :url => auth_source_path(@auth_source), :html => {:id => 'auth_source_form'} do |f| %>
|
||||||
<%= render :partial => auth_source_partial_name(@auth_source), :locals => { :f => f } %>
|
<%= render :partial => auth_source_partial_name(@auth_source), :locals => { :f => f } %>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %>
|
<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2><%=l(:label_auth_source_plural)%></h2>
|
<%= title l(:label_auth_source_plural) %>
|
||||||
|
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h2><%=l(:label_auth_source_new)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
|
<%= title [l(:label_auth_source_plural), auth_sources_path], "#{l(:label_auth_source_new)} (#{@auth_source.auth_method_name})" %>
|
||||||
|
|
||||||
<%= labelled_form_for @auth_source, :as => :auth_source, :url => auth_sources_path, :html => {:id => 'auth_source_form'} do |f| %>
|
<%= labelled_form_for @auth_source, :as => :auth_source, :url => auth_sources_path, :html => {:id => 'auth_source_form'} do |f| %>
|
||||||
<%= hidden_field_tag 'type', @auth_source.type %>
|
<%= hidden_field_tag 'type', @auth_source.type %>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4" class="filename">
|
<th colspan="4" class="filename">
|
||||||
<%= h(Redmine::CodesetUtil.to_utf8_by_setting(table_file.file_name)) %>
|
<%= table_file.file_name %>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -24,11 +24,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="line-num"><%= line.nb_line_left %></th>
|
<th class="line-num"><%= line.nb_line_left %></th>
|
||||||
<td class="line-code <%= line.type_diff_left %>">
|
<td class="line-code <%= line.type_diff_left %>">
|
||||||
<pre><%= Redmine::CodesetUtil.to_utf8_by_setting(line.html_line_left).html_safe %></pre>
|
<pre><%= line.html_line_left.html_safe %></pre>
|
||||||
</td>
|
</td>
|
||||||
<th class="line-num"><%= line.nb_line_right %></th>
|
<th class="line-num"><%= line.nb_line_right %></th>
|
||||||
<td class="line-code <%= line.type_diff_right %>">
|
<td class="line-code <%= line.type_diff_right %>">
|
||||||
<pre><%= Redmine::CodesetUtil.to_utf8_by_setting(line.html_line_right).html_safe %></pre>
|
<pre><%= line.html_line_right.html_safe %></pre>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="3" class="filename">
|
<th colspan="3" class="filename">
|
||||||
<%= h(Redmine::CodesetUtil.to_utf8_by_setting(table_file.file_name)) %>
|
<%= table_file.file_name %>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<th class="line-num"><%= line.nb_line_left %></th>
|
<th class="line-num"><%= line.nb_line_left %></th>
|
||||||
<th class="line-num"><%= line.nb_line_right %></th>
|
<th class="line-num"><%= line.nb_line_right %></th>
|
||||||
<td class="line-code <%= line.type_diff %>">
|
<td class="line-code <%= line.type_diff %>">
|
||||||
<pre><%= Redmine::CodesetUtil.to_utf8_by_setting(line.html_line).html_safe %></pre>
|
<pre><%= line.html_line.html_safe %></pre>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% line_num = 1 %>
|
<% line_num = 1 %>
|
||||||
<% syntax_highlight_lines(filename, Redmine::CodesetUtil.to_utf8_by_setting(content)).each do |line| %>
|
<% syntax_highlight_lines(filename, Redmine::CodesetUtil.to_utf8_by_setting(content)).each do |line| %>
|
||||||
<tr>
|
<tr id="L<%= line_num %>">
|
||||||
<th class="line-num" id="L<%= line_num %>">
|
<th class="line-num">
|
||||||
<a href="#L<%= line_num %>"><%= line_num %></a>
|
<a href="#L<%= line_num %>"><%= line_num %></a>
|
||||||
</th>
|
</th>
|
||||||
<td class="line-code">
|
<td class="line-code">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<li><%= link_to l(tab[:label]), { :tab => tab[:name] },
|
<li><%= link_to l(tab[:label]), { :tab => tab[:name] },
|
||||||
:id => "tab-#{tab[:name]}",
|
:id => "tab-#{tab[:name]}",
|
||||||
:class => (tab[:name] != selected_tab ? nil : 'selected'),
|
:class => (tab[:name] != selected_tab ? nil : 'selected'),
|
||||||
:onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
|
:onclick => "showTab('#{tab[:name]}', this.href); this.blur(); return false;" %></li>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tabs-buttons" style="display:none;">
|
<div class="tabs-buttons" style="display:none;">
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<li><%= context_menu_link l(:button_copy), bulk_edit_issues_path(:ids => @issue_ids, :copy => '1'),
|
<li><%= context_menu_link l(:button_copy), bulk_edit_issues_path(:ids => @issue_ids, :copy => '1'),
|
||||||
:class => 'icon-copy', :disabled => !@can[:move] %></li>
|
:class => 'icon-copy', :disabled => !@can[:move] %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li><%= context_menu_link l(:button_delete), issues_path(:ids => @issue_ids, :back_url => @back),
|
<li><%= context_menu_link l(:button_delete), issues_path(:ids => @issue_ids, :back_url => @back),
|
||||||
:method => :delete, :data => {:confirm => issues_destroy_confirmation_message(@issues)}, :class => 'icon-del', :disabled => !@can[:delete] %></li>
|
:method => :delete, :data => {:confirm => issues_destroy_confirmation_message(@issues)}, :class => 'icon-del', :disabled => !@can[:delete] %></li>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<%= error_messages_for 'custom_field' %>
|
<%= error_messages_for 'custom_field' %>
|
||||||
|
|
||||||
|
<% if @custom_field.is_a?(IssueCustomField) %>
|
||||||
|
<div class="splitcontentleft">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="box tabular">
|
<div class="box tabular">
|
||||||
<p><%= f.text_field :name, :required => true %></p>
|
<p><%= f.text_field :name, :required => true %></p>
|
||||||
<p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :disabled => !@custom_field.new_record? %></p>
|
<p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :disabled => !@custom_field.new_record? %></p>
|
||||||
@@ -46,24 +50,28 @@
|
|||||||
<div class="box tabular">
|
<div class="box tabular">
|
||||||
<% case @custom_field.class.name
|
<% case @custom_field.class.name
|
||||||
when "IssueCustomField" %>
|
when "IssueCustomField" %>
|
||||||
|
|
||||||
<fieldset><legend><%=l(:label_tracker_plural)%></legend>
|
|
||||||
<% Tracker.sorted.all.each do |tracker| %>
|
|
||||||
<%= check_box_tag "custom_field[tracker_ids][]",
|
|
||||||
tracker.id,
|
|
||||||
(@custom_field.trackers.include? tracker),
|
|
||||||
:id => "custom_field_tracker_ids_#{tracker.id}" %>
|
|
||||||
<label class="no-css" for="custom_field_tracker_ids_<%=tracker.id%>">
|
|
||||||
<%= h(tracker.name) %>
|
|
||||||
</label>
|
|
||||||
<% end %>
|
|
||||||
<%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<p><%= f.check_box :is_required %></p>
|
<p><%= f.check_box :is_required %></p>
|
||||||
<p><%= f.check_box :is_for_all %></p>
|
<p><%= f.check_box :is_for_all %></p>
|
||||||
<p><%= f.check_box :is_filter %></p>
|
<p><%= f.check_box :is_filter %></p>
|
||||||
<p><%= f.check_box :searchable %></p>
|
<p><%= f.check_box :searchable %></p>
|
||||||
|
<p>
|
||||||
|
<label><%= l(:field_visible) %></label>
|
||||||
|
<label class="block">
|
||||||
|
<%= radio_button_tag 'custom_field[visible]', 1, @custom_field.visible?, :id => 'custom_field_visible_on' %>
|
||||||
|
<%= l(:label_visibility_public) %>
|
||||||
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<%= radio_button_tag 'custom_field[visible]', 0, !@custom_field.visible?, :id => 'custom_field_visible_off' %>
|
||||||
|
<%= l(:label_visibility_roles) %>:
|
||||||
|
</label>
|
||||||
|
<% Role.givable.sorted.each do |role| %>
|
||||||
|
<label class="block custom_field_role" style="padding-left:2em;">
|
||||||
|
<%= check_box_tag 'custom_field[role_ids][]', role.id, @custom_field.roles.include?(role) %>
|
||||||
|
<%= role.name %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<%= hidden_field_tag 'custom_field[role_ids][]', '' %>
|
||||||
|
</p>
|
||||||
|
|
||||||
<% when "UserCustomField" %>
|
<% when "UserCustomField" %>
|
||||||
<p><%= f.check_box :is_required %></p>
|
<p><%= f.check_box :is_required %></p>
|
||||||
@@ -95,5 +103,45 @@ when "IssueCustomField" %>
|
|||||||
<% end %>
|
<% end %>
|
||||||
<%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
|
<%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
|
||||||
</div>
|
</div>
|
||||||
|
<%= submit_tag l(:button_save) %>
|
||||||
|
|
||||||
|
<% if @custom_field.is_a?(IssueCustomField) %>
|
||||||
|
</div>
|
||||||
|
<div class="splitcontentright">
|
||||||
|
<fieldset class="box"><legend><%=l(:label_tracker_plural)%></legend>
|
||||||
|
<% Tracker.sorted.all.each do |tracker| %>
|
||||||
|
<%= check_box_tag "custom_field[tracker_ids][]",
|
||||||
|
tracker.id,
|
||||||
|
(@custom_field.trackers.include? tracker),
|
||||||
|
:id => "custom_field_tracker_ids_#{tracker.id}" %>
|
||||||
|
<label class="no-css" for="custom_field_tracker_ids_<%=tracker.id%>">
|
||||||
|
<%= h(tracker.name) %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="box" id="custom_field_project_ids"><legend><%= l(:label_project_plural) %></legend>
|
||||||
|
<%= render_project_nested_lists(Project.all) do |p|
|
||||||
|
content_tag('label', check_box_tag('custom_field[project_ids][]', p.id, @custom_field.projects.to_a.include?(p), :id => nil) + ' ' + h(p))
|
||||||
|
end %>
|
||||||
|
<%= hidden_field_tag('custom_field[project_ids][]', '', :id => nil) %>
|
||||||
|
<p><%= check_all_links 'custom_field_project_ids' %></p>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% include_calendar_headers_tags %>
|
<% include_calendar_headers_tags %>
|
||||||
|
|
||||||
|
<%= javascript_tag do %>
|
||||||
|
function toggleCustomFieldRoles(){
|
||||||
|
var checked = $("#custom_field_visible_on").is(':checked');
|
||||||
|
$('.custom_field_role input').attr('disabled', checked);
|
||||||
|
}
|
||||||
|
$("#custom_field_visible_on, #custom_field_visible_off").change(toggleCustomFieldRoles);
|
||||||
|
$(document).ready(toggleCustomFieldRoles);
|
||||||
|
|
||||||
|
$("#custom_field_is_for_all").change(function(){
|
||||||
|
$("#custom_field_project_ids input").attr("disabled", $(this).is(":checked"));
|
||||||
|
}).trigger('change');
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
|
<%= title [l(:label_custom_field_plural), custom_fields_path],
|
||||||
» <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
|
[l(@custom_field.type_name), custom_fields_path(:tab => @custom_field.class.name)],
|
||||||
» <%=h @custom_field.name %></h2>
|
@custom_field.name %>
|
||||||
|
|
||||||
<%= labelled_form_for :custom_field, @custom_field, :url => custom_field_path(@custom_field), :html => {:method => :put, :id => 'custom_field_form'} do |f| %>
|
<%= labelled_form_for :custom_field, @custom_field, :url => custom_field_path(@custom_field), :html => {:method => :put, :id => 'custom_field_form'} do |f| %>
|
||||||
<%= render :partial => 'form', :locals => { :f => f } %>
|
<%= render :partial => 'form', :locals => { :f => f } %>
|
||||||
<%= submit_tag l(:button_save) %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
api.array :custom_fields do
|
||||||
|
@custom_fields.each do |field|
|
||||||
|
api.custom_field do
|
||||||
|
api.id field.id
|
||||||
|
api.name field.name
|
||||||
|
api.customized_type field.class.customized_class.name.underscore if field.class.customized_class
|
||||||
|
api.field_format field.field_format
|
||||||
|
api.regexp field.regexp
|
||||||
|
api.min_length (field.min_length == 0 ? nil : field.min_length)
|
||||||
|
api.max_length (field.max_length == 0 ? nil : field.max_length)
|
||||||
|
api.is_required field.is_required?
|
||||||
|
api.is_filter field.is_filter?
|
||||||
|
api.searchable field.searchable
|
||||||
|
api.multiple field.multiple?
|
||||||
|
api.default_value field.default_value
|
||||||
|
api.visible field.visible?
|
||||||
|
|
||||||
|
if field.field_format == 'list'
|
||||||
|
api.array :possible_values do
|
||||||
|
field.possible_values.each do |v|
|
||||||
|
api.possible_value do
|
||||||
|
api.value v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if field.is_a?(IssueCustomField)
|
||||||
|
api.trackers do
|
||||||
|
field.trackers.each do |tracker|
|
||||||
|
api.tracker :id => tracker.id, :name => tracker.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
api.roles do
|
||||||
|
field.roles.each do |role|
|
||||||
|
api.role :id => role.id, :name => role.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
<h2><%=l(:label_custom_field_plural)%></h2>
|
<%= title l(:label_custom_field_plural) %>
|
||||||
|
|
||||||
<%= render_tabs custom_fields_tabs %>
|
<%= render_tabs custom_fields_tabs %>
|
||||||
|
|
||||||
<% html_title(l(:label_custom_field_plural)) -%>
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<h2><%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %>
|
<%= title [l(:label_custom_field_plural), custom_fields_path],
|
||||||
» <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
|
[l(@custom_field.type_name), custom_fields_path(:tab => @custom_field.class.name)],
|
||||||
» <%= l(:label_custom_field_new) %></h2>
|
l(:label_custom_field_new) %>
|
||||||
|
|
||||||
<%= labelled_form_for :custom_field, @custom_field, :url => custom_fields_path, :html => {:id => 'custom_field_form'} do |f| %>
|
<%= labelled_form_for :custom_field, @custom_field, :url => custom_fields_path, :html => {:id => 'custom_field_form'} do |f| %>
|
||||||
<%= render :partial => 'form', :locals => { :f => f } %>
|
<%= render :partial => 'form', :locals => { :f => f } %>
|
||||||
<%= hidden_field_tag 'type', @custom_field.type %>
|
<%= hidden_field_tag 'type', @custom_field.type %>
|
||||||
<%= submit_tag l(:button_save) %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= javascript_tag do %>
|
<%= javascript_tag do %>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<%= labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
|
<%= labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
|
||||||
<%= render :partial => 'form', :locals => {:f => f} %>
|
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||||
<p>
|
<p>
|
||||||
<%= submit_tag l(:button_create) %>
|
<%= submit_tag l(:button_create) %>
|
||||||
<%= link_to l(:button_cancel), "#", :onclick => '$("#add-document").hide(); return false;' %>
|
<%= link_to l(:button_cancel), "#", :onclick => '$("#add-document").hide(); return false;' %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -25,10 +25,16 @@
|
|||||||
|
|
||||||
<% content_for :sidebar do %>
|
<% content_for :sidebar do %>
|
||||||
<h3><%= l(:label_sort_by, '') %></h3>
|
<h3><%= l(:label_sort_by, '') %></h3>
|
||||||
<%= link_to l(:field_category), {:sort_by => 'category'}, :class => (@sort_by == 'category' ? 'selected' :nil) %><br />
|
<ul>
|
||||||
<%= link_to l(:label_date), {:sort_by => 'date'}, :class => (@sort_by == 'date' ? 'selected' :nil) %><br />
|
<li><%= link_to(l(:field_category), {:sort_by => 'category'},
|
||||||
<%= link_to l(:field_title), {:sort_by => 'title'}, :class => (@sort_by == 'title' ? 'selected' :nil) %><br />
|
:class => (@sort_by == 'category' ? 'selected' :nil)) %></li>
|
||||||
<%= link_to l(:field_author), {:sort_by => 'author'}, :class => (@sort_by == 'author' ? 'selected' :nil) %>
|
<li><%= link_to(l(:label_date), {:sort_by => 'date'},
|
||||||
|
:class => (@sort_by == 'date' ? 'selected' :nil)) %></li>
|
||||||
|
<li><%= link_to(l(:field_title), {:sort_by => 'title'},
|
||||||
|
:class => (@sort_by == 'title' ? 'selected' :nil)) %></li>
|
||||||
|
<li><%= link_to(l(:field_author), {:sort_by => 'author'},
|
||||||
|
:class => (@sort_by == 'author' ? 'selected' :nil)) %></li>
|
||||||
|
</ul>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% html_title(l(:label_document_plural)) -%>
|
<% html_title(l(:label_document_plural)) -%>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<p><em><%=h @document.category.name %><br />
|
<p><em><%=h @document.category.name %><br />
|
||||||
<%= format_date @document.created_on %></em></p>
|
<%= format_date @document.created_on %></em></p>
|
||||||
<div class="wiki">
|
<div class="wiki">
|
||||||
<%= textilizable @document.description, :attachments => @document.attachments %>
|
<%= textilizable @document, :description, :attachments => @document.attachments %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3><%= l(:label_attachment_plural) %></h3>
|
<h3><%= l(:label_attachment_plural) %></h3>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
|
<%= title [l(@enumeration.option_name), enumerations_path], @enumeration.name %>
|
||||||
|
|
||||||
<%= form_tag({}, :method => :delete) do %>
|
<%= form_tag({}, :method => :delete) do %>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
|
<p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
|
||||||
<p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
|
<p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
|
||||||
<%= select_tag 'reassign_to_id', (content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
|
<%= select_tag 'reassign_to_id', (content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= submit_tag l(:button_apply) %>
|
<%= submit_tag l(:button_apply) %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h2><%= link_to l(@enumeration.option_name), enumerations_path %> » <%=h @enumeration %></h2>
|
<%= title [l(@enumeration.option_name), enumerations_path], @enumeration.name %>
|
||||||
|
|
||||||
<%= labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %>
|
<%= labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %>
|
||||||
<%= render :partial => 'form', :locals => {:f => f} %>
|
<%= render :partial => 'form', :locals => {:f => f} %>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<th><%= l(:field_name) %></th>
|
<th><%= l(:field_name) %></th>
|
||||||
<th style="width:15%;"><%= l(:field_is_default) %></th>
|
<th style="width:15%;"><%= l(:field_is_default) %></th>
|
||||||
<th style="width:15%;"><%= l(:field_active) %></th>
|
<th style="width:15%;"><%= l(:field_active) %></th>
|
||||||
<th style="width:15%;"></th>
|
<th style="width:15%;"><%=l(:button_sort)%></th>
|
||||||
<th align="center" style="width:10%;"> </th>
|
<th align="center" style="width:10%;"> </th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<% enumerations.each do |enumeration| %>
|
<% enumerations.each do |enumeration| %>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<td><%= link_to h(enumeration), edit_enumeration_path(enumeration) %></td>
|
<td><%= link_to h(enumeration), edit_enumeration_path(enumeration) %></td>
|
||||||
<td class="center" style="width:15%;"><%= checked_image enumeration.is_default? %></td>
|
<td class="center" style="width:15%;"><%= checked_image enumeration.is_default? %></td>
|
||||||
<td class="center" style="width:15%;"><%= checked_image enumeration.active? %></td>
|
<td class="center" style="width:15%;"><%= checked_image enumeration.active? %></td>
|
||||||
<td style="width:15%;"><%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :put) %></td>
|
<td align="center" style="width:15%;"><%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :put) %></td>
|
||||||
<td class="buttons">
|
<td class="buttons">
|
||||||
<%= delete_link enumeration_path(enumeration) %>
|
<%= delete_link enumeration_path(enumeration) %>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h2><%= link_to l(@enumeration.option_name), enumerations_path %> » <%=l(:label_enumeration_new)%></h2>
|
<%= title [l(@enumeration.option_name), enumerations_path], l(:label_enumeration_new) %>
|
||||||
|
|
||||||
<%= labelled_form_for :enumeration, @enumeration, :url => enumerations_path do |f| %>
|
<%= labelled_form_for :enumeration, @enumeration, :url => enumerations_path do |f| %>
|
||||||
<%= f.hidden_field :type %>
|
<%= f.hidden_field :type %>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
<% @gantt.view = self %>
|
<% @gantt.view = self %>
|
||||||
|
<div class="contextual">
|
||||||
|
<% if !@query.new_record? && @query.editable_by?(User.current) %>
|
||||||
|
<%= link_to l(:button_edit), edit_query_path(@query, :gantt => 1), :class => 'icon icon-edit' %>
|
||||||
|
<%= delete_link query_path(@query, :gantt => 1) %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
|
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
|
||||||
|
|
||||||
<%= form_tag({:controller => 'gantts', :action => 'show',
|
<%= form_tag({:controller => 'gantts', :action => 'show',
|
||||||
@@ -6,6 +13,7 @@
|
|||||||
:year => params[:year], :months => params[:months]},
|
:year => params[:year], :months => params[:months]},
|
||||||
:method => :get, :id => 'query_form') do %>
|
:method => :get, :id => 'query_form') do %>
|
||||||
<%= hidden_field_tag 'set_filter', '1' %>
|
<%= hidden_field_tag 'set_filter', '1' %>
|
||||||
|
<%= hidden_field_tag 'gantt', '1' %>
|
||||||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
|
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
|
||||||
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
|
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
|
||||||
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
|
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
|
||||||
@@ -20,8 +28,8 @@
|
|||||||
<td>
|
<td>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><%= l(:label_related_issues) %></legend>
|
<legend><%= l(:label_related_issues) %></legend>
|
||||||
<label>
|
<label for="draw_relations">
|
||||||
<%= check_box_tag "draw_rels", params["draw_rels"], true %>
|
<%= check_box 'query', 'draw_relations', :id => 'draw_relations' %>
|
||||||
<% rels = [IssueRelation::TYPE_BLOCKS, IssueRelation::TYPE_PRECEDES] %>
|
<% rels = [IssueRelation::TYPE_BLOCKS, IssueRelation::TYPE_PRECEDES] %>
|
||||||
<% rels.each do |rel| %>
|
<% rels.each do |rel| %>
|
||||||
<% color = Redmine::Helpers::Gantt::DRAW_TYPES[rel][:color] %>
|
<% color = Redmine::Helpers::Gantt::DRAW_TYPES[rel][:color] %>
|
||||||
@@ -35,8 +43,8 @@
|
|||||||
<td>
|
<td>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><%= l(:label_gantt_progress_line) %></legend>
|
<legend><%= l(:label_gantt_progress_line) %></legend>
|
||||||
<label>
|
<label for="draw_progress_line">
|
||||||
<%= check_box_tag "draw_progress_line", params[:draw_progress_line], false %>
|
<%= check_box 'query', 'draw_progress_line', :id => 'draw_progress_line' %>
|
||||||
<%= l(:label_display) %>
|
<%= l(:label_display) %>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -62,6 +70,11 @@
|
|||||||
:class => 'icon icon-checked' %>
|
:class => 'icon icon-checked' %>
|
||||||
<%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 },
|
<%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 },
|
||||||
:class => 'icon icon-reload' %>
|
:class => 'icon icon-reload' %>
|
||||||
|
<% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
|
||||||
|
<%= link_to_function l(:button_save),
|
||||||
|
"$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }').submit();",
|
||||||
|
:class => 'icon icon-save' %>
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
@@ -313,7 +326,7 @@
|
|||||||
$(document).ready(drawGanttHandler);
|
$(document).ready(drawGanttHandler);
|
||||||
$(window).resize(drawGanttHandler);
|
$(window).resize(drawGanttHandler);
|
||||||
$(function() {
|
$(function() {
|
||||||
$("#draw_rels").change(drawGanttHandler);
|
$("#draw_relations").change(drawGanttHandler);
|
||||||
$("#draw_progress_line").change(drawGanttHandler);
|
$("#draw_progress_line").change(drawGanttHandler);
|
||||||
});
|
});
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
<h2><%= link_to l(:label_group_plural), groups_path %> » <%= h(@group) %></h2>
|
<%= title [l(:label_group_plural), groups_path], @group.name %>
|
||||||
|
|
||||||
<%= render_tabs group_settings_tabs %>
|
<%= render_tabs group_settings_tabs %>
|
||||||
|
|
||||||
<% html_title(l(:label_group), @group, l(:label_administration)) -%>
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
|
<%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2><%= l(:label_group_plural) %></h2>
|
<%= title l(:label_group_plural) %>
|
||||||
|
|
||||||
<% if @groups.any? %>
|
<% if @groups.any? %>
|
||||||
<table class="list groups">
|
<table class="list groups">
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<td class="buttons"><%= delete_link group %></td>
|
<td class="buttons"><%= delete_link group %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user