Compare commits

...

404 Commits
0.6.0 ... 0.7.3

Author SHA1 Message Date
Jean-Philippe Lang
cba0faadd6 tagged version 0.7.3
git-svn-id: http://redmine.rubyforge.org/svn/tags/0.7.3@1638 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 12:19:00 +00:00
Jean-Philippe Lang
bf45264131 Updated CHANGELOG.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1637 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:13:28 +00:00
Jean-Philippe Lang
1602fd0dfd Updated CHANGELOG and version number.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1636 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:12:21 +00:00
Jean-Philippe Lang
12c4119abd Merged r1609 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1635 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:10:47 +00:00
Jean-Philippe Lang
f3a52d2e98 Merged r1600 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1634 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:09:00 +00:00
Jean-Philippe Lang
ba1af78dc8 Merged r1598 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1633 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:08:02 +00:00
Jean-Philippe Lang
59427dcfbf Merged r1589 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1632 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:05:31 +00:00
Jean-Philippe Lang
50755b5913 Merged r1585 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1631 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:02:37 +00:00
Jean-Philippe Lang
5b0051e710 Merged r1583 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1630 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 11:01:06 +00:00
Jean-Philippe Lang
8af523c694 Merged r1561 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1629 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-06 10:58:31 +00:00
Jean-Philippe Lang
2f84506227 Merged r1612 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1613 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-07-02 17:28:48 +00:00
Jean-Philippe Lang
6e7012c40f Version number updated.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1548 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-15 12:27:25 +00:00
Jean-Philippe Lang
dfcd0f15ae Merged r1546 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1547 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-15 12:26:24 +00:00
Jean-Philippe Lang
8bfe71e803 Merged r1544 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1545 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-15 11:41:55 +00:00
Jean-Philippe Lang
1c1c9fa517 Merged r1542 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1543 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-15 11:04:03 +00:00
Jean-Philippe Lang
a55317f09c Merged r1376 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1541 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-15 10:22:00 +00:00
Jean-Philippe Lang
fcc5d2847c Merged r1535 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1536 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-14 14:11:45 +00:00
Jean-Philippe Lang
e5b3c31e70 Merged r1533 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1534 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-14 12:32:26 +00:00
Jean-Philippe Lang
2af910e8e6 Merged r1505, r1506, r1511, r1512, r1517, r1518, r1524 to 1526 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1532 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-14 12:21:52 +00:00
Jean-Philippe Lang
9c5ec2974d Merged r1503 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1531 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-14 12:12:17 +00:00
Jean-Philippe Lang
6f6f1f1ba3 Merged r1500 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1530 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-14 12:11:13 +00:00
Jean-Philippe Lang
3f62305e91 Merged r1492 to r1498 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1529 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-14 12:09:42 +00:00
Jean-Philippe Lang
15ecbbf928 Merged r1473, r1476, r1477, r1478, r1481, r1482, r1484 to r1487, r1491 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1528 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-06-14 11:59:59 +00:00
Jean-Philippe Lang
2074f0ab41 Merged r1451 to r1454, r1458, r1460 to r1463 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1474 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-05-29 18:07:00 +00:00
Jean-Philippe Lang
290bd1756d Merged r1442, r1443 and r1446 to r1449 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1450 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-05-25 11:33:04 +00:00
Jean-Philippe Lang
729d1176ea Merged r1385 and r1426 to r1434 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1437 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-05-18 16:53:23 +00:00
Jean-Philippe Lang
c0db7007fa Doc and version update for 0.7.1 release.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1413 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-05-04 10:26:25 +00:00
Jean-Philippe Lang
d7e3c4df26 Merged r1409 and r1410 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1411 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-05-04 10:04:19 +00:00
Jean-Philippe Lang
76ec11422d Merged r1400 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1408 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-05-03 10:34:41 +00:00
Jean-Philippe Lang
8bc721c264 Merged r1399, r1403, r1405 and r1406 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1407 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-05-02 18:39:32 +00:00
Jean-Philippe Lang
914d1e6645 Merged r1379 to r1385 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1387 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-04-30 11:41:16 +00:00
Jean-Philippe Lang
5f346bbf2b Version set to 0.7.0.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1371 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-04-28 10:37:47 +00:00
Jean-Philippe Lang
10191813ec Merged r1307 to r1369 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1370 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-04-28 10:36:12 +00:00
Jean-Philippe Lang
be071deae2 Added 0.7 stable branch.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1305 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-29 11:45:53 +00:00
Jean-Philippe Lang
a381b0978f Changelog updated.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1304 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-29 11:43:07 +00:00
Jean-Philippe Lang
0dfa2ad3cf Show date range on the activity (closes #837).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1303 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-29 11:12:57 +00:00
Jean-Philippe Lang
1bb51f743a Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets (patch #882 by Andreas Neuhaus slightly edited).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1302 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-29 10:54:24 +00:00
Jean-Philippe Lang
85858cebe6 Fixed: https urls in the wiki are not displayed as external (closes #943).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1301 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-29 09:24:09 +00:00
Jean-Philippe Lang
6331809bd9 Translation updates: (closes #915, #927, #929, #923, #914, #919)
* Japanese (Satoru Kurashiki)
* Simplified Chinese (chaoqun zou)
* Danish (kim madsen)
* Norwegian (Kai Olav Fredriksen)
* Traditional Chinese (shortie lo)
* German (Sven Schuchmann)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1300 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-27 19:27:40 +00:00
Jean-Philippe Lang
b9e380c9fe Add breadcrumb nav for the forums (#892).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1299 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-27 18:41:42 +00:00
Jean-Philippe Lang
8960200409 If 'Display subprojects issues on main projects' is set to false:
* do not include subprojects in the issue count of the parent project overview (#939)
* do not include subproject timelogs on the parent project (#848)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1298 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-27 18:22:12 +00:00
Jean-Philippe Lang
ef80597c39 Trac importer: exclude more Trac wiki pages (#933).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1297 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-26 23:22:33 +00:00
Jean-Philippe Lang
ecc742f3ce Make the versions with the same date sorted by name (#864).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1296 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-26 22:56:30 +00:00
Jean-Philippe Lang
7a434c902a Bigger wiki h3 headings.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1295 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-26 22:46:18 +00:00
Jean-Philippe Lang
d37a30846f Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql (#935).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1294 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-26 22:22:45 +00:00
Jean-Philippe Lang
805864590a Do not use javascript to hide tabs content on page loading and make tabs work with javascript disabled.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1293 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-26 18:49:12 +00:00
Jean-Philippe Lang
1c49bd4258 Add the description field to the issue csv export (#753).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1292 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-26 18:08:24 +00:00
Jean-Philippe Lang
051875429e Fixed: 'This week' condition in filter consider monday as the first day of the week even if language sets otherwise (closes #913).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1291 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-23 16:16:55 +00:00
Jean-Philippe Lang
6936eb1022 Make 'Planning' string translatable (closes #890).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1290 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-23 09:04:21 +00:00
Jean-Philippe Lang
de5b0ad0a8 Translations (closes #875, #876, #889, #891, #909):
* Norwegian added (Kai Olav Fredriksen)
* Finnish updated (Antti Perkiömäki)
* Czech updated (Maxim Krušina)
* Russian updated (Michael Pirogov)
* Polish updated (Mariusz Olejnik)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1289 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-23 08:34:13 +00:00
Jean-Philippe Lang
472654aebe Fixed: revision is ignored in MercurialAdapter#cat.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1288 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 22:19:37 +00:00
Jean-Philippe Lang
5f78b8a9a0 Added links to repository files in wiki syntax help.
Styles added and small fixes to the detailed wiki syntax help.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1287 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 22:05:23 +00:00
Jean-Philippe Lang
7fb72e671b Trac importer: prevent validation failure due to the default value when saving the Resolution custom field if it already exists (#869).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1286 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 21:37:40 +00:00
Jean-Philippe Lang
180fe8f6a4 Fix IE6 display bug on the issue list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1285 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 21:08:00 +00:00
Jean-Philippe Lang
60c12ca3ab Doc update before 0.7-rc1 release.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1284 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 20:24:10 +00:00
Jean-Philippe Lang
a8e6f68aaf Add unloadable to the sample plugin controller (http://dev.rubyonrails.org/ticket/6001).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1283 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 20:17:09 +00:00
Jean-Philippe Lang
27cf6d8d64 Add unloadable to the sample plugin controller (http://dev.rubyonrails.org/ticket/6001).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1282 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 20:02:41 +00:00
Jean-Philippe Lang
09bf503fd2 Load Engines plugin if available (closes #180).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1281 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 19:59:43 +00:00
Jean-Philippe Lang
35a14cbfdc Wiki links:
* fixes wiki links with pipe in table (closes #893, #870, #894)
* prevent wiki link matching on multiple lines

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1280 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 17:39:02 +00:00
Jean-Philippe Lang
bbf422e229 Add done_ratio to the right-click context menu.
Closes #904 (back_to variable used in patch #641 and in r1277 is no longer defined).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1279 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-21 17:32:15 +00:00
John Goerzen
8ed97f43d2 Revert "Make an issue's done_ratio field adjustable from the right-click"
Reverts commit r1277
fixes #904
refs #641

This had been working for me in testing for some time, but received
issue #904 saying it broke the context menu.  I was able to make it do
so on a new project as well.  Will revert and comment in #641 about this.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1278 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-20 19:25:18 +00:00
John Goerzen
4ff77cc624 Make an issue's done_ratio field adjustable from the right-click
context menu.

Uses patch from, and fixes #641.  Patch by Dov Murik.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1277 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-20 17:41:03 +00:00
Jean-Philippe Lang
0cccce0c43 Change the tick image and replace the issue selection pencil with a small tick.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1276 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-19 21:39:21 +00:00
Jean-Philippe Lang
43fc812e0e Default theme: smaller font in top menu and new style for main menu.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1275 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-19 18:22:16 +00:00
Jean-Philippe Lang
f162337e1b Always show 'View' and 'Annotate' links on repository files (download will be forced when clicking 'View' if the file is binary).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1274 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-19 17:51:13 +00:00
John Goerzen
93a33c6286 Added line under tracker name to make clear what is going on here
Partially implements the patch given in r730


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1273 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-18 18:50:52 +00:00
Jean-Philippe Lang
3e102f281d Add a simple java scanner.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1272 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-18 17:39:12 +00:00
Jean-Philippe Lang
7673d69b96 Fixes #880: code tags not formatted correctly in the wiki (broken by r1216).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1271 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-17 23:01:35 +00:00
Jean-Philippe Lang
1c4bcc5b55 Add NewsController and TimelogController tests.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1270 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-17 20:48:22 +00:00
Jean-Philippe Lang
8cb5acf453 Add some tests on RolesController.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1269 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-17 20:02:51 +00:00
Jean-Philippe Lang
030afe7428 Small fix to the Redmine links regexp.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1268 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-17 18:01:26 +00:00
Jean-Philippe Lang
93d1b2e0a4 Add Redmine links for repository files using source: and export: keyworkds (#867).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1267 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-17 17:45:01 +00:00
Jean-Philippe Lang
cab300e650 Prevent page reloading if something goes wrong in the 'add more file' javascript.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1266 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 21:41:36 +00:00
Jean-Philippe Lang
f271e772af Add the following headers to email notifications (#830, #247):
* X-Redmine-Host: host name defined in application settings
* X-Redmine-Site: application title defined in settings
* X-Redmine-Project: identifier of the project that the notification is related to, if any
* X-Redmine-Issue-Id, -Author, -Assignee: ticket related info
* X-Redmine-Topic-Id: identifies the thread a message belongs to

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1265 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 16:52:49 +00:00
Jean-Philippe Lang
4bd35dae8d Allow issue list to be sorted by target version (#832).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1264 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 15:04:33 +00:00
Jean-Philippe Lang
2d01398d67 Trac importer:
* prevent duplication of associated trackers when the target project already exists (closes #829)
* warn user if the target project already exists

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1263 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 14:54:36 +00:00
Jean-Philippe Lang
f5b5688e8a Move the filters buttons inside the filters fieldset on the issue list (closes #614).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1262 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 14:31:17 +00:00
Jean-Philippe Lang
2eb085506b Adds a title attribute to the next/previous links on the activity view, containing the corresponding date range (#837).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1261 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 14:18:36 +00:00
Jean-Philippe Lang
a8fcf8487d Add a time tracking block for 'My page' (#615).
It lists current user's time entries for the last 7 days across all projects, grouped by day with subtotals for each day, and a grand total.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1260 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 12:21:54 +00:00
Jean-Philippe Lang
5b6978a1f3 Add a margin to the bottom of wiki tables (#843).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1259 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-16 10:09:35 +00:00
John Goerzen
888b254844 Further refine WikiCaps processing for Trac to eliminate problems in
some corner cases


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1258 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-15 13:24:08 +00:00
Jean-Philippe Lang
83061e9ca2 Fix tests broken by r1243 (Redirect to issue page after creating a new issue).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1257 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-15 11:27:46 +00:00
Jean-Philippe Lang
451aa6ba4e Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs (#834).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1256 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-15 11:05:22 +00:00
Jean-Philippe Lang
01fdaf5977 Mercurial adapter:
* fetch changesets by batches of 100 (rather than in a single transaction)
* fix: fetch_changesets tries to re-insert the last revision that exists in the db (#860)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1255 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-15 10:30:56 +00:00
Jean-Philippe Lang
a59e6bfb02 Remove hardcoded "Redmine" strings in account related emails. And use application title instead.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1254 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-15 09:40:16 +00:00
Jean-Philippe Lang
185ecd5854 Translations update (close #845, #822):
* Finnish (Antti Perkiömäki)
* Czech (Maxim Krušina)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1253 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-15 09:25:35 +00:00
Jean-Philippe Lang
aec989707e Workflow copy:
* added the ability the copy an existing workflow when creating a new role (closes #841)
* use a single raw insert statement to copy tracker/role workflow rather than instantiating hundreds/thousands of objects

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1252 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-15 08:27:38 +00:00
Jean-Philippe Lang
993b60d61e Adds 2 permissions (closes #859):
* edit_time_entries: lets a user edit/delete any time entry
* edit_own_time_entries: lets a user edit/delete its own time entries only

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1249 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-14 21:17:09 +00:00
Jean-Philippe Lang
4957752d12 Strip repository urls (closes #852).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1248 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-14 18:59:10 +00:00
John Goerzen
b8b991a7dc Added documentation of commit: syntax to detailed wiki syntax screen
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1247 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-14 13:48:32 +00:00
John Goerzen
cdf3e2b6ef Fix trac converter WikiCaps to put a space before converted WikiCaps words
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1246 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-14 00:52:26 +00:00
John Goerzen
84d2ea054d Update Simplified Chinese lang file
Closes #840 and uses patch from chaoqun zou contained therein, updated
to trunk


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1245 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-13 21:46:31 +00:00
John Goerzen
076d9e7ade This patch takes the RedmineWikiFormatting page and makes it available
as a static HTML file locally. This way, intranet deployments, or
other deployments do not need to be dependent upon redmine.org in
order to display this help.

Closes #815.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1244 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-13 14:14:32 +00:00
John Goerzen
c67c375357 Redirect to issue page after creating a new issue
Previous behavior was to redirect to the issue list with a "successful
creation" message.  This patch will redirect to the page for the
newly-created issue, still with the "successful creation" message.

This matches the behavior after editing an issue and also provides
instant feedback for the user to see if anything went wrong.

Closes #261 and uses the patch contained therein


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1243 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-13 13:59:21 +00:00
John Goerzen
87fb78be0b Support WikiCaps for Trac migrations
Trac wikis support WikiCaps for links to pages.  They also use
!WikiCaps syntax to prevent links.  Support both.

Uses patch from and closes #758.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1242 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-13 05:48:37 +00:00
John Goerzen
eafaae73b7 Updated zh-tw lang file from shortie lo.
Closes #847.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1241 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-13 05:41:31 +00:00
John Goerzen
4f16dc53a4 Add commit: link type feature to wiki_syntax.html
The commit: feature merged to trunk in r1236


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1240 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-13 05:41:28 +00:00
Jean-Philippe Lang
ea161c9ea4 Prevent unexpected nil error in GitAdapter#info if the repository path is invalid.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1239 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-12 23:09:11 +00:00
Jean-Philippe Lang
2c1e63720e SCM AbstractAdapter use shell_quote to more properly escape path (closes #838 by John Goerzen).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1238 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-12 23:00:11 +00:00
Jean-Philippe Lang
d4429a544c Fixes #820: invalid project id causes a NoMethodError in SearchController (Angel Dobbs-Sciortino).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1237 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-12 20:50:48 +00:00
Jean-Philippe Lang
3a9b0988c7 Merged Git support branch (r1200 to r1226).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1236 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-12 20:28:49 +00:00
Jean-Philippe Lang
6fcc512cb7 Adds a setting for whether new projects should be public by default (closes #842, #839).
Patch by Rocco Stanzione, slightly edited.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1235 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-12 19:58:19 +00:00
Jean-Philippe Lang
3a75b6771f Prevent LDAP authentication with empty password related problems.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1231 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-12 17:56:19 +00:00
Jean-Philippe Lang
a9c972fbb3 Quotes Mercurial entries command depending on the OS.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1229 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-11 20:15:05 +00:00
Jean-Philippe Lang
71d3bb2f7b Fixes Mercurial browsing under unix-like os and for directory depth > 2 and (patch #826 by Frederic Moulins, slightly edited to preserve win32 compatibility).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1228 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-11 20:06:25 +00:00
Jean-Philippe Lang
624723d0ce Activity enhancements:
* overall activity view and feed added, link is available on the project list (#423, #494)
* switch added on the project activity view to include subprojects (closes #530)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1227 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-11 19:33:38 +00:00
Jean-Philippe Lang
64fb0a561a Removes the Redmine version from the footer (closes #794). It can still be viewed on admin -> info.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1225 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 22:24:38 +00:00
Jean-Philippe Lang
76a29ae337 Adds atom feed link on boards index.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1224 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 20:16:48 +00:00
Jean-Philippe Lang
4f35ec97e8 Replaces del tags by a css style (a.issue.closed) for closed issue links.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1223 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 18:27:55 +00:00
Jean-Philippe Lang
8195f95464 Fixes migrations that change string limits for sqlite.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1222 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 18:25:37 +00:00
Jean-Philippe Lang
494a6ecfb0 Trac and Mantis importers: check that default configuration is loaded before processing.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1221 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 17:23:22 +00:00
Jean-Philippe Lang
66594d46b2 Allow longer version names (#711, closes #712).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1220 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 15:36:47 +00:00
Jean-Philippe Lang
0ec13848b0 Show replies as well when choosing to display messages in the activity.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1217 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 12:15:27 +00:00
Jean-Philippe Lang
a92cce3851 Textile formatting:
* escape html tags, except pre tags (#807, #795)
* try to avoid unwanted quick phrase modifiers

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1216 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 11:47:36 +00:00
Jean-Philippe Lang
522b9e6b5b Display date/time instead of date on files list (#817).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1215 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-09 11:39:00 +00:00
Jean-Philippe Lang
07882590bb Strip out email address from authors in repository screens (#814).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1208 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-08 13:46:07 +00:00
Jean-Philippe Lang
9b59ea7256 Version details view changes:
* display related issues on the version detail view
* display total estimated and spent hours on the version detail view
* fixed wiki headings size (same as r1168)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1207 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-08 11:17:03 +00:00
Jean-Philippe Lang
1830064918 Changes 'Fixed version' label to 'Target version' (closes #723, #808).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1206 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-08 09:06:16 +00:00
Jean-Philippe Lang
a4c6c62c8b Added preview for forum messages.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1205 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-07 17:49:44 +00:00
Jean-Philippe Lang
4458a7282b Translations (closes #789, #790, #798, #803, #809):
* Danish added (Mads Vestergaard)
* Polish updated (Mariusz Olejnik)
* Finnish updated (Antti Perkiömäki)
* Simplified Chinese updated (chaoqun zou)
* Traditional Chinese updated (shortie lo)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1204 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-07 17:15:50 +00:00
Jean-Philippe Lang
e8035c22b2 Fixes module names localization on projects/add (closes #797).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1203 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-07 17:04:16 +00:00
Jean-Philippe Lang
482d9a4e82 Fix LDAP authentication (#714, broken by r1194).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1199 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-06 17:22:21 +00:00
Jean-Philippe Lang
533994e5ea Adds an application setting to choose whether or not subprojects issues should be displayed by default on the issue list, calendar and gantt (r1178). Default is true.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1198 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-06 08:32:56 +00:00
Jean-Philippe Lang
e951d84584 Add a user preference to choose how comments/replies are displayed: in chronological or reverse chronological order (#589, #776).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1197 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 15:41:54 +00:00
Jean-Philippe Lang
bbe8ea29e8 Display the last 30 days on the activity view rather than the current month.
Number of days can be configured in the application settings.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1196 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 13:44:08 +00:00
Jean-Philippe Lang
fbcdfee622 Trac importer: support database schema for Trac migration (#757 by John Goerzen).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1195 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 12:54:20 +00:00
Jean-Philippe Lang
a70737ad59 Fixed "LdapError: invalid binding information" when no username/password are set on the LDAP account (#764).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1194 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 12:43:14 +00:00
Jean-Philippe Lang
745fec76d0 Fixes #773: Changeset block is smashing the update frame on issues/show.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1193 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 11:59:56 +00:00
Jean-Philippe Lang
1eca3947a1 Reversed versions ordering on files list (closes #763).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1192 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 11:54:54 +00:00
Jean-Philippe Lang
d09f7d73b3 Added jsp mime type.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1191 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 11:42:59 +00:00
Jean-Philippe Lang
c61424e57a Display wiki syntax quick ref link within the jstoolbar (closes #629, #767).
Added named links syntax on quick ref (closes #766, #778).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1190 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 11:14:35 +00:00
Jean-Philippe Lang
b0754ca720 Fixes migration 87 error when running MySQL with STRICT_TRANS_TABLES on (#771).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1189 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 09:16:19 +00:00
Jean-Philippe Lang
4f9f3097d9 Fixes #765: Date pickers don't work on issues' bulk edit view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1188 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 09:06:00 +00:00
Jean-Philippe Lang
d8c97b32dd Fixes #780: Redmine fails to start with a const_missing error on Redmine::MenuManager::MenuItem::GLoc (occurs when a Redmine plugin is loaded before GLoc).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1187 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 09:01:37 +00:00
Jean-Philippe Lang
918937c873 Translations updates:
* Finnish (Antti Perkiömäki)
* Russian (Michael Pirogov)
* Chinese Traditional (shortie lo)
* Japanese (Satoru Kurashiki)
* Korean (Jongyoon Choi)
* Simplified Chinese (chaoqun zou)
* German (Thomas Löber)

Fixed zh and zh-tw encoding in PDF (chaoqun zou).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1186 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 08:54:26 +00:00
Jean-Philippe Lang
942091f9e8 Display links to Atom feeds (closes #496, #750).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1185 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-05 08:25:22 +00:00
Jean-Philippe Lang
8ea2ecb983 Fixed #759: Can not view a project without View time entries permission (broken by r1176).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1184 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-01 20:09:57 +00:00
Jean-Philippe Lang
7ee55562ff Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1183 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-03-01 09:00:02 +00:00
Jean-Philippe Lang
c01c64e978 Let the user choose when deleting issues with reported hours (closes #734, #71):
* to delete the hours
* to assign the hours to the project
* to reassign the hours to another issue


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1182 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-29 22:54:07 +00:00
Jean-Philippe Lang
87742f23ed Login field name changed to username (#755).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1181 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-29 21:18:35 +00:00
Jean-Philippe Lang
9daf39ec52 Adds an optional description to attachments.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1180 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-29 19:46:58 +00:00
Jean-Philippe Lang
4b15dc10c1 Fixed: Multiple "new" statuses in filing new ticket (closes #751).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1179 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-28 22:00:56 +00:00
Jean-Philippe Lang
b6bd5c7f12 Include subprojects on the issue list, calendar and gantt by default.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1178 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-28 21:57:03 +00:00
Jean-Philippe Lang
1ddb59df2a Fixed JournalControllerTest.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1177 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-27 23:01:35 +00:00
Jean-Philippe Lang
200842ba5e Propagates time tracking to the parent project (closes #433). Time report enhancements.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1176 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-27 20:50:19 +00:00
Jean-Philippe Lang
421d420311 Trac importer fix:
* error when trying to convert too long ticket ids (closes #719)

Enhancements (#619):
* Trac guide wiki pages skipped
* wiki attachments migration added

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1175 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-26 19:47:29 +00:00
Jean-Philippe Lang
8531e176aa Do not send an email with no recipient, cc or bcc (closes #743).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1174 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-26 18:30:24 +00:00
Jean-Philippe Lang
328df74dd1 Adds date range filter and pagination on time entries detail view (closes #434).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1173 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-26 18:15:58 +00:00
Jean-Philippe Lang
792b7f30e3 Menus items:
* fixed broken translation when a plugin is installed (closes #649)
* small fix to the plugin API: options parameter added to Redmine::Plugin#menu

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1172 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-22 18:19:00 +00:00
Jean-Philippe Lang
b0bb41ad35 Fixed: Issue custom fields "required" flag not stored (broken by r1090). Closes #715.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1171 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-22 17:48:37 +00:00
Jean-Philippe Lang
b935aebd99 Fixed: LDAP authentication without password may be possible (#714).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1169 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-22 17:26:53 +00:00
Jean-Philippe Lang
ade572048a Roadmap enhancements (closes #697, #698):
* separate related issues from wiki contents
* leading h1 in version wiki pages is hidden (done using css, won't work with IE6)
* smaller wiki headings
* xhtml validation

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1168 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-21 19:49:47 +00:00
Jean-Philippe Lang
7d10755072 Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1167 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-21 19:30:27 +00:00
Jean-Philippe Lang
3f4a8469ac Added 'estimated time' in the csv export of the issue list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1166 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-21 17:57:00 +00:00
Jean-Philippe Lang
d461363d27 Fixed: Adding time when Updating a ticket is gone (closes #701).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1165 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-21 17:50:33 +00:00
Jean-Philippe Lang
948097bc4d Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed (closes #696).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1164 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-21 17:45:49 +00:00
Jean-Philippe Lang
09bd61da74 Order issues by id on the roadmap.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1163 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-18 18:28:37 +00:00
Jean-Philippe Lang
652dc1a73a Fixed: auto closing of duplicates doesn't work.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1162 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-17 15:02:54 +00:00
Jean-Philippe Lang
e5daf25618 Fixes:
* email notifications: host name is missing in generated links (#639, #201)
* email notifications: referenced changesets, wiki pages, attachments... are not turned into links (only ticket ids are)
* attachment links and inline images don't work in issue notes

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1161 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-17 14:17:20 +00:00
Jean-Philippe Lang
5d6d0136c1 Fixes for wiki_syntax.html (#678) by Thomas Löber.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1160 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 21:24:12 +00:00
Jean-Philippe Lang
d69a05a6ee Rescue and display an error message when trying to delete a role that is in use.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1159 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 21:05:10 +00:00
Jean-Philippe Lang
33493f8010 Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1158 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 19:28:24 +00:00
Jean-Philippe Lang
a798a1ac4d Removed fragment caching on gantt and calendar.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1157 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 18:28:46 +00:00
Jean-Philippe Lang
53a04b0a44 Fixed: CVS repository doesn't work if port is used in the url (#653).
Added functional tests for CVS adapter.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1156 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 18:09:26 +00:00
Jean-Philippe Lang
0153954ebb Added a simple rake task to fetch changesets from the repositories:
rake redmine:fetch_changesets

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1155 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 16:25:58 +00:00
Jean-Philippe Lang
153c4c242d Atom feeds:
* prevent duplicate entry ids for issue changes
* prevent empty email in author element

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1154 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 16:20:35 +00:00
Jean-Philippe Lang
0582337372 Added:
* the 'include' macro to include a wiki page
* macro escaping support (with exclamation mark)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1153 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 15:34:17 +00:00
Jean-Philippe Lang
1c8cf4ef83 Added the following permissions (#527, #585, #627):
* edit_issue_notes: let user edit any notes
* edit_own_issue_notes: let user edit his own notes only

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1152 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 13:19:33 +00:00
Jean-Philippe Lang
abb0b15407 Added translation support for project modules names and a few other strings.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1151 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 10:15:19 +00:00
Jean-Philippe Lang
384a444cee Fix grammar error in e-mail footer (closes #673).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1150 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-16 09:14:59 +00:00
Jean-Philippe Lang
38307c0979 Added Ukrainan translation (Natalia Konovka & Mykhaylo Sorochan).
Translation updates:
* Lithuanian (Sergej Jegorov)
* Russian (Michael Pirogov)
* Traditional Chinese (Shortie Lo)
* Finnish (Antti Perkiömäki)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1149 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-15 17:47:19 +00:00
Jean-Philippe Lang
96b78fdae5 Fix applied to ruby-net-ldap (#608).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1148 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-15 17:22:21 +00:00
Jean-Philippe Lang
377a45ae1a Slight changes to calendar and activity h2 titles.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1147 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-15 17:04:27 +00:00
Jean-Philippe Lang
29e297d273 Make Mantis importer preserve bug ids.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1146 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-15 16:41:44 +00:00
Jean-Philippe Lang
71d089c833 Escape titles in activity view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1145 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-14 21:17:28 +00:00
Jean-Philippe Lang
8adb320978 Fixed a bug in localization introduced by r1131 (anonymous users inherit the language of the first anonymous user).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1144 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-13 20:47:27 +00:00
Jean-Philippe Lang
0b6a010a12 Translation for various hard-coded strings (#577).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1143 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-12 22:43:37 +00:00
Jean-Philippe Lang
4dc7f662e3 Added issue subject to the time entries view and subject + tracker to the csv export (#616). Default order on date column set to desc.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1142 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-12 21:43:54 +00:00
Jean-Philippe Lang
a2626bbccf Fixed: not null constraints not removed with Postgresql.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1140 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-12 21:11:16 +00:00
Jean-Philippe Lang
616899d879 Fixed: empty status list shouldn't be displayed when the user can not change the status.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1139 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-12 18:33:38 +00:00
Jean-Philippe Lang
30a7314b86 Fixed: wiki and changeset links not displayed when previewing issue description or notes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1138 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-12 18:31:15 +00:00
Jean-Philippe Lang
c5d998528f The following menus can now be extended by plugins: top_menu, account_menu, application_menu (empty by default).
Sligth layout change: links in the top menu are now li elements.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1137 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-12 17:58:46 +00:00
Jean-Philippe Lang
93ef8b7f77 Replaced window.hash= by Element.scrollTo()
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1136 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-11 20:45:46 +00:00
Jean-Philippe Lang
15ed53cf17 Footer update.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1135 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-11 18:08:36 +00:00
Jean-Philippe Lang
5152771fdd Fixed: 404 error when selecting an other project on issues/move.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1134 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-11 18:06:38 +00:00
Jean-Philippe Lang
913a806f90 Fixed: context menu not available if the initial issue list is empty.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1133 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-11 17:59:41 +00:00
Jean-Philippe Lang
d859d38dd9 Translation updates:
* Finnish (Antti Perkiömäki, #612)
* Russian (Michael Pirogov, #622)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1132 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-10 14:24:51 +00:00
Jean-Philippe Lang
14b3d6d012 Fixed: Anonymous users may not see the issue list headers in the correct language.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1131 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-10 14:14:18 +00:00
Jean-Philippe Lang
4155c97222 Issue list now supports bulk edit/move/delete (#563, #607). For now, issues from different projects can not be bulk edited/moved/deleted at once.
There are 2 ways to select a set of issues on the issue list:
* by using checkbox and/or the little pencil that will select/unselect all issues (#567)
* by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues

Context menu was disabled on links so that the default context menu of the browser is displayed when right-clicking on a link (#545).
All this was tested with Firefox 2, IE 6/7, Opera 8 (use Alt+Click instead of Right-click) and Safari 2/3.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1130 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-10 13:17:41 +00:00
Jean-Philippe Lang
43a6f312ed Merged IssuesController #edit and #update into a single actions.
Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status (#519, #581, #587).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1129 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-09 16:11:18 +00:00
Jean-Philippe Lang
6d109258c9 Added a few extensions/mimetypes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1128 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-07 18:06:35 +00:00
Jean-Philippe Lang
9a2ec76a81 Mantis importer: few fixes in user mapping.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1127 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-06 20:43:31 +00:00
Jean-Philippe Lang
c80c1e1ead Create a journal and send an email when an issue is closed by commit (#609).
The redmine user is found using the committer username or email. Otherwise, the journal is created with anonymous user.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1126 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-06 20:02:30 +00:00
Jean-Philippe Lang
943ba3e34f Trac importer: handle nil usernames.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1125 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-06 19:05:42 +00:00
Jean-Philippe Lang
68d3305ae2 Fixed settings.yml syntax (yaml parsing fails with JRuby 1.0.3: #606).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1124 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-06 18:56:52 +00:00
Jean-Philippe Lang
fde2a497da Activity test fix (r1120).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1123 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-05 18:50:21 +00:00
Jean-Philippe Lang
14b2d3f4b9 Slight change to the issue added/updated email templates.
Translations updates:
* Simplified Chinese (Liang Jin)
* Bulgarian (Nikolay Solakov)
* Lithuanian (Sergej Jegorov)
* Traditional Chinese (Shortie Lo)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1122 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-05 18:38:05 +00:00
Jean-Philippe Lang
54ff294da1 More appropriate default sort order on sortable columns.
Sortable column added on issue subject (#580).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1121 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-05 17:28:02 +00:00
Jean-Philippe Lang
80a60247f5 Slight changes to the activity view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1120 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-04 19:37:23 +00:00
Jean-Philippe Lang
a3dda6dc4a Use Postgresql's reset_pk_sequence in Trac importer to reset issue id sequence (#595).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1119 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-04 18:01:38 +00:00
Jean-Philippe Lang
4e244be21c Slight changes on users list view and hide Anonymous user.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1118 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 19:53:52 +00:00
Jean-Philippe Lang
09cfef094c New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1117 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 18:05:26 +00:00
Jean-Philippe Lang
9c451ac157 Set wiki class to issue notes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1116 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 17:34:31 +00:00
Jean-Philippe Lang
5dd3c239d3 Fixed: error when uploading a file with no content-type specified by the browser.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1115 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 17:24:58 +00:00
Jean-Philippe Lang
0beafecd3b Fixed #208: Issue list does not scroll up when you click next
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1114 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 16:21:51 +00:00
Jean-Philippe Lang
2920daf2c8 Do not use RedCloth's glyphs (#210).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1113 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 15:34:58 +00:00
Jean-Philippe Lang
c3d909e0cc Slight style changes on issue associated changesets list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1112 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 15:15:52 +00:00
Jean-Philippe Lang
1ecef3a95a ProjectsController#add_news moved to NewsController#new.
Preview added when adding/editing a news (#590).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1111 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 14:38:04 +00:00
Jean-Philippe Lang
b124501a4e Add 'Author' to the available columns for the issue list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1110 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 14:02:23 +00:00
Jean-Philippe Lang
225637c5dc Translations updates (#254, #255, #260):
* Traditional Chinese (Shortie Lo)
* German (Thomas Löber)
* Russian (Michael Pirogov)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1109 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-03 13:54:59 +00:00
Jean-Philippe Lang
0123dc3651 Do not authorize project identifier with numbers only (would be interpreted as the project id in urls).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1108 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-02 15:51:48 +00:00
Jean-Philippe Lang
12c0f5f66e Do not show Roadmap menu item if the project doesn't define any versions.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1107 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-02 11:08:04 +00:00
Jean-Philippe Lang
45b69c4a65 Fixed an error on issue feeds.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1106 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-02 10:54:31 +00:00
Jean-Philippe Lang
bea49ae245 Administrators can edit issue notes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1105 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-02-02 10:50:31 +00:00
Jean-Philippe Lang
4abb82fd7b Fixed RepositoriesController: undefined local variable or method `show_error' (broken by r1094).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1104 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-25 10:55:16 +00:00
Jean-Philippe Lang
79f92a675a User display format is now configurable in administration settings.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1103 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-25 10:31:06 +00:00
Jean-Philippe Lang
3d5d1a38e3 Translations updates:
* Russian (Michael Pirogov)
* Finnish (Antti Perkiömäki)
* Traditional Chinese wiki toolbar (Shortie Lo)
* German wiki toolbar (Thomas Löber)


git-svn-id: http://redmine.rubyforge.org/svn/trunk@1102 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-24 18:31:02 +00:00
Jean-Philippe Lang
e13f49d919 Prevent unexpected nil in custom value validation.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1101 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-24 18:15:38 +00:00
Jean-Philippe Lang
7a1e928b8d Mantis importer:
* do not truncate projects descriptions
* encode attachment filenames to utf8

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1100 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-23 22:31:26 +00:00
Jean-Philippe Lang
b0f3de5c3b Fixed: PostgreSQL issues_seq_id not updated when using Trac importer.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1099 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-23 22:19:24 +00:00
Jean-Philippe Lang
2247700f24 Fixed: Incorrect filtering for unset values when using 'is not' filter.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1098 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-23 20:07:17 +00:00
Jean-Philippe Lang
d8ac6d2b79 Issue properties below the description textarea.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1097 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-23 19:58:11 +00:00
Jean-Philippe Lang
2d9e669e85 Added preview for issue notes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1096 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-23 18:21:42 +00:00
Jean-Philippe Lang
d54ba39e7a Fixed: can not lock a topic when creating it.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1095 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-23 17:48:48 +00:00
Jean-Philippe Lang
91dc13f4b2 Show explicit error message when the scm command failed (eg. when svn binary is not available).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1094 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-23 17:25:11 +00:00
Jean-Philippe Lang
97e7432ce6 Fixed migration 87 (mysql: TEXT column can't have a default value).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1093 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-22 17:17:52 +00:00
Jean-Philippe Lang
dad5f6d403 Fixed search with all words (broken in r994).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1092 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-21 18:52:45 +00:00
Jean-Philippe Lang
c65ab7c0f2 Missing migration for r1090.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1091 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 23:38:55 +00:00
Jean-Philippe Lang
d6bfb7fa4d Added default value for custom fields. Fixed javascript on custom field form for project and user custom fields.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1090 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 21:29:51 +00:00
Jean-Philippe Lang
59c5d995c3 Fixed: custom fields empty on issue/edit (broken by r1086).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1089 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 19:45:08 +00:00
Jean-Philippe Lang
df99d8f308 Unlimited and optional project description. The project list will show truncated descriptions only (the first fews lines).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1088 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 18:37:51 +00:00
Jean-Philippe Lang
d5b9dedca2 Added tabindex property on wiki toolbar buttons and 'new category' link.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1087 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 17:36:01 +00:00
Jean-Philippe Lang
0b4a02f607 Display custom fields in two columns on the issue form.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1086 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 17:20:34 +00:00
Jean-Philippe Lang
b6549d763a Added related changesets messages on issue details view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1085 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 15:38:11 +00:00
Jean-Philippe Lang
5e2a01656d Test environments cleanup.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1084 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 14:24:19 +00:00
Jean-Philippe Lang
d79c20c4f2 Fixed: custom field selection is not saved when unchecking them all on project settings
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1083 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 14:01:02 +00:00
Jean-Philippe Lang
889bf388be Fixed: error when removing a project member (postgresql and sqlite only).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1082 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 13:53:45 +00:00
Jean-Philippe Lang
16e9ffce0d Added a 'New issue' link in the main menu (accesskey 7).
The drop-down lists to add an issue on the project overview and the issue list are removed.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1081 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 13:07:19 +00:00
Jean-Philippe Lang
44ac1a0deb ProjectsController#add_issue moved to IssuesController#new.
Tracker can now be changed/selected on the new issue form. This action can be invoked without the tracker_id parameter (the first enabled tracker will be used by default).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1080 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 11:30:57 +00:00
Jean-Philippe Lang
1b002983c3 Display the issue status in the email subject only if the status was actually changed.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1079 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 10:42:16 +00:00
Jean-Philippe Lang
1535ac1220 Fixed: when changing the status of an issue, the email subject contains the previous status of the issue.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1078 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-20 09:49:25 +00:00
Jean-Philippe Lang
2a945f794f Wiki toolbar Russian and Czech translations (Michael Pirogov).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1077 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-19 12:29:41 +00:00
Jean-Philippe Lang
0faa4568a0 Highlight the current item of the main menu.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1076 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-19 11:53:43 +00:00
Jean-Philippe Lang
3e031b4243 Fixed: locked users should not receive email notifications.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1075 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-17 19:58:03 +00:00
Jean-Philippe Lang
32b9bf0ef2 Added i18n support to the jstoolbar (only english and french are actually translated).
Translations can be found in public/javascripts/jstoolbar/lang/

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1074 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-17 18:39:17 +00:00
Jean-Philippe Lang
1c5a9b1773 Prettier tabs for settings views.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1073 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-17 17:43:54 +00:00
Jean-Philippe Lang
061977d77d Fixed: typo in en.yml
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1072 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-17 17:40:45 +00:00
Jean-Philippe Lang
049051103b On the calendar, the gantt and in the Tracker filter on the issue list, only active trackers of the project (and its sub projects) can be selected.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1071 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-16 21:27:48 +00:00
Jean-Philippe Lang
18066ba8bf Added a couple of mime types so that corresponding files can be viewed in the browser.
Added a simple (and not perfect) CodeRay scanner for php.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1070 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-16 20:45:13 +00:00
Jean-Philippe Lang
ad25d15d6c Fixed error on migration 85 with Oracle (index name can not be more than 30 characters long).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1069 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-15 20:40:59 +00:00
Jean-Philippe Lang
e228110614 Lithuanian translation added (Sergej Jegorov).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1068 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-15 19:03:17 +00:00
Jean-Philippe Lang
8a39862b19 Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1067 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-15 18:31:45 +00:00
Jean-Philippe Lang
bf3066d698 Fixed: Date custom fields not displayed as specified in application settings.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1066 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-15 18:30:21 +00:00
Jean-Philippe Lang
35c17355fb Fixed: issue filters may be lost when paginating after r1026.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1065 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-15 18:18:49 +00:00
Jean-Philippe Lang
702b521b45 Redmine links can be used to link to documents, versions and attachments.
For now, attachments of the current object can be referenced only (if you're on an issue, it's possible reference attachments of this issue only).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1064 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-15 18:12:12 +00:00
Jean-Philippe Lang
4e1e5985a1 Wiki toolbar improvements (mainly for Firefox).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1063 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-13 18:39:37 +00:00
Jean-Philippe Lang
8b710cf9de Do not show sticky and locked checkboxes when replying to a message.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1062 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-13 18:33:55 +00:00
Jean-Philippe Lang
df30e18a8c Added missing wiki div on news/index and news/show.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1059 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-12 16:26:37 +00:00
Jean-Philippe Lang
64eaf328ea Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1058 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-12 14:54:26 +00:00
Jean-Philippe Lang
941f9bf3dd Non-ascii attachement filename fix for IE.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1053 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-10 22:42:41 +00:00
Jean-Philippe Lang
a39f655a7c Added details by assignees on issue summary view (Hans Yoon).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1052 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-10 19:20:36 +00:00
Jean-Philippe Lang
84cdc8f2bc Translations:
* Finnish added (Antti Perkiömäki)
* Traditional Chinese updated (Shortie Lo)
* Russian updated
* Polish updated (Mariusz Olejnik)
* Korean updated (Hans Yoon)
* Japanese calendar fix (Nobuhiro Imai)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1051 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-10 18:34:14 +00:00
Jean-Philippe Lang
b2b56adbfa Fixed: Themes are not applied with Rails 2.0
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1047 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-07 20:14:50 +00:00
Jean-Philippe Lang
a80dbc49b1 Admin settings screen split to tabs.
Email notification options moved to this view as a tab and LDAP list is accessible from the 'Authentication' tab.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1046 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-06 20:24:26 +00:00
Jean-Philippe Lang
78d9ae9754 Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil (same fix as r921).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1045 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-06 17:33:41 +00:00
Jean-Philippe Lang
3d920c13b4 wrapper div added to the layout.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1044 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-06 17:28:20 +00:00
Jean-Philippe Lang
976a31e941 Merged IssuesController change_status and add_note actions.
The 'Change status' specific form removed and now accessible from issue/show view with no additional request (click on 'Update' to show the form).
The 'Change issue status' permission is removed. To change the status, the user just needs to have either 'Edit' or 'Add note' permissions and some workflow transitions allowed.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1043 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-06 17:06:14 +00:00
Jean-Philippe Lang
4a729036bf Prevent 'has already been taken' error messages for user login and email if these fields are left empty.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1042 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-06 13:09:23 +00:00
Jean-Philippe Lang
c9fd7513ed Default configuration data can now be loaded from the administration screen.
A message is automatically displayed on this screen if roles, trackers,... have not been configured yet.
The rake task is still available and the data loading code is wrapped in a transaction.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1040 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-05 11:33:49 +00:00
Jean-Philippe Lang
8d9b04865b Fixed Serbian lang file.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1039 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-04 20:03:31 +00:00
Jean-Philippe Lang
4024f4ac39 Updated Serbian translation (Dragan Matic).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1038 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-04 19:07:16 +00:00
Jean-Philippe Lang
6ae87d7c4c Moved redmine:load_default_data code to a module so that it can be called from the application.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1037 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-04 18:27:05 +00:00
Jean-Philippe Lang
860ff9345f Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1036 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-04 18:02:34 +00:00
Jean-Philippe Lang
bd85c0cbb1 Fixed: table of content not rendered properly when used in an issue or document description.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1035 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-04 17:55:08 +00:00
Jean-Philippe Lang
832c7eaaa5 Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1034 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-03 18:28:14 +00:00
Jean-Philippe Lang
b21e85b6dc Updated Spanish translation (Gumer Coronel Pérez).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1033 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-03 18:10:52 +00:00
Jean-Philippe Lang
3e0acc0b7e Slight improvements to the browser views.
ApplicationHelper#set_html_title replaced by html_title with arguments and can be called several times in views to append elements to the title.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1032 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-01-02 22:41:53 +00:00
Jean-Philippe Lang
636579b17d Fix query management broken by r1027.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1031 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-31 12:37:32 +00:00
Jean-Philippe Lang
97f0da0b1a Moved login and logout links to ApplicationHelper methods for easier customization.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1030 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-30 11:44:46 +00:00
Jean-Philippe Lang
9072753489 Moved current user management to a dedicated method for modularity.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1029 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-30 10:51:34 +00:00
Jean-Philippe Lang
66420fe4b7 TabularFormBuilder moved out of application_helper.rb
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1028 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-30 10:48:11 +00:00
Jean-Philippe Lang
5bc7dc77e2 Do not store query object in session but id or filters only. This allows to use Rails 2.0 cookie based sessions.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1027 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-30 10:46:09 +00:00
Jean-Philippe Lang
9a1b46fe42 New setting added to specify how many objects should be displayed on most paginated lists.
Default is: 25, 50, 100 (users can choose one of these values).
If one value only is entered in this setting (eg. 25), the 'per page' links are not displayed (prior behaviour).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1026 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-29 11:36:30 +00:00
Jean-Philippe Lang
7eec539222 Fixed r1024: context submenus on the issue list don’t show up with IE6.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1025 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-29 10:43:33 +00:00
Jean-Philippe Lang
7f3c516940 Fixed: context submenus on the issue list don't show up with IE6.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1024 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-28 21:58:33 +00:00
Jean-Philippe Lang
3ff95e4031 Fixed 'export to' links positioning on wiki page.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1023 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-22 12:47:23 +00:00
Jean-Philippe Lang
bb8d360188 Set doctype to transitional. Fixed a few non matching tags in views.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1022 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-21 18:01:02 +00:00
Jean-Philippe Lang
9595029709 Trunk version changed to 0.6.devel
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1021 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-20 19:44:16 +00:00
Jean-Philippe Lang
31c6ebb310 Added wiki annotate view. It's accessible for each version from the page history view.
Slight style change: pre-wrap added on file/diff contents.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1020 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-20 19:10:24 +00:00
Jean-Philippe Lang
3d5381b24b Do not run Mercurial functional tests if the test repository is not set up.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1019 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-19 22:14:03 +00:00
Jean-Philippe Lang
a456225852 Add email notification to IssuesController#edit.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1018 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-19 22:09:31 +00:00
Jean-Philippe Lang
5f871e9657 Fixed: Textile image with style attribute cause internal server error.
Also added tests for inline images with attributes and moved auto_link and auto_mailto rules after textile rules.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1017 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-19 21:06:06 +00:00
Jean-Philippe Lang
bf3b2ec973 Change status select box default to current status (Rocco Stanzione).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1016 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-19 20:23:03 +00:00
Jean-Philippe Lang
49bdf62243 Add autoscroll div for each file diff.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1015 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-19 20:19:48 +00:00
Jean-Philippe Lang
e219af1fbd Ignore empty diffs.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1014 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-19 20:11:44 +00:00
Jean-Philippe Lang
4a2efa9252 Ported r1009 from 0.6-stable branch.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1013 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-18 19:07:54 +00:00
Jean-Philippe Lang
8e00f57a88 Moved ProjectsController#list_documents and add_document to DocumentsController#index and new.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1011 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-18 18:50:41 +00:00
Jean-Philippe Lang
524cd689cf Project identifier is now used in URLs (instead of project id).
URLs with a project id will still be recognized.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1007 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-17 21:00:56 +00:00
Jean-Philippe Lang
b4eafd9ea8 Changes for 0.6.2 release.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1004 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-16 14:03:26 +00:00
Jean-Philippe Lang
2bcd448dda Updated Japanese translation (Satoru Kurashiki).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1003 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-16 13:33:15 +00:00
Nicolas Chuche
1af9c47a27 bug when using apache authentication method
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1002 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-15 12:23:39 +00:00
Jean-Philippe Lang
ea35fff5bf SCM adapters: moved Errno::ENOENT exception rescuing to the abstract adapter.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1001 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-15 12:14:40 +00:00
Jean-Philippe Lang
124ef65c1f Fixed warning: toplevel constant User referenced by WikiContent::User
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1000 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-15 11:57:48 +00:00
Jean-Philippe Lang
361c50c1f4 Added some functional tests (wiki).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@999 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-15 11:57:06 +00:00
Jean-Philippe Lang
27d6334afa Fixed: Unable to create a wiki (Rails 2.0 compatibility).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@998 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 22:00:03 +00:00
Jean-Philippe Lang
963b1283c2 Fixed Bazaar test repository path.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@997 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 21:49:23 +00:00
Jean-Philippe Lang
777edc13c1 Fixed: can not save numeric, date and boolean custom fields (broken by r994).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@996 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 21:47:57 +00:00
Jean-Philippe Lang
d1a3fbea40 Fixed Trac importer error with Rails 2.0: readonly? is defined by ActiveRecord.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@995 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 21:31:30 +00:00
Jean-Philippe Lang
58610ec52a Search engine: issue custom fields can now be searched.
Each issue custom field (excepting numeric, date and boolean fields) can be marked as "Searchable" (default to false).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@994 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 18:54:55 +00:00
Jean-Philippe Lang
38b185f1dc Fixed: empty lines when displaying repository files with Windows style eol.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@993 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 17:48:11 +00:00
Jean-Philippe Lang
e69631f26c Fixed: missing body closing tag in repository annotate and entry views.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@992 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 17:47:30 +00:00
Jean-Philippe Lang
47f399104b Added a Mercurial test repository with unit and functional tests.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@991 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 17:46:45 +00:00
Jean-Philippe Lang
86319feef2 Added ApplicationController#attach_files as a common method to attach files in all actions.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@990 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 17:33:05 +00:00
Jean-Philippe Lang
eb7cbd481e Added some tests for projects controller and helper.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@989 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-14 17:31:24 +00:00
Jean-Philippe Lang
17c7886791 Removed unused UsersController#destroy.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@988 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-13 18:53:34 +00:00
Jean-Philippe Lang
48949f979a Added some functional tests and a CVS test repository.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@987 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-13 18:52:09 +00:00
Jean-Philippe Lang
86d756d22d News comments are now textilized.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@986 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 22:57:20 +00:00
Jean-Philippe Lang
ab4ff48abc Added /coverage and /vendor/rails to svn-ignore.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@985 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 22:23:34 +00:00
Jean-Philippe Lang
63ef594033 Added some functional tests (issues).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@984 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 22:22:33 +00:00
Jean-Philippe Lang
bd8eded670 Fixed: 500 error when validation fails on issue edition with no custom fields.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@983 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 21:10:04 +00:00
Jean-Philippe Lang
d5cc40c9b6 Fixed: calendar and gantt broken with Rails 2.0
git-svn-id: http://redmine.rubyforge.org/svn/trunk@982 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 21:03:36 +00:00
Jean-Philippe Lang
76ed8cc200 Added some functional tests (projects and repositories).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@981 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 20:56:22 +00:00
Jean-Philippe Lang
3539bef96b Updated Chinese translation (Shortie Lo).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@980 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 20:49:02 +00:00
Jean-Philippe Lang
e29539df9c Fixed: 'assigned to me' filter broken.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@979 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-12 20:45:32 +00:00
Jean-Philippe Lang
7e9c454478 Fixed: 'LDAP account password is too long' error when leaving the field empty on creation.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@977 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-10 20:58:24 +00:00
Jean-Philippe Lang
c00b8fd12e Doc and version changes for 0.6.1 release.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@976 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-10 18:39:57 +00:00
Jean-Philippe Lang
6d9490ddcc Merged Rails 2.0 compatibility changes.
Compatibility with Rails 1.2 is preserved.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@975 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-10 17:58:07 +00:00
Jean-Philippe Lang
f58db70bde More detailed html title on several views.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@964 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 18:42:40 +00:00
Jean-Philippe Lang
51adc242a3 Updated Polish translation (Mariusz Olejnik).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@963 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 17:58:44 +00:00
Jean-Philippe Lang
7fae4b0892 Bazaar adapter: fixed log with partial revisions parsing.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@962 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 17:51:54 +00:00
Jean-Philippe Lang
6239e319ac Fixed versions/show layout with IE.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@961 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 15:02:57 +00:00
Jean-Philippe Lang
60c82a03db Fixed drop down lists overflow on My account.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@960 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 14:57:17 +00:00
Jean-Philippe Lang
36c0ab196c Added Traditional Chinese translation (by Shortie Lo).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@959 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 14:43:07 +00:00
Jean-Philippe Lang
a7c2b63fb6 Transaction and performance improvement on workflow copy when creating a new tracker.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@958 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 13:35:38 +00:00
Jean-Philippe Lang
5e38bd9363 Performance improvement on workflow setup screen.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@957 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 11:19:30 +00:00
Jean-Philippe Lang
a79cf8d574 Fixed a parenthesis warning in news/show.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@956 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 10:27:52 +00:00
Jean-Philippe Lang
3c44aac1f5 Added version details view accessible from the roadmap.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@955 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-07 10:26:07 +00:00
Jean-Philippe Lang
355289fa67 Roadmap progress bars now differentiate the progress due to closed issues from the open issues progress (2 different colors).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@954 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-05 19:21:25 +00:00
Jean-Philippe Lang
b536f4c657 Restored wiki syntax quick ref pop-up (accessible from wiki/edit).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@953 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-04 18:14:09 +00:00
Jean-Philippe Lang
d00014221e Changesets retrieval optimization on the activity view. Prevents additional query from being executed for each displayed changeset.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@952 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-03 23:05:45 +00:00
Jean-Philippe Lang
3b4cfe0ba8 Added some unit tests for the Bazaar adapter.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@951 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-03 19:19:36 +00:00
Jean-Philippe Lang
056e3703da Added Bazaar adapter.
Fixed 'quick jump to a revision' form on the revisions list.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@950 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-03 17:40:43 +00:00
Jean-Philippe Lang
92a23c05bb Project name format limitation removed (name can now contain any character).
Project identifier maximum length changed from 12 to 20.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@949 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-03 10:28:08 +00:00
Jean-Philippe Lang
7d8af70a63 Changed the maximum length of LDAP account to 255 characters.
Added length validations on AuthSource model.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@948 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-02 22:07:56 +00:00
Jean-Philippe Lang
8c65cc4712 Added Annotate/Blame view for Subversion, CVS and Mercurial repositories.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@947 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-02 20:58:02 +00:00
Jean-Philippe Lang
e4724c7626 Trac importer:
* should now support mysql and postgresql Trac database
* minor fixes

git-svn-id: http://redmine.rubyforge.org/svn/trunk@946 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-02 15:09:43 +00:00
Jean-Philippe Lang
6e74a06808 Fixed: error on admin/info if there's more than 1 plugin installed.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@945 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-02 14:20:28 +00:00
Jean-Philippe Lang
bc060b31ae Email notifications are now sent as Blind carbon copy by default. This can be changed in email notifications settings (new setting added).
Emission email address setting moved to the email notifications settings view.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@944 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-02 13:52:16 +00:00
Jean-Philippe Lang
aebcfb1eda When creating a new role, permissions are pre-filled with 'Non member' role permissions.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@943 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-02 12:58:07 +00:00
Jean-Philippe Lang
457c9a8e72 Fixed: svn or ldap password can be found in clear text in the html source in editing mode.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@942 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-01 22:03:45 +00:00
Jean-Philippe Lang
3f2f7153a9 Fixed: Date and time formats defined in settings not applied to the issues CSV export.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@941 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-01 21:34:16 +00:00
Jean-Philippe Lang
f5d68cf688 Fixed a broken link in the SCM browser
git-svn-id: http://redmine.rubyforge.org/svn/trunk@940 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-01 21:33:15 +00:00
Jean-Philippe Lang
81ada666bb 'Assigned to' drop down list is now sorted by user's lastname.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@939 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-01 17:42:26 +00:00
Jean-Philippe Lang
db002edabd * Added links to previous and next revisions on revision view (patch by Cyril Mougel slightly edited)
* Fixed TimelogController#report december error
* Fixed ProjectsControllerTest#test_activity 1st and 2nd day of the month failure

git-svn-id: http://redmine.rubyforge.org/svn/trunk@938 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-01 17:15:42 +00:00
Jean-Philippe Lang
3baf086e2d Fixed: 'View all issues' link doesn't work on issues/show.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@937 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-29 20:14:01 +00:00
Jean-Philippe Lang
2cf11bd64e Fixed Mantis importer: projects trackers and modules assignment
Fixed Trac and Mantis importers: roles assignments

git-svn-id: http://redmine.rubyforge.org/svn/trunk@936 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-29 20:08:14 +00:00
Jean-Philippe Lang
bf6e02c739 Search engine: search can be restricted to an exact phrase by using quotation marks (eg. hello "bye bye" can be used to search for "hello" and "bye bye" strings).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@935 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-29 18:33:42 +00:00
Jean-Philippe Lang
233990dac3 * Updated German translation (Thomas Löber)
* Updated Spanish translation (Gumer Coronel Pérez)
* Fixed: test in method project in Attachment Model useless (Cyril Mougel)
* Fixed: public/.htaccess lacks support for mod_fcgid and adds handlers without checking for their existance (Nils Adermann)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@934 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-27 17:20:57 +00:00
Jean-Philippe Lang
c383486d71 Added custom fields marked as "For all projects" to the csv export of the cross project issue list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@933 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-27 17:04:13 +00:00
Jean-Philippe Lang
508b0bbb8e * Updated Spanish translation (Gumer Coronel Pérez)
* Fixed mailer test errors

git-svn-id: http://redmine.rubyforge.org/svn/trunk@932 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-26 18:47:49 +00:00
Jean-Philippe Lang
3937caeb3c Trac importer improvements (by Mat Trudel):
* better support for wiki internal links (still not perfect, but much improved)
* support for unordered lists
* support for most of trac's highlighting tags (underline, bold, etc)
* import progress dots now flush to stdout on every dot, so the import doesn't look frozen
* support for migration of multiple trac instances into a single Redmine install (as separate projects)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@931 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-25 12:11:40 +00:00
Jean-Philippe Lang
f2a12d9eb5 Added user status criteria in Redmine.pm
git-svn-id: http://redmine.rubyforge.org/svn/trunk@930 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-24 16:55:08 +00:00
Jean-Philippe Lang
acad206063 Issue context menu now also available on 'My page'.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@929 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-24 15:16:04 +00:00
Jean-Philippe Lang
f80f04e379 Slight optimization in User#role_for_project.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@928 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-24 15:14:32 +00:00
Jean-Philippe Lang
fde4a42e2a Removed the 12 characters limit on passwords.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@927 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-24 12:52:29 +00:00
Jean-Philippe Lang
29b3614bcb Forums enhancements:
* messages can now be edited/deleted (explicit permissions need to be given)
* topics can be locked so that no reply can be added (only by users allowed to edit messages)
* topics can be marked as sticky so that they always appear at the top of the list (only by users allowed to edit messages)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@926 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-24 12:25:07 +00:00
Jean-Philippe Lang
866e9e2503 Fixed: error on account/register when validation fails.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@925 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-23 23:23:39 +00:00
Jean-Philippe Lang
c21ee95ade * Updated Russian translation (iGor kMeta)
* Fixed front page typo: View all issues instead of View all news (Derek Montgomery)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@924 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-23 18:27:26 +00:00
Jean-Philippe Lang
7704e2d919 Forums: attachments can now be added to replies.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@923 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-22 19:19:47 +00:00
Jean-Philippe Lang
0634591b3d Themes:
* Fixed: themes are not found when running Apache+fastcgi
* Added 'Classic' theme (inspired from the v0.51 design)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@922 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-22 18:03:02 +00:00
Jean-Philippe Lang
5e6fa147da * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter'
* Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt)
* Fixed: Wrong feed URLs on the home page

git-svn-id: http://redmine.rubyforge.org/svn/trunk@921 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-21 18:45:18 +00:00
Jean-Philippe Lang
8d91afc33e Added per-project tracker selection. Trackers can be selected on project settings.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@920 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-20 20:29:03 +00:00
Jean-Philippe Lang
987a5aa221 Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums.
These permissions need to be explicitly given to the Anonymous role (Admin -> Roles & Permissions -> Anonymous).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@919 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-20 15:40:16 +00:00
Jean-Philippe Lang
99f9aea80a * Referencing issues in commit messages: enter * in 'Referencing keywords' to link any issue id without using keywords.
* Updated Polish translation (Mariusz Olejnik).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@918 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-20 12:07:28 +00:00
Jean-Philippe Lang
deb182337d * Added time zone support: users can select their time zone on their account view.
* Updated Polish translation (Mariusz Olejnik).
* Fixed: Projects should be listed with case mixed.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@917 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-19 22:28:43 +00:00
Nicolas Chuche
a1f3497ec4 * add Redmine.pm to authenticate with mod_perl
* add a --test option in reposman.rb
* change owner right to fit with apache write access to repositories
* add a deprecated warning in reposman.pl


git-svn-id: http://redmine.rubyforge.org/svn/trunk@916 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 18:51:48 +00:00
Jean-Philippe Lang
9c9ae21771 There's now 3 account activation strategies (available in application settings):
* activation by email: the user receives an email containing a link to active his account
* manual activation: an email is sent to administrators for account approval (default)
* automatic activation: the user can log in as soon as he has registered

git-svn-id: http://redmine.rubyforge.org/svn/trunk@915 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 17:46:55 +00:00
Jean-Philippe Lang
f8aa2dc9b7 'fixed version' field can now be displayed on the issue list.
Category and fixed version fields added to the CSV export.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@914 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 15:53:58 +00:00
Jean-Philippe Lang
bb2de53d93 Added an alternate theme which provides issue list colorization based on issues priority.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@912 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 14:32:39 +00:00
Jean-Philippe Lang
e2c606e974 Fixed: Update of time entry fails when the issue has been moved to an other project.
Fixed: Error when moving an issue without changing its tracker (Postgresql).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@909 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-17 17:45:21 +00:00
Jean-Philippe Lang
9ad79612fe Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account).
Issues and issues list: 'done ratio' field now displayed as a progress bar.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@908 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-17 15:34:10 +00:00
Jean-Philippe Lang
1469a4c8df Fixed (CVS adapter): changes not recorded when using :pserver string.
Updated Russian js calendar (iGor kMeta).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@907 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-16 21:24:22 +00:00
Jean-Philippe Lang
76cdef46f2 Fixed: localization problem on issue list headers.
Updated Russian translation (iGor kMeta).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@906 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-16 20:46:33 +00:00
Jean-Philippe Lang
e4ce95c3a1 Added a couple of new formats for the 'date format' setting.
Added a 'time format' setting.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@905 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-16 20:26:36 +00:00
Jean-Philippe Lang
4951676172 Added Russian translation (iGor kMeta).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@904 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-15 18:24:18 +00:00
Jean-Philippe Lang
b0a8888e35 Fixed: admin should be able to move issues to any project.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@903 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-14 21:54:16 +00:00
Jean-Philippe Lang
833c7bd659 Fixed: adding an attachment is not possible when changing the status of an issue.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@902 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-12 17:29:41 +00:00
Jean-Philippe Lang
5944696b6d Custom fields can now be reordered.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@901 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-12 17:23:14 +00:00
Jean-Philippe Lang
a727f0d25a Removed hard coded string on 'My account'.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@900 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-12 16:47:07 +00:00
Jean-Philippe Lang
0fe5c7b3e0 Added an option on 'My account' for users who don't want to be notified of changes that they make.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@899 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-12 16:43:49 +00:00
Jean-Philippe Lang
843d04f0e3 Fixed: No mime-types in documents/files downloading
git-svn-id: http://redmine.rubyforge.org/svn/trunk@898 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-12 15:13:42 +00:00
Jean-Philippe Lang
8a8f819d27 Added wiki macros support. 2 builtin macros are defined: hello_world (sample macro that displays the arguments) and macro_list (display the list of installed macros).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@897 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-12 14:36:33 +00:00
Jean-Philippe Lang
a8419c1425 Fixed: error on activity page when displaying a document (undefined method 'author').
git-svn-id: http://redmine.rubyforge.org/svn/trunk@896 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-12 08:00:00 +00:00
Jean-Philippe Lang
362364fd6f Fixed: CSV constant warning
git-svn-id: http://redmine.rubyforge.org/svn/trunk@895 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-11 20:55:27 +00:00
Jean-Philippe Lang
b0bb1baeaa Trac importer: user can now choose between sqlite and sqlite3 adapter for Trac database.
Trac importer: issues and wiki modules are enabled by default for the imported project.
Fixed: 404 error when trying to save a custom query under certain circumstance.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@894 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-09 13:32:14 +00:00
Jean-Philippe Lang
1454a711ca Diff style (inline or side by side) automatically saved as a user preference.
Fixed a Postgres test failure.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@893 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-08 19:14:34 +00:00
Jean-Philippe Lang
fa95501fe5 Added issues status changes on the activity view (initial patch by Cyril Mougel).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@892 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-08 19:00:37 +00:00
Jean-Philippe Lang
a069c4afcf Added forums topics on the activity view (disabled by default).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@891 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-07 23:01:24 +00:00
Jean-Philippe Lang
cc8220dde8 Fixed: error when sorting the messages if there's only one board for the project.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@890 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-07 21:21:12 +00:00
Jean-Philippe Lang
63ceea2e21 Custom fields can now be displayed as columns on the issue list.
Custom fields marked as "for all projects" can be added to the default columns of the issue list (in application settings).
Project specific custom fields can be displayed on custom queries.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@889 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-07 20:42:28 +00:00
Jean-Philippe Lang
ad68a82be1 Moved ProjectsController#list_news to NewsController#index.
Removed FeedsController, issues and news feeds are now handled by issues and news controllers.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@888 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-05 22:22:51 +00:00
Jean-Philippe Lang
8509cf80f0 ProjectsController#list_issues, #export_issues_csv and #export_issues_pdf merged into IssuesController#index
git-svn-id: http://redmine.rubyforge.org/svn/trunk@887 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-05 18:38:42 +00:00
Jean-Philippe Lang
26a1ae4808 Fixed: <td> closed with a </th>
git-svn-id: http://redmine.rubyforge.org/svn/trunk@886 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-05 17:33:28 +00:00
Jean-Philippe Lang
040cfa6ae2 Fixed: <<me>> doesn't appear in the drop down filters on a project issue list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@885 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-05 17:30:45 +00:00
597 changed files with 32541 additions and 5915 deletions

View File

@@ -21,7 +21,7 @@ class AccountController < ApplicationController
include CustomFieldsHelper
# prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register]
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
# Show user's account
def show
@@ -30,7 +30,7 @@ class AccountController < ApplicationController
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.role_for_project(membership.project))
membership.project.is_public? || (User.current.member_of?(membership.project))
end
rescue ActiveRecord::RecordNotFound
render_404
@@ -43,7 +43,7 @@ class AccountController < ApplicationController
self.logged_user = nil
else
# Authenticate user
user = User.try_to_login(params[:login], params[:password])
user = User.try_to_login(params[:username], params[:password])
if user
self.logged_user = user
# generate a key and set cookie if autologin
@@ -56,6 +56,8 @@ class AccountController < ApplicationController
flash.now[:error] = l(:notice_account_invalid_creditentials)
end
end
rescue User::OnTheFlyCreationFailure
flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.'
end
# Log out current user and redirect to welcome page
@@ -106,40 +108,62 @@ class AccountController < ApplicationController
# User self-registration
def register
redirect_to(home_url) && return unless Setting.self_registration?
if params[:token]
token = Token.find_by_action_and_value("register", params[:token])
redirect_to(home_url) && return unless token and !token.expired?
user = token.user
redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
user.status = User::STATUS_ACTIVE
if user.save
token.destroy
flash[:notice] = l(:notice_account_activated)
redirect_to :action => 'login'
return
end
if request.get?
@user = User.new(:language => Setting.default_language)
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
else
if request.get?
@user = User.new(:language => Setting.default_language)
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
else
@user = User.new(params[:user])
@user.admin = false
@user.login = params[:user][:login]
@user.status = User::STATUS_REGISTERED
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@user.custom_values = @custom_values
@user = User.new(params[:user])
@user.admin = false
@user.login = params[:user][:login]
@user.status = User::STATUS_REGISTERED
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x,
:customized => @user,
:value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) }
@user.custom_values = @custom_values
case Setting.self_registration
when '1'
# Email activation
token = Token.new(:user => @user, :action => "register")
if @user.save and token.save
Mailer.deliver_register(token)
flash[:notice] = l(:notice_account_register_done)
redirect_to :controller => 'account', :action => 'login'
redirect_to :action => 'login'
end
when '3'
# Automatic activation
@user.status = User::STATUS_ACTIVE
if @user.save
flash[:notice] = l(:notice_account_activated)
redirect_to :action => 'login'
end
else
# Manual activation by the administrator
if @user.save
# Sends an email to the administrators
Mailer.deliver_account_activation_request(@user)
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
end
end
end
end
# Token based account activation
def activate
redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
token = Token.find_by_action_and_value('register', params[:token])
redirect_to(home_url) && return unless token and !token.expired?
user = token.user
redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
user.status = User::STATUS_ACTIVE
if user.save
token.destroy
flash[:notice] = l(:notice_account_activated)
end
redirect_to :action => 'login'
end
private
def logged_user=(user)
if user && user.is_a?(User)

View File

@@ -22,7 +22,8 @@ class AdminController < ApplicationController
helper :sort
include SortHelper
def index
def index
@no_configuration_data = Redmine::DefaultData::Loader::no_data?
end
def projects
@@ -35,7 +36,7 @@ class AdminController < ApplicationController
@project_count = Project.count(:conditions => conditions)
@project_pages = Paginator.new self, @project_count,
25,
per_page_option,
params['page']
@projects = Project.find :all, :order => sort_clause,
:conditions => conditions,
@@ -44,15 +45,19 @@ class AdminController < ApplicationController
render :action => "projects", :layout => false if request.xhr?
end
def mail_options
@notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted)
# Loads the default configuration
# (roles, trackers, statuses, workflow, enumerations)
def default_configuration
if request.post?
Setting.notified_events = (params[:notified_events] || [])
Setting.emails_footer = params[:emails_footer] if params[:emails_footer]
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'admin', :action => 'mail_options'
begin
Redmine::DefaultData::Loader::load(params[:lang])
flash[:notice] = l(:notice_default_data_loaded)
rescue Exception => e
flash[:error] = l(:error_can_t_load_default_data, e.message)
end
end
redirect_to :action => 'index'
end
def test_email
@@ -66,7 +71,7 @@ class AdminController < ApplicationController
flash[:error] = l(:notice_email_error, e.message)
end
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
redirect_to :action => 'mail_options'
redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
end
def info

View File

@@ -19,31 +19,35 @@ class ApplicationController < ActionController::Base
before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password
include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper
REDMINE_SUPPORTED_SCM.each do |scm|
require_dependency "repository/#{scm.underscore}"
end
def logged_in_user
User.current.logged? ? User.current : nil
end
def current_role
@current_role ||= User.current.role_for_project(@project)
end
def user_setup
# Check the settings cache for each request
Setting.check_cache
# Find the current user
User.current = find_current_user
end
# Returns the current user or nil if no user is logged in
def find_current_user
if session[:user_id]
# existing session
User.current = User.find(session[:user_id])
(User.find_active(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature
User.current = User.find_by_autologin_key(cookies[:autologin])
User.find_by_autologin_key(cookies[:autologin])
elsif params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication
User.current = User.find_by_rss_key(params[:key])
else
User.current = User.anonymous
User.find_by_rss_key(params[:key])
end
end
@@ -55,13 +59,14 @@ class ApplicationController < ActionController::Base
end
def set_localization
User.current.language = nil unless User.current.logged?
lang = begin
if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym
if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
User.current.language
elsif request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first
if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym
accept_lang
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
User.current.language = accept_lang
end
end
rescue
@@ -97,13 +102,17 @@ class ApplicationController < ActionController::Base
# make sure that the user is a member of the project (or admin) if project is private
# used as a before_filter for actions that do not require any particular permission on the project
def check_project_privacy
unless @project.active?
if @project && @project.active?
if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
true
else
User.current.logged? ? render_403 : require_login
end
else
@project = nil
render_404
return false
false
end
return true if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
User.current.logged? ? render_403 : require_login
end
# store current uri in session.
@@ -133,9 +142,15 @@ class ApplicationController < ActionController::Base
return false
end
def render_error(msg)
flash.now[:error] = msg
render :nothing => true, :layout => !request.xhr?, :status => 500
end
def render_feed(items, options={})
@items = items || []
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
@items = @items.slice(0, Setting.feeds_limit.to_i)
@title = options[:title] || Setting.app_title
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end
@@ -148,6 +163,38 @@ class ApplicationController < ActionController::Base
def accept_key_auth_actions
self.class.read_inheritable_attribute('accept_key_auth_actions') || []
end
# TODO: move to model
def attach_files(obj, attachments)
attached = []
if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
next unless file && file.size > 0
a = Attachment.create(:container => obj,
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
attached << a unless a.new_record?
end
end
attached
end
# Returns the number of objects that should be displayed
# on the paginated list
def per_page_option
per_page = nil
if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
per_page = params[:per_page].to_s.to_i
session[:per_page] = per_page
elsif session[:per_page]
per_page = session[:per_page]
else
per_page = Setting.per_page_options_array.first || 25
end
per_page
end
# qvalues http header parser
# code taken from webrick
@@ -167,4 +214,9 @@ class ApplicationController < ActionController::Base
end
return tmp
end
# Returns a string that can be used as filename value in Content-Disposition header
def filename_for_content_disposition(name)
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
end
end

View File

@@ -21,7 +21,7 @@ class AttachmentsController < ApplicationController
def download
# images are sent inline
send_file @attachment.diskfile, :filename => @attachment.filename,
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
rescue

View File

@@ -32,7 +32,6 @@ class BoardsController < ApplicationController
if @boards.size == 1
@board = @boards.first
show
render :action => 'show'
end
end
@@ -41,12 +40,12 @@ class BoardsController < ApplicationController
sort_update
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, 25, params['page']
@topics = @board.topics.find :all, :order => sort_clause,
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}",
:include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset
render :action => 'show', :layout => false if request.xhr?
render :action => 'show', :layout => !request.xhr?
end
verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }

View File

@@ -45,7 +45,7 @@ class CustomFieldsController < ApplicationController
end
if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list', :tab => @custom_field.type
redirect_to :action => 'list', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
end
@@ -57,14 +57,29 @@ class CustomFieldsController < ApplicationController
@custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : []
end
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list', :tab => @custom_field.type
redirect_to :action => 'list', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
end
def move
@custom_field = CustomField.find(params[:id])
case params[:position]
when 'highest'
@custom_field.move_to_top
when 'higher'
@custom_field.move_higher
when 'lower'
@custom_field.move_lower
when 'lowest'
@custom_field.move_to_bottom
end if params[:position]
redirect_to :action => 'list', :tab => @custom_field.class.name
end
def destroy
@custom_field = CustomField.find(params[:id]).destroy
redirect_to :action => 'list', :tab => @custom_field.type
redirect_to :action => 'list', :tab => @custom_field.class.name
rescue
flash[:error] = "Unable to delete custom field"
redirect_to :action => 'list'

View File

@@ -17,12 +17,42 @@
class DocumentsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
before_filter :find_project, :only => [:index, :new]
before_filter :find_document, :except => [:index, :new]
before_filter :authorize
helper :attachments
def index
@sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
documents = @project.documents.find :all, :include => [:attachments, :category]
case @sort_by
when 'date'
@grouped = documents.group_by {|d| d.created_on.to_date }
when 'title'
@grouped = documents.group_by {|d| d.title.first.upcase}
when 'author'
@grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
else
@grouped = documents.group_by(&:category)
end
render :layout => false if request.xhr?
end
def show
@attachments = @document.attachments.find(:all, :order => "created_on DESC")
end
def new
@document = @project.documents.build(params[:document])
if request.post? and @document.save
attach_files(@document, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
redirect_to :action => 'index', :project_id => @project
end
end
def edit
@categories = Enumeration::get_values('DCAT')
if request.post? and @document.update_attributes(params[:document])
@@ -33,26 +63,21 @@ class DocumentsController < ApplicationController
def destroy
@document.destroy
redirect_to :controller => 'projects', :action => 'list_documents', :id => @project
redirect_to :controller => 'documents', :action => 'index', :project_id => @project
end
def download
@attachment = @document.attachments.find(params[:attachment_id])
@attachment.increment_download
send_file @attachment.diskfile, :filename => @attachment.filename
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type
rescue
render_404
end
def add_attachment
# Save the attachments
@attachments = []
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @document, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('document_added')
attachments = attach_files(@document, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
redirect_to :action => 'show', :id => @document
end
@@ -63,9 +88,15 @@ class DocumentsController < ApplicationController
private
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_document
@document = Document.find(params[:id])
@project = @document.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
end

View File

@@ -1,98 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2007 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 FeedsController < ApplicationController
before_filter :find_scope
session :off
helper :issues
include IssuesHelper
helper :custom_fields
include CustomFieldsHelper
# news feeds
def news
News.with_scope(:find => @find_options) do
@news = News.find :all, :order => "#{News.table_name}.created_on DESC", :include => [ :author, :project ]
end
headers["Content-Type"] = "application/rss+xml"
render :action => 'news_atom' if 'atom' == params[:format]
end
# issue feeds
def issues
if @project && params[:query_id]
query = Query.find(params[:query_id])
query.executed_by = @user
# ignore query if it's not valid
query = nil unless query.valid?
# override with query conditions
@find_options[:conditions] = query.statement if query.valid? and @project == query.project
end
Issue.with_scope(:find => @find_options) do
@issues = Issue.find :all, :include => [:project, :author, :tracker, :status],
:order => "#{Issue.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (query ? query.name : l(:label_reported_issues))
headers["Content-Type"] = "application/rss+xml"
render :action => 'issues_atom' if 'atom' == params[:format]
end
# issue changes feeds
def history
if @project && params[:query_id]
query = Query.find(params[:query_id])
query.executed_by = @user
# ignore query if it's not valid
query = nil unless query.valid?
# override with query conditions
@find_options[:conditions] = query.statement if query.valid? and @project == query.project
end
Journal.with_scope(:find => @find_options) do
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:order => "#{Journal.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (query ? query.name : l(:label_changes_details))
headers["Content-Type"] = "application/rss+xml"
render :action => 'history_atom' if 'atom' == params[:format]
end
private
# override for feeds specific authentication
def check_if_login_required
@user = User.find_by_rss_key(params[:key])
render(:nothing => true, :status => 403) and return false if !@user && Setting.login_required?
end
def find_scope
if params[:project_id]
# project feed
# check if project is public or if the user is a member
@project = Project.find(params[:project_id])
render(:nothing => true, :status => 403) and return false unless @project.is_public? || (@user && @user.role_for_project(@project))
scope = ["#{Project.table_name}.id=?", params[:project_id].to_i]
else
# global feed
scope = ["#{Project.table_name}.is_public=?", true]
end
@find_options = {:conditions => scope, :limit => Setting.feeds_limit.to_i}
return true
end
end

View File

@@ -17,6 +17,7 @@
class IssueCategoriesController < ApplicationController
layout 'base'
menu_item :settings
before_filter :find_project, :authorize
verify :method => :post, :only => :destroy

View File

@@ -16,12 +16,17 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuesController < ApplicationController
layout 'base', :except => :export_pdf
before_filter :find_project, :authorize, :except => [:index, :preview]
accept_key_auth :index
layout 'base'
menu_item :new_issue, :only => :new
cache_sweeper :issue_sweeper, :only => [ :edit, :change_status, :destroy ]
before_filter :find_issue, :only => [:show, :edit, :destroy_attachment]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes]
accept_key_auth :index, :changes
helper :journals
helper :projects
include ProjectsHelper
helper :custom_fields
@@ -37,128 +42,271 @@ class IssuesController < ApplicationController
helper :queries
helper :sort
include SortHelper
include IssuesHelper
def index
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
if @query.valid?
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, 25, params['page']
limit = per_page_option
respond_to do |format|
format.html { }
format.atom { }
format.csv { limit = Setting.issues_export_limit.to_i }
format.pdf { limit = Setting.issues_export_limit.to_i }
end
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, limit, params['page']
@issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :status, :tracker, :project, :priority, :category ],
:include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
:conditions => @query.statement,
:limit => @issue_pages.items_per_page,
:offset => @issue_pages.current.offset
:limit => limit,
:offset => @issue_pages.current.offset
respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
end
else
# Send html if the query is not valid
render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
end
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
rescue ActiveRecord::RecordNotFound
render_404
end
def changes
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
if @query.valid?
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:conditions => @query.statement,
:limit => 25,
:order => "#{Journal.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def show
@custom_values = @issue.custom_values.find(:all, :include => :custom_field)
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
if params[:format]=='pdf'
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "#{@project.identifier}-#{@issue.id}.pdf"
render :template => 'issues/show.rfpdf', :layout => false
else
@status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
render :template => 'issues/show.rhtml'
@journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@activities = Enumeration::get_values('ACTI')
@priorities = Enumeration::get_values('IPRI')
@time_entry = TimeEntry.new
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
end
def edit
# Add a new issue
# The new issue will be created from an existing one if copy_from parameter is given
def new
@issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
@issue.project = @project
@issue.author = User.current
@issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
if @issue.tracker.nil?
flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
render :nothing => true, :layout => true
return
end
default_status = IssueStatus.default
unless default_status
flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
render :nothing => true, :layout => true
return
end
@issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
if request.get? || request.xhr?
@issue.start_date ||= Date.today
@custom_values = @issue.custom_values.empty? ?
@project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
@issue.custom_values
else
requested_status = (params[:issue] && params[:issue][:status_id] ? IssueStatus.find_by_id(params[:issue][:status_id]) : default_status)
# Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x,
:customized => @issue,
:value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
@issue.custom_values = @custom_values
if @issue.save
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
redirect_to :controller => 'issues', :action => 'show', :id => @issue
return
end
end
@priorities = Enumeration::get_values('IPRI')
render :layout => !request.xhr?
end
# Attributes that can be updated on workflow transition (without :edit permission)
# TODO: make it configurable (at least per role)
UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
def edit
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@activities = Enumeration::get_values('ACTI')
@priorities = Enumeration::get_values('IPRI')
@custom_values = []
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@notes = params[:notes]
journal = @issue.init_journal(User.current, @notes)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
attrs = params[:issue].dup
attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.attributes = attrs
end
if request.get?
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
else
begin
@issue.init_journal(self.logged_in_user)
# Retrieve custom fields and values
if params["custom_fields"]
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
@issue.custom_values = @custom_values
end
@issue.attributes = params[:issue]
if @issue.save
flash[:notice] = l(:notice_successful_update)
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
# Update custom fields if user has :edit permission
if @edit_allowed && params[:custom_fields]
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
@issue.custom_values = @custom_values
end
end
end
def add_note
journal = @issue.init_journal(User.current, params[:notes])
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:value => a.filename) unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
if journal.save
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
redirect_to :action => 'show', :id => @issue
return
end
show
end
def change_status
@status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
@new_status = IssueStatus.find(params[:new_status_id])
if params[:confirm]
begin
journal = @issue.init_journal(self.logged_in_user, params[:notes])
@issue.status = @new_status
if @issue.update_attributes(params[:issue])
# Save attachments
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:value => a.filename) unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
# Log time
if current_role.allowed_to?(:log_time)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
@time_entry.save
end
@time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
if @issue.save
# Log spend time
if current_role.allowed_to?(:log_time)
@time_entry.save
end
if !journal.new_record?
# Only send notification if something was actually changed
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
redirect_to :action => 'show', :id => @issue
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end
end
@assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user }
@activities = Enumeration::get_values('ACTI')
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict)
end
# Bulk edit a set of issues
def bulk_edit
if request.post?
status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
unsaved_issue_ids = []
@issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category || params[:category_id] == 'none'
issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
# Don't save any change to the issue if the user is not authorized to apply the requested status
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
# Send notification for each issue (if changed)
Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
else
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
end
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
end
def move
@allowed_projects = []
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
else
User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
end
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
@issues.each do |issue|
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
render :layout => false if request.xhr?
end
def destroy
@issue.destroy
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project
@hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
if @hours > 0
case params[:todo]
when 'destroy'
# nothing to do
when 'nullify'
TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
when 'reassign'
reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
if reassign_to.nil?
flash.now[:error] = l(:error_issue_not_found_in_project)
return
else
TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
end
else
# display the destroy form
return
end
end
@issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project
end
def destroy_attachment
a = @issue.attachments.find(params[:attachment_id])
a.destroy
journal = @issue.init_journal(self.logged_in_user)
journal = @issue.init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:old_value => a.filename)
@@ -167,51 +315,107 @@ class IssuesController < ApplicationController
end
def context_menu
@issues = Issue.find_all_by_id(params[:ids], :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@assignables = @issue.assignable_users
@assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
@priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@allowed_statuses = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
@assignables = @issue.assignable_users
@assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@can = {:edit => User.current.allowed_to?(:edit_issues, @project),
:change_status => User.current.allowed_to?(:change_issue_status, @project),
:add => User.current.allowed_to?(:add_issues, @project),
:move => User.current.allowed_to?(:move_issues, @project),
:delete => User.current.allowed_to?(:delete_issues, @project)}
@back = request.env['HTTP_REFERER']
render :layout => false
end
def update_form
@issue = Issue.new(params[:issue])
render :action => :new, :layout => false
end
def preview
issue = Issue.find_by_id(params[:id])
@attachements = issue.attachments if issue
@text = params[:issue][:description]
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
@attachements = @issue.attachments if @issue
@text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
render :partial => 'common/preview'
end
private
def find_project
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
# Filter for bulk operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
projects = @issues.collect(&:project).compact.uniq
if projects.size == 1
@project = projects.first
else
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
end
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
return true unless params[:project_id]
@project = Project.find(params[:project_id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
# Retrieve query from session or build a new query
def retrieve_query
if params[:set_filter] or !session[:query] or session[:query].project_id
# Give it a name, required to be valid
@query = Query.new(:name => "_", :executed_by => logged_in_user)
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
session[:query] = @query
if !params[:query_id].blank?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = Query.find(params[:query_id], :conditions => cond)
@query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
else
@query = session[:query]
if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
@query.project = @project
end
end
end
end

View File

@@ -0,0 +1,41 @@
# redMine - project management software
# Copyright (C) 2006-2008 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 JournalsController < ApplicationController
layout 'base'
before_filter :find_journal
def edit
if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
end
end
private
def find_journal
@journal = Journal.find(params[:id])
render_403 and return false unless @journal.editable_by?(User.current)
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -17,44 +17,91 @@
class MessagesController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
menu_item :boards
before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => :preview
verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
helper :attachments
include AttachmentsHelper
# Show a topic and its replies
def show
@replies = @topic.children
@replies.reverse! if User.current.wants_comments_in_reverse_order?
@reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr?
end
# Create a new topic
def new
@message = Message.new(params[:message])
@message.author = logged_in_user
@message.board = @board
@message.author = User.current
@message.board = @board
if params[:message] && User.current.allowed_to?(:edit_messages, @project)
@message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky']
end
if request.post? && @message.save
params[:attachments].each { |file|
next unless file.size > 0
Attachment.create(:container => @message, :file => file, :author => logged_in_user)
} if params[:attachments] and params[:attachments].is_a? Array
attach_files(@message, params[:attachments])
redirect_to :action => 'show', :id => @message
end
end
# Reply to a topic
def reply
@reply = Message.new(params[:reply])
@reply.author = logged_in_user
@reply.author = User.current
@reply.board = @board
@message.children << @reply
redirect_to :action => 'show', :id => @message
@topic.children << @reply
if !@reply.new_record?
attach_files(@reply, params[:attachments])
end
redirect_to :action => 'show', :id => @topic
end
# Edit a message
def edit
if params[:message] && User.current.allowed_to?(:edit_messages, @project)
@message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky']
end
if request.post? && @message.update_attributes(params[:message])
attach_files(@message, params[:attachments])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @topic
end
end
# Delete a messages
def destroy
@message.destroy
redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
{ :action => 'show', :id => @message.parent }
end
def preview
message = @board.messages.find_by_id(params[:id])
@attachements = message.attachments if message
@text = (params[:message] || params[:reply])[:content]
render :partial => 'common/preview'
end
private
def find_project
def find_message
find_board
@message = @board.messages.find(params[:id], :include => :parent)
@topic = @message.root
rescue ActiveRecord::RecordNotFound
render_404
end
def find_board
@board = Board.find(params[:board_id], :include => :project)
@project = @board.project
@message = @board.topics.find(params[:id]) if params[:id]
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -26,7 +26,8 @@ class MyController < ApplicationController
'issueswatched' => :label_watched_issues,
'news' => :label_news_latest,
'calendar' => :label_calendar,
'documents' => :label_document_plural
'documents' => :label_document_plural,
'timelog' => :label_spent_time
}.freeze
DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
@@ -44,7 +45,7 @@ class MyController < ApplicationController
# Show user's page
def page
@user = self.logged_in_user
@user = User.current
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
end
@@ -56,6 +57,7 @@ class MyController < ApplicationController
@user.attributes = params[:user]
@user.mail_notification = (params[:notification_option] == 'all')
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
@@ -75,7 +77,7 @@ class MyController < ApplicationController
# Manage user's password
def password
@user = self.logged_in_user
@user = User.current
flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id
if request.post?
if @user.check_password?(params[:password])
@@ -101,7 +103,7 @@ class MyController < ApplicationController
# User's page layout configuration
def page_layout
@user = self.logged_in_user
@user = User.current
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
session[:page_layout] = @blocks
%w(top left right).each {|f| session[:page_layout][f] ||= [] }
@@ -115,7 +117,7 @@ class MyController < ApplicationController
def add_block
block = params[:block]
render(:nothing => true) and return unless block && (BLOCKS.keys.include? block)
@user = self.logged_in_user
@user = User.current
# remove if already present in a group
%w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
# add it on top
@@ -150,7 +152,7 @@ class MyController < ApplicationController
# Save user's page layout
def page_layout_save
@user = self.logged_in_user
@user = User.current
@user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout]
@user.pref.save
session[:page_layout] = nil

View File

@@ -17,11 +17,41 @@
class NewsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_project, :only => :new
before_filter :authorize, :except => [:index, :preview]
before_filter :find_optional_project, :only => :index
accept_key_auth :index
def index
@news_pages, @newss = paginate :news,
:per_page => 10,
:conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
:include => [:author, :project],
:order => "#{News.table_name}.created_on DESC"
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
end
end
def show
@comments = @news.comments
@comments.reverse! if User.current.wants_comments_in_reverse_order?
end
def new
@news = News.new(:project => @project, :author => User.current)
if request.post?
@news.attributes = params[:news]
if @news.save
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
redirect_to :controller => 'news', :action => 'index', :project_id => @project
end
end
end
def edit
if request.post? and @news.update_attributes(params[:news])
flash[:notice] = l(:notice_successful_update)
@@ -31,7 +61,7 @@ class NewsController < ApplicationController
def add_comment
@comment = Comment.new(params[:comment])
@comment.author = logged_in_user
@comment.author = User.current
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news
@@ -47,14 +77,33 @@ class NewsController < ApplicationController
def destroy
@news.destroy
redirect_to :controller => 'projects', :action => 'list_news', :id => @project
redirect_to :action => 'index', :project_id => @project
end
def preview
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
end
private
def find_project
def find_news
@news = News.find(params[:id])
@project = @news.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
return true unless params[:project_id]
@project = Project.find(params[:project_id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -15,19 +15,21 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'csv'
class ProjectsController < ApplicationController
layout 'base'
before_filter :find_project, :except => [ :index, :list, :add ]
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
menu_item :overview
menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings
menu_item :issues, :only => [:changelog]
before_filter :find_project, :except => [ :index, :list, :add, :activity ]
before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
accept_key_auth :activity, :calendar
cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
cache_sweeper :issue_sweeper, :only => [ :add_issue ]
cache_sweeper :version_sweeper, :only => [ :add_version ]
helper :sort
include SortHelper
helper :custom_fields
@@ -50,7 +52,7 @@ class ProjectsController < ApplicationController
# Lists visible projects
def list
projects = Project.find :all,
:conditions => Project.visible_by(logged_in_user),
:conditions => Project.visible_by(User.current),
:include => :parent
@project_tree = projects.group_by {|p| p.parent || p}
@project_tree.each_key {|p| @project_tree[p] -= [p]}
@@ -58,18 +60,23 @@ class ProjectsController < ApplicationController
# Add a new project
def add
@custom_fields = IssueCustomField.find(:all)
@root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}")
@custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@root_projects = Project.find(:all,
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
:order => 'name')
@project = Project.new(params[:project])
@project.enabled_module_names = Redmine::AccessControl.available_project_modules
if request.get?
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Redmine::AccessControl.available_project_modules
else
@project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@project.custom_values = @custom_values
@project.enabled_module_names = params[:enabled_modules]
if @project.save
@project.enabled_module_names = params[:enabled_modules]
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
end
@@ -78,23 +85,38 @@ class ProjectsController < ApplicationController
# Show @project
def show
@custom_values = @project.custom_values.find(:all, :include => :custom_field)
@custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
@subprojects = @project.active_children
@subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = Tracker.find(:all, :order => 'position')
@open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
@total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
@total_hours = @project.time_entries.sum(:hours)
@trackers = @project.rolled_up_trackers
cond = @project.project_condition(Setting.display_subprojects_issues?)
Issue.visible_by(User.current) do
@open_issues_by_tracker = Issue.count(:group => :tracker,
:include => [:project, :status, :tracker],
:conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
@total_issues_by_tracker = Issue.count(:group => :tracker,
:include => [:project, :status, :tracker],
:conditions => cond)
end
TimeEntry.visible_by(User.current) do
@total_hours = TimeEntry.sum(:hours,
:include => :project,
:conditions => cond).to_f
end
@key = User.current.rss_key
end
def settings
@root_projects = Project::find(:all, :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id])
@root_projects = Project.find(:all,
:conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
:order => 'name')
@custom_fields = IssueCustomField.find(:all)
@issue_category ||= IssueCategory.new
@member ||= @project.members.new
@custom_values ||= ProjectCustomField.find(:all).collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
@trackers = Tracker.all
@custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
@repository ||= @project.repository
@wiki ||= @project.wiki
end
@@ -102,9 +124,8 @@ class ProjectsController < ApplicationController
# Edit @project
def edit
if request.post?
@project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
if params[:custom_fields]
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
@project.custom_values = @custom_values
end
@project.attributes = params[:project]
@@ -172,368 +193,98 @@ class ProjectsController < ApplicationController
end
end
# Add a new document to @project
def add_document
@document = @project.documents.build(params[:document])
if request.post? and @document.save
# Save the attachments
params[:attachments].each { |a|
Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0
} if params[:attachments] and params[:attachments].is_a? Array
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
redirect_to :action => 'list_documents', :id => @project
end
end
# Show documents list of @project
def list_documents
@sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
documents = @project.documents.find :all, :include => [:attachments, :category]
case @sort_by
when 'date'
@grouped = documents.group_by {|d| d.created_on.to_date }
when 'title'
@grouped = documents.group_by {|d| d.title.first.upcase}
when 'author'
@grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
else
@grouped = documents.group_by(&:category)
end
render :layout => false if request.xhr?
end
# Add a new issue to @project
# The new issue will be created from an existing one if copy_from parameter is given
def add_issue
@issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
@issue.project = @project
@issue.author = User.current
@issue.tracker ||= Tracker.find(params[:tracker_id])
default_status = IssueStatus.default
unless default_status
flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
render :nothing => true, :layout => true
return
end
@issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker))if logged_in_user
if request.get?
@issue.start_date ||= Date.today
@custom_values = @issue.custom_values.empty? ?
@project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
@issue.custom_values
else
requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
# Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
@issue.custom_values = @custom_values
if @issue.save
if params[:attachments] && params[:attachments].is_a?(Array)
# Save attachments
params[:attachments].each {|a| Attachment.create(:container => @issue, :file => a, :author => User.current) unless a.size == 0}
end
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
redirect_to :action => 'list_issues', :id => @project
return
end
end
@priorities = Enumeration::get_values('IPRI')
end
# Show filtered/sorted issues list of @project
def list_issues
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
@results_per_page_options = [ 15, 25, 50, 100 ]
if params[:per_page] and @results_per_page_options.include? params[:per_page].to_i
@results_per_page = params[:per_page].to_i
session[:results_per_page] = @results_per_page
else
@results_per_page = session[:results_per_page] || 25
end
if @query.valid?
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, @results_per_page, params['page']
@issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :status, :tracker, :project, :priority, :category ],
:conditions => @query.statement,
:limit => @issue_pages.items_per_page,
:offset => @issue_pages.current.offset
end
render :layout => false if request.xhr?
end
# Export filtered/sorted issues list to CSV
def export_issues_csv
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
render :action => 'list_issues' and return unless @query.valid?
@issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :author, :status, :tracker, :priority, :project, {:custom_values => :custom_field} ],
:conditions => @query.statement,
:limit => Setting.issues_export_limit.to_i
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#", l(:field_status),
l(:field_project),
l(:field_tracker),
l(:field_priority),
l(:field_subject),
l(:field_assigned_to),
l(:field_author),
l(:field_start_date),
l(:field_due_date),
l(:field_done_ratio),
l(:field_created_on),
l(:field_updated_on)
]
for custom_field in @project.all_custom_fields
headers << custom_field.name
end
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
@issues.each do |issue|
fields = [issue.id, issue.status.name,
issue.project.name,
issue.tracker.name,
issue.priority.name,
issue.subject,
(issue.assigned_to ? issue.assigned_to.name : ""),
issue.author.name,
issue.start_date ? l_date(issue.start_date) : nil,
issue.due_date ? l_date(issue.due_date) : nil,
issue.done_ratio,
l_datetime(issue.created_on),
l_datetime(issue.updated_on)
]
for custom_field in @project.all_custom_fields
fields << (show_value issue.custom_value_for(custom_field))
end
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
end
# Export filtered/sorted issues to PDF
def export_issues_pdf
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
render :action => 'list_issues' and return unless @query.valid?
@issues = Issue.find :all, :order => sort_clause,
:include => [ :author, :status, :tracker, :priority, :project ],
:conditions => @query.statement,
:limit => Setting.issues_export_limit.to_i
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "export.pdf"
render :layout => false
end
# Bulk edit issues
def bulk_edit_issues
if request.post?
status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
issues = @project.issues.find_all_by_id(params[:issue_ids])
unsaved_issue_ids = []
issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category
issue.fixed_version = fixed_version if fixed_version
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
# Don't save any change to the issue if the user is not authorized to apply the requested status
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
# Send notification for each issue (if changed)
Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
else
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
end
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :action => 'list_issues', :id => @project
return
end
if current_role && User.current.allowed_to?(:change_issue_status, @project)
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
end
render :update do |page|
page.hide 'query_form'
page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
end
end
def move_issues
@issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
redirect_to :action => 'list_issues', :id => @project and return unless @issues
@projects = []
# find projects to which the user is allowed to move the issue
User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:controller => 'projects', :action => 'move_issues')}
# issue can be moved to any tracker
@trackers = Tracker.find(:all)
if request.post? and params[:new_project_id] and params[:new_tracker_id]
new_project = Project.find_by_id(params[:new_project_id])
new_tracker = Tracker.find_by_id(params[:new_tracker_id])
@issues.each do |i|
if new_project && i.project_id != new_project.id
# issue is moved to another project
i.category = nil
i.fixed_version = nil
# delete issue relations
i.relations_from.clear
i.relations_to.clear
i.project = new_project
end
if new_tracker
i.tracker = new_tracker
end
i.save
end
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list_issues', :id => @project
end
end
# Add a news to @project
def add_news
@news = News.new(:project => @project)
if request.post?
@news.attributes = params[:news]
@news.author_id = self.logged_in_user.id if self.logged_in_user
if @news.save
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
redirect_to :action => 'list_news', :id => @project
end
end
end
# Show news list of @project
def list_news
@news_pages, @newss = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "#{News.table_name}.created_on DESC"
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@newss, :title => "#{@project.name}: #{l(:label_news_plural)}") }
end
end
def add_file
if request.post?
@version = @project.versions.find_by_id(params[:version_id])
# Save the attachments
@attachments = []
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @version, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('file_added')
attachments = attach_files(@version, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
@versions = @project.versions.sort
end
def list_files
@versions = @project.versions.sort
sort_init "#{Attachment.table_name}.filename", "asc"
sort_update
@versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
# Show changelog for @project
def changelog
@trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
@trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
end
def roadmap
@trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position')
@trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
@versions = @versions.select {|v| !v.completed? } unless params[:completed]
end
def activity
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date; rescue; end
end
@year ||= Date.today.year
@month ||= Date.today.month
case params[:format]
when 'atom'
# 30 last days
@date_from = Date.today - 30
@date_to = Date.today + 1
else
# current month
@date_from = Date.civil(@year, @month, 1)
@date_to = @date_from >> 1
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@event_types = %w(issues news files documents changesets wiki_pages messages)
if @project
@event_types.delete('wiki_pages') unless @project.wiki
@event_types.delete('changesets') unless @project.repository
@event_types.delete('messages') unless @project.boards.any?
# only show what the user is allowed to view
@event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
end
@event_types = %w(issues news files documents wiki_pages changesets)
@event_types.delete('wiki_pages') unless @project.wiki
@event_types.delete('changesets') unless @project.repository
# only show what the user is allowed to view
@event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
@scope = @event_types.select {|t| params["show_#{t}"]}
# default events if none is specified in parameters
@scope = (@event_types - %w(wiki_pages))if @scope.empty?
@scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
@events = []
if @scope.include?('issues')
@events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions)
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{JournalDetail.table_name}.prop_key = 'status_id' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions)
end
if @scope.include?('news')
@events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions)
end
if @scope.include?('files')
@events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id",
:conditions => cond.conditions)
end
if @scope.include?('documents')
@events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
@events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Document.find(:all, :include => :project, :conditions => cond.conditions)
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id",
:conditions => cond.conditions)
end
if @scope.include?('wiki_pages')
@@ -542,27 +293,36 @@ class ProjectsController < ApplicationController
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
"#{WikiContent.versioned_table_name}.id"
joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
@project.id, @date_from, @date_to]
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"
@events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to])
@events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions)
end
if @scope.include?('changesets')
@events += @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions)
end
if @scope.include?('messages')
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions)
end
@events_by_day = @events.group_by(&:event_date)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
format.atom { render_feed(@events, :title => "#{@project || Setting.app_title}: #{l(:label_activity)}") }
end
end
def calendar
@trackers = Tracker.find(:all, :order => 'position')
@trackers = @project.rolled_up_trackers
retrieve_selected_tracker_ids(@trackers)
if params[:year] and params[:year].to_i > 1900
@@ -574,22 +334,23 @@ class ProjectsController < ApplicationController
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
events = []
@project.issues_with_subprojects(params[:with_subprojects]) do
@project.issues_with_subprojects(@with_subprojects) do
events += Issue.find(:all,
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
) unless @selected_tracker_ids.empty?
events += Version.find(:all, :include => :project,
:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
end
events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
render :layout => false if request.xhr?
end
def gantt
@trackers = Tracker.find(:all, :order => 'position')
@trackers = @project.rolled_up_trackers
retrieve_selected_tracker_ids(@trackers)
if params[:year] and params[:year].to_i >0
@@ -617,16 +378,18 @@ class ProjectsController < ApplicationController
@date_from = Date.civil(@year_from, @month_from, 1)
@date_to = (@date_from >> @months) - 1
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@events = []
@project.issues_with_subprojects(params[:with_subprojects]) do
@project.issues_with_subprojects(@with_subprojects) do
@events += Issue.find(:all,
:order => "start_date, due_date",
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
) unless @selected_tracker_ids.empty?
@events += Version.find(:all, :include => :project,
:conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
end
@events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
@events.sort! {|x,y| x.start_date <=> y.start_date }
if params[:format]=='pdf'
@@ -652,6 +415,14 @@ private
render_404
end
def find_optional_project
return true unless params[:id]
@project = Project.find(params[:id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
def retrieve_selected_tracker_ids(selectable_trackers)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
@@ -659,31 +430,4 @@ private
@selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
end
end
# Retrieve query from session or build a new query
def retrieve_query
if params[:query_id]
@query = @project.queries.find(params[:query_id])
@query.executed_by = logged_in_user
session[:query] = @query
else
if params[:set_filter] or !session[:query] or session[:query].project_id != @project.id
# Give it a name, required to be valid
@query = Query.new(:name => "_", :executed_by => logged_in_user)
@query.project = @project
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
session[:query] = @query
else
@query = session[:query]
end
end
end
end

View File

@@ -17,20 +17,15 @@
class QueriesController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
def index
@queries = @project.queries.find(:all,
:order => "name ASC",
:conditions => ["is_public = ? or user_id = ?", true, (logged_in_user ? logged_in_user.id : 0)])
end
menu_item :issues
before_filter :find_query, :except => :new
before_filter :find_optional_project, :only => :new
def new
@query = Query.new(params[:query])
@query.project = @project
@query.user = logged_in_user
@query.executed_by = logged_in_user
@query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
@query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
params[:fields].each do |field|
@@ -39,7 +34,7 @@ class QueriesController < ApplicationController
if request.post? && params[:confirm] && @query.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project, :query_id => @query
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
return
end
render :layout => false if request.xhr?
@@ -52,31 +47,34 @@ class QueriesController < ApplicationController
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.attributes = params[:query]
@query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
@query.project = nil if params[:query_is_for_all]
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
if @query.save
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project, :query_id => @query
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
end
end
end
def destroy
@query.destroy if request.post?
redirect_to :controller => 'queries', :project_id => @project
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
end
private
def find_project
if params[:id]
@query = Query.find(params[:id])
@query.executed_by = logged_in_user
@project = @query.project
render_403 unless @query.editable_by?(logged_in_user)
else
@project = Project.find(params[:project_id])
end
def find_query
@query = Query.find(params[:id])
@project = @query.project
render_403 unless @query.editable_by?(User.current)
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id]
User.current.allowed_to?(:save_queries, @project, :global => true)
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -17,6 +17,7 @@
class ReportsController < ApplicationController
layout 'base'
menu_item :issues
before_filter :find_project, :authorize
def issue_report
@@ -25,7 +26,7 @@ class ReportsController < ApplicationController
case params[:detail]
when "tracker"
@field = "tracker_id"
@rows = Tracker.find :all, :order => 'position'
@rows = @project.trackers
@data = issues_by_tracker
@report_title = l(:field_tracker)
render :template => "reports/issue_report_details"
@@ -47,6 +48,12 @@ class ReportsController < ApplicationController
@data = issues_by_category
@report_title = l(:field_category)
render :template => "reports/issue_report_details"
when "assigned_to"
@field = "assigned_to_id"
@rows = @project.members.collect { |m| m.user }
@data = issues_by_assigned_to
@report_title = l(:field_assigned_to)
render :template => "reports/issue_report_details"
when "author"
@field = "author_id"
@rows = @project.members.collect { |m| m.user }
@@ -60,16 +67,18 @@ class ReportsController < ApplicationController
@report_title = l(:field_subproject)
render :template => "reports/issue_report_details"
else
@trackers = Tracker.find(:all, :order => 'position')
@trackers = @project.trackers
@versions = @project.versions.sort
@priorities = Enumeration::get_values('IPRI')
@categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }
@authors = @project.members.collect { |m| m.user }
@subprojects = @project.active_children
issues_by_tracker
issues_by_version
issues_by_priority
issues_by_category
issues_by_assigned_to
issues_by_author
issues_by_subproject
@@ -180,7 +189,22 @@ private
and i.project_id=#{@project.id}
group by s.id, s.is_closed, c.id")
end
def issues_by_assigned_to
@issues_by_assigned_to ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
a.id as assigned_to_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
where
i.status_id=s.id
and i.assigned_to_id=a.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, a.id")
end
def issues_by_author
@issues_by_author ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,

View File

@@ -19,8 +19,12 @@ require 'SVG/Graph/Bar'
require 'SVG/Graph/BarHorizontal'
require 'digest/sha1'
class ChangesetNotFound < Exception; end
class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController
layout 'base'
menu_item :repository
before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit
before_filter :authorize
@@ -47,11 +51,13 @@ class RepositoriesController < ApplicationController
def show
# check if new revisions have been committed in the repository
@repository.fetch_changesets if Setting.autofetch_changesets?
# get entries for the browse frame
@entries = @repository.entries('')
# root entries
@entries = @repository.entries('', @rev)
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error and return unless @entries || @changesets.any?
show_error_not_found unless @entries || @changesets.any?
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def browse
@@ -59,20 +65,25 @@ class RepositoriesController < ApplicationController
if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
show_error unless @entries
show_error_not_found and return unless @entries
render :action => 'browse'
end
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def changes
@entry = @repository.scm.entry(@path, @rev)
show_error and return unless @entry
show_error_not_found and return unless @entry
@changesets = @repository.changesets_for_path(@path)
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def revisions
@changeset_count = @repository.changesets.count
@changeset_pages = Paginator.new self, @changeset_count,
25,
per_page_option,
params['page']
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
@@ -85,34 +96,68 @@ class RepositoriesController < ApplicationController
end
def entry
@entry = @repository.scm.entry(@path, @rev)
show_error_not_found and return unless @entry
# If the entry is a dir, show the browser
browse and return if @entry.is_dir?
@content = @repository.scm.cat(@path, @rev)
show_error and return unless @content
if 'raw' == params[:format]
show_error_not_found and return unless @content
if 'raw' == params[:format] || @content.is_binary_data?
# Force the download if it's a binary file
send_data @content, :filename => @path.split('/').last
else
# Prevent empty lines when displaying a file with Windows style eol
@content.gsub!("\r\n", "\n")
end
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def annotate
@annotate = @repository.scm.annotate(@path, @rev)
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def revision
@changeset = @repository.changesets.find_by_revision(@rev)
show_error and return unless @changeset
raise ChangesetNotFound unless @changeset
@changes_count = @changeset.changes.size
@changes_pages = Paginator.new self, @changes_count, 150, params['page']
@changes = @changeset.changes.find(:all,
:limit => @changes_pages.items_per_page,
:offset => @changes_pages.current.offset)
render :action => "revision", :layout => false if request.xhr?
respond_to do |format|
format.html
format.js {render :layout => false}
end
rescue ChangesetNotFound
show_error_not_found
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def diff
@rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
@diff_type = ('sbs' == params[:type]) ? 'sbs' : 'inline'
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
# Save diff type as user preference
if User.current.logged? && @diff_type != User.current.pref[:diff_type]
User.current.pref[:diff_type] = @diff_type
User.current.preference.save
end
@cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
unless read_fragment(@cache_key)
@diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
show_error and return unless @diff
show_error_not_found unless @diff
end
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def stats
@@ -141,20 +186,29 @@ private
render_404
end
REV_PARAM_RE = %r{^[a-f0-9]*$}
def find_repository
@project = Project.find(params[:id])
@repository = @project.repository
render_404 and return false unless @repository
@path = params[:path].join('/') unless params[:path].nil?
@path ||= ''
@rev = params[:rev].to_i if params[:rev]
@rev = params[:rev]
@rev_to = params[:rev_to]
raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
show_error_not_found
end
def show_error
flash.now[:error] = l(:notice_scm_error)
render :nothing => true, :layout => true
def show_error_not_found
render_error l(:error_scm_not_found)
end
def show_error_command_failed(msg)
render_error l(:error_scm_command_failed, msg)
end
def graph_commits_per_month(repository)
@@ -213,6 +267,9 @@ private
commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
# Remove email adress in usernames
fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
graph = SVG::Graph::BarHorizontal.new(
:height => 300,
:width => 500,

View File

@@ -33,12 +33,18 @@ class RolesController < ApplicationController
end
def new
@role = Role.new(params[:role])
# Prefills the form with 'Non member' role permissions
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
if request.post? && @role.save
# workflow copy
if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
@role.workflows.copy(copy_from)
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
@permissions = @role.setable_permissions
@roles = Role.find :all, :order => 'builtin, position'
end
def edit
@@ -52,12 +58,11 @@ class RolesController < ApplicationController
def destroy
@role = Role.find(params[:id])
#unless @role.members.empty?
# flash[:error] = 'Some members have this role. Can\'t delete it.'
#else
@role.destroy
#end
@role.destroy
redirect_to :action => 'list'
rescue
flash[:error] = 'This role is in use and can not be deleted.'
redirect_to :action => 'index'
end
def move
@@ -93,7 +98,7 @@ class RolesController < ApplicationController
end
@roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :include => :workflows, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
end
def report

View File

@@ -17,6 +17,8 @@
class SearchController < ApplicationController
layout 'base'
before_filter :find_optional_project
helper :messages
include MessagesHelper
@@ -31,16 +33,11 @@ class SearchController < ApplicationController
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
# quick jump to an issue
if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(logged_in_user))
if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current))
redirect_to :controller => "issues", :action => "show", :id => $1
return
end
if params[:id]
find_project
return unless check_project_privacy
end
if @project
# only show what the user is allowed to view
@object_types = %w(issues news documents changesets wiki_pages messages)
@@ -52,8 +49,11 @@ class SearchController < ApplicationController
@object_types = @scope = %w(projects)
end
# extract tokens from the question
# eg. hello "bye bye" => ["hello", "bye bye"]
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
# tokens must be at least 3 character long
@tokens = @question.split.uniq.select {|w| w.length > 2 }
@tokens = @tokens.uniq.select {|w| w.length > 2 }
if !@tokens.empty?
# no more than 5 tokens to search for
@@ -87,13 +87,13 @@ class SearchController < ApplicationController
end
else
operator = @all_words ? ' AND ' : ' OR '
Project.with_scope(:find => {:conditions => Project.visible_by(logged_in_user)}) do
@results += Project.find(:all, :limit => limit, :conditions => [ (["(LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'projects'
end
@results += Project.find(:all,
:limit => limit,
:conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort]
) if @scope.include? 'projects'
# if only one project is found, user is redirected to its overview
redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1
end
@question = @tokens.join(" ")
else
@question = ""
end
@@ -101,8 +101,10 @@ class SearchController < ApplicationController
end
private
def find_project
def find_optional_project
return true unless params[:id]
@project = Project.find(params[:id])
check_project_privacy
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -25,10 +25,20 @@ class SettingsController < ApplicationController
end
def edit
if request.post? and params[:settings] and params[:settings].is_a? Hash
params[:settings].each { |name, value| Setting[name] = value }
redirect_to :action => 'edit' and return
@notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted)
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value|
# remove blank values in array settings
value.delete_if {|v| v.blank? } if value.is_a?(Array)
Setting[name] = value
end
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'edit', :tab => params[:tab]
return
end
@options = {}
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
end
def plugin

View File

@@ -16,55 +16,71 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TimelogController < ApplicationController
layout 'base'
layout 'base'
menu_item :issues
before_filter :find_project, :authorize
verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
helper :sort
include SortHelper
helper :issues
include TimelogHelper
helper :custom_fields
include CustomFieldsHelper
def report
@available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:values => @project.versions,
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:values => @project.issue_categories,
:klass => IssueCategory,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:values => @project.users,
:klass => User,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:values => Tracker.find(:all),
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:values => Enumeration::get_values('ACTI'),
:label => :label_activity}
:klass => Enumeration,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
}
# Add list and boolean custom fields as available criterias
@project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
:format => cf.field_format,
:label => cf.name}
end
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
if params[:date_from]
begin; @date_from = params[:date_from].to_date; rescue; end
end
if params[:date_to]
begin; @date_to = params[:date_to].to_date; rescue; end
end
@date_from ||= Date.civil(Date.today.year, 1, 1)
@date_to ||= Date.civil(Date.today.year, Date.today.month+1, 1) - 1
retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id
sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@@ -76,54 +92,101 @@ class TimelogController < ApplicationController
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
end
@periods = []
date_from = @date_from
# 100 columns max
while date_from < @date_to && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = date_from >> 12
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = date_from >> 1
when 'week'
@periods << "#{date_from.year}-#{date_from.cweek}"
date_from = date_from + 7
end
end
render :layout => false if request.xhr?
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
def details
sort_init 'spent_on', 'desc'
sort_update
@entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause)
@total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours }
@owner_id = logged_in_user ? logged_in_user.id : 0
cond = ARCondition.new
cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
send_csv and return if 'csv' == params[:export]
render :action => 'details', :layout => false if request.xhr?
retrieve_date_range
cond << ['spent_on BETWEEN ? AND ?', @from, @to]
TimeEntry.visible_by(User.current) do
respond_to do |format|
format.html {
# Paginate results
@entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
@total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
render :layout => !request.xhr?
}
format.csv {
# Export all entries
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions,
:order => sort_clause)
send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
}
end
end
end
def edit
render_404 and return if @time_entry && @time_entry.user != logged_in_user
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url])
return
end
@activities = Enumeration::get_values('ACTI')
end
def destroy
render_404 and return unless @time_entry
render_403 and return unless @time_entry.editable_by?(User.current)
@time_entry.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :back
rescue RedirectBackError
redirect_to :action => 'details', :project_id => @time_entry.project
end
private
def find_project
@@ -139,34 +202,53 @@ private
render_404
return false
end
rescue ActiveRecord::RecordNotFound
render_404
end
def send_csv
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [l(:field_spent_on),
l(:field_user),
l(:field_activity),
l(:field_issue),
l(:field_hours),
l(:field_comments)
]
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
@entries.each do |entry|
fields = [l_date(entry.spent_on),
entry.user.name,
entry.activity.name,
(entry.issue ? entry.issue.id : nil),
entry.hours,
entry.comments
]
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# Retrieves the date range based on predefined ranges or specific from/to param dates
def retrieve_date_range
@free_period = false
@from, @to = nil, nil
if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
case params[:period].to_s
when 'today'
@from = @to = Date.today
when 'yesterday'
@from = @to = Date.today - 1
when 'current_week'
@from = Date.today - (Date.today.cwday - 1)%7
@to = @from + 6
when 'last_week'
@from = Date.today - 7 - (Date.today.cwday - 1)%7
@to = @from + 6
when '7_days'
@from = Date.today - 7
@to = Date.today
when 'current_month'
@from = Date.civil(Date.today.year, Date.today.month, 1)
@to = (@from >> 1) - 1
when 'last_month'
@from = Date.civil(Date.today.year, Date.today.month, 1) << 1
@to = (@from >> 1) - 1
when '30_days'
@from = Date.today - 30
@to = Date.today
when 'current_year'
@from = Date.civil(Date.today.year, 1, 1)
@to = Date.civil(Date.today.year, 12, 31)
end
elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
@free_period = true
else
# default
end
export.rewind
send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
@to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
end
end

View File

@@ -37,14 +37,12 @@ class TrackersController < ApplicationController
if request.post? and @tracker.save
# workflow copy
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
copy_from.workflows.each do |w|
@tracker.workflows << w.clone
end
@tracker.workflows.copy(copy_from)
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
@trackers = Tracker.find :all
@trackers = Tracker.find :all, :order => 'position'
end
def edit

View File

@@ -33,13 +33,13 @@ class UsersController < ApplicationController
sort_init 'login', 'asc'
sort_update
@status = params[:status] ? params[:status].to_i : 1
conditions = nil
@status = params[:status] ? params[:status].to_i : 1
conditions = "status <> 0"
conditions = ["status=?", @status] unless @status == 0
@user_count = User.count(:conditions => conditions)
@user_pages = Paginator.new self, @user_count,
15,
per_page_option,
params['page']
@users = User.find :all,:order => sort_clause,
:conditions => conditions,
@@ -52,13 +52,13 @@ class UsersController < ApplicationController
def add
if request.get?
@user = User.new(:language => Setting.default_language)
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
else
@user = User.new(params[:user])
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@user.custom_values = @custom_values
if @user.save
Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
@@ -72,18 +72,19 @@ class UsersController < ApplicationController
def edit
@user = User.find(params[:id])
if request.get?
@custom_values = UserCustomField.find(:all).collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
else
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
if params[:custom_fields]
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@user.custom_values = @custom_values
end
if @user.update_attributes(params[:user])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list'
# Give a string to redirect_to otherwise it would use status param as the response code
redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
end
end
@auth_sources = AuthSource.find(:all)
@@ -109,12 +110,4 @@ class UsersController < ApplicationController
end
redirect_to :action => 'edit', :id => @user and return
end
def destroy
User.find(params[:id]).destroy
redirect_to :action => 'list'
rescue
flash[:error] = "Unable to delete user"
redirect_to :action => 'list'
end
end

View File

@@ -17,9 +17,11 @@
class VersionsController < ApplicationController
layout 'base'
menu_item :roadmap
before_filter :find_project, :authorize
cache_sweeper :version_sweeper, :only => [ :edit, :destroy ]
def show
end
def edit
if request.post? and @version.update_attributes(params[:version])
@@ -39,7 +41,8 @@ class VersionsController < ApplicationController
def download
@attachment = @version.attachments.find(params[:attachment_id])
@attachment.increment_download
send_file @attachment.diskfile, :filename => @attachment.filename
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type
rescue
render_404
end
@@ -49,6 +52,13 @@ class VersionsController < ApplicationController
flash[:notice] = l(:notice_successful_delete)
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
def status_by
respond_to do |format|
format.html { render :action => 'show' }
format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
end
end
private
def find_project

View File

@@ -19,7 +19,7 @@ class WelcomeController < ApplicationController
layout 'base'
def index
@news = News.latest logged_in_user
@projects = Project.latest logged_in_user
@news = News.latest User.current
@projects = Project.latest User.current
end
end

View File

@@ -69,7 +69,7 @@ class WikiController < ApplicationController
#@content.text = params[:content][:text]
#@content.comments = params[:content][:comments]
@content.attributes = params[:content]
@content.author = logged_in_user
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
redirect_to :action => 'index', :id => @project, :page => @page.title
@@ -97,7 +97,7 @@ class WikiController < ApplicationController
@page = @wiki.find_page(params[:page])
@version_count = @page.content.versions.count
@version_pages = Paginator.new self, @version_count, 25, params['p']
@version_pages = Paginator.new self, @version_count, per_page_option, params['p']
# don't load text
@versions = @page.content.versions.find :all,
:select => "id, author_id, comments, updated_on, version",
@@ -114,6 +114,11 @@ class WikiController < ApplicationController
render_404 unless @diff
end
def annotate
@page = @wiki.find_page(params[:page])
@annotate = @page.annotate(params[:version])
end
# remove a wiki page and its history
def destroy
@page = @wiki.find_page(params[:page])
@@ -154,11 +159,7 @@ class WikiController < ApplicationController
def add_attachment
@page = @wiki.find_page(params[:page])
# Save the attachments
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @page, :file => file, :author => logged_in_user)
} if params[:attachments] and params[:attachments].is_a? Array
attach_files(@page, params[:attachments])
redirect_to :action => 'index', :page => @page.title
end

View File

@@ -17,13 +17,14 @@
class WikisController < ApplicationController
layout 'base'
menu_item :settings
before_filter :find_project, :authorize
# Create or update a project's wiki
def edit
@wiki = @project.wiki || Wiki.new(:project => @project)
@wiki.attributes = params[:wiki]
@wiki.save if @request.post?
@wiki.save if request.post?
render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
def current_role
@current_role ||= User.current.role_for_project(@project)
@@ -33,11 +34,11 @@ module ApplicationHelper
# Display a link to user's account page
def link_to_user(user)
link_to user.name, :controller => 'account', :action => 'show', :id => user
user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
end
def link_to_issue(issue)
link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
def link_to_issue(issue, options={})
link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
end
def toggle_link(name, id, options={})
@@ -50,7 +51,7 @@ module ApplicationHelper
def show_and_goto_link(name, id, options={})
onclick = "Element.show('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
onclick << "location.href='##{id}-anchor'; "
onclick << "Element.scrollTo('#{id}'); "
onclick << "return false;"
link_to(name, "#", options.merge(:onclick => onclick))
end
@@ -70,20 +71,41 @@ module ApplicationHelper
def format_date(date)
return nil unless date
@date_format ||= (Setting.date_format.to_i == 0 ? l(:general_fmt_date) : "%Y-%m-%d")
# "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
date.strftime(@date_format)
end
def format_time(time)
def format_time(time, include_date = true)
return nil unless time
@date_format_setting ||= Setting.date_format.to_i
time = time.to_time if time.is_a?(String)
@date_format_setting == 0 ? l_datetime(time) : (time.strftime("%Y-%m-%d") + ' ' + l_time(time))
zone = User.current.time_zone
if time.utc?
local = zone ? zone.adjust(time) : time.getlocal
else
local = zone ? zone.adjust(time.getutc) : time
end
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
@time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
end
# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
end
def html_hours(text)
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
end
def authoring(created, author)
time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
l(:label_added_time_by, author.name, time_tag)
l(:label_added_time_by, author || 'Anonymous', time_tag)
end
def l_or_humanize(s)
l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
end
def day_name(day)
@@ -94,71 +116,115 @@ module ApplicationHelper
l(:actionview_datehelper_select_month_names).split(',')[month-1]
end
def pagination_links_full(paginator, options={}, html_options={})
def pagination_links_full(paginator, count=nil, options={})
page_param = options.delete(:page_param) || :page
url_param = params.dup
# don't reuse params if filters are present
url_param.clear if url_param.has_key?(:set_filter)
html = ''
html << link_to_remote(('&#171; ' + l(:label_previous)),
{:update => "content", :url => options.merge(page_param => paginator.current.previous)},
{:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.previous),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
html << (pagination_links_each(paginator, options) do |n|
link_to_remote(n.to_s,
{:url => {:params => options.merge(page_param => n)}, :update => 'content'},
{:href => url_for(:params => options.merge(page_param => n))})
{:url => {:params => url_param.merge(page_param => n)},
:update => 'content',
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => n))})
end || '')
html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
{:update => "content", :url => options.merge(page_param => paginator.current.next)},
{:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.next),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
unless count.nil?
html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
end
html
end
def set_html_title(text)
@html_header_title = text
def per_page_links(selected=nil)
url_param = params.dup
url_param.clear if url_param.has_key?(:set_filter)
links = Setting.per_page_options_array.collect do |n|
n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
{:href => url_for(url_param.merge(:per_page => n))})
end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end
def html_title
title = []
title << @project.name if @project
title << @html_header_title
title << Setting.app_title
title.compact.join(' - ')
def breadcrumb(*args)
content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb')
end
ACCESSKEYS = {:edit => 'e',
:preview => 'r',
:quick_search => 'f',
:search => '4',
}.freeze
def html_title(*args)
if args.empty?
title = []
title << @project.name if @project
title += @html_title if @html_title
title << Setting.app_title
title.compact.join(' - ')
else
@html_title ||= []
@html_title += args
end
end
def accesskey(s)
ACCESSKEYS[s]
Redmine::AccessKeys.key_for s
end
# format text according to system settings
def textilizable(text, options = {})
return "" if text.blank?
# Formats text according to system settings.
# 2 ways to call this method:
# * with a String: textilizable(text, options)
# * with an object and one of its attribute: textilizable(issue, :description, options)
def textilizable(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
case args.size
when 1
obj = nil
text = args.shift
when 2
obj = args.shift
text = obj.send(args.shift).to_s
else
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
only_path = options.delete(:only_path) == false ? false : true
# when using an image link, try to use an attachment, if possible
attachments = options[:attachments]
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments
text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
align = $1
filename = $2
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
style = $1
filename = $6
rf = Regexp.new(filename, Regexp::IGNORECASE)
# search for the picture in attachments
if found = attachments.detect { |att| att.filename =~ rf }
image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
"!#{align}#{image_url}!"
image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
alt = desc.blank? ? nil : "(#{desc})"
"!#{style}#{image_url}#{alt}!"
else
"!#{align}#{filename}!"
"!#{style}#{filename}!"
end
end
end
text = (Setting.text_formatting == 'textile') ?
Redmine::WikiFormatting.to_html(text) : simple_format(auto_link(h(text)))
Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
simple_format(auto_link(h(text)))
# different methods for formatting wiki links
case options[:wiki_links]
@@ -169,13 +235,14 @@ module ApplicationHelper
# used for single-file wiki export
format_wiki_link = Proc.new {|project, title| "##{title}" }
else
format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
end
project = options[:project] || @project
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
# turn wiki links into html links
# example:
# Wiki links
#
# Examples:
# [[mypage]]
# [[mypage|mytext]]
# wiki links can refer other project wikis, using project name or identifier:
@@ -183,47 +250,124 @@ module ApplicationHelper
# [[project:|mytext]]
# [[project:mypage]]
# [[project:mypage|mytext]]
text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m|
text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
link_project = project
page = $1
title = $3
if page =~ /^([^\:]+)\:(.*)$/
link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
page = title || $2
title = $1 if page.blank?
end
if link_project && link_project.wiki
# check if page exists
wiki_page = link_project.wiki.find_page(page)
link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
:class => ('wiki-page' + (wiki_page ? '' : ' new')))
esc, all, page, title = $1, $2, $3, $5
if esc.nil?
if page =~ /^([^\:]+)\:(.*)$/
link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
page = $2
title ||= $1 if page.blank?
end
if link_project && link_project.wiki
# check if page exists
wiki_page = link_project.wiki.find_page(page)
link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
:class => ('wiki-page' + (wiki_page ? '' : ' new')))
else
# project or wiki doesn't exist
title || page
end
else
# project or wiki doesn't exist
title || page
all
end
end
# turn issue and revision ids into links
# example:
# #52 -> <a href="/issues/show/52">#52</a>
# r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (project.id is 6)
text = text.gsub(%r{([\s,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m|
leading, otype, oid = $1, $2, $3
# Redmine links
#
# Examples:
# Issues:
# #52 -> Link to issue #52
# Changesets:
# r52 -> Link to revision 52
# commit:a85130f -> Link to scmid starting with a85130f
# Documents:
# document#17 -> Link to document with id 17
# document:Greetings -> Link to the document with title "Greetings"
# document:"Some document" -> Link to the document with title "Some document"
# Versions:
# version#3 -> Link to version with id 3
# version:1.0.0 -> Link to version named "1.0.0"
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
# Attachments:
# attachment:file.zip -> Link to the attachment of the current object named file.zip
# Source files:
# source:some/file -> Link to the file located at /some/file in the project's repository
# source:some/file@52 -> Link to the file's revision 52
# source:some/file#L120 -> Link to line 120 of the file
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil
if otype == 'r'
if project && (changeset = project.changesets.find_by_revision(oid))
link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset',
:title => truncate(changeset.comments, 100))
end
else
if issue = Issue.find_by_id(oid.to_i, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue',
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
link = content_tag('del', link) if issue.closed?
if esc.nil?
if prefix.nil? && sep == 'r'
if project && (changeset = project.changesets.find_by_revision(oid))
link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, 100))
end
elsif sep == '#'
oid = oid.to_i
case prefix
when nil
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
:class => (issue.closed? ? 'issue closed' : 'issue'),
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
link = content_tag('del', link) if issue.closed?
end
when 'document'
if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
:class => 'document'
end
when 'version'
if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version'
end
end
elsif sep == ':'
# removes the double quotes if any
name = oid.gsub(%r{^"(.*)"$}, "\\1")
case prefix
when 'document'
if project && document = project.documents.find_by_title(name)
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
:class => 'document'
end
when 'version'
if project && version = project.versions.find_by_name(name)
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version'
end
when 'commit'
if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, 100)
end
when 'source', 'export'
if project && project.repository
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
path, rev, anchor = $1, $3, $5
link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :path => path,
:rev => rev,
:anchor => anchor,
:format => (prefix == 'export' ? 'raw' : nil)},
:class => (prefix == 'export' ? 'source download' : 'source')
end
when 'attachment'
if attachments && attachment = attachments.detect {|a| a.filename == name }
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
:class => 'attachment'
end
end
end
end
leading + (link || "#{otype}#{oid}")
leading + (link || "#{prefix}#{sep}#{oid}")
end
text
@@ -276,7 +420,7 @@ module ApplicationHelper
def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) +
GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first }
GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
end
def label_tag_for(name, option_tags = nil, options = {})
@@ -286,16 +430,35 @@ module ApplicationHelper
def labelled_tabular_form_for(name, object, options, &proc)
options[:html] ||= {}
options[:html].store :class, "tabular"
options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
end
def back_url_hidden_field_tag
hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER'])
end
def check_all_links(form_name)
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
" | " +
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
end
def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts[1] = pcts[1] - pcts[0]
pcts << (100 - pcts[1] - pcts[0])
width = options[:width] || '100px;'
legend = options[:legend] || ''
content_tag('table',
content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
), :class => 'progress', :style => "width: #{width};") +
content_tag('p', legend, :class => 'pourcent')
end
def context_menu_link(name, url, options={})
options[:class] ||= ''
if options.delete(:selected)
@@ -313,13 +476,33 @@ module ApplicationHelper
end
def calendar_for(field_id)
include_calendar_headers_tags
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
end
def include_calendar_headers_tags
unless @calendar_headers_tags_included
@calendar_headers_tags_included = true
content_for :header_tags do
javascript_include_tag('calendar/calendar') +
javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
javascript_include_tag('calendar/calendar-setup') +
stylesheet_link_tag('calendar')
end
end
end
def wikitoolbar_for(field_id)
return '' unless Setting.text_formatting == 'textile'
javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
help_link = l(:setting_text_formatting) + ': ' +
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
:onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
javascript_include_tag('jstoolbar/jstoolbar') +
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
end
def content_for(name, content = nil, &block)
@@ -332,38 +515,3 @@ module ApplicationHelper
(@has_content && @has_content[name]) || false
end
end
class TabularFormBuilder < ActionView::Helpers::FormBuilder
include GLoc
def initialize(object_name, object, template, options, proc)
set_language_if_valid options.delete(:lang)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
end
(field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
src = <<-END_SRC
def #{selector}(field, options = {})
return super if options.delete :no_label
label_text = l(options[:label]) if options[:label]
label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
label = @template.content_tag("label", label_text,
:class => (@object && @object.errors[field] ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
label + super
end
END_SRC
class_eval src, __FILE__, __LINE__
end
def select(field, choices, options = {}, html_options = {})
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
label = @template.content_tag("label", label_text,
:class => (@object && @object.errors[field] ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
label + super
end
end

View File

@@ -17,6 +17,13 @@
module CustomFieldsHelper
def custom_fields_tabs
tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
{:name => 'ProjectCustomField', :label => :label_project_plural},
{:name => 'UserCustomField', :label => :label_user_plural}
]
end
# Return custom field html tag corresponding to its format
def custom_field_tag(custom_value)
custom_field = custom_value.custom_field
@@ -28,7 +35,7 @@ module CustomFieldsHelper
text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) +
calendar_for(field_id)
when "text"
text_area 'custom_value', 'value', :name => field_name, :id => field_id, :cols => 60, :rows => 3
text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%'
when "bool"
check_box 'custom_value', 'value', :name => field_name, :id => field_id
when "list"
@@ -62,7 +69,7 @@ module CustomFieldsHelper
return "" unless value && !value.empty?
case field_format
when "date"
begin; l_date(value.to_date); rescue; value end
begin; format_date(value.to_date); rescue; value end
when "bool"
l_YesNo(value == "1")
else

View File

@@ -27,13 +27,18 @@ module IfpdfHelper
def initialize(lang)
super()
set_language_if_valid lang
case current_language
when :ja
case current_language.to_s
when 'ja'
extend(PDF_Japanese)
AddSJISFont()
@font_for_content = 'SJIS'
@font_for_footer = 'SJIS'
when :zh
when 'zh'
extend(PDF_Chinese)
AddGBFont()
@font_for_content = 'GB'
@font_for_footer = 'GB'
when 'zh-tw'
extend(PDF_Chinese)
AddBig5Font()
@font_for_content = 'Big5'

View File

@@ -15,7 +15,10 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'csv'
module IssuesHelper
include ApplicationHelper
def render_issue_tooltip(issue)
@cached_label_start_date ||= l(:field_start_date)
@@ -29,6 +32,19 @@ module IssuesHelper
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end
def sidebar_queries
unless @sidebar_queries
# User can see public queries and his own queries
visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
# Project specific queries and global queries
visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
@sidebar_queries = Query.find(:all,
:order => "name ASC",
:conditions => visible.conditions)
end
@sidebar_queries
end
def show_detail(detail, no_html=false)
case detail.property
@@ -101,4 +117,61 @@ module IssuesHelper
end
end
end
def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#",
l(:field_status),
l(:field_project),
l(:field_tracker),
l(:field_priority),
l(:field_subject),
l(:field_assigned_to),
l(:field_category),
l(:field_fixed_version),
l(:field_author),
l(:field_start_date),
l(:field_due_date),
l(:field_done_ratio),
l(:field_estimated_hours),
l(:field_created_on),
l(:field_updated_on)
]
# Export project custom fields if project is given
# otherwise export custom fields marked as "For all projects"
custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
custom_fields.each {|f| headers << f.name}
# Description in the last column
headers << l(:field_description)
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
issues.each do |issue|
fields = [issue.id,
issue.status.name,
issue.project.name,
issue.tracker.name,
issue.priority.name,
issue.subject,
issue.assigned_to,
issue.category,
issue.fixed_version,
issue.author.name,
format_date(issue.start_date),
format_date(issue.due_date),
issue.done_ratio,
issue.estimated_hours,
format_time(issue.created_on),
format_time(issue.updated_on)
]
custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
fields << issue.description
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
export
end
end

View File

@@ -0,0 +1,37 @@
# redMine - project management software
# Copyright (C) 2006-2008 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.
module JournalsHelper
def render_notes(journal, options={})
content = ''
editable = journal.editable_by?(User.current)
if editable && !journal.notes.blank?
links = []
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal },
:title => l(:button_edit))
content << content_tag('div', links.join(' '), :class => 'contextual')
end
content << textilizable(journal, :notes)
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki'))
end
def link_to_in_place_notes_editor(text, field_id, url, options={})
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
link_to text, '#', options.merge(:onclick => onclick)
end
end

View File

@@ -18,12 +18,19 @@
module ProjectsHelper
def link_to_version(version, options = {})
return '' unless version && version.is_a?(Version)
link_to version.name, {:controller => 'projects',
:action => 'roadmap',
:id => version.project_id,
:completed => (version.completed? ? 1 : nil),
:anchor => version.name
}, options
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end
def format_activity_title(text)
h(truncate_single_line(text, 100))
end
def format_activity_day(date)
date == Date.today ? l(:label_today).titleize : format_date(date)
end
def format_activity_description(text)
h(truncate(text, 250))
end
def project_settings_tabs
@@ -179,21 +186,13 @@ module ProjectsHelper
end
# today red line
if Date.today >= @date_from and Date.today <= @date_to
if Date.today >= date_from and Date.today <= date_to
gc.stroke('red')
x = (Date.today-@date_from+1)*zoom + subject_width
x = (Date.today-date_from+1)*zoom + subject_width
gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
end
gc.draw(imgl)
imgl
end if Object.const_defined?(:Magick)
def new_issue_selector
trackers = Tracker.find(:all, :order => 'position')
# can't use form tag inside helper
content_tag('form',
select_tag('tracker_id', '<option></option>' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"),
:action => url_for(:controller => 'projects', :action => 'add_issue', :id => @project), :method => 'get')
end
end

View File

@@ -22,24 +22,32 @@ module QueriesHelper
end
def column_header(column)
if column.sortable
sort_header_tag(column.sortable, :caption => l("field_#{column.name}"))
else
content_tag('th', l("field_#{column.name}"))
end
column.sortable ? sort_header_tag(column.sortable, :caption => column.caption,
:default_order => column.default_order) :
content_tag('th', column.caption)
end
def column_content(column, issue)
value = issue.send(column.name)
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
elsif column.name == :subject
((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
if column.is_a?(QueryCustomFieldColumn)
cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv)
else
h(value)
value = issue.send(column.name)
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
else
case column.name
when :subject
h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
when :done_ratio
progress_bar(value, :width => '80px')
else
h(value)
end
end
end
end
end

View File

@@ -25,6 +25,10 @@ module RepositoriesHelper
type ? CodeRay.scan(content, type).html : h(content)
end
def format_revision(txt)
txt.to_s[0,8]
end
def to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
@encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
@@ -54,15 +58,21 @@ module RepositoriesHelper
end
def with_leading_slash(path)
path ||= ''
path.starts_with?("/") ? "/#{path}" : path
path.to_s.starts_with?('/') ? path : "/#{path}"
end
def without_leading_slash(path)
path.gsub(%r{^/+}, '')
end
def subversion_field_tags(form, repository)
content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />(http://, https://, svn://, file:///)') +
content_tag('p', form.text_field(:login, :size => 30)) +
content_tag('p', form.password_field(:password, :size => 30))
content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
:onfocus => "this.value=''; this.name='repository[password]';",
:onchange => "this.name='repository[password]';"))
end
def darcs_field_tags(form, repository)
@@ -73,8 +83,16 @@ module RepositoriesHelper
content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
end
def git_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
end
def cvs_field_tags(form, repository)
content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
end
def bazaar_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
end
end

View File

@@ -16,4 +16,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module SettingsHelper
def administration_settings_tabs
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
]
end
end

View File

@@ -83,7 +83,7 @@ module SortHelper
# Use this to sort the controller's table items collection.
#
def sort_clause()
session[@sort_name][:key] + ' ' + session[@sort_name][:order]
session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
end
# Returns a link which sorts by the named column.
@@ -92,7 +92,7 @@ module SortHelper
# - The optional caption explicitly specifies the displayed link text.
# - A sort icon image is positioned to the right of the sort link.
#
def sort_link(column, caption=nil)
def sort_link(column, caption, default_order)
key, order = session[@sort_name][:key], session[@sort_name][:order]
if key == column
if order.downcase == 'asc'
@@ -104,15 +104,17 @@ module SortHelper
end
else
icon = nil
order = 'desc' # changed for desc order by default
order = default_order
end
caption = titleize(Inflector::humanize(column)) unless caption
url = {:sort_key => column, :sort_order => order, :issue_id => params[:issue_id], :project_id => params[:project_id]}
sort_options = { :sort_key => column, :sort_order => order }
# don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
link_to_remote(caption,
{:update => "content", :url => url},
{:href => url_for(url)}) +
{:update => "content", :url => url_options},
{:href => url_for(url_options)}) +
(icon ? nbsp(2) + image_tag(icon) : '')
end
@@ -138,8 +140,9 @@ module SortHelper
#
def sort_header_tag(column, options = {})
caption = options.delete(:caption) || titleize(Inflector::humanize(column))
default_order = options.delete(:default_order) || 'asc'
options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
content_tag('th', sort_link(column, caption), options)
content_tag('th', sort_link(column, caption, default_order), options)
end
private

View File

@@ -17,7 +17,7 @@
module TimelogHelper
def select_hours(data, criteria, value)
data.select {|row| row[criteria] == value.to_s}
data.select {|row| row[criteria] == value}
end
def sum_hours(data)
@@ -27,4 +27,109 @@ module TimelogHelper
end
sum
end
def options_for_period_select(value)
options_for_select([[l(:label_all_time), 'all'],
[l(:label_today), 'today'],
[l(:label_yesterday), 'yesterday'],
[l(:label_this_week), 'current_week'],
[l(:label_last_week), 'last_week'],
[l(:label_last_n_days, 7), '7_days'],
[l(:label_this_month), 'current_month'],
[l(:label_last_month), 'last_month'],
[l(:label_last_n_days, 30), '30_days'],
[l(:label_this_year), 'current_year']],
value)
end
def entries_to_csv(entries)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [l(:field_spent_on),
l(:field_user),
l(:field_activity),
l(:field_project),
l(:field_issue),
l(:field_tracker),
l(:field_subject),
l(:field_hours),
l(:field_comments)
]
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
entries.each do |entry|
fields = [l_date(entry.spent_on),
entry.user,
entry.activity,
entry.project,
(entry.issue ? entry.issue.id : nil),
(entry.issue ? entry.issue.tracker : nil),
(entry.issue ? entry.issue.subject : nil),
entry.hours,
entry.comments
]
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
export
end
def format_criteria_value(criteria, value)
value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
end
def report_to_csv(criterias, periods, hours)
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# Column headers
headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
headers += periods
headers << l(:label_total)
csv << headers.collect {|c| to_utf8(c) }
# Content
report_criteria_to_csv(csv, criterias, periods, hours)
# Total row
row = [ l(:label_total) ] + [''] * (criterias.size - 1)
total = 0
periods.each do |period|
sum = sum_hours(select_hours(hours, @columns, period.to_s))
total += sum
row << (sum > 0 ? "%.2f" % sum : '')
end
row << "%.2f" %total
csv << row
end
export.rewind
export
end
def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
hours_for_value = select_hours(hours, criterias[level], value)
next if hours_for_value.empty?
row = [''] * level
row << to_utf8(format_criteria_value(criterias[level], value))
row += [''] * (criterias.length - level - 1)
total = 0
periods.each do |period|
sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
total += sum
row << (sum > 0 ? "%.2f" % sum : '')
end
row << "%.2f" %total
csv << row
if criterias.length > level + 1
report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
end
end
end
def to_utf8(s)
@ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
begin; @ic.iconv(s.to_s); rescue; s.to_s; end
end
end

View File

@@ -17,9 +17,21 @@
module UsersHelper
def status_options_for_select(selected)
options_for_select([[l(:label_all), "*"],
options_for_select([[l(:label_all), ''],
[l(:status_active), 1],
[l(:status_registered), 2],
[l(:status_locked), 3]], selected)
end
def change_status_link(user)
url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]}
if user.locked?
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user.registered?
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user != User.current
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
end
end
end

View File

@@ -16,4 +16,32 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module VersionsHelper
STATUS_BY_CRITERIAS = %w(category tracker priority author assigned_to)
def render_issue_status_by(version, criteria)
criteria ||= 'category'
raise 'Unknown criteria' unless STATUS_BY_CRITERIAS.include?(criteria)
h = Hash.new {|k,v| k[v] = [0, 0]}
begin
# Total issue count
Issue.count(:group => criteria,
:conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
# Open issues count
Issue.count(:group => criteria,
: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
# When grouping by an association, Rails throws this exception if there's no result (bug)
end
counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
max = counts.collect {|c| c[:total]}.max
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
end
def status_by_options_for_select(value)
options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
end
end

View File

@@ -21,12 +21,11 @@ class Attachment < ActiveRecord::Base
belongs_to :container, :polymorphic => true
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
validates_presence_of :container, :filename
validates_presence_of :container, :filename, :author
validates_length_of :filename, :maximum => 255
validates_length_of :disk_filename, :maximum => 255
acts_as_event :title => :filename,
:description => :filename,
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}}
cattr_accessor :storage_path
@@ -36,75 +35,82 @@ class Attachment < ActiveRecord::Base
errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
end
def file=(incomming_file)
unless incomming_file.nil?
@temp_file = incomming_file
if @temp_file.size > 0
self.filename = sanitize_filename(@temp_file.original_filename)
self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename
self.content_type = @temp_file.content_type.chomp
self.filesize = @temp_file.size
end
end
end
def file=(incoming_file)
unless incoming_file.nil?
@temp_file = incoming_file
if @temp_file.size > 0
self.filename = sanitize_filename(@temp_file.original_filename)
self.disk_filename = Attachment.disk_filename(filename)
self.content_type = @temp_file.content_type.to_s.chomp
self.filesize = @temp_file.size
end
end
end
def file
nil
end
# Copy temp file to its final location
def before_save
if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'")
File.open(diskfile, "wb") do |f|
f.write(@temp_file.read)
end
self.digest = Digest::MD5.hexdigest(File.read(diskfile))
end
# Don't save the content type if it's longer than the authorized length
if self.content_type && self.content_type.length > 255
self.content_type = nil
end
end
# Deletes file on the disk
def after_destroy
if self.filename?
File.delete(diskfile) if File.exist?(diskfile)
end
end
# Returns file's location on disk
def diskfile
"#{@@storage_path}/#{self.disk_filename}"
end
def file
nil
end
# Copy temp file to its final location
def before_save
if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'")
File.open(diskfile, "wb") do |f|
f.write(@temp_file.read)
end
self.digest = Digest::MD5.hexdigest(File.read(diskfile))
end
# Don't save the content type if it's longer than the authorized length
if self.content_type && self.content_type.length > 255
self.content_type = nil
end
end
# Deletes file on the disk
def after_destroy
if self.filename?
File.delete(diskfile) if File.exist?(diskfile)
end
end
# Returns file's location on disk
def diskfile
"#{@@storage_path}/#{self.disk_filename}"
end
def increment_download
increment!(:downloads)
end
# returns last created projects
def self.most_downloaded
find(:all, :limit => 5, :order => "downloads DESC")
end
def project
container.is_a?(Project) ? container : container.project
container.project
end
def image?
self.filename =~ /\.(jpeg|jpg|gif|png)$/i
self.filename =~ /\.(jpe?g|gif|png)$/i
end
private
def sanitize_filename(value)
# get only the filename, not the whole path
just_filename = value.gsub(/^.*(\\|\/)/, '')
# NOTE: File.basename doesn't work right with Windows paths on Unix
# INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
# get only the filename, not the whole path
just_filename = value.gsub(/^.*(\\|\/)/, '')
# NOTE: File.basename doesn't work right with Windows paths on Unix
# INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
# Finally, replace all non alphanumeric, underscore or periods with underscore
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
# Finally, replace all non alphanumeric, hyphens or periods with underscore
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
end
# Returns an ASCII or hashed filename
def self.disk_filename(filename)
df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
df << filename
else
df << Digest::MD5.hexdigest(filename)
# keep the extension if any
df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
end
df
end
end

View File

@@ -20,6 +20,10 @@ class AuthSource < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :host, :maximum => 60
validates_length_of :account_password, :maximum => 60, :allow_nil => true
validates_length_of :account, :base_dn, :maximum => 255
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30
def authenticate(login, password)
end

View File

@@ -27,6 +27,7 @@ class AuthSourceLdap < AuthSource
end
def authenticate(login, password)
return nil if login.blank? || password.blank?
attrs = []
# get user's DN
ldap_con = initialize_ldap_con(self.account, self.account_password)
@@ -69,11 +70,12 @@ class AuthSourceLdap < AuthSource
private
def initialize_ldap_con(ldap_user, ldap_password)
Net::LDAP.new( {:host => self.host,
:port => self.port,
:auth => { :method => :simple, :username => ldap_user, :password => ldap_password },
:encryption => (self.tls ? :simple_tls : nil)}
)
options = { :host => self.host,
:port => self.port,
:encryption => (self.tls ? :simple_tls : nil)
}
options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
Net::LDAP.new options
end
def self.get_attr(entry, attr_name)

View File

@@ -19,4 +19,8 @@ class Change < ActiveRecord::Base
belongs_to :changeset
validates_presence_of :changeset_id, :action, :path
def relative_path
changeset.repository.relative_path(path)
end
end

View File

@@ -32,10 +32,13 @@ class Changeset < ActiveRecord::Base
:date_column => 'committed_on'
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_numericality_of :revision, :only_integer => true
validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s)
end
def comments=(comment)
write_attribute(:comments, comment.strip)
end
@@ -45,9 +48,14 @@ class Changeset < ActiveRecord::Base
super
end
def project
repository.project
end
def after_create
scan_comment_for_issue_ids
end
require 'pp'
def scan_comment_for_issue_ids
return if comments.blank?
@@ -63,6 +71,14 @@ class Changeset < ActiveRecord::Base
return if kw_regexp.blank?
referenced_issues = []
if ref_keywords.delete('*')
# find any issue ID in the comments
target_issue_ids = []
comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
end
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
action = match[0]
target_issue_ids = match[1].scan(/\d+/)
@@ -71,15 +87,45 @@ class Changeset < ActiveRecord::Base
# update status of issues
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
target_issues.each do |issue|
# don't change the status is the issue is already closed
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
next if issue.status.is_closed?
user = committer_user || User.anonymous
csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
end
journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
issue.status = fix_status
issue.done_ratio = done_ratio if done_ratio
issue.save
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
end
referenced_issues += target_issues
end
self.issues = referenced_issues.uniq
end
# Returns the Redmine User corresponding to the committer
def committer_user
if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3
u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank?
u
end
end
# Returns the previous changeset
def previous
@previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
end
# Returns the next changeset
def next
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
end
end

View File

@@ -17,6 +17,7 @@
class CustomField < ActiveRecord::Base
has_many :custom_values, :dependent => :delete_all
acts_as_list :scope => 'type = \'#{self.class}\''
serialize :possible_values
FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
@@ -42,6 +43,9 @@ class CustomField < ActiveRecord::Base
def before_validation
# remove empty values
self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
# make sure these fields are not searchable
self.searchable = false if %w(int float date bool).include?(field_format)
true
end
def validate
@@ -49,8 +53,17 @@ class CustomField < ActiveRecord::Base
errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
end
# validate default value
v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
v.custom_field.is_required = false
errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
end
def <=>(field)
position <=> field.position
end
# to move in project_custom_field
def self.for_all
find(:all, :conditions => ["is_for_all=?", true])

View File

@@ -19,21 +19,32 @@ class CustomValue < ActiveRecord::Base
belongs_to :custom_field
belongs_to :customized, :polymorphic => true
def after_initialize
if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
self.value ||= custom_field.default_value
end
end
protected
def validate
errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank?
errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0
errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
case custom_field.field_format
when 'int'
errors.add(:value, :activerecord_error_not_a_number) unless value.blank? || value =~ /^[+-]?\d+$/
when 'float'
begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
when 'date'
errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.empty?
when 'list'
errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include? value or value.empty?
if value.blank?
errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank?
else
errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
# Format specific validations
case custom_field.field_format
when 'int'
errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/
when 'float'
begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
when 'date'
errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
when 'list'
errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value)
end
end
end
end

View File

@@ -22,6 +22,7 @@ class Document < ActiveRecord::Base
acts_as_searchable :columns => ['title', 'description']
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
validates_presence_of :project, :title, :category

View File

@@ -23,7 +23,6 @@ class Enumeration < ActiveRecord::Base
validates_presence_of :opt, :name
validates_uniqueness_of :name, :scope => [:opt]
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
OPTIONS = {
"IPRI" => :enumeration_issue_priorities,

View File

@@ -27,7 +27,7 @@ class Issue < ActiveRecord::Base
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries, :dependent => :nullify
has_many :time_entries, :dependent => :delete_all
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :custom_fields, :through => :custom_values
has_and_belongs_to_many :changesets, :order => "revision ASC"
@@ -40,7 +40,7 @@ class Issue < ActiveRecord::Base
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
validates_presence_of :subject, :description, :priority, :tracker, :author, :status
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
validates_inclusion_of :done_ratio, :in => 0..100
validates_numericality_of :estimated_hours, :allow_nil => true
@@ -61,11 +61,43 @@ class Issue < ActiveRecord::Base
self
end
# Move an issue to a new project and tracker
def move_to(new_project, new_tracker = nil)
transaction do
if new_project && project_id != new_project.id
# delete issue relations
unless Setting.cross_project_issue_relations?
self.relations_from.clear
self.relations_to.clear
end
# issue is moved to another project
self.category = nil
self.fixed_version = nil
self.project = new_project
end
if new_tracker
self.tracker = new_tracker
end
if save
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
else
rollback_db_transaction
return false
end
end
return true
end
def priority_id=(pid)
self.priority = nil
write_attribute(:priority_id, pid)
end
def estimated_hours=(h)
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end
def validate
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
errors.add :due_date, :activerecord_error_not_a_date
@@ -80,6 +112,10 @@ class Issue < ActiveRecord::Base
end
end
def validate_on_create
errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
end
def before_create
# default assignment based on category
if assigned_to.nil? && category && category.assigned_to
@@ -112,12 +148,17 @@ class Issue < ActiveRecord::Base
end
def after_save
# Reload is needed in order to get the right status
reload
# Update start/due dates of following issues
relations_from.each(&:set_issue_to_dates)
# Close duplicates if the issue was closed
if @issue_before_change && !@issue_before_change.closed? && self.closed?
duplicates.each do |duplicate|
# Reload is need in case the duplicate was updated by a previous duplicate
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
@@ -135,6 +176,7 @@ class Issue < ActiveRecord::Base
def init_journal(user, notes = "")
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
@issue_before_change = self.clone
@issue_before_change.status = self.status
@custom_values_before_change = {}
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
@current_journal
@@ -150,12 +192,19 @@ class Issue < ActiveRecord::Base
project.assignable_users
end
# Returns an array of status that user is able to apply
def new_statuses_allowed_to(user)
statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
statuses << status unless statuses.empty?
statuses.uniq.sort
end
# Returns the mail adresses of users that should be notified for the issue
def recipients
recipients = project.recipients
# Author and assignee are always notified
recipients << author.mail if author
recipients << assigned_to.mail if assigned_to
# Author and assignee are always notified unless they have been locked
recipients << author.mail if author && author.active?
recipients << assigned_to.mail if assigned_to && assigned_to.active?
recipients.compact.uniq
end
@@ -188,4 +237,14 @@ class Issue < ActiveRecord::Base
def soonest_start
@soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
end
def self.visible_by(usr)
with_scope(:find => { :conditions => Project.visible_by(usr) }) do
yield
end
end
def to_s
"#{tracker} ##{id}: #{subject}"
end
end

View File

@@ -35,5 +35,9 @@ class IssueCategory < ActiveRecord::Base
destroy_without_reassign
end
def <=>(category)
name <=> category.name
end
def to_s; name end
end

View File

@@ -56,6 +56,10 @@ class IssueStatus < ActiveRecord::Base
false
end
def <=>(status)
position <=> status.position
end
def to_s; name end
private

View File

@@ -23,14 +23,44 @@ class Journal < ActiveRecord::Base
belongs_to :user
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
attr_accessor :indice
acts_as_searchable :columns => 'notes',
:include => :issue,
:project_key => "#{Issue.table_name}.project_id",
:date_column => "#{Issue.table_name}.created_on"
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
:description => :notes,
:author => :user,
:type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' },
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
def save
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end
# Returns the new status if the journal contains a status change, otherwise nil
def new_status
c = details.detect {|detail| detail.prop_key == 'status_id'}
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
end
def new_value_for(prop)
c = details.detect {|detail| detail.prop_key == prop}
c ? c.value : nil
end
def editable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
end
def project
journalized.respond_to?(:project) ? journalized.project : nil
end
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
end

View File

@@ -16,31 +16,43 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Mailer < ActionMailer::Base
helper ApplicationHelper
helper IssuesHelper
helper CustomFieldsHelper
helper :application
helper :issues
helper :custom_fields
include ActionController::UrlWriter
def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients
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}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
def issue_edit(journal)
issue = journal.journalized
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
s << issue.subject
subject s
body :issue => issue,
:journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
def document_added(document)
redmine_headers 'Project' => document.project.identifier
recipients document.project.recipients
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
body :document => document,
@@ -59,6 +71,7 @@ class Mailer < ActionMailer::Base
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
end
redmine_headers 'Project' => container.project.identifier
recipients container.project.recipients
subject "[#{container.project.name}] #{l(:label_attachment_new)}"
body :attachments => attachments,
@@ -67,6 +80,7 @@ class Mailer < ActionMailer::Base
end
def news_added(news)
redmine_headers 'Project' => news.project.identifier
recipients news.project.recipients
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
@@ -74,6 +88,8 @@ class Mailer < ActionMailer::Base
end
def message_posted(message, recipients)
redmine_headers 'Project' => message.project.identifier,
'Topic-Id' => (message.parent_id || message.id)
recipients(recipients)
subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
body :message => message,
@@ -83,16 +99,24 @@ class Mailer < ActionMailer::Base
def account_information(user, password)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_register)
subject l(:mail_subject_register, Setting.app_title)
body :user => user,
:password => password,
:login_url => url_for(:controller => 'account', :action => 'login')
end
def account_activation_request(user)
# Send the email to all active administrators
recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
subject l(:mail_subject_account_activation_request, Setting.app_title)
body :user => user,
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
end
def lost_password(token)
set_language_if_valid(token.user.language)
recipients token.user.mail
subject l(:mail_subject_lost_password)
subject l(:mail_subject_lost_password, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
end
@@ -100,9 +124,9 @@ class Mailer < ActionMailer::Base
def register(token)
set_language_if_valid(token.user.language)
recipients token.user.mail
subject l(:mail_subject_register)
subject l(:mail_subject_register, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'register', :token => token.value)
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
end
def test(user)
@@ -111,7 +135,16 @@ class Mailer < ActionMailer::Base
subject 'Redmine test'
body :url => url_for(:controller => 'welcome')
end
# Overrides default deliver! method to prevent from sending an email
# with no recipient, cc or bcc
def deliver!(mail = @mail)
return false if (recipients.nil? || recipients.empty?) &&
(cc.nil? || cc.empty?) &&
(bcc.nil? || bcc.empty?)
super
end
private
def initialize_defaults(method_name)
super
@@ -119,12 +152,43 @@ class Mailer < ActionMailer::Base
from Setting.mail_from
default_url_options[:host] = Setting.host_name
default_url_options[:protocol] = Setting.protocol
# Common headers
headers 'X-Mailer' => 'Redmine',
'X-Redmine-Host' => Setting.host_name,
'X-Redmine-Site' => Setting.app_title
end
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
def redmine_headers(h)
h.each { |k,v| headers["X-Redmine-#{k}"] = v }
end
# Overrides the create_mail method
def create_mail
# Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does
if User.current.pref[:no_self_notified]
recipients.delete(User.current.mail) if recipients
cc.delete(User.current.mail) if cc
end
# Blind carbon copy recipients
if Setting.bcc_recipients?
bcc([recipients, cc].flatten.compact.uniq)
recipients []
cc []
end
super
end
# Renders a message with the corresponding layout
def render_message(method_name, body)
layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
body[:content_for_layout] = render(:file => method_name, :body => body)
ActionView::Base.new(File.join(template_root, 'mailer'), body, self).render(:file => layout)
ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}")
end
# Makes partial rendering work with Rails 1.2 (retro-compatibility)
def self.controller_path
''
end unless respond_to?('controller_path')
end

View File

@@ -31,8 +31,12 @@ class Member < ActiveRecord::Base
self.user.name
end
def <=>(member)
role == member.role ? (user <=> member.user) : (role <=> member.role)
end
def before_destroy
# remove category based auto assignments for this member
project.issue_categories.update_all "assigned_to_id = NULL", ["assigned_to_id = ?", self.user.id]
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
end
end

View File

@@ -28,11 +28,18 @@ class Message < ActiveRecord::Base
:date_column => 'created_on'
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content,
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}}
attr_protected :locked, :sticky
validates_presence_of :subject, :content
validates_length_of :subject, :maximum => 255
def validate_on_create
# Can not reply to a locked topic
errors.add_to_base 'Topic is locked' if root.locked? && self != root
end
def after_create
board.update_attribute(:last_message_id, self.id)
board.increment! :messages_count
@@ -43,6 +50,18 @@ class Message < ActiveRecord::Base
end
end
def after_destroy
# The following line is required so that the previous counter
# updates (due to children removal) are not overwritten
board.reload
board.decrement! :messages_count
board.decrement! :topics_count unless parent
end
def sticky?
sticky == 1
end
def project
board.project
end

View File

@@ -18,9 +18,11 @@
class MessageObserver < ActiveRecord::Observer
def after_create(message)
# send notification to the authors of the thread
recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?}
# send notification to the board watchers
recipients += message.board.watcher_recipients
# send notification to project members who want to be notified
recipients += message.board.project.recipients
recipients = recipients.compact.uniq
Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
end

View File

@@ -20,10 +20,11 @@ class Project < ActiveRecord::Base
STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9
has_many :members, :dependent => :delete_all, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :users, :through => :members
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :enabled_modules, :dependent => :delete_all
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
has_many :issue_changes, :through => :issues, :source => :journals
has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
@@ -32,11 +33,17 @@ class Project < ActiveRecord::Base
has_many :documents, :dependent => :destroy
has_many :news, :dependent => :delete_all, :include => :author
has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
has_many :boards, :order => "position ASC"
has_many :boards, :dependent => :destroy, :order => "position ASC"
has_one :repository, :dependent => :destroy
has_many :changesets, :through => :repository
has_one :wiki, :dependent => :destroy
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
# Custom field for the project issues
has_and_belongs_to_many :custom_fields,
:class_name => 'IssueCustomField',
:order => "#{CustomField.table_name}.position",
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
:association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
@@ -45,17 +52,17 @@ class Project < ActiveRecord::Base
attr_protected :status, :enabled_module_names
validates_presence_of :name, :description, :identifier
validates_presence_of :name, :identifier
validates_uniqueness_of :name, :identifier
validates_associated :custom_values, :on => :update
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_length_of :description, :maximum => 255
validates_length_of :homepage, :maximum => 60
validates_length_of :identifier, :in => 3..12
validates_length_of :identifier, :in => 3..20
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
before_destroy :delete_all_members
def identifier=(identifier)
super unless identifier_frozen?
end
@@ -66,16 +73,19 @@ class Project < ActiveRecord::Base
def issues_with_subprojects(include_subprojects=false)
conditions = nil
if include_subprojects && !active_children.empty?
ids = [id] + active_children.collect {|c| c.id}
conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
if include_subprojects
ids = [id] + child_ids
conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
end
conditions ||= ["#{Issue.table_name}.project_id = ?", id]
Issue.with_scope :find => { :conditions => conditions } do
yield
conditions ||= ["#{Project.table_name}.id = ?", id]
# Quick and dirty fix for Rails 2 compatibility
Issue.send(:with_scope, :find => { :conditions => conditions }) do
Version.send(:with_scope, :find => { :conditions => conditions }) do
yield
end
end
end
# returns latest created projects
# non public projects will be returned only if user is a member of those
def self.latest(user=nil, count=5)
@@ -83,6 +93,7 @@ class Project < ActiveRecord::Base
end
def self.visible_by(user=nil)
user ||= User.current
if user && user.admin?
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
elsif user && user.memberships.any?
@@ -92,6 +103,51 @@ class Project < ActiveRecord::Base
end
end
def self.allowed_to_condition(user, permission, options={})
statements = []
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if options[:project]
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
base_statement = "(#{project_statement}) AND (#{base_statement})"
end
if user.admin?
# no restriction
elsif user.logged?
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
elsif Role.anonymous.allowed_to?(permission)
# anonymous user allowed on public project
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
else
# anonymous user is not authorized
statements << "1=0"
end
statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
end
def project_condition(with_subprojects)
cond = "#{Project.table_name}.id = #{id}"
cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
cond
end
def self.find(*args)
if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
project = find_by_identifier(*args)
raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
project
else
super
end
end
def to_param
# id is used for projects with a numeric identifier (compatibility)
@to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
end
def active?
self.status == STATUS_ACTIVE
end
@@ -113,9 +169,23 @@ class Project < ActiveRecord::Base
children.select {|child| child.active?}
end
# Returns an array of the trackers used by the project and its sub projects
def rolled_up_trackers
@rolled_up_trackers ||=
Tracker.find(:all, :include => :projects,
:select => "DISTINCT #{Tracker.table_name}.*",
:conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
:order => "#{Tracker.table_name}.position")
end
# Deletes all project's members
def delete_all_members
Member.delete_all(['project_id = ?', id])
end
# Users issues can be assigned to
def assignable_users
members.select {|m| m.role.assignable?}.collect {|m| m.user}
members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
end
# Returns the mail adresses of users that should be always notified on project events
@@ -134,7 +204,16 @@ class Project < ActiveRecord::Base
end
def <=>(project)
name <=> project.name
name.downcase <=> project.name.downcase
end
def to_s
name
end
# Returns a short description of the projects (first lines)
def short_description(length = 255)
description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
end
def allows_to?(action)
@@ -162,6 +241,7 @@ protected
def validate
errors.add(parent_id, " must be a root project") if parent and parent.parent
errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
end
private

View File

@@ -16,14 +16,36 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueryColumn
attr_accessor :name, :sortable
attr_accessor :name, :sortable, :default_order
include GLoc
def initialize(name, options={})
self.name = name
self.sortable = options[:sortable]
self.default_order = options[:default_order]
end
def default?; default end
def caption
set_language_if_valid(User.current.language)
l("field_#{name}")
end
end
class QueryCustomFieldColumn < QueryColumn
def initialize(custom_field)
self.name = "cf_#{custom_field.id}".to_sym
self.sortable = false
@cf = custom_field
end
def caption
@cf.name
end
def custom_field
@cf
end
end
class Query < ActiveRecord::Base
@@ -32,8 +54,7 @@ class Query < ActiveRecord::Base
serialize :filters
serialize :column_names
attr_protected :project, :user
attr_accessor :executed_by
attr_protected :project_id, :user_id
validates_presence_of :name, :on => :save
validates_length_of :name, :maximum => 255
@@ -62,7 +83,7 @@ class Query < ActiveRecord::Base
@@operators_by_filter_type = { :list => [ "=", "!" ],
:list_status => [ "o", "=", "!", "c", "*" ],
:list_optional => [ "=", "!", "!*", "*" ],
:list_one_or_more => [ "*", "=" ],
:list_subprojects => [ "*", "!*", "=" ],
:date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t", "w" ],
:string => [ "=", "~", "!", "!~" ],
@@ -74,34 +95,37 @@ class Query < ActiveRecord::Base
@@available_columns = [
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position"),
QueryColumn.new(:subject),
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
QueryColumn.new(:author),
QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"),
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on"),
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
]
cattr_reader :available_columns
def initialize(attributes = nil)
super attributes
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
set_language_if_valid(User.current.language)
end
def executed_by=(user)
@executed_by = user
set_language_if_valid(user.language) if user
def after_initialize
# Store the fact that project is nil (used in #editable_by?)
@is_for_all = project.nil?
end
def validate
filters.each_key do |field|
errors.add label_for(field), :activerecord_error_blank unless
# filter requires one or more values
(values_for(field) and !values_for(field).first.empty?) or
(values_for(field) and !values_for(field).first.blank?) or
# filter doesn't require any value
["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
end if filters
@@ -109,15 +133,20 @@ class Query < ActiveRecord::Base
def editable_by?(user)
return false unless user
return true if !is_public && self.user_id == user.id
is_public && user.allowed_to?(:manage_public_queries, project)
# 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)
# 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)
end
def available_filters
return @available_filters if @available_filters
trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"tracker_id" => { :type => :list, :order => 2, :values => Tracker.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI']).collect{|s| [s.name, s.id.to_s] } },
"tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
"priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"subject" => { :type => :text, :order => 8 },
"created_on" => { :type => :date_past, :order => 9 },
"updated_on" => { :type => :date_past, :order => 10 },
@@ -126,12 +155,12 @@ class Query < ActiveRecord::Base
"done_ratio" => { :type => :integer, :order => 13 }}
user_values = []
user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
if project
user_values += project.users.collect{|s| [s.name, s.id.to_s] }
elsif executed_by
user_values << ["<< #{l(:label_me)} >>", "me"] if executed_by
user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
else
# members of the user's projects
user_values += executed_by.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
end
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
@@ -141,12 +170,10 @@ class Query < ActiveRecord::Base
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
unless @project.active_children.empty?
@available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
end
@project.all_custom_fields.select(&:is_filter?).each do |field|
case field.field_format
when "string", "int"
options = { :type => :string, :order => 20 }
when "text"
options = { :type => :text, :order => 20 }
when "list"
@@ -155,6 +182,8 @@ class Query < ActiveRecord::Base
options = { :type => :date, :order => 20 }
when "bool"
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
else
options = { :type => :string, :order => 20 }
end
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
end
@@ -198,12 +227,17 @@ class Query < ActiveRecord::Base
end
def label_for(field)
label = @available_filters[field][:name] if @available_filters.has_key?(field)
label = available_filters[field][:name] if available_filters.has_key?(field)
label ||= field.gsub(/\_id$/, "")
end
def available_columns
cols = Query.available_columns
return @available_columns if @available_columns
@available_columns = Query.available_columns
@available_columns += (project ?
project.all_custom_fields :
IssueCustomField.find(:all, :conditions => {:is_for_all => true})
).collect {|cf| QueryCustomFieldColumn.new(cf) }
end
def columns
@@ -231,20 +265,28 @@ class Query < ActiveRecord::Base
def statement
# project/subprojects clause
clause = ''
if project && has_filter?("subproject_id")
subproject_ids = []
if operator_for("subproject_id") == "="
subproject_ids = values_for("subproject_id").each(&:to_i)
else
subproject_ids = project.active_children.collect{|p| p.id}
project_clauses = []
if project && !@project.active_children.empty?
ids = [project.id]
if has_filter?("subproject_id")
case operator_for("subproject_id")
when '='
# include the selected subprojects
ids += values_for("subproject_id").each(&:to_i)
when '!*'
# main project only
else
# all subprojects
ids += project.child_ids
end
elsif Setting.display_subprojects_issues?
ids += project.child_ids
end
clause << "#{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project
project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
elsif project
clause << "#{Issue.table_name}.project_id=%d" % project.id
else
clause << Project.visible_by(executed_by)
project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
end
project_clauses << Project.visible_by(User.current)
# filters clauses
filters_clauses = []
@@ -253,12 +295,14 @@ class Query < ActiveRecord::Base
v = values_for(field).clone
next unless v and !v.empty?
sql = ''
sql = ''
is_custom_filter = false
if field =~ /^cf_(\d+)$/
# custom field
db_table = CustomValue.table_name
db_field = 'value'
sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND "
is_custom_filter = true
sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
else
# regular field
db_table = Issue.table_name
@@ -268,18 +312,20 @@ class Query < ActiveRecord::Base
# "me" value subsitution
if %w(assigned_to_id author_id).include?(field)
v.push(executed_by ? executed_by.id.to_s : "0") if v.delete("me")
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
end
case operator_for field
when "="
sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!"
sql = sql + "#{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
sql = sql + "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
when "<="
@@ -303,7 +349,12 @@ class Query < ActiveRecord::Base
when "t"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
when "w"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Time.now.at_beginning_of_week), connection.quoted_date(Time.now.next_week.yesterday)]
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
when "!~"
@@ -313,8 +364,6 @@ class Query < ActiveRecord::Base
filters_clauses << sql
end if filters and valid?
clause << ' AND ' unless clause.empty?
clause << filters_clauses.join(' AND ') unless filters_clauses.empty?
clause
(project_clauses + filters_clauses).join(' AND ')
end
end

View File

@@ -17,9 +17,19 @@
class Repository < ActiveRecord::Base
belongs_to :project
has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
has_many :changes, :through => :changesets
# Removes leading and trailing whitespace
def url=(arg)
write_attribute(:url, arg ? arg.to_s.strip : nil)
end
# Removes leading and trailing whitespace
def root_url=(arg)
write_attribute(:root_url, arg ? arg.to_s.strip : nil)
end
def scm
@scm ||= self.scm_adapter.new url, root_url, login, password
update_attribute(:root_url, @scm.root_url) if root_url.blank?
@@ -33,6 +43,10 @@ class Repository < ActiveRecord::Base
def supports_cat?
scm.supports_cat?
end
def supports_annotate?
scm.supports_annotate?
end
def entries(path=nil, identifier=nil)
scm.entries(path, identifier)
@@ -47,7 +61,12 @@ class Repository < ActiveRecord::Base
path = "/#{path}" unless path.starts_with?('/')
Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
:order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
end
# Returns a path relative to the url of the repository
def relative_path(path)
path
end
def latest_changeset
@@ -84,4 +103,13 @@ class Repository < ActiveRecord::Base
rescue
nil
end
private
def before_save
# Strips url and root_url
url.strip!
root_url.strip!
true
end
end

View File

@@ -0,0 +1,86 @@
# redMine - project management software
# Copyright (C) 2006-2007 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.
require 'redmine/scm/adapters/bazaar_adapter'
class Repository::Bazaar < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
Redmine::Scm::Adapters::BazaarAdapter
end
def self.scm_name
'Bazaar'
end
def entries(path=nil, identifier=nil)
entries = scm.entries(path, identifier)
if entries
entries.each do |e|
next if e.lastrev.revision.blank?
c = Change.find(:first,
:include => :changeset,
:conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
:order => "#{Changeset.table_name}.revision DESC")
if c
e.lastrev.identifier = c.changeset.revision
e.lastrev.name = c.changeset.revision
e.lastrev.author = c.changeset.committer
end
end
end
end
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
# latest revision in the repository
scm_revision = scm_info.lastrev.identifier.to_i
if db_revision < scm_revision
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = db_revision + 1
while (identifier_from <= scm_revision)
# loads changesets by batches of 200
identifier_to = [identifier_from + 199, scm_revision].min
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:committer => revision.author,
:committed_on => revision.time,
:scmid => revision.scmid,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:revision => change[:revision])
end
end
end unless revisions.nil?
identifier_from = identifier_to + 1
end
end
end
end
end

View File

@@ -35,7 +35,8 @@ class Repository::Cvs < Repository
end
def entries(path=nil, identifier=nil)
entries=scm.entries(path, identifier)
rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
if entries
entries.each() do |entry|
unless entry.lastrev.nil? || entry.lastrev.identifier
@@ -75,16 +76,14 @@ class Repository::Cvs < Repository
unless revision_to
revision_to=scm.get_previous_revision(revision_from)
end
diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
file_diff = scm.diff(change_from.path, revision_from, revision_to)
diff = diff + file_diff unless file_diff.nil?
end
end
return diff
end
def fetch_changesets
#not the preferred way with CVS. maybe we should introduce always a cron-job for this
last_commit = changesets.maximum(:committed_on)
# some nifty bits to introduce a commit-id with cvs
# natively cvs doesn't provide any kind of changesets, there is only a revision per file.
# we now take a guess using the author, the commitlog and the commit-date.
@@ -94,8 +93,10 @@ class Repository::Cvs < Repository
# we use a small delta here, to merge all changes belonging to _one_ changeset
time_delta=10.seconds
fetch_since = latest_changeset ? latest_changeset.committed_on : nil
transaction do
scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
tmp_rev_num = 1
scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
# only add the change to the database, if it doen't exists. the cvs log
# is not exclusive at all.
unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
@@ -107,18 +108,16 @@ class Repository::Cvs < Repository
})
# create a new changeset....
unless cs
# we use a negative changeset-number here (just for inserting)
unless cs
# we use a temporaray revision number here (just for inserting)
# later on, we calculate a continous positive number
next_rev = changesets.minimum(:revision)
next_rev = 0 if next_rev.nil? or next_rev > 0
next_rev = next_rev - 1
cs=Changeset.create(:repository => self,
:revision => next_rev,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
latest = changesets.find(:first, :order => 'id DESC')
cs = Changeset.create(:repository => self,
:revision => "_#{tmp_rev_num}",
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
tmp_rev_num += 1
end
#convert CVS-File-States to internal Action-abbrevations
@@ -139,12 +138,19 @@ class Repository::Cvs < Repository
end
end
next_rev = [changesets.maximum(:revision) || 0, 0].max
changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset|
next_rev = next_rev + 1
changeset.revision = next_rev
changeset.save!
# Renumber new changesets in chronological order
changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
changeset.update_attribute :revision, next_revision_number
end
end
end # transaction
end
private
# Returns the next revision number to assign to a CVS changeset
def next_revision_number
# Need to retrieve existing revision numbers to sort them as integers
@current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
@current_revision_number += 1
end
end

View File

@@ -29,7 +29,8 @@ class Repository::Darcs < Repository
end
def entries(path=nil, identifier=nil)
entries=scm.entries(path, identifier)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
if entries
entries.each do |entry|
# Search the DB for the entry's last change
@@ -47,18 +48,19 @@ class Repository::Darcs < Repository
def diff(path, rev, rev_to, type)
patch_from = changesets.find_by_revision(rev)
return nil if patch_from.nil?
patch_to = changesets.find_by_revision(rev_to) if rev_to
if path.blank?
path = patch_from.changes.collect{|change| change.path}.join(' ')
end
scm.diff(path, patch_from.scmid, patch_to.scmid, type)
patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil
end
def fetch_changesets
scm_info = scm.info
if scm_info
db_last_id = latest_changeset ? latest_changeset.scmid : nil
next_rev = latest_changeset ? latest_changeset.revision + 1 : 1
next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
# latest revision in the repository
scm_revision = scm_info.lastrev.scmid
unless changesets.find_by_scmid(scm_revision)
@@ -71,9 +73,7 @@ class Repository::Darcs < Repository
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
next if changeset.new_record?
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],

View File

@@ -0,0 +1,70 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
# 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.
require 'redmine/scm/adapters/git_adapter'
class Repository::Git < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
Redmine::Scm::Adapters::GitAdapter
end
def self.scm_name
'Git'
end
def changesets_for_path(path)
Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
end
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision : nil
# latest revision in the repository
scm_revision = scm_info.lastrev.scmid
unless changesets.find_by_scmid(scm_revision)
revisions = scm.revisions('', db_revision, nil)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
end
end
end
end
end
end

View File

@@ -34,6 +34,11 @@ class Repository::Mercurial < Repository
if entries
entries.each do |entry|
next unless entry.is_file?
# Set the filesize unless browsing a specific revision
if identifier.nil?
full_path = File.join(root_url, entry.path)
entry.size = File.stat(full_path).size if File.file?(full_path)
end
# Search the DB for the entry's last change
change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
if change
@@ -51,29 +56,37 @@ class Repository::Mercurial < Repository
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision : nil
db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
# latest revision in the repository
scm_revision = scm_info.lastrev.identifier.to_i
unless changesets.find_by_revision(scm_revision)
revisions = scm.revisions('', db_revision, nil)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
latest_revision = scm_info.lastrev
return if latest_revision.nil?
scm_revision = latest_revision.identifier.to_i
if db_revision < scm_revision
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = db_revision + 1
while (identifier_from <= scm_revision)
# loads changesets by batches of 100
identifier_to = [identifier_from + 99, scm_revision].min
revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true)
transaction do
revisions.each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
end
end
end unless revisions.nil?
identifier_from = identifier_to + 1
end
end
end

View File

@@ -35,11 +35,16 @@ class Repository::Subversion < Repository
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
end
# Returns a path relative to the url of the repository
def relative_path(path)
path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
end
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision : 0
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
# latest revision in the repository
scm_revision = scm_info.lastrev.identifier.to_i
if db_revision < scm_revision
@@ -71,4 +76,14 @@ class Repository::Subversion < Repository
end
end
end
private
# Returns the relative url of the repository
# Eg: root_url = file:///var/svn/foo
# url = file:///var/svn/foo/bar
# => returns /bar
def relative_url
@relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url)}"), '')
end
end

View File

@@ -21,7 +21,18 @@ class Role < ActiveRecord::Base
BUILTIN_ANONYMOUS = 2
before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all
has_many :workflows, :dependent => :delete_all do
def copy(role)
raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
" FROM workflows" +
" WHERE role_id = #{role.id}"
end
end
has_many :members
acts_as_list

View File

@@ -17,6 +17,23 @@
class Setting < ActiveRecord::Base
DATE_FORMATS = [
'%Y-%m-%d',
'%d/%m/%Y',
'%d.%m.%Y',
'%d-%m-%Y',
'%m/%d/%Y',
'%d %b %Y',
'%d %B %Y',
'%b %d, %Y',
'%B %d, %Y'
]
TIME_FORMATS = [
'%H:%M',
'%I:%M %p'
]
cattr_accessor :available_settings
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml"))
Redmine::Plugin.registered_plugins.each do |id, plugin|
@@ -36,12 +53,13 @@ class Setting < ActiveRecord::Base
v = read_attribute(:value)
# Unserialize serialized settings
v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank?
v
end
def value=(v)
v = v.to_yaml if v && @@available_settings[name]['serialized']
write_attribute(:value, v)
write_attribute(:value, v.to_s)
end
# Returns the value of the setting named name
@@ -78,6 +96,11 @@ class Setting < ActiveRecord::Base
class_eval src, __FILE__, __LINE__
end
# Helper that returns an array based on per_page_options setting
def self.per_page_options_array
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
end
# Checks if settings have changed since the values were read
# and clears the cache hash if it's the case
# Called once per request

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Copyright (C) 2006-2008 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
@@ -29,6 +29,14 @@ class TimeEntry < ActiveRecord::Base
validates_numericality_of :hours, :allow_nil => true
validates_length_of :comments, :maximum => 255
def after_initialize
if new_record? && self.activity.nil?
if default_activity = Enumeration.default('ACTI')
self.activity_id = default_activity.id
end
end
end
def before_validation
self.project = issue.project if issue && project.nil?
end
@@ -39,6 +47,10 @@ class TimeEntry < ActiveRecord::Base
errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
end
def hours=(h)
write_attribute :hours, (h.is_a?(String) ? h.to_hours : h)
end
# tyear, tmonth, tweek assigned where setting spent_on attributes
# these attributes make time aggregations easier
def spent_on=(date)
@@ -46,5 +58,16 @@ class TimeEntry < ActiveRecord::Base
self.tyear = spent_on ? spent_on.year : nil
self.tmonth = spent_on ? spent_on.month : nil
self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
end
end
# Returns true if the time entry can be edited by usr, otherwise false
def editable_by?(usr)
(usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
end
def self.visible_by(usr)
with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
yield
end
end
end

View File

@@ -18,7 +18,19 @@
class Tracker < ActiveRecord::Base
before_destroy :check_integrity
has_many :issues
has_many :workflows, :dependent => :delete_all
has_many :workflows, :dependent => :delete_all do
def copy(tracker)
raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker)
raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
" FROM workflows" +
" WHERE tracker_id = #{tracker.id}"
end
end
has_and_belongs_to_many :projects
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
acts_as_list
@@ -29,6 +41,14 @@ class Tracker < ActiveRecord::Base
def to_s; name end
def <=>(tracker)
name <=> tracker.name
end
def self.all
find(:all, :order => 'position')
end
private
def check_integrity
raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])

View File

@@ -18,10 +18,22 @@
require "digest/sha1"
class User < ActiveRecord::Base
class OnTheFlyCreationFailure < Exception; end
# Account statuses
STATUS_ANONYMOUS = 0
STATUS_ACTIVE = 1
STATUS_REGISTERED = 2
STATUS_LOCKED = 3
USER_FORMATS = {
:firstname_lastname => '#{firstname} #{lastname}',
:firstname => '#{firstname}',
:lastname_firstname => '#{lastname} #{firstname}',
:lastname_coma_firstname => '#{lastname}, #{firstname}',
:username => '#{login}'
}
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
has_many :projects, :through => :memberships
@@ -36,17 +48,17 @@ class User < ActiveRecord::Base
# Prevents unauthorized assignments
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
validates_presence_of :login, :firstname, :lastname, :mail
validates_uniqueness_of :login, :mail
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
# Login must contain lettres, numbers, underscores only
validates_format_of :login, :with => /^[a-z0-9_\-@\.]+$/i
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
validates_length_of :login, :maximum => 30
validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i
validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
validates_length_of :firstname, :lastname, :maximum => 30
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
validates_length_of :mail, :maximum => 60
# Password length between 4 and 12
validates_length_of :password, :in => 4..12, :allow_nil => true
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
validates_length_of :mail, :maximum => 60, :allow_nil => true
validates_length_of :password, :minimum => 4, :allow_nil => true
validates_confirmation_of :password, :allow_nil => true
validates_associated :custom_values, :on => :update
@@ -74,6 +86,8 @@ class User < ActiveRecord::Base
# Returns the user that matches provided login and password, or nil
def self.try_to_login(login, password)
# Make sure no one can sign in with an empty password
return nil if password.to_s.empty?
user = find(:first, :conditions => ["login=?", login])
if user
# user is already in local database
@@ -94,20 +108,23 @@ class User < ActiveRecord::Base
onthefly.language = Setting.default_language
if onthefly.save
user = find(:first, :conditions => ["login=?", login])
logger.info("User '#{user.login}' created on the fly.") if logger
logger.info("User '#{user.login}' created from the LDAP") if logger
else
logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger
raise OnTheFlyCreationFailure.new
end
end
end
user.update_attribute(:last_login_on, Time.now) if user
user
rescue => text
raise text
rescue => text
raise text
end
# Return user's full name for display
def name
"#{firstname} #{lastname}"
def name(formatter = nil)
f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
eval '"' + f + '"'
end
def active?
@@ -130,6 +147,14 @@ class User < ActiveRecord::Base
self.preference ||= UserPreference.new(:user => self)
end
def time_zone
self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
end
def wants_comments_in_reverse_order?
self.pref[:comments_sorting] == 'desc'
end
# Return user's RSS key (a 40 chars long string), used to access feeds
def rss_key
token = self.rss_token || Token.create(:user => self, :action => 'feeds')
@@ -159,7 +184,13 @@ class User < ActiveRecord::Base
end
def <=>(user)
lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname
if user.nil?
-1
elsif lastname.to_s.downcase == user.lastname.to_s.downcase
firstname.to_s.downcase <=> user.firstname.to_s.downcase
else
lastname.to_s.downcase <=> user.lastname.to_s.downcase
end
end
def to_s
@@ -174,14 +205,16 @@ class User < ActiveRecord::Base
def role_for_project(project)
# No role on archived projects
return nil unless project && project.active?
# Find project membership
membership = memberships.detect {|m| m.project_id == project.id}
if membership
membership.role
elsif logged?
Role.non_member
if logged?
# Find project membership
membership = memberships.detect {|m| m.project_id == project.id}
if membership
membership.role
else
@role_non_member ||= Role.non_member
end
else
Role.anonymous
@role_anonymous ||= Role.anonymous
end
end
@@ -194,17 +227,26 @@ class User < ActiveRecord::Base
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
def allowed_to?(action, project)
# No action allowed on archived projects
return false unless project.active?
# No action allowed on disabled modules
return false unless project.allows_to?(action)
# Admin users are authorized for anything else
return true if admin?
role = role_for_project(project)
return false unless role
role.allowed_to?(action) && (project.is_public? || role.member?)
def allowed_to?(action, project, options={})
if project
# No action allowed on archived projects
return false unless project.active?
# No action allowed on disabled modules
return false unless project.allows_to?(action)
# Admin users are authorized for anything else
return true if admin?
role = role_for_project(project)
return false unless role
role.allowed_to?(action) && (project.is_public? || role.member?)
elsif options[:global]
# authorize if user has at least one role that has this permission
roles = memberships.collect {|m| m.role}.uniq
roles.detect {|r| r.allowed_to?(action)}
else
false
end
end
def self.current=(user)
@@ -212,11 +254,16 @@ class User < ActiveRecord::Base
end
def self.current
@current_user ||= AnonymousUser.new
@current_user ||= User.anonymous
end
def self.anonymous
AnonymousUser.new
anonymous_user = AnonymousUser.find(:first)
if anonymous_user.nil?
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
end
anonymous_user
end
private
@@ -227,12 +274,17 @@ private
end
class AnonymousUser < User
def logged?
false
def validate_on_create
# There should be only one AnonymousUser in the database
errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
end
# Anonymous user has no RSS key
def rss_key
nil
end
# Overrides a few properties
def logged?; false end
def admin; false end
def name; 'Anonymous' end
def mail; nil end
def time_zone; nil end
def rss_key; nil end
end

View File

@@ -46,4 +46,7 @@ class UserPreference < ActiveRecord::Base
self.others.store attr_name, value
end
end
def comments_sorting; self[:comments_sorting] end
def comments_sorting=(order); self[:comments_sorting]=order end
end

View File

@@ -23,7 +23,7 @@ class Version < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 30
validates_length_of :name, :maximum => 60
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date, :allow_nil => true
def start_date
@@ -34,11 +34,39 @@ class Version < ActiveRecord::Base
effective_date
end
# Returns the total estimated time for this version
def estimated_hours
@estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
end
# Returns the total reported time for this version
def spent_hours
@spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
end
# Returns true if the version is completed: due date reached and no open issues
def completed?
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
end
def completed_pourcent
if fixed_issues.count == 0
0
elsif open_issues_count == 0
100
else
(closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count
end
end
def closed_pourcent
if fixed_issues.count == 0
0
else
closed_issues_count * 100.0 / fixed_issues.count
end
end
# Returns true if the version is overdue: due date reached and some open issues
def overdue?
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
@@ -61,11 +89,11 @@ class Version < ActiveRecord::Base
def to_s; name end
# Versions are sorted by effective_date
# Versions are sorted by effective_date and name
# Those with no effective_date are at the end, sorted by name
def <=>(version)
if self.effective_date
version.effective_date ? (self.effective_date <=> version.effective_date) : -1
version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
else
version.effective_date ? 1 : (self.name <=> version.name)
end

View File

@@ -25,13 +25,14 @@ class WikiContent < ActiveRecord::Base
acts_as_versioned
class Version
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
attr_protected :data
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
:description => :comments,
:datetime => :updated_on,
:type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
def text=(plain)
@@ -60,6 +61,18 @@ class WikiContent < ActiveRecord::Base
data
end
end
def project
page.project
end
# Returns the previous version or nil
def previous
@previous ||= WikiContent::Version.find(:first,
:order => 'version DESC',
:include => :author,
:conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
end
end
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'diff'
require 'enumerator'
class WikiPage < ActiveRecord::Base
belongs_to :wiki
@@ -87,6 +88,12 @@ class WikiPage < ActiveRecord::Base
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
end
def annotate(version=nil)
version = version ? version.to_i : self.content.version
c = content.versions.find_by_version(version)
c ? WikiAnnotate.new(c) : nil
end
def self.pretty_title(str)
(str && str.is_a?(String)) ? str.tr('_', ' ') : str
end
@@ -113,3 +120,41 @@ class WikiDiff
@diff = words_from.diff @words
end
end
class WikiAnnotate
attr_reader :lines, :content
def initialize(content)
@content = content
current = content
current_lines = current.text.split(/\r?\n/)
@lines = current_lines.collect {|t| [nil, nil, t]}
positions = []
current_lines.size.times {|i| positions << i}
while (current.previous)
d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
d.each_slice(3) do |s|
sign, line = s[0], s[1]
if sign == '+' && positions[line] && positions[line] != -1
if @lines[positions[line]][0].nil?
@lines[positions[line]][0] = current.version
@lines[positions[line]][1] = current.author
end
end
end
d.each_slice(3) do |s|
sign, line = s[0], s[1]
if sign == '-'
positions.insert(line, -1)
else
positions[line] = nil
end
end
positions.compact!
# Stop if every line is annotated
break unless @lines.detect { |line| line[0].nil? }
current = current.previous
end
@lines.each { |line| line[0] ||= current.version }
end
end

View File

@@ -2,8 +2,8 @@
<% form_tag({:action=> "login"}) do %>
<table>
<tr>
<td align="right"><label for="login"><%=l(:field_login)%>:</label></td>
<td align="left"><p><%= text_field_tag 'login', nil, :size => 40 %></p></td>
<td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
<td align="left"><p><%= text_field_tag 'username', nil, :size => 40 %></p></td>
</tr>
<tr>
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
@@ -28,6 +28,6 @@
</td>
</tr>
</table>
<%= javascript_tag "Form.Element.focus('login');" %>
<%= javascript_tag "Form.Element.focus('username');" %>
<% end %>
</div>

View File

@@ -6,7 +6,7 @@
<div class="box tabular">
<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
<%= password_field_tag 'new_password', nil, :size => 25 %><br />
<em><%= l(:text_length_between, 4, 12) %></em></p>
<em><%= l(:text_caracters_minimum, 4) %></em></p>
<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>

View File

@@ -9,7 +9,8 @@
<%= text_field 'user', 'login', :size => 25 %></p>
<p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
<%= password_field_tag 'password', nil, :size => 25 %></p>
<%= password_field_tag 'password', nil, :size => 25 %><br />
<em><%= l(:text_caracters_minimum, 4) %></em></p>
<p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
<%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
@@ -29,18 +30,8 @@
<% for @custom_value in @custom_values %>
<p><%= custom_field_tag_with_label @custom_value %></p>
<% end %>
<p><label for="user_mail_notification"><%=l(:field_mail_notification)%></label>
<%= check_box 'user', 'mail_notification' %></p>
<!--[eoform:user]-->
</div>
<%= submit_tag l(:button_submit) %>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag 'calendar/calendar' %>
<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
<%= javascript_include_tag 'calendar/calendar-setup' %>
<%= stylesheet_link_tag 'calendar' %>
<% end %>

View File

@@ -1,7 +1,7 @@
<h2><%=h @user.name %></h2>
<p>
<%= mail_to @user.mail unless @user.pref.hide_mail %>
<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %>
<ul>
<li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
<% for custom_value in @custom_values %>
@@ -16,8 +16,8 @@
<h3><%=l(:label_project_plural)%></h3>
<ul>
<% for membership in @memberships %>
<li><%= link_to membership.project.name, :controller => 'projects', :action => 'show', :id => membership.project %>
(<%= membership.role.name %>, <%= format_date(membership.created_on) %>)</li>
<li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %>
(<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)</li>
<% end %>
</ul>
<% end %>

View File

@@ -0,0 +1,8 @@
<div class="nodata">
<% form_tag({:action => 'default_configuration'}) do %>
<%= simple_format(l(:text_no_configuration_data)) %>
<p><%= l(:field_language) %>:
<%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %>
<%= submit_tag l(:text_load_default_configuration) %></p>
<% end %>
</div>

View File

@@ -1,5 +1,7 @@
<h2><%=l(:label_administration)%></h2>
<%= render :partial => 'no_data' if @no_configuration_data %>
<p class="icon22 icon22-projects">
<%= link_to l(:label_project_plural), :controller => 'admin', :action => 'projects' %> |
<%= link_to l(:label_new), :controller => 'projects', :action => 'add' %>
@@ -28,18 +30,12 @@
<%= link_to l(:label_enumerations), :controller => 'enumerations' %>
</p>
<p class="icon22 icon22-notifications">
<%= link_to l(:field_mail_notification), :controller => 'admin', :action => 'mail_options' %>
</p>
<p class="icon22 icon22-authent">
<%= link_to l(:label_authentication), :controller => 'auth_sources' %>
</p>
<p class="icon22 icon22-settings">
<%= link_to l(:label_settings), :controller => 'settings' %>
</p>
<p class="icon22 icon22-info">
<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %>
</p>
</p>
<% html_title(l(:label_administration)) -%>

View File

@@ -1,25 +1,27 @@
<h2><%=l(:label_information_plural)%></h2>
<p><%=l(:field_version)%>: <strong><%= Redmine::Info.versioned_name %></strong> (<%= @db_adapter_name %>)</p>
<p><strong><%= Redmine::Info.versioned_name %></strong> (<%= @db_adapter_name %>)</p>
<table class="list">
<tr class="odd"><td>Default administrator account changed</td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td>File repository writable</td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="odd"><td>RMagick available</td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="odd"><td><%= l(:text_default_administrator_account_changed) %></td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_file_repository_writable) %></td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="odd"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
</table>
<% if @plugins.any? %>
&nbsp;
<h3 class="icon22 icon22-plugin">Plugins</h3>
<h3 class="icon22 icon22-plugin"><%= l(:label_plugins) %></h3>
<table class="list">
<% @plugins.keys.sort.each do |plugin| %>
<% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %>
<tr class="<%= cycle('odd', 'even') %>">
<td><%=h @plugins[plugin].name %></td>
<td><%=h @plugins[plugin].description %></td>
<td><%=h @plugins[plugin].author %></td>
<td><%=h @plugins[plugin].version %></td>
<td><%= link_to('Configure', :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %></td>
<td><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %></td>
</tr>
<% end %>
</table>
<% end %>
<% html_title(l(:label_information_plural)) -%>

View File

@@ -1,22 +0,0 @@
<div class="contextual">
<%= link_to l(:label_send_test_email), :action => 'test_email' %>
</div>
<h2><%=l(:field_mail_notification)%></h2>
<% form_tag({:action => 'mail_options'}, :id => 'mail-options-form') do %>
<fieldset class="box"><legend><%=l(:text_select_mail_notifications)%></legend>
<% @notifiables.each do |notifiable| %>
<label><%= check_box_tag "notified_events[]", notifiable, Setting.notified_events.include?(notifiable) %>
<%= notifiable.humanize %></label><br />
<% end %>
<p><%= check_all_links('mail-options-form') %></p>
</fieldset>
<fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend>
<%= text_area_tag 'emails_footer', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %>
</fieldset>
<%= submit_tag l(:button_save) %>
<% end %>

View File

@@ -17,19 +17,19 @@
<thead><tr>
<%= sort_header_tag('name', :caption => l(:label_project)) %>
<th><%=l(:field_description)%></th>
<th><%=l(:field_is_public)%></th>
<th><%=l(:label_subproject_plural)%></th>
<%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
<%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %>
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
<th></th>
<th></th>
</tr></thead>
<tbody>
<% for project in @projects %>
<tr class="<%= cycle("odd", "even") %>">
<td><%= project.active? ? link_to(project.name, :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %>
<td><%= textilizable project.description, :project => project %>
<td align="center"><%= image_tag 'true.png' if project.is_public? %>
<td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %>
<td><%= textilizable project.short_description, :project => project %>
<td align="center"><%= project.children.size %>
<td align="center"><%= image_tag 'true.png' if project.is_public? %>
<td align="center"><%= format_date(project.created_on) %>
<td align="center" style="width:10%">
<small>
@@ -45,5 +45,6 @@
</tbody>
</table>
<p><%= pagination_links_full @project_pages, :status => @status %>
[ <%= @project_pages.current.first_item %> - <%= @project_pages.current.last_item %> / <%= @project_count %> ]</p>
<p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p>
<% html_title(l(:label_project_plural)) -%>

View File

@@ -1,4 +1,9 @@
<p id="attachments_p"><label for="attachment_file"><%=l(:label_attachment)%>
<%= image_to_function "add.png", "addFileField();return false" %></label>
<%= file_field_tag 'attachments[]', :size => 30 %> <em>(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)</em></p>
<span id="attachments_fields">
<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%>
<%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %>
<em><%= l(:label_optional_description) %></em>
</span>
<br />
<small><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %>
(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
</small>

View File

@@ -1,12 +1,17 @@
<div class="attachments">
<% for attachment in attachments %>
<p><%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' %>
(<%= number_to_human_size attachment.filesize %>)
<% unless options[:no_author] %>
<span class="author"><%= attachment.author.name %>, <%= format_date(attachment.created_on) %></span>
<% end %>
<p><%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' -%>
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
<% if options[:delete_url] %>
<%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}), :confirm => l(:text_are_you_sure), :method => :post %>
<%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
:confirm => l(:text_are_you_sure),
:method => :post,
:class => 'delete',
:title => l(:button_delete) %>
<% end %>
<% unless options[:no_author] %>
<span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
<% end %>
</p>
<% end %>

View File

@@ -15,7 +15,10 @@
<%= text_field 'auth_source', 'account' %></p>
<p><label for="auth_source_account_password"><%=l(:field_password)%></label>
<%= password_field 'auth_source', 'account_password' %></p>
<%= password_field 'auth_source', 'account_password', :name => 'ignore',
:value => ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('x'*15)),
:onfocus => "this.value=''; this.name='auth_source[account_password]';",
:onchange => "this.name='auth_source[account_password]';" %></p>
<p><label for="auth_source_base_dn"><%=l(:field_base_dn)%> <span class="required">*</span></label>
<%= text_field 'auth_source', 'base_dn', :size => 60 %></p>

View File

@@ -25,4 +25,4 @@
</tbody>
</table>
<%= pagination_links_full @auth_source_pages %>
<p class="pagination"><%= pagination_links_full @auth_source_pages %></p>

View File

@@ -1,6 +1,6 @@
<h2><%= l(:label_board_plural) %></h2>
<table class="list">
<table class="list boards">
<thead><tr>
<th><%= l(:label_board) %></th>
<th><%= l(:label_topic_plural) %></th>
@@ -19,7 +19,7 @@
<td>
<small>
<% if board.last_message %>
<%= board.last_message.author.name %>, <%= format_time(board.last_message.created_on) %><br />
<%= authoring board.last_message.created_on, board.last_message.author %><br />
<%= link_to_message board.last_message %>
<% end %>
</small>
@@ -28,3 +28,15 @@
<% end %>
</tbody>
</table>
<p class="other-formats">
<%= l(:label_export_to) %>
<span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key},
:class => 'feed' %></span>
</p>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %>
<% end %>
<% html_title l(:label_board_plural) %>

View File

@@ -1,3 +1,5 @@
<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %>
<div class="contextual">
<%= link_to_if_authorized l(:label_message_new),
{:controller => 'messages', :action => 'new', :board_id => @board},
@@ -8,17 +10,25 @@
<div id="add-message" style="display:none;">
<h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
<% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true} do |f| %>
<% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
<%= render :partial => 'messages/form', :locals => {:f => f} %>
<p><%= submit_tag l(:button_create) %>
<%= link_to_remote l(:label_preview),
{ :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
:method => 'post',
:update => 'preview',
:with => "Form.serialize('message-form')",
:complete => "Element.scrollTo('preview')"
}, :accesskey => accesskey(:preview) %> |
<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
<% end %>
<div id="preview" class="wiki"></div>
</div>
<h2><%=h @board.name %></h2>
<% if @topics.any? %>
<table class="list">
<table class="list messages">
<thead><tr>
<th><%= l(:field_subject) %></th>
<th><%= l(:field_author) %></th>
@@ -28,25 +38,24 @@
</tr></thead>
<tbody>
<% @topics.each do |topic| %>
<tr class="<%= cycle 'odd', 'even' %>">
<td><%= link_to h(topic.subject), :controller => 'messages', :action => 'show', :board_id => @board, :id => topic %></td>
<td align="center"><%= link_to_user topic.author %></td>
<td align="center"><%= format_time(topic.created_on) %></td>
<td align="center"><%= topic.replies_count %></td>
<td>
<small>
<tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
<td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %></td>
<td class="author" align="center"><%= topic.author %></td>
<td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
<td class="replies" align="center"><%= topic.replies_count %></td>
<td class="last_message">
<% if topic.last_reply %>
<%= topic.last_reply.author.name %>, <%= format_time(topic.last_reply.created_on) %><br />
<%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
<%= link_to_message topic.last_reply %>
<% end %>
</small>
</td>
</tr>
<% end %>
</tbody>
</table>
<p><%= pagination_links_full @topic_pages %>
[ <%= @topic_pages.current.first_item %> - <%= @topic_pages.current.last_item %> / <%= @topic_count %> ]</p>
<p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
<% else %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>
<% html_title h(@board.name) %>

View File

@@ -3,4 +3,4 @@
<p><%= l(:notice_not_authorized) %></p>
<p><a href="javascript:history.back()">Back</a></p>
<% set_html_title '403' %>
<% html_title '403' %>

View File

@@ -3,4 +3,4 @@
<p><%= l(:notice_file_not_found) %></p>
<p><a href="javascript:history.back()">Back</a></p>
<% set_html_title '404' %>
<% html_title '404' %>

View File

@@ -1,6 +0,0 @@
<p id="attachments_p">
<label for="attachment_file"><%=l(:label_attachment)%>
<%= image_to_function "add.png", "addFileField();return false" %></label>
<%= file_field_tag 'attachments[]', :size => 30 %>
<em>(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)</em>
</p>

View File

@@ -19,12 +19,15 @@ while day <= calendar.enddt %>
elsif day == i.due_date
image_tag('arrow_to.png')
end %>
<%= h("#{i.project.name} -") unless @project && @project == i.project %>
<%= h("#{i.project} -") unless @project && @project == i.project %>
<%= link_to_issue i %>: <%= h(truncate(i.subject, 30)) %>
<span class="tip"><%= render_issue_tooltip i %></span>
</div>
<% else %>
<%= link_to_version i, :class => "icon icon-package" %>
<span class="icon icon-package">
<%= h("#{i.project} -") unless @project && @project == i.project %>
<%= link_to_version i%>
</span>
<% end %>
<% end %>
</td>

View File

@@ -1,6 +1,6 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.title @title
xml.title truncate_single_line(@title, 100)
xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false}))
xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false)
xml.id url_for(:controller => 'welcome', :only_path => false)
@@ -9,14 +9,19 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.generator(:uri => Redmine::Info.url, :version => Redmine::VERSION) { xml.text! Redmine::Info.versioned_name; }
@items.each do |item|
xml.entry do
xml.title truncate(item.event_title, 100)
xml.link "rel" => "alternate", "href" => url_for(item.event_url(:only_path => false))
xml.id url_for(item.event_url(:only_path => false))
url = url_for(item.event_url(:only_path => false))
if @project
xml.title truncate_single_line(item.event_title, 100)
else
xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100)
end
xml.link "rel" => "alternate", "href" => url
xml.id url
xml.updated item.event_datetime.xmlschema
author = item.event_author if item.respond_to?(:author)
author = item.event_author if item.respond_to?(:event_author)
xml.author do
xml.name(author.is_a?(User) ? author.name : author)
xml.email(author.mail) if author.is_a?(User)
xml.name(author)
xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank?
end if author
xml.content "type" => "html" do
xml.text! textilizable(item.event_description)

View File

@@ -7,21 +7,43 @@ function toggle_custom_field_format() {
p_length = $("custom_field_min_length");
p_regexp = $("custom_field_regexp");
p_values = $("custom_field_possible_values");
p_searchable = $("custom_field_searchable");
p_default = $("custom_field_default_value");
p_default.setAttribute('type','text');
Element.show(p_default.parentNode);
switch (format.value) {
case "list":
Element.hide(p_length.parentNode);
Element.hide(p_regexp.parentNode);
if (p_searchable) Element.show(p_searchable.parentNode);
Element.show(p_values);
break;
case "date":
case "bool":
p_default.setAttribute('type','checkbox');
Element.hide(p_length.parentNode);
Element.hide(p_regexp.parentNode);
if (p_searchable) Element.hide(p_searchable.parentNode);
Element.hide(p_values);
break;
case "date":
Element.hide(p_length.parentNode);
Element.hide(p_regexp.parentNode);
if (p_searchable) Element.hide(p_searchable.parentNode);
Element.hide(p_values);
break;
case "float":
case "int":
Element.show(p_length.parentNode);
Element.show(p_regexp.parentNode);
if (p_searchable) Element.hide(p_searchable.parentNode);
Element.hide(p_values);
break;
default:
Element.show(p_length.parentNode);
Element.show(p_regexp.parentNode);
if (p_searchable) Element.show(p_searchable.parentNode);
Element.hide(p_values);
break;
}
@@ -47,7 +69,6 @@ function deleteValueField(e) {
//]]>
</script>
<!--[form:custom_field]-->
<div class="box">
<p><%= f.text_field :name, :required => true %></p>
<p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %></p>
@@ -59,11 +80,9 @@ function deleteValueField(e) {
<% (@custom_field.possible_values.to_a + [""]).each do |value| %>
<span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span>
<% end %>
</p>
<p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p>
</div>
<%= javascript_tag "toggle_custom_field_format();" %>
<!--[eoform:custom_field]-->
<div class="box">
<% case @custom_field.type.to_s
@@ -78,6 +97,7 @@ when "IssueCustomField" %>
<p><%= f.check_box :is_required %></p>
<p><%= f.check_box :is_for_all %></p>
<p><%= f.check_box :is_filter %></p>
<p><%= f.check_box :searchable %></p>
<% when "UserCustomField" %>
<p><%= f.check_box :is_required %></p>
@@ -87,3 +107,4 @@ when "IssueCustomField" %>
<% end %>
</div>
<%= javascript_tag "toggle_custom_field_format();" %>

View File

@@ -1,36 +1,48 @@
<h2><%=l(:label_custom_field_plural)%></h2>
<% selected_tab = params[:tab] ? params[:tab].to_s : custom_fields_tabs.first[:name] %>
<div class="tabs">
<ul>
<li><%= link_to l(:label_issue_plural), {}, :id=> "tab-IssueCustomField", :onclick => "showTab('IssueCustomField'); this.blur(); return false;" %></li>
<li><%= link_to l(:label_project_plural), {}, :id=> "tab-ProjectCustomField", :onclick => "showTab('ProjectCustomField'); this.blur(); return false;" %></li>
<li><%= link_to l(:label_user_plural), {}, :id=> "tab-UserCustomField", :onclick => "showTab('UserCustomField'); this.blur(); return false;" %></li>
<% custom_fields_tabs.each do |tab| -%>
<li><%= link_to l(tab[:label]), { :tab => tab[:name] },
:id => "tab-#{tab[:name]}",
:class => (tab[:name] != selected_tab ? nil : 'selected'),
:onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
<% end -%>
</ul>
</div>
<% %w(IssueCustomField ProjectCustomField UserCustomField).each do |type| %>
<div id="tab-content-<%= type %>" class="tab-content">
<% custom_fields_tabs.each do |tab| %>
<div id="tab-content-<%= tab[:name] %>" class="tab-content" style="<%= tab[:name] != selected_tab ? 'display:none' : nil %>">
<table class="list">
<thead><tr>
<th width="30%"><%=l(:field_name)%></th>
<th><%=l(:field_field_format)%></th>
<th><%=l(:field_is_required)%></th>
<% if type == 'IssueCustomField' %>
<% if tab[:name] == 'IssueCustomField' %>
<th><%=l(:field_is_for_all)%></th>
<th><%=l(:label_used_by)%></th>
<% end %>
<th><%=l(:button_sort)%></th>
<th width="10%"></th>
</tr></thead>
<tbody>
<% for custom_field in (@custom_fields_by_type[type] || []) %>
<% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%>
<tr class="<%= cycle("odd", "even") %>">
<td><%= link_to custom_field.name, :action => 'edit', :id => custom_field %></td>
<td align="center"><%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %></td>
<td align="center"><%= image_tag 'true.png' if custom_field.is_required? %></td>
<% if type == 'IssueCustomField' %>
<% if tab[:name] == 'IssueCustomField' %>
<td align="center"><%= image_tag 'true.png' if custom_field.is_for_all? %></td>
<td align="center"><%= custom_field.projects.count.to_s + ' ' + lwr(:label_project, custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td>
<% end %>
<td align="center" style="width:15%;">
<%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => custom_field, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
<%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => custom_field, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
<%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => custom_field, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
<%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => custom_field, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
</td>
<td align="center">
<%= button_to l(:button_delete), { :action => 'destroy', :id => custom_field }, :confirm => l(:text_are_you_sure), :class => "button-small" %>
</td>
@@ -38,9 +50,9 @@
<% end; reset_cycle %>
</tbody>
</table>
<br />
<%= link_to l(:label_custom_field_new), {:action => 'new', :type => type}, :class => 'icon icon-add' %>
<p><%= link_to l(:label_custom_field_new), {:action => 'new', :type => tab[:name]}, :class => 'icon icon-add' %></p>
</div>
<% end %>
<%= javascript_tag "showTab('#{@tab}');" %>
<% html_title(l(:label_custom_field_plural)) -%>

View File

@@ -1,16 +1,16 @@
<div class="contextual">
<%= link_to_if_authorized l(:label_document_new),
{:controller => 'projects', :action => 'add_document', :id => @project},
{:controller => 'documents', :action => 'new', :project_id => @project},
:class => 'icon icon-add',
:onclick => 'Element.show("add-document"); return false;' %>
</div>
<div id="add-document" style="display:none;">
<h2><%=l(:label_document_new)%></h2>
<% form_tag({:action => 'add_document', :id => @project}, :class => "tabular", :multipart => true) do %>
<% form_tag({:controller => 'documents', :action => 'new', :project_id => @project}, :class => "tabular", :multipart => true) do %>
<%= render :partial => 'documents/form' %>
<div class="box">
<%= render :partial => 'common/attachments_form'%>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
</div>
<%= submit_tag l(:button_create) %>
<%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-document")' %>
@@ -35,3 +35,5 @@
<label><%= radio_button_tag 'sort_by', 'author', (@sort_by == 'author'), :onclick => 'this.form.submit();' %> <%= l(:field_author) %></label>
<% end %>
<% end %>
<% html_title(l(:label_document_plural)) -%>

View File

@@ -0,0 +1,13 @@
<h2><%=l(:label_document_new)%></h2>
<% form_tag({:controller => 'documents', :action => 'new', :project_id => @project}, :class => "tabular", :multipart => true) do %>
<%= render :partial => 'documents/form' %>
<div class="box">
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
</div>
<%= submit_tag l(:button_create) %>
<% end %>

Some files were not shown because too many files have changed in this diff Show More