Compare commits

...

538 Commits
0.5.0 ... 0.6.3

Author SHA1 Message Date
Jean-Philippe Lang
63f11b1355 tagged version 0.6.3
git-svn-id: http://redmine.rubyforge.org/svn/tags/0.6.3@1012 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-18 19:02:44 +00:00
Jean-Philippe Lang
eb86e5e7e1 Changes for 0.6.3 release.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.6-stable@1010 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-18 18:12:18 +00:00
Jean-Philippe Lang
fc26668cd9 Fixed: upload doesn't work in "Files" section.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.6-stable@1009 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-18 18:04:10 +00:00
Jean-Philippe Lang
2b42e3fc50 Added 0.6 stable branch.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.6-stable@1008 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-12-18 17:53:13 +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
Jean-Philippe Lang
d0af24870e Slight changes for 0.6.0 release.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@883 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-04 13:19:42 +00:00
Jean-Philippe Lang
308118ce97 LDAP authentication: only ask for the user's DN if on-the-fly registration is disabled
git-svn-id: http://redmine.rubyforge.org/svn/trunk@882 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-04 11:49:03 +00:00
Jean-Philippe Lang
7535c211d5 Removed IssueStatus html_color attribute.
Issue list colorization can be done using a custom theme. Each row is given a class that correspond to the issue status (eg. status-1 where 1 is the status position).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@881 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-04 11:40:17 +00:00
Jean-Philippe Lang
d46e3a501e Pretty URL for the repository browser (Cyril Mougel)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@880 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-04 11:20:21 +00:00
Jean-Philippe Lang
d4e47d5d64 Added radio buttons on the documents list to sort documents by category, date, title or author.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@879 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-04 11:15:04 +00:00
Jean-Philippe Lang
11cf016c58 Added Korean translation (Choi Jong Yoon)
Updated Japanese translation (Go Maeda)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@878 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-04 10:59:37 +00:00
Jean-Philippe Lang
bd434427e6 Mailer:
* added an application setting to specify the application protocol (http or https) used to generate urls
* added layouts for text and html emails
* no more language specific templates
* urls are now generated using UrlWriter
* the mailer now uses AdvAttrAccessor code style to set email parameters

git-svn-id: http://redmine.rubyforge.org/svn/trunk@877 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-04 10:43:39 +00:00
Jean-Philippe Lang
4ded4277c5 Activity view now uses events attributes.
Fixed: error on activity feed if it contains a document.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@876 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-28 16:13:39 +00:00
Jean-Philippe Lang
b60bedafc0 Added a missing string to lang files.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@875 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-28 15:07:10 +00:00
Jean-Philippe Lang
9a3d743ae5 Some slight improvements in AJAX errors handling.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@874 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-28 15:04:17 +00:00
Jean-Philippe Lang
0af6f34758 Added the hability to copy an issue.
It can be done from the 'issue/show' view or from the context menu on the issue list.
The Copy functionality is of course only available if the user is allowed to create an issue.
It copies the issue attributes and the custom fields values.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@873 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-28 14:31:59 +00:00
Jean-Philippe Lang
bb4acc02d0 Added AJAX based context menu on the project issue list that provide shortcuts for editing, re-assigning, changing the status or the priority, moving or deleting an issue.
The context menu shows up when right-clicking an issue (Opera users have to use Ctrl + left-click instead since right-click can't be reassigned for this browser).
Works with Firefox 2, IE 7 (not perfect), Opera 9 and Safari 2. IE 6 doesn't display submenus.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@872 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-28 10:55:59 +00:00
Jean-Philippe Lang
d9e6359a83 Fixed: some quotation marks are rendered as strange characters in pdf.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@871 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-26 16:35:27 +00:00
Jean-Philippe Lang
8610b191ae Added "Float" as a custom field format.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@870 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-25 17:38:05 +00:00
Jean-Philippe Lang
01ef9f12c8 Serbian translation added (Dragan Matic)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@869 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-25 16:49:06 +00:00
Jean-Philippe Lang
24ec30b10c SCM browser:
* open subfolders are preserved when collapsing/re-expanding a folder
* prevent a folder to be loaded twice if double-clicked while loading

git-svn-id: http://redmine.rubyforge.org/svn/trunk@868 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-23 20:42:15 +00:00
Jean-Philippe Lang
152a5da64d SCM browser:
* js code improvement (was very slow when collapsing a folder with a lot of entries)
* folder icons changed

git-svn-id: http://redmine.rubyforge.org/svn/trunk@867 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-23 19:07:59 +00:00
Jean-Philippe Lang
295e8c86ab Fixed: error when bulk editing with Postgresql
git-svn-id: http://redmine.rubyforge.org/svn/trunk@866 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-23 17:15:00 +00:00
Jean-Philippe Lang
5d1d778ed5 Removed 'not null' constraint on position fields.
Previous migrations updated for new sqlite databases.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@865 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-22 21:07:29 +00:00
Jean-Philippe Lang
8f7f305f7c Removed 'not null' constraint on position fields.
Previous migrations updated for new sqlite databases.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@864 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-22 21:07:13 +00:00
Jean-Philippe Lang
33003e5b2d SCM browser: directories can now be collapsed and re-expanded with no additional request.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@863 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-22 19:34:21 +00:00
Jean-Philippe Lang
8ca4d35dcc Added a bit of AJAX on the SCM browser (tree view).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@862 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-22 17:45:41 +00:00
Jean-Philippe Lang
b30b6717a2 Commit message parser:
* Fixed: Error when parsing a commit message with duplicate issue identifiers
* Strip referencing and fixing keywords

git-svn-id: http://redmine.rubyforge.org/svn/trunk@861 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-22 16:52:36 +00:00
Jean-Philippe Lang
cedca57620 SVN integration: reposman.rb can now register created repositories in Redmine, so that the administrator doesn't have to enter the repository url in Redmine once it's created.
To do so, use the --url option when running reposman (see reposman help).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@860 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-21 16:26:14 +00:00
Jean-Philippe Lang
0d605006a3 Fixed flashes style for IE6.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@859 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-21 14:30:01 +00:00
Jean-Philippe Lang
a200e97667 * Emails footer can now be customized from the admin interface (Admin -> Email notifications).
* Added html part to all email templates.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@858 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-21 14:10:07 +00:00
Jean-Philippe Lang
2ee11c9089 Added status and priority related classes for each row on the issue list so that a custom theme can colorize issues by priority or status.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@857 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-20 16:47:10 +00:00
Jean-Philippe Lang
01105966d9 More accurate vertical positioning of #main-menu
git-svn-id: http://redmine.rubyforge.org/svn/trunk@856 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-20 15:09:32 +00:00
Jean-Philippe Lang
90d33c3e51 More flexible mail notifications settings at user level. A user has now 3 options:
* notification on any event on all his projects
* notification on any event on selected projects only (if the user belongs to more than 1 project)
* notification only for things that he watches or he is involded in (eg. issues that he watches or he is author or assignee)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@855 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-20 12:47:05 +00:00
Jean-Philippe Lang
eacd050630 Fixed: LDAP authentication crashes if one of the LDAP attributes name is left blank on the LDAP setup screen.
When not checking "On-the-fly" register, the 3 optional fields can now be safely left blank.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@854 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-19 19:36:42 +00:00
Jean-Philippe Lang
0e4e0a795a Fixed: unable to add a file to an issue without entering a note.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@853 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-19 19:09:47 +00:00
Jean-Philippe Lang
ecfc40629f Quote subversion username and password in svn commands.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@852 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-19 17:59:59 +00:00
Jean-Philippe Lang
0006c5f3b0 New document form can be accessed from the document list with no additional request.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@851 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-18 16:59:28 +00:00
Jean-Philippe Lang
3c42abe07e Added the ability to unassign issues with bulk edit.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@850 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-18 16:29:23 +00:00
Jean-Philippe Lang
11cc0a4857 Updated Polish translation (Mariusz Olejnik)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@849 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-18 16:12:13 +00:00
Jean-Philippe Lang
11f7a13971 Removed issue assignment validation to avoid validation failure when updating the issue and if the assignee is no longer a member of the project.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@848 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-18 16:07:36 +00:00
Jean-Philippe Lang
d10e9f1157 Disallow /repositories/diff in robots.txt
git-svn-id: http://redmine.rubyforge.org/svn/trunk@847 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-17 19:18:20 +00:00
Jean-Philippe Lang
f3eeff03c4 Added Hebrew translation (Bob Builder).
Updated Bulgarian translation (Nikolay Solakov).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@846 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-16 20:11:17 +00:00
Jean-Philippe Lang
49eb65e925 Added LDAPS support migration and fixed connection test flash messages.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@845 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-16 19:19:10 +00:00
Jean-Philippe Lang
4c509c9423 Added LDAPS support for LDAP authentication.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@844 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-16 19:09:21 +00:00
Jean-Philippe Lang
9ce5713ee4 Fixed: log is not displayed when browsing a copy in a svn repository.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@843 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-15 18:41:27 +00:00
Jean-Philippe Lang
b9cc65db61 Search engine: added a checkbox to search titles only (usefull when searching on common words).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@842 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-15 16:53:39 +00:00
Jean-Philippe Lang
74ecb37be6 Search engine:
* results are now displayed using different colors for tokens highlighting
* added label tag around "all words" checkbox

git-svn-id: http://redmine.rubyforge.org/svn/trunk@841 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-15 16:15:20 +00:00
Jean-Philippe Lang
7d54215ccb Fixed: 'Issues' table shows weird date for 'Start' and 'Due date' columns
git-svn-id: http://redmine.rubyforge.org/svn/trunk@840 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-15 15:58:53 +00:00
Jean-Philippe Lang
58b67fa914 Added reposman Ruby version (Nicolas Chuche).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@839 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-14 17:27:04 +00:00
Jean-Philippe Lang
f55a5e4572 Slight style change on plugin configuration view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@837 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-13 14:11:06 +00:00
Jean-Philippe Lang
a53c86f063 Removed inconsistent user manual stuff (html/docbook content and controller). Help link now points to the Redmine guide which will be more easy to maintain.
Also removed the useless components directory.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@835 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-13 10:30:24 +00:00
Jean-Philippe Lang
dead6a28f8 Removed translated email templates attachments_added and document_added (no longer usefull).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@834 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-12 18:04:56 +00:00
Jean-Philippe Lang
0682b8cfc4 Added html part to news_added mail.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@833 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-12 17:22:18 +00:00
Jean-Philippe Lang
4b3ff7af50 Moved translated strings of 'register' and 'lost password' mail templates to lang files.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@832 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-12 16:28:06 +00:00
Jean-Philippe Lang
65e05d822a Status can now be updated when bulk editing issues.
Workflow permissions are applied as when changing the status of a single issue. The issue is not saved (and an error is displayed) if the status transition is not allowed for the user.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@831 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-12 15:55:06 +00:00
Jean-Philippe Lang
bce286f458 Fixed: Can not set 'No change' value for commit_fix_done_ratio setting.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@830 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-12 15:11:10 +00:00
Jean-Philippe Lang
b950caa21e Gantt chart:
* now starts at the current month by default
* month count and zoom factor are automatically saved as user preferences

git-svn-id: http://redmine.rubyforge.org/svn/trunk@829 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-11 20:57:37 +00:00
Jean-Philippe Lang
3a0cd55622 Fixed: unable to change how many months are displayed on the gantt.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@828 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-11 20:38:49 +00:00
Jean-Philippe Lang
f7daee47a6 Updated Japanese translation (Go Maeda).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@827 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-11 17:32:09 +00:00
Jean-Philippe Lang
7486125e6d Bulk editing:
* Fixed: Done ratio always set to 0 even if (No change) is selected
* Added mail notifications

git-svn-id: http://redmine.rubyforge.org/svn/trunk@826 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-11 17:20:23 +00:00
Jean-Philippe Lang
ff2e031400 Bulk editing:
* Fixed: Done ratio always set to 0 even if (No change) is selected
* Added mail notifications

git-svn-id: http://redmine.rubyforge.org/svn/trunk@825 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-11 17:20:02 +00:00
Jean-Philippe Lang
2ce184d1de Added revision number in Redmine::VERSION (the revision number is read from .svn/entries if it exists).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@824 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-11 16:59:08 +00:00
Jean-Philippe Lang
3844e4bca8 Fixed: a user not authorized to edit wiki pages gets the edit form if the page doesn't exist. He now gets a 404.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@823 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-10 21:18:10 +00:00
Jean-Philippe Lang
38f540a701 Added custom fields in issue related mail notifications.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@822 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-10 19:43:54 +00:00
Jean-Philippe Lang
5f10cc8673 Added the ability to set the "done ratio" of issues fixed by commit (original path by Nikolay Solakov, slightly edited).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@821 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-10 18:56:59 +00:00
Jean-Philippe Lang
45203143c1 Fixed: Bulk edit doesn't change the category or fixed version properties.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@820 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-10 18:08:49 +00:00
Jean-Philippe Lang
6b63889fcd Added missing string in lang files.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@819 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-10 17:27:14 +00:00
Jean-Philippe Lang
824a67ab5a Added basic Theme support.
Theme is set at application level (in Admin -> Settings).
For now, themes can override stylesheets only (application.css).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@818 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-10 17:17:37 +00:00
Jean-Philippe Lang
2c4647f8c6 Added 'Bulk edit' functionality.
This can be done by clicking on the edit link (little pen icon) at the upper-left corner of the issue list.
Most properties can be set (priority, assignee, category, fixed version, start and due dates, done ratio) and a note can be entered.
Only issues of the current project can be selected for bulk edit (subproject issues can't).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@817 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-09 19:07:19 +00:00
Jean-Philippe Lang
df631e8c06 Set enumeration positions and default priority (Normal) in default configuration data.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@816 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-08 16:58:52 +00:00
Jean-Philippe Lang
ec51cdd0f9 Calendar:
* added an helper and moved the rendering code to a shared partial (used by project calendar and my calendar)
* first day of week can now be set in lang files (general_first_day_of_week)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@815 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-07 20:07:11 +00:00
Jean-Philippe Lang
cdb2781b48 Default encodings for repository files can now be set in application settings (Admin -> Settings -> Repositories encodings).
These encodings are used to convert files content and diff to UTF-8 so that they're properly displayed.
Multiple values are allowed (comma separated).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@814 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-07 15:21:40 +00:00
Jean-Philippe Lang
bb724e75c0 Fixed: improper 0x5c char handling in PDF output (Go Maeda).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@813 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-06 14:27:35 +00:00
Jean-Philippe Lang
5259dec061 Added preview on add/edit issue form.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@812 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-06 08:54:05 +00:00
Jean-Philippe Lang
75c4c4f6b3 Content of projects/add_issue.rhtml and issues/edit.rhtml moved to a shared partial.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@811 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-06 08:08:29 +00:00
Jean-Philippe Lang
fa094a3fe5 Merged IssuesController#export_pdf into IssuesController#show.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@810 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 23:17:49 +00:00
Jean-Philippe Lang
31467731fa Fixed: Migration 71 broken if run with code >= r803 (Enumeration#before_save relies on an attribute added in migration 72).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@809 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 22:53:26 +00:00
Jean-Philippe Lang
57d3dd3b80 Sligth changes on my page issue tables and wiki history, changesets tables.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@808 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 20:34:21 +00:00
Jean-Philippe Lang
26528d1004 'Manage repository' permission requires to be a project member.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@807 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 20:08:46 +00:00
Jean-Philippe Lang
df8945177a Added label tags on role form permission checkboxes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@806 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 20:06:53 +00:00
Jean-Philippe Lang
69c2965051 Fixed: Title with non-ascii characters breaks wiki
git-svn-id: http://redmine.rubyforge.org/svn/trunk@805 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 19:33:25 +00:00
Jean-Philippe Lang
e8971e5f83 Added some accesskeys:
* e => edit
* r => preview
* f => quick search
* 4 => search


git-svn-id: http://redmine.rubyforge.org/svn/trunk@804 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 18:05:41 +00:00
Jean-Philippe Lang
73dba2ac04 Added default value for enumerations.
Only default issue priority is actually used.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@803 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-05 17:44:15 +00:00
Jean-Philippe Lang
fd7910efc9 Added wiki_syntax.html Japanese translation (Motohiro Takayama).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@802 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-04 18:59:21 +00:00
Jean-Philippe Lang
185547af69 Issues sorted by priority position.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@801 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-04 17:15:55 +00:00
Jean-Philippe Lang
c216ab325b Added position on Enumeration model.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@800 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-04 17:04:50 +00:00
Jean-Philippe Lang
479b9b5433 Updated Japanese translation (Go Maeda).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@799 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-03 19:02:07 +00:00
Jean-Philippe Lang
f6f1fee92f Updated Spanish translation (Manuel Garcia Ortega).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@798 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-03 18:59:08 +00:00
Jean-Philippe Lang
a32d6a49c0 Fixed: pagination broken on news list with Opera.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@797 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-03 18:43:38 +00:00
Jean-Philippe Lang
586231067b Fixed: URL with ~ broken in wiki formatting.
All RedCloth quick phrase modifiers are now limited to a single line.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@796 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-03 18:00:50 +00:00
Jean-Philippe Lang
cb8bee3a4e Fixed: can't select columns when creating a new query.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@795 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-03 17:38:18 +00:00
Jean-Philippe Lang
20b4e226fe Performance improvement on calendar and gantt (about 45% on gantt for large number of issues).
Partial issues/_tooltip is replaced by an helper and some translated strings are now cached.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@794 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-03 17:20:04 +00:00
Jean-Philippe Lang
c3e3a1891b Small fix in project activity feed.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@793 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 18:49:31 +00:00
Jean-Philippe Lang
47e7ceacd7 Better handling of external link style assignment.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@792 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 17:45:40 +00:00
Jean-Philippe Lang
48fb20f540 Fixed TOC positionning in wiki pages.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@791 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 16:46:46 +00:00
Jean-Philippe Lang
0fc93a1197 Span caps turned off in textile formatting.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@790 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 15:25:31 +00:00
Jean-Philippe Lang
0f966dbcfd Added label tags on various checkboxes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@789 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 15:22:03 +00:00
Jean-Philippe Lang
ca094446b3 Don't show attributes on role form for built-in roles.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@788 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 14:00:38 +00:00
Jean-Philippe Lang
50096526bc Removed ProjectsController#feeds. This view was incomplete and inconsistent with permissions.
Feeds can still be accessed from the corresponding screens (eg. news feed is accessible from the news view).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@787 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 13:13:04 +00:00
Jean-Philippe Lang
c6e8cf5c21 Moved media print specific styles to application.css (print.css removed).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@786 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 12:36:38 +00:00
Jean-Philippe Lang
c6e61a5f19 Default columns displayed on the issue list can now be selected at application level.
Saved queries can overide this selection.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@785 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 12:04:24 +00:00
Jean-Philippe Lang
514cdacc87 Custom query columns: checkboxes replaced by two selects that let the user specify columns order.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@784 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 11:39:34 +00:00
Jean-Philippe Lang
96b4ac12cb Added a checkbox on custom query form to explicitly say if the query uses default columns or not.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@783 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 10:44:45 +00:00
Jean-Philippe Lang
e5f5671d66 Added the ability to customize columns of a saved query.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@782 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-10-01 08:44:17 +00:00
Jean-Philippe Lang
427b47b4d7 Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@781 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-30 18:07:23 +00:00
Jean-Philippe Lang
c2220cffcf Added svn command output to error log when SubversionAdapter#entries parsing fails.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@780 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-30 17:38:07 +00:00
Jean-Philippe Lang
3e86dde31e Added Romanian translation (Csongor Bartus).
Fixed a typo in en.yml.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@779 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-30 15:40:20 +00:00
Jean-Philippe Lang
665497f5fb Fixed: Manage pu(b)lic queries typo (Thomas Lecavelier).
As this fix modifies manage_public_queries permission symbol, this permission has to re-entered.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@778 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-30 15:32:03 +00:00
Jean-Philippe Lang
dc74a1780f Updated Japanese language file for calendar (Go Maeda).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@777 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-30 15:26:59 +00:00
Jean-Philippe Lang
3cacd4b3b9 Updated polish translation (Tomasz Gawryl).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@776 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-30 15:23:12 +00:00
Jean-Philippe Lang
b4d66593ef Fixed: Links get chopped by punctuation marks in anchors.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@775 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-30 15:16:58 +00:00
Jean-Philippe Lang
cae547a7ea Redmine acts_as_* plugins moved to vendor/plugins.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@774 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-29 10:37:08 +00:00
Jean-Philippe Lang
f2a5304d64 Sligth changes to wiki views.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@773 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-29 10:26:57 +00:00
Jean-Philippe Lang
b64e89daec Native eol property set on config/*
git-svn-id: http://redmine.rubyforge.org/svn/trunk@772 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-27 22:30:57 +00:00
Jean-Philippe Lang
66c8c1e292 Fixed: unable to migrate from an empty database.
(Message model is loaded at startup, which fails if it doesn't exist)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@771 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-27 22:24:09 +00:00
Jean-Philippe Lang
05e27d0d83 Slight style update.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@770 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-27 19:56:33 +00:00
Jean-Philippe Lang
e1bb4a5baa Fixed: Links to other wiki pages in headings get corrupted (class attributes shown).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@769 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-27 19:44:23 +00:00
Jean-Philippe Lang
20aff7cc70 Added wiki index by date.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@768 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-27 19:35:53 +00:00
Jean-Philippe Lang
d823c28484 Updated lang files (:label_result)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@767 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-27 18:52:02 +00:00
Jean-Philippe Lang
a96421019f Search engines now supports pagination.
Results are sorted in reverse chronological order.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@766 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-27 17:28:22 +00:00
Jean-Philippe Lang
d723bea502 Slight changes to the stylesheet and footer.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@765 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-25 16:44:09 +00:00
Jean-Philippe Lang
4b430e6ce2 Added Czech translation (Jan Kadlecek).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@764 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-25 16:37:51 +00:00
Jean-Philippe Lang
19f2853a2a Boards: new message form displayed with no additional request.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@763 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 19:54:48 +00:00
Jean-Philippe Lang
24b000fb24 Slight change to issues/show.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@762 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 19:26:09 +00:00
Jean-Philippe Lang
964c550a60 Sligth stylesheet change.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@761 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 18:59:35 +00:00
Jean-Philippe Lang
650888c73b Added a named route for the home page.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@760 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 18:38:45 +00:00
Jean-Philippe Lang
042ef42da0 Fixed projects search.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@759 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 18:08:24 +00:00
Jean-Philippe Lang
42fe6c6e04 Search engine now only searches objects the user is allowed to view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@758 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 17:46:25 +00:00
Jean-Philippe Lang
dfffa0a7f8 Project activity view now only shows what the user is allowed to view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@757 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 17:33:24 +00:00
Jean-Philippe Lang
f3673aff28 Added length validation for IssueCategory name.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@756 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-24 17:15:05 +00:00
Jean-Philippe Lang
dd6f438fd1 Modified history.png
git-svn-id: http://redmine.rubyforge.org/svn/trunk@755 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-23 22:18:45 +00:00
Jean-Philippe Lang
9a360bd574 Fixed: Planning title displayed in the project sidebar even if there is no link below.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@754 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-23 22:00:21 +00:00
Jean-Philippe Lang
dbcf2065b8 Added a sample plugin.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@753 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-23 18:50:53 +00:00
Jean-Philippe Lang
e4f0864e3a Basic plugin support.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@752 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-23 17:19:27 +00:00
Jean-Philippe Lang
a42a115b8f Subversion repository now accepts svn+ssh url.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@751 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-22 21:02:41 +00:00
Jean-Philippe Lang
2b77964468 The news list now shows the full news contents.
News add/edit forms are now accessible with no additional request.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@750 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-22 17:52:43 +00:00
Jean-Philippe Lang
fe7c0d6484 Added polish translation (Tomasz Gawryl).
Added a gloc task to update lang files based on the english file content.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@749 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-22 14:56:09 +00:00
Jean-Philippe Lang
384cbadce6 Fixed: error in calendar french translation (Thomas Lecavelier).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@748 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-22 14:12:44 +00:00
Jean-Philippe Lang
827e998afe Application layout refactored.
The project menu is now the main menu.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@747 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-22 13:17:49 +00:00
Jean-Philippe Lang
43f583d332 Fixed: error in english string :label_end_to_start
git-svn-id: http://redmine.rubyforge.org/svn/trunk@746 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-21 15:24:52 +00:00
Jean-Philippe Lang
99dcf2ffe5 Fixed: project homepage length validation inconsistent with database field.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@745 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-20 18:58:19 +00:00
Jean-Philippe Lang
31eda0fcb2 Fixed setting value serialization.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@744 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-19 16:29:45 +00:00
Jean-Philippe Lang
83eed09109 Added 2 log messages in SubversionAdapter#entries.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@743 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-18 21:22:28 +00:00
Jean-Philippe Lang
25012efc9c Fixed: Oracle error when saving serialized setting (eg. mail notifications)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@742 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-18 17:50:17 +00:00
Jean-Philippe Lang
eb371db5b9 Fixed: RMagick not loaded on case sensitive filesystems.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@741 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-17 16:56:11 +00:00
Jean-Philippe Lang
f23daffd7d Fixed: error on document list with a document without description.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@740 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-17 16:52:39 +00:00
Jean-Philippe Lang
29413d6bee Fixed: error when uploading a file with a content type longer than 60 chars.
(Maximum size changed to 255. Mime type not saved if longer)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@739 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-17 16:51:39 +00:00
Jean-Philippe Lang
fc64fc3e9c Added RMagick availability on admin/info.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@738 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 20:39:40 +00:00
Jean-Philippe Lang
a2206ab5a0 Updated german translation (Thomas Löber).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@737 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 20:14:12 +00:00
Jean-Philippe Lang
00bf5f04db Date added as acronym title in ApplicationHelper#authoring
git-svn-id: http://redmine.rubyforge.org/svn/trunk@736 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 17:56:48 +00:00
Jean-Philippe Lang
780d5fa070 Fixed: lines in wiki content containing just a single wiki link are not displayed.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@735 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 17:17:09 +00:00
Jean-Philippe Lang
1c69c43293 Fixed: unable to save a new wiki page that just contains the default h1 title
git-svn-id: http://redmine.rubyforge.org/svn/trunk@734 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 16:57:37 +00:00
Jean-Philippe Lang
fa969504d4 A category with assigned issue can now be deleted. 2 options are proposed:
* remove assignments (issues are set to 'no category')
* reassign issues to another category (if at least an other category exists)
If no issue is assigned to the category, it's deleted silently.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@733 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 16:52:32 +00:00
Jean-Philippe Lang
3c6ddc9cec Changed author display on issues, news and document files.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@732 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 15:20:08 +00:00
Jean-Philippe Lang
89b349818b Added 'Estimated hours' attribute on issues.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@731 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 14:54:15 +00:00
Jean-Philippe Lang
4967f10356 Project modules are checked (default) when creating a project.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@730 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-15 08:56:31 +00:00
Jean-Philippe Lang
f2a058f8cf Main project list now displays root projects with their subprojects.
Added files turned into links (if not removed) on the issue history.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@729 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-14 19:55:51 +00:00
Jean-Philippe Lang
23264ec3eb Mail notification options restored (default is: issue_added and issue_updated).
Added mail notification when adding a news (disabled by default).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@728 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-14 15:30:46 +00:00
Jean-Philippe Lang
1fa1f62018 Fixed: Spent Time on Reports page only visible to administrators.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@727 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-14 11:42:23 +00:00
Jean-Philippe Lang
8929bfca63 Fixed: can't preview/save the very first wiki page.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@726 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-14 11:35:47 +00:00
Jean-Philippe Lang
21c97c6a13 Added project module concept.
A project module (eg. issue tracking, news, wiki,...) is a set of permissions that can enabled/disabled at project level.
For each project, modules can be enabled on the project settings view ('Modules' tab).
This requires a specific permission: 'Select project modules' (if this permission is turned off, only Redmine administrators can choose which modules a project uses).

When applying this migration, all modules are enabled for all existing projects.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@725 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-14 11:34:08 +00:00
Jean-Philippe Lang
29348fafb7 Fixed: watchers should be notified even if they uncheck 'mail notifications'
git-svn-id: http://redmine.rubyforge.org/svn/trunk@724 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-11 18:13:32 +00:00
Jean-Philippe Lang
74ed14f8a2 Notifications about issues (add/edit) are now sent in plain text and html.
Removed lang specific strings in the corresponding mail templates, so that there is only one template for all languages.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@723 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-11 17:12:34 +00:00
Jean-Philippe Lang
6db28b3899 Added Subversion repository unit tests.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@722 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-10 18:23:44 +00:00
Jean-Philippe Lang
1590ad2518 Added "% done" in the filter list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@721 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-09 19:46:28 +00:00
Jean-Philippe Lang
b4d9ca8875 Added the ability to rename wiki pages (specific permission required).
Existing links that point to the old page are preserved and automatically redirected to the new page (this behaviour can be disabled when renaming the page).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@720 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-09 17:05:38 +00:00
Jean-Philippe Lang
f6fe15716e Small fix to TOC regexp.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@719 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-09 10:41:02 +00:00
Jean-Philippe Lang
f3364b9dce Added automatic table of content support on wiki pages, based on h1., h2. and h3. headings.
To display the page TOC, insert a line with this tag:
{{toc}} or {{<toc}} => left aligned toc
{{>toc}} => right aligned toc

git-svn-id: http://redmine.rubyforge.org/svn/trunk@718 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-09 10:16:59 +00:00
Jean-Philippe Lang
7d899166c6 Updated INSTALL doc.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@717 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-08 17:22:42 +00:00
Jean-Philippe Lang
27d6da9457 Added javascript highlightment support (http://pastie.textmate.org/50774/)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@716 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-07 21:30:46 +00:00
Jean-Philippe Lang
7e4611ad31 Removed @html_title assignments in controllers.
Views can now set the header title using set_htmh_title(title) method.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@715 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-07 20:52:45 +00:00
Jean-Philippe Lang
fdf842a4c4 Improved Redmine links:
* issue and changeset links generated only if the object exists
* issue subject and status appear in the link title
* strike issue link if issue is closed
* red wiki page link if page doesn't exist
* new icon for external links

Wiki page cache had to be disabled.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@714 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-07 20:07:54 +00:00
Jean-Philippe Lang
b812705976 Fixed: 'Move' button always visible on issues list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@713 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-07 17:26:14 +00:00
Jean-Philippe Lang
98a01d62c2 Automatically scroll down to the preview when previewing wiki page.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@712 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-06 21:56:40 +00:00
Jean-Philippe Lang
422677b9b7 Added background on <pre> wiki blocks.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@711 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-06 18:51:35 +00:00
Jean-Philippe Lang
42db3cac06 Added code highlighting support in wiki, using this syntax:
<pre><code> <-- cut here

  <pre><code class="ruby">
    Place you code here.
  </code></pre>  

cut here --> </pre></code>

git-svn-id: http://redmine.rubyforge.org/svn/trunk@710 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-06 17:06:07 +00:00
Jean-Philippe Lang
22c5e0d614 'current week' filter fix.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@709 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-06 16:24:12 +00:00
Jean-Philippe Lang
22ad806a43 Removed RedCloth checks since it's now supplied with the application.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@708 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-05 20:51:11 +00:00
Jean-Philippe Lang
0803e6fcaa Search field automatically focused.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@707 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-05 20:13:06 +00:00
Jean-Philippe Lang
1b2e80545a Fixed: custom field displayed as deleted in the issue history even if no value was set.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@706 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-05 19:56:33 +00:00
Jean-Philippe Lang
8dcc041244 Issue notes are now included in search.
Fixed: search results too long when there are many matches.
Fixed: search results not escaped.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@705 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-05 19:37:13 +00:00
Jean-Philippe Lang
c27106d859 Added a new value for date filters: 'this week'
git-svn-id: http://redmine.rubyforge.org/svn/trunk@704 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-05 17:47:17 +00:00
Jean-Philippe Lang
a19a0d7b92 Subprojects are now grouped by projects in the 'Projects' top navigation drop-down menu.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@703 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-05 17:24:22 +00:00
Jean-Philippe Lang
17a3f9e44c Added an index on custom_values table (customized_type + customized_id columns) to speed up issue queries that use custom field filters.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@702 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-04 22:07:44 +00:00
Jean-Philippe Lang
f16ab838d8 Small fix in Mailer.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@701 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-04 16:58:30 +00:00
Jean-Philippe Lang
6260892949 'Due in' label removed from the changelog view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@700 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 20:47:41 +00:00
Jean-Philippe Lang
8a3e713f2f Added Redmine::WikiFormatting module and tests for wiki links.
RedCloth librairy is now present in Redmine lib directory.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@699 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 20:41:47 +00:00
Jean-Philippe Lang
324b904ed5 Fix: Inline image links broken in rev 690.
TODO: Fix urls with &

git-svn-id: http://redmine.rubyforge.org/svn/trunk@698 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 15:06:29 +00:00
Jean-Philippe Lang
b1cafef07a Buttons (edit, watch...) on issues/show are now located in the upper-right corner.
Pdf export link is at the bottom of the screen.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@697 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 14:34:13 +00:00
Jean-Philippe Lang
70da1c7771 Changed the application default title (redMine -> Redmine).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@696 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 12:22:17 +00:00
Jean-Philippe Lang
3ae8c53cba Small fix in issue history display.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@695 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 12:20:05 +00:00
Jean-Philippe Lang
eb6ab2af50 Trac importer: 'resolution' field imported with history as a custom field.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@694 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 12:00:48 +00:00
Jean-Philippe Lang
72b5cd3889 Trac importer now migrates status changes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@693 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 10:07:04 +00:00
Jean-Philippe Lang
a2439f3b34 Trac importer now checks the existence of trac.db and attachments directory before processing.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@692 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 09:12:49 +00:00
Jean-Philippe Lang
e76d153064 Added namespace for Redmine specific rake tasks.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@691 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-02 08:08:10 +00:00
Jean-Philippe Lang
521f4a6ddb Fixed: auto-generated links corrupted when url contains & characters in the query string.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@690 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-01 20:17:45 +00:00
Jean-Philippe Lang
296b6aee5a Trac importer initial commit. The script migrates:
* users (default password = 'trac')
* components
* milestones
* tickets
* ticket comments, files, custom fields
* wiki

git-svn-id: http://redmine.rubyforge.org/svn/trunk@689 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-09-01 20:03:34 +00:00
Jean-Philippe Lang
c6ac590d17 Added 'reported by me' and 'assigned to me' issue feeds on 'My page'.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@688 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-31 21:43:13 +00:00
Jean-Philippe Lang
1281d99f30 Added the ability to move issues (to another project) without changing their trackers.
Added length validation for homepage project attribute.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@687 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-31 20:56:14 +00:00
Jean-Philippe Lang
c68dac7e9a Added atom feed on the new cross-project issue list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@686 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-31 20:22:36 +00:00
Jean-Philippe Lang
6bdc13b33d Added cache for application settings (Setting model).
Once the values are cached, only one database query is done at each user request (to check if the cache is still valid).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@685 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-31 17:45:32 +00:00
Jean-Philippe Lang
404bfce446 Added a cross-project issue list. It displays the issues of all the projects visible by the user.
The users list available in the filters ('assigned to' / 'created by') is made of the members of all projects the current user belongs to.
For now, this view is only accessible from 'My page' ('issues assigned to me' or 'issues reported by me' blocks, to view the full lists)

On 'My page', assigned issue are now sorted by priority.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@684 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-31 17:02:44 +00:00
Jean-Philippe Lang
1187ad96ac Fixed: autologin broken.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@683 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-30 16:33:50 +00:00
Jean-Philippe Lang
840f18b1c7 Added Redmine::Info to store various information about the application.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@682 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 22:27:43 +00:00
Jean-Philippe Lang
088563db3e Fixed: "new issue" drop-down only visible by admin users.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@681 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 21:52:50 +00:00
Jean-Philippe Lang
bd6ab74318 Added border and padding on wiki table headers.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@680 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 21:43:01 +00:00
Jean-Philippe Lang
fc1024a52a Translated a few strings in fr.yml
git-svn-id: http://redmine.rubyforge.org/svn/trunk@679 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 20:17:52 +00:00
Jean-Philippe Lang
05ff26c141 Fixed: "subproject of" list should not show archived projects.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@678 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 20:13:02 +00:00
Jean-Philippe Lang
39c9874a41 Added the ability to reset its own RSS access key on "My account".
git-svn-id: http://redmine.rubyforge.org/svn/trunk@677 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 20:07:28 +00:00
Jean-Philippe Lang
317b460d96 Some deprecation fixes (end_form_tag and count API).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@676 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 17:25:22 +00:00
Jean-Philippe Lang
c8b3c8dfec Fix: error when posting to projects/add or users/add with no custom_fields parameter.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@675 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 17:07:28 +00:00
Jean-Philippe Lang
603e11d7a5 Merged 0.6 branch into trunk.
Permissions management was rewritten. Some permissions can now be specifically defined for non member and anonymous users.
This migration:
* is irreversible (please, don't forget to *backup* your database before upgrading)
* resets role's permissions (go to "Admin -> Roles & Permissions" to set them after upgrading)

git-svn-id: http://redmine.rubyforge.org/svn/trunk@674 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-29 16:52:35 +00:00
Jean-Philippe Lang
8da5bad295 Fixed: queries with multiple custom fields return no result.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@668 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-26 12:29:53 +00:00
Jean-Philippe Lang
452a20a69a Added missing image.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@667 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-26 12:29:00 +00:00
Jean-Philippe Lang
edba1f692b Gantt chart can now be exported to a graphic file (png).
This functionality is only available if RMagick is present.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@666 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-26 07:55:57 +00:00
Jean-Philippe Lang
c1eb587c6d Fixed: calendar and gantt not updated when adding/editing/deleting a project version.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@665 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 20:19:19 +00:00
Jean-Philippe Lang
db4781d068 Replaced hard-coded urls in Mailer#attachments_add
git-svn-id: http://redmine.rubyforge.org/svn/trunk@664 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 20:09:33 +00:00
Jean-Philippe Lang
4fa51992b5 Automatic closing of duplicate issues.
When closing an issue, all related issues marked as duplicates are now also closed.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@663 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 17:45:51 +00:00
Jean-Philippe Lang
b96dc97d15 Fixed: Bad url to project forums from the wiki home page.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@662 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 16:40:52 +00:00
Jean-Philippe Lang
0a629237fd Mantis importer: small fix in categories mapping.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@661 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 15:53:40 +00:00
Jean-Philippe Lang
f44f6c7876 Mantis importer:
* validation skip on issue assignement (assignee must be a project member in Redmine)
* Mantis bugs with severity 'feature' are imported as 'Feature request' in Redmine

git-svn-id: http://redmine.rubyforge.org/svn/trunk@660 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 15:45:12 +00:00
Jean-Philippe Lang
4dad4fab50 Mantis importer: issue categories truncated to 30 chars.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@659 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 14:55:07 +00:00
Jean-Philippe Lang
72714d86f6 Fixed: wiki preview doesn't work on long entries.
POST is now used for previews instead of GET.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@658 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 14:51:06 +00:00
Jean-Philippe Lang
c754e017dc Changeset comments are now stripped before being stored in the database (patch by Nicholas Wieland).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@657 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 14:41:55 +00:00
Jean-Philippe Lang
138566de0b Committer field in the Changesets table is now varchar 255.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@656 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-25 14:35:18 +00:00
Jean-Philippe Lang
1e28f7758f Added wiki syntax to online help.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@655 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-17 15:53:39 +00:00
Jean-Philippe Lang
f601d5fb9e Fixed: cursor not positioned correctly when using wiki toolbar buttons under IE (Balazs Dan).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@654 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-17 12:58:01 +00:00
Jean-Philippe Lang
d1780270da Small change to ApplicationHelper#textilizable to allow links to other wikis main page with custom text (eg. project:|mytext).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@653 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-17 12:06:50 +00:00
Jean-Philippe Lang
00238738b1 Application welcome text is now textilized.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@652 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-17 11:32:58 +00:00
Jean-Philippe Lang
7b366758fd Textilized project descriptions on project list and home page.
Added jsToolbar on project form.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@651 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-17 11:21:58 +00:00
Jean-Philippe Lang
a7033c8bac Code improvement in ProjectsController#activity (Nicolas Chuche)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@650 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-16 21:23:54 +00:00
Jean-Philippe Lang
446889b3f0 Added a 'Assignable' boolean on Role model.
If unchecked, issues can not be assigned to users having this role.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@649 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-16 17:47:41 +00:00
Jean-Philippe Lang
4cedecad4d Added a link to add a new category when creating or editing an issue.
The user is prompted for the category name. The new category is created and the drop-down list updated using an ajax call.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@648 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-16 16:45:06 +00:00
Jean-Philippe Lang
1f991c6a79 Added feeds auto discovery links on projects/show (patch by Nicolas Chuche).
Also fixed 'issue changes details' feed title.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@647 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-16 11:01:00 +00:00
Jean-Philippe Lang
2f0edb0b61 Activity view improvements:
* issue status changes are now displayed
* display options are kept when using previous/next month links

git-svn-id: http://redmine.rubyforge.org/svn/trunk@646 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-16 10:38:23 +00:00
Jean-Philippe Lang
5b35bfd434 Fixed: nil error on 'commits per month' graph.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@645 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-15 23:04:14 +00:00
Jean-Philippe Lang
889d50089d Added syntax highlightment for repository files (using CodeRay).
Supported languages: c, ruby, rhtml, yaml, html, xml.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@644 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-15 20:20:18 +00:00
Jean-Philippe Lang
a5849ee044 Wiki links can now refer other project wikis, using this syntax:
[[project:]] -> wiki starting page
[[project:page]]
[[project:page|text]]
where 'project' is the project name or identifier.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@643 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-15 17:31:01 +00:00
Jean-Philippe Lang
38e0c237a4 Image attachments are now sent inline to be viewed directly in the browser.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@642 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-15 15:36:15 +00:00
Jean-Philippe Lang
c4239a4fd2 Removed hard-coded URLs in mail templates.
Untranslated templates were deleted.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@641 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-15 15:16:15 +00:00
Jean-Philippe Lang
7379d4e2d0 Mantis importer: fixed default role mapping
git-svn-id: http://redmine.rubyforge.org/svn/trunk@638 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-15 11:11:27 +00:00
Jean-Philippe Lang
f94be4f8ab Fixed: SQL error on roadmap and changelog if there is no tracker to display.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@636 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-15 10:45:46 +00:00
Jean-Philippe Lang
3eec500320 Small change to mantis importer to be 0.5.1 compatible.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@635 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-14 13:58:39 +00:00
Jean-Philippe Lang
c16e27625c Mantis importer: fixed a few bugs
git-svn-id: http://redmine.rubyforge.org/svn/trunk@634 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-14 12:51:25 +00:00
Jean-Philippe Lang
5313f48ff4 Mantis importer: replaced find by find_by_id
git-svn-id: http://redmine.rubyforge.org/svn/trunk@633 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-14 10:48:27 +00:00
Jean-Philippe Lang
0061a3f04a Added 'email sending test' functionality.
Go to Admin -> Mail notifications and click on 'Send a test email'.
If an error occurs while sending the mail, the error message is displayed to the user.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@632 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-14 10:36:19 +00:00
Jean-Philippe Lang
7e755a53b8 Firstname, lastname and email LDAP attributes are now required if "on-the-fly register" is checked.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@631 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-14 09:14:33 +00:00
Jean-Philippe Lang
ecf208f660 Issue history is now 'oldest first' sorted.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@630 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-14 09:02:40 +00:00
Jean-Philippe Lang
b3f3634df3 On the issue report page, 0 issue counts are now displayed as dashes.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@629 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 17:00:59 +00:00
Jean-Philippe Lang
29c623fa58 Mantis importer: added priorities mapping.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@628 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 16:48:43 +00:00
Jean-Philippe Lang
343cab8148 Mantis importer: steps_to_reproduce and additional_information fields added to issue description.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@627 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 15:51:23 +00:00
Jean-Philippe Lang
f179eee7a0 Added migration of Mantis bug monitors.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@626 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 15:35:52 +00:00
Jean-Philippe Lang
5b4812add8 Added migration of Mantis bug relationships.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@625 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 15:29:16 +00:00
Jean-Philippe Lang
005ce1b21d More accurate status and role mapping in Mantis importer.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@624 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 15:04:45 +00:00
Jean-Philippe Lang
1dcf50b59a Mantis migration task initial import.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@623 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 14:37:11 +00:00
Jean-Philippe Lang
a8b202c9bc Fixed: non-active users can be viewed with account/show
git-svn-id: http://redmine.rubyforge.org/svn/trunk@622 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 10:28:55 +00:00
Jean-Philippe Lang
64d805b009 Updated UPGRADING documentation
git-svn-id: http://redmine.rubyforge.org/svn/trunk@621 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-13 10:19:46 +00:00
Jean-Philippe Lang
79b8bd0a38 Added an option to be able to relate issues in different projects.
It can be set in Admin -> Settings (default: false).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@620 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 15:31:04 +00:00
Jean-Philippe Lang
cf4e216502 Project name is now displayed in news titles when not inside the project (eg. home page).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@619 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 14:47:54 +00:00
Jean-Philippe Lang
1eff42eda6 Translated 'Sort by' label on table headers.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@618 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 14:42:15 +00:00
Jean-Philippe Lang
5478e5cbae Attachment content-type is now chomped before being saved.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@617 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 14:32:07 +00:00
Jean-Philippe Lang
29f25601bb Fixed: versions not sorted correctly in drop down lists.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@616 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 14:27:40 +00:00
Jean-Philippe Lang
e4f2e0fd37 Removed deprecated Object#type in CustomFieldsController.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@615 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 14:24:04 +00:00
Jean-Philippe Lang
4e65be9ed1 Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations
git-svn-id: http://redmine.rubyforge.org/svn/trunk@614 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 14:22:02 +00:00
Jean-Philippe Lang
6862b97909 Removed IssuesController#history, all changes are now displayed on issues/show (not only the last 15).
Added anchor links to issue notes, eg: /issues/show/1#note-3

git-svn-id: http://redmine.rubyforge.org/svn/trunk@613 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 13:57:29 +00:00
Jean-Philippe Lang
64e91b2e13 Fixed custom fields alignment on issues/show (Balazs Dan)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@612 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 12:59:05 +00:00
Jean-Philippe Lang
9f0ddb89a0 Changed 'versions' table creation to allow null values for 'effective_date' field.
This change was done in migration 048 but seems to have no effect with SQLite.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@611 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 12:37:05 +00:00
Jean-Philippe Lang
e9ba8a4494 Changed AR error messages style (same as flash errors).
Fixed flash styles for IE6.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@610 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 11:25:25 +00:00
Jean-Philippe Lang
58ea44a462 Updated german translation (Hans-Peter Werner)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@609 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 10:29:29 +00:00
Jean-Philippe Lang
7681487af4 Fixed: Roadmap percentages not being calculated correctly (Nick Read).
The roadmap percentages are now calculated using floating point.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@608 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 10:23:07 +00:00
Jean-Philippe Lang
c4fee11969 Forum notifications are now also sent to the authors of the thread, even if they don't watch the board.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@607 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 10:16:30 +00:00
Jean-Philippe Lang
5bd26ede42 Fixed my_controller_test.rb (errors are now in flash[:error])
git-svn-id: http://redmine.rubyforge.org/svn/trunk@606 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 10:04:44 +00:00
Jean-Philippe Lang
3c8e7c79b6 Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@604 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 09:58:38 +00:00
Jean-Philippe Lang
3be226d0c4 Added a confirmation dialog box for project archiving
git-svn-id: http://redmine.rubyforge.org/svn/trunk@603 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-02 20:10:37 +00:00
Jean-Philippe Lang
dfaf8f7e6a Changed the footer link from http://redmine.rubyforge.org to http://www.redmine.org/
git-svn-id: http://redmine.rubyforge.org/svn/trunk@602 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-02 19:44:38 +00:00
Jean-Philippe Lang
acebceb1d7 Applied 'register notice' patch by Matt Jones.
The user is now redirected to the login screen after having registered.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@601 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-02 17:54:35 +00:00
Jean-Philippe Lang
2d5ac54d2e Applied the flash notices patch by Matt Jones (slightly edited).
flash[:notice] and flash[:error] are now used for notice/error messages.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@600 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-02 17:42:20 +00:00
Jean-Philippe Lang
7701de9ae4 Fixed: on spent time details view, the h3 issue link now links back to the issue
git-svn-id: http://redmine.rubyforge.org/svn/trunk@599 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-01 20:04:31 +00:00
Jean-Philippe Lang
46745614b4 Fixed: error when creating a tracker without copying an existing wokflow
git-svn-id: http://redmine.rubyforge.org/svn/trunk@598 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-01 19:51:02 +00:00
Jean-Philippe Lang
efbe29249c Added icon on external wiki links
git-svn-id: http://redmine.rubyforge.org/svn/trunk@597 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-01 19:38:13 +00:00
Jean-Philippe Lang
188077d634 History box on issues/show is now hidden if there's no history
git-svn-id: http://redmine.rubyforge.org/svn/trunk@596 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-01 18:59:39 +00:00
Jean-Philippe Lang
f8da6fb679 Fixed a bug in CVS model (clash when 2 projects point to the same repository)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@595 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-01 17:53:05 +00:00
Jean-Philippe Lang
6edfcd83c1 Removed a line break on news/show
git-svn-id: http://redmine.rubyforge.org/svn/trunk@594 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-18 17:22:19 +00:00
Jean-Philippe Lang
fcefdb22bf Added several validates_length_of
git-svn-id: http://redmine.rubyforge.org/svn/trunk@593 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-16 17:16:49 +00:00
Jean-Philippe Lang
7363428703 Fixed mailer (error when no assignee)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@592 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-15 12:01:53 +00:00
Jean-Philippe Lang
25f8c64f92 Doc update for 0.5.1 release.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@591 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-15 11:44:52 +00:00
Jean-Philippe Lang
fd2839c833 Now, when a user turns mail notifications off, he will still receive notifications about issue changes if he is the author or the assignee of the issue.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@590 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-15 11:12:08 +00:00
Jean-Philippe Lang
1cb5fc747f Slight documentation update.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@589 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-15 10:11:25 +00:00
Jean-Philippe Lang
4670426e4e Hours displayed using %.2f on time report.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@588 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-14 19:07:35 +00:00
Jean-Philippe Lang
05c1888703 Fix: SVN commit dates are now stored as local time
git-svn-id: http://redmine.rubyforge.org/svn/trunk@587 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-14 14:53:18 +00:00
Jean-Philippe Lang
e94e88e3ad Added the ability to log time when changing an issue status.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@586 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-14 14:37:22 +00:00
Jean-Philippe Lang
9c38458f8b Added an option to choose the date format: language based (as defined in each lang file) or ISO 8601 (YYYY-MM-DD).
This option can be set in Admin -> Settings.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@585 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-14 13:29:20 +00:00
Jean-Philippe Lang
98cf33070f Fixed: Error when editing the wokflow after deleting a status
git-svn-id: http://redmine.rubyforge.org/svn/trunk@584 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-14 11:31:43 +00:00
Jean-Philippe Lang
5e20417e6d Added wiki diff.
Diff can be viewed from the page history, or directly from the project activity page for each edit.
Uses Lars Christensen's diff library.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@583 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-14 11:25:03 +00:00
Jean-Philippe Lang
bf74efcd11 Added a margin for inline images in wiki text
git-svn-id: http://redmine.rubyforge.org/svn/trunk@582 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-02 17:23:31 +00:00
Jean-Philippe Lang
384aa50be3 Fixed: issue_id not nullified on time entries when deleting the issue
git-svn-id: http://redmine.rubyforge.org/svn/trunk@581 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-02 15:59:35 +00:00
Jean-Philippe Lang
8687401d65 Fixed: trash icon not visible on files list.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@580 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-07-02 15:32:42 +00:00
Jean-Philippe Lang
7eba6786d3 Changesets stored in the database are now displayed on the repository page even if the repository can not be reached (eg. svnserve down).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@579 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-29 18:50:48 +00:00
Jean-Philippe Lang
65be9d3c86 Removed User#display_name (replaced by User#name).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@578 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-29 17:27:27 +00:00
Jean-Philippe Lang
87bad767c6 Each category can now be associated to a user, so that new issues in that category are automatically assigned to that user.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@577 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-29 17:21:37 +00:00
Jean-Philippe Lang
f816d4f378 Fixed: Long text custom fields displayed without line breaks
Added wiki toolbar for issue notes

git-svn-id: http://redmine.rubyforge.org/svn/trunk@576 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-27 19:12:52 +00:00
Jean-Philippe Lang
184be9d0e3 Fixed: time report doesn't take account of the project for time calculations
git-svn-id: http://redmine.rubyforge.org/svn/trunk@575 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-27 18:59:36 +00:00
Jean-Philippe Lang
7eb3b186c4 Fixed an error when trying to delete a project (versions/issues dependency)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@574 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-25 19:17:58 +00:00
Jean-Philippe Lang
ba1b857197 Added Darcs basic support.
Files in the repository can not be viewed or downloaded since Darcs doesn't support cat-like command.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@573 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-24 19:30:38 +00:00
Jean-Philippe Lang
faa3d984ab Added time report.
Report can be generated by member/activity/tracker/version and year/month/week for the selected period.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@572 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-24 16:07:06 +00:00
Jean-Philippe Lang
6d7a855ca2 Added pagination on wiki page history.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@571 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-23 18:53:45 +00:00
Jean-Philippe Lang
600018d5ad Fixed Iconv::IllegalSequence errors in csv exports
git-svn-id: http://redmine.rubyforge.org/svn/trunk@570 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-23 16:55:17 +00:00
Jean-Philippe Lang
ec44c94c12 Fixed an error on welcome screen for users with no membership
git-svn-id: http://redmine.rubyforge.org/svn/trunk@569 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-23 16:36:01 +00:00
Jean-Philippe Lang
48c2a690c2 Don't show "Projects" label if projects list is empty.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@568 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-23 14:10:13 +00:00
Jean-Philippe Lang
1a2aee84b2 Fixed confidentiality issue on account/show.
Only public projects or private projects that the logged in user belongs to are displayed.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@567 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-23 14:06:21 +00:00
Jean-Philippe Lang
1c44600c62 Added per user custom queries.
Any logged in user can now save queries (they are not visible to the other users).
Only users with explicit permission can manage queries that are visible to anyone.
The queries list is removed from the "Reports" view. It can now be accessed from the issues list.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@566 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-23 13:49:29 +00:00
Jean-Philippe Lang
5332c4362c Added page association on versioned WikiContent
git-svn-id: http://redmine.rubyforge.org/svn/trunk@565 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-17 19:32:58 +00:00
Jean-Philippe Lang
f2acb56041 A wiki page can now be attached to each version.
For that, edit the version and set the wiki page name (project wiki must be enabled).
The wiki page content is displayed for each version on the roadmap view.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@564 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-14 18:26:27 +00:00
Jean-Philippe Lang
ff1343882a Swedish mail templates added (Thomas Habets)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@563 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-13 16:56:37 +00:00
Jean-Philippe Lang
2dbb397818 Fixed version field on issue view page now links to the corresponding version in the roadmap.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@562 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-12 23:07:00 +00:00
Jean-Philippe Lang
136a2a614b Applied this fix http://dev.rubyonrails.org/ticket/4967 to solve namespaced models dependencies problem.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@561 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-12 23:03:38 +00:00
Jean-Philippe Lang
a681d8bf4d Added an error message when trying to create an issue with no default status defined.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@560 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-12 22:31:05 +00:00
Jean-Philippe Lang
438161ad1f Added basic support for CVS and Mercurial SCMs.
Browsing, changesets fetching and diff viewing are implemented.
Only tested with local repositories.

Thanks to Ralph Vater for CVS specific code.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@559 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-06-12 20:12:05 +00:00
Jean-Philippe Lang
4dddb606a6 Added :dependent => :delete_all on IssueStatus Workflow association.
Also added compact in find_new_statuses_allowed_to to remove nil statuses.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@558 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-31 17:19:01 +00:00
Jean-Philippe Lang
22c7419a70 Japanese translation updated (Satoru KURASHIKI)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@557 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-30 17:52:24 +00:00
Jean-Philippe Lang
7fb03b1ca3 Fixed: error on csv/pdf export and feeds (oracle)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@556 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-29 22:20:43 +00:00
Jean-Philippe Lang
97f6315bd0 Fixed: SQL error when a non-admin user displays the project list
git-svn-id: http://redmine.rubyforge.org/svn/trunk@555 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-29 19:48:50 +00:00
Jean-Philippe Lang
e7ff47cff5 Fixed date query filters (wrong results and sql error with postgresql)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@554 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-29 19:40:48 +00:00
Jean-Philippe Lang
4a20ece43e Added Swedish translation supplied by Thomas Habets.
English email templates suffix removed so that they are used when translated template is not available.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@553 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-29 17:57:10 +00:00
Jean-Philippe Lang
b2d4666bf1 Attachments can be displayed inline in Documents
git-svn-id: http://redmine.rubyforge.org/svn/trunk@552 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-28 12:15:19 +00:00
Jean-Philippe Lang
515caa8f37 Fixed: open/closed issue counts are always 0 on reports view (postgresql)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@551 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-28 09:09:37 +00:00
Jean-Philippe Lang
4cf1b68969 Added filename header when sending an image inline
git-svn-id: http://redmine.rubyforge.org/svn/trunk@550 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-27 18:45:48 +00:00
Jean-Philippe Lang
413247ee5b Added the ability to archive projects:
* Only administrators can archive/unarchive projects.
* Once archived, the project is visible on the admin project listing only. It doesn't show up anywhere else in the app. Subprojects are also archived.
* Archive/unarchive preserve everything on the project (issues, members, ...).
* A subproject can not be unarchived if its parent project is archived.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@549 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-27 17:42:04 +00:00
Jean-Philippe Lang
70374d084e Slight views refactoring
git-svn-id: http://redmine.rubyforge.org/svn/trunk@548 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-27 12:38:29 +00:00
Jean-Philippe Lang
f335e943ad Password fields hidden on users/add form when selecting an external authentication mode.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@547 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-27 11:57:49 +00:00
Jean-Philippe Lang
f04225321c Account information can now be sent to the user when creating an account.
ActionMailer logger set to nil for production environment to disable email contents output in production.log

git-svn-id: http://redmine.rubyforge.org/svn/trunk@546 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-27 10:57:13 +00:00
Jean-Philippe Lang
f12315075f Optimistic locking added for wiki edits.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@545 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-26 17:22:27 +00:00
Jean-Philippe Lang
c99da15445 Show a 404 error page if attachment can not be read.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@544 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-26 16:57:25 +00:00
Jean-Philippe Lang
ef39db234b Added issue count details for versions on Reports view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@543 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-26 16:06:02 +00:00
Jean-Philippe Lang
8af6d24a36 Fixed "Projects" drop-down menu for IE.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@542 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-26 15:53:15 +00:00
Jean-Philippe Lang
f8ef65e8f6 Attachments can now be added to wiki pages (original patch by Pavol Murin). Only authorized users can add/delete attachments.
Attached images can be displayed inline, using textile image tag (for wiki pages, issue descriptions and forum messages).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@541 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-26 15:42:37 +00:00
Jean-Philippe Lang
6446c312be Added the ability to destroy wiki pages (content and its history are deleted from the database).
This permission has to be explicitly given (Roles & Permissions -> Wiki pages/Delete).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@540 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-25 16:44:50 +00:00
Jean-Philippe Lang
4a524ff911 Dutch translation added (Linda van den Brink)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@539 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-24 17:29:11 +00:00
Jean-Philippe Lang
9fe0dd051d User's projects alphabetically sorted in the Projects drop down menu.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@538 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-23 17:18:21 +00:00
Jean-Philippe Lang
3782501275 Fixed a couple of spelling errors (JT Zemp)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@537 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-20 18:11:49 +00:00
Jean-Philippe Lang
d34ea9a569 Versions can now be created with no date.
Versions with no date appear at the end of the roadmap, sorted by name.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@536 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-20 17:46:02 +00:00
Jean-Philippe Lang
bb1fccb7b7 Fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000).
Also added pagination for changes on changeset details view.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@535 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-15 21:32:36 +00:00
Jean-Philippe Lang
777c9acae8 "My projects" are now listed under the drop-down "Projects" top menu item (20 projects max.).
The left menu section for "My projects" is removed.
Patch by Damien McKenna (slightly edited).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@534 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-15 17:39:01 +00:00
Jean-Philippe Lang
7b13b58a2f Text search added on messages.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@533 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-14 17:46:58 +00:00
Jean-Philippe Lang
b11dd1f213 Reply form on the message view is displayed only if user is logged in.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@532 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-14 17:07:40 +00:00
Jean-Philippe Lang
7ca7e4bad5 Added mail notification when a new message is posted in the forums.
Only users who "watch" the board receive notifications. To watch a board, go to the board and click on the "Watch" link.

Notifications are sent by MessageObserver observer.
GLoc was modified to use the mail template without language suffix when translated template (with language suffix) doesn't exist.
Usefull when there's no hard coded text in the mail tempate.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@531 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-14 17:03:59 +00:00
Jean-Philippe Lang
bca5bd9c62 Added watchers for message boards (watchers controller modified to support any watchable model).
No notification yet when a new message is posted.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@530 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-13 19:43:35 +00:00
Jean-Philippe Lang
b90e84b9fe Per project forums added.
Permissions for forums management can be set in "Admin -> Roles & Permissions".
Forums can be created on the project settings screen ("Forums" tab).
Once a project has a forum, a "Forums" link appears in the project menu.
For now, posting messages in forums requires to be logged in. Files can be attached to messages.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@529 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-13 17:09:56 +00:00
Jean-Philippe Lang
75582f80f8 Fixed: error when viewing a file diff from a revision view (only if repository url doesn't point to the root of the repository).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@528 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-10 18:39:49 +00:00
Jean-Philippe Lang
270e3a4345 Added "assigned to" field in mail notifications.
Also fixed mail_handler unit test (wrong fixture filename).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@527 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-10 17:46:16 +00:00
Jean-Philippe Lang
0fab627a3c Added some javascript to prevent from selecting the same from/to revision for the diff view.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@526 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-10 17:36:45 +00:00
Jean-Philippe Lang
599e49e4d0 Added the ability to view a file diff with free to/from revision selection.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@525 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-10 17:28:18 +00:00
Jean-Philippe Lang
cff3950e6b Added a test for the mail handler.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@524 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-09 19:24:07 +00:00
Jean-Philippe Lang
42193960f2 Added a simple mail handler.
It lets users add notes to an existing issue by replying to the initial notification email.
Permissions are checked in the same way as in the application (the user is identified by its mail address).

Information about configuring the application so that it receives emails can be found here:
http://wiki.rubyonrails.com/rails/pages/HowToReceiveEmailsWithActionMailer

RedMine mail hander is MailHandler#receive

git-svn-id: http://redmine.rubyforge.org/svn/trunk@523 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-09 18:46:43 +00:00
Jean-Philippe Lang
98d08439dc Fixed: files with an apostrophe in their names can't be accessed in the repository
git-svn-id: http://redmine.rubyforge.org/svn/trunk@522 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-09 17:22:15 +00:00
Jean-Philippe Lang
8614c00c8a Text files can now be viewed online when browsing the repository.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@521 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-08 15:49:20 +00:00
Jean-Philippe Lang
df0a49ff14 "me" value is now available in queries for "assigned to" and "author" filters.
When executing the query, it is replaced by the currently logged in user.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@520 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-08 12:46:15 +00:00
Jean-Philippe Lang
50429d0819 Fixed the order of the japanese day names
git-svn-id: http://redmine.rubyforge.org/svn/trunk@519 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-08 11:51:20 +00:00
Jean-Philippe Lang
5c88c1f50b Changed the length of 'language' column in users table from 2 to 5, to allow long language codes like pt-br.
Updated portuguese translation (Arthur Zapparoli).
Current pt translation moved to pt-br.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@518 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-08 09:26:57 +00:00
Jean-Philippe Lang
d85f5518d9 Removed "Wiki edits" option in the activity view if the project has no wiki.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@517 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-07 16:57:42 +00:00
Jean-Philippe Lang
8e24c6458d Added an option to see all versions in the roadmap view (including completed ones).
On calendar and gantt, versions are now clickable and link to the corresponding entry in the roadmap.

Since calendar and gantt are now cached, don't forget to empty your cache before restarting the application.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@516 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-07 16:54:26 +00:00
Jean-Philippe Lang
b748455d96 Added fragment caching for calendar and gantt views
git-svn-id: http://redmine.rubyforge.org/svn/trunk@515 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-06 16:40:33 +00:00
Jean-Philippe Lang
7eb6471559 Added autologin feature (disabled by default).
To enable this feature, go to administration settings and choose a duration for autologin.
When enabled, a checkbox on the login form lets users activate autologin.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@514 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-06 12:49:32 +00:00
Jean-Philippe Lang
56513a8c66 Added a "clear" link when displaying a saved query. It clears the query filter to show all issues.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@513 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 17:52:08 +00:00
Jean-Philippe Lang
8b1455c945 Filter values hidden by default to avoid show/hide flashes when loading screen.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@512 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 16:10:45 +00:00
Jean-Philippe Lang
b9be1d1268 Ajaxified issue filter reset link
git-svn-id: http://redmine.rubyforge.org/svn/trunk@511 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 15:59:06 +00:00
Jean-Philippe Lang
71e6f22670 Issue#long_id no more used and removed
git-svn-id: http://redmine.rubyforge.org/svn/trunk@510 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 15:55:50 +00:00
Jean-Philippe Lang
0dbbf776c6 Slight modifications on project settings views
git-svn-id: http://redmine.rubyforge.org/svn/trunk@509 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 15:35:28 +00:00
Jean-Philippe Lang
3eed7e622c Members management in project settings is now AJAXified
git-svn-id: http://redmine.rubyforge.org/svn/trunk@508 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 15:21:18 +00:00
Jean-Philippe Lang
a6a181c70c Issue filter no more reseted when clicking 'Issues' menu link
git-svn-id: http://redmine.rubyforge.org/svn/trunk@507 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 13:35:33 +00:00
Jean-Philippe Lang
92b02014d2 Issue relations first commit (not thoroughly tested). 4 kinds of relation are available:
* relates to: do nothing special. Just to know that the 2 issues are related...
* duplicates: will close the related issue with the same status when closing the issue (not implemented yet)
* blocks: will require to close the blocking issue before closing the blocked issue (not implemented yet)
* precedes (end to start relation): start date of the related issue depends on the due date of the preceding issue (implemented). A delay can be set so that the related issue can only start n days after the end of the preceding issue. When setting dates for an issue, dates of all downstream issues are set according to these relations.

To set a relation, the 2 issues have to belong to the same project (may change in the future). So if an issue is moved to another project, all its relations are removed.
Circular dependencies are checked when creating a relation.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@506 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 13:22:27 +00:00
Jean-Philippe Lang
987e843cd1 Removed alpha transparency from time icon
git-svn-id: http://redmine.rubyforge.org/svn/trunk@505 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 10:28:49 +00:00
Jean-Philippe Lang
5050586a9e Changed delete icon
git-svn-id: http://redmine.rubyforge.org/svn/trunk@504 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 10:06:13 +00:00
Jean-Philippe Lang
da4fd6b71a Login field automatically focused on login form
git-svn-id: http://redmine.rubyforge.org/svn/trunk@503 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-05 09:23:56 +00:00
Jean-Philippe Lang
e07c44543f Bulgarian translation added (Nikolay Solakov)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@502 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-02 19:36:00 +00:00
Jean-Philippe Lang
78b5e57a4a Fixed an unicode problem on gantt (first letter of the day name)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@501 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-02 19:13:38 +00:00
Jean-Philippe Lang
10a49d476d HTML select tags are no longer hidden with IE7 when showing navigation drop-down menu.
They are still hidden for IE6 (no z-index support for select tags).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@500 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-02 17:31:42 +00:00
Jean-Philippe Lang
0759212a44 Added fragment caching for svn diffs.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@499 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-01 20:56:19 +00:00
Jean-Philippe Lang
7eda64e464 Issue subjects column width set to 330 on gantt chart
git-svn-id: http://redmine.rubyforge.org/svn/trunk@498 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-01 08:48:24 +00:00
Jean-Philippe Lang
124cca3af0 Text formatting drop down disabled if RedCloth is not available (system settings).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@497 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-05-01 08:37:07 +00:00
Jean-Philippe Lang
7faf77804d Subproject name added in csv and pdf exports.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@496 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 20:18:28 +00:00
Jean-Philippe Lang
d94bcd285a A 403 error page is now displayed (instead of a blank page) when trying to access a protected page.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@495 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 19:47:28 +00:00
Jean-Philippe Lang
9af49e07f3 Added last commit message for each entry in repository browser.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@494 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 18:20:22 +00:00
Jean-Philippe Lang
0f0ab74560 Fixed: last day of the month not included in project activity
git-svn-id: http://redmine.rubyforge.org/svn/trunk@493 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 18:03:42 +00:00
Jean-Philippe Lang
5288b8550b Removed hard coded 'Search' string in base layout.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@492 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 14:17:20 +00:00
Jean-Philippe Lang
66d789229d Added a DropOut effect when removing a block on my page layout.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@491 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 09:09:45 +00:00
Jean-Philippe Lang
bba9be6a4d Removed an used li element in navigation menu.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@490 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 09:08:13 +00:00
Jean-Philippe Lang
ebe10fa645 Added a quick search form in page header. Search functionality moved to a dedicated controller.
When used:
* outside of a project: searches projects
* inside a project: searches issues, changesets, news, documents and wiki pages of the current project

If an issue number is given, user is redirected to the corresponding issue.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@489 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-30 08:52:39 +00:00
Jean-Philippe Lang
833c5035a6 Fixed comments header label on time log details
git-svn-id: http://redmine.rubyforge.org/svn/trunk@488 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-28 17:32:11 +00:00
Jean-Philippe Lang
aa3445bdb6 Moved functional tests for MyController
git-svn-id: http://redmine.rubyforge.org/svn/trunk@487 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-28 17:30:17 +00:00
Jean-Philippe Lang
66fe12db71 Fixed a TemplateError nil:NilClass (oracle specific) on _news.rhtml
git-svn-id: http://redmine.rubyforge.org/svn/trunk@486 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-27 19:34:31 +00:00
Jean-Philippe Lang
5db3396c07 Added an ajax indicator for all ajax calls. Also removed highlight effects on my page layout edition.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@485 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-27 19:32:57 +00:00
Jean-Philippe Lang
c1a18a2889 Subproject name displayed on issue list, calendar and gantt (only for issues that belong to a subproject).
git-svn-id: http://redmine.rubyforge.org/svn/trunk@484 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-27 16:21:53 +00:00
Jean-Philippe Lang
89db185726 Added a link on revision screen to see the entire diff for the revision.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@483 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-26 18:46:55 +00:00
Jean-Philippe Lang
a301d0eb4e Updated migration 41 to support table name prefix/sufix.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@482 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-25 16:48:01 +00:00
Jean-Philippe Lang
076655cdd8 Updated migration 41 to support table name prefix/sufix.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@481 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-25 16:47:29 +00:00
Jean-Philippe Lang
8e4a5996b0 Fixed: 9268 These files should be moved out of revision control
Renamed to *.example:
* config/database.yml
* public/dispatch.cgi
* public/dispatch.fcgi
* public/dispatch.rb

git-svn-id: http://redmine.rubyforge.org/svn/trunk@480 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-25 16:21:23 +00:00
Jean-Philippe Lang
52547466f0 Fixed: 10342 Creation of Schema in Oracle
Comment is a reserved keyword for Oracle. The five 'Comment' columns are renamed to 'Commments'.
Migration scripts were modified to let oracle users create the database. For the others, migration 41 will rename the columns (only if columns have the 'old' name).
Fixed also a few oracle specific issues.

Note: currently (in Rails 1.2.3), there's bug in Rails oracle adapter. See: http://dev.rubyonrails.org/ticket/7344
Attached patch is required for redMine to work properly.


git-svn-id: http://redmine.rubyforge.org/svn/trunk@479 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-25 15:06:20 +00:00
Jean-Philippe Lang
4967fa8733 Journal details truncated only if values are strings
git-svn-id: http://redmine.rubyforge.org/svn/trunk@478 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-24 18:26:42 +00:00
Jean-Philippe Lang
8065001c0d Fixed 10335 Error in journalizing an issue with longtext custom fields (Postgresql)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@477 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-24 16:17:57 +00:00
Jean-Philippe Lang
72a5839931 Removed an old, no longer used, model.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@476 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-24 15:53:00 +00:00
Jean-Philippe Lang
c995be94d9 Fixed: default status not showing in new issue
git-svn-id: http://redmine.rubyforge.org/svn/trunk@475 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-24 15:19:49 +00:00
Jean-Philippe Lang
018b81a46a Fixed 10337 Regression: Admin raises issue in project not assigned to
git-svn-id: http://redmine.rubyforge.org/svn/trunk@474 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-24 14:49:05 +00:00
Jean-Philippe Lang
941a240535 Commit messages are now scanned for referenced or fixed issue IDs.
Keywords and the status to apply to fixed issues can be defined in Admin -> Settings.

Default keywords:
- for referencing issues: refs, references, IssueID
- for fixing issues: fixes,closes
There's no default status defined for fixed issue. You'll have to specify it if you want to enable auto closure of issues.

Example of a working commit message: "This commit references #1, #2 and fixes #3"

git-svn-id: http://redmine.rubyforge.org/svn/trunk@473 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-24 13:57:27 +00:00
Jean-Philippe Lang
ed5b5d0559 Missing fav_off icon.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@472 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-23 19:56:13 +00:00
Jean-Philippe Lang
104b7d457d Removed some parentheses before arguments.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@471 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-23 16:46:04 +00:00
Jean-Philippe Lang
feb973b970 The ability to change the issue status to the same status is no longer forced.
This behaviour can be defined in workflow setup.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@470 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 22:20:09 +00:00
Jean-Philippe Lang
e02068eb2a Removed some parentheses before arguments.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@469 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 21:19:16 +00:00
Jean-Philippe Lang
eea1878a3b Italian translation update (Alessio Spadaro)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@468 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 20:29:00 +00:00
Jean-Philippe Lang
4499c7a030 Grey scale icon for issue not watched.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@467 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 20:03:08 +00:00
Jean-Philippe Lang
2f1d8630f1 Fixed: error on history atom feed when there's no notes on an issue change
git-svn-id: http://redmine.rubyforge.org/svn/trunk@466 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 12:50:26 +00:00
Jean-Philippe Lang
cf59496957 Fixed 10058: issue URL in repository comment break
git-svn-id: http://redmine.rubyforge.org/svn/trunk@465 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 10:58:15 +00:00
Jean-Philippe Lang
32de29ea35 Fixed 10061 problem with textilize and :hard_breaks (Pavol Murin)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@464 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 10:10:56 +00:00
Jean-Philippe Lang
8d7de50ca8 Fixed 10211 Wiki names can't have periods in them.
Same validations as WikiPage model now applied to wiki start page.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@463 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-22 09:50:16 +00:00
Jean-Philippe Lang
3d685f7bec Fixed: blank lines in left menu with IE
git-svn-id: http://redmine.rubyforge.org/svn/trunk@462 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 18:06:01 +00:00
Jean-Philippe Lang
05b08ae38f Right overflow of the left menu now hidden.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@461 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 17:11:30 +00:00
Jean-Philippe Lang
623d2f25b1 Projects menu item now shows the list of public projects and projects for which the user is a member (marked with a star).
If current user is admin, private projects are also listed.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@460 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 16:40:56 +00:00
Jean-Philippe Lang
634d3557f2 Online help japanese translation added (Ken Date)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@459 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 16:11:12 +00:00
Jean-Philippe Lang
6957eceee0 Added portuguese translation (Joao Carlos Clementoni).
Email templates copied from english.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@458 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 16:02:28 +00:00
Jean-Philippe Lang
5097f6394d Fixed: error when using a custom query feed with custom field filter.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@457 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 13:16:04 +00:00
Jean-Philippe Lang
77cfc1cc59 Added a new block available for my page: "Watched issues"
git-svn-id: http://redmine.rubyforge.org/svn/trunk@456 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 12:28:22 +00:00
Jean-Philippe Lang
5d8200b9fc Added missing parentheses on news/show and removed useless call to auto_link
git-svn-id: http://redmine.rubyforge.org/svn/trunk@455 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 12:10:50 +00:00
Jean-Philippe Lang
18255881ca Added create_watchers migration
git-svn-id: http://redmine.rubyforge.org/svn/trunk@454 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 12:09:07 +00:00
Jean-Philippe Lang
2fb84af3e9 Added "Watch" functionality on issues. It allows users to receive mail notifications about issue changes.
For now, it's only usefull for users who are not members of the project, since members receive notifications for each issue (this behaviour will change).

git-svn-id: http://redmine.rubyforge.org/svn/trunk@453 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 12:08:31 +00:00
Jean-Philippe Lang
907f906ec6 Fixed: error when exporting to PDF an issue list using a custom field filter.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@452 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-21 08:21:23 +00:00
Jean-Philippe Lang
acfb87a07b News box on project/show is hidden if there is no news to display.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@451 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-20 10:26:57 +00:00
Jean-Philippe Lang
c10541feb7 Removed some spaces before argument parentheses.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@450 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-20 10:23:25 +00:00
Jean-Philippe Lang
e72778ea84 Removed some spaces before argument parentheses.
git-svn-id: http://redmine.rubyforge.org/svn/trunk@449 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-20 09:53:44 +00:00
Jean-Philippe Lang
a824017d8c Checkboxes on the workflow screen diagonal no longer disabled
git-svn-id: http://redmine.rubyforge.org/svn/trunk@448 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-17 11:02:46 +00:00
Jean-Philippe Lang
d570bc5cc5 Custom fields for issues can now be used as filters on issue list.
To use a custom field as a filter, check "Used as a filter" on the custom field edit screen.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@447 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-17 10:53:20 +00:00
Jean-Philippe Lang
559b2069ac User#<=> modified to sort on firstname if same lastname
git-svn-id: http://redmine.rubyforge.org/svn/trunk@446 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-12 16:49:02 +00:00
Jean-Philippe Lang
6c5e89ede0 Redmine::VERSION updated
git-svn-id: http://redmine.rubyforge.org/svn/trunk@445 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-04-11 16:38:19 +00:00
869 changed files with 43146 additions and 9162 deletions

View File

@@ -20,6 +20,6 @@ class SysApi < ActionWebService::API::Base
:expects => [],
:returns => [[Project]]
api_method :repository_created,
:expects => [:int, :string],
:expects => [:string, :string],
:returns => [:int]
end

View File

@@ -21,13 +21,17 @@ 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]
before_filter :require_login, :only => :logout
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
# Show user's account
def show
@user = User.find(params[:id])
@user = User.find_active(params[:id])
@custom_values = @user.custom_values.find(:all, :include => :custom_field)
# 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))
end
rescue ActiveRecord::RecordNotFound
render_404
end
@@ -36,31 +40,38 @@ class AccountController < ApplicationController
def login
if request.get?
# Logout user
self.logged_in_user = nil
self.logged_user = nil
else
# Authenticate user
user = User.try_to_login(params[:login], params[:password])
if user
self.logged_in_user = user
self.logged_user = user
# generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin?
token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end
redirect_back_or_default :controller => 'my', :action => 'page'
else
flash.now[:notice] = l(:notice_account_invalid_creditentials)
flash.now[:error] = l(:notice_account_invalid_creditentials)
end
end
end
# Log out current user and redirect to welcome page
def logout
self.logged_in_user = nil
redirect_to :controller => 'welcome'
cookies.delete :autologin
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
self.logged_user = nil
redirect_to home_url
end
# Enable user to choose a new password
def lost_password
redirect_to :controller => 'welcome' and return unless Setting.lost_password?
redirect_to(home_url) && return unless Setting.lost_password?
if params[:token]
@token = Token.find_by_action_and_value("recovery", params[:token])
redirect_to :controller => 'welcome' and return unless @token and !@token.expired?
redirect_to(home_url) && return unless @token and !@token.expired?
@user = @token.user
if request.post?
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
@@ -77,9 +88,9 @@ class AccountController < ApplicationController
if request.post?
user = User.find_by_mail(params[:mail])
# user not found in db
flash.now[:notice] = l(:notice_account_unknown_email) and return unless user
flash.now[:error] = l(:notice_account_unknown_email) and return unless user
# user uses an external authentification
flash.now[:notice] = l(:notice_can_t_change_password) and return if user.auth_source_id
flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
# create a new token for password recovery
token = Token.new(:user => user, :action => "recovery")
if token.save
@@ -94,38 +105,71 @@ class AccountController < ApplicationController
# User self-registration
def register
redirect_to :controller => 'welcome' and return unless Setting.self_registration?
if params[:token]
token = Token.find_by_action_and_value("register", params[:token])
redirect_to :controller => 'welcome' and return unless token and !token.expired?
user = token.user
redirect_to :controller => 'welcome' and 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
redirect_to(home_url) && return unless Setting.self_registration?
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 => 'welcome' and return
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)
User.current = user
session[:user_id] = user.id
else
User.current = User.anonymous
session[:user_id] = nil
end
end
end

View File

@@ -27,12 +27,18 @@ class AdminController < ApplicationController
def projects
sort_init 'name', 'asc'
sort_update
@project_count = Project.count
sort_update
@status = params[:status] ? params[:status].to_i : 0
conditions = nil
conditions = ["status=?", @status] unless @status == 0
@project_count = Project.count(:conditions => conditions)
@project_pages = Paginator.new self, @project_count,
15,
25,
params['page']
@projects = Project.find :all, :order => sort_clause,
:conditions => conditions,
:limit => @project_pages.items_per_page,
:offset => @project_pages.current.offset
@@ -40,21 +46,37 @@ class AdminController < ApplicationController
end
def mail_options
@actions = Permission.find(:all, :conditions => ["mail_option=?", true]) || []
@notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted)
if request.post?
@actions.each { |a|
a.mail_enabled = (params[:action_ids] || []).include? a.id.to_s
a.save
}
flash.now[:notice] = l(:notice_successful_update)
settings = (params[:settings] || {}).dup.symbolize_keys
settings[:notified_events] ||= []
settings.each { |name, value| Setting[name] = value }
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'admin', :action => 'mail_options'
end
end
def test_email
raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
# Force ActionMailer to raise delivery errors so we can catch it
ActionMailer::Base.raise_delivery_errors = true
begin
@test = Mailer.deliver_test(User.current)
flash[:notice] = l(:notice_email_sent, User.current.mail)
rescue Exception => e
flash[:error] = l(:notice_email_error, e.message)
end
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
redirect_to :action => 'mail_options'
end
def info
@db_adapter_name = ActiveRecord::Base.connection.adapter_name
@flags = Hash.new
@flags[:default_admin_changed] = User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?
@flags[:file_repository_writable] = File.writable?(Attachment.storage_path)
@flags[:textile_available] = ActionView::Helpers::TextHelper.method_defined? "textilize"
@flags = {
:default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
:file_repository_writable => File.writable?(Attachment.storage_path),
:rmagick_available => Object.const_defined?(:Magick)
}
@plugins = Redmine::Plugin.registered_plugins
end
end

View File

@@ -16,37 +16,44 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ApplicationController < ActionController::Base
before_filter :check_if_login_required, :set_localization
before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password
def logged_in_user=(user)
@logged_in_user = user
session[:user_id] = (user ? user.id : nil)
REDMINE_SUPPORTED_SCM.each do |scm|
require_dependency "repository/#{scm.underscore}"
end
def logged_in_user
def current_role
@current_role ||= User.current.role_for_project(@project)
end
def user_setup
Setting.check_cache
if session[:user_id]
@logged_in_user ||= User.find(session[:user_id])
# existing session
User.current = User.find(session[:user_id])
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature
User.current = 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
nil
User.current = User.anonymous
end
end
# Returns the role that the logged in user has on the current project
# or nil if current user is not a member of the project
def logged_in_user_membership
@user_membership ||= logged_in_user.role_for_project(@project)
end
# check if login is globally required to access the application
def check_if_login_required
# no check needed if user is already logged in
return true if User.current.logged?
require_login if Setting.login_required?
end
def set_localization
lang = begin
if self.logged_in_user and self.logged_in_user.language and !self.logged_in_user.language.empty? and GLoc.valid_languages.include? self.logged_in_user.language.to_sym
self.logged_in_user.language
if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym
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
@@ -60,7 +67,7 @@ class ApplicationController < ActionController::Base
end
def require_login
unless self.logged_in_user
if !User.current.logged?
store_location
redirect_to :controller => "account", :action => "login"
return false
@@ -70,39 +77,29 @@ class ApplicationController < ActionController::Base
def require_admin
return unless require_login
unless self.logged_in_user.admin?
render :nothing => true, :status => 403
if !User.current.admin?
render_403
return false
end
true
end
# authorizes the user for the requested action.
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action])
# check if action is allowed on public projects
if @project.is_public? and Permission.allowed_to_public "%s/%s" % [ ctrl, action ]
return true
end
# if action is not public, force login
return unless require_login
# admin is always authorized
return true if self.logged_in_user.admin?
# if not admin, check membership permission
if logged_in_user_membership and Permission.allowed_to_role( "%s/%s" % [ ctrl, action ], logged_in_user_membership )
return true
end
render :nothing => true, :status => 403
false
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : (User.current.logged? ? render_403 : require_login)
end
# 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
return true if @project.is_public?
return false unless logged_in_user
return true if logged_in_user.admin? || logged_in_user_membership
render :nothing => true, :status => 403
false
unless @project.active?
@project = nil
render_404
return 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.
@@ -121,11 +118,45 @@ class ApplicationController < ActionController::Base
end
end
def render_404
@html_title = "404"
render :template => "common/404", :layout => true, :status => 404
def render_403
@project = nil
render :template => "common/403", :layout => !request.xhr?, :status => 403
return false
end
def render_404
render :template => "common/404", :layout => !request.xhr?, :status => 404
return false
end
def render_feed(items, options={})
@items = items || []
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
@title = options[:title] || Setting.app_title
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end
def self.accept_key_auth(*actions)
actions = actions.flatten.map(&:to_s)
write_inheritable_attribute('accept_key_auth_actions', actions)
end
def accept_key_auth_actions
self.class.read_inheritable_attribute('accept_key_auth_actions') || []
end
# TODO: move to model
def attach_files(obj, files)
attachments = []
if files && files.is_a?(Array)
files.each do |file|
next unless file.size > 0
a = Attachment.create(:container => obj, :file => file, :author => User.current)
attachments << a unless a.new_record?
end
end
attachments
end
# qvalues http header parser
# code taken from webrick
@@ -145,4 +176,4 @@ class ApplicationController < ActionController::Base
end
return tmp
end
end
end

View File

@@ -0,0 +1,39 @@
# 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 AttachmentsController < ApplicationController
layout 'base'
before_filter :find_project, :check_project_privacy
def download
# images are sent inline
send_file @attachment.diskfile, :filename => @attachment.filename,
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
rescue
# in case the disk file was deleted
render_404
end
private
def find_project
@attachment = Attachment.find(params[:id])
@project = @attachment.project
rescue
render_404
end
end

View File

@@ -65,10 +65,10 @@ class AuthSourcesController < ApplicationController
@auth_method = AuthSource.find(params[:id])
begin
@auth_method.test_connection
flash[:notice] = l(:notice_successful_connection)
rescue => text
flash[:notice] = text
flash[:error] = "Unable to connect (#{text})"
end
flash[:notice] ||= l(:notice_successful_connection)
redirect_to :action => 'list'
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.
class BoardsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
helper :messages
include MessagesHelper
helper :sort
include SortHelper
helper :watchers
include WatchersHelper
def index
@boards = @project.boards
# show the board if there is only one
if @boards.size == 1
@board = @boards.first
show
end
end
def show
sort_init "#{Message.table_name}.updated_on", "desc"
sort_update
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, 25, 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 => !request.xhr?
end
verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
def new
@board = Board.new(params[:board])
@board.project = @project
if request.post? && @board.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end
end
def edit
if request.post? && @board.update_attributes(params[:board])
case params[:position]
when 'highest'; @board.move_to_top
when 'higher'; @board.move_higher
when 'lower'; @board.move_lower
when 'lowest'; @board.move_to_bottom
end if params[:position]
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end
end
def destroy
@board.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end
private
def find_project
@project = Project.find(params[:project_id])
@board = @project.boards.find(params[:id]) if params[:id]
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -25,7 +25,7 @@ class CustomFieldsController < ApplicationController
end
def list
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.type.to_s }
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name }
@tab = params[:tab] || 'IssueCustomField'
render :action => "list", :layout => false if request.xhr?
end
@@ -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,16 +57,31 @@ 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[:notice] = "Unable to delete custom field"
flash[:error] = "Unable to delete custom field"
redirect_to :action => 'list'
end
end

View File

@@ -39,20 +39,14 @@ class DocumentsController < ApplicationController
def download
@attachment = @document.attachments.find(params[:attachment_id])
@attachment.increment_download
send_file @attachment.diskfile, :filename => @attachment.filename
send_file @attachment.diskfile, :filename => @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_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
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

View File

@@ -59,12 +59,27 @@ class EnumerationsController < ApplicationController
end
end
def move
@enumeration = Enumeration.find(params[:id])
case params[:position]
when 'highest'
@enumeration.move_to_top
when 'higher'
@enumeration.move_higher
when 'lower'
@enumeration.move_lower
when 'lowest'
@enumeration.move_to_bottom
end if params[:position]
redirect_to :action => 'index'
end
def destroy
Enumeration.find(params[:id]).destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :action => 'list'
rescue
flash[:notice] = "Unable to delete enumeration"
flash[:error] = "Unable to delete enumeration"
redirect_to :action => 'list'
end
end

View File

@@ -1,96 +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])
# 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])
# 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_reported_issues))
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}
return true
end
end

View File

@@ -1,44 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 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 HelpController < ApplicationController
skip_before_filter :check_if_login_required
before_filter :load_help_config
# displays help page for the requested controller/action
def index
# select help page to display
if params[:ctrl] and @help_config['pages'][params[:ctrl]]
if params[:page] and @help_config['pages'][params[:ctrl]][params[:page]]
template = @help_config['pages'][params[:ctrl]][params[:page]]
else
template = @help_config['pages'][params[:ctrl]]['index']
end
end
# choose language according to available help translations
lang = (@help_config['langs'].include? current_language.to_s) ? current_language.to_s : @help_config['langs'].first
url = "/manual/#{lang}/" + (template || "index.html")
redirect_to(request.relative_url_root + url)
end
private
def load_help_config
@help_config = YAML::load(File.open("#{RAILS_ROOT}/config/help.yml"))
end
end

View File

@@ -18,6 +18,8 @@
class IssueCategoriesController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
verify :method => :post, :only => :destroy
def edit
if request.post? and @category.update_attributes(params[:category])
@@ -27,11 +29,17 @@ class IssueCategoriesController < ApplicationController
end
def destroy
@category.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
rescue
flash[:notice] = "Categorie can't be deleted"
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
@issue_count = @category.issues.size
if @issue_count == 0
# No issue assigned to this category
@category.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
elsif params[:todo]
reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign'
@category.destroy(reassign_to)
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
end
@categories = @project.issue_categories - [@category]
end
private

View File

@@ -0,0 +1,59 @@
# 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 IssueRelationsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
def new
@relation = IssueRelation.new(params[:relation])
@relation.issue_from = @issue
@relation.save if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js do
render :update do |page|
page.replace_html "relations", :partial => 'issues/relations'
if @relation.errors.empty?
page << "$('relation_delay').value = ''"
page << "$('relation_issue_to_id').value = ''"
end
end
end
end
end
def destroy
relation = IssueRelation.find(params[:id])
if request.post? && @issue.relations.include?(relation)
relation.destroy
@issue.reload
end
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} }
end
end
private
def find_project
@issue = Issue.find(params[:issue_id])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -79,7 +79,7 @@ class IssueStatusesController < ApplicationController
IssueStatus.find(params[:id]).destroy
redirect_to :action => 'list'
rescue
flash[:notice] = "Unable to delete issue status"
flash[:error] = "Unable to delete issue status"
redirect_to :action => 'list'
end
end

View File

@@ -16,144 +16,224 @@
# 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
layout 'base'
before_filter :find_project, :authorize, :except => [:index, :changes, :preview]
before_filter :find_optional_project, :only => [:index, :changes]
accept_key_auth :index, :changes
cache_sweeper :issue_sweeper, :only => [ :edit, :change_status, :destroy ]
helper :projects
include ProjectsHelper
helper :custom_fields
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issue_relations
include IssueRelationsHelper
helper :watchers
include WatchersHelper
helper :attachments
include AttachmentsHelper
helper :queries
helper :sort
include SortHelper
include IssuesHelper
def index
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
if @query.valid?
limit = %w(pdf csv).include?(params[:format]) ? Setting.issues_export_limit.to_i : 25
@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, :fixed_version ],
:conditions => @query.statement,
: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
end
def changes
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
if @query.valid?
@changes = 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'
end
def show
@status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
@custom_values = @issue.custom_values.find(:all, :include => :custom_field)
@journals_count = @issue.journals.count
@journals = @issue.journals.find(:all, :include => [:user, :details], :limit => 15, :order => "#{Journal.table_name}.created_on desc")
end
def history
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on desc")
@journals_count = @journals.length
end
def export_pdf
@custom_values = @issue.custom_values.find(:all, :include => :custom_field)
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "#{@project.name}_#{@issue.long_id}.pdf"
@custom_values = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
end
def edit
@priorities = Enumeration::get_values('IPRI')
@custom_values = []
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)
@issue.init_journal(User.current)
# Retrieve custom fields and values
@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 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 :action => 'show', :id => @issue
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:notice] = l(:notice_locking_conflict)
flash[:error] = l(:notice_locking_conflict)
end
end
end
def add_note
unless params[:notes].empty?
journal = @issue.init_journal(self.logged_in_user, params[:notes])
if @issue.save
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
return
end
journal = @issue.init_journal(User.current, params[:notes])
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
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
render :action => '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
@status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)
@new_status = IssueStatus.find(params[:new_status_id])
if params[:confirm]
begin
journal = @issue.init_journal(self.logged_in_user, params[:notes])
journal = @issue.init_journal(User.current, 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
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
# Log time
if current_role.allowed_to?(:log_time)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
@time_entry.save
end
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
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[:notice] = l(:notice_locking_conflict)
flash[:error] = l(:notice_locking_conflict)
end
end
@assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user }
@activities = Enumeration::get_values('ACTI')
end
def destroy
@issue.destroy
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project
end
def add_attachment
# Save the attachments
@attachments = []
journal = @issue.init_journal(self.logged_in_user)
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
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
journal.save if journal.details.any?
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
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)
journal.save
redirect_to :action => 'show', :id => @issue
end
# Send the file in stream mode
def download
@attachment = @issue.attachments.find(params[:attachment_id])
send_file @attachment.diskfile, :filename => @attachment.filename
rescue
render_404
def context_menu
@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),
:copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => User.current.allowed_to?(:delete_issues, @project)}
render :layout => false
end
def preview
issue = Issue.find_by_id(params[:id])
@attachements = issue.attachments if issue
@text = params[:issue][:description]
render :partial => 'common/preview'
end
private
def find_project
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
@html_title = "#{@project.name} - #{@issue.tracker.name} ##{@issue.id}"
rescue ActiveRecord::RecordNotFound
render_404
end
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[:query_id]
@query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)})
session[:query] = @query
else
if params[:set_filter] or !session[:query] or session[:query].project != @project
# 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] = @query
else
@query = session[:query]
end
end
end
end

View File

@@ -17,23 +17,43 @@
class MembersController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
before_filter :find_member, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize
def new
@project.members << Member.new(params[:member]) if request.post?
respond_to do |format|
format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end
end
def edit
if request.post? and @member.update_attributes(params[:member])
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project
respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end
end
end
def destroy
@member.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project
respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end
end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_member
@member = Member.find(params[:id])
@project = @member.project
rescue ActiveRecord::RecordNotFound

View File

@@ -0,0 +1,98 @@
# 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 MessagesController < ApplicationController
layout 'base'
before_filter :find_board, :only => :new
before_filter :find_message, :except => :new
before_filter :authorize
verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
helper :attachments
include AttachmentsHelper
# Show a topic and its replies
def show
@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 = 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
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 = User.current
@reply.board = @board
@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
private
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
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -16,11 +16,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MyController < ApplicationController
helper :issues
layout 'base'
before_filter :require_login
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
'issuesreportedbyme' => :label_reported_issues,
'issueswatched' => :label_watched_issues,
'news' => :label_news_latest,
'calendar' => :label_calendar,
'documents' => :label_document_plural
@@ -41,44 +44,65 @@ 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
# Edit user's account
def account
@user = self.logged_in_user
@user = User.current
@pref = @user.pref
@user.attributes = params[:user]
@user.pref.attributes = params[:pref]
if request.post? and @user.save and @user.pref.save
set_localization
flash.now[:notice] = l(:notice_account_updated)
self.logged_in_user.reload
end
end
# Change user's password
def change_password
@user = self.logged_in_user
flash[:notice] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id
if @user.check_password?(params[:password])
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
if request.post?
@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
flash[:notice] = l(:notice_account_password_updated)
else
render :action => 'account'
@user.pref.save
@user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
set_language_if_valid @user.language
flash[:notice] = l(:notice_account_updated)
redirect_to :action => 'account'
return
end
else
flash[:notice] = l(:notice_account_wrong_password)
end
@notification_options = [[l(:label_user_mail_option_all), 'all'],
[l(:label_user_mail_option_none), 'none']]
# Only users that belong to more than 1 project can select projects for which they are notified
# Note that @user.membership.size would fail since AR ignores :include association option when doing a count
@notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1
@notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
end
# Manage user's password
def password
@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])
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
if @user.save
flash[:notice] = l(:notice_account_password_updated)
redirect_to :action => 'account'
end
else
flash[:error] = l(:notice_account_wrong_password)
end
end
end
# Create a new feeds key
def reset_rss_key
if request.post? && User.current.rss_token
User.current.rss_token.destroy
flash[:notice] = l(:notice_feeds_access_key_reseted)
end
redirect_to :action => 'account'
end
# 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] ||= [] }
@@ -92,7 +116,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
@@ -127,7 +151,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,8 +17,22 @@
class NewsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
before_filter :find_project, :authorize, :except => :index
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
end
@@ -31,7 +45,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,7 +61,7 @@ class NewsController < ApplicationController
def destroy
@news.destroy
redirect_to :controller => 'projects', :action => 'list_news', :id => @project
redirect_to :action => 'index', :project_id => @project
end
private
@@ -56,5 +70,13 @@ private
@project = @news.project
rescue ActiveRecord::RecordNotFound
render_404
end
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,12 +15,16 @@
# 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, :authorize, :except => [ :index, :list, :add ]
before_filter :require_admin, :only => [ :add, :destroy ]
before_filter :find_project, :except => [ :index, :list, :add ]
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
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
@@ -28,52 +32,46 @@ class ProjectsController < ApplicationController
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issues
helper IssuesHelper
helper :queries
include QueriesHelper
helper :repositories
include RepositoriesHelper
include ProjectsHelper
def index
list
render :action => 'list' unless request.xhr?
end
# Lists public projects
# Lists visible projects
def list
sort_init "#{Project.table_name}.name", "asc"
sort_update
@project_count = Project.count(:all, :conditions => ["is_public=?", true])
@project_pages = Paginator.new self, @project_count,
15,
params['page']
@projects = Project.find :all, :order => sort_clause,
:conditions => ["#{Project.table_name}.is_public=?", true],
:include => :parent,
:limit => @project_pages.items_per_page,
:offset => @project_pages.current.offset
render :action => "list", :layout => false if request.xhr?
projects = Project.find :all,
: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]}
end
# Add a new project
def add
@custom_fields = IssueCustomField.find(:all)
@root_projects = Project.find(:all, :conditions => "parent_id is null")
@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
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"][x.id.to_s]) }
@project.custom_values = @custom_values
if params[:repository_enabled] && params[:repository_enabled] == "1"
@project.repository = Repository.new
@project.repository.attributes = params[:repository]
end
if "1" == params[:wiki_enabled]
@project.wiki = Wiki.new
@project.wiki.attributes = params[:wiki]
end
@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
if @project.save
@project.enabled_module_names = params[:enabled_modules]
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
end
@@ -82,23 +80,28 @@ 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.children if @project.children.size > 0
@subprojects = @project.active_children
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = Tracker.find(:all, :order => 'position')
@trackers = @project.trackers
@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)
@key = User.current.rss_key
end
def settings
@root_projects = Project::find(:all, :conditions => ["parent_id is null 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
@roles = Role.find(:all, :order => 'position')
@users = User.find_active(:all) - @project.users
@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
# Edit @project
@@ -106,27 +109,9 @@ class ProjectsController < ApplicationController
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
if params[:repository_enabled]
case params[:repository_enabled]
when "0"
@project.repository = nil
when "1"
@project.repository ||= Repository.new
@project.repository.update_attributes params[:repository]
end
end
if params[:wiki_enabled]
case params[:wiki_enabled]
when "0"
@project.wiki.destroy if @project.wiki
when "1"
@project.wiki ||= Wiki.new
@project.wiki.update_attributes params[:wiki]
end
end
@project.attributes = params[:project]
if @project.save
flash[:notice] = l(:notice_successful_update)
@@ -137,28 +122,51 @@ class ProjectsController < ApplicationController
end
end
end
def modules
@project.enabled_module_names = params[:enabled_modules]
redirect_to :action => 'settings', :id => @project, :tab => 'modules'
end
def archive
@project.archive if request.post? && @project.active?
redirect_to :controller => 'admin', :action => 'projects'
end
def unarchive
@project.unarchive if request.post? && !@project.active?
redirect_to :controller => 'admin', :action => 'projects'
end
# Delete @project
def destroy
@project_to_destroy = @project
if request.post? and params[:confirm]
@project.destroy
@project_to_destroy.destroy
redirect_to :controller => 'admin', :action => 'projects'
end
# hide project in layout
@project = nil
end
# Add a new issue category to @project
def add_issue_category
if request.post?
@issue_category = @project.issue_categories.build(params[:issue_category])
if @issue_category.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'categories', :id => @project
else
settings
render :action => 'settings'
@category = @project.issue_categories.build(params[:category])
if request.post? and @category.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'categories', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_category_id",
content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
}
end
end
end
end
end
# Add a new version to @project
def add_version
@@ -169,286 +177,191 @@ class ProjectsController < ApplicationController
end
end
# Add a new member to @project
def add_member
@member = @project.members.build(params[:member])
if request.post?
if @member.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'members', :id => @project
else
settings
render :action => 'settings'
end
end
end
# Show members list of @project
def list_members
@members = @project.members.find(:all)
end
# Add a new document to @project
def add_document
@categories = Enumeration::get_values('DCAT')
@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
attach_files(@document, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_add(@document) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
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
@documents = @project.documents.find :all, :include => :category
end
# Add a new issue to @project
def add_issue
@tracker = Tracker.find(params[:tracker_id])
@priorities = Enumeration::get_values('IPRI')
default_status = IssueStatus.default
@issue = Issue.new(:project => @project, :tracker => @tracker)
@issue.status = default_status
@allowed_statuses = 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 = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) }
@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
@issue.attributes = params[:issue]
requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
@issue.author_id = self.logged_in_user.id if self.logged_in_user
# Multiple file upload
@attachments = []
params[:attachments].each { |a|
@attachments << Attachment.new(:container => @issue, :file => a, :author => logged_in_user) unless a.size == 0
} if params[:attachments] and params[:attachments].is_a? Array
@custom_values = @project.custom_fields_for_issues(@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
@attachments.each(&:save)
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'list_issues', :id => @project
end
end
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
@grouped = documents.group_by(&:category)
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 ],
:conditions => @query.statement,
:limit => @issue_pages.items_per_page,
:offset => @issue_pages.current.offset
end
@trackers = Tracker.find :all, :order => 'position'
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, {:custom_values => :custom_field} ],
:conditions => @query.statement,
:limit => Setting.issues_export_limit
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_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| ic.iconv(c) }
# csv lines
@issues.each do |issue|
fields = [issue.id, issue.status.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| ic.iconv(c.to_s) }
end
end
export.rewind
send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
# 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 ||= @project.trackers.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(User.current.role_for_project(@project), @issue.tracker))
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
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 => 'index', :project_id => @project
return
end
end
@priorities = Enumeration::get_values('IPRI')
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 ],
:conditions => @query.statement,
:limit => Setting.issues_export_limit
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "export.pdf"
render :layout => false
# 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 :controller => 'issues', :action => 'index', :project_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
redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
@projects = []
# find projects to which the user is allowed to move the issue
@logged_in_user.memberships.each {|m| @projects << m.project if Permission.allowed_to_role("projects/move_issues", m.role)}
# 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(params[:new_project_id])
new_tracker = Tracker.find(params[:new_tracker_id])
@issues.each { |i|
# project dependent properties
unless i.project_id == new_project.id
i.category = nil
i.fixed_version = nil
end
# move the issue
i.project = new_project
i.tracker = new_tracker
i.save
}
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list_issues', :id => @project
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
else
User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
end
@target_project = @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
end
def add_query
@query = Query.new(params[:query])
@query.project = @project
@query.user = logged_in_user
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
if request.post? and @query.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'reports', :action => 'issue_report', :id => @project
end
render :layout => false if request.xhr?
end
# Add a news to @project
def add_news
@news = News.new(:project => @project)
@news = News.new(:project => @project, :author => User.current)
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)
redirect_to :action => 'list_news', :id => @project
Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
redirect_to :controller => 'news', :action => 'index', :project_id => @project
end
end
end
# Show news list of @project
def list_news
@news_pages, @news = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "#{News.table_name}.created_on DESC"
render :action => "list_news", :layout => false if request.xhr?
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_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
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
@versions = @project.versions.sort
end
def list_files
@versions = @project.versions
@versions = @project.versions.sort
end
# Show changelog for @project
def changelog
@trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
retrieve_selected_tracker_ids(@trackers)
@fixed_issues = @project.issues.find(:all,
:include => [ :fixed_version, :status, :tracker ],
:conditions => [ "#{IssueStatus.table_name}.is_closed=? and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}) and #{Issue.table_name}.fixed_version_id is not null", true],
:order => "#{Version.table_name}.effective_date DESC, #{Issue.table_name}.id DESC"
) unless @selected_tracker_ids.empty?
@fixed_issues ||= []
@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.find(:all,
:conditions => [ "#{Version.table_name}.effective_date>?", Date.today],
:order => "#{Version.table_name}.effective_date ASC"
)
@versions = @project.versions.sort
@versions = @versions.select {|v| !v.completed? } unless params[:completed]
end
def activity
@@ -461,79 +374,77 @@ class ProjectsController < ApplicationController
@year ||= Date.today.year
@month ||= Date.today.month
@date_from = Date.civil(@year, @month, 1)
@date_to = (@date_from >> 1)-1
@events_by_day = {}
unless params[:show_issues] == "0"
@project.issues.find(:all, :include => [:author], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_issues = 1
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
end
unless params[:show_news] == "0"
@project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_news = 1
@event_types = %w(issues news files documents changesets wiki_pages messages)
@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)}
@scope = @event_types.select {|t| params["show_#{t}"]}
# default events if none is specified in parameters
@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] )
@events += @project.issues_status_changes(@date_from, @date_to)
end
unless params[:show_files] == "0"
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 ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_files = 1
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 )
end
unless params[:show_documents] == "0"
@project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
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 ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_documents = 1
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 )
end
unless params[:show_wiki_edits] == "0"
select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comment, " +
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title"
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 )
end
if @scope.include?('wiki_pages')
select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
"#{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]
WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions).each { |i|
# We provide this alias so all events can be treated in the same manner
def i.created_on
self.updated_on
end
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_wiki_edits = 1
@events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
end
unless @project.repository.nil? || params[:show_changesets] == "0"
@project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]).each { |i|
def i.created_on
self.committed_on
end
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_changesets = 1
if @scope.include?('changesets')
@events += Changeset.find(:all, :include => :repository, :conditions => ["#{Repository.table_name}.project_id = ? AND #{Changeset.table_name}.committed_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
end
render :layout => false if request.xhr?
if @scope.include?('messages')
@events += Message.find(:all,
:include => [:board, :author],
:conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
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)}") }
end
end
def calendar
@@ -547,26 +458,18 @@ class ProjectsController < ApplicationController
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
@date_from = Date.civil(@year, @month, 1)
@date_to = (@date_from >> 1)-1
# start on monday
@date_from = @date_from - (@date_from.cwday-1)
# finish on sunday
@date_to = @date_to + (7-@date_to.cwday)
@events = []
events = []
@project.issues_with_subprojects(params[:with_subprojects]) do
@events += Issue.find(:all,
:include => [:tracker, :status, :assigned_to, :priority],
:conditions => ["((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)) and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", @date_from, @date_to, @date_from, @date_to]
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?
end
@events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
@ending_events_by_days = @events.group_by {|event| event.due_date}
@starting_events_by_days = @events.group_by {|event| event.start_date}
events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
render :layout => false if request.xhr?
end
@@ -583,12 +486,20 @@ class ProjectsController < ApplicationController
@month_from = 1
end
else
@month_from ||= (Date.today << 1).month
@year_from ||= (Date.today << 1).year
@month_from ||= Date.today.month
@year_from ||= Date.today.year
end
@zoom = (params[:zoom].to_i > 0 and params[:zoom].to_i < 5) ? params[:zoom].to_i : 2
@months = (params[:months].to_i > 0 and params[:months].to_i < 25) ? params[:months].to_i : 6
zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
@zoom = (zoom > 0 && zoom < 5) ? zoom : 2
months = (params[:months] || User.current.pref[:gantt_months]).to_i
@months = (months > 0 && months < 25) ? months : 6
# Save gantt paramters as user preference (zoom and months count)
if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
User.current.preference.save
end
@date_from = Date.civil(@year_from, @month_from, 1)
@date_to = (@date_from >> @months) - 1
@@ -597,60 +508,32 @@ class ProjectsController < ApplicationController
@project.issues_with_subprojects(params[:with_subprojects]) do
@events += Issue.find(:all,
:order => "start_date, due_date",
:include => [:tracker, :status, :assigned_to, :priority],
: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?
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[:output]=='pdf'
if params[:format]=='pdf'
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "gantt.pdf"
@options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
render :template => "projects/gantt.rfpdf", :layout => false
elsif params[:format]=='png' && respond_to?('gantt_image')
image = gantt_image(@events, @date_from, @months, @zoom)
image.format = 'PNG'
send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
else
render :template => "projects/gantt.rhtml"
end
end
def search
@question = params[:q] || ""
@question.strip!
@all_words = params[:all_words] || (params[:submit] ? false : true)
@scope = params[:scope] || (params[:submit] ? [] : %w(issues changesets news documents wiki) )
# tokens must be at least 3 character long
@tokens = @question.split.uniq.select {|w| w.length > 2 }
if !@tokens.empty?
# no more than 5 tokens to search for
@tokens.slice! 5..-1 if @tokens.size > 5
# strings used in sql like statement
like_tokens = @tokens.collect {|w| "%#{w}%"}
operator = @all_words ? " AND " : " OR "
limit = 10
@results = []
@results += @project.issues.find(:all, :limit => limit, :include => :author, :conditions => [ (["(LOWER(subject) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'issues'
@results += @project.news.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort], :include => :author ) if @scope.include? 'news'
@results += @project.documents.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'documents'
@results += @project.wiki.pages.find(:all, :limit => limit, :include => :content, :conditions => [ (["(LOWER(title) like ? OR LOWER(text) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @project.wiki && @scope.include?('wiki')
@results += @project.repository.changesets.find(:all, :limit => limit, :conditions => [ (["(LOWER(comment) like ?)"] * like_tokens.size).join(operator), * (like_tokens).sort] ) if @project.repository && @scope.include?('changesets')
@question = @tokens.join(" ")
else
@question = ""
end
end
def feeds
@queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)]
@key = logged_in_user.get_or_create_rss_key.value if logged_in_user
end
private
# Find project of id params[:id]
# if not found, redirect to project list
# Used as a before_filter
def find_project
@project = Project.find(params[:id])
@html_title = @project.name
rescue ActiveRecord::RecordNotFound
render_404
end
@@ -662,30 +545,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])
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 => "_")
@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

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# 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
@@ -17,8 +17,33 @@
class QueriesController < ApplicationController
layout 'base'
before_filter :require_login, :find_query
before_filter :find_project, :authorize
def index
@queries = @project.queries.find(:all,
:order => "name ASC",
:conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
end
def new
@query = Query.new(params[:query])
@query.project = @project
@query.user = User.current
@query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
@query.column_names = nil if params[:default_columns]
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
if request.post? && params[:confirm] && @query.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
return
end
render :layout => false if request.xhr?
end
def edit
if request.post?
@query.filters = {}
@@ -26,25 +51,30 @@ 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.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 => 'reports', :action => 'issue_report', :id => @project
redirect_to :controller => 'queries', :project_id => @project
end
private
def find_query
@query = Query.find(params[:id])
@project = @query.project
# check if user is allowed to manage queries (same permission as add_query)
authorize('projects', 'add_query')
def find_project
if params[:id]
@query = Query.find(params[:id])
@project = @query.project
render_403 unless @query.editable_by?(User.current)
else
@project = Project.find(params[:project_id])
end
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -25,10 +25,16 @@ 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"
when "version"
@field = "fixed_version_id"
@rows = @project.versions.sort
@data = issues_by_version
@report_title = l(:field_version)
render :template => "reports/issue_report_details"
when "priority"
@field = "priority_id"
@rows = Enumeration::get_values('IPRI')
@@ -49,23 +55,24 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details"
when "subproject"
@field = "project_id"
@rows = @project.children
@rows = @project.active_children
@data = issues_by_subproject
@report_title = l(:field_subproject)
render :template => "reports/issue_report_details"
else
@queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)]
@trackers = Tracker.find(:all, :order => 'position')
@trackers = @project.trackers
@versions = @project.versions.sort
@priorities = Enumeration::get_values('IPRI')
@categories = @project.issue_categories
@authors = @project.members.collect { |m| m.user }
@subprojects = @project.children
@subprojects = @project.active_children
issues_by_tracker
issues_by_version
issues_by_priority
issues_by_category
issues_by_author
issues_by_subproject
@total_hours = @project.time_entries.sum(:hours)
render :template => "reports/issue_report"
end
end
@@ -128,7 +135,22 @@ private
and i.project_id=#{@project.id}
group by s.id, s.is_closed, t.id")
end
def issues_by_version
@issues_by_version ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
v.id as fixed_version_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
where
i.status_id=s.id
and i.fixed_version_id=v.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, v.id")
end
def issues_by_priority
@issues_by_priority ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
@@ -184,8 +206,8 @@ private
#{Issue.table_name} i, #{IssueStatus.table_name} s
where
i.status_id=s.id
and i.project_id IN (#{@project.children.collect{|p| p.id}.join(',')})
group by s.id, s.is_closed, i.project_id") if @project.children.any?
and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
@issues_by_subproject ||= []
end
end

View File

@@ -17,66 +17,125 @@
require 'SVG/Graph/Bar'
require 'SVG/Graph/BarHorizontal'
require 'digest/sha1'
class ChangesetNotFound < Exception
end
class RepositoriesController < ApplicationController
layout 'base'
before_filter :find_project
before_filter :authorize, :except => [:stats, :graph]
before_filter :check_project_privacy, :only => [:stats, :graph]
before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit
before_filter :authorize
accept_key_auth :revisions
def edit
@repository = @project.repository
if !@repository
@repository = Repository.factory(params[:repository_scm])
@repository.project = @project
end
if request.post?
@repository.attributes = params[:repository]
@repository.save
end
render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
end
def destroy
@repository.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
end
def show
# get entries for the browse frame
@entries = @repository.scm.entries('')
show_error and return unless @entries
# check if new revisions have been committed in the repository
scm_latestrev = @entries.revisions.latest
if Setting.autofetch_changesets? && scm_latestrev && ((@repository.latest_changeset.nil?) || (@repository.latest_changeset.revision < scm_latestrev.identifier.to_i))
@repository.fetch_changesets
@repository.reload
end
@changesets = @repository.changesets.find(:all, :limit => 5, :order => "committed_on DESC")
@repository.fetch_changesets if Setting.autofetch_changesets?
# get entries for the browse frame
@entries = @repository.entries('')
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error and return unless @entries || @changesets.any?
end
def browse
@entries = @repository.scm.entries(@path, @rev)
show_error and return unless @entries
@entries = @repository.entries(@path, @rev)
if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
show_error unless @entries
end
end
def changes
@entry = @repository.scm.entry(@path, @rev)
show_error and return unless @entry
@changesets = @repository.changesets_for_path(@path)
end
def revisions
unless @path == ''
@entry = @repository.scm.entry(@path, @rev)
show_error and return unless @entry
@changeset_count = @repository.changesets.count
@changeset_pages = Paginator.new self, @changeset_count,
25,
params['page']
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
end
@repository.changesets_with_path @path do
@changeset_count = @repository.changesets.count
@changeset_pages = Paginator.new self, @changeset_count,
25,
params['page']
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset)
end
render :action => "revisions", :layout => false if request.xhr?
end
def entry
if 'raw' == params[:format]
content = @repository.scm.cat(@path, @rev)
show_error and return unless content
send_data content, :filename => @path.split('/').last
@content = @repository.scm.cat(@path, @rev)
show_error and return unless @content
if 'raw' == params[:format]
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
end
def annotate
@annotate = @repository.scm.annotate(@path, @rev)
show_error and return if @annotate.nil? || @annotate.empty?
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)
respond_to do |format|
format.html
format.js {render :layout => false}
end
rescue ChangesetNotFound
show_error
end
def diff
@rev_to = params[:rev_to] || (@rev-1)
type = params[:type] || 'inline'
@diff = @repository.scm.diff(params[:path], @rev, @rev_to, type)
show_error and return unless @diff
@rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
@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
end
end
def stats
@@ -101,28 +160,35 @@ class RepositoriesController < ApplicationController
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_repository
@project = Project.find(params[:id])
@repository = @project.repository
render_404 and return false unless @repository
@path = params[:path].squeeze('/').gsub(/^\//, '') if params[:path]
@path = params[:path].join('/') unless params[:path].nil?
@path ||= ''
@rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0
@rev = params[:rev].to_i if params[:rev]
rescue ActiveRecord::RecordNotFound
render_404
end
def show_error
flash.now[:notice] = l(:notice_scm_error)
flash.now[:error] = l(:notice_scm_error)
render :nothing => true, :layout => true
end
def graph_commits_per_month(repository)
@date_to = Date.today
@date_from = @date_to << 12
@date_from = @date_to << 11
@date_from = Date.civil(@date_from.year, @date_from.month, 1)
commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
commits_by_month = [0] * 12
commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
changes_by_day = repository.changes.count(:all, :group => :commit_date)
changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
changes_by_month = [0] * 12
changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
@@ -206,3 +272,9 @@ class Date
(date.year - self.year)*52 + (date.cweek - self.cweek)
end
end
class String
def with_leading_slash
starts_with?('/') ? self : "/#{self}"
end
end

View File

@@ -28,40 +28,36 @@ class RolesController < ApplicationController
end
def list
@role_pages, @roles = paginate :roles, :per_page => 25, :order => "position"
@role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
render :action => "list", :layout => false if request.xhr?
end
def new
@role = Role.new(params[:role])
if request.post?
@role.permissions = Permission.find(params[:permission_ids]) if params[:permission_ids]
if @role.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
# Prefills the form with 'Non member' role permissions
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
if request.post? && @role.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
@permissions = Permission.find(:all, :conditions => ["is_public=?", false], :order => 'sort ASC')
@permissions = @role.setable_permissions
end
def edit
@role = Role.find(params[:id])
if request.post? and @role.update_attributes(params[:role])
@role.permissions = Permission.find(params[:permission_ids] || [])
Permission.allowed_to_role_expired
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list'
end
@permissions = Permission.find(:all, :conditions => ["is_public=?", false], :order => 'sort ASC')
@permissions = @role.setable_permissions
end
def destroy
@role = Role.find(params[:id])
unless @role.members.empty?
flash[:notice] = 'Some members have this role. Can\'t delete it.'
else
#unless @role.members.empty?
# flash[:error] = 'Some members have this role. Can\'t delete it.'
#else
@role.destroy
end
#end
redirect_to :action => 'list'
end
@@ -93,21 +89,22 @@ class RolesController < ApplicationController
}
if @role.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'workflow', :role_id => @role, :tracker_id => @tracker
end
end
@roles = Role.find(:all, :order => 'position')
@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
@roles = Role.find(:all, :order => 'position')
@permissions = Permission.find :all, :conditions => ["is_public=?", false], :order => 'sort'
@roles = Role.find(:all, :order => 'builtin, position')
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
if request.post?
@roles.each do |role|
role.permissions = Permission.find(params[:permission_ids] ? (params[:permission_ids][role.id.to_s] || []) : [] )
role.permissions = params[:permissions][role.id.to_s]
role.save
end
Permission.allowed_to_role_expired
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list'
end

View File

@@ -0,0 +1,112 @@
# redMine - project management software
# Copyright (C) 2006 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 SearchController < ApplicationController
layout 'base'
helper :messages
include MessagesHelper
def index
@question = params[:q] || ""
@question.strip!
@all_words = params[:all_words] || (params[:submit] ? false : true)
@titles_only = !params[:titles_only].nil?
offset = nil
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(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)
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
@scope = @object_types.select {|t| params[t]}
@scope = @object_types if @scope.empty?
else
@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 = @tokens.uniq.select {|w| w.length > 2 }
if !@tokens.empty?
# no more than 5 tokens to search for
@tokens.slice! 5..-1 if @tokens.size > 5
# strings used in sql like statement
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
@results = []
limit = 10
if @project
@scope.each do |s|
@results += s.singularize.camelcase.constantize.search(like_tokens, @project,
:all_words => @all_words,
:titles_only => @titles_only,
:limit => (limit+1),
:offset => offset,
:before => params[:previous].nil?)
end
@results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
if params[:previous].nil?
@pagination_previous_date = @results[0].event_datetime if offset && @results[0]
if @results.size > limit
@pagination_next_date = @results[limit-1].event_datetime
@results = @results[0, limit]
end
else
@pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
if @results.size > limit
@pagination_previous_date = @results[-(limit)].event_datetime
@results = @results[-(limit), limit]
end
end
else
operator = @all_words ? ' AND ' : ' OR '
@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
else
@question = ""
end
render :layout => false if request.xhr?
end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -30,4 +30,16 @@ class SettingsController < ApplicationController
redirect_to :action => 'edit' and return
end
end
def plugin
plugin_id = params[:id].to_sym
@plugin = Redmine::Plugin.registered_plugins[plugin_id]
if request.post?
Setting["plugin_#{plugin_id}"] = params[:settings]
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => params[:id]
end
@partial = "../../vendor/plugins/#{plugin_id}/app/views/" + @plugin.settings[:partial]
@settings = Setting["plugin_#{plugin_id}"]
end
end

View File

@@ -22,18 +22,21 @@ class SysController < ActionController::Base
before_invocation :check_enabled
# Returns the projects list, with their repositories
def projects
Project.find(:all, :include => :repository)
end
def repository_created(project_id, url)
project = Project.find_by_id(project_id)
# Registers a repository for the given project identifier
# (Subversion specific)
def repository_created(identifier, url)
project = Project.find_by_identifier(identifier)
# Do not create the repository if the project has already one
return 0 unless project && project.repository.nil?
logger.debug "Repository for #{project.name} created"
repository = Repository.new(:project => project, :url => url)
repository.root_url = url
logger.debug "Repository for #{project.name} was created"
repository = Repository.factory('Subversion', :project => project, :url => url)
repository.save
repository.id
repository.id || 0
end
protected

View File

@@ -1,12 +1,104 @@
# 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 TimelogController < ApplicationController
layout 'base'
before_filter :find_project
before_filter :authorize, :only => :edit
before_filter :check_project_privacy, :only => :details
layout 'base'
before_filter :find_project, :authorize
helper :sort
include SortHelper
helper :issues
def report
@available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:values => @project.versions,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:values => @project.issue_categories,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:values => @project.users,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:values => Tracker.find(:all),
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:values => Enumeration::get_values('ACTI'),
:label => :label_activity}
}
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : '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
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"
@hours = ActiveRecord::Base.connection.select_all(sql)
@hours.each do |row|
case @columns
when 'year'
row['year'] = row['tyear']
when 'month'
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
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?
end
def details
sort_init 'spent_on', 'desc'
@@ -15,15 +107,15 @@ class TimelogController < ApplicationController
@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
@owner_id = User.current.id
send_csv and return if 'csv' == params[:export]
render :action => 'details', :layout => false if request.xhr?
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_404 and return if @time_entry && @time_entry.user != 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)
@@ -59,9 +151,9 @@ private
l(:field_activity),
l(:field_issue),
l(:field_hours),
l(:field_comment)
l(:field_comments)
]
csv << headers.collect {|c| ic.iconv(c) }
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),
@@ -69,9 +161,9 @@ private
entry.activity.name,
(entry.issue ? entry.issue.id : nil),
entry.hours,
entry.comment
entry.comments
]
csv << fields.collect {|c| ic.iconv(c.to_s) }
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind

View File

@@ -36,9 +36,11 @@ class TrackersController < ApplicationController
@tracker = Tracker.new(params[:tracker])
if request.post? and @tracker.save
# workflow copy
if params[:copy_workflow_from] && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
copy_from.workflows.each do |w|
@tracker.workflows << w.clone
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
Workflow.transaction do
copy_from.workflows.find(:all, :include => [:role, :old_status, :new_status]).each do |w|
Workflow.create(:tracker_id => @tracker.id, :role => w.role, :old_status => w.old_status, :new_status => w.new_status)
end
end
end
flash[:notice] = l(:notice_successful_create)
@@ -73,7 +75,7 @@ class TrackersController < ApplicationController
def destroy
@tracker = Tracker.find(params[:id])
unless @tracker.issues.empty?
flash[:notice] = "This tracker contains issues and can\'t be deleted."
flash[:error] = "This tracker contains issues and can\'t be deleted."
else
@tracker.destroy
end

View File

@@ -52,15 +52,16 @@ 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"][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] ? 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]
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
@@ -71,13 +72,13 @@ 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])
@@ -86,8 +87,8 @@ class UsersController < ApplicationController
end
end
@auth_sources = AuthSource.find(:all)
@roles = Role.find(:all, :order => 'position')
@projects = Project.find(:all) - @user.projects
@roles = Role.find_all_givable
@projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
@membership ||= Member.new
end
@@ -108,12 +109,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[:notice] = "Unable to delete user"
redirect_to :action => 'list'
end
end

View File

@@ -19,6 +19,11 @@ class VersionsController < ApplicationController
layout 'base'
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])
flash[:notice] = l(:notice_successful_update)
@@ -30,14 +35,14 @@ class VersionsController < ApplicationController
@version.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
rescue
flash[:notice] = "Unable to delete version"
flash[:error] = "Unable to delete version"
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def download
@attachment = @version.attachments.find(params[:attachment_id])
@attachment.increment_download
send_file @attachment.diskfile, :filename => @attachment.filename
send_file @attachment.diskfile, :filename => @attachment.filename, :type => @attachment.content_type
rescue
render_404
end
@@ -47,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

@@ -0,0 +1,49 @@
# 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 WatchersController < ApplicationController
layout 'base'
before_filter :require_login, :find_project, :check_project_privacy
def add
user = User.current
@watched.add_watcher(user)
respond_to do |format|
format.html { render :text => 'Watcher added.', :layout => true }
format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
end
end
def remove
user = User.current
@watched.remove_watcher(user)
respond_to do |format|
format.html { render :text => 'Watcher removed.', :layout => true }
format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
end
end
private
def find_project
klass = Object.const_get(params[:object_type].camelcase)
return false unless klass.respond_to?('watched_by')
@watched = klass.find(params[:object_id])
@project = @watched.project
rescue
render_404
end
end

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

@@ -15,17 +15,29 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'diff'
class WikiController < ApplicationController
layout 'base'
before_filter :find_wiki, :check_project_privacy, :except => [:preview]
before_filter :find_wiki, :authorize
verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index }
helper :attachments
include AttachmentsHelper
# display a page (in editing mode if it doesn't exist)
def index
page_title = params[:page]
@page = @wiki.find_or_new_page(page_title)
if @page.new_record?
edit
render :action => 'edit' and return
if User.current.allowed_to?(:edit_wiki_pages, @project)
edit
render :action => 'edit'
else
render_404
end
return
end
@content = @page.content_for_version(params[:version])
if params[:export] == 'html'
@@ -47,30 +59,66 @@ class WikiController < ApplicationController
@content = @page.content_for_version(params[:version])
@content.text = "h1. #{@page.pretty_title}" if @content.text.blank?
# don't keep previous comment
@content.comment = nil
@content.comments = nil
if request.post?
if @content.text == params[:content][:text]
if !@page.new_record? && @content.text == params[:content][:text]
# don't save if text wasn't changed
redirect_to :action => 'index', :id => @project, :page => @page.title
return
end
@content.text = params[:content][:text]
@content.comment = params[:content][:comment]
@content.author = logged_in_user
#@content.text = params[:content][:text]
#@content.comments = params[:content][:comments]
@content.attributes = params[:content]
@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
end
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
end
# rename a page
def rename
@page = @wiki.find_page(params[:page])
@page.redirect_existing_links = true
# used to display the *original* title if some AR validation errors occur
@original_title = @page.pretty_title
if request.post? && @page.update_attributes(params[:wiki_page])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index', :id => @project, :page => @page.title
end
end
# show page history
def history
@page = @wiki.find_page(params[:page])
# don't load text
@version_count = @page.content.versions.count
@version_pages = Paginator.new self, @version_count, 25, params['p']
# don't load text
@versions = @page.content.versions.find :all,
:select => "id, author_id, comment, updated_on, version",
:order => 'version DESC'
:select => "id, author_id, comments, updated_on, version",
:order => 'version DESC',
:limit => @version_pages.items_per_page + 1,
:offset => @version_pages.current.offset
render :layout => false if request.xhr?
end
def diff
@page = @wiki.find_page(params[:page])
@diff = @page.diff(params[:version], params[:version_from])
render_404 unless @diff
end
# remove a wiki page and its history
def destroy
@page = @wiki.find_page(params[:page])
@page.destroy if @page
redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end
# display special pages
@@ -78,11 +126,12 @@ class WikiController < ApplicationController
page_title = params[:page].downcase
case page_title
# show pages index, sorted by title
when 'page_index'
when 'page_index', 'date_index'
# eager load information about last updates, without loading text
@pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
:order => 'title'
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
# export wiki to a single html file
when 'export'
@pages = @wiki.pages.find :all, :order => 'title'
@@ -97,8 +146,22 @@ class WikiController < ApplicationController
end
def preview
page = @wiki.find_page(params[:page])
@attachements = page.attachments if page
@text = params[:content][:text]
render :partial => 'preview'
render :partial => 'common/preview'
end
def add_attachment
@page = @wiki.find_page(params[:page])
attach_files(@page, params[:attachments])
redirect_to :action => 'index', :page => @page.title
end
def destroy_attachment
@page = @wiki.find_page(params[:page])
@page.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'index', :page => @page.title
end
private
@@ -106,6 +169,7 @@ private
def find_wiki
@project = Project.find(params[:id])
@wiki = @project.wiki
render_404 unless @wiki
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -0,0 +1,44 @@
# 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 WikisController < ApplicationController
layout 'base'
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?
render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
end
# Delete a project's wiki
def destroy
if request.post? && params[:confirm] && @project.wiki
@project.wiki.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
end
end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -16,4 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module AdminHelper
def project_status_options_for_select(selected)
options_for_select([[l(:label_all), "*"],
[l(:status_active), 1]], selected)
end
end

View File

@@ -16,38 +16,25 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
# Return current logged in user or nil
def loggedin?
@logged_in_user
def current_role
@current_role ||= User.current.role_for_project(@project)
end
# Return true if user is logged in and is admin, otherwise false
def admin_loggedin?
@logged_in_user and @logged_in_user.admin?
end
# Return true if user is authorized for controller/action, otherwise false
def authorize_for(controller, action)
# check if action is allowed on public projects
if @project.is_public? and Permission.allowed_to_public "%s/%s" % [ controller, action ]
return true
end
# check if user is authorized
if @logged_in_user and (@logged_in_user.admin? or Permission.allowed_to_role( "%s/%s" % [ controller, action ], @logged_in_user.role_for_project(@project) ) )
return true
end
return false
def authorize_for(controller, action)
User.current.allowed_to?({:controller => controller, :action => action}, @project)
end
# Display a link if user is authorized
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller], options[:action])
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
end
# Display a link to user's account page
def link_to_user(user)
link_to user.display_name, :controller => 'account', :action => 'show', :id => user
user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
end
def link_to_issue(issue)
@@ -61,6 +48,14 @@ module ApplicationHelper
link_to(name, "#", :onclick => onclick)
end
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 << "return false;"
link_to(name, "#", options.merge(:onclick => onclick))
end
def image_to_function(name, function, html_options = {})
html_options.symbolize_keys!
tag(:input, html_options.merge({
@@ -69,12 +64,35 @@ module ApplicationHelper
}))
end
def format_date(date)
l_date(date) if date
def prompt_to_remote(name, text, param, url, html_options = {})
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
link_to name, {}, html_options
end
def format_time(time)
l_datetime((time.is_a? String) ? time.to_time : time) if time
def format_date(date)
return nil unless date
# "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, include_date = true)
return nil unless time
time = time.to_time if time.is_a?(String)
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
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 || 'Anonymous', time_tag)
end
def day_name(day)
@@ -86,60 +104,160 @@ module ApplicationHelper
end
def pagination_links_full(paginator, options={}, html_options={})
page_param = options.delete(:page_param) || :page
html = ''
html << link_to_remote(('&#171; ' + l(:label_previous)),
{:update => "content", :url => options.merge(:page => paginator.current.previous)},
{:href => url_for(:params => options.merge(:page => paginator.current.previous))}) + ' ' if paginator.current.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
html << (pagination_links_each(paginator, options) do |n|
link_to_remote(n.to_s,
{:url => {:params => options.merge(:page => n)}, :update => 'content'},
{:href => url_for(:params => options.merge(:page => n))})
{:url => {:params => options.merge(page_param => n)}, :update => 'content'},
{:href => url_for(:params => options.merge(page_param => n))})
end || '')
html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
{:update => "content", :url => options.merge(:page => paginator.current.next)},
{:href => url_for(:params => options.merge(:page => paginator.current.next))}) if paginator.current.next
{: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
html
end
# textilize text according to system settings and RedCloth availability
def textilizable(text, options = {})
def set_html_title(text)
@html_header_title = text
end
def html_title
title = []
title << @project.name if @project
title << @html_header_title
title << Setting.app_title
title.compact.join(' - ')
end
ACCESSKEYS = {:edit => 'e',
:preview => 'r',
:quick_search => 'f',
:search => '4',
}.freeze unless const_defined?(:ACCESSKEYS)
def accesskey(s)
ACCESSKEYS[s]
end
# 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)
else
raise ArgumentError, 'invalid arguments to textilizable'
end
# when using an image link, try to use an attachment, if possible
attachments = options[:attachments]
if attachments
text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
align = $1
filename = $2
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}!"
else
"!#{align}#{filename}!"
end
end
end
text = (Setting.text_formatting == 'textile') ?
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]
when :local
# used for local links to html files
format_wiki_link = Proc.new {|title| "#{title}.html" }
format_wiki_link = Proc.new {|project, title| "#{title}.html" }
when :anchor
# used for single-file wiki export
format_wiki_link = Proc.new {|title| "##{title}" }
format_wiki_link = Proc.new {|project, title| "##{title}" }
else
if @project
format_wiki_link = Proc.new {|title| url_for :controller => 'wiki', :action => 'index', :id => @project, :page => title }
else
format_wiki_link = Proc.new {|title| title }
end
format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
end
# turn wiki links into textile links:
project = options[:project] || @project
# turn wiki links into html links
# example:
# [[link]] -> "link":link
# [[link|title]] -> "title":link
text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) {|m| "\"#{$3 || $1}\":" + format_wiki_link.call(Wiki.titleize($1)) }
# [[mypage]]
# [[mypage|mytext]]
# wiki links can refer other project wikis, using project name or identifier:
# [[project:]] -> wiki starting page
# [[project:|mytext]]
# [[project:mypage]]
# [[project:mypage|mytext]]
text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) 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')))
else
# project or wiki doesn't exist
title || page
end
end
# turn issue ids to textile links
# turn issue and revision ids into links
# example:
# #52 -> "#52":/issues/show/52
text = text.gsub(/#(\d+)(?=\b)/) {|m| "\"##{$1}\":" + url_for(:controller => 'issues', :action => 'show', :id => $1) }
# turn revision ids to textile links (@project needed)
# example:
# r52 -> "r52":/repositories/revision/6?rev=52 (@project.id is 6)
text = text.gsub(/(?=\b)r(\d+)(?=\b)/) {|m| "\"r#{$1}\":" + url_for(:controller => 'repositories', :action => 'revision', :id => @project.id, :rev => $1) } if @project
# finally textilize text
@do_textilize ||= (Setting.text_formatting == 'textile') && (ActionView::Helpers::TextHelper.method_defined? "textilize")
text = @do_textilize ? auto_link(RedCloth.new(text).to_html) : simple_format(auto_link(h(text)))
# #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
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?
end
end
leading + (link || "#{otype}#{oid}")
end
text
end
# Same as Rails' simple_format helper without using paragraphs
def simple_format_without_paragraph(text)
text.to_s.
gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
end
def error_messages_for(object_name, options = {})
@@ -169,7 +287,7 @@ module ApplicationHelper
end
content_tag("div",
content_tag(
options[:header_tag] || "h2", lwr(:gui_validation_error, full_messages.length) + " :"
options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
) +
content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
"id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
@@ -181,7 +299,7 @@ module ApplicationHelper
def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) +
(GLoc.valid_languages.sort {|x,y| x.to_s <=> y.to_s }).collect {|lang| [ l_lang_name(lang.to_s, lang), lang.to_s]}
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 = {})
@@ -201,10 +319,56 @@ module ApplicationHelper
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)
options[:class] << ' icon-checked disabled'
options[:disabled] = true
end
if options.delete(:disabled)
options.delete(:method)
options.delete(:confirm)
options.delete(:onclick)
options[:class] << ' disabled'
url = '#'
end
link_to name, url, options
end
def calendar_for(field_id)
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 wikitoolbar_for(field_id)
return '' unless Setting.text_formatting == 'textile'
javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
end
def content_for(name, content = nil, &block)
@has_content ||= {}
@has_content[name] = true
super(name, content, &block)
end
def has_content?(name)
(@has_content && @has_content[name]) || false
end
end
class TabularFormBuilder < ActionView::Helpers::FormBuilder
@@ -219,7 +383,9 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
src = <<-END_SRC
def #{selector}(field, options = {})
return super if options.delete :no_label
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
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))

View File

@@ -0,0 +1,25 @@
# 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.
module AttachmentsHelper
# displays the links to a collection of attachments
def link_to_attachments(attachments, options = {})
if attachments.any?
render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
end
end
end

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# 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
@@ -15,5 +15,5 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module FeedsHelper
module BoardsHelper
end

View File

@@ -24,8 +24,6 @@ module CustomFieldsHelper
field_id = "custom_fields_#{custom_field.id}"
case custom_field.field_format
when "string", "int"
text_field 'custom_value', 'value', :name => field_name, :id => field_id
when "date"
text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) +
calendar_for(field_id)
@@ -35,6 +33,8 @@ module CustomFieldsHelper
check_box 'custom_value', 'value', :name => field_name, :id => field_id
when "list"
select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id
else
text_field 'custom_value', 'value', :name => field_name, :id => field_id
end
end

View File

@@ -52,11 +52,16 @@ module IfpdfHelper
def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
@ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
# these quotation marks are not correctly rendered in the pdf
txt = txt.gsub(/[“”]/, '"') if txt
txt = begin
@ic.iconv(txt)
# 0x5c char handling
txtar = txt.split('\\')
txtar << '' if txt[-1] == ?\\
txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
rescue
txt
end
end || ''
super w,h,txt,border,ln,align,fill,link
end

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# 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
@@ -15,10 +15,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueHistory < ActiveRecord::Base
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
belongs_to :issue
validates_presence_of :status
module IssueRelationsHelper
def collection_for_relation_type_select
values = IssueRelation::TYPES
values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]}
end
end

View File

@@ -15,7 +15,23 @@
# 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)
@cached_label_due_date ||= l(:field_due_date)
@cached_label_assigned_to ||= l(:field_assigned_to)
@cached_label_priority ||= l(:field_priority)
link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end
def show_detail(detail, no_html=false)
case detail.property
@@ -60,13 +76,18 @@ module IssuesHelper
label = content_tag('strong', label)
old_value = content_tag("i", h(old_value)) if detail.old_value
old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
value = content_tag("i", h(value)) if value
if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
# Link to the attachment if it has not been removed
value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key)
else
value = content_tag("i", h(value)) if value
end
end
if detail.value and !detail.value.to_s.empty?
if !detail.value.blank?
case detail.property
when 'attr', 'cf'
if old_value
if !detail.old_value.blank?
label + " " + l(:text_journal_changed, old_value, value)
else
label + " " + l(:text_journal_set_to, value)
@@ -83,4 +104,56 @@ 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_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}
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,
format_time(issue.created_on),
format_time(issue.updated_on)
]
custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
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,28 @@
# 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.
module MessagesHelper
def link_to_message(message)
return '' unless message
link_to h(truncate(message.subject, 60)), :controller => 'messages',
:action => 'show',
:board_id => message.board_id,
:id => message.root,
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
end
end

View File

@@ -16,14 +16,184 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ProjectsHelper
def highlight_tokens(text, tokens)
return text unless tokens && !tokens.empty?
regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE
result = ''
text.split(regexp).each_with_index do |words, i|
result << (i.even? ? (words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words) : content_tag('span', words, :class => 'highlight'))
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
end
def project_settings_tabs
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
{:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
{:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
{:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
{:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
# Generates a gantt image
# Only defined if RMagick is avalaible
def gantt_image(events, date_from, months, zoom)
date_to = (date_from >> months)-1
show_weeks = zoom > 1
show_days = zoom > 2
subject_width = 320
header_heigth = 18
# width of one day in pixels
zoom = zoom*2
g_width = (date_to - date_from + 1)*zoom
g_height = 20 * events.length + 20
headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
height = g_height + headers_heigth
imgl = Magick::ImageList.new
imgl.new_image(subject_width+g_width+1, height)
gc = Magick::Draw.new
# Subjects
top = headers_heigth + 20
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
events.each do |i|
gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
top = top + 20
end
result
# Months headers
month_f = date_from
left = subject_width
months.times do
width = ((month_f >> 1) - month_f) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, 0, left + width, height)
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
left = left + width
month_f = month_f >> 1
end
# Weeks headers
if show_weeks
left = subject_width
height = header_heigth
if date_from.cwday == 1
# date_from is monday
week_f = date_from
else
# find next monday after date_from
week_f = date_from + (7 - date_from.cwday + 1)
width = (7 - date_from.cwday + 1) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
left = left + width
end
while week_f <= date_to
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
left = left + width
week_f = week_f+7
end
end
# Days details (week-end in grey)
if show_days
left = subject_width
height = g_height + header_heigth - 1
wday = date_from.cwday
(date_to - date_from + 1).to_i.times do
width = zoom
gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end
end
# border
gc.fill('transparent')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
gc.stroke('black')
gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
# content
top = headers_heigth + 20
gc.stroke('transparent')
events.each do |i|
if i.is_a?(Issue)
i_start_date = (i.start_date >= date_from ? i.start_date : date_from )
i_end_date = (i.due_date <= date_to ? i.due_date : date_to )
i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= date_from ? date_from : i_done_date )
i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = subject_width + ((i_start_date - date_from)*zoom).floor
i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
d_width = ((i_done_date - i_start_date)*zoom).floor # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
gc.fill('grey')
gc.rectangle(i_left, top, i_left + i_width, top - 6)
gc.fill('red')
gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
gc.fill('blue')
gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
gc.fill('black')
gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
else
i_left = subject_width + ((i.start_date - date_from)*zoom).floor
gc.fill('green')
gc.rectangle(i_left, top, i_left + 6, top - 6)
gc.fill('black')
gc.text(i_left + 11, top + 1, i.name)
end
top = top + 20
end
# today red line
if Date.today >= date_from and Date.today <= date_to
gc.stroke('red')
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 = @project.trackers
# 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

@@ -1,6 +1,51 @@
module QueriesHelper
# 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.
module QueriesHelper
def operators_for_select(filter_type)
Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
end
def column_header(column)
column.sortable ? sort_header_tag(column.sortable, :caption => column.caption) : content_tag('th', column.caption)
end
def column_content(column, issue)
if column.is_a?(QueryCustomFieldColumn)
cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv)
else
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

@@ -22,11 +22,15 @@ module ReportsHelper
data.each { |row|
match = 1
criteria.each { |k, v|
match = 0 unless row[k].to_s == v.to_s
match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && row[k] == (v == 0 ? "f" : "t"))
} unless criteria.nil?
a = a + row["total"].to_i if match == 1
} unless data.nil?
a
end
def aggregate_link(data, criteria, *args)
a = aggregate data, criteria
a > 0 ? link_to(a, *args) : '-'
end
end

View File

@@ -15,5 +15,73 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'coderay'
require 'coderay/helpers/file_type'
require 'iconv'
module RepositoriesHelper
def syntax_highlight(name, content)
type = CodeRay::FileType[name]
type ? CodeRay.scan(content, type).html : h(content)
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)
@encodings.each do |encoding|
begin
return Iconv.conv('UTF-8', encoding, str)
rescue Iconv::Failure
# do nothing here and try the next encoding
end
end
str
end
def repository_field_tags(form, repository)
method = repository.class.name.demodulize.underscore + "_field_tags"
send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
end
def scm_select_tag(repository)
container = [[]]
REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
select_tag('repository_scm',
options_for_select(container, repository.class.name.demodulize),
:disabled => (repository && !repository.new_record?),
:onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
)
end
def with_leading_slash(path)
path ||= ''
path.starts_with?("/") ? "/#{path}" : path
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, :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)
content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
end
def mercurial_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => 'Root 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

@@ -0,0 +1,38 @@
# 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.
module SearchHelper
def highlight_tokens(text, tokens)
return text unless text && tokens && !tokens.empty?
regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE
result = ''
text.split(regexp).each_with_index do |words, i|
if result.length > 1200
# maximum length of the preview reached
result << '...'
break
end
if i.even?
result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words)
else
t = (tokens.index(words.downcase) || 0) % 4
result << content_tag('span', h(words), :class => "highlight token-#{t}")
end
end
result
end
end

View File

@@ -137,13 +137,8 @@ module SortHelper
# </th>
#
def sort_header_tag(column, options = {})
if options[:caption]
caption = options[:caption]
options.delete(:caption)
else
caption = titleize(Inflector::humanize(column))
end
options[:title]= "Sort by #{caption}" unless options[:title]
caption = options.delete(:caption) || titleize(Inflector::humanize(column))
options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
content_tag('th', sort_link(column, caption), options)
end

View File

@@ -1,2 +1,30 @@
# redMine - project management software
# Copyright (C) 2006 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 TimelogHelper
def select_hours(data, criteria, value)
data.select {|row| row[criteria] == value.to_s}
end
def sum_hours(data)
sum = 0
data.each do |row|
sum += row['hours'].to_f
end
sum
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

@@ -0,0 +1,36 @@
# 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.
module WatchersHelper
def watcher_tag(object, user)
content_tag("span", watcher_link(object, user), :id => 'watcher')
end
def watcher_link(object, user)
return '' unless user && user.logged? && object.respond_to?('watched_by?')
watched = object.watched_by?(user)
url = {:controller => 'watchers',
:action => (watched ? 'remove' : 'add'),
:object_type => object.class.to_s.underscore,
:object_id => object.id}
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
{:url => url},
:href => url_for(url),
:class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
end
end

View File

@@ -16,4 +16,41 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module WikiHelper
def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)}
words_add = 0
words_del = 0
dels = 0
del_off = 0
wdiff.diff.diffs.each do |diff|
add_at = nil
add_to = nil
del_at = nil
deleted = ""
diff.each do |change|
pos = change[1]
if change[0] == "+"
add_at = pos + dels unless add_at
add_to = pos + dels
words_add += 1
else
del_at = pos unless del_at
deleted << ' ' + change[2]
words_del += 1
end
end
if add_at
words[add_at] = '<span class="diff_in">' + words[add_at]
words[add_to] = words[add_to] + '</span>'
end
if del_at
words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
dels += 1
del_off += words_del
words_del = 0
end
end
simple_format_without_paragraph(words.join(' '))
end
end

View File

@@ -21,8 +21,14 @@ 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
@@storage_path = "#{RAILS_ROOT}/files"
@@ -36,7 +42,7 @@ class Attachment < ActiveRecord::Base
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
self.content_type = @temp_file.content_type.chomp
self.filesize = @temp_file.size
end
end
@@ -55,6 +61,10 @@ class Attachment < ActiveRecord::Base
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
@@ -72,12 +82,15 @@ class Attachment < ActiveRecord::Base
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.project
end
def image?
self.filename =~ /\.(jpeg|jpg|gif|png)$/i
end
private
def sanitize_filename(value)
# get only the filename, not the whole path

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

@@ -20,7 +20,8 @@ require 'iconv'
class AuthSourceLdap < AuthSource
validates_presence_of :host, :port, :attr_login
validates_presence_of :attr_firstname, :attr_lastname, :attr_mail, :if => Proc.new { |a| a.onthefly_register? }
def after_initialize
self.port = 389 if self.port == 0
end
@@ -34,12 +35,13 @@ class AuthSourceLdap < AuthSource
dn = String.new
ldap_con.search( :base => self.base_dn,
:filter => object_filter & login_filter,
:attributes=> ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]) do |entry|
# only ask for the DN if on-the-fly registration is disabled
:attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry|
dn = entry.dn
attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
:lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
:mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
:auth_source_id => self.id ]
:auth_source_id => self.id ] if onthefly_register?
end
return nil if dn.empty?
logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug?
@@ -69,7 +71,8 @@ private
def initialize_ldap_con(ldap_user, ldap_password)
Net::LDAP.new( {:host => self.host,
:port => self.port,
:auth => { :method => :simple, :username => Iconv.new('iso-8859-15', 'utf-8').iconv(ldap_user), :password => Iconv.new('iso-8859-15', 'utf-8').iconv(ldap_password) }}
:auth => { :method => :simple, :username => ldap_user, :password => ldap_password },
:encryption => (self.tls ? :simple_tls : nil)}
)
end

29
app/models/board.rb Normal file
View File

@@ -0,0 +1,29 @@
# 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 Board < ActiveRecord::Base
belongs_to :project
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
acts_as_list :scope => :project_id
acts_as_watchable
validates_presence_of :name, :description
validates_length_of :name, :maximum => 30
validates_length_of :description, :maximum => 255
end

View File

@@ -18,13 +18,87 @@
class Changeset < ActiveRecord::Base
belongs_to :repository
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
:description => :comments,
:datetime => :committed_on,
:author => :committer,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments',
:include => :repository,
:project_key => "#{Repository.table_name}.project_id",
: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 comments=(comment)
write_attribute(:comments, comment.strip)
end
def committed_on=(date)
self.commit_date = date
super
end
def after_create
scan_comment_for_issue_ids
end
def scan_comment_for_issue_ids
return if comments.blank?
# keywords used to reference issues
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
# keywords used to fix issues
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
# status and optional done ratio applied
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
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+/)
target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
if fix_status && fix_keywords.include?(action.downcase)
# 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
next if issue.status.is_closed?
issue.status = fix_status
issue.done_ratio = done_ratio if done_ratio
issue.save
end
end
referenced_issues += target_issues
end
self.issues = referenced_issues.uniq
end
# Returns the previous changeset
def previous
@previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC')
end
# Returns the next changeset
def next
@next ||= Changeset.find(:first, :conditions => ['revision > ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision ASC')
end
end

View File

@@ -19,5 +19,5 @@ class Comment < ActiveRecord::Base
belongs_to :commented, :polymorphic => true, :counter_cache => true
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
validates_presence_of :commented, :author, :comment
validates_presence_of :commented, :author, :comments
end

View File

@@ -17,18 +17,21 @@
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 },
"text" => { :name => :label_text, :order => 2 },
"int" => { :name => :label_integer, :order => 3 },
"list" => { :name => :label_list, :order => 4 },
"date" => { :name => :label_date, :order => 5 },
"bool" => { :name => :label_boolean, :order => 6 }
"float" => { :name => :label_float, :order => 4 },
"list" => { :name => :label_list, :order => 5 },
"date" => { :name => :label_date, :order => 6 },
"bool" => { :name => :label_boolean, :order => 7 }
}.freeze
validates_presence_of :name, :field_format
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
@@ -40,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,6 +55,10 @@ class CustomField < ActiveRecord::Base
end
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

@@ -21,18 +21,19 @@ class CustomValue < ActiveRecord::Base
protected
def validate
errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.empty?
errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.empty? or value =~ Regexp.new(custom_field.regexp)
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 =~ /^[0-9]*$/
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?
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.blank?
when 'list'
errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) or value.blank?
end
end
end

View File

@@ -20,5 +20,11 @@ class Document < ActiveRecord::Base
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
has_many :attachments, :as => :container, :dependent => :destroy
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
validates_length_of :title, :maximum => 60
end

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# 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
@@ -15,5 +15,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module HelpHelper
class EnabledModule < ActiveRecord::Base
belongs_to :project
validates_presence_of :name
validates_uniqueness_of :name, :scope => :project_id
end

View File

@@ -16,25 +16,42 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Enumeration < ActiveRecord::Base
acts_as_list :scope => 'opt = \'#{opt}\''
before_destroy :check_integrity
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,
"DCAT" => :enumeration_doc_categories,
"ACTI" => :enumeration_activities
}.freeze
def self.get_values(option)
find(:all, :conditions => ['opt=?', option])
end
OPTIONS = {
"IPRI" => :enumeration_issue_priorities,
"DCAT" => :enumeration_doc_categories,
"ACTI" => :enumeration_activities
}.freeze
def self.get_values(option)
find(:all, :conditions => {:opt => option}, :order => 'position')
end
def self.default(option)
find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position')
end
def option_name
OPTIONS[self.opt]
end
def before_save
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default?
end
def <=>(enumeration)
position <=> enumeration.position
end
def to_s; name end
private
def check_integrity

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# 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
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :tracker
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
@@ -28,17 +27,69 @@ class Issue < ActiveRecord::Base
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries
has_many :time_entries, :dependent => :nullify
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :custom_fields, :through => :custom_values
validates_presence_of :subject, :description, :priority, :tracker, :author, :status
has_and_belongs_to_many :changesets, :order => "revision ASC"
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
acts_as_watchable
acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
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, :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
validates_associated :custom_values, :on => :update
# set default status for new issues
def before_validation
self.status = IssueStatus.default if status.nil?
def after_initialize
if new_record?
# set default values for new records only
self.status ||= IssueStatus.default
self.priority ||= Enumeration.default('IPRI')
end
end
def copy_from(arg)
issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
self.attributes = issue.attributes.dup
self.custom_values = issue.custom_values.collect {|v| v.clone}
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
self.relations_from.clear
self.relations_to.clear
# 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 validate
@@ -49,13 +100,24 @@ class Issue < ActiveRecord::Base
if self.due_date and self.start_date and self.due_date < self.start_date
errors.add :due_date, :activerecord_error_greater_than_start_date
end
if start_date && soonest_start && start_date < soonest_start
errors.add :start_date, :activerecord_error_invalid
end
end
#def before_create
# build_history
#end
def before_save
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
self.assigned_to = category.assigned_to
end
end
def before_save
if @current_journal
# attributes changes
(Issue.column_names - %w(id description)).each {|c|
@@ -66,17 +128,33 @@ class Issue < ActiveRecord::Base
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value) unless @custom_values_before_change[c.custom_field_id]==c.value
:value => c.value)
}
@current_journal.save unless @current_journal.details.empty? and @current_journal.notes.empty?
@current_journal.save
end
# Save the issue even if the journal is not saved (because empty)
true
end
def long_id
"%05d" % self.id
def after_save
# 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|
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
duplicate.init_journal(@current_journal.user, @current_journal.notes)
duplicate.update_attribute :status, self.status
end
end
end
def custom_value_for(custom_field)
@@ -92,15 +170,52 @@ class Issue < ActiveRecord::Base
@current_journal
end
# Return true if the issue is closed, otherwise false
def closed?
self.status.is_closed?
end
# Users the issue can be assigned to
def assignable_users
project.assignable_users
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
recipients.compact.uniq
end
def spent_hours
@spent_hours ||= time_entries.sum(:hours) || 0
end
private
# Creates an history for the issue
#def build_history
# @history = self.histories.build
# @history.status = self.status
# @history.author = self.author
#end
def relations
(relations_from + relations_to).sort
end
def all_dependent_issues
dependencies = []
relations_from.each do |relation|
dependencies << relation.issue_to
dependencies += relation.issue_to.all_dependent_issues
end
dependencies
end
# Returns an array of the duplicate issues
def duplicates
relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
end
def duration
(start_date && due_date) ? due_date - start_date : 0
end
def soonest_start
@soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
end
end

View File

@@ -16,14 +16,28 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueCategory < ActiveRecord::Base
before_destroy :check_integrity
belongs_to :project
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 30
private
def check_integrity
raise "Can't delete category" if Issue.find(:first, :conditions => ["category_id=?", self.id])
alias :destroy_without_reassign :destroy
# Destroy the category
# If a category is specified, issues are reassigned to this category
def destroy(reassign_to = nil)
if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project
Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
end
destroy_without_reassign
end
def <=>(category)
name <=> category.name
end
def to_s; name end
end

View File

@@ -0,0 +1,79 @@
# 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 IssueRelation < ActiveRecord::Base
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
TYPE_RELATES = "relates"
TYPE_DUPLICATES = "duplicates"
TYPE_BLOCKS = "blocks"
TYPE_PRECEDES = "precedes"
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 },
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
}.freeze
validates_presence_of :issue_from, :issue_to, :relation_type
validates_inclusion_of :relation_type, :in => TYPES.keys
validates_numericality_of :delay, :allow_nil => true
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
def validate
if issue_from && issue_to
errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
end
end
def other_issue(issue)
(self.issue_from_id == issue.id) ? issue_to : issue_from
end
def label_for(issue)
TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
end
def before_save
if TYPE_PRECEDES == relation_type
self.delay ||= 0
else
self.delay = nil
end
set_issue_to_dates
end
def set_issue_to_dates
soonest_start = self.successor_soonest_start
if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration
issue_to.save
end
end
def successor_soonest_start
return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date)
(issue_from.due_date || issue_from.start_date) + 1 + delay
end
def <=>(relation)
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
end
end

View File

@@ -17,14 +17,13 @@
class IssueStatus < ActiveRecord::Base
before_destroy :check_integrity
has_many :workflows, :foreign_key => "old_status_id"
has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all
acts_as_list
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_length_of :html_color, :is => 6
validates_format_of :html_color, :with => /^[a-f0-9]*$/i
def before_save
IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default?
@@ -38,21 +37,27 @@ class IssueStatus < ActiveRecord::Base
# Returns an array of all statuses the given role can switch to
# Uses association cache when called more than one time
def new_statuses_allowed_to(role, tracker)
new_statuses = [self]
new_statuses += workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker
new_statuses.sort{|x, y| x.position <=> y.position }
new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker
new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : []
end
# Same thing as above but uses a database query
# More efficient than the previous method if called just once
def find_new_statuses_allowed_to(role, tracker)
new_statuses = [self]
new_statuses += workflows.find(:all,
new_statuses = workflows.find(:all,
:include => :new_status,
:conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status } if role && tracker
new_statuses.sort{|x, y| x.position <=> y.position }
:conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker
new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : []
end
def new_status_allowed_to?(status, role, tracker)
status && role && tracker ?
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? :
false
end
def to_s; name end
private
def check_integrity
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])

View File

@@ -23,4 +23,25 @@ class Journal < ActiveRecord::Base
belongs_to :user
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
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| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') },
:description => :notes,
:author => :user,
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.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
end

View File

@@ -17,4 +17,9 @@
class JournalDetail < ActiveRecord::Base
belongs_to :journal
def before_save
self.value = value[0..254] if value && value.is_a?(String)
self.old_value = old_value[0..254] if old_value && old_value.is_a?(String)
end
end

View File

@@ -0,0 +1,40 @@
# 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 MailHandler < ActionMailer::Base
# Processes incoming emails
# Currently, it only supports adding a note to an existing issue
# by replying to the initial notification message
def receive(email)
# find related issue by parsing the subject
m = email.subject.match %r{\[.*#(\d+)\]}
return unless m
issue = Issue.find_by_id(m[1])
return unless issue
# find user
user = User.find_active(:first, :conditions => {:mail => email.from.first})
return unless user
# check permission
return unless user.allowed_to?(:add_issue_notes, issue.project)
# add the note
issue.init_journal(user, email.body.chomp)
issue.save
end
end

View File

@@ -16,73 +16,145 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Mailer < ActionMailer::Base
helper ApplicationHelper
helper IssuesHelper
def issue_add(issue)
set_language_if_valid(Setting.default_language)
# Sends to all project members
@recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }.compact
@from = Setting.mail_from
@subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
@body['issue'] = issue
helper CustomFieldsHelper
include ActionController::UrlWriter
def issue_add(issue)
recipients issue.recipients
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)
set_language_if_valid(Setting.default_language)
# Sends to all project members
issue = journal.journalized
@recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }.compact
@from = Setting.mail_from
@subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
@body['issue'] = issue
@body['journal']= journal
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}"
body :issue => issue,
:journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
def document_add(document)
set_language_if_valid(Setting.default_language)
@recipients = document.project.users.collect { |u| u.mail if u.mail_notification }.compact
@from = Setting.mail_from
@subject = "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
@body['document'] = document
def document_added(document)
recipients document.project.recipients
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
end
def attachments_add(attachments)
set_language_if_valid(Setting.default_language)
def attachments_added(attachments)
container = attachments.first.container
url = "http://#{Setting.host_name}/"
added_to = ""
case container.class.to_s
added_to = ''
added_to_url = ''
case container.class.name
when 'Version'
url << "projects/list_files/#{container.project_id}"
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}"
when 'Document'
url << "documents/show/#{container.id}"
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
when 'Issue'
url << "issues/show/#{container.id}"
added_to = "#{container.tracker.name} ##{container.id}: #{container.subject}"
end
@recipients = container.project.users.collect { |u| u.mail if u.mail_notification }.compact
@from = Setting.mail_from
@subject = "[#{container.project.name}] #{l(:label_attachment_new)}"
@body['attachments'] = attachments
@body['url'] = url
@body['added_to'] = added_to
recipients container.project.recipients
subject "[#{container.project.name}] #{l(:label_attachment_new)}"
body :attachments => attachments,
:added_to => added_to,
:added_to_url => added_to_url
end
def news_added(news)
recipients news.project.recipients
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
end
def message_posted(message, recipients)
recipients(recipients)
subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
body :message => message,
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
end
def account_information(user, password)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_register)
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)
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
@from = Setting.mail_from
@subject = l(:mail_subject_lost_password)
@body['token'] = token
recipients token.user.mail
subject l(:mail_subject_lost_password)
body :token => token,
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
end
def register(token)
set_language_if_valid(token.user.language)
@recipients = token.user.mail
@from = Setting.mail_from
@subject = l(:mail_subject_register)
@body['token'] = token
recipients token.user.mail
subject l(:mail_subject_register)
body :token => token,
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
end
def test(user)
set_language_if_valid(user.language)
recipients user.mail
subject 'Redmine test'
body :url => url_for(:controller => 'welcome')
end
private
def initialize_defaults(method_name)
super
set_language_if_valid Setting.default_language
from Setting.mail_from
default_url_options[:host] = Setting.host_name
default_url_options[:protocol] = Setting.protocol
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(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

@@ -23,7 +23,16 @@ class Member < ActiveRecord::Base
validates_presence_of :role, :user, :project
validates_uniqueness_of :user_id, :scope => :project_id
def validate
errors.add :role_id, :activerecord_error_invalid if role && !role.member?
end
def name
self.user.display_name
self.user.name
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]
end
end

67
app/models/message.rb Normal file
View File

@@ -0,0 +1,67 @@
# 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 Message < ActiveRecord::Base
belongs_to :board
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
has_many :attachments, :as => :container, :dependent => :destroy
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_searchable :columns => ['subject', 'content'],
:include => :board,
:project_key => 'project_id',
:date_column => 'created_on'
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content,
: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?
end
def after_create
board.update_attribute(:last_message_id, self.id)
board.increment! :messages_count
if parent
parent.reload.update_attribute(:last_reply_id, self.id)
else
board.increment! :topics_count
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
end

View File

@@ -0,0 +1,27 @@
# 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 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}
# send notification to the board watchers
recipients += message.board.watcher_recipients
recipients = recipients.compact.uniq
Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
end
end

View File

@@ -21,7 +21,12 @@ class News < ActiveRecord::Base
has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
validates_presence_of :title, :description
validates_length_of :title, :maximum => 60
validates_length_of :summary, :maximum => 255
acts_as_searchable :columns => ['title', 'description']
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
# returns latest news for projects visible by user
def self.latest(user=nil, count=5)
find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")

View File

@@ -1,66 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 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 Permission < ActiveRecord::Base
has_and_belongs_to_many :roles
validates_presence_of :controller, :action, :description
GROUPS = {
100 => :label_project,
200 => :label_member_plural,
300 => :label_version_plural,
400 => :label_issue_category_plural,
600 => :label_query_plural,
1000 => :label_issue_plural,
1100 => :label_news_plural,
1200 => :label_document_plural,
1300 => :label_attachment_plural,
1400 => :label_repository,
1500 => :label_time_tracking
}.freeze
@@cached_perms_for_public = nil
@@cached_perms_for_roles = nil
def name
self.controller + "/" + self.action
end
def group_id
(self.sort / 100)*100
end
def self.allowed_to_public(action)
@@cached_perms_for_public ||= find(:all, :conditions => ["is_public=?", true]).collect {|p| "#{p.controller}/#{p.action}"}
@@cached_perms_for_public.include? action
end
def self.allowed_to_role(action, role)
@@cached_perms_for_roles ||=
begin
perms = {}
find(:all, :include => :roles).each {|p| perms.store "#{p.controller}/#{p.action}", p.roles.collect {|r| r.id } }
perms
end
allowed_to_public(action) or (role && @@cached_perms_for_roles[action] && @@cached_perms_for_roles[action].include?(role.id))
end
def self.allowed_to_role_expired
@@cached_perms_for_roles = nil
end
end

View File

@@ -16,31 +16,54 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Project < ActiveRecord::Base
has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
has_many :members, :dependent => :delete_all, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
# Project statuses
STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9
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"
has_many :time_entries, :dependent => :delete_all
has_many :queries, :dependent => :delete_all
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_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'
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
attr_protected :status, :enabled_module_names
validates_presence_of :name, :description, :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 :identifier, :in => 3..12
validates_length_of :homepage, :maximum => 60
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
@@ -51,17 +74,27 @@ class Project < ActiveRecord::Base
def issues_with_subprojects(include_subprojects=false)
conditions = nil
if include_subprojects && children.size > 0
ids = [id] + children.collect {|c| c.id}
if include_subprojects && !active_children.empty?
ids = [id] + active_children.collect {|c| c.id}
conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
else
conditions = ["#{Issue.table_name}.project_id = ?", id]
end
Issue.with_scope :find => { :conditions => conditions } do
conditions ||= ["#{Issue.table_name}.project_id = ?", id]
# Quick and dirty fix for Rails 2 compatibility
Issue.send(:with_scope, :find => { :conditions => conditions }) do
yield
end
end
# Return all issues status changes for the project between the 2 given dates
def issues_status_changes(from, to)
Journal.find(:all, :include => [:issue, :details, :user],
:conditions => ["#{Journal.table_name}.journalized_type = 'Issue'" +
" AND #{Issue.table_name}.project_id = ?" +
" AND #{JournalDetail.table_name}.prop_key = 'status_id'" +
" AND #{Journal.table_name}.created_on BETWEEN ? AND ?",
id, from, to+1])
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)
@@ -69,13 +102,51 @@ class Project < ActiveRecord::Base
end
def self.visible_by(user=nil)
if user && !user.memberships.empty?
return ["#{Project.table_name}.is_public = ? or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')})", true]
if user && user.admin?
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
elsif user && user.memberships.any?
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
else
return ["#{Project.table_name}.is_public = ?", true]
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
end
end
def active?
self.status == STATUS_ACTIVE
end
def archive
# Archive subprojects if any
children.each do |subproject|
subproject.archive
end
update_attribute :status, STATUS_ARCHIVED
end
def unarchive
return false if parent && !parent.active?
update_attribute :status, STATUS_ACTIVE
end
def active_children
children.select {|child| child.active?}
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}.sort
end
# Returns the mail adresses of users that should be always notified on project events
def recipients
members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
end
# Returns an array of all custom fields enabled for project issues
# (explictly associated custom fields and custom fields enabled for all projects)
def custom_fields_for_issues(tracker)
@@ -85,10 +156,47 @@ class Project < ActiveRecord::Base
def all_custom_fields
@all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
end
def <=>(project)
name.downcase <=> project.name.downcase
end
def allows_to?(action)
if action.is_a? Hash
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
else
allowed_permissions.include? action
end
end
def module_enabled?(module_name)
module_name = module_name.to_s
enabled_modules.detect {|m| m.name == module_name}
end
def enabled_module_names=(module_names)
enabled_modules.clear
module_names = [] unless module_names && module_names.is_a?(Array)
module_names.each do |name|
enabled_modules << EnabledModule.new(:name => name.to_s)
end
end
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
end
private
def allowed_permissions
@allowed_permissions ||= begin
module_names = enabled_modules.collect {|m| m.name}
Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
end
end
def allowed_actions
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
end
end

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# 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
@@ -15,14 +15,48 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueryColumn
attr_accessor :name, :sortable
include GLoc
def initialize(name, options={})
self.name = name
self.sortable = options[:sortable]
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
belongs_to :project
belongs_to :user
serialize :filters
serialize :column_names
attr_protected :project, :user
validates_presence_of :name, :on => :save
validates_length_of :name, :maximum => 255
@@operators = { "=" => :label_equals,
"!" => :label_not_equals,
@@ -30,10 +64,13 @@ class Query < ActiveRecord::Base
"c" => :label_closed_issues,
"!*" => :label_none,
"*" => :label_all,
">=" => '>=',
"<=" => '<=',
"<t+" => :label_in_less_than,
">t+" => :label_in_more_than,
"t+" => :label_in,
"t" => :label_today,
"w" => :label_this_week,
">t-" => :label_less_than_ago,
"<t-" => :label_more_than_ago,
"t-" => :label_ago,
@@ -46,28 +83,53 @@ class Query < ActiveRecord::Base
:list_status => [ "o", "=", "!", "c", "*" ],
:list_optional => [ "=", "!", "!*", "*" ],
:list_one_or_more => [ "*", "=" ],
:date => [ "<t+", ">t+", "t+", "t", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t" ],
:text => [ "~", "!~" ] }
:date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t", "w" ],
:string => [ "=", "~", "!", "!~" ],
:text => [ "~", "!~" ],
:integer => [ "=", ">=", "<=" ] }
cattr_reader :operators_by_filter_type
@@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(:assigned_to, :sortable => "#{User.table_name}.lastname"),
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"),
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
QueryColumn.new(:fixed_version),
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"),
]
cattr_reader :available_columns
def initialize(attributes = nil)
super attributes
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
self.is_public = true
set_language_if_valid(User.current.language)
end
def validate
filters.each_key do |field|
errors.add field.gsub(/\_id$/, ""), :activerecord_error_blank unless
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
# filter doesn't require any value
["o", "c", "!*", "*", "t"].include? operator_for(field)
["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
end if filters
end
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)
end
def available_filters
return @available_filters if @available_filters
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
@@ -77,15 +139,41 @@ class Query < ActiveRecord::Base
"created_on" => { :type => :date_past, :order => 9 },
"updated_on" => { :type => :date_past, :order => 10 },
"start_date" => { :type => :date, :order => 11 },
"due_date" => { :type => :date, :order => 12 } }
unless project.nil?
# project specific filters
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => @project.users.collect{|s| [s.name, s.id.to_s] } }
@available_filters["author_id"] = { :type => :list, :order => 5, :values => @project.users.collect{|s| [s.name, s.id.to_s] } }
"due_date" => { :type => :date, :order => 12 },
"done_ratio" => { :type => :integer, :order => 13 }}
user_values = []
user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
if project
user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
else
# members of the user's projects
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?
if project
# project specific filters
@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.collect{|s| [s.name, s.id.to_s] } }
unless @project.children.empty?
@available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.children.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] } }
end
@project.all_custom_fields.select(&:is_filter?).each do |field|
case field.field_format
when "text"
options = { :type => :text, :order => 20 }
when "list"
options = { :type => :list_optional, :values => field.possible_values, :order => 20}
when "date"
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
# remove category filter if no category defined
@available_filters.delete "category_id" if @available_filters["category_id"][:values].empty?
@@ -126,57 +214,129 @@ class Query < ActiveRecord::Base
has_filter?(field) ? filters[field][:values] : nil
end
def label_for(field)
label = @available_filters[field][:name] if @available_filters.has_key?(field)
label ||= field.gsub(/\_id$/, "")
end
def available_columns
return @available_columns if @available_columns
@available_columns = Query.available_columns
@available_columns += (project ?
project.custom_fields :
IssueCustomField.find(:all, :conditions => {:is_for_all => true})
).collect {|cf| QueryCustomFieldColumn.new(cf) }
end
def columns
if has_default_columns?
available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
else
# preserve the column_names order
column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
end
end
def column_names=(names)
names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
write_attribute(:column_names, names)
end
def has_column?(column)
column_names && column_names.include?(column.name)
end
def has_default_columns?
column_names.nil? || column_names.empty?
end
def statement
sql = "1=1"
if has_filter?("subproject_id")
# 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.children.collect{|p| p.id}
subproject_ids = project.active_children.collect{|p| p.id}
end
sql << " AND #{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project
clause << "#{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project
elsif project
clause << "#{Issue.table_name}.project_id=%d" % project.id
else
sql << " AND #{Issue.table_name}.project_id=%d" % project.id if project
clause << Project.visible_by(User.current)
end
# filters clauses
filters_clauses = []
filters.each_key do |field|
next if field == "subproject_id"
v = values_for field
next unless v and !v.empty?
sql = sql + " AND " unless sql.empty?
v = values_for(field).clone
next unless v and !v.empty?
sql = ''
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 "
else
# regular field
db_table = Issue.table_name
db_field = field
sql << '('
end
# "me" value subsitution
if %w(assigned_to_id author_id).include?(field)
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
end
case operator_for field
when "="
sql = sql + "#{Issue.table_name}.#{field} IN (" + v.each(&:to_i).join(",") + ")"
sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!"
sql = sql + "#{Issue.table_name}.#{field} NOT IN (" + v.each(&:to_i).join(",") + ")"
sql = sql + "#{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!*"
sql = sql + "#{Issue.table_name}.#{field} IS NULL"
sql = sql + "#{db_table}.#{db_field} IS NULL"
when "*"
sql = sql + "#{Issue.table_name}.#{field} IS NOT NULL"
sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
when ">="
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
when "<="
sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
when "o"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = sql + "#{Issue.table_name}.#{field} >= '%s'" % connection.quoted_date(Date.today - v.first.to_i)
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
when "<t-"
sql = sql + "#{Issue.table_name}.#{field} <= '" + (Date.today - v.first.to_i).strftime("%Y-%m-%d") + "'"
sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
when "t-"
sql = sql + "#{Issue.table_name}.#{field} = '" + (Date.today - v.first.to_i).strftime("%Y-%m-%d") + "'"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
when ">t+"
sql = sql + "#{Issue.table_name}.#{field} >= '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'"
sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
when "<t+"
sql = sql + "#{Issue.table_name}.#{field} <= '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
when "t+"
sql = sql + "#{Issue.table_name}.#{field} = '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
when "t"
sql = sql + "#{Issue.table_name}.#{field} = '%s'" % connection.quoted_date(Date.today)
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)]
when "~"
sql = sql + "#{Issue.table_name}.#{field} LIKE '%#{connection.quote_string(v.first)}%'"
sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
when "!~"
sql = sql + "#{Issue.table_name}.#{field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
end
sql << ')'
filters_clauses << sql
end if filters and valid?
sql
clause << ' AND ' unless clause.empty?
clause << filters_clauses.join(' AND ') unless filters_clauses.empty?
clause
end
end

View File

@@ -17,66 +17,49 @@
class Repository < ActiveRecord::Base
belongs_to :project
has_many :changesets, :dependent => :destroy, :order => 'revision DESC'
has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
has_many :changes, :through => :changesets
has_one :latest_changeset, :class_name => 'Changeset', :foreign_key => :repository_id, :order => 'revision DESC'
attr_protected :root_url
validates_presence_of :url
validates_format_of :url, :with => /^(http|https|svn|file):\/\/.+/i
def scm
@scm ||= SvnRepos::Base.new url, root_url, login, password
@scm ||= self.scm_adapter.new url, root_url, login, password
update_attribute(:root_url, @scm.root_url) if root_url.blank?
@scm
end
def url=(str)
super if root_url.blank?
def scm_name
self.class.scm_name
end
def changesets_with_path(path="")
path = "/#{path}%"
path = url.gsub(/^#{root_url}/, '') + path if root_url && root_url != url
path.squeeze!("/")
Changeset.with_scope(:find => { :include => :changes, :conditions => ["#{Change.table_name}.path LIKE ?", path] }) do
yield
end
def supports_cat?
scm.supports_cat?
end
def supports_annotate?
scm.supports_annotate?
end
def fetch_changesets
scm_info = scm.info
if scm_info
lastrev_identifier = scm_info.lastrev.identifier.to_i
if latest_changeset.nil? || latest_changeset.revision < lastrev_identifier
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = latest_changeset ? latest_changeset.revision + 1 : 1
while (identifier_from <= lastrev_identifier)
# loads changesets by batches of 200
identifier_to = [identifier_from + 199, lastrev_identifier].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,
:comment => 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 unless revisions.nil?
identifier_from = identifier_to + 1
end
end
end
def entries(path=nil, identifier=nil)
scm.entries(path, identifier)
end
def diff(path, rev, rev_to, type)
scm.diff(path, rev, rev_to, type)
end
# Default behaviour: we search in cached changesets
def changesets_for_path(path)
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)
end
def latest_changeset
@latest_changeset ||= changesets.find(:first)
end
def scan_changesets_for_issue_ids
self.changesets.each(&:scan_comment_for_issue_ids)
end
# fetch new changesets for all repositories
@@ -85,4 +68,24 @@ class Repository < ActiveRecord::Base
def self.fetch_changesets
find(:all).each(&:fetch_changesets)
end
# scan changeset comments to find related and fixed issues for all repositories
def self.scan_changesets_for_issue_ids
find(:all).each(&:scan_changesets_for_issue_ids)
end
def self.scm_name
'Abstract'
end
def self.available_scm
subclasses.collect {|klass| [klass.scm_name, klass.name]}
end
def self.factory(klass_name, *args)
klass = "Repository::#{klass_name}".constantize
klass.new(*args)
rescue
nil
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 : 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

@@ -0,0 +1,150 @@
# 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/cvs_adapter'
require 'digest/sha1'
class Repository::Cvs < Repository
validates_presence_of :url, :root_url
def scm_adapter
Redmine::Scm::Adapters::CvsAdapter
end
def self.scm_name
'CVS'
end
def entry(path, identifier)
e = entries(path, identifier)
e ? e.first : nil
end
def entries(path=nil, identifier=nil)
entries=scm.entries(path, identifier)
if entries
entries.each() do |entry|
unless entry.lastrev.nil? || entry.lastrev.identifier
change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
if change
entry.lastrev.identifier=change.changeset.revision
entry.lastrev.author=change.changeset.committer
entry.lastrev.revision=change.revision
entry.lastrev.branch=change.branch
end
end
end
end
entries
end
def diff(path, rev, rev_to, type)
#convert rev to revision. CVS can't handle changesets here
diff=[]
changeset_from=changesets.find_by_revision(rev)
if rev_to.to_i > 0
changeset_to=changesets.find_by_revision(rev_to)
end
changeset_from.changes.each() do |change_from|
revision_from=nil
revision_to=nil
revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
if revision_from
if changeset_to
changeset_to.changes.each() do |change_to|
revision_to=change_to.revision if change_to.path==change_from.path
end
end
unless revision_to
revision_to=scm.get_previous_revision(revision_from)
end
diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
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.
# last one is the next step to take. the commit-date is not equal for all
# commits in one changeset. cvs update the commit-date when the *,v file was touched. so
# we use a small delta here, to merge all changes belonging to _one_ changeset
time_delta=10.seconds
transaction do
scm.revisions('', last_commit, 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])
revision
cs = changesets.find(:first, :conditions=>{
:committed_on=>revision.time-time_delta..revision.time+time_delta,
:committer=>revision.author,
:comments=>revision.message
})
# create a new changeset....
unless cs
# we use a negative changeset-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)
end
#convert CVS-File-States to internal Action-abbrevations
#default action is (M)odified
action="M"
if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
action="A" #add-action always at first revision (= 1.1)
elsif revision.paths[0][:action]=="dead"
action="D" #dead-state is similar to Delete
end
Change.create(:changeset => cs,
:action => action,
:path => scm.with_leading_slash(revision.paths[0][:path]),
:revision => revision.paths[0][:revision],
:branch => revision.paths[0][:branch]
)
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!
end
end
end
end

View File

@@ -0,0 +1,90 @@
# 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/darcs_adapter'
class Repository::Darcs < Repository
validates_presence_of :url
def scm_adapter
Redmine::Scm::Adapters::DarcsAdapter
end
def self.scm_name
'Darcs'
end
def entries(path=nil, identifier=nil)
entries=scm.entries(path, identifier)
if entries
entries.each do |entry|
# Search the DB for the entry's last change
changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank?
if changeset
entry.lastrev.identifier = changeset.revision
entry.lastrev.name = changeset.revision
entry.lastrev.time = changeset.committed_on
entry.lastrev.author = changeset.committer
end
end
end
entries
end
def diff(path, rev, rev_to, type)
patch_from = changesets.find_by_revision(rev)
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)
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
# latest revision in the repository
scm_revision = scm_info.lastrev.scmid
unless changesets.find_by_scmid(scm_revision)
revisions = scm.revisions('', db_last_id, nil, :with_path => true)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => next_rev,
:scmid => revision.scmid,
: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],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
next_rev += 1
end if revisions
end
end
end
end
end

View File

@@ -0,0 +1,81 @@
# 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/mercurial_adapter'
class Repository::Mercurial < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
Redmine::Scm::Adapters::MercurialAdapter
end
def self.scm_name
'Mercurial'
end
def entries(path=nil, identifier=nil)
entries=scm.entries(path, identifier)
if entries
entries.each do |entry|
next unless entry.is_file?
# 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
entry.lastrev.identifier = change.changeset.revision
entry.lastrev.name = change.changeset.revision
entry.lastrev.author = change.changeset.committer
entry.lastrev.revision = change.revision
end
end
end
entries
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.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])
end
end
end
end
end
end
end

View File

@@ -0,0 +1,74 @@
# 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/subversion_adapter'
class Repository::Subversion < Repository
attr_protected :root_url
validates_presence_of :url
validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
def scm_adapter
Redmine::Scm::Adapters::SubversionAdapter
end
def self.scm_name
'Subversion'
end
def changesets_for_path(path)
revisions = scm.revisions(path)
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
end
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision : 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,
: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 unless revisions.nil?
identifier_from = identifier_to + 1
end
end
end
end
end

View File

@@ -16,22 +16,93 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Role < ActiveRecord::Base
before_destroy :check_integrity
has_and_belongs_to_many :permissions
# Built-in roles
BUILTIN_NON_MEMBER = 1
BUILTIN_ANONYMOUS = 2
before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all
has_many :members
acts_as_list
serialize :permissions
attr_protected :builtin
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
def permissions
read_attribute(:permissions) || []
end
def permissions=(perms)
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact if perms
write_attribute(:permissions, perms)
end
def <=>(role)
position <=> role.position
end
# Return true if the role is a builtin role
def builtin?
self.builtin != 0
end
# Return true if the role is a project member role
def member?
!self.builtin?
end
# Return true if role is allowed to do the specified action
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
def allowed_to?(action)
if action.is_a? Hash
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
else
allowed_permissions.include? action
end
end
# Return all the permissions that can be given to the role
def setable_permissions
setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
setable_permissions
end
# Find all the roles that can be given to a project member
def self.find_all_givable
find(:all, :conditions => {:builtin => 0}, :order => 'position')
end
# Return the builtin 'non member' role
def self.non_member
find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER}) || raise('Missing non-member builtin role.')
end
# Return the builtin 'anonymous' role
def self.anonymous
find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS}) || raise('Missing anonymous builtin role.')
end
private
def check_integrity
raise "Can't delete role" if Member.find(:first, :conditions =>["role_id=?", self.id])
def allowed_permissions
@allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
end
def allowed_actions
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
end
def check_deletable
raise "Can't delete role" if members.any?
raise "Can't delete builtin role" if builtin?
end
end

View File

@@ -17,31 +17,67 @@
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|
next unless plugin.settings
@@available_settings["plugin_#{id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
end
validates_uniqueness_of :name
validates_inclusion_of :name, :in => @@available_settings.keys
validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
# Hash used to cache setting values
@cached_settings = {}
@cached_cleared_on = Time.now
def self.get(name)
name = name.to_s
setting = find_by_name(name)
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
setting
def value
v = read_attribute(:value)
# Unserialize serialized settings
v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
v
end
def value=(v)
v = v.to_yaml if v && @@available_settings[name]['serialized']
write_attribute(:value, v)
end
# Returns the value of the setting named name
def self.[](name)
get(name).value
v = @cached_settings[name]
v ? v : (@cached_settings[name] = find_or_default(name).value)
end
def self.[]=(name, value)
setting = get(name)
setting.value = (value ? value.to_s : "")
def self.[]=(name, v)
setting = find_or_default(name)
setting.value = (v ? v : "")
@cached_settings[name] = nil
setting.save
setting.value
end
# Defines getter and setter for each setting
# Then setting values can be read using: Setting.some_setting_name
# or set using Setting.some_setting_name = "some value"
@@available_settings.each do |name, params|
src = <<-END_SRC
def self.#{name}
@@ -49,7 +85,7 @@ class Setting < ActiveRecord::Base
end
def self.#{name}?
self[:#{name}].to_s == "1"
self[:#{name}].to_i > 0
end
def self.#{name}=(value)
@@ -58,4 +94,26 @@ class Setting < ActiveRecord::Base
END_SRC
class_eval src, __FILE__, __LINE__
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
def self.check_cache
settings_updated_on = Setting.maximum(:updated_on)
if settings_updated_on && @cached_cleared_on <= settings_updated_on
@cached_settings.clear
@cached_cleared_on = Time.now
logger.info "Settings cache cleared." if logger
end
end
private
# Returns the Setting instance for the setting named name
# (record found in database or new record with default value)
def self.find_or_default(name)
name = name.to_s
raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
setting = find_by_name(name)
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
end
end

View File

@@ -1,431 +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.
require 'rexml/document'
require 'cgi'
module SvnRepos
class CommandFailed < StandardError #:nodoc:
end
class Base
def initialize(url, root_url=nil, login=nil, password=nil)
@url = url
@login = login if login && !login.empty?
@password = (password || "") if @login
@root_url = root_url.blank? ? retrieve_root_url : root_url
end
def root_url
@root_url
end
def url
@url
end
# get info about the svn repository
def info
cmd = "svn info --xml #{target('')}"
cmd << " --username #{@login} --password #{@password}" if @login
info = nil
shellout(cmd) do |io|
begin
doc = REXML::Document.new(io)
#root_url = doc.elements["info/entry/repository/root"].text
info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
:lastrev => Revision.new({
:identifier => doc.elements["info/entry/commit"].attributes['revision'],
:time => Time.parse(doc.elements["info/entry/commit/date"].text),
:author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
})
})
rescue
end
end
return nil if $? && $?.exitstatus != 0
info
rescue Errno::ENOENT => e
return nil
end
# Returns the entry identified by path and revision identifier
# or nil if entry doesn't exist in the repository
def entry(path=nil, identifier=nil)
e = entries(path, identifier)
e ? e.first : nil
end
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
def entries(path=nil, identifier=nil)
path ||= ''
identifier = 'HEAD' unless identifier and identifier > 0
entries = Entries.new
cmd = "svn list --xml #{target(path)}@#{identifier}"
cmd << " --username #{@login} --password #{@password}" if @login
shellout(cmd) do |io|
begin
doc = REXML::Document.new(io)
doc.elements.each("lists/list/entry") do |entry|
entries << Entry.new({:name => entry.elements['name'].text,
:path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
:kind => entry.attributes['kind'],
:size => (entry.elements['size'] and entry.elements['size'].text).to_i,
:lastrev => Revision.new({
:identifier => entry.elements['commit'].attributes['revision'],
:time => Time.parse(entry.elements['commit'].elements['date'].text),
:author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
})
})
end
rescue
end
end
return nil if $? && $?.exitstatus != 0
entries.sort_by_name
rescue Errno::ENOENT => e
raise CommandFailed
end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path ||= ''
identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
revisions = Revisions.new
cmd = "svn log --xml -r #{identifier_from}:#{identifier_to}"
cmd << " --username #{@login} --password #{@password}" if @login
cmd << " --verbose " if options[:with_paths]
cmd << target(path)
shellout(cmd) do |io|
begin
doc = REXML::Document.new(io)
doc.elements.each("log/logentry") do |logentry|
paths = []
logentry.elements.each("paths/path") do |path|
paths << {:action => path.attributes['action'],
:path => path.text,
:from_path => path.attributes['copyfrom-path'],
:from_revision => path.attributes['copyfrom-rev']
}
end
paths.sort! { |x,y| x[:path] <=> y[:path] }
revisions << Revision.new({:identifier => logentry.attributes['revision'],
:author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
:time => Time.parse(logentry.elements['date'].text),
:message => logentry.elements['msg'].text,
:paths => paths
})
end
rescue
end
end
return nil if $? && $?.exitstatus != 0
revisions
rescue Errno::ENOENT => e
raise CommandFailed
end
def diff(path, identifier_from, identifier_to=nil, type="inline")
path ||= ''
if identifier_to and identifier_to.to_i > 0
identifier_to = identifier_to.to_i
else
identifier_to = identifier_from.to_i - 1
end
cmd = "svn diff -r "
cmd << "#{identifier_to}:"
cmd << "#{identifier_from}"
cmd << "#{target(path)}@#{identifier_from}"
cmd << " --username #{@login} --password #{@password}" if @login
diff = []
shellout(cmd) do |io|
io.each_line do |line|
diff << line
end
end
return nil if $? && $?.exitstatus != 0
DiffTableList.new diff, type
rescue Errno::ENOENT => e
raise CommandFailed
end
def cat(path, identifier=nil)
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
cmd = "svn cat #{target(path)}@#{identifier}"
cmd << " --username #{@login} --password #{@password}" if @login
cat = nil
shellout(cmd) do |io|
cat = io.read
end
return nil if $? && $?.exitstatus != 0
cat
rescue Errno::ENOENT => e
raise CommandFailed
end
private
def retrieve_root_url
info = self.info
info ? info.root_url : nil
end
def target(path)
path ||= ""
base = path.match(/^\//) ? root_url : url
" \"" << "#{base}/#{path}".gsub(/["'?<>\*]/, '') << "\""
end
def logger
RAILS_DEFAULT_LOGGER
end
def shellout(cmd, &block)
logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
IO.popen(cmd, "r+") do |io|
io.close_write
block.call(io) if block_given?
end
end
end
class Entries < Array
def sort_by_name
sort {|x,y|
if x.kind == y.kind
x.name <=> y.name
else
x.kind <=> y.kind
end
}
end
def revisions
revisions ||= Revisions.new(collect{|entry| entry.lastrev})
end
end
class Info
attr_accessor :root_url, :lastrev
def initialize(attributes={})
self.root_url = attributes[:root_url] if attributes[:root_url]
self.lastrev = attributes[:lastrev]
end
end
class Entry
attr_accessor :name, :path, :kind, :size, :lastrev
def initialize(attributes={})
self.name = attributes[:name] if attributes[:name]
self.path = attributes[:path] if attributes[:path]
self.kind = attributes[:kind] if attributes[:kind]
self.size = attributes[:size].to_i if attributes[:size]
self.lastrev = attributes[:lastrev]
end
def is_file?
'file' == self.kind
end
def is_dir?
'dir' == self.kind
end
end
class Revisions < Array
def latest
sort {|x,y| x.time <=> y.time}.last
end
end
class Revision
attr_accessor :identifier, :author, :time, :message, :paths
def initialize(attributes={})
self.identifier = attributes[:identifier]
self.author = attributes[:author]
self.time = attributes[:time]
self.message = attributes[:message] || ""
self.paths = attributes[:paths]
end
end
# A line of Diff
class Diff
attr_accessor :nb_line_left
attr_accessor :line_left
attr_accessor :nb_line_right
attr_accessor :line_right
attr_accessor :type_diff_right
attr_accessor :type_diff_left
def initialize ()
self.nb_line_left = ''
self.nb_line_right = ''
self.line_left = ''
self.line_right = ''
self.type_diff_right = ''
self.type_diff_left = ''
end
def inspect
puts '### Start Line Diff ###'
puts self.nb_line_left
puts self.line_left
puts self.nb_line_right
puts self.line_right
end
end
class DiffTableList < Array
def initialize (diff, type="inline")
diff_table = DiffTable.new type
diff.each do |line|
if line =~ /^Index: (.*)$/
self << diff_table if diff_table.length > 1
diff_table = DiffTable.new type
end
a = diff_table.add_line line
end
self << diff_table
end
end
# Class for create a Diff
class DiffTable < Hash
attr_reader :file_name, :line_num_l, :line_num_r
# Initialize with a Diff file and the type of Diff View
# The type view must be inline or sbs (side_by_side)
def initialize (type="inline")
@parsing = false
@nb_line = 1
@start = false
@before = 'same'
@second = true
@type = type
end
# Function for add a line of this Diff
def add_line(line)
unless @parsing
if line =~ /^Index: (.*)$/
@file_name = $1
return false
elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
@line_num_l = $2.to_i
@line_num_r = $5.to_i
@parsing = true
end
else
if line =~ /^_+$/
self.delete(self.keys.sort.last)
@parsing = false
return false
elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
@line_num_l = $2.to_i
@line_num_r = $5.to_i
else
@nb_line += 1 if parse_line(line, @type)
end
end
return true
end
def inspect
puts '### DIFF TABLE ###'
puts "file : #{file_name}"
self.each do |d|
d.inspect
end
end
private
# Test if is a Side By Side type
def sbs?(type, func)
if @start and type == "sbs"
if @before == func and @second
tmp_nb_line = @nb_line
self[tmp_nb_line] = Diff.new
else
@second = false
tmp_nb_line = @start
@start += 1
@nb_line -= 1
end
else
tmp_nb_line = @nb_line
@start = @nb_line
self[tmp_nb_line] = Diff.new
@second = true
end
unless self[tmp_nb_line]
@nb_line += 1
self[tmp_nb_line] = Diff.new
else
self[tmp_nb_line]
end
end
# Escape the HTML for the diff
def escapeHTML(line)
CGI.escapeHTML(line).gsub(/\s/, '&nbsp;')
end
def parse_line (line, type="inline")
if line[0, 1] == "+"
diff = sbs? type, 'add'
@before = 'add'
diff.line_left = escapeHTML line[1..-1]
diff.nb_line_left = @line_num_l
diff.type_diff_left = 'diff_in'
@line_num_l += 1
true
elsif line[0, 1] == "-"
diff = sbs? type, 'remove'
@before = 'remove'
diff.line_right = escapeHTML line[1..-1]
diff.nb_line_right = @line_num_r
diff.type_diff_right = 'diff_out'
@line_num_r += 1
true
elsif line[0, 1] =~ /\s/
@before = 'same'
@start = false
diff = Diff.new
diff.line_right = escapeHTML line[1..-1]
diff.nb_line_right = @line_num_r
diff.line_left = escapeHTML line[1..-1]
diff.nb_line_left = @line_num_l
self[@nb_line] = diff
@line_num_l += 1
@line_num_r += 1
true
else
false
end
end
end
end

View File

@@ -1,3 +1,20 @@
# 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 TimeEntry < ActiveRecord::Base
# could have used polymorphic association
# project association here allows easy loading of time entries at project level with one database trip
@@ -10,7 +27,7 @@ class TimeEntry < ActiveRecord::Base
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true
validates_length_of :comment, :maximum => 255
validates_length_of :comments, :maximum => 255
def before_validation
self.project = issue.project if issue && project.nil?
@@ -28,6 +45,6 @@ class TimeEntry < ActiveRecord::Base
super
self.tyear = spent_on ? spent_on.year : nil
self.tmonth = spent_on ? spent_on.month : nil
self.tweek = spent_on ? spent_on.cweek : nil
self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
end
end

View File

@@ -24,8 +24,19 @@ class Tracker < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
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,11 +18,18 @@
require "digest/sha1"
class User < ActiveRecord::Base
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :dependent => :delete_all
# Account statuses
STATUS_ANONYMOUS = 0
STATUS_ACTIVE = 1
STATUS_REGISTERED = 2
STATUS_LOCKED = 3
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
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_key, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
belongs_to :auth_source
attr_accessor :password, :password_confirmation
@@ -30,25 +37,24 @@ class User < ActiveRecord::Base
# Prevents unauthorized assignments
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
validates_presence_of :login, :firstname, :lastname, :mail
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
validates_uniqueness_of :login, :mail
# 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_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
# Account statuses
STATUS_ACTIVE = 1
STATUS_REGISTERED = 2
STATUS_LOCKED = 3
def before_create
self.mail_notification = false
true
end
def before_save
# update hashed_password if password was set
self.hashed_password = User.hash_password(self.password) if self.password
@@ -100,12 +106,8 @@ class User < ActiveRecord::Base
end
# Return user's full name for display
def display_name
firstname + " " + lastname
end
def name
display_name
"#{firstname} #{lastname}"
end
def active?
@@ -124,26 +126,109 @@ class User < ActiveRecord::Base
User.hash_password(clear_password) == self.hashed_password
end
def role_for_project(project)
member = memberships.detect {|m| m.project_id == project.id}
member ? member.role : nil
end
def pref
self.preference ||= UserPreference.new(:user => self)
end
def get_or_create_rss_key
self.rss_key || Token.create(:user => self, :action => 'feeds')
def time_zone
self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
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')
token.value
end
# Return an array of project ids for which the user has explicitly turned mail notifications on
def notified_projects_ids
@notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
end
def notified_project_ids=(ids)
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
@notified_projects_ids = nil
notified_projects_ids
end
def self.find_by_rss_key(key)
token = Token.find_by_value(key)
token && token.user.active? ? token.user : nil
end
def self.find_by_autologin_key(key)
token = Token.find_by_action_and_value('autologin', key)
token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
end
def <=>(user)
lastname <=> user.lastname
user.nil? ? -1 : (lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname)
end
def to_s
name
end
def logged?
true
end
# Return user's role for project
def role_for_project(project)
# No role on archived projects
return nil unless project && project.active?
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
end
end
# Return true if the user is a member of project
def member_of?(project)
role_for_project(project).member?
end
# Return true if the user is allowed to do the specified action on project
# 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?)
end
def self.current=(user)
@current_user = user
end
def self.current
@current_user ||= User.anonymous
end
def self.anonymous
return @anonymous_user if @anonymous_user
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 = anonymous_user
end
private
@@ -152,3 +237,19 @@ private
Digest::SHA1.hexdigest(clear_password || "")
end
end
class AnonymousUser < User
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
# 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

@@ -26,11 +26,15 @@ class UserPreference < ActiveRecord::Base
self.others ||= {}
end
def before_save
self.others ||= {}
end
def [](attr_name)
if attribute_present? attr_name
super
else
others[attr_name]
others ? others[attr_name] : nil
end
end
@@ -38,7 +42,8 @@ class UserPreference < ActiveRecord::Base
if attribute_present? attr_name
super
else
others.store attr_name, value
self.others ||= {}
self.others.store attr_name, value
end
end
end

View File

@@ -23,7 +23,8 @@ class Version < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date
validates_length_of :name, :maximum => 30
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date, :allow_nil => true
def start_date
effective_date
@@ -33,6 +34,61 @@ class Version < ActiveRecord::Base
effective_date
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)
end
def open_issues_count
@open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
end
def closed_issues_count
@closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
end
def wiki_page
if project.wiki && !wiki_page_title.blank?
@wiki_page ||= project.wiki.find_page(wiki_page_title)
end
@wiki_page
end
def to_s; name end
# Versions are sorted by effective_date
# 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
else
version.effective_date ? 1 : (self.name <=> version.name)
end
end
private
def check_integrity
raise "Can't delete version" if self.fixed_issues.find(:first)

23
app/models/watcher.rb Normal file
View File

@@ -0,0 +1,23 @@
# 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 Watcher < ActiveRecord::Base
belongs_to :watchable, :polymorphic => true
belongs_to :user
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
end

View File

@@ -18,27 +18,37 @@
class Wiki < ActiveRecord::Base
belongs_to :project
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
validates_presence_of :start_page
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
# find the page with the given title
# if page doesn't exist, return a new page
def find_or_new_page(title)
title = Wiki.titleize(title || start_page)
find_page(title) || WikiPage.new(:wiki => self, :title => title)
title = start_page if title.blank?
find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
end
# find the page with the given title
def find_page(title)
pages.find_by_title(Wiki.titleize(title || start_page))
def find_page(title, options = {})
title = start_page if title.blank?
title = Wiki.titleize(title)
page = pages.find_by_title(title)
if !page && !(options[:with_redirect] == false)
# search for a redirect
redirect = redirects.find_by_title(title)
page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
end
page
end
# turn a string into a valid page title
def self.titleize(title)
# replace spaces with _ and remove unwanted caracters
title = title.gsub(/\s+/, '_').delete(',;|') if title
title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
# upcase the first letter
title = title[0..0].upcase + title[1..-1] if title
title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
title
end
end

View File

@@ -18,15 +18,22 @@
require 'zlib'
class WikiContent < ActiveRecord::Base
set_locking_column :version
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
validates_presence_of :text
acts_as_versioned
class Version
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,
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
def text=(plain)
case Setting.wiki_compression
when 'gzip'

View File

@@ -15,17 +15,55 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'diff'
class WikiPage < ActiveRecord::Base
belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
:description => :text,
:datetime => :created_on,
:url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
acts_as_searchable :columns => ['title', 'text'],
:include => [:wiki, :content],
:project_key => "#{Wiki.table_name}.project_id"
attr_accessor :redirect_existing_links
validates_presence_of :title
validates_format_of :title, :with => /^[^,\s]*$/
validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
validates_associated :content
def title=(value)
value = Wiki.titleize(value)
@previous_title = read_attribute(:title) if @previous_title.blank?
write_attribute(:title, value)
end
def before_save
self.title = Wiki.titleize(title)
self.title = Wiki.titleize(title)
# Manage redirects if the title has changed
if !@previous_title.blank? && (@previous_title != title) && !new_record?
# Update redirects that point to the old title
wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
r.redirects_to = title
r.title == r.redirects_to ? r.destroy : r.save
end
# Remove redirects for the new title
wiki.redirects.find_all_by_title(title).each(&:destroy)
# Create a redirect to the new title
wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
@previous_title = nil
end
end
def before_destroy
# Remove redirects to this page
wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
end
def pretty_title
@@ -38,7 +76,40 @@ class WikiPage < ActiveRecord::Base
result
end
def diff(version_to=nil, version_from=nil)
version_to = version_to ? version_to.to_i : self.content.version
version_from = version_from ? version_from.to_i : version_to - 1
version_to, version_from = version_from, version_to unless version_from < version_to
content_to = content.versions.find_by_version(version_to)
content_from = content.versions.find_by_version(version_from)
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
end
def self.pretty_title(str)
(str && str.is_a?(String)) ? str.tr('_', ' ') : str
end
def project
wiki.project
end
def text
content.text if content
end
end
class WikiDiff
attr_reader :diff, :words, :content_to, :content_from
def initialize(content_to, content_from)
@content_to = content_to
@content_from = content_from
@words = content_to.text.split(/(\s+)/)
@words = @words.select {|word| word != ' '}
words_from = content_from.text.split(/(\s+)/)
words_from = words_from.select {|word| word != ' '}
@diff = words_from.diff @words
end
end

View File

@@ -0,0 +1,23 @@
# 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 WikiRedirect < ActiveRecord::Base
belongs_to :wiki
validates_presence_of :title, :redirects_to
validates_length_of :title, :redirects_to, :maximum => 255
end

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