Compare commits

..

54 Commits
1.1.2 ... nbc

Author SHA1 Message Date
Nicolas Chuche
8c669eeb80 r18658@gaspard (orig r1900): jplang | 2008-09-22 21:50:10 +0200
Truncate comments on changeset list.
 r18659@gaspard (orig r1901):  jplang | 2008-09-23 19:03:51 +0200
 Fixes html escaping.
 r18660@gaspard (orig r1902):  winterheart | 2008-09-24 16:45:20 +0200
 Patch #1938, update for nl.yml
 r18661@gaspard (orig r1903):  jplang | 2008-09-24 19:30:36 +0200
 Fixes back_url in login filter (#1900).
 r18662@gaspard (orig r1904):  jplang | 2008-09-24 19:32:49 +0200
 Reverts r1903.
 r18663@gaspard (orig r1905):  jplang | 2008-09-24 19:33:02 +0200
 Fixes back_url in login filter (#1900).
 r18667@gaspard (orig r1907):  jplang | 2008-09-25 20:51:03 +0200
 Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled.
 r18669@gaspard (orig r1909):  winterheart | 2008-09-27 21:06:48 +0200
 Fixed #1961, pt-br update
 r18670@gaspard (orig r1910):  jplang | 2008-09-28 09:54:41 +0200
 Fixed: Latest news appear on the homepage for projects with the News module disabled (#1941).
 r18671@gaspard (orig r1911):  jplang | 2008-09-28 10:05:55 +0200
 Fixed: the default status is lost when reordering issue statuses (#1955).
 r18672@gaspard (orig r1912):  jplang | 2008-09-28 10:19:25 +0200
 Wrap 'Assigned to' column on the issue list (#1960).
 r18673@gaspard (orig r1913):  jplang | 2008-09-28 10:41:17 +0200
 Fixed: Status list on bulk edit form does not follow normal sequence (#1956).
 r18674@gaspard (orig r1914):  jplang | 2008-09-28 14:03:17 +0200
 Adds a workflow overview screen.
 Workflow setup moved to a dedicated controller.
 r18675@gaspard (orig r1915):  jplang | 2008-09-28 14:20:47 +0200
 Fixes workflow setup link on trackers list (follows r1914).
 r18676@gaspard (orig r1916):  jplang | 2008-09-28 14:36:30 +0200
 Slight changes to the workflow setup screen.
 r18677@gaspard (orig r1917):  jplang | 2008-09-28 15:10:00 +0200
 Fixes Workflow.count_by_tracker_and_role.
 r18678@gaspard (orig r1918):  edavis10 | 2008-09-30 01:55:11 +0200
 Slight non-code change
 
 r18679@gaspard (orig r1919):  edavis10 | 2008-09-30 01:56:35 +0200
 Reverting slight non-code change
 
 r18680@gaspard (orig r1920):  edavis10 | 2008-09-30 02:02:46 +0200
 Slight non-code change to test git sync
 
 r18681@gaspard (orig r1921):  edavis10 | 2008-09-30 07:18:50 +0200
 Adds :view_layouts_base_body_bottom hook
 
 r18682@gaspard (orig r1922):  edavis10 | 2008-10-02 04:40:29 +0200
 Fixed a failing assertion in test_post_edit_with_attachment_only that would
 occur when running the full test suite but not the functional test suite.
 
 r18683@gaspard (orig r1923):  edavis10 | 2008-10-02 05:23:35 +0200
 Added tests to cover IssueStatus.destroy and IssueStatus.check_integrity
 
 r18684@gaspard (orig r1924):  jplang | 2008-10-04 19:38:31 +0200
 Escape image filename regexp (#1971).
 r18685@gaspard (orig r1925):  winterheart | 2008-10-05 21:30:58 +0200
 #1988, update for ko.yml
 r18686@gaspard (orig r1926):  winterheart | 2008-10-05 22:40:25 +0200
 Patch #1987, ca.yml update, thanks to Joan Duran for file
 r18687@gaspard (orig r1927):  winterheart | 2008-10-06 17:00:56 +0200
 #1992 update pt.yml, thanks to Pedro Araújo
 r18688@gaspard (orig r1928):  winterheart | 2008-10-07 19:41:16 +0200
 Patch #2001, update for Polish language
 r18689@gaspard (orig r1929):  winterheart | 2008-10-11 13:32:30 +0200
 Patch #2005, nl.yml update
 r18690@gaspard (orig r1930):  jplang | 2008-10-12 21:13:36 +0200
 Remove pre tag attributes.
 r18691@gaspard (orig r1931):  nbc | 2008-10-16 00:30:57 +0200
 bugfix to two failed tests
 r18692@gaspard (orig r1932):  nbc | 2008-10-16 01:50:33 +0200
 add plain text option for mail #2029
 r18693@gaspard (orig r1933):  jplang | 2008-10-16 21:13:43 +0200
 Makes email address case-insensitive in MailHandler (#2032).
 r18694@gaspard (orig r1934):  winterheart | 2008-10-16 22:50:50 +0200
 #2036 update for hu.yml
 r18695@gaspard (orig r1935):  winterheart | 2008-10-16 22:51:27 +0200
 Update for ru.yml
 r18696@gaspard (orig r1936):  edavis10 | 2008-10-18 01:30:37 +0200
 Added a plugin hook :routes that plugins can use to add and even override routes
 
 r18697@gaspard (orig r1937):  winterheart | 2008-10-18 12:03:50 +0200
 #2043, #2044, #2046, translation updates
 r18698@gaspard (orig r1938):  jplang | 2008-10-18 12:07:49 +0200
 Adds 'Delete wiki pages attachments' permission.
 r18699@gaspard (orig r1939):  jplang | 2008-10-18 12:18:21 +0200
 Show the most recent file when displaying an inline image.
 r18700@gaspard (orig r1940):  jplang | 2008-10-18 12:42:29 +0200
 link_to project homepage instead of auto_link (#1937).
 r18701@gaspard (orig r1941):  jplang | 2008-10-18 13:25:27 +0200
 Fixed: textile footnotes no longer work after r1113 (#974).
 r18702@gaspard (orig r1942):  winterheart | 2008-10-23 17:24:16 +0200
 #1928 it.yml update
 r18703@gaspard (orig r1943):  jplang | 2008-10-24 17:24:35 +0200
 Makes permission screens localized (#2070).
 r18704@gaspard (orig r1944):  jplang | 2008-10-24 17:39:40 +0200
 AuthSource list: display associated users count and disable 'Delete' buton if any (#2041).
 r18705@gaspard (orig r1945):  jplang | 2008-10-24 18:59:15 +0200
 Adds the ability to search for a user on the administration users list.
 r18706@gaspard (orig r1946):  jplang | 2008-10-24 19:01:42 +0200
 Adds functional test for user search.
 r18707@gaspard (orig r1947):  jplang | 2008-10-24 19:12:39 +0200
 Adds the ability to search for a project name or identifier on the administration projects list.
 r18708@gaspard (orig r1948):  edavis10 | 2008-10-25 06:21:57 +0200
 Added hook :view_repositories_show_contextual to allow adding items to the
 repository's contextual menu.
 
   #2073
 
 r18709@gaspard (orig r1949):  edavis10 | 2008-10-25 06:37:31 +0200
 Renamed the .rb files in the plugin_generator to end in .erb.  The .rb was
 causing rdoc to try to document them and fail.
 
 * Updated the generator's manifest to use the new files
 * Renamed template README to README.rdoc
 
   #2011
 
 r18710@gaspard (orig r1950):  edavis10 | 2008-10-25 06:46:21 +0200
 Added the board's description below the board's name.
 
 Thanks to Go MAEDA for the patch.  #2079
 
 r18711@gaspard (orig r1951):  jplang | 2008-10-25 11:35:51 +0200
 Renames template ruby files to erb.
 r18712@gaspard (orig r1952):  jplang | 2008-10-25 11:55:31 +0200
 Adds #delete_menu_item to the plugin API (#2087).
 r18713@gaspard (orig r1953):  jplang | 2008-10-25 12:23:29 +0200
 Check that git changeset is not in the database before creating it (#1419).
 r18714@gaspard (orig r1954):  jplang | 2008-10-26 16:17:26 +0100
 Slight change to english string (#2088).
 r18715@gaspard (orig r1955):  jplang | 2008-10-27 12:08:29 +0100
 Makes wiki text formatter pluggable.
 Original patch #2025 by Yuki Sonoda slightly edited.
 r18716@gaspard (orig r1956):  jplang | 2008-10-27 12:50:23 +0100
 Adds back textile acronyms support (#2077).
 r18717@gaspard (orig r1957):  jplang | 2008-10-27 13:34:01 +0100
 Makes GLoc language global.
 r18718@gaspard (orig r1958):  jplang | 2008-10-28 11:43:34 +0100
 Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format (#2102).
 r18719@gaspard (orig r1959):  winterheart | 2008-10-28 17:08:19 +0100
 #2080, #2097, #2100 - ja, zh-tw, zh updates
 r18720@gaspard (orig r1960):  edavis10 | 2008-10-28 21:29:38 +0100
 Added :view_timelog_edit_form_bottom hook to the timelog/edit form.
 
 r18721@gaspard (orig r1961):  winterheart | 2008-10-29 00:31:14 +0100
 Update for ru.yml
 
 r18722@gaspard (orig r1962):  edavis10 | 2008-10-30 03:58:04 +0100
 Gravatar support for issue detai, user grid, and activity stream
 
 r18723@gaspard (orig r1963):  edavis10 | 2008-10-30 03:58:10 +0100
 styling tweaks for gravatars
 
 r18724@gaspard (orig r1964):  edavis10 | 2008-10-30 03:58:16 +0100
 styling tweaks for gravatars
 
 r18725@gaspard (orig r1965):  edavis10 | 2008-10-30 03:58:23 +0100
 Reduced the size of the gravatar on the issue history
 
 r18726@gaspard (orig r1966):  edavis10 | 2008-10-30 03:58:28 +0100
 Fixed a bug with using gravatar on a nil value.
 
 r18727@gaspard (orig r1967):  edavis10 | 2008-10-30 03:58:34 +0100
 Added gravatar image to the user's public account page
 
 r18728@gaspard (orig r1968):  edavis10 | 2008-10-30 04:29:30 +0100
 Fixed typo in an English string, 'View calender'
 
 r18729@gaspard (orig r1969):  edavis10 | 2008-10-30 04:49:04 +0100
 Link the version name to VersionsController#show in the issue list.
 
 r18730@gaspard (orig r1970):  edavis10 | 2008-10-31 01:09:36 +0100
 Tweaking of the CSS for the gravatars. #1776
 
 r18731@gaspard (orig r1971):  edavis10 | 2008-10-31 01:19:48 +0100
 Tighened up the gravator CSS in the issue div
 
 r18732@gaspard (orig r1972):  edavis10 | 2008-10-31 01:41:28 +0100
 Added an option to turn user Gravatars on or off
 
 * Option can be found in Administration > General, called
   "Use Gravatar user icons"
 * Defaulting Gravatars to off
 * Added a helper gravatar_for_mail to check the setting before rendering
   the Gravatar.
 
   #1776
 
 r18733@gaspard (orig r1973):  winterheart | 2008-10-31 15:38:09 +0100
 Populating new string with rake gloc:update
 r18734@gaspard (orig r1974):  winterheart | 2008-10-31 15:48:07 +0100
 Update pt-rb, #2105
 r18735@gaspard (orig r1975):  winterheart | 2008-10-31 15:49:33 +0100
 Update zh-tw, #2116
 r18736@gaspard (orig r1976):  winterheart | 2008-10-31 15:58:05 +0100
 update ru.yml
 r18737@gaspard (orig r1977):  winterheart | 2008-11-01 17:42:49 +0100
 #2121, pt-br update
 r18738@gaspard (orig r1978):  edavis10 | 2008-11-04 19:27:13 +0100
 Added :view_projects_form plugin hook
 
 r18739@gaspard (orig r1979):  edavis10 | 2008-11-06 06:37:29 +0100
 Included Redmine::Hook::Helper to ActionController::Base so call_hook
 is available in all controllers. #2111
 
 r18740@gaspard (orig r1980):  winterheart | 2008-11-07 11:41:10 +0100
 #2127, #2129, #2130, #2135, translation updates. Thanks to all participants :)
 r18741@gaspard (orig r1981):  winterheart | 2008-11-07 11:53:09 +0100
 Intial support Vietnamese language (#2125), thanks to Kỳ Anh Huỳnh for work
 r18742@gaspard (orig r1982):  winterheart | 2008-11-07 12:07:25 +0100
 Ooops, wrong.
 r18743@gaspard (orig r1983):  winterheart | 2008-11-07 12:12:12 +0100
 Intial support Vietnamese language (#2125), thanks to Kỳ Anh Huỳnh for work (now - really)
 r18744@gaspard (orig r1984):  winterheart | 2008-11-07 12:20:25 +0100
 refreshing vn.yml (#2125)
 r18745@gaspard (orig r1985):  winterheart | 2008-11-07 12:22:26 +0100
 D'oh...
 
 r18746@gaspard (orig r1986):  winterheart | 2008-11-07 12:28:29 +0100
 Update for pl.yml, #1299
 
 r18747@gaspard (orig r1987):  jplang | 2008-11-07 14:08:01 +0100
 French translation update.
 r18748@gaspard (orig r1988):  jplang | 2008-11-07 15:35:18 +0100
 Email address should be lowercased for gravatar (#2145).
 r18749@gaspard (orig r1989):  jplang | 2008-11-07 16:37:17 +0100
 Host setting should contain the path prefix (Redmine base URL) to properly generate links in emails that are sent offline (#2122).
 r18750@gaspard (orig r1990):  jplang | 2008-11-07 18:27:56 +0100
 Fixed: broken subject when submitting issue via email written in japanese (Patch #2059 by Go MAEDA).
 r18751@gaspard (orig r1991):  edavis10 | 2008-11-08 01:12:43 +0100
 Removing the custom Redmine hook in routes in favor of Engine's hook.
 
 * Plugins' routes.rb are now added automatically to Redmine's routing,
   including the ability to override Redmine's default routing.
 
   Thank you to Jean-Baptiste Barth for the suggestion.  #2142
 
 r18752@gaspard (orig r1992):  jplang | 2008-11-08 14:25:45 +0100
 Do not use @:skip_relative_url_root@ to generate urls in Mailer (#2122).
 r18753@gaspard (orig r1993):  jplang | 2008-11-08 16:18:02 +0100
 Fixes syntax highlighting broken by r1930 (#2143).
 r18754@gaspard (orig r1994):  jplang | 2008-11-08 16:28:00 +0100
 Fixed Bazaar shared repository browsing (#2101, patch #1685 by Dmitry Shaposhnik).
 r18755@gaspard (orig r1995):  jplang | 2008-11-08 16:50:51 +0100
 Tells git to output dates in ISO format.
 Fixes: Git Adapter date parsing ignores timezone (#2149).
 r18756@gaspard (orig r1996):  jplang | 2008-11-08 18:15:18 +0100
 git path reverted.
 r18757@gaspard (orig r1997):  winterheart | 2008-11-08 23:34:41 +0100
 #2126, initial support of Slovak, thank to Stanislav Pach for translation
 r18758@gaspard (orig r1998):  winterheart | 2008-11-09 01:29:20 +0100
 populating new string, updates for ru.yml and sv.yml (#2126)
 r18759@gaspard (orig r1999):  jplang | 2008-11-09 13:07:35 +0100
 Git adapter: use commit time instead of author time (#2108).
 r18760@gaspard (orig r2000):  jplang | 2008-11-09 15:52:16 +0100
 Changes ApplicationHelper#gravatar_for_mail to #avatar that takes a User or a String (less code in views).
 r18761@gaspard (orig r2001):  jplang | 2008-11-09 18:53:30 +0100
 Fixes activity date param.
 r18762@gaspard (orig r2002):  jplang | 2008-11-09 18:56:20 +0100
 Link to activity view when displaying dates.
 r18763@gaspard (orig r2003):  jplang | 2008-11-09 21:39:49 +0100
 Hide Redmine version in atom feeds and pdf properties (#794).
 r18764@gaspard (orig r2004):  jplang | 2008-11-10 12:33:04 +0100
 Fixed: non-ASCII subversion path can't be displayed (patch #1993 by Chaoqun Zou).
 r18765@gaspard (orig r2005):  jplang | 2008-11-10 13:23:54 +0100
 Include GLoc in hook listener base class (#2112).
 r18766@gaspard (orig r2006):  jplang | 2008-11-10 19:59:06 +0100
 Maps repository users to Redmine users (#1383).
 Users with same username or email are automatically mapped. Mapping can be manually adjusted in repository settings. Multiple usernames can be mapped to the same Redmine user.
 r18767@gaspard (orig r2007):  jplang | 2008-11-10 20:09:00 +0100
 Eager-load users.
 r18768@gaspard (orig r2008):  jplang | 2008-11-11 13:07:03 +0100
 Fixes a typo in en.yml.
 r18769@gaspard (orig r2009):  jplang | 2008-11-11 13:50:11 +0100
 Eager-load users.
 r18770@gaspard (orig r2010):  jplang | 2008-11-11 13:59:28 +0100
 Sort users by their display names so that user dropdown lists are sorted alphabetically (#2015).
 r18771@gaspard (orig r2011):  jplang | 2008-11-11 14:22:05 +0100
 Trac importer improvements (patch #2050 by Karl Heinz Marbaise).
 r18772@gaspard (orig r2012):  jplang | 2008-11-11 14:28:13 +0100
 Fixed: Trac migration of ticket:123 or [ticket:34] do not work (#2053).
 r18773@gaspard (orig r2013):  jplang | 2008-11-11 14:28:48 +0100
 Fixed: Trac migration of ticket:123 or [ticket:34] do not work (#2053).
 r18774@gaspard (orig r2014):  jplang | 2008-11-11 14:32:22 +0100
 Fixed: Trac milestone links not correctly converted (#2052).
 r18775@gaspard (orig r2015):  jplang | 2008-11-11 14:37:10 +0100
 Documents Wiki page anchors (#1647).
 r18776@gaspard (orig r2016):  jplang | 2008-11-11 14:49:07 +0100
 Updated pt-br and zh-tw lang files.
 r18777@gaspard (orig r2017):  jplang | 2008-11-11 14:54:10 +0100
 Changes ruby bang path to #!/usr/bin/env ruby (#1876).
 r18778@gaspard (orig r2018):  jplang | 2008-11-11 15:24:06 +0100
 Turn ftps and sftp proto into links (#1514).
 r18779@gaspard (orig r2019):  jplang | 2008-11-11 16:07:55 +0100
 Adds permissions to let users edit and/or delete their messages (#854, patch by Markus Knittig with slight changes).
 r18780@gaspard (orig r2020):  jplang | 2008-11-11 17:26:05 +0100
 Less agressive Redcloth lang attribute parsing (#2091).
 r18781@gaspard (orig r2021):  jplang | 2008-11-11 17:49:20 +0100
 Hungarian language file updated.
 r18782@gaspard (orig r2022):  jplang | 2008-11-11 19:10:21 +0100
 Pluggable admin menu (patch #2031 by Yuki Sonoda with slight changes).
 r18783@gaspard (orig r2023):  winterheart | 2008-11-12 16:13:49 +0100
 update for pt-br (#2164)
 r18784@gaspard (orig r2024):  winterheart | 2008-11-12 16:17:47 +0100
 update for zh (#2151)
 r18785@gaspard (orig r2025):  winterheart | 2008-11-12 16:18:55 +0100
 Populating new strings for zh.yml
 r18786@gaspard (orig r2026):  winterheart | 2008-11-12 16:22:57 +0100
 New file for sk (#2126)
 r18787@gaspard (orig r2027):  winterheart | 2008-11-12 16:23:52 +0100
 Populating new strings for sk.yml
 r18788@gaspard (orig r2028):  winterheart | 2008-11-12 16:34:11 +0100
 update for ru
 r18789@gaspard (orig r2029):  edavis10 | 2008-11-13 02:07:58 +0100
 Changed the CSS clear on journals so they will wrap around the revisions. #2165
 
 r18790@gaspard (orig r2030):  jplang | 2008-11-13 17:39:50 +0100
 Fixes #2171: issue pdf export broken by r2006.
 r18791@gaspard (orig r2031):  jplang | 2008-11-13 17:43:39 +0100
 Fixes #2170: user display format in application settings broken by r2010.
 r18792@gaspard (orig r2032):  winterheart | 2008-11-14 16:00:23 +0100
 Missed %s in label, thank Martin Bächtold for reporting (#2186)
 r18793@gaspard (orig r2033):  winterheart | 2008-11-14 16:18:13 +0100
 Translation updates (#2168, #2172, #2176, #2178)
 r18794@gaspard (orig r2034):  winterheart | 2008-11-14 16:33:27 +0100
 Polish update, #2188
 r18795@gaspard (orig r2035):  winterheart | 2008-11-15 09:35:17 +0100
 Translation updates (#2189, #2193)
 r18796@gaspard (orig r2036):  jplang | 2008-11-16 12:49:37 +0100
 Changes version naming rule (#2162).
 r18797@gaspard (orig r2037):  jplang | 2008-11-16 12:58:41 +0100
 Moves plugin list to its own administration menu item.
 r18798@gaspard (orig r2038):  jplang | 2008-11-16 16:22:48 +0100
 Adds plugin id attribute.
 r18799@gaspard (orig r2039):  jplang | 2008-11-16 16:38:37 +0100
 Adds .find and .all Plugin class methods.
 r18800@gaspard (orig r2040):  jplang | 2008-11-16 17:08:25 +0100
 Adds a few Plugin tests.
 r18801@gaspard (orig r2041):  jplang | 2008-11-16 18:12:02 +0100
 Adds url and author_url plugin attributes (#2162).
 r18802@gaspard (orig r2042):  jplang | 2008-11-16 21:00:20 +0100
 Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version (#2162).
 r18803@gaspard (orig r2043):  jplang | 2008-11-17 18:27:08 +0100
 Do not query multiple times git for branch (#1435).
 r18804@gaspard (orig r2044):  jplang | 2008-11-18 18:22:28 +0100
 Vietnamese language updated (#2125).
 r18805@gaspard (orig r2045):  jplang | 2008-11-18 19:36:47 +0100
 SubversionAdapter#entries performance improvement.
 r18806@gaspard (orig r2046):  jplang | 2008-11-18 22:11:25 +0100
 Fixed: Printing long roadmap doesn't split across pages (#2203).
 r18807@gaspard (orig r2047):  winterheart | 2008-11-19 16:52:09 +0100
 Typo in sv, #2213
 r18808@gaspard (orig r2048):  jplang | 2008-11-19 20:38:19 +0100
 Remove eclipse files
 r18811@gaspard (orig r2051):  winterheart | 2008-11-21 17:35:00 +0100
 fix for Polish, #2215
 r18812@gaspard (orig r2052):  winterheart | 2008-11-21 17:40:11 +0100
 removing BOM, sorting, #2169
 r18813@gaspard (orig r2053):  jplang | 2008-11-22 12:44:07 +0100
 Extends child_pages macro to display child pages based on page parameter (#1975).
 It can also be called from anywhere now (not only from wiki pages).
 r18814@gaspard (orig r2054):  jplang | 2008-11-23 17:40:35 +0100
 Fixed date filters accuracy with SQLite (#2221).
 r18815@gaspard (orig r2055):  jplang | 2008-11-25 18:37:41 +0100
 Slight tests fixes.
 r18816@gaspard (orig r2056):  jplang | 2008-11-25 20:33:41 +0100
 Do not request blank LDAP attributes.
 r18817@gaspard (orig r2057):  winterheart | 2008-11-26 18:32:56 +0100
 rake gloc:update, update for Serbian (#2232)
 r18819@gaspard (orig r2059):  jplang | 2008-11-27 19:04:48 +0100
 Adds a css class on menu items in order to apply item specific styles (eg. icons).
 r18820@gaspard (orig r2060):  jplang | 2008-11-27 19:41:40 +0100
 Typo in lang files (#2241).
 r18821@gaspard (orig r2061):  jplang | 2008-11-27 19:43:18 +0100
 Typo in gloc:update task description (#2243).
 r18822@gaspard (orig r2062):  jplang | 2008-11-27 21:15:45 +0100
 Fixed: inappropriate redirection to login or register page may occur (#2206). Eg. user clicks login link twice before logging in.
 r18823@gaspard (orig r2063):  winterheart | 2008-11-28 16:44:59 +0100
 Italian update (#2239)
 r18826@gaspard (orig r2066):  jplang | 2008-11-30 12:18:22 +0100
 Display latest user's activity on account/show view.
 r18827@gaspard (orig r2067):  jplang | 2008-11-30 13:12:06 +0100
 Makes activity view accept a user_id param to show user's activity (#1002).
 r18828@gaspard (orig r2068):  jplang | 2008-11-30 13:14:12 +0100
 Fixes activity atom link params (when not on first page).
 r18829@gaspard (orig r2069):  jplang | 2008-11-30 13:18:59 +0100
 Adds atom feed on user's account page.
 r18830@gaspard (orig r2070):  jplang | 2008-11-30 14:38:07 +0100
 Adds links between account and user's activity pages.
 r18831@gaspard (orig r2071):  jplang | 2008-11-30 14:42:15 +0100
 Slight changes to profile on account page and last connexion date added.
 r18832@gaspard (orig r2072):  jplang | 2008-11-30 15:23:57 +0100
 Obfuscates email address on user's account page using javascript.
 r18833@gaspard (orig r2073):  jplang | 2008-11-30 15:31:01 +0100
 Adds link to user's account on issue history.
 r18834@gaspard (orig r2074):  jplang | 2008-11-30 15:55:45 +0100
 Mail handler: check workflow for status set/change.
 r18835@gaspard (orig r2075):  jplang | 2008-11-30 15:57:46 +0100
 Adds status option to email integration rake tasks.
 r18836@gaspard (orig r2076):  jplang | 2008-11-30 16:51:44 +0100
 Adds --status option to rdm-mailhandler.
 r18837@gaspard (orig r2077):  jplang | 2008-11-30 17:00:45 +0100
 Adds To and Cc as watchers when submitting an issue by email (#2245).
 Only works if the sender has the 'Add issue watchers' permission.
 r18838@gaspard (orig r2078):  jplang | 2008-11-30 17:34:39 +0100
 Changes Portuguese decimal separator (#1372).
 r18839@gaspard (orig r2079):  jplang | 2008-11-30 17:57:56 +0100
 Replaces User.find_active with a named scope.
 r18840@gaspard (orig r2080):  winterheart | 2008-12-01 17:00:54 +0100
 Translation updates (#2249, #2250, #2252, #2254)
 r18841@gaspard (orig r2081):  winterheart | 2008-12-01 17:11:05 +0100
 ru.yml update
 r18842@gaspard (orig r2082):  jplang | 2008-12-01 18:27:44 +0100
 Fixed: 404 when "Apply" clicked on activity page (#2251).
 r18843@gaspard (orig r2083):  jplang | 2008-12-02 18:16:06 +0100
 Fixed: activity broken by r2066 with postgresql (#2266).
 r18844@gaspard (orig r2084):  jplang | 2008-12-02 18:29:52 +0100
 Use style attribute for setting width of table cells in progress bars (#2267).
 r18845@gaspard (orig r2085):  jplang | 2008-12-02 18:57:13 +0100
 Fixed: wrong digest for text files under Windows (#2264).
 r18846@gaspard (orig r2086):  edavis10 | 2008-12-04 00:18:07 +0100
 Added :controller_issues_edit_before_save hook
 
 r18847@gaspard (orig r2087):  edavis10 | 2008-12-04 00:18:12 +0100
 Added :view_issues_edit_notes_bottom hook
 
 r18848@gaspard (orig r2088):  jplang | 2008-12-05 16:41:32 +0100
 Cross-project gantt and calendar (#1157).
 r18849@gaspard (orig r2089):  edavis10 | 2008-12-05 22:03:55 +0100
 Added :view_issues_history_journal_bottom hook
 
 r18850@gaspard (orig r2090):  edavis10 | 2008-12-05 23:56:03 +0100
 Refactor: Extracted new method Query#sql_for_field from Query#statement in
 order to clean up Query#statement.
 
 r18851@gaspard (orig r2091):  edavis10 | 2008-12-05 23:56:08 +0100
 Bit more refactoring on Query#sql_for_field to remove multiple returns
 
 r18852@gaspard (orig r2092):  edavis10 | 2008-12-05 23:56:13 +0100
 Final refactoring on Query#sql_for_field to rename v to value
 
 r18853@gaspard (orig r2093):  edavis10 | 2008-12-06 01:51:03 +0100
 Added several useful hooks to the Issue sidebar
 
 * :view_issues_sidebar_issues_bottom
 * :view_issues_sidebar_planning_bottom
 * :view_issues_sidebar_queries_bottom
 
 r18854@gaspard (orig r2094):  jplang | 2008-12-06 12:21:10 +0100
 Changes issue history headings.
 r18855@gaspard (orig r2095):  jplang | 2008-12-06 18:20:37 +0100
 Fixes Darcs#cat with Postgresql.
 r18856@gaspard (orig r2096):  jplang | 2008-12-06 18:40:54 +0100
 Fixed: CVS connexion string may not contain @.
 r18857@gaspard (orig r2097):  jplang | 2008-12-06 19:01:20 +0100
 Slight change to css so that gravatar is vertically centered on user's page.
 r18858@gaspard (orig r2098):  jplang | 2008-12-06 23:40:50 +0100
 Translations updates.
 r18860@gaspard (orig r2100):  jplang | 2008-12-07 09:41:54 +0100
 Changelog updated.
 r18861@gaspard (orig r2101):  jplang | 2008-12-07 09:48:29 +0100
 Show project name in front of related issues if cross-project issue relations are enabled (#2282).
 r18862@gaspard (orig r2102):  jplang | 2008-12-07 10:53:27 +0100
 Upgrade to Rails 2.1.2
 r18863@gaspard (orig r2103):  jplang | 2008-12-07 10:54:37 +0100
 Set version to 0.8
 r18864@gaspard (orig r2104):  jplang | 2008-12-07 10:56:28 +0100
 Update changelog for 0.8 rc1
 r18865@gaspard (orig r2105):  jplang | 2008-12-07 10:59:19 +0100
 UPGRADING updated
 r18869@gaspard (orig r2109):  jplang | 2008-12-07 14:12:19 +0100
 Makes logged-in username in topbar linking to (#2291).
 r18870@gaspard (orig r2110):  jplang | 2008-12-07 15:40:33 +0100
 Use options hash in UnifiedDiff.new
 r18871@gaspard (orig r2111):  jplang | 2008-12-07 15:44:08 +0100
 Follows r2110.
 r18872@gaspard (orig r2112):  jplang | 2008-12-07 16:21:40 +0100
 Adds a setting to limit the number of diff lines that should be displayed (default to 1500).
 r18874@gaspard (orig r2114):  jplang | 2008-12-08 19:20:26 +0100
 Fixed: project activity truncated after viewing user's activity.
 r18876@gaspard (orig r2116):  jplang | 2008-12-09 17:54:46 +0100
 AttachmentsController now handles attachments deletion.
 r18877@gaspard (orig r2117):  jplang | 2008-12-09 19:00:27 +0100
 Files module: makes version field non required (#1053).
 r18878@gaspard (orig r2118):  jplang | 2008-12-09 19:30:22 +0100
 Fixed: Firefox cuts off large diffs (#2234).
 r18879@gaspard (orig r2119):  winterheart | 2008-12-10 18:01:39 +0100
 Translation updates (#2310, #2309, #2306, #2304, #2302, #2300, #2299)
 r18880@gaspard (orig r2120):  winterheart | 2008-12-10 18:13:04 +0100
 russian update
 r18881@gaspard (orig r2121):  edavis10 | 2008-12-11 00:44:22 +0100
 Added plugin hooks around Journal editing
 
 * :controller_journals_edit_post
 * :view_journals_notes_form_after_notes
 * :view_journals_update_rjs_bottom
 
 r18882@gaspard (orig r2122):  jplang | 2008-12-12 13:07:09 +0100
 Makes User.find_by_mail case-insensitive (password reminder #2322, repo users mapping).
 r18883@gaspard (orig r2123):  jplang | 2008-12-12 14:32:39 +0100
 Fixed: default flag removed when editing a default enumeration (#2327).
 r18884@gaspard (orig r2124):  jplang | 2008-12-12 14:49:14 +0100
 Fixed: default category ignored when adding a document (#2328).
 r18885@gaspard (orig r2125):  jplang | 2008-12-12 17:01:35 +0100
 Escape back_url field value (#2320).
 r18886@gaspard (orig r2126):  jplang | 2008-12-12 17:03:57 +0100
 Rescue back_url param parsing on redirect.
 r18887@gaspard (orig r2127):  jplang | 2008-12-12 17:04:54 +0100
 Undo unwanted change.
 r18888@gaspard (orig r2128):  jplang | 2008-12-12 17:07:14 +0100
 Capture scm CLI stderr to log/scm.stderr.log when running in dev environment
 r18889@gaspard (orig r2129):  jplang | 2008-12-12 20:11:16 +0100
 Make use of User.find_by_mail
 r18890@gaspard (orig r2130):  winterheart | 2008-12-12 20:34:31 +0100
 translation updates
 r18891@gaspard (orig r2131):  winterheart | 2008-12-12 20:41:12 +0100
 Fixing quotes
 r18894@gaspard (orig r2134):  jplang | 2008-12-14 16:36:59 +0100
 Rails 2.1.2 deprecations (#2332).
 r18895@gaspard (orig r2135):  jplang | 2008-12-14 16:57:13 +0100
 Fixed: CVS browser should not show dead revisions (deleted files) (#2319).
 r18896@gaspard (orig r2136):  jplang | 2008-12-14 18:10:16 +0100
 Mail handler: strip tags when receiving a html-only email (#2312).
 r18897@gaspard (orig r2137):  jplang | 2008-12-15 19:02:25 +0100
 Fixes repository user mapping submission when a repository username is blank (#2339, Conflicting types for parameter containers).
 r18899@gaspard (orig r2139):  jplang | 2008-12-16 22:11:37 +0100
 Adds a helper that returns issues css classes.
 r18900@gaspard (orig r2140):  jplang | 2008-12-16 22:13:35 +0100
 Adds a css class (overdue) to overdue issues on issue lists and detail views (#2337).
 r18901@gaspard (orig r2141):  edavis10 | 2008-12-18 08:10:23 +0100
 Fixed a failing test caused by comparing a Time object (n.day.ago) with a Date object
 
 r18902@gaspard (orig r2142):  winterheart | 2008-12-18 23:27:32 +0100
 Typo on translation, #2352
 r18903@gaspard (orig r2143):  jplang | 2008-12-19 09:10:35 +0100
 Escape textarea content when editing a issue note.
 r18904@gaspard (orig r2144):  jplang | 2008-12-19 11:16:15 +0100
 Escape double-quotes in image titles.
 r18905@gaspard (orig r2145):  jplang | 2008-12-19 11:43:06 +0100
 Check that wiki page exists before processing (#2360).
 r18907@gaspard (orig r2147):  jplang | 2008-12-19 15:13:24 +0100
 CHANGELOG updated.
 r18924@gaspard (orig r2164):  jplang | 2008-12-22 20:21:02 +0100
 Adds watchers selection on new issue form (#398). Permission 'add issue watchers' required.
 r18925@gaspard (orig r2165):  jplang | 2008-12-22 20:24:17 +0100
 Do not hardcode Watcher string in r2164.
 r18926@gaspard (orig r2166):  jplang | 2008-12-22 20:25:07 +0100
 Sligth change to fr.yml.
 r18927@gaspard (orig r2167):  jplang | 2008-12-22 21:33:01 +0100
 Show view/annotate/download links on repositories/entries and repositories/annotate views (#2367).
 r18928@gaspard (orig r2168):  jplang | 2008-12-23 01:16:26 +0100
 Escape wiki annotate lines content (#2380).
 r18929@gaspard (orig r2169):  jplang | 2008-12-23 01:19:15 +0100
 Escape query names (#2379).
 r18930@gaspard (orig r2170):  jplang | 2008-12-23 18:05:38 +0100
 Escape textile titles and styles (#2377).
 r18931@gaspard (orig r2171):  jplang | 2008-12-24 11:03:13 +0100
 Validates sort_key and sort_order params (#2378).
 r18938@gaspard (orig r2178):  jplang | 2008-12-24 14:29:43 +0100
 Fixes a JS error on context_menu with IE (#2390).
 r18940@gaspard (orig r2180):  winterheart | 2008-12-24 16:44:43 +0100
 #2329, swedish lang update
 
 r18941@gaspard (orig r2181):  winterheart | 2008-12-24 16:47:24 +0100
 #2368, pt.yml update
 
 r18942@gaspard (orig r2182):  winterheart | 2008-12-24 16:48:59 +0100
 #2386, korean translation update
 
 r18943@gaspard (orig r2183):  jplang | 2008-12-27 15:05:03 +0100
 Prevent SQL error with old sessions after r2171.
 r18946@gaspard (orig r2186):  jplang | 2008-12-27 18:49:01 +0100
 Fixtures update.
 r18947@gaspard (orig r2187):  jplang | 2008-12-27 19:07:46 +0100
 Fixes functional test failures.
 r18948@gaspard (orig r2188):  jplang | 2008-12-27 19:10:36 +0100
 Do not show a link to the current annotate or view page (#2367).
 r18949@gaspard (orig r2189):  jplang | 2008-12-27 19:33:35 +0100
 Fixed: deleted files should not be shown when browsing a Darcs repository (#2385).
 r18950@gaspard (orig r2190):  jplang | 2008-12-28 10:46:16 +0100
 Fixes functional tests fixtures (#2398).
 r18951@gaspard (orig r2191):  jplang | 2008-12-28 11:12:09 +0100
 Fixed bold syntax around single character in series (#2351).
 r18952@gaspard (orig r2192):  jplang | 2008-12-28 14:38:34 +0100
 Disable textile inline styles to prevent XSS attacks (#2377).
 r18955@gaspard (orig r2195):  jplang | 2008-12-28 15:48:23 +0100
 Mail handler: add watchers before sending notification (#2245).
 r18956@gaspard (orig r2196):  jplang | 2008-12-29 13:40:56 +0100
 Renumbers projects_trackers fixtures (#2411).
 r18959@gaspard (orig r2199):  jplang | 2008-12-29 16:43:42 +0100
 Translations updates.
 r18961@gaspard (orig r2201):  jplang | 2008-12-29 17:08:31 +0100
 CHANGELOG updated.
 r18962@gaspard (orig r2202):  winterheart | 2008-12-29 19:27:27 +0100
 #2373, fixing encoding
 
 r18968@gaspard (orig r2208):  jplang | 2008-12-30 14:32:14 +0100
 CHANGELOG updated.
 r18969@gaspard (orig r2209):  jplang | 2008-12-30 14:32:51 +0100
 Increment project files downloads.
 r18970@gaspard (orig r2210):  jplang | 2008-12-30 15:24:51 +0100
 Jump to the current tab when using the project quick-jump combo (#2364).
 r18971@gaspard (orig r2211):  jplang | 2008-12-30 15:57:33 +0100
 Import custom fields values from emails (#2413).
 r18972@gaspard (orig r2212):  jplang | 2008-12-30 17:23:05 +0100
 Stricter textile links parsing (#2417).
 r18973@gaspard (orig r2213):  jplang | 2008-12-30 17:43:26 +0100
 Changes pt-br decimal separator (#1372).
 r18974@gaspard (orig r2214):  jplang | 2008-12-31 11:39:33 +0100
 Do not escape back_url twice when login fails.
 r18978@gaspard (orig r2218):  jplang | 2008-12-31 12:48:56 +0100
 Admin Info Screen: Display if plugin assets directory is writable (#2425).
 r18979@gaspard (orig r2219):  jplang | 2008-12-31 14:59:30 +0100
 Fix sv lang file
 r18980@gaspard (orig r2220):  jplang | 2008-12-31 15:56:30 +0100
 IMAP: add options to move received emails.
 r18981@gaspard (orig r2221):  jplang | 2009-01-03 14:09:36 +0100
 Lower the project identifier limit to a minimum of two characters (#2003).
 r18982@gaspard (orig r2222):  jplang | 2009-01-03 14:14:28 +0100
 Fixed: syntax highlight doesn't appear in new ticket preview (#1976).
 r18983@gaspard (orig r2223):  jplang | 2009-01-03 15:11:44 +0100
 Moves flash messages rendering to a helper method.
 r18984@gaspard (orig r2224):  jplang | 2009-01-03 15:44:12 +0100
 Display a warning if some attachments were not saved (#2008).
 r18985@gaspard (orig r2225):  jplang | 2009-01-03 17:03:12 +0100
 Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets (#1957).
 r18986@gaspard (orig r2226):  jplang | 2009-01-04 13:03:39 +0100
 Move PDF stuff to a single helper.
 r18987@gaspard (orig r2227):  jplang | 2009-01-04 13:14:05 +0100
 Makes the app boot with Rails 2.2.2
 r18988@gaspard (orig r2228):  jplang | 2009-01-04 13:50:45 +0100
 Do not use compute_public_path.
 r18992@gaspard (orig r2232):  jplang | 2009-01-04 14:27:48 +0100
 Merged r2231 from 0.8-stable (#2402).
 r18993@gaspard (orig r2233):  jplang | 2009-01-04 15:54:19 +0100
 Scramble PDF title (#1204).
 r18994@gaspard (orig r2234):  jplang | 2009-01-04 18:09:25 +0100
 Slight changes to ease Rails 2.2 support.
 r18995@gaspard (orig r2235):  jplang | 2009-01-04 19:14:51 +0100
 Slight changes in functional tests.
 r19006@gaspard (orig r2246):  jplang | 2009-01-07 20:47:24 +0100
 Makes issue description a non-required field (#2456).
 r19007@gaspard (orig r2247):  jplang | 2009-01-07 21:03:33 +0100
 Refactor TabularFormBuilder field helpers (#2461).
 r19008@gaspard (orig r2248):  jplang | 2009-01-07 21:21:27 +0100
 Fixes functional test broken by r2246.
 r19009@gaspard (orig r2249):  jplang | 2009-01-07 21:22:06 +0100
 Fixes a test failure with svn < 1.5 (#2455).
 r19010@gaspard (orig r2250):  jplang | 2009-01-07 21:30:02 +0100
 Adds 'closed' css class to closed issues in the issue list (#2458).
 r19011@gaspard (orig r2251):  jplang | 2009-01-09 18:32:46 +0100
 Fixed: no error is raised when entering invalid hours on the issue update form (#2465).
 r19013@gaspard (orig r2253):  jplang | 2009-01-10 12:29:35 +0100
 Makes email adress uniqueness case-insensitive (#2473).
 r19016@gaspard (orig r2256):  jplang | 2009-01-11 12:01:35 +0100
 Different icon for closed issues in search result (#992).
 r19017@gaspard (orig r2257):  jplang | 2009-01-11 17:33:51 +0100
 Ability to sort the issue list by text, list, date and boolean custom fields (#1139).
 r19018@gaspard (orig r2258):  jplang | 2009-01-11 19:38:07 +0100
 Ability to sort the issue list by text, int and float custom fields (#1139).
 r19019@gaspard (orig r2259):  jplang | 2009-01-11 20:48:16 +0100
 Use margin-right instead of padding-right on top menu links.
 r19020@gaspard (orig r2260):  edavis10 | 2009-01-12 05:44:01 +0100
 Codified instructions from RUNNING_TESTS as rake tasks for convenience
 
 Rake tasks are in testing.rake and can be run by `rake test:scm:setup:<scm>`
 Updated RUNNING_TESTS
 
 Contributed by Gerrit Kaiser
 
 r19021@gaspard (orig r2261):  edavis10 | 2009-01-12 05:52:56 +0100
 Added two new plugin hooks to IssuesController:
 
 * :controller_issues_new_after_save
 * :controller_issues_edit_after_save
 
   #2475
 
 r19022@gaspard (orig r2262):  jplang | 2009-01-12 18:45:23 +0100
 Fixes r2226: exporting an issue with attachments to PDF raises an error (#2492).
 r19023@gaspard (orig r2263):  jplang | 2009-01-12 18:46:53 +0100
 Typo (#2489).
 r19025@gaspard (orig r2265):  jplang | 2009-01-16 18:20:41 +0100
 Adds a 'Create and continue' button on the new issue form, that will create the issue and display the form again (#2523).
 r19026@gaspard (orig r2266):  jplang | 2009-01-16 21:57:18 +0100
 Makes subject field get focus on 'New issue' form (#2522).
 r19027@gaspard (orig r2267):  jplang | 2009-01-16 22:02:03 +0100
 Use a textarea for custom fields possible values (#2472).
 r19028@gaspard (orig r2268):  jplang | 2009-01-16 22:02:56 +0100
 Adds custom fields functional tests.
 r19029@gaspard (orig r2269):  jplang | 2009-01-17 08:53:32 +0100
 Slight visual changes on the issue form.
 r19030@gaspard (orig r2270):  jplang | 2009-01-17 09:03:53 +0100
 Do not show Category field when categories are not defined.
 r19031@gaspard (orig r2271):  jplang | 2009-01-17 09:08:33 +0100
 Project jump box fix.
 r19032@gaspard (orig r2272):  jplang | 2009-01-17 09:25:55 +0100
 Make use of tracker_ids association in issue custom field form.
 r19033@gaspard (orig r2273):  jplang | 2009-01-17 09:41:30 +0100
 CustomFieldsController refactoring.
 r19034@gaspard (orig r2274):  jplang | 2009-01-17 09:46:23 +0100
 CustomFieldsController#list moved to #index.
 r19035@gaspard (orig r2275):  jplang | 2009-01-17 10:04:10 +0100
 Moves a few settings to a "Display" panel.
 r19036@gaspard (orig r2276):  jplang | 2009-01-17 12:18:04 +0100
 User custom fields can now be set as editable so that users can edit them on 'My account'.
 For existing user custom fields, this new attribute is set to false by default to preserve the prior behaviour (it can turned on by editing the custom field in admin area).
 
 Note: on the registration form, *required* custom fields will be displayed even if they are not defined as editable so that the account can be created.
 r19039@gaspard (orig r2279):  jplang | 2009-01-18 11:54:08 +0100
 Fixes 103_set_custom_fields_editable migration from r2276 (#2526).
 r19040@gaspard (orig r2280):  jplang | 2009-01-18 12:54:56 +0100
 Fixed that Trac importer was creating duplicate custom values (#2506).
 r19041@gaspard (orig r2281):  jplang | 2009-01-18 16:16:31 +0100
 Adds Message-Id and References headers to email notifications so that issues and messages threads can be displayed by email clients (#1401).
 r19042@gaspard (orig r2282):  jplang | 2009-01-18 21:00:03 +0100
 Fix in AttachmentsController#show.
 r19043@gaspard (orig r2283):  winterheart | 2009-01-19 16:55:54 +0100
 #2439, translation update
 r19044@gaspard (orig r2284):  winterheart | 2009-01-19 16:57:19 +0100
 #2442, translation update
 r19045@gaspard (orig r2285):  winterheart | 2009-01-19 17:02:57 +0100
 #2429, translation update
 r19046@gaspard (orig r2286):  winterheart | 2009-01-19 17:06:39 +0100
 #2442, small fix
 r19047@gaspard (orig r2287):  winterheart | 2009-01-19 17:43:28 +0100
 translation updates (#2535, #2505, #2524, #2434)
 r19048@gaspard (orig r2288):  jplang | 2009-01-19 19:29:07 +0100
 Use In-Reply-To and References headers to handle replies by email.
 r19049@gaspard (orig r2289):  jplang | 2009-01-19 20:03:53 +0100
 Allow email to reply to a forum message (#1616).
 r19050@gaspard (orig r2290):  winterheart | 2009-01-20 16:45:34 +0100
 #2453, sv.yml patch, some errors still exist (see ticket)
 r19051@gaspard (orig r2291):  winterheart | 2009-01-20 16:53:09 +0100
 #2445, nl.yml update
 r19052@gaspard (orig r2292):  winterheart | 2009-01-20 17:09:07 +0100
 #2463, partially solved
 r19053@gaspard (orig r2293):  winterheart | 2009-01-20 17:13:14 +0100
 #2540, pt-br update
 r19054@gaspard (orig r2294):  jplang | 2009-01-21 19:22:30 +0100
 Accept replies to forum messages by subject recognition (#1616).
 r19055@gaspard (orig r2295):  jplang | 2009-01-22 17:34:54 +0100
 Automatically focus several form fields.
 r19056@gaspard (orig r2296):  winterheart | 2009-01-23 16:37:59 +0100
 New Galician Translation (#2547), thanks to Martín Vázquez for intial translation
 r19057@gaspard (orig r2297):  winterheart | 2009-01-23 16:40:38 +0100
 #2562, update for zh.yml
 r19058@gaspard (orig r2298):  winterheart | 2009-01-23 16:46:22 +0100
 Translation updates (#2453, #2463, #2551)
 r19059@gaspard (orig r2299):  winterheart | 2009-01-23 16:58:58 +0100
 removing \r\n
 
 r19060@gaspard (orig r2300):  winterheart | 2009-01-23 17:30:04 +0100
 ru.yml update
 
 r19062@gaspard (orig r2302):  jplang | 2009-01-24 09:58:03 +0100
 Fixed: Details time log report CSV export doesn't honour date format from settings (patch #2466 by Russell Hind).
 r19063@gaspard (orig r2303):  jplang | 2009-01-24 10:02:55 +0100
 Fixes a test that was broken by r2294.
 r19064@gaspard (orig r2304):  jplang | 2009-01-24 12:31:15 +0100
 Merged nested projects branch. Removes limit on subproject nesting (#594).
 r19065@gaspard (orig r2305):  jplang | 2009-01-24 12:48:38 +0100
 Removes unused projects_count column from projects table.
 r19071@gaspard (orig r2311):  jplang | 2009-01-25 12:15:28 +0100
 Ignore archived subprojects in Project#rolled_up_trackers (#2550).
 r19072@gaspard (orig r2312):  jplang | 2009-01-25 13:13:27 +0100
 Fixed that the project jump box does not preserve current tab after r2304.
 r19073@gaspard (orig r2313):  jplang | 2009-01-25 14:12:56 +0100
 Adds ability to bulk copy issues (#1847).
 This can be done by checking the 'Copy' checkbox on the 'Move' form.
 r19074@gaspard (orig r2314):  jplang | 2009-01-25 14:18:44 +0100
 Removes spaces before colons.
 r19075@gaspard (orig r2315):  jplang | 2009-01-25 14:52:40 +0100
 Render the project list as a tree on Move form.
 r19076@gaspard (orig r2316):  jplang | 2009-01-25 17:04:28 +0100
 Ability to bulk edit custom fields of type 'list' (#461).
 r19077@gaspard (orig r2317):  edavis10 | 2009-01-26 02:47:51 +0100
 Converted routing and urls to follow the Rails REST convention.
 
 Patch supplied by commits from Gerrit Kaiser on Github.  Existing routes will
 still work (backwards compatible) but any new urls will be generated using the
 new routing rules.
 
 Changes listed below:
 
 * made the URLs for some project tabs and project settings follow the new rails RESTful conventions of /collection/:id/subcollection/:sub_id
 * prettier URL for project roadmap
 * more nice project URLs
 * use GET for filtering form
 * prettified URLs used on issues tab
 * custom route for activity atom feeds
 * prettier repository urls
 * fixed broken route definition
 * fixed failing tests for issuecontroller that were hardcoding the url string
 * more RESTful routes for boards and messages
 * RESTful routes for wiki pages
 * RESTful routes for documents
 * moved old routes that are retained for compatibility to the bottom and grouped them together
 * added RESTful URIs for issues
 * RESTfulness for the news section
 * fixed route order
 * changed hardcoded URLs in tests
 * fixed badly written tests
 * fixed forgotten parameter in routes
 * changed hardcoded URLS to new scheme
 * changed project add url to the standard POST to collection
 * create new issue by POSTing to collection
 * changed hardcoded URLs in integrations tests
 * made project add form work again
 * restful routes for project deletion
 * prettier routes for project (un)archival
 * made routes table more readable
 * fixed note quoting
 * user routing
 * fixed bug
 * always sort by GET
 * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled.
 * prettified URLs used on issues tab
 * urls for time log
 * fixed reply routing
 * eliminate revision query paremeter for diff and entry actions
 * fixed test failures with hard-coded urls
 * ensure ajax links always use get
 * refactored ajax link generation into separate method
 
   #1901
 
 r19078@gaspard (orig r2318):  jplang | 2009-01-26 18:43:58 +0100
 Fixes activity pagination broken by r2317.
 r19079@gaspard (orig r2319):  jplang | 2009-01-27 18:27:50 +0100
 Replaces the obsolete robots.txt with a cached action (#2491).
 r19080@gaspard (orig r2320):  jplang | 2009-01-27 18:40:55 +0100
 Fixed actions on issues (gantt, calendar, move, bulk_edit...) at global level broken by r2317.
 r19081@gaspard (orig r2321):  jplang | 2009-01-27 18:58:56 +0100
 Explicitly require 'rfpdf/fpdf' (#2584).
 r19082@gaspard (orig r2322):  jplang | 2009-01-27 19:19:27 +0100
 Fixed that 'My page' blocks may display issues that the user is no longer allowed to view (#2590).
 r19083@gaspard (orig r2323):  jplang | 2009-01-27 20:33:03 +0100
 Fixed: users should not be able to add relations with issues they're not allowed to view (#2589).
 r19084@gaspard (orig r2324):  edavis10 | 2009-01-27 21:42:19 +0100
 Fixes Issue sorting in a project, broken by #2317
 
 Issues were sorting but the project id wasn't being added so the
 IssuesController would return all issues (cross-project).
 
 r19085@gaspard (orig r2325):  edavis10 | 2009-01-27 21:59:02 +0100
 Fixed clearing the Issue filters in the issue list, broken by #2317
 
 r19086@gaspard (orig r2326):  jplang | 2009-01-28 21:52:39 +0100
 Fixed user's activity atom feed broken by r2317.
 r19087@gaspard (orig r2327):  jplang | 2009-01-28 22:11:13 +0100
 Fixed calendar navigation links broken by r2317.
 r19088@gaspard (orig r2328):  jplang | 2009-01-28 22:20:39 +0100
 Fixing calendar and gantt links broken by r2317.
 r19089@gaspard (orig r2329):  jplang | 2009-01-28 22:25:35 +0100
 Fixed project news atom link broken by r2317.
 r19090@gaspard (orig r2330):  jplang | 2009-01-29 10:05:36 +0100
 Sort target versions list on bulk edit form (#2616).
 r19091@gaspard (orig r2331):  jplang | 2009-01-29 12:09:46 +0100
 Fixes other formats download links on the project issue list (project_id lost) broken r2317.
 r19092@gaspard (orig r2332):  jplang | 2009-01-29 13:26:32 +0100
 Fixed an error when downloading gantt png at global level.
 r19093@gaspard (orig r2333):  jplang | 2009-01-29 14:53:17 +0100
 Adds an helper to render other formats download links.
 r19094@gaspard (orig r2334):  jplang | 2009-01-29 14:54:44 +0100
 Adds rel='nofollow' attribute to other formats download links (#2491).
 r19095@gaspard (orig r2335):  jplang | 2009-01-29 15:22:56 +0100
 Adds projects association on tracker form (#2578).
 r19096@gaspard (orig r2336):  jplang | 2009-01-29 17:33:45 +0100
 Fixed: TOC does not parse wiki page reference links with description (#2601).
 r19097@gaspard (orig r2337):  jplang | 2009-01-29 17:34:00 +0100
 Cleaning test.
 r19098@gaspard (orig r2338):  jplang | 2009-01-30 18:50:28 +0100
 Changes time related icons.
 r19099@gaspard (orig r2339):  jplang | 2009-01-31 12:43:54 +0100
 Adds :async_smtp and :async_sendmail delivery methods to perform email deliveries asynchronously.
 Code from http://www.datanoise.com/articles/2006/7/14/asynchronous-email-delivery.
 r19100@gaspard (orig r2340):  winterheart | 2009-01-31 13:02:37 +0100
 New translation - Slovenian, thank to Nejc Vidmar for work (#2577), translation updates (#2129, #2586)
 r19101@gaspard (orig r2341):  jplang | 2009-01-31 13:42:02 +0100
 Updates footer year.
 r19102@gaspard (orig r2342):  jplang | 2009-01-31 13:48:09 +0100
 Removes Issue.visible_by
 r19103@gaspard (orig r2343):  jplang | 2009-01-31 14:22:29 +0100
 Fixed: issue details view discloses relations to issues that the user is not allowed to view (#2589).
 r19104@gaspard (orig r2344):  jplang | 2009-01-31 15:50:56 +0100
 Less strict textile links parsing (#2582).
 r19105@gaspard (orig r2345):  jplang | 2009-02-01 15:36:38 +0100
 Fixed: Contextual divs after attachments are placed incorrectly in FireFox (#2633).
 r19106@gaspard (orig r2346):  jplang | 2009-02-01 16:48:56 +0100
 Do not repeat one-line commit logs on the activity view.
 r19107@gaspard (orig r2347):  jplang | 2009-02-01 16:57:01 +0100
 Show line breaks in activity events summary.
 r19108@gaspard (orig r2348):  jplang | 2009-02-01 17:00:20 +0100
 Changes color of activity events/search results summary.
 r19109@gaspard (orig r2349):  jplang | 2009-02-01 19:54:05 +0100
 Use estimated hours to weight issues in version completion calculation (#2182).
 r19110@gaspard (orig r2350):  jplang | 2009-02-01 20:54:50 +0100
 Adds a setting to limit the number of revisions displayed on a repository file log (default=100).
 r19111@gaspard (orig r2351):  jplang | 2009-02-01 21:56:10 +0100
 Include both last and first name when sorting issues by assignee (#1841).
 r19112@gaspard (orig r2352):  jplang | 2009-02-01 21:57:44 +0100
 Include both version date and name when sorting issues by target version (#1502).
 r19113@gaspard (orig r2353):  jplang | 2009-02-02 18:34:12 +0100
 Adds a 'box' div around news comment form (#2632).
 r19119@gaspard (orig r2359):  jplang | 2009-02-03 18:13:37 +0100
 Fixes message search eager loading (#2654).
 r19120@gaspard (orig r2360):  jplang | 2009-02-03 18:15:59 +0100
 Typos/fixes in views (#2654).
 r19121@gaspard (orig r2361):  jplang | 2009-02-03 18:32:07 +0100
 Closed issue are not overdue, fixes r2140 (#2337).
 r19122@gaspard (orig r2362):  jplang | 2009-02-05 18:43:49 +0100
 Typo in wiki link example (#2673).
 r19123@gaspard (orig r2363):  jplang | 2009-02-05 21:25:01 +0100
 Fixed: inline attached image should not match partial filename (#2683).
 r19159@gaspard (orig r2399):  jplang | 2009-02-07 21:11:03 +0100
 Fixed: path parameter is not an array when changing diff style (#2695), broken by r2317.
 r19175@gaspard (orig r2415):  jplang | 2009-02-08 18:24:39 +0100
 Fixed: migration 98 breaks when using table name prefix.
 r19183@gaspard (orig r2423):  jplang | 2009-02-09 18:18:41 +0100
 Fixed: TypeError (can't modify frozen string) on settings view (#2700).
 r19184@gaspard (orig r2424):  jplang | 2009-02-09 18:24:06 +0100
 Removes hardcoded table names (#2701).
 r19186@gaspard (orig r2426):  jplang | 2009-02-09 21:17:58 +0100
 Strip keywords from received email body (#2436).
 r19187@gaspard (orig r2427):  edavis10 | 2009-02-10 02:18:49 +0100
 Added plugin hook :view_projects_roadmap_version_bottom.  #2543
 r19188@gaspard (orig r2428):  edavis10 | 2009-02-10 02:24:32 +0100
 Added two new plugin hooks:
 
 * :view_layouts_base_sidebar
 * :view_layouts_base_content
 r19189@gaspard (orig r2429):  edavis10 | 2009-02-10 04:12:40 +0100
 Added request and controller objects to the hooks by default.
 
 The request and controller objects are now added to all hook contexts by
 default.  This will also make url_for work better in hooks by setting up
 the default_url_options :host, :port, and :protocol.
 
 Finally a new helper method @render_or@ has been added to ViewListener.  This
 will let a hook easily render a partial without a full method definition.
 
 Thanks to Thomas Löber for the original patch.  #2542
 r19190@gaspard (orig r2430):  edavis10 | 2009-02-10 04:12:45 +0100
 Renamed variables to be more descriptive. #2542
 r19191@gaspard (orig r2431):  winterheart | 2009-02-10 16:41:05 +0100
 Updated translations (#2577, #2640, #2644, #2652)
 
 r19192@gaspard (orig r2432):  winterheart | 2009-02-10 16:57:52 +0100
 Translation updates (#2643, #2645, #2668)
 r19193@gaspard (orig r2433):  winterheart | 2009-02-10 17:05:31 +0100
 New language - Macedonian (mk). Thank to Ilin Tatabitovski for work.
 
 r19194@gaspard (orig r2434):  jplang | 2009-02-10 18:18:19 +0100
 Fixes broken action url on time edit form (#2707).
 r19195@gaspard (orig r2435):  jplang | 2009-02-10 23:03:25 +0100
 Replaces the repositories management SOAP API with a simple REST API.
 reposman usage is unchanged but the script now requires activeresource.
 actionwebservice is now longer used and thus removed from plugins.
 r19196@gaspard (orig r2436):  jplang | 2009-02-10 23:54:22 +0100
 Leave wiki links untouched if target project doesn't exist or have no wiki.
 r19197@gaspard (orig r2437):  edavis10 | 2009-02-11 20:06:37 +0100
 Unpacked OpenID gem. #699
 r19198@gaspard (orig r2438):  edavis10 | 2009-02-11 20:06:45 +0100
 Added open_id_authentication plugin
 r19199@gaspard (orig r2439):  edavis10 | 2009-02-11 20:06:50 +0100
 Added OpenID tables. #699
 r19200@gaspard (orig r2440):  edavis10 | 2009-02-11 20:06:55 +0100
 Added identity_url to User. #699
 r19201@gaspard (orig r2441):  edavis10 | 2009-02-11 20:07:00 +0100
 Fixed a bug in open_id_authentication, where relative_url_root is defined
 on ActionController:AbstractRequest not Base
 
   #699
 r19202@gaspard (orig r2442):  edavis10 | 2009-02-11 20:07:07 +0100
 Added the ability to login via OpenID.
 
 * Refactored AccountController#login to use either
   password or openid based authentication
 * Extracted AccountController#successful_authentication
   to setup a user's session cookies and redirect
 * Implemented the start of AccountController#open_id_authentication
   which will check with the OpenID server and perform authentication.
 * Added text field for the OpenID url to /login
 * Added identity_url for OpenID to the user forms.
 * Added option to login with OpenID to the register form.
 * Added a root url route, which is used by the OpenID plugin
 
   #699
 r19203@gaspard (orig r2443):  edavis10 | 2009-02-11 20:07:12 +0100
 Hooked up on the fly OpenID user creation.
 
 * Use OpenID registration fields for the user.
 * Generate a random password when a user is created.
 r19204@gaspard (orig r2444):  edavis10 | 2009-02-11 20:07:18 +0100
 Adding OpenID mock and test. #699
 r19205@gaspard (orig r2445):  edavis10 | 2009-02-11 20:07:23 +0100
 Added tests for the other OpenID authentication cases.  #699
 r19206@gaspard (orig r2446):  edavis10 | 2009-02-11 20:07:28 +0100
 Added user setup needed based on the system's registration settings
 
 * Copied the register action's chunk of code used to setup the account
   based on Setting.self_registration
 * Extracted method for when onthefly_creation_failed
 * Added tests to confirm the behavior
 
   #699
 r19207@gaspard (orig r2447):  edavis10 | 2009-02-11 20:07:34 +0100
 Refactored common methods out of register and open_id_authenticate
 
 * Extracted register_by_email_activation
 * Extracted register_automatically
 * Extracted register_manually_by_administrator
 
   #699
 r19208@gaspard (orig r2448):  edavis10 | 2009-02-11 20:07:41 +0100
 Prevent registration via OpenID if self registration is off. #699
 r19209@gaspard (orig r2449):  edavis10 | 2009-02-11 20:24:28 +0100
 Added a system setting for allowing OpenID logins and registrations
 
 * Defaults to off
 * Is set in the Administration panel under Authentication
 
   #699
 r19210@gaspard (orig r2450):  edavis10 | 2009-02-11 20:45:53 +0100
 Added a space so words don't runtogeatherlikethis. #699
 r19211@gaspard (orig r2451):  jplang | 2009-02-11 21:25:05 +0100
 Slight changes to the issue lists displayed on My page.
 r19212@gaspard (orig r2452):  edavis10 | 2009-02-12 02:32:50 +0100
 Fixed the bundled ruby-openid gem
 
 * The open_id_authentication plugin will require the gem automatically so
   it doesn't need to be added to environment.rb
 * Changed the version requirement on the open_id_authentication to match
   the latest stable version.  Rails config.gem looks for a directory named
   after that specific version and will not load newer versions.
 
   #699
 r19213@gaspard (orig r2453):  edavis10 | 2009-02-12 05:31:28 +0100
 Normalize the identity_url when it's set.
 
 OpenId uses a specific format for the url it uses which requires the protocol
 and trailing slash.  This change will normalize the value to when a user sets it.
 
   #699
 r19214@gaspard (orig r2454):  jplang | 2009-02-12 18:19:32 +0100
 Hide openid stuff on my account if disabled (#699).
 r19215@gaspard (orig r2455):  jplang | 2009-02-12 18:30:56 +0100
 Adds missing strings (#699).
 r19216@gaspard (orig r2456):  jplang | 2009-02-12 18:35:57 +0100
 Adds ability to filter watched issues (#846).
 r19217@gaspard (orig r2457):  jplang | 2009-02-12 18:38:36 +0100
 Link to watched issues list on my page.
 r19218@gaspard (orig r2458):  jplang | 2009-02-12 22:25:50 +0100
 Removes the fat ruby-openid gem. Simply use 'gem install ruby-openid' to enable openid support.
 r19219@gaspard (orig r2459):  jplang | 2009-02-12 23:01:20 +0100
 Issues pagination loses project param after applying or clearing filter (#2726).
 r19220@gaspard (orig r2460):  jplang | 2009-02-12 23:14:22 +0100
 Adds watch/unwatch link on the issue context menu (#2730).
 r19221@gaspard (orig r2461):  jplang | 2009-02-13 18:29:49 +0100
 Removes invalid css class on issue details (#2733).
 r19223@gaspard (orig r2463):  jplang | 2009-02-13 18:59:45 +0100
 Timelog is ignored when updating an issue if user is admin but not a project member (#2717).


git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/nbc@2464 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-02-14 12:06:45 +00:00
Nicolas Chuche
7e46d9b35b implement local cache for mercurial remote repositories
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1908 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-27 11:00:05 +00:00
Nicolas Chuche
fdbe52a2f7 * use svnsync instead of checkout for subversion cache
* create repositories cache directory if it doesn't exists (default to RAILS_ROOT/tmp/scm)


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1906 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-24 20:08:51 +00:00
Nicolas Chuche
89b8bf3dc5 add local cache repository for speed purpose (subversion) or for browsing a external repository
for SCM that can handle only local copy (git).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1899 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-22 18:46:46 +00:00
Nicolas Chuche
9b94342bc3 r18645@gaspard (orig r1887): jplang | 2008-09-20 16:07:52 +0200
Fixed: Roadmap crashes when a version has a due date > 2037.
 r18646@gaspard (orig r1888):  jplang | 2008-09-21 10:54:02 +0200
 Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen.
 r18647@gaspard (orig r1889):  jplang | 2008-09-21 10:54:50 +0200
 Fixes VersionTest class.
 r18648@gaspard (orig r1890):  jplang | 2008-09-21 14:07:44 +0200
 Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory (#1900).
 r18649@gaspard (orig r1891):  winterheart | 2008-09-21 14:31:34 +0200
 de.yml from #1745, thank to Sven Schuchmann and Thomas Löber for contribution
 r18650@gaspard (orig r1892):  winterheart | 2008-09-21 14:32:16 +0200
 #1928, update for Italian language
 r18651@gaspard (orig r1893):  jplang | 2008-09-21 14:45:22 +0200
 Unescape back_url param before calling redirect_to.
 r18652@gaspard (orig r1894):  jplang | 2008-09-21 15:28:12 +0200
 Strip LDAP attribute names before saving (#1890).
 r18653@gaspard (orig r1895):  jplang | 2008-09-21 20:45:30 +0200
 Switch order of current and previous revisions in side-by-side diff (#1903).
 r18654@gaspard (orig r1896):  jplang | 2008-09-21 22:38:36 +0200
 Typo in migration 97 name (#1929).
 r18655@gaspard (orig r1897):  winterheart | 2008-09-22 16:49:18 +0200
 #1921, pt translation


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1898 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-22 18:33:53 +00:00
Nicolas Chuche
d8549c5541 r18633@gaspard (orig r1875): jplang | 2008-09-17 21:18:31 +0200
reposman: change #log arguments.
 r18634@gaspard (orig r1876):  jplang | 2008-09-17 21:38:20 +0200
 Slight change on git repository creation command.
 r18635@gaspard (orig r1877):  jplang | 2008-09-17 21:47:36 +0200
 Make --command option usable on Windows.
 r18636@gaspard (orig r1878):  jplang | 2008-09-19 17:32:52 +0200
 Adds watch/unwatch functionality at forum topic level (#1912).
 Users who create/reply a topic are automatically added as watchers but are now able to unwatch the topic.
 r18637@gaspard (orig r1879):  winterheart | 2008-09-19 18:15:49 +0200
 update of ru.yml
 r18638@gaspard (orig r1880):  winterheart | 2008-09-19 18:17:35 +0200
 #1918, translation for zh-tw
 r18639@gaspard (orig r1881):  winterheart | 2008-09-19 18:20:45 +0200
 fixed #1920, patch for Hungarian language
 r18640@gaspard (orig r1882):  winterheart | 2008-09-19 18:23:04 +0200
 patch #1922, update for nl.yml
 r18641@gaspard (orig r1883):  winterheart | 2008-09-19 18:26:19 +0200
 #1923, updated zh.yml
 r18642@gaspard (orig r1884):  winterheart | 2008-09-19 18:30:39 +0200
 #1924, update for da.yml
 r18643@gaspard (orig r1885):  winterheart | 2008-09-19 18:33:08 +0200
 #1925, patch for lt.yml


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1886 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-19 20:14:13 +00:00
Nicolas Chuche
54433282d3 r18596@gaspard (orig r1860): nbc | 2008-09-14 21:03:46 +0200
bugfix
 r18597@gaspard (orig r1861):  winterheart | 2008-09-15 17:14:34 +0200
 #1902, translation for zh-tw
 r18598@gaspard (orig r1862):  winterheart | 2008-09-15 17:16:53 +0200
 #1907, translation for zh
 r18599@gaspard (orig r1863):  winterheart | 2008-09-15 17:19:51 +0200
 fixed #1905, patch for Hungarian language
 r18600@gaspard (orig r1864):  winterheart | 2008-09-15 17:22:53 +0200
 Minor typo, fixed #1897, thank Denis Tomashenko for reporting.
 r18601@gaspard (orig r1865):  winterheart | 2008-09-15 18:07:30 +0200
 Catalan translation (#1822), thanks to Joan Duran for contribuition. Some strings has wrong quoting, I fixed that.
 r18602@gaspard (orig r1866):  nbc | 2008-09-15 21:37:43 +0200
 * reposman can create git repository with "--scm git" option
 * light refactoring
 r18603@gaspard (orig r1867):  jplang | 2008-09-16 23:54:53 +0200
 Use RDoc.usage
 r18604@gaspard (orig r1868):  jplang | 2008-09-16 23:56:02 +0200
 mailhandler: fixes exit status and adds an explicit message if response code is 403.
 r18605@gaspard (orig r1869):  winterheart | 2008-09-17 17:31:35 +0200
 Patch #1909, updates for ru.yml
 r18606@gaspard (orig r1870):  jplang | 2008-09-17 18:39:23 +0200
 Render the commit changes list as a tree (#1896).
 r18607@gaspard (orig r1871):  jplang | 2008-09-17 18:48:04 +0200
 Fixed: http links containing parentheses fail to reder correctly (#1591). Patch by Paul Rivier.
 r18608@gaspard (orig r1872):  jplang | 2008-09-17 19:18:05 +0200
 Removes unused image references in stylesheets (#1914).
 r18609@gaspard (orig r1873):  jplang | 2008-09-17 19:23:08 +0200
 Fixed custom query sidebar links broken by r1797 (#1899).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1874 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-17 18:42:46 +00:00
Nicolas Chuche
e5aed8b912 r18566@gaspard (orig r1830): winterheart | 2008-09-14 15:08:02 +0200
sorting new string...


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1859 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:38:43 +00:00
Nicolas Chuche
056f72fd6d r18565@gaspard (orig r1829): winterheart | 2008-09-14 14:59:12 +0200
fixed #1216, thank Antti Perkiömäki for reporting


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1858 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:38:36 +00:00
Nicolas Chuche
c74648d86f r18564@gaspard (orig r1828): jplang | 2008-09-14 14:41:24 +0200
Functional tests fail when run on their own (#1895).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1857 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:38:21 +00:00
Nicolas Chuche
04040b822a r18563@gaspard (orig r1827): jplang | 2008-09-14 12:54:19 +0200
Adds :view_wiki_edits permission to default roles.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1856 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:38:10 +00:00
Nicolas Chuche
dc38a43d51 r18562@gaspard (orig r1826): winterheart | 2008-09-13 21:43:27 +0200
Fixed #1810, one string left


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1855 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:38:01 +00:00
Nicolas Chuche
c64d7f7697 r18561@gaspard (orig r1825): winterheart | 2008-09-13 21:12:50 +0200
fixed #1839, #1814, #1747 and #1698


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1854 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:37:48 +00:00
Nicolas Chuche
9cc4cc9830 r18560@gaspard (orig r1824): winterheart | 2008-09-13 21:05:14 +0200
fixed #1597, thank to Alexandre da Silva for patience :)


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1853 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:37:32 +00:00
Nicolas Chuche
b280742dca r18559@gaspard (orig r1823): winterheart | 2008-09-13 20:55:54 +0200
oops, we have newest #1849, sorry


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1852 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:36:46 +00:00
Nicolas Chuche
42c610104f r18558@gaspard (orig r1822): winterheart | 2008-09-13 20:52:27 +0200
fixed #1832


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1851 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:36:36 +00:00
Nicolas Chuche
baba12c09c r18557@gaspard (orig r1821): jplang | 2008-09-13 20:45:56 +0200
Fixed: unable to revert to a previous wiki page version.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1850 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:36:23 +00:00
Nicolas Chuche
c6f927ffc3 r18556@gaspard (orig r1820): jplang | 2008-09-13 20:32:37 +0200
Expand RAILS_ROOT path on startup (#1892).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1849 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:36:09 +00:00
Nicolas Chuche
7a79bbb9c3 r18555@gaspard (orig r1819): winterheart | 2008-09-13 20:12:19 +0200
fixed #1818, some strings untranslated


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1848 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:36:01 +00:00
Nicolas Chuche
589c7166a0 r18554@gaspard (orig r1818): winterheart | 2008-09-13 19:54:55 +0200
patch #1639, some new strings untranslated


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1847 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:35:54 +00:00
Nicolas Chuche
5231f0e1c0 r18553@gaspard (orig r1817): jplang | 2008-09-13 19:25:01 +0200
Adds Turkish translation by Ismail Sezen (#1866).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1846 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:35:42 +00:00
Nicolas Chuche
1a23493d87 r18552@gaspard (orig r1816): winterheart | 2008-09-13 19:06:37 +0200
fixed #1632, patch from Станислав Герман-Евтушенко


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1845 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:35:19 +00:00
Nicolas Chuche
3810bac939 r18551@gaspard (orig r1815): jplang | 2008-09-13 18:45:01 +0200
Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users (#1154).
 A migration automatically adds this permission to roles that were allowed to view wiki pages.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1844 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:35:10 +00:00
Nicolas Chuche
b6e15f96e4 r18550@gaspard (orig r1814): jplang | 2008-09-13 18:31:11 +0200
Merged nbc branch @ r1812 (commit access permission and reposman improvements).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1843 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:34:59 +00:00
Nicolas Chuche
319bbed456 r18549@gaspard (orig r1813): winterheart | 2008-09-13 18:30:03 +0200
fixed #1635, merged changes to trunk, sorting all strings


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1842 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:34:51 +00:00
Nicolas Chuche
bd3777694a r18547@gaspard (orig r1811): jplang | 2008-09-13 17:33:46 +0200
Mailer compatibility with Rails 2.1.1.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1841 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:33:30 +00:00
Nicolas Chuche
1fdbdb818a r18546@gaspard (orig r1810): jplang | 2008-09-13 17:33:12 +0200
Engines compatibility with Rails 2.1.1 (#1886).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1840 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:33:22 +00:00
Nicolas Chuche
15a6352b59 r18539@gaspard (orig r1803): jplang | 2008-09-13 11:45:07 +0200
Removes double quotes in commit link syntax (#1872).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1839 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:33:08 +00:00
Nicolas Chuche
99a47d6f0c r18538@gaspard (orig r1802): jplang | 2008-09-12 19:17:25 +0200
Adds links to changesets atom feed on repository browser (#1873).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1838 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:33:01 +00:00
Nicolas Chuche
b6f7b138aa r18537@gaspard (orig r1801): jplang | 2008-09-11 19:45:21 +0200
Template error when user's timezone isn't set and UTC timestamps are used (#1889).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1837 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:32:43 +00:00
Nicolas Chuche
4508f1ca91 r18536@gaspard (orig r1800): jplang | 2008-09-11 19:19:26 +0200
Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead (#1754).


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1836 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:32:30 +00:00
Nicolas Chuche
ea30dbada1 r18535@gaspard (orig r1799): jplang | 2008-09-11 19:08:00 +0200
Changes versions retrieval on gantt chart.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1835 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:32:21 +00:00
Nicolas Chuche
145107cb83 r18534@gaspard (orig r1798): jplang | 2008-09-11 19:03:26 +0200
Adds support for free ticket filtering and custom queries on Calendar.
 ProjectsController#calendar moved to IssuesController.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1834 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:32:13 +00:00
Nicolas Chuche
2252d61ef6 r18533@gaspard (orig r1797): jplang | 2008-09-10 20:26:13 +0200
Adds support for free ticket filtering and custom queries on Gantt chart.
 ProjectsController#gantt moved to IssuesController.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1833 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:32:03 +00:00
Nicolas Chuche
07a1c5beeb r18532@gaspard (orig r1796): edavis10 | 2008-09-10 03:49:51 +0200
Added context fields to the :view_projects_settings_members_table hooks
 
 * :view_projects_settings_members_table_header now has :project
 * :view_projects_settings_members_table_row now has :project
 * :view_projects_settings_members_table_row has a fixed :member field
 


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1832 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:30:23 +00:00
Nicolas Chuche
0f0217d39b r18531@gaspard (orig r1795): edavis10 | 2008-09-10 02:14:38 +0200
Reverting commit r1748 again.  r1786 pulled in in again
 


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1831 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-14 15:29:34 +00:00
Jean-Philippe Lang
7608eb4162 Renames commit access permission migration.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1812 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-13 16:27:30 +00:00
Jean-Philippe Lang
0b74888fb8 Adds --scm option support to reposman. It can be used to register non subversion repositories in Redmine.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1809 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-13 12:48:37 +00:00
Jean-Philippe Lang
3f35344aeb Removes reposman perl version since it's no longer compatible with repository API.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1808 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-13 11:13:43 +00:00
Jean-Philippe Lang
73123fa484 Adds support for --scm option to reposman.rb.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1807 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-13 11:11:48 +00:00
Jean-Philippe Lang
7bde4105b8 Adds SCM vendor argument (eg. 'Subversion') to repository_created API method.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1806 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-13 11:03:11 +00:00
Jean-Philippe Lang
231e98f0c7 Replaces repository_enable named scope on Project with a more generic one: has_module.
The new named scope uses raw sql condition to avoid enabled_modules association loading.

git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1805 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-13 10:37:23 +00:00
Jean-Philippe Lang
c77d9712ca Fixes functional tests.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1804 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-13 10:04:39 +00:00
Nicolas Chuche
8e48c9ff9d don't use direct table name
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1794 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-09 18:07:00 +00:00
Nicolas Chuche
2bda4e4c11 * reposman can now use an external command with "-c" to create repository of other kind than svn
* WS used by reposman only return projects with repository module enable (so reposman no longer create repository if module is disable)
* it doesn't create repository if repository definition already exists in redmine database (unless -f is used)

git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1793 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 17:42:15 +00:00
Nicolas Chuche
562d3aa6a6 add builtin named_scope
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1792 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 13:09:54 +00:00
Nicolas Chuche
f5f51f4f83 Add write control on repository from Redmine interface
* new methods to add/remove rights in app/models/role.rb
  * some unit tests
  * add write check in Redmine.pm

To keep compatibility migration add write rights to non builtin roles
but default clean install give write access only to manager and
developer, not to reporter.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1791 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 13:01:40 +00:00
Nicolas Chuche
473869db6b recreate new nbc branch from trunk
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1790 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 12:22:22 +00:00
Nicolas Chuche
c5d5045c83 r17045@gaspard (orig r916): nbc | 2007-11-18 19:51:48 +0100
* 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/branches/nbc@1788 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 12:18:13 +00:00
Nicolas Chuche
1bb237c3e0 r17044@gaspard (orig r915): jplang | 2007-11-18 18:46:55 +0100
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/branches/nbc@1781 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-01 17:52:12 +00:00
Nicolas Chuche
77f65a81c7 r17043@gaspard (orig r914): jplang | 2007-11-18 16:53:58 +0100
'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/branches/nbc@1780 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-01 17:37:27 +00:00
Nicolas Chuche
f48cf8e67d r16121@gaspard (orig r912): jplang | 2007-11-18 15:32:39 +0100
Added an alternate theme which provides issue list colorization based on issues priority.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@913 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 15:03:57 +00:00
Nicolas Chuche
d6e8121ef1 change owner rights to work with mod_perl authentication. add a test option that just connect to WS and say what should be done
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@911 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 12:39:13 +00:00
Jean-Philippe Lang
83b6fa57f6 Added nbc branch.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@910 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 11:58:02 +00:00
1392 changed files with 48224 additions and 104149 deletions

22
.gitignore vendored
View File

@@ -1,22 +0,0 @@
/.project
/.loadpath
/config/additional_environment.rb
/config/database.yml
/config/email.yml
/config/initializers/session_store.rb
/coverage
/db/*.db
/db/*.sqlite3
/db/schema.rb
/files/*
/log/*.log*
/log/mongrel_debug
/public/dispatch.*
/public/plugin_assets
/tmp/*
/tmp/cache/*
/tmp/sessions/*
/tmp/sockets/*
/tmp/test/*
/vendor/rails
*.rbc

View File

@@ -1,24 +0,0 @@
syntax: glob
.project
.loadpath
config/additional_environment.rb
config/database.yml
config/email.yml
config/initializers/session_store.rb
coverage
db/*.db
db/*.sqlite3
db/schema.rb
files/*
log/*.log*
log/mongrel_debug
public/dispatch.*
public/plugin_assets
tmp/*
tmp/cache/*
tmp/sessions/*
tmp/sockets/*
tmp/test/*
vendor/rails
*.rbc

View File

@@ -1,5 +0,0 @@
= Redmine
Redmine is a flexible project management web application written using Ruby on Rails framework.
More details can be found at in the doc directory or on the official website http://www.redmine.org

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -20,20 +20,45 @@ 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
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
# Show user's account
def show
@user = User.active.find(params[:id])
@custom_values = @user.custom_values
# 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.member_of?(membership.project))
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
rescue ActiveRecord::RecordNotFound
render_404
end
# Login request and validation
def login
if request.get?
logout_user
# Logout user
self.logged_user = nil
else
authenticate_user
# Authenticate user
if Setting.openid? && using_open_id?
open_id_authenticate(params[:openid_url])
else
password_authentication
end
end
end
# Log out current user and redirect to welcome page
def logout
logout_user
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
@@ -59,9 +84,9 @@ class AccountController < ApplicationController
if request.post?
user = User.find_by_mail(params[:mail])
# user not found in db
(flash.now[:error] = l(:notice_account_unknown_email); return) unless user
flash.now[:error] = l(:notice_account_unknown_email) and return unless user
# user uses an external authentification
(flash.now[:error] = l(:notice_can_t_change_password); 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
@@ -83,9 +108,9 @@ class AccountController < ApplicationController
else
@user = User.new(params[:user])
@user.admin = false
@user.register
@user.status = User::STATUS_REGISTERED
if session[:auth_source_registration]
@user.activate
@user.status = User::STATUS_ACTIVE
@user.login = session[:auth_source_registration][:login]
@user.auth_source_id = session[:auth_source_registration][:auth_source_id]
if @user.save
@@ -116,8 +141,8 @@ class AccountController < ApplicationController
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.registered?
user.activate
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)
@@ -125,31 +150,27 @@ class AccountController < ApplicationController
redirect_to :action => 'login'
end
private
def logout_user
if User.current.logged?
cookies.delete :autologin
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
self.logged_user = nil
end
end
def authenticate_user
if Setting.openid? && using_open_id?
open_id_authenticate(params[:openid_url])
private
def logged_user=(user)
if user && user.is_a?(User)
User.current = user
session[:user_id] = user.id
else
password_authentication
User.current = User.anonymous
session[:user_id] = nil
end
end
def password_authentication
user = User.try_to_login(params[:username], params[:password])
if user.nil?
invalid_credentials
# Invalid credentials
flash.now[:error] = l(:notice_account_invalid_creditentials)
elsif user.new_record?
onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
# Onthefly creation failed, display the registration form to fill/fix attributes
@user = user
session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
render :action => 'register'
else
# Valid user
successful_authentication(user)
@@ -170,29 +191,25 @@ class AccountController < ApplicationController
user.mail = registration['email'] unless registration['email'].nil?
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
user.random_password
user.register
user.status = User::STATUS_REGISTERED
case Setting.self_registration
when '1'
register_by_email_activation(user) do
onthefly_creation_failed(user)
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
when '3'
register_automatically(user) do
onthefly_creation_failed(user)
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
else
register_manually_by_administrator(user) do
onthefly_creation_failed(user)
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
end
else
# Existing record
if user.active?
successful_authentication(user)
else
account_pending
end
successful_authentication(user)
end
end
end
@@ -206,7 +223,6 @@ class AccountController < ApplicationController
token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end
call_hook(:controller_account_success_authentication_after, {:user => user })
redirect_back_or_default :controller => 'my', :action => 'page'
end
@@ -217,11 +233,6 @@ class AccountController < ApplicationController
render :action => 'register'
end
def invalid_credentials
logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
flash.now[:error] = l(:notice_account_invalid_creditentials)
end
# Register a user for email activation.
#
# Pass a block for behavior when a user fails to save
@@ -241,8 +252,7 @@ class AccountController < ApplicationController
# Pass a block for behavior when a user fails to save
def register_automatically(user, &block)
# Automatic activation
user.activate
user.last_login_on = Time.now
user.status = User::STATUS_ACTIVE
if user.save
self.logged_user = user
flash[:notice] = l(:notice_account_activated)
@@ -259,14 +269,10 @@ class AccountController < ApplicationController
if user.save
# Sends an email to the administrators
Mailer.deliver_account_activation_request(user)
account_pending
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
else
yield if block_given?
end
end
def account_pending
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
end
end

View File

@@ -1,59 +0,0 @@
class ActivitiesController < ApplicationController
menu_item :activity
before_filter :find_optional_project
accept_key_auth :index
def index
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
end
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
events = @activity.events(@date_from, @date_to)
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
private
# TODO: refactor, duplicated in projects_controller
def find_optional_project
return true unless params[:id]
@project = Project.find(params[:id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AdminController < ApplicationController
layout 'admin'
before_filter :require_admin
helper :sort
@@ -76,11 +74,11 @@ class AdminController < ApplicationController
def info
@db_adapter_name = ActiveRecord::Base.connection.adapter_name
@checklist = [
[:text_default_administrator_account_changed, User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?],
[:text_file_repository_writable, File.writable?(Attachment.storage_path)],
[:text_plugin_assets_writable, File.writable?(Engines.public_directory)],
[:text_rmagick_available, Object.const_defined?(:Magick)]
]
@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),
:plugin_assets_writable => File.writable?(Engines.public_directory),
:rmagick_available => Object.const_defined?(:Magick)
}
end
end

View File

@@ -0,0 +1,234 @@
# 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 'uri'
require 'cgi'
class ApplicationController < ActionController::Base
layout 'base'
before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password
include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper
REDMINE_SUPPORTED_SCM.each do |scm|
require_dependency "repository/#{scm.underscore}"
end
def current_role
@current_role ||= User.current.role_for_project(@project)
end
def user_setup
# Check the settings cache for each request
Setting.check_cache
# Find the current user
User.current = find_current_user
end
# Returns the current user or nil if no user is logged in
def find_current_user
if session[:user_id]
# existing session
(User.active.find(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature
User.find_by_autologin_key(cookies[:autologin])
elsif params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication
User.find_by_rss_key(params[:key])
end
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
User.current.language = nil unless User.current.logged?
lang = begin
if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
User.current.language
elsif request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
User.current.language = accept_lang
end
end
rescue
nil
end || Setting.default_language
set_language_if_valid(lang)
end
def require_login
if !User.current.logged?
redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
return false
end
true
end
def require_admin
return unless require_login
if !User.current.admin?
render_403
return false
end
true
end
def deny_access
User.current.logged? ? render_403 : require_login
end
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action])
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : deny_access
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
if @project && @project.active?
if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
true
else
User.current.logged? ? render_403 : require_login
end
else
@project = nil
render_404
false
end
end
def redirect_back_or_default(default)
back_url = CGI.unescape(params[:back_url].to_s)
if !back_url.blank?
begin
uri = URI.parse(back_url)
# do not redirect user to another host or to the login or register page
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
redirect_to(back_url) and return
end
rescue URI::InvalidURIError
# redirect to default
end
end
redirect_to default
end
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_error(msg)
flash.now[:error] = msg
render :nothing => true, :layout => !request.xhr?, :status => 500
end
def render_feed(items, options={})
@items = items || []
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
@items = @items.slice(0, Setting.feeds_limit.to_i)
@title = options[:title] || Setting.app_title
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end
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, attachments)
attached = []
unsaved = []
if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
next unless file && file.size > 0
a = Attachment.create(:container => obj,
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
a.new_record? ? (unsaved << a) : (attached << a)
end
if unsaved.any?
flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
end
end
attached
end
# Returns the number of objects that should be displayed
# on the paginated list
def per_page_option
per_page = nil
if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
per_page = params[:per_page].to_s.to_i
session[:per_page] = per_page
elsif session[:per_page]
per_page = session[:per_page]
else
per_page = Setting.per_page_options_array.first || 25
end
per_page
end
# qvalues http header parser
# code taken from webrick
def parse_qvalues(value)
tmp = []
if value
parts = value.split(/,\s*/)
parts.each {|part|
if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
val = m[1]
q = (m[2] or 1).to_f
tmp.push([val, q])
end
}
tmp = tmp.sort_by{|val, q| -q}
tmp.collect!{|val, q| val}
end
return tmp
end
# Returns a string that can be used as filename value in Content-Disposition header
def filename_for_content_disposition(name)
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
end
end

View File

@@ -1,482 +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 'uri'
require 'cgi'
class ApplicationController < ActionController::Base
include Redmine::I18n
layout 'base'
exempt_from_layout 'builder', 'rsb'
# Remove broken cookie after upgrade from 0.8.x (#4292)
# See https://rails.lighthouseapp.com/projects/8994/tickets/3360
# TODO: remove it when Rails is fixed
before_filter :delete_broken_cookies
def delete_broken_cookies
if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
cookies.delete '_redmine_session'
redirect_to home_path
return false
end
end
before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password
protect_from_forgery
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
include Redmine::Search::Controller
include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper
Redmine::Scm::Base.all.each do |scm|
require_dependency "repository/#{scm.underscore}"
end
def user_setup
# Check the settings cache for each request
Setting.check_cache
# Find the current user
User.current = find_current_user
end
# Returns the current user or nil if no user is logged in
# and starts a session if needed
def find_current_user
if session[:user_id]
# existing session
(User.active.find(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature starts a new session
user = User.try_to_autologin(cookies[:autologin])
session[:user_id] = user.id if user
user
elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication does not start a session
User.find_by_rss_key(params[:key])
elsif Setting.rest_api_enabled? && api_request?
if (key = api_key_from_request) && accept_key_auth_actions.include?(params[:action])
# Use API key
User.find_by_api_key(key)
else
# HTTP Basic, either username/password or API key/random
authenticate_with_http_basic do |username, password|
User.try_to_login(username, password) || User.find_by_api_key(username)
end
end
end
end
# Sets the logged in user
def logged_user=(user)
reset_session
if user && user.is_a?(User)
User.current = user
session[:user_id] = user.id
else
User.current = User.anonymous
end
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 = nil
if User.current.logged?
lang = find_language(User.current.language)
end
if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
if !accept_lang.blank?
accept_lang = accept_lang.downcase
lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
end
end
lang ||= Setting.default_language
set_language_if_valid(lang)
end
def require_login
if !User.current.logged?
# Extract only the basic url parameters on non-GET requests
if request.get?
url = url_for(params)
else
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
end
respond_to do |format|
format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
end
return false
end
true
end
def require_admin
return unless require_login
if !User.current.admin?
render_403
return false
end
true
end
def deny_access
User.current.logged? ? render_403 : require_login
end
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action], global = false)
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
if allowed
true
else
if @project && @project.archived?
render_403 :message => :notice_not_authorized_archived_project
else
deny_access
end
end
end
# Authorize the user for the requested action outside a project
def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
authorize(ctrl, action, global)
end
# Find project of id params[:id]
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
# Find project of id params[:project_id]
def find_project_by_project_id
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
# Find a project based on params[:project_id]
# TODO: some subclasses override this, see about merging their logic
def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
allowed ? true : deny_access
rescue ActiveRecord::RecordNotFound
render_404
end
# Finds and sets @project based on @object.project
def find_project_from_association
render_404 unless @object.present?
@project = @object.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_model_object
model = self.class.read_inheritable_attribute('model_object')
if model
@object = model.find(params[:id])
self.instance_variable_set('@' + controller_name.singularize, @object) if @object
end
rescue ActiveRecord::RecordNotFound
render_404
end
def self.model_object(model)
write_inheritable_attribute('model_object', model)
end
# Filter for bulk issue operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
# Check if project is unique before bulk operations
def check_project_uniqueness
unless @project
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects'
return false
end
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
if @project && @project.active?
if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
true
else
User.current.logged? ? render_403 : require_login
end
else
@project = nil
render_404
false
end
end
def back_url
params[:back_url] || request.env['HTTP_REFERER']
end
def redirect_back_or_default(default)
back_url = CGI.unescape(params[:back_url].to_s)
if !back_url.blank?
begin
uri = URI.parse(back_url)
# do not redirect user to another host or to the login or register page
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
redirect_to(back_url)
return
end
rescue URI::InvalidURIError
# redirect to default
end
end
redirect_to default
end
def render_403(options={})
@project = nil
render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
return false
end
def render_404(options={})
render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
return false
end
# Renders an error response
def render_error(arg)
arg = {:message => arg} unless arg.is_a?(Hash)
@message = arg[:message]
@message = l(@message) if @message.is_a?(Symbol)
@status = arg[:status] || 500
respond_to do |format|
format.html {
render :template => 'common/error', :layout => use_layout, :status => @status
}
format.atom { head @status }
format.xml { head @status }
format.js { head @status }
format.json { head @status }
end
end
# Picks which layout to use based on the request
#
# @return [boolean, string] name of the layout to use or false for no layout
def use_layout
request.xhr? ? false : 'base'
end
def invalid_authenticity_token
if api_request?
logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
end
render_error "Invalid form authenticity token."
end
def render_feed(items, options={})
@items = items || []
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
@items = @items.slice(0, Setting.feeds_limit.to_i)
@title = options[:title] || Setting.app_title
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end
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
# Returns the number of objects that should be displayed
# on the paginated list
def per_page_option
per_page = nil
if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
per_page = params[:per_page].to_s.to_i
session[:per_page] = per_page
elsif session[:per_page]
per_page = session[:per_page]
else
per_page = Setting.per_page_options_array.first || 25
end
per_page
end
# Returns offset and limit used to retrieve objects
# for an API response based on offset, limit and page parameters
def api_offset_and_limit(options=params)
if options[:offset].present?
offset = options[:offset].to_i
if offset < 0
offset = 0
end
end
limit = options[:limit].to_i
if limit < 1
limit = 25
elsif limit > 100
limit = 100
end
if offset.nil? && options[:page].present?
offset = (options[:page].to_i - 1) * limit
offset = 0 if offset < 0
end
offset ||= 0
[offset, limit]
end
# qvalues http header parser
# code taken from webrick
def parse_qvalues(value)
tmp = []
if value
parts = value.split(/,\s*/)
parts.each {|part|
if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
val = m[1]
q = (m[2] or 1).to_f
tmp.push([val, q])
end
}
tmp = tmp.sort_by{|val, q| -q}
tmp.collect!{|val, q| val}
end
return tmp
rescue
nil
end
# Returns a string that can be used as filename value in Content-Disposition header
def filename_for_content_disposition(name)
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
end
def api_request?
%w(xml json).include? params[:format]
end
# Returns the API key present in the request
def api_key_from_request
if params[:key].present?
params[:key]
elsif request.headers["X-Redmine-API-Key"].present?
request.headers["X-Redmine-API-Key"]
end
end
# Renders a warning flash if obj has unsaved attachments
def render_attachment_warning_if_needed(obj)
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
end
# Sets the `flash` notice or error based the number of issues that did not save
#
# @param [Array, Issue] issues all of the saved and unsaved Issues
# @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues,
:count => unsaved_issue_ids.size,
:total => issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end
end
# Rescues an invalid query statement. Just in case...
def query_statement_invalid(exception)
logger.error "Query::StatementInvalid: #{exception.message}" if logger
session.delete(:query)
sort_clear if respond_to?(:sort_clear)
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
end
# Converts the errors on an ActiveRecord object into a common JSON format
def object_errors_to_json(object)
object.errors.collect do |attribute, error|
{ attribute => error }
end.to_json
end
# Renders API response on validation failure
def render_validation_errors(object)
options = { :status => :unprocessable_entity, :layout => false }
options.merge!(case params[:format]
when 'xml'; { :xml => object.errors }
when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
else
raise "Unknown format #{params[:format]} in #render_validation_errors"
end
)
render options
end
# Overrides #default_template so that the api template
# is used automatically if it exists
def default_template(action_name = self.action_name)
if api_request?
begin
return self.view_paths.find_template(default_template_name(action_name), 'api')
rescue ::ActionView::MissingTemplate
# the api template was not found
# fallback to the default behaviour
end
end
super
end
# Overrides #pick_layout so that #render with no arguments
# doesn't use the layout for api requests
def pick_layout(*args)
api_request? ? nil : super
end
end

View File

@@ -17,7 +17,7 @@
class AttachmentsController < ApplicationController
before_filter :find_project
before_filter :file_readable, :read_authorize, :except => :destroy
before_filter :read_authorize, :except => :destroy
before_filter :delete_authorize, :only => :destroy
verify :method => :post, :only => :destroy
@@ -26,7 +26,7 @@ class AttachmentsController < ApplicationController
if @attachment.is_diff?
@diff = File.new(@attachment.diskfile, "rb").read
render :action => 'diff'
elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
elsif @attachment.is_text?
@content = File.new(@attachment.diskfile, "rb").read
render :action => 'file'
else
@@ -41,7 +41,7 @@ class AttachmentsController < ApplicationController
# images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => detect_content_type(@attachment),
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
end
@@ -64,11 +64,6 @@ private
render_404
end
# Checks that the file exists and is readable
def file_readable
@attachment.readable? ? true : render_404
end
def read_authorize
@attachment.visible? ? true : deny_access
end
@@ -76,12 +71,4 @@ private
def delete_authorize
@attachment.deletable? ? true : deny_access
end
def detect_content_type(attachment)
content_type = attachment.content_type
if content_type.blank?
content_type = Redmine::MimeType.of(attachment.filename)
end
content_type.to_s
end
end

View File

@@ -16,46 +16,47 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSourcesController < ApplicationController
layout 'admin'
before_filter :require_admin
def index
list
render :action => 'list' unless request.xhr?
end
# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :create, :update ],
:redirect_to => { :template => :index }
:redirect_to => { :action => :list }
def index
@auth_source_pages, @auth_sources = paginate auth_source_class.name.tableize, :per_page => 10
render "auth_sources/index"
def list
@auth_source_pages, @auth_sources = paginate :auth_sources, :per_page => 10
render :action => "list", :layout => false if request.xhr?
end
def new
@auth_source = auth_source_class.new
render 'auth_sources/new'
@auth_source = AuthSourceLdap.new
end
def create
@auth_source = auth_source_class.new(params[:auth_source])
@auth_source = AuthSourceLdap.new(params[:auth_source])
if @auth_source.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index'
redirect_to :action => 'list'
else
render 'auth_sources/new'
render :action => 'new'
end
end
def edit
@auth_source = AuthSource.find(params[:id])
render 'auth_sources/edit'
end
def update
@auth_source = AuthSource.find(params[:id])
if @auth_source.update_attributes(params[:auth_source])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index'
redirect_to :action => 'list'
else
render 'auth_sources/edit'
render :action => 'edit'
end
end
@@ -65,9 +66,9 @@ class AuthSourcesController < ApplicationController
@auth_method.test_connection
flash[:notice] = l(:notice_successful_connection)
rescue => text
flash[:error] = l(:error_unable_to_connect, text.message)
flash[:error] = "Unable to connect (#{text})"
end
redirect_to :action => 'index'
redirect_to :action => 'list'
end
def destroy
@@ -76,12 +77,6 @@ class AuthSourcesController < ApplicationController
@auth_source.destroy
flash[:notice] = l(:notice_successful_delete)
end
redirect_to :action => 'index'
end
protected
def auth_source_class
AuthSource
redirect_to :action => 'list'
end
end

View File

@@ -1,27 +0,0 @@
class AutoCompletesController < ApplicationController
before_filter :find_project
def issues
@issues = []
q = params[:q].to_s
query = (params[:scope] == "all" && Setting.cross_project_issue_relations?) ? Issue : @project.issues
if q.match(/^\d+$/)
@issues << query.visible.find_by_id(q.to_i)
end
unless q.blank?
@issues += query.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
end
@issues.compact!
render :layout => false
end
private
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -16,9 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class BoardsController < ApplicationController
default_search_scope :messages
before_filter :find_project, :find_board_if_available, :authorize
accept_key_auth :index, :show
before_filter :find_project, :authorize
helper :messages
include MessagesHelper
@@ -37,29 +35,18 @@ class BoardsController < ApplicationController
end
def show
respond_to do |format|
format.html {
sort_init 'updated_on', 'desc'
sort_update 'created_on' => "#{Message.table_name}.created_on",
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
:include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset
@message = Message.new
render :action => 'show', :layout => !request.xhr?
}
format.atom {
@messages = @board.messages.find :all, :order => 'created_on DESC',
:include => [:author, :board],
:limit => Setting.feeds_limit.to_i
render_feed(@messages, :title => "#{@project}: #{@board}")
}
end
sort_init 'updated_on', 'desc'
sort_update 'created_on' => "#{Message.table_name}.created_on",
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
: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 }
@@ -69,33 +56,30 @@ class BoardsController < ApplicationController
@board.project = @project
if request.post? && @board.save
flash[:notice] = l(:notice_successful_create)
redirect_to_settings_in_projects
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end
end
def edit
if request.post? && @board.update_attributes(params[:board])
redirect_to_settings_in_projects
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_settings_in_projects
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end
private
def redirect_to_settings_in_projects
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_board_if_available
@board = @project.boards.find(params[:id]) if params[:id]
rescue ActiveRecord::RecordNotFound
render_404

View File

@@ -1,44 +0,0 @@
class CalendarsController < ApplicationController
menu_item :calendar
before_filter :find_optional_project
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :issues
helper :projects
helper :queries
include QueriesHelper
helper :sort
include SortHelper
def show
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
retrieve_query
@query.group_by = nil
if @query.valid?
events = []
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
)
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
end
render :action => 'show', :layout => false if request.xhr?
end
def update
show
end
end

View File

@@ -1,36 +0,0 @@
class CommentsController < ApplicationController
default_search_scope :news
model_object News
before_filter :find_model_object
before_filter :find_project_from_association
before_filter :authorize
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@comment = Comment.new(params[:comment])
@comment.author = User.current
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
end
redirect_to :controller => 'news', :action => 'show', :id => @news
end
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy
@news.comments.find(params[:comment_id]).destroy
redirect_to :controller => 'news', :action => 'show', :id => @news
end
private
# ApplicationController's find_model_object sets it based on the controller
# name so it needs to be overriden and set to @news instead
def find_model_object
super
@news = @object
@comment = nil
@news
end
end

View File

@@ -1,44 +0,0 @@
class ContextMenusController < ApplicationController
helper :watchers
def issues
@issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
else
@allowed_statuses = @issues.map do |i|
i.new_statuses_allowed_to(User.current)
end.inject do |memo,s|
memo & s
end
end
@projects = @issues.collect(&:project).compact.uniq
@project = @projects.first if @projects.size == 1
@can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => User.current.allowed_to?(:delete_issues, @projects)
}
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@trackers = @project.trackers
else
#when multiple projects, we only keep the intersection of each set
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
@trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
end
@priorities = IssuePriority.all.reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = back_url
render :layout => false
end
end

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CustomFieldsController < ApplicationController
layout 'admin'
before_filter :require_admin
def index
@@ -32,11 +30,10 @@ class CustomFieldsController < ApplicationController
end
rescue
end
(redirect_to(:action => 'index'); return) unless @custom_field.is_a?(CustomField)
redirect_to(:action => 'index') and return unless @custom_field.is_a?(CustomField)
if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create)
call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
@@ -46,17 +43,31 @@ class CustomFieldsController < ApplicationController
@custom_field = CustomField.find(params[:id])
if request.post? and @custom_field.update_attributes(params[:custom_field])
flash[:notice] = l(:notice_successful_update)
call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :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 => 'index', :tab => @custom_field.class.name
end
def destroy
@custom_field = CustomField.find(params[:id]).destroy
redirect_to :action => 'index', :tab => @custom_field.class.name
rescue
flash[:error] = l(:error_can_not_delete_custom_field)
flash[:error] = "Unable to delete custom field"
redirect_to :action => 'index'
end
end

View File

@@ -16,11 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentsController < ApplicationController
default_search_scope :documents
model_object Document
before_filter :find_project, :only => [:index, :new]
before_filter :find_model_object, :except => [:index, :new]
before_filter :find_project_from_association, :except => [:index, :new]
before_filter :find_document, :except => [:index, :new]
before_filter :authorize
helper :attachments
@@ -30,7 +27,7 @@ class DocumentsController < ApplicationController
documents = @project.documents.find :all, :include => [:attachments, :category]
case @sort_by
when 'date'
@grouped = documents.group_by {|d| d.updated_on.to_date }
@grouped = documents.group_by {|d| d.created_on.to_date }
when 'title'
@grouped = documents.group_by {|d| d.title.first.upcase}
when 'author'
@@ -49,15 +46,15 @@ class DocumentsController < ApplicationController
def new
@document = @project.documents.build(params[:document])
if request.post? and @document.save
attachments = Attachment.attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document)
attach_files(@document, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
redirect_to :action => 'index', :project_id => @project
end
end
def edit
@categories = DocumentCategory.all
@categories = Enumeration::get_values('DCAT')
if request.post? and @document.update_attributes(params[:document])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @document
@@ -70,10 +67,8 @@ class DocumentsController < ApplicationController
end
def add_attachment
attachments = Attachment.attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document)
Mailer.deliver_attachments_added(attachments[:files]) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
attachments = attach_files(@document, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
redirect_to :action => 'show', :id => @document
end
@@ -83,4 +78,11 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
def find_document
@document = Document.find(params[:id])
@project = @document.project
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -16,12 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class EnumerationsController < ApplicationController
layout 'admin'
before_filter :require_admin
helper :custom_fields
include CustomFieldsHelper
def index
list
@@ -36,19 +31,14 @@ class EnumerationsController < ApplicationController
end
def new
begin
@enumeration = params[:type].constantize.new
rescue NameError
@enumeration = Enumeration.new
end
@enumeration = Enumeration.new(:opt => params[:opt])
end
def create
@enumeration = Enumeration.new(params[:enumeration])
@enumeration.type = params[:enumeration][:type]
if @enumeration.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list', :type => @enumeration.type
redirect_to :action => 'list', :opt => @enumeration.opt
else
render :action => 'new'
end
@@ -60,14 +50,28 @@ class EnumerationsController < ApplicationController
def update
@enumeration = Enumeration.find(params[:id])
@enumeration.type = params[:enumeration][:type] if params[:enumeration][:type]
if @enumeration.update_attributes(params[:enumeration])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list', :type => @enumeration.type
redirect_to :action => 'list', :opt => @enumeration.opt
else
render :action => 'edit'
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 = Enumeration.find(params[:id])
@@ -76,12 +80,12 @@ class EnumerationsController < ApplicationController
@enumeration.destroy
redirect_to :action => 'index'
elsif params[:reassign_to_id]
if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id])
if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id])
@enumeration.destroy(reassign_to)
redirect_to :action => 'index'
end
end
@enumerations = @enumeration.class.find(:all) - [@enumeration]
@enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration]
#rescue
# flash[:error] = 'Unable to delete enumeration'
# redirect_to :action => 'index'

View File

@@ -1,36 +0,0 @@
class FilesController < ApplicationController
menu_item :files
before_filter :find_project_by_project_id
before_filter :authorize
helper :sort
include SortHelper
def index
sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename",
'created_on' => "#{Attachment.table_name}.created_on",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
def new
@versions = @project.versions.sort
end
def create
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = Attachment.attach_files(container, params[:attachments])
render_attachment_warning_if_needed(container)
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments[:files])
end
redirect_to project_files_path(@project)
end
end

View File

@@ -1,36 +0,0 @@
class GanttsController < ApplicationController
menu_item :gantt
before_filter :find_optional_project
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :gantt
helper :issues
helper :projects
helper :queries
include QueriesHelper
helper :sort
include SortHelper
include Redmine::Export::PDF
def show
@gantt = Redmine::Helpers::Gantt.new(params)
@gantt.project = @project
retrieve_query
@query.group_by = nil
@gantt.query = @query if @query.valid?
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format|
format.html { render :action => "show", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
end
end
def update
show
end
end

View File

@@ -1,170 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 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 GroupsController < ApplicationController
layout 'admin'
before_filter :require_admin
helper :custom_fields
# GET /groups
# GET /groups.xml
def index
@groups = Group.find(:all, :order => 'lastname')
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @groups }
end
end
# GET /groups/1
# GET /groups/1.xml
def show
@group = Group.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @group }
end
end
# GET /groups/new
# GET /groups/new.xml
def new
@group = Group.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @group }
end
end
# GET /groups/1/edit
def edit
@group = Group.find(params[:id], :include => :projects)
end
# POST /groups
# POST /groups.xml
def create
@group = Group.new(params[:group])
respond_to do |format|
if @group.save
flash[:notice] = l(:notice_successful_create)
format.html { redirect_to(groups_path) }
format.xml { render :xml => @group, :status => :created, :location => @group }
else
format.html { render :action => "new" }
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
end
end
end
# PUT /groups/1
# PUT /groups/1.xml
def update
@group = Group.find(params[:id])
respond_to do |format|
if @group.update_attributes(params[:group])
flash[:notice] = l(:notice_successful_update)
format.html { redirect_to(groups_path) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /groups/1
# DELETE /groups/1.xml
def destroy
@group = Group.find(params[:id])
@group.destroy
respond_to do |format|
format.html { redirect_to(groups_url) }
format.xml { head :ok }
end
end
def add_users
@group = Group.find(params[:id])
users = User.find_all_by_id(params[:user_ids])
@group.users << users if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-users", :partial => 'groups/users'
users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
}
}
end
end
def remove_user
@group = Group.find(params[:id])
@group.users.delete(User.find(params[:user_id])) if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
end
end
def autocomplete_for_user
@group = Group.find(params[:id])
@users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
render :layout => false
end
def edit_membership
@group = Group.find(params[:id])
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
@membership.save if request.post?
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
else
format.js {
render(:update) {|page|
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
}
}
end
end
end
def destroy_membership
@group = Group.find(params[:id])
Member.find(params[:membership_id]).destroy if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
end
end
end

View File

@@ -17,41 +17,10 @@
class IssueCategoriesController < ApplicationController
menu_item :settings
model_object IssueCategory
before_filter :find_model_object, :except => :new
before_filter :find_project_from_association, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize
before_filter :find_project, :authorize
verify :method => :post, :only => :destroy
def new
@category = @project.issue_categories.build(params[:category])
if request.post?
if @category.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :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
else
respond_to do |format|
format.html
format.js do
render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
end
end
end
end
end
def edit
if request.post? and @category.update_attributes(params[:category])
flash[:notice] = l(:notice_successful_update)
@@ -74,16 +43,10 @@ class IssueCategoriesController < ApplicationController
end
private
# Wrap ApplicationController's find_model_object method to set
# @category instead of just @issue_category
def find_model_object
super
@category = @object
end
def find_project
@project = Project.find(params[:project_id])
@category = IssueCategory.find(params[:id])
@project = @category.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
end

View File

@@ -1,68 +0,0 @@
class IssueMovesController < ApplicationController
default_search_scope :issues
before_filter :find_issues, :check_project_uniqueness
before_filter :authorize
def new
prepare_for_issue_move
render :layout => false if request.xhr?
end
def create
prepare_for_issue_move
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
moved_issues = []
@issues.each do |issue|
issue.reload
issue.init_journal(User.current)
issue.current_journal.notes = @notes if @notes.present?
call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)})
moved_issues << r
else
unsaved_issue_ids << issue.id
end
end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
if params[:follow]
if @issues.size == 1 && moved_issues.size == 1
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
else
redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
end
else
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
end
return
end
end
private
def prepare_for_issue_move
@issues.sort!
@copy = params[:copy_options] && params[:copy_options][:copy]
@allowed_projects = Issue.allowed_target_projects_on_move
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
@available_statuses = Workflow.available_statuses(@project)
@notes = params[:notes]
@notes ||= ''
end
def extract_changed_attributes_for_move(params)
changed_attributes = {}
[:assigned_to_id, :status_id, :start_date, :due_date, :priority_id].each do |valid_attribute|
unless params[valid_attribute].blank?
changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
end
end
changed_attributes
end
end

View File

@@ -16,19 +16,18 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueRelationsController < ApplicationController
before_filter :find_issue, :find_project_from_association, :authorize
before_filter :find_project, :authorize
def new
@relation = IssueRelation.new(params[:relation])
@relation.issue_from = @issue
if params[:relation] && m = params[:relation][:issue_to_id].to_s.match(/^#?(\d+)$/)
@relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
if params[:relation] && !params[:relation][:issue_to_id].blank?
@relation.issue_to = Issue.visible.find_by_id(params[:relation][:issue_to_id])
end
@relation.save if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js do
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
render :update do |page|
page.replace_html "relations", :partial => 'issues/relations'
if @relation.errors.empty?
@@ -48,16 +47,14 @@ class IssueRelationsController < ApplicationController
end
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
format.js {
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'}
}
format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} }
end
end
private
def find_issue
@issue = @object = Issue.find(params[:issue_id])
def find_project
@issue = Issue.find(params[:issue_id])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -16,16 +16,19 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueStatusesController < ApplicationController
layout 'admin'
before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move, :update_issue_done_ratio ],
:redirect_to => { :action => :index }
verify :method => :post, :only => [ :destroy, :create, :update, :move ],
:redirect_to => { :action => :list }
def index
list
render :action => 'list' unless request.xhr?
end
def list
@issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position"
render :action => "index", :layout => false if request.xhr?
render :action => "list", :layout => false if request.xhr?
end
def new
@@ -36,7 +39,7 @@ class IssueStatusesController < ApplicationController
@issue_status = IssueStatus.new(params[:issue_status])
if @issue_status.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index'
redirect_to :action => 'list'
else
render :action => 'new'
end
@@ -50,26 +53,32 @@ class IssueStatusesController < ApplicationController
@issue_status = IssueStatus.find(params[:id])
if @issue_status.update_attributes(params[:issue_status])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index'
redirect_to :action => 'list'
else
render :action => 'edit'
end
end
def move
@issue_status = IssueStatus.find(params[:id])
case params[:position]
when 'highest'
@issue_status.move_to_top
when 'higher'
@issue_status.move_higher
when 'lower'
@issue_status.move_lower
when 'lowest'
@issue_status.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def destroy
IssueStatus.find(params[:id]).destroy
redirect_to :action => 'index'
redirect_to :action => 'list'
rescue
flash[:error] = l(:error_unable_delete_issue_status)
redirect_to :action => 'index'
flash[:error] = "Unable to delete issue status"
redirect_to :action => 'list'
end
def update_issue_done_ratio
if IssueStatus.update_issue_done_ratios
flash[:notice] = l(:notice_issue_done_ratios_updated)
else
flash[:error] = l(:error_issue_done_ratios_not_updated)
end
redirect_to :action => 'index'
end
end

View File

@@ -16,21 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuesController < ApplicationController
menu_item :new_issue, :only => [:new, :create]
default_search_scope :issues
menu_item :new_issue, :only => :new
before_filter :find_issue, :only => [:show, :edit, :update]
before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
before_filter :check_project_uniqueness, :only => [:move, :perform_move]
before_filter :find_project, :only => [:new, :create]
before_filter :authorize, :except => [:index]
before_filter :find_optional_project, :only => [:index]
before_filter :check_for_default_issue_status, :only => [:new, :create]
before_filter :build_new_issue_from_params, :only => [:new, :create]
accept_key_auth :index, :show, :create, :update, :destroy
before_filter :find_issue, :only => [:show, :edit, :reply]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
accept_key_auth :index, :changes
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :journals
helper :projects
include ProjectsHelper
@@ -43,56 +37,37 @@ class IssuesController < ApplicationController
helper :attachments
include AttachmentsHelper
helper :queries
include QueriesHelper
helper :repositories
include RepositoriesHelper
helper :sort
include SortHelper
include IssuesHelper
helper :timelog
helper :gantt
include Redmine::Export::PDF
verify :method => [:post, :delete],
:only => :destroy,
:render => { :nothing => true, :status => :method_not_allowed }
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def index
retrieve_query
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid?
case params[:format]
when 'csv', 'pdf'
@limit = Setting.issues_export_limit.to_i
when 'atom'
@limit = Setting.feeds_limit.to_i
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
limit = per_page_option
respond_to do |format|
format.html { }
format.atom { }
format.csv { limit = Setting.issues_export_limit.to_i }
format.pdf { limit = Setting.issues_export_limit.to_i }
end
@issue_count = @query.issue_count
@issue_pages = Paginator.new self, @issue_count, @limit, params['page']
@offset ||= @issue_pages.current.offset
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
:order => sort_clause,
:offset => @offset,
:limit => @limit)
@issue_count_by_group = @query.issue_count_by_group
@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.api
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
end
else
# Send html if the query is not valid
@@ -102,21 +77,34 @@ class IssuesController < ApplicationController
render_404
end
def changes
retrieve_query
sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid?
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:conditions => @query.statement,
:limit => 25,
:order => "#{Journal.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def show
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@priorities = IssuePriority.all
@priorities = Enumeration::get_values('IPRI')
@time_entry = TimeEntry.new
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.api
format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
end
@@ -124,95 +112,191 @@ class IssuesController < ApplicationController
# Add a new issue
# The new issue will be created from an existing one if copy_from parameter is given
def new
respond_to do |format|
format.html { render :action => 'new', :layout => !request.xhr? }
format.js { render :partial => 'attributes' }
end
end
def create
call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
if @issue.save
attachments = Attachment.attach_files(@issue, params[:attachments])
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_create)
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
respond_to do |format|
format.html {
redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
{ :action => 'show', :id => @issue })
}
format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
end
@issue = Issue.new
@issue.copy_from(params[:copy_from]) if params[:copy_from]
@issue.project = @project
# Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
if @issue.tracker.nil?
flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
render :nothing => true, :layout => true
return
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@issue) }
end
end
end
if params[:issue].is_a?(Hash)
@issue.attributes = params[:issue]
@issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
end
@issue.author = User.current
def edit
update_issue_from_params
@journal = @issue.current_journal
respond_to do |format|
format.html { }
format.xml { }
end
end
def update
update_issue_from_params
if @issue.save_issue_with_child_records(params, @time_entry)
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
respond_to do |format|
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
format.api { head :ok }
end
default_status = IssueStatus.default
unless default_status
flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
render :nothing => true, :layout => true
return
end
@issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
if request.get? || request.xhr?
@issue.start_date ||= Date.today
else
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
@journal = @issue.current_journal
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
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')
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
{ :action => 'show', :id => @issue })
return
end
end
@priorities = Enumeration::get_values('IPRI')
render :layout => !request.xhr?
end
# Attributes that can be updated on workflow transition (without :edit permission)
# TODO: make it configurable (at least per role)
UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
def edit
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@priorities = Enumeration::get_values('IPRI')
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new
@notes = params[:notes]
journal = @issue.init_journal(User.current, @notes)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
attrs = params[:issue].dup
attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.attributes = attrs
end
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@issue) }
if request.post?
@time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
# Log spend time
if User.current.allowed_to?(:log_time, @project)
@time_entry.save
end
if !journal.new_record?
# Only send notification if something was actually changed
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict)
end
def reply
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end
# Bulk edit a set of issues
def bulk_edit
@issues.sort!
@available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
@custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
@assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
@trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
if request.post?
status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
unsaved_issue_ids = []
@issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category || params[:category_id] == 'none'
issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
# 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(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
return
end
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
@custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
end
def bulk_update
@issues.sort!
attributes = parse_params_for_bulk_issue_attributes(params)
unsaved_issue_ids = []
@issues.each do |issue|
issue.reload
journal = issue.init_journal(User.current, params[:notes])
issue.safe_attributes = attributes
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
unless issue.save
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
end
def move
@allowed_projects = []
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
else
User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
end
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
@issues.each do |issue|
issue.init_journal(User.current)
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
render :layout => false if request.xhr?
end
def destroy
@@ -232,17 +316,112 @@ class IssuesController < ApplicationController
TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
end
else
# display the destroy form if it's a user request
return unless api_request?
# display the destroy form
return
end
end
@issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project
end
def gantt
@gantt = Redmine::Helpers::Gantt.new(params)
retrieve_query
if @query.valid?
events = []
# Issues that have start and due dates
events += Issue.find(:all,
:order => "start_date, due_date",
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(#{@query.statement}) AND (((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)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date
events += Issue.find(:all,
:order => "start_date, effective_date",
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
:conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Versions
events += Version.find(:all, :include => :project,
:conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
@gantt.events = events
end
respond_to do |format|
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
format.api { head :ok }
format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
end
end
def calendar
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
retrieve_query
if @query.valid?
events = []
events += Issue.find(:all,
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
)
events += Version.find(:all, :include => :project,
:conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
end
render :layout => false if request.xhr?
end
def context_menu
@issues = Issue.find_all_by_id(params[:ids], :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
end
@priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = request.env['HTTP_REFERER']
render :layout => false
end
def update_form
@issue = Issue.new(params[:issue])
render :action => :new, :layout => false
end
def preview
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
@attachements = @issue.attachments if @issue
@text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
render :partial => 'common/preview'
end
private
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@@ -251,69 +430,63 @@ private
render_404
end
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
# Filter for bulk operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
projects = @issues.collect(&:project).compact.uniq
if projects.size == 1
@project = projects.first
else
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
end
rescue ActiveRecord::RecordNotFound
render_404
end
# Used by #edit and #update to set some common instance variables
# from the params
# TODO: Refactor, not everything in here is needed by #edit
def update_issue_from_params
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@priorities = IssuePriority.all
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new
@time_entry.attributes = params[:time_entry]
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
@issue.init_journal(User.current, @notes)
@issue.safe_attributes = params[:issue]
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
# TODO: Refactor, lots of extra code in here
# TODO: Changing tracker on an existing issue should not trigger this
def build_new_issue_from_params
if params[:id].blank?
@issue = Issue.new
@issue.copy_from(params[:copy_from]) if params[:copy_from]
@issue.project = @project
def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
allowed ? true : deny_access
rescue ActiveRecord::RecordNotFound
render_404
end
# Retrieve query from session or build a new query
def retrieve_query
if !params[:query_id].blank?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = Query.find(params[:query_id], :conditions => cond)
@query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
else
@issue = @project.issues.visible.find(params[:id])
end
@issue.project = @project
# Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
if @issue.tracker.nil?
render_error l(:error_no_tracker_in_project)
return false
end
@issue.start_date ||= Date.today
if params[:issue].is_a?(Hash)
@issue.safe_attributes = params[:issue]
if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
@issue.watcher_user_ids = params[:issue]['watcher_user_ids']
if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
@query.project = @project
end
end
@issue.author = User.current
@priorities = IssuePriority.all
@allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
end
def check_for_default_issue_status
if IssueStatus.default.nil?
render_error l(:error_no_default_issue_status)
return false
end
end
def parse_params_for_bulk_issue_attributes(params)
attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
attributes
end
end

View File

@@ -16,56 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit]
before_filter :find_issue, :only => [:new]
before_filter :find_optional_project, :only => [:index]
before_filter :authorize, :only => [:new, :edit]
accept_key_auth :index
helper :issues
helper :custom_fields
helper :queries
include QueriesHelper
helper :sort
include SortHelper
def index
retrieve_query
sort_init 'id', 'desc'
sort_update(@query.sortable_columns)
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def new
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end
before_filter :find_journal
def edit
if request.post?
@@ -76,31 +27,15 @@ class JournalsController < ApplicationController
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
else
respond_to do |format|
format.html {
# TODO: implement non-JS journal update
render :nothing => true
}
format.js
end
end
end
private
def find_journal
@journal = Journal.find(params[:id])
(render_403; return false) unless @journal.editable_by?(User.current)
render_403 and return false unless @journal.editable_by?(User.current)
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404
end
# TODO: duplicated in IssuesController
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -1,25 +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 LdapAuthSourcesController < AuthSourcesController
protected
def auth_source_class
AuthSourceLdap
end
end

View File

@@ -37,8 +37,8 @@ class MailHandlerController < ActionController::Base
def check_credential
User.current = nil
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
unless Setting.mail_handler_api_enabled? && params[:key] == Setting.mail_handler_api_key
render :nothing => true, :status => 403
end
end
end

View File

@@ -16,50 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MembersController < ApplicationController
model_object Member
before_filter :find_model_object, :except => [:new, :autocomplete_for_member]
before_filter :find_project_from_association, :except => [:new, :autocomplete_for_member]
before_filter :find_project, :only => [:new, :autocomplete_for_member]
before_filter :find_member, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize
def new
members = []
if params[:member] && request.post?
attrs = params[:member].dup
if (user_ids = attrs.delete(:user_ids))
user_ids.each do |user_id|
members << Member.new(attrs.merge(:user_id => user_id))
end
else
members << Member.new(attrs)
end
@project.members << members
end
@project.members << Member.new(params[:member]) if request.post?
respond_to do |format|
if members.present? && members.all? {|m| m.valid? }
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js {
render(:update) {|page|
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
page << 'hideOnLoad()'
members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
}
}
else
format.js {
render(:update) {|page|
errors = members.collect {|m|
m.errors.full_messages
}.flatten.uniq
page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', ')))
}
}
end
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
@@ -67,34 +32,30 @@ class MembersController < ApplicationController
if request.post? and @member.update_attributes(params[:member])
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'
page << 'hideOnLoad()'
page.visual_effect(:highlight, "member-#{@member.id}")
}
}
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end
end
end
def destroy
if request.post? && @member.deletable?
@member.destroy
end
respond_to do |format|
@member.destroy
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'
page << 'hideOnLoad()'
}
}
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end
end
def autocomplete_for_member
@principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals
render :layout => false
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
render_404
end
end

View File

@@ -17,7 +17,6 @@
class MessagesController < ApplicationController
menu_item :boards
default_search_scope :messages
before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => [:preview, :edit, :destroy]
@@ -29,24 +28,10 @@ class MessagesController < ApplicationController
helper :attachments
include AttachmentsHelper
REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
# Show a topic and its replies
def show
page = params[:page]
# Find the page of the requested reply
if params[:r] && page.nil?
offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
page = 1 + offset / REPLIES_PER_PAGE
end
@reply_count = @topic.children.count
@reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
@replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
:order => "#{Message.table_name}.created_on ASC",
:limit => @reply_pages.items_per_page,
:offset => @reply_pages.current.offset)
@replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
@replies.reverse! if User.current.wants_comments_in_reverse_order?
@reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr?
end
@@ -61,9 +46,7 @@ class MessagesController < ApplicationController
@message.sticky = params[:message]['sticky']
end
if request.post? && @message.save
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
attachments = Attachment.attach_files(@message, params[:attachments])
render_attachment_warning_if_needed(@message)
attach_files(@message, params[:attachments])
redirect_to :action => 'show', :id => @message
end
end
@@ -75,47 +58,40 @@ class MessagesController < ApplicationController
@reply.board = @board
@topic.children << @reply
if !@reply.new_record?
call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
attachments = Attachment.attach_files(@reply, params[:attachments])
render_attachment_warning_if_needed(@reply)
attach_files(@reply, params[:attachments])
end
redirect_to :action => 'show', :id => @topic, :r => @reply
redirect_to :action => 'show', :id => @topic
end
# Edit a message
def edit
(render_403; return false) unless @message.editable_by?(User.current)
render_403 and return false unless @message.editable_by?(User.current)
if params[:message]
@message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky']
end
if request.post? && @message.update_attributes(params[:message])
attachments = Attachment.attach_files(@message, params[:attachments])
render_attachment_warning_if_needed(@message)
attach_files(@message, params[:attachments])
flash[:notice] = l(:notice_successful_update)
@message.reload
redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
redirect_to :action => 'show', :id => @topic
end
end
# Delete a messages
def destroy
(render_403; return false) unless @message.destroyable_by?(User.current)
render_403 and return false unless @message.destroyable_by?(User.current)
@message.destroy
redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
{ :action => 'show', :id => @message.parent, :r => @message }
{ :action => 'show', :id => @message.parent }
end
def quote
user = @message.author
text = @message.content
subject = @message.subject.gsub('"', '\"')
subject = "RE: #{subject}" unless subject.starts_with?('RE:')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page|
page << "$('reply_subject').value = \"#{subject}\";"
page.<< "$('message_content').value = \"#{content}\";"
page.show 'reply'
page << "Form.Element.focus('message_content');"

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# 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
@@ -19,7 +19,6 @@ class MyController < ApplicationController
before_filter :require_login
helper :issues
helper :users
helper :custom_fields
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
@@ -29,13 +28,14 @@ class MyController < ApplicationController
'calendar' => :label_calendar,
'documents' => :label_document_plural,
'timelog' => :label_spent_time
}.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
}.freeze
DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
'right' => ['issuesreportedbyme']
}.freeze
verify :xhr => true,
:session => :page_layout,
:only => [:add_block, :remove_block, :order_blocks]
def index
@@ -54,28 +54,31 @@ class MyController < ApplicationController
@user = User.current
@pref = @user.pref
if request.post?
@user.safe_attributes = params[:user]
@user.attributes = params[:user]
@user.mail_notification = (params[:notification_option] == 'all')
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
@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
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
unless @user.change_password_allowed?
flash[:error] = l(:notice_can_t_change_password)
redirect_to :action => 'account'
return
end
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]
@@ -91,65 +94,43 @@ class MyController < ApplicationController
# Create a new feeds key
def reset_rss_key
if request.post?
if User.current.rss_token
User.current.rss_token.destroy
User.current.reload
end
User.current.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
# Create a new API key
def reset_api_key
if request.post?
if User.current.api_token
User.current.api_token.destroy
User.current.reload
end
User.current.api_key
flash[:notice] = l(:notice_api_access_key_reseted)
end
redirect_to :action => 'account'
end
# User's page layout configuration
def page_layout
@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] ||= [] }
@block_options = []
BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
BLOCKS.each {|k, v| @block_options << [l(v), k]}
end
# Add a block to user's page
# The block is added on top of the page
# params[:block] : id of the block to add
def add_block
block = params[:block].to_s.underscore
(render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
block = params[:block]
render(:nothing => true) and return unless block && (BLOCKS.keys.include? block)
@user = User.current
layout = @user.pref[:my_page_layout] || {}
# remove if already present in a group
%w(top left right).each {|f| (layout[f] ||= []).delete block }
%w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
# add it on top
layout['top'].unshift block
@user.pref[:my_page_layout] = layout
@user.pref.save
session[:page_layout]['top'].unshift block
render :partial => "block", :locals => {:user => @user, :block_name => block}
end
# Remove a block to user's page
# params[:block] : id of the block to remove
def remove_block
block = params[:block].to_s.underscore
@user = User.current
block = params[:block]
# remove block in all groups
layout = @user.pref[:my_page_layout] || {}
%w(top left right).each {|f| (layout[f] ||= []).delete block }
@user.pref[:my_page_layout] = layout
@user.pref.save
%w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
render :nothing => true
end
@@ -158,20 +139,23 @@ class MyController < ApplicationController
# params[:list-(top|left|right)] : array of block ids of the group
def order_blocks
group = params[:group]
@user = User.current
if group.is_a?(String)
group_items = (params["list-#{group}"] || []).collect(&:underscore)
if group_items and group_items.is_a? Array
layout = @user.pref[:my_page_layout] || {}
# remove group blocks if they are presents in other groups
%w(top left right).each {|f|
layout[f] = (layout[f] || []) - group_items
}
layout[group] = group_items
@user.pref[:my_page_layout] = layout
@user.pref.save
end
group_items = params["list-#{group}"]
if group_items and group_items.is_a? Array
# remove group blocks if they are presents in other groups
%w(top left right).each {|f|
session[:page_layout][f] = (session[:page_layout][f] || []) - group_items
}
session[:page_layout][group] = group_items
end
render :nothing => true
end
# Save user's page layout
def page_layout_save
@user = User.current
@user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout]
@user.pref.save
session[:page_layout] = nil
redirect_to :action => 'page'
end
end

View File

@@ -16,36 +16,20 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class NewsController < ApplicationController
default_search_scope :news
model_object News
before_filter :find_model_object, :except => [:new, :create, :index]
before_filter :find_project_from_association, :except => [:new, :create, :index]
before_filter :find_project, :only => [:new, :create]
before_filter :authorize, :except => [:index]
before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_project, :only => [:new, :preview]
before_filter :authorize, :except => [:index, :preview]
before_filter :find_optional_project, :only => :index
accept_key_auth :index
def index
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = 10
end
scope = @project ? @project.news.visible : News.visible
@news_count = scope.count
@news_pages = Paginator.new self, @news_count, @limit, params['page']
@offset ||= @news_pages.current.offset
@newss = scope.all(:include => [:author, :project],
:order => "#{News.table_name}.created_on DESC",
:offset => @offset,
:limit => @limit)
@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.api
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
end
end
@@ -57,39 +41,57 @@ class NewsController < ApplicationController
def new
@news = News.new(:project => @project, :author => User.current)
end
def create
@news = News.new(:project => @project, :author => User.current)
if request.post?
@news.attributes = params[:news]
if @news.save
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
redirect_to :controller => 'news', :action => 'index', :project_id => @project
else
render :action => 'new'
end
end
end
def edit
end
def update
if request.put? and @news.update_attributes(params[:news])
def edit
if request.post? and @news.update_attributes(params[:news])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @news
else
render :action => 'edit'
end
end
def add_comment
@comment = Comment.new(params[:comment])
@comment.author = User.current
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news
else
render :action => 'show'
end
end
def destroy_comment
@news.comments.find(params[:comment_id]).destroy
redirect_to :action => 'show', :id => @news
end
def destroy
@news.destroy
redirect_to :action => 'index', :project_id => @project
end
def preview
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
end
private
def find_news
@news = News.find(params[:id])
@project = @news.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound

View File

@@ -1,33 +0,0 @@
class PreviewsController < ApplicationController
before_filter :find_project
def issue
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
if @issue
@attachements = @issue.attachments
@description = params[:issue] && params[:issue][:description]
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
@description = nil
end
@notes = params[:notes]
else
@description = (params[:issue] ? params[:issue][:description] : nil)
end
render :layout => false
end
def news
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
end
private
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -1,26 +0,0 @@
class ProjectEnumerationsController < ApplicationController
before_filter :find_project_by_project_id
before_filter :authorize
def update
if request.put? && params[:enumerations]
Project.transaction do
params[:enumerations].each do |id, activity|
@project.update_or_create_time_entry_activity(id, activity)
end
end
flash[:notice] = l(:notice_successful_update)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def destroy
@project.time_entry_activities.each do |time_entry_activity|
time_entry_activity.destroy(time_entry_activity.parent)
end
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# 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
@@ -17,26 +17,30 @@
class ProjectsController < ApplicationController
menu_item :overview
menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings
menu_item :issues, :only => [:changelog]
before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
before_filter :authorize_global, :only => [:new, :create]
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
accept_key_auth :index, :show, :create, :update, :destroy
after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
before_filter :find_project, :except => [ :index, :list, :add, :activity ]
before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
accept_key_auth :activity
after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
if controller.request.post?
controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
end
end
helper :sort
include SortHelper
helper :custom_fields
include CustomFieldsHelper
helper :issues
helper IssuesHelper
helper :queries
include QueriesHelper
helper :repositories
@@ -49,11 +53,6 @@ class ProjectsController < ApplicationController
format.html {
@projects = Project.visible.find(:all, :order => 'lft')
}
format.api {
@offset, @limit = api_offset_and_limit
@project_count = Project.visible.count
@projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
}
format.atom {
projects = Project.visible.find(:all, :order => 'created_on DESC',
:limit => Setting.feeds_limit.to_i)
@@ -62,77 +61,24 @@ class ProjectsController < ApplicationController
end
end
def new
# Add a new project
def add
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@project = Project.new(params[:project])
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@project = Project.new
@project.safe_attributes = params[:project]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
# Add current user as a project member if he is not admin
unless User.current.admin?
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
m = Member.new(:user => User.current, :roles => [r])
@project.members << m
end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project
}
format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@project) }
end
end
end
def copy
@issue_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')
@source_project = Project.find(params[:id])
if request.get?
@project = Project.copy_from(@source_project)
if @project
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
else
redirect_to :controller => 'admin', :action => 'projects'
end
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Redmine::AccessControl.available_project_modules
else
Mailer.with_deliveries(params[:notifications] == '1') do
@project = Project.new
@project.safe_attributes = params[:project]
@project.enabled_module_names = params[:enabled_modules]
if validate_parent_id && @project.copy(@source_project, :only => params[:only])
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project
elsif !@project.new_record?
# Project was created
# But some objects were not copied due to validation failures
# (eg. issues from disabled trackers)
# TODO: inform about that
redirect_to :controller => 'projects', :action => 'settings', :id => @project
end
end
end
rescue ActiveRecord::RecordNotFound
redirect_to :controller => 'admin', :action => 'projects'
@project.enabled_module_names = params[:enabled_modules]
if @project.save
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
end
end
end
# Show @project
@@ -142,8 +88,9 @@ class ProjectsController < ApplicationController
redirect_to_project_menu_item(@project, params[:jump]) && return
end
@users_by_role = @project.users_by_role
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
@subprojects = @project.children.visible
@ancestors = @project.ancestors.visible
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers
@@ -162,11 +109,6 @@ class ProjectsController < ApplicationController
:conditions => cond).to_f
end
@key = User.current.rss_key
respond_to do |format|
format.html
format.api
end
end
def settings
@@ -178,73 +120,164 @@ class ProjectsController < ApplicationController
@wiki ||= @project.wiki
end
# Edit @project
def edit
end
# TODO: convert to PUT only
verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@project.safe_attributes = params[:project]
if validate_parent_id && @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project
}
format.api { head :ok }
end
else
respond_to do |format|
format.html {
settings
render :action => 'settings'
}
format.api { render_validation_errors(@project) }
if request.post?
@project.attributes = params[:project]
if @project.save
@project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project
else
settings
render :action => 'settings'
end
end
end
verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed }
def modules
@project.enabled_module_names = params[:enabled_module_names]
flash[:notice] = l(:notice_successful_update)
@project.enabled_module_names = params[:enabled_modules]
redirect_to :action => 'settings', :id => @project, :tab => 'modules'
end
def archive
if request.post?
unless @project.archive
flash[:error] = l(:error_can_not_archive_project)
end
end
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
@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(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
redirect_to :controller => 'admin', :action => 'projects'
end
# Delete @project
def destroy
@project_to_destroy = @project
if request.get?
# display confirmation view
else
if api_request? || params[:confirm]
@project_to_destroy.destroy
respond_to do |format|
format.html { redirect_to :controller => 'admin', :action => 'projects' }
format.api { head :ok }
end
end
if request.post? and params[:confirm]
@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
@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
# Add a new version to @project
def add_version
@version = @project.versions.build(params[:version])
if request.post? and @version.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'versions', :id => @project
end
end
def add_file
if request.post?
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = attach_files(container, params[:attachments])
if !attachments.empty? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments)
end
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
return
end
@versions = @project.versions.sort
end
def list_files
sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename",
'created_on' => "#{Attachment.table_name}.created_on",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
# Show changelog for @project
def changelog
@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 = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
@versions = @versions.select {|v| !v.completed? } unless params[:completed]
end
def activity
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
end
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
events = @activity.events(@date_from, @date_to)
respond_to do |format|
format.html {
@events_by_day = events.group_by(&:event_date)
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
rescue ActiveRecord::RecordNotFound
render_404
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])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
return true unless params[:id]
@project = Project.find(params[:id])
@@ -253,18 +286,11 @@ private
render_404
end
# Validates parent_id param according to user's permissions
# TODO: move it to Project model in a validation that depends on User.current
def validate_parent_id
return true if User.current.admin?
parent_id = params[:project] && params[:project][:parent_id]
if parent_id || @project.new_record?
parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
unless @project.allowed_parents.include?(parent)
@project.errors.add :parent_id, :invalid
return false
end
def retrieve_selected_tracker_ids(selectable_trackers)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else
@selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
end
true
end
end

View File

@@ -24,11 +24,12 @@ class QueriesController < ApplicationController
@query = Query.new(params[:query])
@query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
@query.add_filters(params[:fields], params[:operators], params[:values]) if params[:fields]
@query.group_by ||= params[:group_by]
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)
@@ -41,10 +42,12 @@ class QueriesController < ApplicationController
def edit
if request.post?
@query.filters = {}
@query.add_filters(params[:fields], params[:operators], params[:values]) if params[:fields]
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.attributes = params[:query]
@query.project = nil if params[:query_is_for_all]
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
if @query.save
@@ -70,7 +73,7 @@ private
def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id]
render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
User.current.allowed_to?(:save_queries, @project, :global => true)
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -17,79 +17,220 @@
class ReportsController < ApplicationController
menu_item :issues
before_filter :find_project, :authorize, :find_issue_statuses
before_filter :find_project, :authorize
def issue_report
@trackers = @project.trackers
@versions = @project.shared_versions.sort
@priorities = IssuePriority.all
@categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }.sort
@authors = @project.members.collect { |m| m.user }.sort
@subprojects = @project.descendants.visible
@issues_by_tracker = Issue.by_tracker(@project)
@issues_by_version = Issue.by_version(@project)
@issues_by_priority = Issue.by_priority(@project)
@issues_by_category = Issue.by_category(@project)
@issues_by_assigned_to = Issue.by_assigned_to(@project)
@issues_by_author = Issue.by_author(@project)
@issues_by_subproject = Issue.by_subproject(@project) || []
render :template => "reports/issue_report"
end
def issue_report_details
@statuses = IssueStatus.find(:all, :order => 'position')
case params[:detail]
when "tracker"
@field = "tracker_id"
@rows = @project.trackers
@data = Issue.by_tracker(@project)
@data = issues_by_tracker
@report_title = l(:field_tracker)
render :template => "reports/issue_report_details"
when "version"
@field = "fixed_version_id"
@rows = @project.shared_versions.sort
@data = Issue.by_version(@project)
@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 = IssuePriority.all
@data = Issue.by_priority(@project)
@rows = Enumeration::get_values('IPRI')
@data = issues_by_priority
@report_title = l(:field_priority)
render :template => "reports/issue_report_details"
when "category"
@field = "category_id"
@rows = @project.issue_categories
@data = Issue.by_category(@project)
@data = issues_by_category
@report_title = l(:field_category)
render :template => "reports/issue_report_details"
when "assigned_to"
@field = "assigned_to_id"
@rows = @project.members.collect { |m| m.user }.sort
@data = Issue.by_assigned_to(@project)
@rows = @project.members.collect { |m| m.user }
@data = issues_by_assigned_to
@report_title = l(:field_assigned_to)
render :template => "reports/issue_report_details"
when "author"
@field = "author_id"
@rows = @project.members.collect { |m| m.user }.sort
@data = Issue.by_author(@project)
@rows = @project.members.collect { |m| m.user }
@data = issues_by_author
@report_title = l(:field_author)
render :template => "reports/issue_report_details"
when "subproject"
@field = "project_id"
@rows = @project.descendants.visible
@data = Issue.by_subproject(@project) || []
@rows = @project.descendants.active
@data = issues_by_subproject
@report_title = l(:field_subproject)
render :template => "reports/issue_report_details"
else
@trackers = @project.trackers
@versions = @project.versions.sort
@priorities = Enumeration::get_values('IPRI')
@categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }
@authors = @project.members.collect { |m| m.user }
@subprojects = @project.descendants.active
issues_by_tracker
issues_by_version
issues_by_priority
issues_by_category
issues_by_assigned_to
issues_by_author
issues_by_subproject
render :template => "reports/issue_report"
end
respond_to do |format|
if @field
format.html {}
else
format.html { redirect_to :action => 'issue_report', :id => @project }
end
end
def delays
@trackers = Tracker.find(:all)
if request.get?
@selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
else
@selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
end
@selected_tracker_ids ||= []
@raw =
ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
FROM issue_histories a, issue_histories b, issues i
WHERE a.status_id =5
AND a.issue_id = b.issue_id
AND a.issue_id = i.id
AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
AND b.id = (
SELECT min( c.id )
FROM issue_histories c
WHERE b.issue_id = c.issue_id )
GROUP BY delay") unless @selected_tracker_ids.empty?
@raw ||=[]
@x_from = 0
@x_to = 0
@y_from = 0
@y_to = 0
@sum_total = 0
@sum_delay = 0
@raw.each do |r|
@x_to = [r['delay'].to_i, @x_to].max
@y_to = [r['total'].to_i, @y_to].max
@sum_total = @sum_total + r['total'].to_i
@sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
end
end
private
# Find project of id params[:id]
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
private
def issues_by_tracker
@issues_by_tracker ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
t.id as tracker_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
where
i.status_id=s.id
and i.tracker_id=t.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, t.id")
end
def find_issue_statuses
@statuses = IssueStatus.find(:all, :order => 'position')
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,
s.is_closed as closed,
p.id as priority_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
where
i.status_id=s.id
and i.priority_id=p.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, p.id")
end
def issues_by_category
@issues_by_category ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
c.id as category_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
where
i.status_id=s.id
and i.category_id=c.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, c.id")
end
def issues_by_assigned_to
@issues_by_assigned_to ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
a.id as assigned_to_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
where
i.status_id=s.id
and i.assigned_to_id=a.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, a.id")
end
def issues_by_author
@issues_by_author ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
a.id as author_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
where
i.status_id=s.id
and i.author_id=a.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, a.id")
end
def issues_by_subproject
@issues_by_subproject ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
i.project_id as project_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s
where
i.status_id=s.id
and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')})
group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
@issues_by_subproject ||= []
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# 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
@@ -24,9 +24,6 @@ class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController
menu_item :repository
menu_item :settings, :only => :edit
default_search_scope :changesets
before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit
before_filter :authorize
@@ -44,13 +41,7 @@ class RepositoriesController < ApplicationController
@repository.attributes = params[:repository]
@repository.save
end
render(:update) do |page|
page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'
if @repository && !@project.repository
@project.reload #needed to reload association
page.replace_html "main-menu", render_main_menu(@project)
end
end
render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
end
def committers
@@ -73,26 +64,31 @@ class RepositoriesController < ApplicationController
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
end
def show
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
def show
# check if new revisions have been committed in the repository
@repository.fetch_changesets if Setting.autofetch_changesets?
# root entries
@entries = @repository.entries('', @rev)
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error_not_found unless @entries || @changesets.any?
end
def browse
@entries = @repository.entries(@path, @rev)
if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
(show_error_not_found; return) unless @entries
@changesets = @repository.latest_changesets(@path, @rev)
show_error_not_found and return unless @entries
@properties = @repository.properties(@path, @rev)
render :action => 'show'
render :action => 'browse'
end
end
alias_method :browse, :show
def changes
@entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry
@changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
show_error_not_found and return unless @entry
@changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
@properties = @repository.properties(@path, @rev)
end
@@ -104,7 +100,7 @@ class RepositoriesController < ApplicationController
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset,
:include => [:user, :repository])
:include => :user)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
@@ -114,16 +110,16 @@ class RepositoriesController < ApplicationController
def entry
@entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry
show_error_not_found and return unless @entry
# If the entry is a dir, show the browser
(show; return) if @entry.is_dir?
browse and return if @entry.is_dir?
@content = @repository.cat(@path, @rev)
(show_error_not_found; return) unless @content
if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
# Force the download
send_data @content, :filename => filename_for_content_disposition(@path.split('/').last)
show_error_not_found and return unless @content
if 'raw' == params[:format] || @content.is_binary_data?
# Force the download if it's a binary file
send_data @content, :filename => @path.split('/').last
else
# Prevent empty lines when displaying a file with Windows style eol
@content.gsub!("\r\n", "\n")
@@ -132,15 +128,14 @@ class RepositoriesController < ApplicationController
def annotate
@entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry
show_error_not_found and return unless @entry
@annotate = @repository.scm.annotate(@path, @rev)
(render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
end
def revision
raise ChangesetNotFound if @rev.nil? || @rev.empty?
@changeset = @repository.find_changeset_by_name(@rev)
@changeset = @repository.changesets.find_by_revision(@rev)
raise ChangesetNotFound unless @changeset
respond_to do |format|
@@ -154,7 +149,7 @@ class RepositoriesController < ApplicationController
def diff
if params[:format] == 'diff'
@diff = @repository.diff(@path, @rev, @rev_to)
(show_error_not_found; return) unless @diff
show_error_not_found and return unless @diff
filename = "changeset_r#{@rev}"
filename << "_r#{@rev_to}" if @rev_to
send_data @diff.join, :filename => "#{filename}.diff",
@@ -175,9 +170,6 @@ class RepositoriesController < ApplicationController
@diff = @repository.diff(@path, @rev, @rev_to)
show_error_not_found unless @diff
end
@changeset = @repository.find_changeset_by_name(@rev)
@changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
end
end
@@ -200,24 +192,24 @@ class RepositoriesController < ApplicationController
end
end
private
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
REV_PARAM_RE = %r{^[a-f0-9]*$}
def find_repository
@project = Project.find(params[:id])
@repository = @project.repository
(render_404; return false) unless @repository
render_404 and return false unless @repository
@path = params[:path].join('/') unless params[:path].nil?
@path ||= ''
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
@rev = params[:rev]
@rev_to = params[:rev_to]
unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
if @repository.branches.blank?
raise InvalidRevisionParam
end
end
raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
@@ -225,7 +217,7 @@ class RepositoriesController < ApplicationController
end
def show_error_not_found
render_error :message => l(:error_scm_not_found), :status => 404
render_error l(:error_scm_not_found)
end
# Handler for Redmine::Scm::Adapters::CommandFailed exception
@@ -246,7 +238,8 @@ class RepositoriesController < ApplicationController
changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
fields = []
12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
graph = SVG::Graph::Bar.new(
:height => 300,
@@ -275,7 +268,7 @@ class RepositoriesController < ApplicationController
def graph_commits_per_author(repository)
commits_by_author = repository.changesets.count(:all, :group => :committer)
commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
commits_by_author.sort! {|x, y| x.last <=> y.last}
changes_by_author = repository.changes.count(:all, :group => :committer)
h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}

View File

@@ -16,16 +16,19 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class RolesController < ApplicationController
layout 'admin'
before_filter :require_admin
verify :method => :post, :only => [ :destroy, :move ],
:redirect_to => { :action => :index }
:redirect_to => { :action => :list }
def index
list
render :action => 'list' unless request.xhr?
end
def list
@role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
render :action => "index", :layout => false if request.xhr?
render :action => "list", :layout => false if request.xhr?
end
def new
@@ -37,7 +40,7 @@ class RolesController < ApplicationController
@role.workflows.copy(copy_from)
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index'
redirect_to :action => 'list'
end
@permissions = @role.setable_permissions
@roles = Role.find :all, :order => 'builtin, position'
@@ -47,7 +50,7 @@ class RolesController < ApplicationController
@role = Role.find(params[:id])
if request.post? and @role.update_attributes(params[:role])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index'
redirect_to :action => 'list'
end
@permissions = @role.setable_permissions
end
@@ -55,12 +58,27 @@ class RolesController < ApplicationController
def destroy
@role = Role.find(params[:id])
@role.destroy
redirect_to :action => 'index'
redirect_to :action => 'list'
rescue
flash[:error] = l(:error_can_not_remove_role)
flash[:error] = 'This role is in use and can not be deleted.'
redirect_to :action => 'index'
end
def move
@role = Role.find(params[:id])
case params[:position]
when 'highest'
@role.move_to_top
when 'higher'
@role.move_higher
when 'lower'
@role.move_lower
when 'lowest'
@role.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def report
@roles = Role.find(:all, :order => 'builtin, position')
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
@@ -70,7 +88,7 @@ class RolesController < ApplicationController
role.save
end
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index'
redirect_to :action => 'list'
end
end
end

View File

@@ -43,12 +43,12 @@ class SearchController < ApplicationController
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
# quick jump to an issue
if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1.to_i)
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
@object_types = Redmine::Search.available_search_types.dup
@object_types = %w(issues news documents changesets wiki_pages messages projects)
if projects_to_search.is_a? Project
# don't search projects
@object_types.delete('projects')
@@ -62,19 +62,21 @@ class SearchController < ApplicationController
# 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 2 characters long
@tokens = @tokens.uniq.select {|w| w.length > 1 }
# 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
@tokens.slice! 5..-1 if @tokens.size > 5
# strings used in sql like statement
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
@results = []
@results_by_type = Hash.new {|h,k| h[k] = 0}
limit = 10
@scope.each do |s|
r, c = s.singularize.camelcase.constantize.search(@tokens, projects_to_search,
r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
:all_words => @all_words,
:titles_only => @titles_only,
:limit => (limit+1),

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SettingsController < ApplicationController
layout 'admin'
before_filter :require_admin
def index
@@ -26,7 +24,7 @@ class SettingsController < ApplicationController
end
def edit
@notifiables = Redmine::Notifiable.all
@notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted)
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value|
@@ -44,8 +42,6 @@ class SettingsController < ApplicationController
@guessed_host_and_path = request.host_with_port.dup
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
Redmine::Themes.rescan
end
def plugin

View File

@@ -37,30 +37,13 @@ class SysController < ActionController::Base
end
end
end
def fetch_changesets
projects = []
if params[:id]
projects << Project.active.has_module(:repository).find(params[:id])
else
projects = Project.active.has_module(:repository).find(:all, :include => :repository)
end
projects.each do |project|
if project.repository
project.repository.fetch_changesets
end
end
render :nothing => true, :status => 200
rescue ActiveRecord::RecordNotFound
render :nothing => true, :status => 404
end
protected
def check_enabled
User.current = nil
unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
unless Setting.sys_api_enabled?
render :nothing => 'Access denied. Repository management WS is disabled.', :status => 403
return false
end
end

View File

@@ -1,209 +0,0 @@
class TimeEntryReportsController < ApplicationController
menu_item :issues
before_filter :find_optional_project
before_filter :load_available_criterias
helper :sort
include SortHelper
helper :issues
helper :timelog
include TimelogHelper
helper :custom_fields
include CustomFieldsHelper
def report
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql_condition = ''
if @project.nil?
sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
else
sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
end
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << time_report_joins
sql << " WHERE"
sql << " (%s) AND" % sql_condition
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@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']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
private
# TODO: duplicated in TimelogController
def find_optional_project
if !params[:issue_id].blank?
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif !params[:project_id].blank?
@project = Project.find(params[:project_id])
end
deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
end
# Retrieves the date range based on predefined ranges or specific from/to param dates
# TODO: duplicated in TimelogController
def retrieve_date_range
@free_period = false
@from, @to = nil, nil
if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
case params[:period].to_s
when 'today'
@from = @to = Date.today
when 'yesterday'
@from = @to = Date.today - 1
when 'current_week'
@from = Date.today - (Date.today.cwday - 1)%7
@to = @from + 6
when 'last_week'
@from = Date.today - 7 - (Date.today.cwday - 1)%7
@to = @from + 6
when '7_days'
@from = Date.today - 7
@to = Date.today
when 'current_month'
@from = Date.civil(Date.today.year, Date.today.month, 1)
@to = (@from >> 1) - 1
when 'last_month'
@from = Date.civil(Date.today.year, Date.today.month, 1) << 1
@to = (@from >> 1) - 1
when '30_days'
@from = Date.today - 30
@to = Date.today
when 'current_year'
@from = Date.civil(Date.today.year, 1, 1)
@to = Date.civil(Date.today.year, 12, 31)
end
elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
@free_period = true
else
# default
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
@to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
end
def load_available_criterias
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:klass => IssueCategory,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:klass => User,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => TimeEntryActivity,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
}
# Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end if @project
# Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end
# Add list and boolean time entry activity custom fields
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
:format => cf.field_format,
:label => cf.name}
end
call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
@available_criterias
end
def time_report_joins
sql = ''
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
# TODO: rename hook
call_hook(:controller_timelog_time_report_joins, {:sql => sql} )
sql
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# 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
@@ -17,11 +17,10 @@
class TimelogController < ApplicationController
menu_item :issues
before_filter :find_project, :only => [:new, :create]
before_filter :find_time_entry, :only => [:show, :edit, :update, :destroy]
before_filter :authorize, :except => [:index]
before_filter :find_optional_project, :only => [:index]
accept_key_auth :index, :show, :create, :update, :destroy
before_filter :find_project, :authorize, :only => [:edit, :destroy]
before_filter :find_optional_project, :only => [:report, :details]
verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
helper :sort
include SortHelper
@@ -30,7 +29,114 @@ class TimelogController < ApplicationController
helper :custom_fields
include CustomFieldsHelper
def index
def report
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:klass => IssueCategory,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:klass => User,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:klass => Tracker,
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => Enumeration,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
}
# Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end if @project
# Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE"
sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@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']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
end
def details
sort_init 'spent_on', 'desc'
sort_update 'spent_on' => 'spent_on',
'user' => 'user_id',
@@ -45,7 +151,7 @@ class TimelogController < ApplicationController
elsif @issue.nil?
cond << @project.project_condition(Setting.display_subprojects_issues?)
else
cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
end
retrieve_date_range
@@ -55,7 +161,7 @@ class TimelogController < ApplicationController
respond_to do |format|
format.html {
# Paginate results
@entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
@entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
@@ -63,20 +169,10 @@ class TimelogController < ApplicationController
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
@total_hours = TimeEntry.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
@total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
render :layout => !request.xhr?
}
format.api {
@entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
}
format.atom {
entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
@@ -91,120 +187,43 @@ class TimelogController < ApplicationController
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions,
:order => sort_clause)
send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
}
end
end
end
def show
respond_to do |format|
# TODO: Implement html response
format.html { render :nothing => true, :status => 406 }
format.api
end
end
def new
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
render :action => 'edit'
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if @time_entry.save
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
}
format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@time_entry) }
end
end
end
def edit
render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if @time_entry.save
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
}
format.api { head :ok }
end
else
respond_to do |format|
format.html { render :action => 'edit' }
format.api { render_validation_errors(@time_entry) }
end
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'details', :project_id => @time_entry.project
return
end
end
verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
def destroy
if @time_entry.destroy && @time_entry.destroyed?
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_delete)
redirect_to :back
}
format.api { head :ok }
end
else
respond_to do |format|
format.html {
flash[:error] = l(:notice_unable_delete_time_entry)
redirect_to :back
}
format.api { render_validation_errors(@time_entry) }
end
end
render_404 and return unless @time_entry
render_403 and return unless @time_entry.editable_by?(User.current)
@time_entry.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :back
rescue ::ActionController::RedirectBackError
redirect_to :action => 'index', :project_id => @time_entry.project
redirect_to :action => 'details', :project_id => @time_entry.project
end
private
def find_time_entry
@time_entry = TimeEntry.find(params[:id])
unless @time_entry.editable_by?(User.current)
render_403
return false
end
@project = @time_entry.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
@issue = Issue.find(issue_id)
if params[:id]
@time_entry = TimeEntry.find(params[:id])
@project = @time_entry.project
elsif params[:issue_id]
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
@project = Project.find(project_id)
elsif params[:project_id]
@project = Project.find(params[:project_id])
else
render_404
return false
@@ -265,8 +284,7 @@ private
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
@to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
@from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
@to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# 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
@@ -16,15 +16,19 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TrackersController < ApplicationController
layout 'admin'
before_filter :require_admin
verify :method => :post, :only => :destroy, :redirect_to => { :action => :index }
def index
list
render :action => 'list' unless request.xhr?
end
# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :move ], :redirect_to => { :action => :list }
def list
@tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
render :action => "index", :layout => false if request.xhr?
render :action => "list", :layout => false if request.xhr?
end
def new
@@ -35,7 +39,7 @@ class TrackersController < ApplicationController
@tracker.workflows.copy(copy_from)
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index'
redirect_to :action => 'list'
return
end
@trackers = Tracker.find :all, :order => 'position'
@@ -46,19 +50,34 @@ class TrackersController < ApplicationController
@tracker = Tracker.find(params[:id])
if request.post? and @tracker.update_attributes(params[:tracker])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index'
redirect_to :action => 'list'
return
end
@projects = Project.find(:all)
end
def move
@tracker = Tracker.find(params[:id])
case params[:position]
when 'highest'
@tracker.move_to_top
when 'higher'
@tracker.move_higher
when 'lower'
@tracker.move_lower
when 'lowest'
@tracker.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def destroy
@tracker = Tracker.find(params[:id])
unless @tracker.issues.empty?
flash[:error] = l(:error_can_not_delete_tracker)
flash[:error] = "This tracker contains issues and can\'t be deleted."
else
@tracker.destroy
end
redirect_to :action => 'index'
redirect_to :action => 'list'
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# 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
@@ -16,11 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class UsersController < ApplicationController
layout 'admin'
before_filter :require_admin, :except => :show
before_filter :find_user, :only => [:show, :edit, :update, :edit_membership, :destroy_membership]
accept_key_auth :index, :show, :create, :update
before_filter :require_admin
helper :sort
include SortHelper
@@ -28,198 +24,81 @@ class UsersController < ApplicationController
include CustomFieldsHelper
def index
list
render :action => 'list' unless request.xhr?
end
def list
sort_init 'login', 'asc'
sort_update %w(login firstname lastname mail admin created_on last_login_on)
case params[:format]
when 'xml', 'json'
@offset, @limit = api_offset_and_limit
else
@limit = per_page_option
end
@status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
unless params[:name].blank?
name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
end
@user_count = User.count(:conditions => c.conditions)
@user_pages = Paginator.new self, @user_count, @limit, params['page']
@offset ||= @user_pages.current.offset
@users = User.find :all,
:order => sort_clause,
@user_pages = Paginator.new self, @user_count,
per_page_option,
params['page']
@users = User.find :all,:order => sort_clause,
:conditions => c.conditions,
:limit => @limit,
:offset => @offset
:limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.api
end
end
def show
# show projects based on current user visibility
@memberships = @user.memberships.all(:conditions => Project.visible_by(User.current))
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
unless User.current.admin?
if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
render_404
return
end
end
respond_to do |format|
format.html { render :layout => 'base' }
format.api
end
render :action => "list", :layout => false if request.xhr?
end
def new
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
@auth_sources = AuthSource.find(:all)
end
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
def create
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
@user.safe_attributes = params[:user]
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
# TODO: Similar to My#account
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
Mailer.deliver_account_information(@user, params[:user][:password]) if params[:send_information]
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_create)
redirect_to(params[:continue] ?
{:controller => 'users', :action => 'new'} :
{:controller => 'users', :action => 'edit', :id => @user}
)
}
format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
end
def add
if request.get?
@user = User.new(:language => Setting.default_language)
else
@auth_sources = AuthSource.find(:all)
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format|
format.html { render :action => 'new' }
format.api { render_validation_errors(@user) }
@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
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
end
@auth_sources = AuthSource.find(:all)
end
def edit
@user = User.find(params[:id])
if request.post?
@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 @user.update_attributes(params[:user])
flash[:notice] = l(:notice_successful_update)
# Give a string to redirect_to otherwise it would use status param as the response code
redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
end
end
@auth_sources = AuthSource.find(:all)
@roles = Role.find_all_givable
@projects = Project.active.find(:all, :order => 'lft')
@membership ||= Member.new
@memberships = @user.memberships
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def update
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
end
@user.safe_attributes = params[:user]
# Was the account actived ? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
# TODO: Similar to My#account
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
if was_activated
Mailer.deliver_account_activated(@user)
elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
Mailer.deliver_account_information(@user, params[:user][:password])
end
respond_to do |format|
format.html {
flash[:notice] = l(:notice_successful_update)
redirect_to :back
}
format.api { head :ok }
end
else
@auth_sources = AuthSource.find(:all)
@membership ||= Member.new
# Clear password input
@user.password = @user.password_confirmation = nil
respond_to do |format|
format.html { render :action => :edit }
format.api { render_validation_errors(@user) }
end
end
rescue ::ActionController::RedirectBackError
redirect_to :controller => 'users', :action => 'edit', :id => @user
end
def edit_membership
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
@user = User.find(params[:id])
@membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
@membership.attributes = params[:membership]
@membership.save if request.post?
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'users/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
else
format.js {
render(:update) {|page|
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
}
}
end
end
redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
end
def destroy_membership
@membership = Member.find(params[:membership_id])
if request.post? && @membership.deletable?
@membership.destroy
end
respond_to do |format|
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
end
end
private
def find_user
if params[:id] == 'current'
require_login || return
@user = User.current
else
@user = User.find(params[:id])
end
rescue ActiveRecord::RecordNotFound
render_404
@user = User.find(params[:id])
Member.find(params[:membership_id]).destroy if request.post?
redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
end
end

View File

@@ -17,121 +17,24 @@
class VersionsController < ApplicationController
menu_item :roadmap
model_object Version
before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
before_filter :find_project, :only => [:index, :new, :create, :close_completed]
before_filter :authorize
before_filter :find_project, :authorize
helper :custom_fields
helper :projects
def index
@trackers = @project.trackers.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
@versions = @project.shared_versions || []
@versions += @project.rolled_up_versions.visible if @with_subprojects
@versions = @versions.uniq.sort
@versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
@issues_by_version = {}
unless @selected_tracker_ids.empty?
@versions.each do |version|
issues = version.fixed_issues.visible.find(:all,
:include => [:project, :status, :tracker, :priority],
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version[version] = issues
end
end
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
end
def show
@issues = @version.fixed_issues.visible.find(:all,
:include => [:status, :tracker, :priority],
:order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
end
def new
@version = @project.versions.build
if params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
end
def create
# TODO: refactor with code above in #new
@version = @project.versions.build
if params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
@version.attributes = attributes
end
if request.post?
if @version.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :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_fixed_version_id",
content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
}
end
end
else
respond_to do |format|
format.html { render :action => 'new' }
format.js do
render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
end
end
end
end
end
def edit
end
def update
if request.put? && params[:version]
attributes = params[:version].dup
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
if @version.update_attributes(attributes)
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
else
respond_to do |format|
format.html { render :action => 'edit' }
end
end
if request.post? and @version.update_attributes(params[:version])
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
end
def close_completed
if request.put?
@project.close_completed_versions
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def destroy
if @version.fixed_issues.empty?
@version.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
else
flash[:error] = l(:notice_unable_delete_version)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
@version.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
rescue
flash[:error] = l(:notice_unable_delete_version)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def status_by
@@ -143,17 +46,9 @@ class VersionsController < ApplicationController
private
def find_project
@project = Project.find(params[:project_id])
@version = Version.find(params[:id])
@project = @version.project
rescue ActiveRecord::RecordNotFound
render_404
end
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
end
end
end
end

View File

@@ -18,18 +18,14 @@
class WatchersController < ApplicationController
before_filter :find_project
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
before_filter :authorize, :only => [:new, :destroy]
before_filter :authorize, :only => :new
verify :method => :post,
:only => [ :watch, :unwatch ],
:render => { :nothing => true, :status => :method_not_allowed }
def watch
if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
render_403
else
set_watcher(User.current, true)
end
set_watcher(User.current, true)
end
def unwatch
@@ -52,18 +48,6 @@ class WatchersController < ApplicationController
render :text => 'Watcher added.', :layout => true
end
def destroy
@watched.set_watcher(User.find(params[:user_id]), false) if request.post?
respond_to do |format|
format.html { redirect_to :back }
format.js do
render :update do |page|
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
end
end
end
end
private
def find_project
klass = Object.const_get(params[:object_type].camelcase)
@@ -76,24 +60,9 @@ private
def set_watcher(user, watching)
@watched.set_watcher(user, watching)
if params[:replace].present?
if params[:replace].is_a? Array
replace_ids = params[:replace]
else
replace_ids = [params[:replace]]
end
else
replace_ids = ['watcher']
end
respond_to do |format|
format.html { redirect_to :back }
format.js do
render(:update) do |page|
replace_ids.each do |replace_id|
page.replace_html replace_id, watcher_link(@watched, user, :replace => replace_ids)
end
end
end
format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
end
rescue ::ActionController::RedirectBackError
render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true

View File

@@ -24,7 +24,7 @@ class WelcomeController < ApplicationController
end
def robots
@projects = Project.all_public.active
@projects = Project.public.active
render :layout => false, :content_type => 'text/plain'
end
end

View File

@@ -17,42 +17,21 @@
require 'diff'
# The WikiController follows the Rails REST controller pattern but with
# a few differences
#
# * index - shows a list of WikiPages grouped by page or date
# * new - not used
# * create - not used
# * show - will also show the form for creating a new wiki page
# * edit - used to edit an existing or new page
# * update - used to save a wiki page update to the database, including new pages
# * destroy - normal
#
# Other member and collection methods are also used
#
# TODO: still being worked on
class WikiController < ApplicationController
default_search_scope :wiki_pages
before_filter :find_wiki, :authorize
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
verify :method => :post, :only => [:protect], :redirect_to => { :action => :show }
verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
helper :attachments
include AttachmentsHelper
helper :watchers
# List of pages, sorted alphabetically and by parent (hierarchy)
def index
load_pages_grouped_by_date_without_content
end
# display a page (in editing mode if it doesn't exist)
def show
page_title = params[:id]
def index
page_title = params[:page]
@page = @wiki.find_or_new_page(page_title)
if @page.new_record?
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
if User.current.allowed_to?(:edit_wiki_pages, @project)
edit
render :action => 'edit'
else
@@ -66,23 +45,21 @@ class WikiController < ApplicationController
return
end
@content = @page.content_for_version(params[:version])
if User.current.allowed_to?(:export_wiki_pages, @project)
if params[:format] == 'html'
export = render_to_string :action => 'export', :layout => false
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
return
elsif params[:format] == 'txt'
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
return
end
if params[:format] == 'html'
export = render_to_string :action => 'export', :layout => false
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
return
elsif params[:format] == 'txt'
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
return
end
@editable = editable?
@editable = editable?
render :action => 'show'
end
# edit an existing page or a new one
def edit
@page = @wiki.find_or_new_page(params[:id])
@page = @wiki.find_or_new_page(params[:page])
return render_403 unless editable?
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@@ -90,50 +67,29 @@ class WikiController < ApplicationController
@content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment
@content.comments = nil
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
# Creates a new page or updates an existing one
def update
@page = @wiki.find_or_new_page(params[:id])
return render_403 unless editable?
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version])
@content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment
@content.comments = nil
if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
# don't save if text wasn't changed
redirect_to :action => 'show', :project_id => @project, :id => @page.title
return
end
@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)
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
redirect_to :action => 'show', :project_id => @project, :id => @page.title
if request.get?
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
else
render :action => 'edit'
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.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
return render_403 unless editable?
@@ -142,13 +98,13 @@ class WikiController < ApplicationController
@original_title = @page.pretty_title
if request.post? && @page.update_attributes(params[:wiki_page])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :project_id => @project, :id => @page.title
redirect_to :action => 'index', :id => @project, :page => @page.title
end
end
def protect
@page.update_attribute :protected, params[:protected]
redirect_to :action => 'show', :project_id => @project, :id => @page.title
redirect_to :action => 'index', :id => @project, :page => @page.title
end
# show page history
@@ -174,54 +130,41 @@ class WikiController < ApplicationController
@annotate = @page.annotate(params[:version])
render_404 unless @annotate
end
verify :method => :delete, :only => [:destroy], :redirect_to => { :action => :show }
# Removes a wiki page and its history
# Children can be either set as root pages, removed or reassigned to another parent page
# remove a wiki page and its history
def destroy
return render_403 unless editable?
@descendants_count = @page.descendants.size
if @descendants_count > 0
case params[:todo]
when 'nullify'
# Nothing to do
when 'destroy'
# Removes all its descendants
@page.descendants.each(&:destroy)
when 'reassign'
# Reassign children to another parent page
reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
return unless reassign_to
@page.children.each do |child|
child.update_attribute(:parent, reassign_to)
end
else
@reassignable_to = @wiki.pages - @page.self_and_descendants
return
end
end
@page.destroy
redirect_to :action => 'index', :project_id => @project
redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end
# Export wiki to a single html file
def export
if User.current.allowed_to?(:export_wiki_pages, @project)
# display special pages
def special
page_title = params[:page].downcase
case page_title
# show pages index, sorted by title
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}
@pages_by_parent_id = @pages.group_by(&:parent_id)
# export wiki to a single html file
when 'export'
@pages = @wiki.pages.find :all, :order => 'title'
export = render_to_string :action => 'export_multiple', :layout => false
send_data(export, :type => 'text/html', :filename => "wiki.html")
return
else
redirect_to :action => 'show', :project_id => @project, :id => nil
# requested special page doesn't exist, redirect to default page
redirect_to :action => 'index', :id => @project, :page => nil and return
end
end
def date_index
load_pages_grouped_by_date_without_content
render :action => "special_#{page_title}"
end
def preview
page = @wiki.find_page(params[:id])
page = @wiki.find_page(params[:page])
# page is nil when previewing a new page
return render_403 unless page.nil? || editable?(page)
if page
@@ -234,15 +177,14 @@ class WikiController < ApplicationController
def add_attachment
return render_403 unless editable?
attachments = Attachment.attach_files(@page, params[:attachments])
render_attachment_warning_if_needed(@page)
redirect_to :action => 'show', :id => @page.title, :project_id => @project
attach_files(@page, params[:attachments])
redirect_to :action => 'index', :page => @page.title
end
private
def find_wiki
@project = Project.find(params[:project_id])
@project = Project.find(params[:id])
@wiki = @project.wiki
render_404 unless @wiki
rescue ActiveRecord::RecordNotFound
@@ -251,7 +193,7 @@ private
# Finds the requested page and returns a 404 error if it doesn't exist
def find_existing_page
@page = @wiki.find_page(params[:id])
@page = @wiki.find_page(params[:page])
render_404 if @page.nil?
end
@@ -266,14 +208,4 @@ private
extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page)
end
# eager load information about last updates, without loading text
def load_pages_grouped_by_date_without_content
@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}
@pages_by_parent_id = @pages.group_by(&:parent_id)
end
end

View File

@@ -34,4 +34,11 @@ class WikisController < ApplicationController
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,12 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WorkflowsController < ApplicationController
layout 'admin'
before_filter :require_admin
before_filter :find_roles
before_filter :find_trackers
def index
@workflow_counts = Workflow.count_by_tracker_and_role
end
@@ -42,50 +38,8 @@ class WorkflowsController < ApplicationController
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
end
end
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
@statuses = @tracker.issue_statuses
end
@statuses ||= IssueStatus.find(:all, :order => 'position')
end
def copy
if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
@source_tracker = nil
else
@source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
end
if params[:source_role_id].blank? || params[:source_role_id] == 'any'
@source_role = nil
else
@source_role = Role.find_by_id(params[:source_role_id].to_i)
end
@target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
@target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
if request.post?
if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
flash.now[:error] = l(:error_workflow_copy_source)
elsif @target_trackers.nil? || @target_roles.nil?
flash.now[:error] = l(:error_workflow_copy_target)
else
Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
end
end
end
private
def find_roles
@roles = Role.find(:all, :order => 'builtin, position')
end
def find_trackers
@trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
end
end

View File

@@ -20,4 +20,12 @@ module AdminHelper
options_for_select([[l(:label_all), ''],
[l(:status_active), 1]], selected)
end
def css_project_classes(project)
s = 'project'
s << ' root' if project.root?
s << ' child' if project.child?
s << (project.leaf? ? ' leaf' : ' parent')
s
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# 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
@@ -15,28 +15,28 @@
# 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 'forwardable'
require 'cgi'
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
include Redmine::I18n
include GravatarHelper::PublicMethods
extend Forwardable
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
def current_role
@current_role ||= User.current.role_for_project(@project)
end
# Return true if user is authorized for controller/action, otherwise false
def authorize_for(controller, action)
User.current.allowed_to?({:controller => controller, :action => action}, @project)
end
# Display a link if user is authorized
#
# @param [String] name Anchor text (passed to link_to)
# @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
# @param [optional, Hash] html_options Options passed to link_to
# @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
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] || params[:controller], options[:action])
end
@@ -47,45 +47,16 @@ module ApplicationHelper
link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
end
# Displays a link to user's account page if active
# Display a link to user's account page
def link_to_user(user, options={})
if user.is_a?(User)
name = h(user.name(options[:format]))
if user.active?
link_to name, :controller => 'users', :action => 'show', :id => user
else
name
end
else
h(user.to_s)
end
(user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
end
# Displays a link to +issue+ with its subject.
# Examples:
#
# link_to_issue(issue) # => Defect #6: This is the subject
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
# link_to_issue(issue, :subject => false) # => Defect #6
# link_to_issue(issue, :project => true) # => Foo - Defect #6
#
def link_to_issue(issue, options={})
title = nil
subject = nil
if options[:subject] == false
title = truncate(issue.subject, :length => 60)
else
subject = issue.subject
if options[:truncate]
subject = truncate(subject, :length => options[:truncate])
end
end
s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
:class => issue.css_classes,
:title => title
s << ": #{h subject}" if subject
s = "#{h issue.project} - " + s if options[:project]
s
options[:class] ||= ''
options[:class] << ' issue'
options[:class] << ' closed' if issue.closed?
link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
end
# Generates a link to an attachment.
@@ -99,34 +70,6 @@ module ApplicationHelper
link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
end
# Generates a link to a SCM revision
# Options:
# * :text - Link text (default to the formatted revision)
def link_to_revision(revision, project, options={})
text = options.delete(:text) || format_revision(revision)
rev = revision.respond_to?(:identifier) ? revision.identifier : revision
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
:title => l(:label_revision_id, format_revision(revision)))
end
# Generates a link to a project if active
# Examples:
#
# link_to_project(project) # => link to the specified project overview
# link_to_project(project, :action=>'settings') # => link to project settings
# link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
# link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
#
def link_to_project(project, options={}, html_options = nil)
if project.active?
url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
link_to(h(project), url, html_options)
else
h(project)
end
end
def toggle_link(name, id, options={})
onclick = "Element.toggle('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
@@ -146,9 +89,26 @@ module ApplicationHelper
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
link_to name, {}, html_options
end
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
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
@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 format_activity_title(text)
h(truncate_single_line(text, :length => 100))
h(truncate_single_line(text, 100))
end
def format_activity_day(date)
@@ -156,17 +116,16 @@ module ApplicationHelper
end
def format_activity_description(text)
h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
end
def format_version_name(version)
if version.project == @project
h(version)
else
h("#{version.project} - #{version}")
end
def distance_of_date_in_words(from_date, to_date = 0)
from_date = from_date.to_date if from_date.respond_to?(:to_date)
to_date = to_date.to_date if to_date.respond_to?(:to_date)
distance_in_days = (to_date - from_date).abs
lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
end
def due_date_distance_in_words(date)
if date
l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
@@ -179,7 +138,7 @@ module ApplicationHelper
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
:title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
content << "</li>\n"
@@ -198,24 +157,15 @@ module ApplicationHelper
s
end
# Renders tabs and their content
def render_tabs(tabs)
if tabs.any?
render :partial => 'common/tabs', :locals => {:tabs => tabs}
else
content_tag 'p', l(:label_no_data), :class => "nodata"
end
end
# Renders the project quick-jump box
def render_project_jump_box
# Retrieve them now to avoid a COUNT query
projects = User.current.projects.all
if projects.any?
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
'<option value="" disabled="disabled">---</option>'
s << project_tree_options_for_select(projects, :selected => @project) do |p|
"<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
'<option disabled="disabled">---</option>'
s << project_tree_options_for_select(projects) do |p|
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
end
s << '</select>'
@@ -227,12 +177,7 @@ module ApplicationHelper
s = ''
project_tree(projects) do |project, level|
name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
tag_options = {:value => project.id}
if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
tag_options[:selected] = 'selected'
else
tag_options[:selected] = nil
end
tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
tag_options.merge!(yield(project)) if block_given?
s << content_tag('option', name_prefix + h(project), tag_options)
end
@@ -240,10 +185,15 @@ module ApplicationHelper
end
# Yields the given block for each project with its level in the tree
#
# Wrapper for Project#project_tree
def project_tree(projects, &block)
Project.project_tree(projects, &block)
ancestors = []
projects.sort_by(&:lft).each do |project|
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
end
yield project, ancestors.size
ancestors << project
end
end
def project_nested_ul(projects, &block)
@@ -269,28 +219,10 @@ module ApplicationHelper
end
s
end
def principals_check_box_tags(name, principals)
s = ''
principals.sort.each do |principal|
s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
end
s
end
# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
end
# Truncates at line break after 250 characters or options[:length]
def truncate_lines(string, options={})
length = options[:length] || 250
if string.to_s =~ /\A(.{#{length}}.*?)$/m
"#{$1}..."
else
string
end
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
end
def html_hours(text)
@@ -298,20 +230,30 @@ module ApplicationHelper
end
def authoring(created, author, options={})
l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
link_to(distance_of_time_in_words(Time.now, created),
{:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
:title => format_time(created))
author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
l(options[:label] || :label_added_time_by, author_tag, time_tag)
end
def time_tag(time)
text = distance_of_time_in_words(Time.now, time)
if @project
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
else
content_tag('acronym', text, :title => format_time(time))
end
def l_or_humanize(s, options={})
k = "#{options[:prefix]}#{s}".to_sym
l_has_string?(k) ? l(k) : s.to_s.humanize
end
def day_name(day)
l(:general_day_names).split(',')[day-1]
end
def month_name(month)
l(:actionview_datehelper_select_month_names).split(',')[month-1]
end
def syntax_highlight(name, content)
Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
type = CodeRay::FileType[name]
type ? CodeRay.scan(content, type).html : h(content)
end
def to_path_param(path)
@@ -320,7 +262,6 @@ module ApplicationHelper
def pagination_links_full(paginator, count=nil, options={})
page_param = options.delete(:page_param) || :page
per_page_links = options.delete(:per_page_links)
url_param = params.dup
# don't reuse query params if filters are present
url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
@@ -339,10 +280,10 @@ module ApplicationHelper
end
unless count.nil?
html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
if per_page_links != false && links = per_page_links(paginator.items_per_page)
html << " | #{links}"
end
html << [
" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
per_page_links(paginator.items_per_page)
].compact.join(' | ')
end
html
@@ -360,13 +301,6 @@ module ApplicationHelper
end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end
def reorder_links(name, url)
link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
end
def breadcrumb(*args)
elements = args.flatten
@@ -374,29 +308,9 @@ module ApplicationHelper
end
def other_formats_links(&block)
concat('<p class="other-formats">' + l(:label_export_to))
concat('<p class="other-formats">' + l(:label_export_to), block.binding)
yield Redmine::Views::OtherFormatsBuilder.new(self)
concat('</p>')
end
def page_header_title
if @project.nil? || @project.new_record?
h(Setting.app_title)
else
b = []
ancestors = (@project.root? ? [] : @project.ancestors.visible)
if ancestors.any?
root = ancestors.shift
b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
if ancestors.size > 2
b << '&#8230;'
ancestors = ancestors[-2, 2]
end
b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
end
b << h(@project)
b.join(' &#187; ')
end
concat('</p>', block.binding)
end
def html_title(*args)
@@ -405,26 +319,13 @@ module ApplicationHelper
title << @project.name if @project
title += @html_title if @html_title
title << Setting.app_title
title.select {|t| !t.blank? }.join(' - ')
title.compact.join(' - ')
else
@html_title ||= []
@html_title += args
end
end
# Returns the theme, controller name, and action as css classes for the
# HTML body.
def body_css_classes
css = []
if theme = Redmine::Themes.theme(Setting.ui_theme)
css << 'theme-' + theme.name
end
css << 'controller-' + params[:controller]
css << 'action-' + params[:action]
css.join(' ')
end
def accesskey(s)
Redmine::AccessKeys.key_for s
end
@@ -441,99 +342,66 @@ module ApplicationHelper
text = args.shift
when 2
obj = args.shift
attr = args.shift
text = obj.send(attr).to_s
text = obj.send(args.shift).to_s
else
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
only_path = options.delete(:only_path) == false ? false : true
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
@parsed_headings = []
text = parse_non_pre_blocks(text) do |text|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
send method_name, text, project, obj, attr, only_path, options
end
end
if @parsed_headings.any?
replace_toc(text, @parsed_headings)
end
text
end
def parse_non_pre_blocks(text)
s = StringScanner.new(text)
tags = []
parsed = ''
while !s.eos?
s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
if tags.empty?
yield text
end
parsed << text
if tag
if closing
if tags.last == tag.downcase
tags.pop
end
else
tags << tag.downcase
end
parsed << full_tag
end
end
# Close any non closing tags
while tag = tags.pop
parsed << "</#{tag}>"
end
parsed
end
def parse_inline_attachments(text, project, obj, attr, only_path, options)
# when using an image link, try to use an attachment, if possible
if options[:attachments] || (obj && obj.respond_to?(:attachments))
attachments = nil
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
filename, ext, alt, alttext = $1.downcase, $2, $3, $4
attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments
attachments = attachments.sort_by(&:created_on).reverse
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
style = $1
filename = $6.downcase
# search for the picture in attachments
if found = attachments.detect { |att| att.filename.downcase == filename }
image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
desc = found.description.to_s.gsub('"', '')
if !desc.blank? && alttext.blank?
alt = " title=\"#{desc}\" alt=\"#{desc}\""
end
"src=\"#{image_url}\"#{alt}"
desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
alt = desc.blank? ? nil : "(#{desc})"
"!#{style}#{image_url}#{alt}!"
else
m
end
end
end
end
# Wiki links
#
# Examples:
# [[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]]
def parse_wiki_links(text, project, obj, attr, only_path, options)
text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
# different methods for formatting wiki links
case options[:wiki_links]
when :local
# used for local links to html files
format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
when :anchor
# used for single-file wiki export
format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
else
format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
end
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
# Wiki links
#
# Examples:
# [[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(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
link_project = project
esc, all, page, title = $1, $2, $3, $5
if esc.nil?
if page =~ /^([^\:]+)\:(.*)$/
link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
page = $2
title ||= $1 if page.blank?
end
@@ -546,14 +414,8 @@ module ApplicationHelper
end
# check if page exists
wiki_page = link_project.wiki.find_page(page)
url = case options[:wiki_links]
when :local; "#{title}.html"
when :anchor; "##{title}" # used for single-file wiki export
else
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
end
link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
:class => ('wiki-page' + (wiki_page ? '' : ' new')))
else
# project or wiki doesn't exist
all
@@ -562,53 +424,52 @@ module ApplicationHelper
all
end
end
end
# Redmine links
#
# Examples:
# Issues:
# #52 -> Link to issue #52
# Changesets:
# r52 -> Link to revision 52
# commit:a85130f -> Link to scmid starting with a85130f
# Documents:
# document#17 -> Link to document with id 17
# document:Greetings -> Link to the document with title "Greetings"
# document:"Some document" -> Link to the document with title "Some document"
# Versions:
# version#3 -> Link to version with id 3
# version:1.0.0 -> Link to version named "1.0.0"
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
# Attachments:
# attachment:file.zip -> Link to the attachment of the current object named file.zip
# Source files:
# source:some/file -> Link to the file located at /some/file in the project's repository
# source:some/file@52 -> Link to the file's revision 52
# source:some/file#L120 -> Link to line 120 of the file
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file
# Forum messages:
# message#1218 -> Link to message with id 1218
def parse_redmine_links(text, project, obj, attr, only_path, options)
text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
# Redmine links
#
# Examples:
# Issues:
# #52 -> Link to issue #52
# Changesets:
# r52 -> Link to revision 52
# commit:a85130f -> Link to scmid starting with a85130f
# Documents:
# document#17 -> Link to document with id 17
# document:Greetings -> Link to the document with title "Greetings"
# document:"Some document" -> Link to the document with title "Some document"
# Versions:
# version#3 -> Link to version with id 3
# version:1.0.0 -> Link to version named "1.0.0"
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
# Attachments:
# attachment:file.zip -> Link to the attachment of the current object named file.zip
# Source files:
# source:some/file -> Link to the file located at /some/file in the project's repository
# source:some/file@52 -> Link to the file's revision 52
# source:some/file#L120 -> Link to line 120 of the file
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file
# Forum messages:
# message#1218 -> Link to message with id 1218
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil
if esc.nil?
if prefix.nil? && sep == 'r'
if project && (changeset = project.changesets.find_by_revision(identifier))
link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
if project && (changeset = project.changesets.find_by_revision(oid))
link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100))
:title => truncate_single_line(changeset.comments, 100))
end
elsif sep == '#'
oid = identifier.to_i
oid = oid.to_i
case prefix
when nil
if issue = Issue.visible.find_by_id(oid, :include => :status)
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
:class => issue.css_classes,
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
:class => (issue.closed? ? 'issue closed' : 'issue'),
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
link = content_tag('del', link) if issue.closed?
end
when 'document'
if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
@@ -622,7 +483,7 @@ module ApplicationHelper
end
when 'message'
if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
:controller => 'messages',
:action => 'show',
:board_id => message.board,
@@ -630,14 +491,10 @@ module ApplicationHelper
:anchor => (message.parent ? "message-#{message.id}" : nil)},
:class => 'message'
end
when 'project'
if p = Project.visible.find_by_id(oid)
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
end
end
elsif sep == ':'
# removes the double quotes if any
name = identifier.gsub(%r{^"(.*)"$}, "\\1")
name = oid.gsub(%r{^"(.*)"$}, "\\1")
case prefix
when 'document'
if project && document = project.documents.find_by_title(name)
@@ -653,7 +510,7 @@ module ApplicationHelper
if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100)
:title => truncate_single_line(changeset.comments, 100)
end
when 'source', 'export'
if project && project.repository
@@ -667,69 +524,17 @@ module ApplicationHelper
:class => (prefix == 'export' ? 'source download' : 'source')
end
when 'attachment'
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments && attachment = attachments.detect {|a| a.filename == name }
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
:class => 'attachment'
end
when 'project'
if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
end
end
end
end
leading + (link || "#{prefix}#{sep}#{identifier}")
end
end
HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
# Headings and TOC
# Adds ids and links to headings unless options[:headings] is set to false
def parse_headings(text, project, obj, attr, only_path, options)
return if options[:headings] == false
text.gsub!(HEADING_RE) do
level, attrs, content = $1.to_i, $2, $3
item = strip_tags(content).strip
anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
@parsed_headings << [level, anchor, item]
"<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
end
end
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
# Renders the TOC with given headings
def replace_toc(text, headings)
text.gsub!(TOC_RE) do
if headings.empty?
''
else
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<ul class=\"#{div_class}\"><li>"
root = headings.map(&:first).min
current = root
started = false
headings.each do |level, anchor, item|
if level > current
out << '<ul><li>' * (level - current)
elsif level < current
out << "</li></ul>\n" * (current - level) + "</li><li>"
elsif started
out << '</li><li>'
end
out << "<a href=\"##{anchor}\">#{item}</a>"
current = level
started = true
end
out << '</li></ul>' * (current - root)
out << '</li></ul>'
end
leading + (link || "#{prefix}#{sep}#{oid}")
end
text
end
# Same as Rails' simple_format helper without using paragraphs
@@ -740,9 +545,46 @@ module ApplicationHelper
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
end
def error_messages_for(object_name, options = {})
options = options.symbolize_keys
object = instance_variable_get("@#{object_name}")
if object && !object.errors.empty?
# build full_messages here with controller current language
full_messages = []
object.errors.each do |attr, msg|
next if msg.nil?
msg = msg.first if msg.is_a? Array
if attr == "base"
full_messages << l(msg)
else
full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
end
end
# retrieve custom values error messages
if object.errors[:custom_values]
object.custom_values.each do |v|
v.errors.each do |attr, msg|
next if msg.nil?
msg = msg.first if msg.is_a? Array
full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
end
end
end
content_tag("div",
content_tag(
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"
)
else
""
end
end
def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) +
valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
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 = {})
@@ -770,41 +612,18 @@ module ApplicationHelper
def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts = pcts.collect(&:round)
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', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
), :class => 'progress', :style => "width: #{width};") +
content_tag('p', legend, :class => 'pourcent')
end
def checked_image(checked=true)
if checked
image_tag 'toggle_check.png'
end
end
def context_menu(url)
unless @context_menu_included
content_for :header_tags do
javascript_include_tag('context_menu') +
stylesheet_link_tag('context_menu')
end
if l(:direction) == 'rtl'
content_for :header_tags do
stylesheet_link_tag('context_menu_rtl')
end
end
@context_menu_included = true
end
javascript_tag "new ContextMenu('#{ url_for(url) }')"
end
def context_menu_link(name, url, options={})
options[:class] ||= ''
@@ -832,18 +651,8 @@ module ApplicationHelper
unless @calendar_headers_tags_included
@calendar_headers_tags_included = true
content_for :header_tags do
start_of_week = case Setting.start_of_week.to_i
when 1
'Calendar._FD = 1;' # Monday
when 7
'Calendar._FD = 0;' # Sunday
else
'' # use language
end
javascript_include_tag('calendar/calendar') +
javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
javascript_tag(start_of_week) +
javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
javascript_include_tag('calendar/calendar-setup') +
stylesheet_link_tag('calendar')
end
@@ -864,7 +673,6 @@ module ApplicationHelper
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { })
if Setting.gravatar_enabled?
options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
email = nil
if user.respond_to?(:mail)
email = user.mail
@@ -872,37 +680,9 @@ module ApplicationHelper
email = $1
end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
else
''
end
end
def favicon
"<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
end
# Returns true if arg is expected in the API response
def include_in_api_response?(arg)
unless @included_in_api_response
param = params[:include]
@included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
@included_in_api_response.collect!(&:strip)
end
@included_in_api_response.include?(arg.to_s)
end
# Returns options or nil if nometa param or X-Redmine-Nometa header
# was set in the request
def api_meta(options)
if params[:nometa].present? || request.headers['X-Redmine-Nometa']
# compatibility mode for activeresource clients that raise
# an error when unserializing an array with attributes
nil
else
options
end
end
private
def wiki_helper

View File

@@ -1,45 +0,0 @@
module CalendarsHelper
def link_to_previous_month(year, month, options={})
target_year, target_month = if month == 1
[year - 1, 12]
else
[year, month - 1]
end
name = if target_month == 12
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
link_to_month(('&#171; ' + name), target_year, target_month, options)
end
def link_to_next_month(year, month, options={})
target_year, target_month = if month == 12
[year + 1, 1]
else
[year, month + 1]
end
name = if target_month == 1
"#{month_name(target_month)} #{target_year}"
else
"#{month_name(target_month)}"
end
link_to_month((name + ' &#187;'), target_year, target_month, options)
end
def link_to_month(link_name, year, month, options={})
project_id = options[:project].present? ? options[:project].to_param : nil
link_target = calendar_path(:year => year, :month => month, :project_id => project_id)
link_to_remote(link_name,
{:update => "content", :url => link_target, :method => :put},
{:href => link_target})
end
end

View File

@@ -18,15 +18,10 @@
module CustomFieldsHelper
def custom_fields_tabs
tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
{:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
{:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
{:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
{:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
{:name => 'TimeEntryCustomField', :label => :label_spent_time},
{:name => 'ProjectCustomField', :label => :label_project_plural},
{:name => 'UserCustomField', :label => :label_user_plural}
]
end
@@ -35,16 +30,15 @@ module CustomFieldsHelper
custom_field = custom_value.custom_field
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}"
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
case field_format.edit_as
case custom_field.field_format
when "date"
text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
calendar_for(field_id)
when "text"
text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
when "bool"
hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0')
when "list"
blank_option = custom_field.is_required? ?
(custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
@@ -67,27 +61,6 @@ module CustomFieldsHelper
def custom_field_tag_with_label(name, custom_value)
custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
end
def custom_field_tag_for_bulk_edit(name, custom_field)
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}"
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
case field_format.edit_as
when "date"
text_field_tag(field_name, '', :id => field_id, :size => 10) +
calendar_for(field_id)
when "text"
text_area_tag(field_name, '', :id => field_id, :rows => 3, :style => 'width:90%')
when "bool"
select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
[l(:general_text_yes), '1'],
[l(:general_text_no), '0']]), :id => field_id)
when "list"
select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values), :id => field_id)
else
text_field_tag(field_name, '', :id => field_id)
end
end
# Return a string used to display a custom value
def show_value(custom_value)
@@ -97,22 +70,19 @@ module CustomFieldsHelper
# Return a string used to display a custom value
def format_value(value, field_format)
Redmine::CustomFieldFormat.format_value(value, field_format) # Proxy
return "" unless value && !value.empty?
case field_format
when "date"
begin; format_date(value.to_date); rescue; value end
when "bool"
l_YesNo(value == "1")
else
value
end
end
# Return an array of custom field formats which can be used in select_tag
def custom_field_formats_for_select
Redmine::CustomFieldFormat.as_select
end
# Renders the custom_values in api views
def render_api_custom_values(custom_values, api)
api.array :custom_fields do
custom_values.each do |custom_value|
api.custom_field :id => custom_value.custom_field_id, :name => custom_value.custom_field.name do
api.value custom_value.value
end
end
end unless custom_values.empty?
CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] }
end
end

View File

@@ -1,49 +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.
module GanttHelper
def gantt_zoom_link(gantt, in_or_out)
case in_or_out
when :in
if gantt.zoom < 4
link_to_remote(l(:text_zoom_in),
{:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :method => :get, :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom+1))),
:class => 'icon icon-zoom-in'})
else
content_tag('span', l(:text_zoom_in), :class => 'icon icon-zoom-in')
end
when :out
if gantt.zoom > 1
link_to_remote(l(:text_zoom_out),
{:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :method => :get, :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom-1))),
:class => 'icon icon-zoom-out'})
else
content_tag('span', l(:text_zoom_out), :class => 'icon icon-zoom-out')
end
end
end
def number_of_issues_on_versions(gantt)
versions = gantt.events.collect {|event| (event.is_a? Version) ? event : nil}.compact
versions.sum {|v| v.fixed_issues.for_gantt.with_query(@query).count}
end
end

View File

@@ -1,34 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 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 GroupsHelper
# Options for the new membership projects combo-box
def options_for_membership_project_select(user, projects)
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
options << project_tree_options_for_select(projects) do |p|
{:disabled => (user.projects.include?(p))}
end
options
end
def group_settings_tabs
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
{:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
]
end
end

View File

@@ -1,2 +0,0 @@
module IssueMovesHelper
end

View File

@@ -15,87 +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 'csv'
module IssuesHelper
include ApplicationHelper
def issue_list(issues, &block)
ancestors = []
issues.each do |issue|
while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
ancestors.pop
end
yield issue, ancestors.size
ancestors << issue unless issue.leaf?
end
end
# Renders a HTML/CSS tooltip
#
# To use, a trigger div is needed. This is a div with the class of "tooltip"
# that contains this method wrapped in a span with the class of "tip"
#
# <div class="tooltip"><%= link_to_issue(issue) %>
# <span class="tip"><%= render_issue_tooltip(issue) %></span>
# </div>
#
def render_issue_tooltip(issue)
@cached_label_status ||= l(:field_status)
@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)
@cached_label_project ||= l(:field_project)
link_to_issue(issue) + "<br /><br />" +
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
"<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
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 render_issue_subject_with_tree(issue)
s = ''
issue.ancestors.each do |ancestor|
s << '<div>' + content_tag('p', link_to_issue(ancestor))
end
s << '<div>' + content_tag('h3', h(issue.subject))
s << '</div>' * (issue.ancestors.size + 1)
s
end
def render_descendants_tree(issue)
s = '<form><table class="list issues">'
issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
s << content_tag('tr',
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
content_tag('td', h(child.status)) +
content_tag('td', link_to_user(child.assigned_to)) +
content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
:class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
end
s << '</form></table>'
s
end
def render_custom_fields_rows(issue)
return if issue.custom_field_values.empty?
ordered_values = []
half = (issue.custom_field_values.size / 2.0).ceil
half.times do |i|
ordered_values << issue.custom_field_values[i]
ordered_values << issue.custom_field_values[i + half]
end
s = "<tr>\n"
n = 0
ordered_values.compact.each do |value|
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
n += 1
end
s << "</tr>\n"
# Returns a string of css classes that apply to the given issue
def css_issue_classes(issue)
s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
s << ' closed' if issue.closed?
s << ' overdue' if issue.overdue?
s
end
@@ -106,7 +48,6 @@ module IssuesHelper
# Project specific queries and global queries
visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
@sidebar_queries = Query.find(:all,
:select => 'id, name',
:order => "name ASC",
:conditions => visible.conditions)
end
@@ -116,25 +57,35 @@ module IssuesHelper
def show_detail(detail, no_html=false)
case detail.property
when 'attr'
field = detail.prop_key.to_s.gsub(/\_id$/, "")
label = l(("field_" + field).to_sym)
case
when ['due_date', 'start_date'].include?(detail.prop_key)
label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
case detail.prop_key
when 'due_date', 'start_date'
value = format_date(detail.value.to_date) if detail.value
old_value = format_date(detail.old_value.to_date) if detail.old_value
when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
value = find_name_by_reflection(field, detail.value)
old_value = find_name_by_reflection(field, detail.old_value)
when detail.prop_key == 'estimated_hours'
when 'project_id'
p = Project.find_by_id(detail.value) and value = p.name if detail.value
p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
when 'status_id'
s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
when 'tracker_id'
t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
when 'assigned_to_id'
u = User.find_by_id(detail.value) and value = u.name if detail.value
u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
when 'priority_id'
e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
when 'category_id'
c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
when 'fixed_version_id'
v = Version.find_by_id(detail.value) and value = v.name if detail.value
v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
when 'estimated_hours'
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
when detail.prop_key == 'parent_id'
label = l(:field_parent_issue)
value = "##{detail.value}" unless detail.value.blank?
old_value = "##{detail.old_value}" unless detail.old_value.blank?
end
when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key)
@@ -168,37 +119,19 @@ module IssuesHelper
case detail.property
when 'attr', 'cf'
if !detail.old_value.blank?
l(:text_journal_changed, :label => label, :old => old_value, :new => value)
label + " " + l(:text_journal_changed, old_value, value)
else
l(:text_journal_set_to, :label => label, :value => value)
label + " " + l(:text_journal_set_to, value)
end
when 'attachment'
l(:text_journal_added, :label => label, :value => value)
"#{label} #{value} #{l(:label_added)}"
end
else
l(:text_journal_deleted, :label => label, :old => old_value)
end
end
# Find the name of an associated record stored in the field attribute
def find_name_by_reflection(field, id)
association = Issue.reflect_on_association(field.to_sym)
if association
record = association.class_name.constantize.find_by_id(id)
return record.name if record
end
end
# Renders issue children recursively
def render_api_issue_children(issue, api)
return if issue.leaf?
api.array :children do
issue.children.each do |child|
api.issue(:id => child.id) do
api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
api.subject child.subject
render_api_issue_children(child, api)
end
case detail.property
when 'attr', 'cf'
label + " " + l(:text_journal_deleted) + " (#{old_value})"
when 'attachment'
"#{label} #{old_value} #{l(:label_deleted)}"
end
end
end
@@ -206,7 +139,8 @@ module IssuesHelper
def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#",
l(:field_status),
@@ -222,7 +156,6 @@ module IssuesHelper
l(:field_due_date),
l(:field_done_ratio),
l(:field_estimated_hours),
l(:field_parent_issue),
l(:field_created_on),
l(:field_updated_on)
]
@@ -249,7 +182,6 @@ module IssuesHelper
format_date(issue.due_date),
issue.done_ratio,
issue.estimated_hours.to_s.gsub('.', decimal_separator),
issue.parent_id,
format_time(issue.created_on),
format_time(issue.updated_on)
]
@@ -258,6 +190,7 @@ module IssuesHelper
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
export
end
end

View File

@@ -16,13 +16,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module JournalsHelper
def render_notes(issue, journal, options={})
def render_notes(journal, options={})
content = ''
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
editable = journal.editable_by?(User.current)
links = []
if !journal.notes.blank?
links << link_to_remote(image_tag('comment.png'),
{ :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} },
{ :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} },
:title => l(:button_quote)) if options[:reply_links]
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal },
@@ -30,9 +30,7 @@ module JournalsHelper
end
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)
css_classes = "wiki"
css_classes << " editable" if editable
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki'))
end
def link_to_in_place_notes_editor(text, field_id, url, options={})

View File

@@ -19,11 +19,10 @@ module MessagesHelper
def link_to_message(message)
return '' unless message
link_to h(truncate(message.subject, :length => 60)), :controller => 'messages',
link_to h(truncate(message.subject, 60)), :controller => 'messages',
:action => 'show',
:board_id => message.board_id,
:id => message.root,
:r => (message.parent_id && message.id),
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
end
end

View File

@@ -18,7 +18,7 @@
module ProjectsHelper
def link_to_version(version, options = {})
return '' unless version && version.is_a?(Version)
link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end
def project_settings_tabs
@@ -29,24 +29,14 @@ module ProjectsHelper
{: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},
{:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
def parent_project_select_tag(project)
selected = project.parent
# retrieve the requested parent project
parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
if parent_id
selected = (parent_id.blank? ? nil : Project.find(parent_id))
end
options = ''
options << "<option value=''></option>" if project.allowed_parents.include?(nil)
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
content_tag('select', options, :name => 'project[parent_id]', :id => 'project_parent_id')
options = '<option></option>' + project_tree_options_for_select(project.possible_parents, :selected => project.parent)
content_tag('select', options, :name => 'project[parent_id]')
end
# Renders a tree of projects as a nested set of unordered lists
@@ -56,10 +46,7 @@ module ProjectsHelper
s = ''
if projects.any?
ancestors = []
original_project = @project
projects.each do |project|
# set the project environment to please macros.
@project = project
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
else
@@ -72,37 +59,13 @@ module ProjectsHelper
end
classes = (ancestors.empty? ? 'root' : 'child')
s << "<li class='#{classes}'><div class='#{classes}'>" +
link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
s << "</div>\n"
ancestors << project
end
s << ("</li></ul>\n" * ancestors.size)
@project = original_project
end
s
end
# Returns a set of options for a select field, grouped by project.
def version_options_for_select(versions, selected=nil)
grouped = Hash.new {|h,k| h[k] = []}
versions.each do |version|
grouped[version.project.name] << [version.name, version.id]
end
# Add in the selected
if selected && !versions.include?(selected)
grouped[selected.project.name] << [selected.name, selected.id]
end
if grouped.keys.size > 1
grouped_options_for_select(grouped, selected && selected.id)
else
options_for_select((grouped.values.first || []), selected && selected.id)
end
end
def format_version_sharing(sharing)
sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
l("label_version_sharing_#{sharing}")
end
end

View File

@@ -28,71 +28,27 @@ module QueriesHelper
end
def column_content(column, issue)
value = column.value(issue)
case value.class.name
when 'String'
if column.name == :subject
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
else
h(value)
end
when 'Time'
format_time(value)
when 'Date'
format_date(value)
when 'Fixnum', 'Float'
if column.name == :done_ratio
progress_bar(value, :width => '80px')
else
value.to_s
end
when 'User'
link_to_user value
when 'Project'
link_to_project value
when 'Version'
link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
when 'TrueClass'
l(:general_text_Yes)
when 'FalseClass'
l(:general_text_No)
when 'Issue'
link_to_issue(value, :subject => false)
if column.is_a?(QueryCustomFieldColumn)
cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv)
else
h(value)
end
end
# Retrieve query from session or build a new query
def retrieve_query
if !params[:query_id].blank?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = Query.find(params[:query_id], :conditions => cond)
@query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
sort_clear
else
if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields]
@query.filters = {}
@query.add_filters(params[:fields], params[:operators], params[:values])
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')
when :fixed_version
link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
h(value)
end
@query.group_by = params[:group_by]
@query.column_names = params[:query] && params[:query][:column_names]
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
@query.project = @project
end
end
end

View File

@@ -18,12 +18,8 @@
require 'iconv'
module RepositoriesHelper
def format_revision(revision)
if revision.respond_to? :format_identifier
revision.format_identifier
else
revision.to_s
end
def format_revision(txt)
txt.to_s[0,8]
end
def truncate_at_line_break(text, length = 255)
@@ -56,19 +52,17 @@ module RepositoriesHelper
else
change
end
end.compact
end.compact
tree = { }
changes.each do |change|
p = tree
dirs = change.path.to_s.split('/').select {|d| !d.blank?}
path = ''
dirs.each do |dir|
path += '/' + dir
p[:s] ||= {}
p = p[:s]
p[path] ||= {}
p = p[path]
p[dir] ||= {}
p = p[dir]
end
p[:c] = change
end
@@ -82,35 +76,31 @@ module RepositoriesHelper
output = ''
output << '<ul>'
tree.keys.sort.each do |file|
s = !tree[file][:s].nil?
c = tree[file][:c]
style = 'change'
text = File.basename(h(file))
if s = tree[file][:s]
style << ' folder'
path_param = to_path_param(@repository.relative_path(file))
text = link_to(text, :controller => 'repositories',
:action => 'show',
:id => @project,
:path => path_param,
:rev => @changeset.identifier)
output << "<li class='#{style}'>#{text}</li>"
output << render_changes_tree(s)
elsif c = tree[file][:c]
style << " change-#{c.action}"
style << ' folder' if s
style << " change-#{c.action}" if c
text = h(file)
unless c.nil?
path_param = to_path_param(@repository.relative_path(c.path))
text = link_to(text, :controller => 'repositories',
:action => 'entry',
:id => @project,
:path => path_param,
:rev => @changeset.identifier) unless c.action == 'D'
:rev => @changeset.revision) unless s || c.action == 'D'
text << " - #{c.revision}" unless c.revision.blank?
text << ' (' + link_to('diff', :controller => 'repositories',
:action => 'diff',
:id => @project,
:path => path_param,
:rev => @changeset.identifier) + ') ' if c.action == 'M'
:rev => @changeset.revision) + ') ' if c.action == 'M'
text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
output << "<li class='#{style}'>#{text}</li>"
end
output << "<li class='#{style}'>#{text}</li>"
output << render_changes_tree(tree[file][:s]) if s
end
output << '</ul>'
output
@@ -131,12 +121,12 @@ module RepositoriesHelper
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) && method != 'repository_field_tags'
send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
end
def scm_select_tag(repository)
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
Redmine::Scm::Base.all.each do |scm|
REDMINE_SUPPORTED_SCM.each do |scm|
scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
end
@@ -157,12 +147,13 @@ module RepositoriesHelper
def subversion_field_tags(form, repository)
content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
'<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]';"))
:onchange => "this.name='repository[password]';")) +
content_tag('p', form.check_box(:cache))
end
def darcs_field_tags(form, repository)

View File

@@ -27,9 +27,8 @@ module SearchHelper
result << '...'
break
end
words = words.mb_chars
if i.even?
result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
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}")
@@ -39,7 +38,7 @@ module SearchHelper
end
def type_label(t)
l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
l("label_#{t.singularize}_plural")
end
def project_select_tag

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# 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
@@ -22,63 +22,9 @@ module SettingsHelper
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
{:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)},
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
]
end
def setting_select(setting, choices, options={})
if blank_text = options.delete(:blank)
choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
end
setting_label(setting, options) +
select_tag("settings[#{setting}]", options_for_select(choices, Setting.send(setting).to_s), options)
end
def setting_multiselect(setting, choices, options={})
setting_values = Setting.send(setting)
setting_values = [] unless setting_values.is_a?(Array)
setting_label(setting, options) +
hidden_field_tag("settings[#{setting}][]", '') +
choices.collect do |choice|
text, value = (choice.is_a?(Array) ? choice : [choice, choice])
content_tag('label',
check_box_tag("settings[#{setting}][]", value, Setting.send(setting).include?(value)) + text.to_s,
:class => 'block'
)
end.join
end
def setting_text_field(setting, options={})
setting_label(setting, options) +
text_field_tag("settings[#{setting}]", Setting.send(setting), options)
end
def setting_text_area(setting, options={})
setting_label(setting, options) +
text_area_tag("settings[#{setting}]", Setting.send(setting), options)
end
def setting_check_box(setting, options={})
setting_label(setting, options) +
hidden_field_tag("settings[#{setting}]", 0) +
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options)
end
def setting_label(setting, options={})
label = options.delete(:label)
label != false ? content_tag("label", l(label || "setting_#{setting}")) : ''
end
# Renders a notification field for a Redmine::Notifiable option
def notification_field(notifiable)
return content_tag(:label,
check_box_tag('settings[notified_events][]',
notifiable.name,
Setting.notified_events.include?(notifiable.name)) +
l_or_humanize(notifiable.name, :prefix => 'label_'),
:class => notifiable.parent.present? ? "parent" : '')
end
end

View File

@@ -1,12 +1,11 @@
# Helpers to sort tables using clickable column headers.
#
# Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
# Jean-Philippe Lang, 2009
# License: This source code is released under the MIT license.
#
# - Consecutive clicks toggle the column's sort order.
# - Sort state is maintained by a session hash entry.
# - CSS classes identify sort column and state.
# - Icon image identifies sort column and state.
# - Typically used in conjunction with the Pagination module.
#
# Example code snippets:
@@ -18,7 +17,7 @@
#
# def list
# sort_init 'last_name'
# sort_update %w(first_name last_name)
# sort_update
# @items = Contact.find_all nil, sort_clause
# end
#
@@ -29,7 +28,7 @@
#
# def list
# sort_init 'last_name'
# sort_update %w(first_name last_name)
# sort_update
# @contact_pages, @items = paginate :contacts,
# :order_by => sort_clause,
# :per_page => 10
@@ -46,170 +45,88 @@
# </tr>
# </thead>
#
# - Introduces instance variables: @sort_default, @sort_criteria
# - Introduces param :sort
# - The ascending and descending sort icon images are sort_asc.png and
# sort_desc.png and reside in the application's images directory.
# - Introduces instance variables: @sort_name, @sort_default.
# - Introduces params :sort_key and :sort_order.
#
module SortHelper
class SortCriteria
def initialize
@criteria = []
end
def available_criteria=(criteria)
unless criteria.is_a?(Hash)
criteria = criteria.inject({}) {|h,k| h[k] = k; h}
end
@available_criteria = criteria
end
def from_param(param)
@criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
normalize!
end
def criteria=(arg)
@criteria = arg
normalize!
end
def to_param
@criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
end
def to_sql
sql = @criteria.collect do |k,o|
if s = @available_criteria[k]
(o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ')
end
end.compact.join(', ')
sql.blank? ? nil : sql
end
def add!(key, asc)
@criteria.delete_if {|k,o| k == key}
@criteria = [[key, asc]] + @criteria
normalize!
end
def add(*args)
r = self.class.new.from_param(to_param)
r.add!(*args)
r
end
def first_key
@criteria.first && @criteria.first.first
end
def first_asc?
@criteria.first && @criteria.first.last
end
def empty?
@criteria.empty?
end
private
def normalize!
@criteria ||= []
@criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
@criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
@criteria.slice!(3)
self
end
# Appends DESC to the sort criterion unless it has a fixed order
def append_desc(criterion)
if criterion =~ / (asc|desc)$/i
criterion
else
"#{criterion} DESC"
end
end
end
def sort_name
controller_name + '_' + action_name + '_sort'
end
# Initializes the default sort.
# Examples:
#
# sort_init 'name'
# sort_init 'id', 'desc'
# sort_init ['name', ['id', 'desc']]
# sort_init [['name', 'desc'], ['id', 'desc']]
# Initializes the default sort column (default_key) and sort order
# (default_order).
#
def sort_init(*args)
case args.size
when 1
@sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
when 2
@sort_default = [[args.first, args.last]]
else
raise ArgumentError
end
# - default_key is a column attribute name.
# - default_order is 'asc' or 'desc'.
# - name is the name of the session hash entry that stores the sort state,
# defaults to '<controller_name>_sort'.
#
def sort_init(default_key, default_order='asc', name=nil)
@sort_name = name || params[:controller] + params[:action] + '_sort'
@sort_default = {:key => default_key, :order => default_order}
end
# Updates the sort state. Call this in the controller prior to calling
# sort_clause.
# - criteria can be either an array or a hash of allowed keys
#
def sort_update(criteria)
@sort_criteria = SortCriteria.new
@sort_criteria.available_criteria = criteria
@sort_criteria.from_param(params[:sort] || session[sort_name])
@sort_criteria.criteria = @sort_default if @sort_criteria.empty?
session[sort_name] = @sort_criteria.to_param
end
# Clears the sort criteria session data
#
def sort_clear
session[sort_name] = nil
# sort_keys can be either an array or a hash of allowed keys
def sort_update(sort_keys)
sort_key = params[:sort_key]
sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key])
sort_order = (params[:sort_order] == 'desc' ? 'DESC' : 'ASC')
if sort_key
sort = {:key => sort_key, :order => sort_order}
elsif session[@sort_name]
sort = session[@sort_name] # Previous sort.
else
sort = @sort_default
end
session[@sort_name] = sort
sort_column = (sort_keys.is_a?(Hash) ? sort_keys[sort[:key]] : sort[:key])
@sort_clause = (sort_column.blank? ? nil : [sort_column].flatten.collect {|s| "#{s} #{sort[:order]}"}.join(','))
end
# Returns an SQL sort clause corresponding to the current sort state.
# Use this to sort the controller's table items collection.
#
def sort_clause()
@sort_criteria.to_sql
@sort_clause
end
# Returns a link which sorts by the named column.
#
# - column is the name of an attribute in the sorted record collection.
# - the optional caption explicitly specifies the displayed link text.
# - 2 CSS classes reflect the state of the link: sort and asc or desc
# - The optional caption explicitly specifies the displayed link text.
# - A sort icon image is positioned to the right of the sort link.
#
def sort_link(column, caption, default_order)
css, order = nil, default_order
if column.to_s == @sort_criteria.first_key
if @sort_criteria.first_asc?
css = 'sort asc'
key, order = session[@sort_name][:key], session[@sort_name][:order]
if key == column
if order.downcase == 'asc'
icon = 'sort_asc.png'
order = 'desc'
else
css = 'sort desc'
icon = 'sort_desc.png'
order = 'asc'
end
else
icon = nil
order = default_order
end
caption = column.to_s.humanize unless caption
caption = titleize(Inflector::humanize(column)) unless caption
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
sort_options = { :sort_key => column, :sort_order => order }
# don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
# Add project_id to url_options
# Add project_id to url_options
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
link_to_remote(caption,
{:update => "content", :url => url_options, :method => :get},
{:href => url_for(url_options),
:class => css})
{:href => url_for(url_options)}) +
(icon ? nbsp(2) + image_tag(icon) : '')
end
# Returns a table header <th> tag with a sort link for the named column
@@ -225,11 +142,30 @@ module SortHelper
#
# <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
#
# Renders:
#
# <th title="Sort by contact ID" width="40">
# <a href="/contact/list?sort_order=desc&amp;sort_key=id">Id</a>
# &nbsp;&nbsp;<img alt="Sort_asc" src="/images/sort_asc.png" />
# </th>
#
def sort_header_tag(column, options = {})
caption = options.delete(:caption) || column.to_s.humanize
caption = options.delete(:caption) || titleize(Inflector::humanize(column))
default_order = options.delete(:default_order) || 'asc'
options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
content_tag('th', sort_link(column, caption, default_order), options)
end
end
private
# Return n non-breaking spaces.
def nbsp(n)
'&nbsp;' * n
end
# Return capitalized title.
def titleize(title)
title.split.map {|w| w.capitalize }.join(' ')
end
end

View File

@@ -22,43 +22,20 @@ module TimelogHelper
links = []
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
if @issue
if @issue.visible?
links << link_to_issue(@issue, :subject => false)
else
links << "##{@issue.id}"
end
end
links << link_to_issue(@issue) if @issue
breadcrumb links
end
# Returns a collection of activities for a select field. time_entry
# is optional and will be used to check if the selected TimeEntryActivity
# is active.
def activity_collection_for_select_options(time_entry=nil, project=nil)
project ||= @project
if project.nil?
activities = TimeEntryActivity.shared.active
else
activities = project.activities
end
def activity_collection_for_select_options
activities = Enumeration::get_values('ACTI')
collection = []
if time_entry && time_entry.activity && !time_entry.activity.active?
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
else
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
end
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
activities.each { |a| collection << [a.name, a.id] }
collection
end
def select_hours(data, criteria, value)
if value.to_s.empty?
data.select {|row| row[criteria].blank? }
else
data.select {|row| row[criteria].to_s == value.to_s}
end
data.select {|row| row[criteria] == value}
end
def sum_hours(data)
@@ -87,7 +64,8 @@ module TimelogHelper
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator)
custom_fields = TimeEntryCustomField.find(:all)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [l(:field_spent_on),
l(:field_user),
@@ -120,26 +98,17 @@ module TimelogHelper
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
export
end
def format_criteria_value(criteria, value)
if value.blank?
l(:label_none)
elsif k = @available_criterias[criteria][:klass]
obj = k.find_by_id(value.to_i)
if obj.is_a?(Issue)
obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
else
obj
end
else
format_value(value, @available_criterias[criteria][:format])
end
value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
end
def report_to_csv(criterias, periods, hours)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# Column headers
headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
headers += periods
@@ -158,6 +127,7 @@ module TimelogHelper
row << "%.2f" %total
csv << row
end
export.rewind
export
end

View File

@@ -33,19 +33,15 @@ module UsersHelper
options
end
def user_mail_notification_options(user)
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
end
def change_status_link(user)
url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]}
if user.locked?
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user.registered?
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user != User.current
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
end
end
@@ -53,9 +49,5 @@ module UsersHelper
tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
{:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
]
if Group.all.any?
tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
end
tabs
end
end

View File

@@ -16,28 +16,17 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module WatchersHelper
# Valid options
# * :id - the element id
# * :replace - a string or array of element ids that will be
# replaced
def watcher_tag(object, user, options={:replace => 'watcher'})
id = options[:id]
id ||= options[:replace] if options[:replace].is_a? String
content_tag("span", watcher_link(object, user, options), :id => id)
def watcher_tag(object, user)
content_tag("span", watcher_link(object, user), :id => 'watcher')
end
# Valid options
# * :replace - a string or array of element ids that will be
# replaced
def watcher_link(object, user, options={:replace => 'watcher'})
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 ? 'unwatch' : 'watch'),
:object_type => object.class.to_s.underscore,
:object_id => object.id,
:replace => options[:replace]}
:object_id => object.id}
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
{:url => url},
:href => url_for(url),
@@ -47,23 +36,6 @@ module WatchersHelper
# Returns a comma separated list of users watching the given object
def watchers_list(object)
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
lis = object.watcher_users.collect do |user|
s = avatar(user, :size => "16").to_s + link_to_user(user, :class => 'user').to_s
if remove_allowed
url = {:controller => 'watchers',
:action => 'destroy',
:object_type => object.class.to_s.underscore,
:object_id => object.id,
:user_id => user}
s += ' ' + link_to_remote(image_tag('delete.png'),
{:url => url},
:href => url_for(url),
:style => "vertical-align: middle",
:class => "delete")
end
"<li>#{ s }</li>"
end
lis.empty? ? "" : "<ul>#{ lis.join("\n") }</ul>"
object.watcher_users.collect {|u| content_tag('span', link_to_user(u), :class => 'user') }.join(",\n")
end
end

View File

@@ -17,19 +17,6 @@
module WikiHelper
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
s = ''
pages.select {|p| p.parent == parent}.each do |page|
attrs = "value='#{page.id}'"
attrs << " selected='selected'" if selected == page
indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
wiki_page_options_for_select(pages, selected, page, level + 1)
end
s
end
def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)}
words_add = 0
@@ -49,7 +36,7 @@ module WikiHelper
words_add += 1
else
del_at = pos unless del_at
deleted << ' ' + h(change[2])
deleted << ' ' + change[2]
words_del += 1
end
end

View File

@@ -33,7 +33,7 @@ class Attachment < ActiveRecord::Base
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
acts_as_activity_provider :type => 'documents',
:permission => :view_documents,
@@ -46,9 +46,7 @@ class Attachment < ActiveRecord::Base
@@storage_path = "#{RAILS_ROOT}/files"
def validate
if self.filesize > Setting.attachment_max_size.to_i.kilobytes
errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
end
errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
end
def file=(incoming_file)
@@ -58,9 +56,6 @@ class Attachment < ActiveRecord::Base
self.filename = sanitize_filename(@temp_file.original_filename)
self.disk_filename = Attachment.disk_filename(filename)
self.content_type = @temp_file.content_type.to_s.chomp
if content_type.blank?
self.content_type = Redmine::MimeType.of(filename)
end
self.filesize = @temp_file.size
end
end
@@ -70,20 +65,14 @@ class Attachment < ActiveRecord::Base
nil
end
# Copies the temporary file to its final location
# and computes its MD5 hash
# Copy temp file to its final location
def before_save
if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'")
md5 = Digest::MD5.new
File.open(diskfile, "wb") do |f|
buffer = ""
while (buffer = @temp_file.read(8192))
f.write(buffer)
md5.update(buffer)
end
f.write(@temp_file.read)
end
self.digest = md5.hexdigest
self.digest = self.class.digest(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
@@ -129,38 +118,6 @@ class Attachment < ActiveRecord::Base
self.filename =~ /\.(patch|diff)$/i
end
# Returns true if the file is readable
def readable?
File.readable?(diskfile)
end
# Bulk attaches a set of files to an object
#
# Returns a Hash of the results:
# :files => array of the attached files
# :unsaved => array of the files that could not be attached
def self.attach_files(obj, attachments)
attached = []
if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
next unless file && file.size > 0
a = Attachment.create(:container => obj,
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
if a.new_record?
obj.unsaved_attachments ||= []
obj.unsaved_attachments << a
else
attached << a
end
end
end
{:files => attached, :unsaved => obj.unsaved_attachments}
end
private
def sanitize_filename(value)
# get only the filename, not the whole path
@@ -174,18 +131,21 @@ private
# Returns an ASCII or hashed filename
def self.disk_filename(filename)
timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
ascii = ''
df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
ascii = filename
df << filename
else
ascii = Digest::MD5.hexdigest(filename)
df << Digest::MD5.hexdigest(filename)
# keep the extension if any
ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
end
while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
timestamp.succ!
df
end
# Returns the MD5 digest of the file at given path
def self.digest(filename)
File.open(filename, 'rb') do |f|
Digest::MD5.hexdigest(f.read)
end
"#{timestamp}_#{ascii}"
end
end

View File

@@ -32,15 +32,6 @@ class AuthSource < ActiveRecord::Base
"Abstract"
end
def allow_password_changes?
self.class.allow_password_changes?
end
# Does this auth source backend allow password changes?
def self.allow_password_changes?
false
end
# Try to authenticate a user not yet registered against available sources
def self.authenticate(login, password)
AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|

View File

@@ -33,12 +33,30 @@ class AuthSourceLdap < AuthSource
def authenticate(login, password)
return nil if login.blank? || password.blank?
attrs = get_user_dn(login)
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
return attrs.except(:dn)
attrs = []
# get user's DN
ldap_con = initialize_ldap_con(self.account, self.account_password)
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
dn = String.new
ldap_con.search( :base => self.base_dn,
:filter => object_filter & login_filter,
# 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 ] if onthefly_register?
end
return nil if dn.empty?
logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug?
# authenticate user
ldap_con = initialize_ldap_con(dn, password)
return nil unless ldap_con.bind
# return user's attributes
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
attrs
rescue Net::LDAP::LdapError => text
raise "LdapError: " + text
end
@@ -71,56 +89,6 @@ class AuthSourceLdap < AuthSource
options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
Net::LDAP.new options
end
def get_user_attributes_from_ldap_entry(entry)
{
:dn => entry.dn,
: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
}
end
# Return the attributes needed for the LDAP search. It will only
# include the user attributes if on-the-fly registration is enabled
def search_attributes
if onthefly_register?
['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
else
['dn']
end
end
# Check if a DN (user record) authenticates with the password
def authenticate_dn(dn, password)
if dn.present? && password.present?
initialize_ldap_con(dn, password).bind
end
end
# Get the user's dn and any attributes for them, given their login
def get_user_dn(login)
ldap_con = initialize_ldap_con(self.account, self.account_password)
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
attrs = {}
ldap_con.search( :base => self.base_dn,
:filter => object_filter & login_filter,
:attributes=> search_attributes) do |entry|
if onthefly_register?
attrs = get_user_attributes_from_ldap_entry(entry)
else
attrs = {:dn => entry.dn}
end
logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
end
attrs
end
def self.get_attr(entry, attr_name)
if !attr_name.blank?

View File

@@ -18,7 +18,7 @@
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 => :destroy, :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
@@ -26,25 +26,4 @@ class Board < ActiveRecord::Base
validates_presence_of :name, :description
validates_length_of :name, :maximum => 30
validates_length_of :description, :maximum => 255
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_messages, project)
end
def to_s
name
end
def reset_counters!
self.class.reset_counters!(id)
end
# Updates topics_count, messages_count and last_message_id attributes for +board_id+
def self.reset_counters!(board_id)
board_id = board_id.to_i
update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
" messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
["id = ?", board_id])
end
end

View File

@@ -19,13 +19,8 @@ class Change < ActiveRecord::Base
belongs_to :changeset
validates_presence_of :changeset_id, :action, :path
before_save :init_path
def relative_path
changeset.repository.relative_path(path)
end
def init_path
self.path ||= ""
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -23,10 +23,10 @@ class Changeset < ActiveRecord::Base
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments,
:datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments',
:include => {:repository => :project},
@@ -35,27 +35,15 @@ class Changeset < ActiveRecord::Base
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => [:user, {:repository => :project}]}
:find_options => {:include => {:repository => :project}}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
named_scope :visible, lambda {|*args| { :include => {:repository => :project},
:conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s)
end
# Returns the identifier of this changeset; depending on repository backends
def identifier
if repository.class.respond_to? :changeset_identifier
repository.class.changeset_identifier self
else
revision.to_s
end
end
def comments=(comment)
write_attribute(:comments, Changeset.normalize_comments(comment))
@@ -65,20 +53,7 @@ class Changeset < ActiveRecord::Base
self.commit_date = date
super
end
# Returns the readable identifier
def format_identifier
if repository.class.respond_to? :format_changeset_identifier
repository.class.format_changeset_identifier self
else
identifier
end
end
def committer=(arg)
write_attribute(:committer, self.class.to_utf8(arg.to_s))
end
def project
repository.project
end
@@ -94,47 +69,57 @@ class Changeset < ActiveRecord::Base
def after_create
scan_comment_for_issue_ids
end
TIMELOG_RE = /
(
((\d+)(h|hours?))((\d+)(m|min)?)?
|
((\d+)(h|hours?|m|min))
|
(\d+):(\d+)
|
(\d+([\.,]\d+)?)h?
)
/x
require 'pp'
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)
ref_keywords_any = ref_keywords.delete('*')
# 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 = []
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
action, refs = match[2], match[3]
next unless action.present? || ref_keywords_any
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
if issue
referenced_issues << issue
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
end
end
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
referenced_issues.uniq!
self.issues = referenced_issues unless referenced_issues.empty?
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|
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
next if issue.status.is_closed?
csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
end
journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
issue.status = fix_status
issue.done_ratio = done_ratio if done_ratio
issue.save
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
end
referenced_issues += target_issues
end
self.issues = referenced_issues.uniq
end
def short_comments
@@ -144,14 +129,6 @@ class Changeset < ActiveRecord::Base
def long_comments
@long_comments || split_comments.last
end
def text_tag
if scmid?
"commit:#{scmid}"
else
"r#{revision}"
end
end
# Returns the previous changeset
def previous
@@ -167,78 +144,9 @@ class Changeset < ActiveRecord::Base
def self.normalize_comments(str)
to_utf8(str.to_s.strip)
end
# Creates a new Change from it's common parameters
def create_change(change)
Change.create(:changeset => self,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
private
# Finds an issue that can be referenced by the commit message
# i.e. an issue that belong to the repository project, a subproject or a parent project
def find_referenced_issue_by_id(id)
return nil if id.blank?
issue = Issue.find_by_id(id.to_i, :include => :project)
if issue
unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
issue = nil
end
end
issue
end
def fix_issue(issue)
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
if status.nil?
logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
return issue
end
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
return if issue.status && issue.status.is_closed?
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
issue.status = status
unless Setting.commit_fix_done_ratio.blank?
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
end
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
unless issue.save
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
end
issue
end
def log_time(issue, hours)
time_entry = TimeEntry.new(
:user => user,
:hours => hours,
:issue => issue,
:spent_on => commit_date,
:comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
)
time_entry.activity = log_time_activity unless log_time_activity.nil?
unless time_entry.save
logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
end
time_entry
end
def log_time_activity
if Setting.commit_logtime_activity_id.to_i > 0
TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
end
end
def split_comments
comments =~ /\A(.+?)\r?\n(.*)$/m
@short_comments = $1 || comments
@@ -251,17 +159,11 @@ class Changeset < ActiveRecord::Base
encoding = Setting.commit_logs_encoding.to_s.strip
unless encoding.blank? || encoding == 'UTF-8'
begin
str = Iconv.conv('UTF-8', encoding, str)
return Iconv.conv('UTF-8', encoding, str)
rescue Iconv::Failure
# do nothing here
end
end
# removes invalid UTF8 sequences
begin
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
str
end
str
end
end

View File

@@ -20,11 +20,20 @@ class CustomField < ActiveRecord::Base
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 },
"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, :scope => :type
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
def initialize(attributes = nil)
super
@@ -39,14 +48,14 @@ class CustomField < ActiveRecord::Base
def validate
if self.field_format == "list"
errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
end
# validate default value
v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
v.custom_field.is_required = false
errors.add(:default_value, :invalid) unless v.valid?
errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
end
# Makes possible_values accept a multiline string
@@ -58,25 +67,6 @@ class CustomField < ActiveRecord::Base
end
end
def cast_value(value)
casted = nil
unless value.blank?
case field_format
when 'string', 'text', 'list'
casted = value
when 'date'
casted = begin; value.to_date; rescue; nil end
when 'bool'
casted = (value == '1' ? true : false)
when 'int'
casted = value.to_i
when 'float'
casted = value.to_f
end
end
casted
end
# Returns a ORDER BY clause that can used to sort customized
# objects by their value of the custom field.
# Returns false, if the custom field can not be used for sorting.

View File

@@ -20,7 +20,7 @@ class CustomValue < ActiveRecord::Base
belongs_to :customized, :polymorphic => true
def after_initialize
if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
self.value ||= custom_field.default_value
end
end
@@ -34,10 +34,6 @@ class CustomValue < ActiveRecord::Base
custom_field.editable?
end
def visible?
custom_field.visible?
end
def required?
custom_field.is_required?
end
@@ -49,22 +45,22 @@ class CustomValue < ActiveRecord::Base
protected
def validate
if value.blank?
errors.add(:value, :blank) if custom_field.is_required? and value.blank?
errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank?
else
errors.add(:value, :invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
errors.add(:value, :too_short, :count => custom_field.min_length) if custom_field.min_length > 0 and value.length < custom_field.min_length
errors.add(:value, :too_long, :count => custom_field.max_length) if custom_field.max_length > 0 and value.length > custom_field.max_length
errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
# Format specific validations
case custom_field.field_format
when 'int'
errors.add(:value, :not_a_number) unless value =~ /^[+-]?\d+$/
errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/
when 'float'
begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end
begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
when 'date'
errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
when 'list'
errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value)
errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value)
end
end
end

View File

@@ -17,7 +17,7 @@
class Document < ActiveRecord::Base
belongs_to :project
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
acts_as_attachable :delete_permission => :manage_documents
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
@@ -29,21 +29,9 @@ class Document < ActiveRecord::Base
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_documents, project)
end
def after_initialize
if new_record?
self.category ||= DocumentCategory.default
self.category ||= Enumeration.default('DCAT')
end
end
def updated_on
unless @updated_on
a = attachments.find(:first, :order => 'created_on DESC')
@updated_on = (a && a.created_on) || created_on
end
@updated_on
end
end

View File

@@ -1,34 +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 DocumentCategory < Enumeration
has_many :documents, :foreign_key => 'category_id'
OptionName = :enumeration_doc_categories
def option_name
OptionName
end
def objects_count
documents.count
end
def transfer_relations(to)
documents.update_all("category_id = #{to.id}")
end
end

View File

@@ -1,23 +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 DocumentCategoryCustomField < CustomField
def type_name
:enumeration_doc_categories
end
end

View File

@@ -1,22 +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 DocumentObserver < ActiveRecord::Observer
def after_create(document)
Mailer.deliver_document_added(document) if Setting.notified_events.include?('document_added')
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# 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
@@ -20,19 +20,4 @@ class EnabledModule < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, :scope => :project_id
after_create :module_enabled
private
# after_create callback used to do things when a module is enabled
def module_enabled
case name
when 'wiki'
# Create a wiki with a default start page
if project && project.wiki.nil?
Wiki.create(:project => project, :start_page => 'Wiki')
end
end
end
end

View File

@@ -16,59 +16,46 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Enumeration < ActiveRecord::Base
default_scope :order => "#{Enumeration.table_name}.position ASC"
belongs_to :project
acts_as_list :scope => 'type = \'#{type}\''
acts_as_customizable
acts_as_tree :order => 'position ASC'
acts_as_list :scope => 'opt = \'#{opt}\''
before_destroy :check_integrity
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:type, :project_id]
validates_presence_of :opt, :name
validates_uniqueness_of :name, :scope => [:opt]
validates_length_of :name, :maximum => 30
named_scope :shared, :conditions => { :project_id => nil }
named_scope :active, :conditions => { :active => true }
def self.default
# Creates a fake default scope so Enumeration.default will check
# it's type. STI subclasses will automatically add their own
# types to the finder.
if self.descends_from_active_record?
find(:first, :conditions => { :is_default => true, :type => 'Enumeration' })
else
# STI classes are
find(:first, :conditions => { :is_default => true })
end
# Single table inheritance would be an option
OPTIONS = {
"IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id},
"DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id},
"ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id}
}.freeze
def self.get_values(option)
find(:all, :conditions => {:opt => option}, :order => 'position')
end
# Overloaded on concrete classes
def self.default(option)
find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position')
end
def option_name
nil
OPTIONS[self.opt][:label]
end
def before_save
if is_default? && is_default_changed?
Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
end
end
# Overloaded on concrete classes
def objects_count
0
OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
end
def in_use?
self.objects_count != 0
end
# Is this enumeration overiding a system level enumeration?
def is_override?
!self.parent.nil?
end
alias :destroy_without_reassign :destroy
@@ -76,7 +63,7 @@ class Enumeration < ActiveRecord::Base
# If a enumeration is specified, objects are reassigned
def destroy(reassign_to = nil)
if reassign_to && reassign_to.is_a?(Enumeration)
self.transfer_relations(reassign_to)
OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
end
destroy_without_reassign
end
@@ -86,49 +73,9 @@ class Enumeration < ActiveRecord::Base
end
def to_s; name end
# Returns the Subclasses of Enumeration. Each Subclass needs to be
# required in development mode.
#
# Note: subclasses is protected in ActiveRecord
def self.get_subclasses
@@subclasses[Enumeration]
end
# Does the +new+ Hash override the previous Enumeration?
def self.overridding_change?(new, previous)
if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
return false
else
return true
end
end
# Does the +new+ Hash have the same custom values as the previous Enumeration?
def self.same_custom_values?(new, previous)
previous.custom_field_values.each do |custom_value|
if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
return false
end
end
return true
end
# Are the new and previous fields equal?
def self.same_active_state?(new, previous)
new = (new == "1" ? true : false)
return new == previous
end
private
def check_integrity
raise "Can't delete enumeration" if self.in_use?
end
end
# Force load the subclasses in development mode
require_dependency 'time_entry_activity'
require_dependency 'document_category'
require_dependency 'issue_priority'

View File

@@ -1,49 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 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 Group < Principal
has_and_belongs_to_many :users, :after_add => :user_added,
:after_remove => :user_removed
acts_as_customizable
validates_presence_of :lastname
validates_uniqueness_of :lastname, :case_sensitive => false
validates_length_of :lastname, :maximum => 30
def to_s
lastname.to_s
end
def user_added(user)
members.each do |member|
next if member.project.nil?
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
member.member_roles.each do |member_role|
user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
end
user_member.save!
end
end
def user_removed(user)
members.each do |member|
MemberRole.find(:all, :include => :member,
:conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
end
end
end

View File

@@ -1,22 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 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 GroupCustomField < CustomField
def type_name
:label_group_plural
end
end

View File

@@ -16,15 +16,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Issue < ActiveRecord::Base
include Redmine::SafeAttributes
belongs_to :project
belongs_to :tracker
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy
@@ -34,7 +32,6 @@ class Issue < ActiveRecord::Base
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_nested_set :scope => 'root_id'
acts_as_attachable :after_remove => :attachment_removed
acts_as_customizable
acts_as_watchable
@@ -42,19 +39,14 @@ class Issue < ActiveRecord::Base
:include => [:project, :journals],
# sort by id so that limited eager loading doesn't break with postgresql
:order_column => "#{table_name}.id"
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
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}},
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
attr_reader :current_journal
validates_presence_of :subject, :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
@@ -62,36 +54,6 @@ class Issue < ActiveRecord::Base
named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
named_scope :with_limit, lambda { |limit| { :limit => limit} }
named_scope :on_active_project, :include => [:status, :project, :tracker],
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
named_scope :for_gantt, lambda {
{
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version]
}
}
named_scope :without_version, lambda {
{
:conditions => { :fixed_version_id => nil}
}
}
named_scope :with_query, lambda {|query|
{
:conditions => Query.merge_conditions(query.statement)
}
}
before_create :default_assign
before_save :close_duplicates, :update_done_ratio_from_issue_status
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
after_destroy :destroy_children
after_destroy :update_parent_attributes
# Returns true if usr or current user is allowed to view the issue
def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project)
@@ -101,7 +63,7 @@ class Issue < ActiveRecord::Base
if new_record?
# set default values for new records only
self.status ||= IssueStatus.default
self.priority ||= IssuePriority.default
self.priority ||= Enumeration.default('IPRI')
end
end
@@ -111,239 +73,127 @@ class Issue < ActiveRecord::Base
end
def copy_from(arg)
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
self.status = issue.status
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
# Moves/copies an issue to a new project and tracker
# Returns the moved/copied issue on success, false on failure
def move_to_project(*args)
ret = Issue.transaction do
move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
end || false
end
def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
def move_to(new_project, new_tracker = nil, options = {})
options ||= {}
issue = options[:copy] ? self.class.new.copy_from(self) : self
if new_project && issue.project_id != new_project.id
# delete issue relations
unless Setting.cross_project_issue_relations?
issue.relations_from.clear
issue.relations_to.clear
end
# issue is moved to another project
# reassign to the category with same name if any
new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
issue.category = new_category
# Keep the fixed_version if it's still valid in the new_project
unless new_project.shared_versions.include?(issue.fixed_version)
issue.fixed_version = nil
end
issue.project = new_project
if issue.parent && issue.parent.project_id != issue.project_id
issue.parent_issue_id = nil
end
end
if new_tracker
issue.tracker = new_tracker
issue.reset_custom_values!
end
if options[:copy]
issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
issue.status = if options[:attributes] && options[:attributes][:status_id]
IssueStatus.find_by_id(options[:attributes][:status_id])
else
self.status
end
end
# Allow bulk setting of attributes on the issue
if options[:attributes]
issue.attributes = options[:attributes]
end
if issue.save
unless options[:copy]
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
issue.children.each do |child|
unless child.move_to_project_without_transaction(new_project)
# Move failed and transaction was rollback'd
return false
end
issue = options[:copy] ? self.clone : self
transaction do
if new_project && issue.project_id != new_project.id
# delete issue relations
unless Setting.cross_project_issue_relations?
issue.relations_from.clear
issue.relations_to.clear
end
# issue is moved to another project
# reassign to the category with same name if any
new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
issue.category = new_category
issue.fixed_version = nil
issue.project = new_project
end
if new_tracker
issue.tracker = new_tracker
end
if options[:copy]
issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
issue.status = self.status
end
if issue.save
unless options[:copy]
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
end
else
Issue.connection.rollback_db_transaction
return false
end
else
return false
end
issue
end
def status_id=(sid)
self.status = nil
write_attribute(:status_id, sid)
return issue
end
def priority_id=(pid)
self.priority = nil
write_attribute(:priority_id, pid)
end
def tracker_id=(tid)
self.tracker = nil
result = write_attribute(:tracker_id, tid)
@custom_field_values = nil
result
end
# Overrides attributes= so that tracker_id gets assigned first
def attributes_with_tracker_first=(new_attributes, *args)
return if new_attributes.nil?
new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
if new_tracker_id
self.tracker_id = new_tracker_id
end
send :attributes_without_tracker_first=, new_attributes, *args
end
# Do not redefine alias chain on reload (see #4838)
alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
def estimated_hours=(h)
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end
safe_attributes 'tracker_id',
'status_id',
'parent_issue_id',
'category_id',
'assigned_to_id',
'priority_id',
'fixed_version_id',
'subject',
'description',
'start_date',
'due_date',
'done_ratio',
'estimated_hours',
'custom_field_values',
'custom_fields',
'lock_version',
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
safe_attributes 'status_id',
'assigned_to_id',
'fixed_version_id',
'done_ratio',
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
# Safely sets attributes
# Should be called from controllers instead of #attributes=
# attr_accessible is too rough because we still want things like
# Issue.new(:project => foo) to work
# TODO: move workflow/permission checks from controllers to here
def safe_attributes=(attrs, user=User.current)
return unless attrs.is_a?(Hash)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
attrs = delete_unsafe_attributes(attrs, user)
return if attrs.empty?
# Tracker must be set before since new_statuses_allowed_to depends on it.
if t = attrs.delete('tracker_id')
self.tracker_id = t
end
if attrs['status_id']
unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
attrs.delete('status_id')
end
end
unless leaf?
attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
end
if attrs.has_key?('parent_issue_id')
if !user.allowed_to?(:manage_subtasks, project)
attrs.delete('parent_issue_id')
elsif !attrs['parent_issue_id'].blank?
attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
end
end
self.attributes = attrs
end
def done_ratio
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
status.default_done_ratio
else
read_attribute(:done_ratio)
end
end
def self.use_status_for_done_ratio?
Setting.issue_done_ratio == 'issue_status'
end
def self.use_field_for_done_ratio?
Setting.issue_done_ratio == 'issue_field'
end
def validate
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
errors.add :due_date, :not_a_date
errors.add :due_date, :activerecord_error_not_a_date
end
if self.due_date and self.start_date and self.due_date < self.start_date
errors.add :due_date, :greater_than_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, :invalid
end
if fixed_version
if !assignable_versions.include?(fixed_version)
errors.add :fixed_version_id, :inclusion
elsif reopened? && fixed_version.closed?
errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
end
end
# Checks that the issue can not be added/moved to a disabled tracker
if project && (tracker_id_changed? || project_id_changed?)
unless project.trackers.include?(tracker)
errors.add :tracker_id, :inclusion
end
end
# Checks parent issue assignment
if @parent_issue
if @parent_issue.project_id != project_id
errors.add :parent_issue_id, :not_same_project
elsif !new_record?
# moving an existing issue
if @parent_issue.root_id != root_id
# we can always move to another tree
elsif move_possible?(@parent_issue)
# move accepted inside tree
else
errors.add :parent_issue_id, :not_a_valid_parent
end
end
errors.add :start_date, :activerecord_error_invalid
end
end
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios
# even if the user turns off the setting later
def update_done_ratio_from_issue_status
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
self.done_ratio = status.default_done_ratio
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|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# 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)
}
@current_journal.save
end
# Save the issue even if the journal is not saved (because empty)
true
end
def after_save
# Reload is needed in order to get the right status
reload
# Update start/due dates of following issues
relations_from.each(&:set_issue_to_dates)
# Close duplicates if the issue was closed
if @issue_before_change && !@issue_before_change.closed? && self.closed?
duplicates.each do |duplicate|
# Reload is need in case the duplicate was updated by a previous duplicate
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
duplicate.init_journal(@current_journal.user, @current_journal.notes)
duplicate.update_attribute :status, self.status
end
end
end
@@ -363,93 +213,34 @@ class Issue < ActiveRecord::Base
self.status.is_closed?
end
# Return true if the issue is being reopened
def reopened?
if !new_record? && status_id_changed?
status_was = IssueStatus.find_by_id(status_id_was)
status_new = IssueStatus.find_by_id(status_id)
if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
return true
end
end
false
end
# Return true if the issue is being closed
def closing?
if !new_record? && status_id_changed?
status_was = IssueStatus.find_by_id(status_id_was)
status_new = IssueStatus.find_by_id(status_id)
if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
return true
end
end
false
end
# Returns true if the issue is overdue
def overdue?
!due_date.nil? && (due_date < Date.today) && !status.is_closed?
end
# Is the amount of work done less than it should for the due date
def behind_schedule?
return false if start_date.nil? || due_date.nil?
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
return done_date <= Date.today
end
# Does this issue have children?
def children?
!leaf?
end
# Users the issue can be assigned to
def assignable_users
users = project.assignable_users
users << author if author
users.uniq.sort
end
# Versions that the issue can be assigned to
def assignable_versions
@assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
end
# Returns true if this issue is blocked by another issue that is still open
def blocked?
!relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
project.assignable_users
end
# Returns an array of status that user is able to apply
def new_statuses_allowed_to(user, include_default=false)
statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
def new_statuses_allowed_to(user)
statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
statuses << status unless statuses.empty?
statuses << IssueStatus.default if include_default
statuses = statuses.uniq.sort
blocked? ? statuses.reject {|s| s.is_closed?} : statuses
statuses.uniq.sort
end
# Returns the mail adresses of users that should be notified
# Returns the mail adresses of users that should be notified for the issue
def recipients
notified = project.notified_users
# Author and assignee are always notified unless they have been
# locked or don't want to be notified
notified << author if author && author.active? && author.notify_about?(self)
notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
notified.uniq!
# Remove users that can not view the issue
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
recipients = project.recipients
# Author and assignee are always notified unless they have been locked
recipients << author.mail if author && author.active?
recipients << assigned_to.mail if assigned_to && assigned_to.active?
recipients.compact.uniq
end
# Returns the total number of hours spent on this issue and its descendants
#
# Example:
# spent_hours => 0.0
# spent_hours => 50.2
def spent_hours
@spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
@spent_hours ||= time_entries.sum(:hours) || 0
end
def relations
@@ -476,313 +267,20 @@ class Issue < ActiveRecord::Base
due_date || (fixed_version ? fixed_version.effective_date : nil)
end
# Returns the time scheduled for this issue.
#
# Example:
# Start Date: 2/26/09, End Date: 3/04/09
# duration => 6
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} +
ancestors.collect(&:soonest_start)
).compact.max
end
def reschedule_after(date)
return if date.nil?
if leaf?
if start_date.nil? || start_date < date
self.start_date, self.due_date = date, date + duration
save
end
else
leaves.each do |leaf|
leaf.reschedule_after(date)
end
end
end
def <=>(issue)
if issue.nil?
-1
elsif root_id != issue.root_id
(root_id || 0) <=> (issue.root_id || 0)
else
(lft || 0) <=> (issue.lft || 0)
end
@soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
end
def to_s
"#{tracker} ##{id}: #{subject}"
end
# Returns a string of css classes that apply to the issue
def css_classes
s = "issue status-#{status.position} priority-#{priority.position}"
s << ' closed' if closed?
s << ' overdue' if overdue?
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
s
end
# Saves an issue, time_entry, attachments, and a journal from the parameters
# Returns false if save fails
def save_issue_with_child_records(params, existing_time_entry=nil)
Issue.transaction do
if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
@time_entry = existing_time_entry || TimeEntry.new
@time_entry.project = project
@time_entry.issue = self
@time_entry.user = User.current
@time_entry.spent_on = Date.today
@time_entry.attributes = params[:time_entry]
self.time_entries << @time_entry
end
if valid?
attachments = Attachment.attach_files(self, params[:attachments])
attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
begin
if save
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
else
raise ActiveRecord::Rollback
end
rescue ActiveRecord::StaleObjectError
attachments[:files].each(&:destroy)
errors.add_to_base l(:notice_locking_conflict)
raise ActiveRecord::Rollback
end
end
end
end
# Unassigns issues from +version+ if it's no longer shared with issue's project
def self.update_versions_from_sharing_change(version)
# Update issues assigned to the version
update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
end
# Unassigns issues from versions that are no longer shared
# after +project+ was moved
def self.update_versions_from_hierarchy_change(project)
moved_project_ids = project.self_and_descendants.reload.collect(&:id)
# Update issues of the moved projects and issues assigned to a version of a moved project
Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
end
def parent_issue_id=(arg)
parent_issue_id = arg.blank? ? nil : arg.to_i
if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
@parent_issue.id
else
@parent_issue = nil
nil
end
end
def parent_issue_id
if instance_variable_defined? :@parent_issue
@parent_issue.nil? ? nil : @parent_issue.id
else
parent_id
end
end
# Extracted from the ReportsController.
def self.by_tracker(project)
count_and_group_by(:project => project,
:field => 'tracker_id',
:joins => Tracker.table_name)
end
def self.by_version(project)
count_and_group_by(:project => project,
:field => 'fixed_version_id',
:joins => Version.table_name)
end
def self.by_priority(project)
count_and_group_by(:project => project,
:field => 'priority_id',
:joins => IssuePriority.table_name)
end
def self.by_category(project)
count_and_group_by(:project => project,
:field => 'category_id',
:joins => IssueCategory.table_name)
end
def self.by_assigned_to(project)
count_and_group_by(:project => project,
:field => 'assigned_to_id',
:joins => User.table_name)
end
def self.by_author(project)
count_and_group_by(:project => project,
:field => 'author_id',
:joins => User.table_name)
end
def self.by_subproject(project)
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
i.project_id as project_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s
where
i.status_id=s.id
and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
end
# End ReportsController extraction
# Returns an array of projects that current user can move issues to
def self.allowed_target_projects_on_move
projects = []
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
projects = Project.visible.all
elsif User.current.logged?
if Role.non_member.allowed_to?(:move_issues)
projects = Project.visible.all
else
User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
end
end
projects
end
private
def update_nested_set_attributes
if root_id.nil?
# issue was just created
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
set_default_left_and_right
Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
if @parent_issue
move_to_child_of(@parent_issue)
end
reload
elsif parent_issue_id != parent_id
former_parent_id = parent_id
# moving an existing issue
if @parent_issue && @parent_issue.root_id == root_id
# inside the same tree
move_to_child_of(@parent_issue)
else
# to another tree
unless root?
move_to_right_of(root)
reload
end
old_root_id = root_id
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
target_maxright = nested_set_scope.maximum(right_column_name) || 0
offset = target_maxright + 1 - lft
Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
self[left_column_name] = lft + offset
self[right_column_name] = rgt + offset
if @parent_issue
move_to_child_of(@parent_issue)
end
end
reload
# delete invalid relations of all descendants
self_and_descendants.each do |issue|
issue.relations.each do |relation|
relation.destroy unless relation.valid?
end
end
# update former parent
recalculate_attributes_for(former_parent_id) if former_parent_id
end
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
end
def update_parent_attributes
recalculate_attributes_for(parent_id) if parent_id
end
def recalculate_attributes_for(issue_id)
if issue_id && p = Issue.find_by_id(issue_id)
# priority = highest priority of children
if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
p.priority = IssuePriority.find_by_position(priority_position)
end
# start/due dates = lowest/highest dates of children
p.start_date = p.children.minimum(:start_date)
p.due_date = p.children.maximum(:due_date)
if p.start_date && p.due_date && p.due_date < p.start_date
p.start_date, p.due_date = p.due_date, p.start_date
end
# done ratio = weighted average ratio of leaves
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
leaves_count = p.leaves.count
if leaves_count > 0
average = p.leaves.average(:estimated_hours).to_f
if average == 0
average = 1
end
done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
progress = done / (average * leaves_count)
p.done_ratio = progress.round
end
end
# estimate = sum of leaves estimates
p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
p.estimated_hours = nil if p.estimated_hours == 0.0
# ancestors will be recursively updated
p.save(false)
end
end
def destroy_children
unless leaf?
children.each do |child|
child.destroy
end
end
end
# Update issues so their versions are not pointing to a
# fixed_version that is not shared with the issue's project
def self.update_versions(conditions=nil)
# Only need to update issues with a fixed_version from
# a different project and that is not systemwide shared
Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
" AND #{Version.table_name}.sharing <> 'system'",
conditions),
:include => [:project, :fixed_version]
).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version)
issue.init_journal(User.current)
issue.fixed_version = nil
issue.save
end
end
end
# Callback on attachment deletion
def attachment_removed(obj)
journal = init_journal(User.current)
@@ -791,92 +289,4 @@ class Issue < ActiveRecord::Base
:old_value => obj.filename)
journal.save
end
# Default assignment based on category
def default_assign
if assigned_to.nil? && category && category.assigned_to
self.assigned_to = category.assigned_to
end
end
# Updates start/due dates of following issues
def reschedule_following_issues
if start_date_changed? || due_date_changed?
relations_from.each do |relation|
relation.set_issue_to_dates
end
end
end
# Closes duplicates if the issue is being closed
def close_duplicates
if closing?
duplicates.each do |duplicate|
# Reload is need in case the duplicate was updated by a previous duplicate
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
if @current_journal
duplicate.init_journal(@current_journal.user, @current_journal.notes)
end
duplicate.update_attribute :status, self.status
end
end
end
# Saves the changes in a Journal
# Called after_save
def create_journal
if @current_journal
# attributes changes
(Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# 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)
}
@current_journal.save
# reset current journal
init_journal @current_journal.user, @current_journal.notes
end
end
# Query generator for selecting groups of issue counts for a project
# based on specific criteria
#
# Options
# * project - Project to search in.
# * field - String. Issue field to key off of in the grouping.
# * joins - String. The table name to join against.
def self.count_and_group_by(options)
project = options.delete(:project)
select_field = options.delete(:field)
joins = options.delete(:joins)
where = "i.#{select_field}=j.id"
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
j.id as #{select_field},
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
where
i.status_id=s.id
and #{where}
and i.project_id=#{project.id}
group by s.id, s.is_closed, j.id")
end
end

View File

@@ -1,22 +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 IssueObserver < ActiveRecord::Observer
def after_create(issue)
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
end
end

View File

@@ -1,34 +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 IssuePriority < Enumeration
has_many :issues, :foreign_key => 'priority_id'
OptionName = :enumeration_issue_priorities
def option_name
OptionName
end
def objects_count
issues.count
end
def transfer_relations(to)
issues.update_all("priority_id = #{to.id}")
end
end

View File

@@ -1,23 +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 IssuePriorityCustomField < CustomField
def type_name
:enumeration_issue_priorities
end
end

View File

@@ -21,19 +21,13 @@ class IssueRelation < ActiveRecord::Base
TYPE_RELATES = "relates"
TYPE_DUPLICATES = "duplicates"
TYPE_DUPLICATED = "duplicated"
TYPE_BLOCKS = "blocks"
TYPE_BLOCKED = "blocked"
TYPE_PRECEDES = "precedes"
TYPE_FOLLOWS = "follows"
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES },
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED },
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :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
@@ -45,10 +39,9 @@ class IssueRelation < ActiveRecord::Base
def validate
if issue_from && issue_to
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from
errors.add_to_base :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(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
@@ -56,24 +49,11 @@ class IssueRelation < ActiveRecord::Base
(self.issue_from_id == issue.id) ? issue_to : issue_from
end
# Returns the relation type for +issue+
def relation_type_for(issue)
if TYPES[relation_type]
if self.issue_from_id == issue.id
relation_type
else
TYPES[relation_type][:sym]
end
end
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
reverse_if_needed
if TYPE_PRECEDES == relation_type
self.delay ||= 0
else
@@ -84,30 +64,18 @@ class IssueRelation < ActiveRecord::Base
def set_issue_to_dates
soonest_start = self.successor_soonest_start
if soonest_start && issue_to
issue_to.reschedule_after(soonest_start)
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
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date)
(issue_from.due_date || issue_from.start_date) + 1 + delay
end
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
private
# Reverses the relation if needed so that it gets stored in the proper way
def reverse_if_needed
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
issue_tmp = issue_to
self.issue_to = issue_from
self.issue_from = issue_tmp
self.relation_type = TYPES[relation_type][:reverse]
end
end
end

View File

@@ -17,16 +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
before_destroy :delete_workflows
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
def after_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
@@ -36,49 +33,27 @@ class IssueStatus < ActiveRecord::Base
def self.default
find(:first, :conditions =>["is_default=?", true])
end
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
def self.update_issue_done_ratios
if Issue.use_status_for_done_ratio?
IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
Issue.update_all(["done_ratio = ?", status.default_done_ratio],
["status_id = ?", status.id])
end
end
return Issue.use_status_for_done_ratio?
end
# 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(roles, tracker)
if roles && tracker
role_ids = roles.collect(&:id)
new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
else
[]
end
def new_statuses_allowed_to(role, tracker)
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(roles, tracker)
if roles && tracker
workflows.find(:all,
:include => :new_status,
:conditions => { :role_id => roles.collect(&:id),
:tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
else
[]
end
def find_new_statuses_allowed_to(role, tracker)
new_statuses = workflows.find(:all,
:include => :new_status,
: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, roles, tracker)
if status && roles && tracker
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
else
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
end
def <=>(status)
@@ -91,9 +66,4 @@ private
def check_integrity
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
end
# Deletes associated workflows
def delete_workflows
Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
end
end

View File

@@ -38,7 +38,7 @@ class Journal < ActiveRecord::Base
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
def save(*args)
def save
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end
@@ -65,12 +65,4 @@ class Journal < ActiveRecord::Base
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
# Returns a string of css classes
def css_classes
s = 'journal'
s << ' has-notes' unless notes.blank?
s << ' has-details' unless details.blank?
s
end
end

View File

@@ -1,27 +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 JournalObserver < ActiveRecord::Observer
def after_create(journal)
if Setting.notified_events.include?('issue_updated') ||
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
Mailer.deliver_issue_edit(journal)
end
end
end

View File

@@ -17,7 +17,6 @@
class MailHandler < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper
include Redmine::I18n
class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end
@@ -35,46 +34,19 @@ class MailHandler < ActionMailer::Base
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
# Status overridable by default
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
super email
end
# Processes incoming emails
# Returns the created object (eg. an issue, a message) or false
def receive(email)
@email = email
sender_email = email.from.to_a.first.to_s.strip
# Ignore emails received from the application emission address to avoid hell cycles
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
@user = User.active.find_by_mail(email.from.first.to_s.strip)
unless @user
# Unknown user => the email is ignored
# TODO: ability to create the user's account
logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
return false
end
@user = User.find_by_mail(sender_email) if sender_email.present?
if @user && !@user.active?
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
return false
end
if @user.nil?
# Email was submitted by an unknown user
case @@handler_options[:unknown_user]
when 'accept'
@user = User.anonymous
when 'create'
@user = MailHandler.create_user_from_email(email)
if @user
logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
Mailer.deliver_account_information(@user, @user.password)
else
logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
return false
end
else
# Default behaviour, emails from unknown users are ignored
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
return false
end
end
User.current = @user
dispatch
end
@@ -82,15 +54,15 @@ class MailHandler < ActionMailer::Base
private
MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
def dispatch
headers = [email.in_reply_to, email.references].flatten.compact
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
klass, object_id = $1, $2.to_i
method_name = "receive_#{klass}_reply"
if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
if self.class.private_instance_methods.include?(method_name)
send method_name, object_id
else
# ignoring it
@@ -100,7 +72,7 @@ class MailHandler < ActionMailer::Base
elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
receive_message_reply(m[1].to_i)
else
dispatch_to_default
receive_issue
end
rescue ActiveRecord::RecordInvalid => e
# TODO: send a email to the user
@@ -113,54 +85,70 @@ class MailHandler < ActionMailer::Base
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
false
end
def dispatch_to_default
receive_issue
end
# Creates a new issue
def receive_issue
project = target_project
# check permission
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
end
tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
issue = Issue.new(:author => user, :project => project)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
issue.subject = email.subject.to_s.chomp[0,255]
if issue.subject.blank?
issue.subject = '(no subject)'
# check permission
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.subject = email.subject.chomp.toutf8
issue.description = plain_text_body
# custom fields
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
if value = get_keyword(c.name, :override => true)
h[c.id] = value
end
h
end
issue.description = cleaned_up_text_body
# add To and Cc as watchers before saving so the watchers can reply to Redmine
add_watchers(issue)
issue.save!
add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
# add To and Cc as watchers
add_watchers(issue)
# send notification after adding watchers so that they can reply to Redmine
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
issue
end
def target_project
# TODO: other ways to specify project:
# * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project)
target = Project.find_by_identifier(get_keyword(:project))
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Adds a note to an existing issue
def receive_issue_reply(issue_id)
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
issue = Issue.find_by_id(issue_id)
return unless issue
# check permission
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
end
# ignore CLI-supplied defaults for new issues
@@handler_options[:issue].clear
journal = issue.init_journal(user, cleaned_up_text_body)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
# add the note
journal = issue.init_journal(user, plain_text_body)
add_attachments(issue)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
journal
end
@@ -177,21 +165,16 @@ class MailHandler < ActionMailer::Base
message = Message.find_by_id(message_id)
if message
message = message.root
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
end
if !message.locked?
if user.allowed_to?(:add_messages, message.project) && !message.locked?
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
:content => cleaned_up_text_body)
:content => plain_text_body)
reply.author = user
reply.board = message.board
message.children << reply
add_attachments(reply)
reply
else
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
raise UnauthorizedAction
end
end
end
@@ -225,8 +208,8 @@ class MailHandler < ActionMailer::Base
@keywords[attr]
else
@keywords[attr] = begin
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
v
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}:[ \t]*(.+)\s*$/i, '')
$1.strip
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
end
@@ -234,65 +217,6 @@ class MailHandler < ActionMailer::Base
end
end
# Destructively extracts the value for +attr+ in +text+
# Returns nil if no matching keyword found
def extract_keyword!(text, attr, format=nil)
keys = [attr.to_s.humanize]
if attr.is_a?(Symbol)
keys << l("field_#{attr}", :default => '', :locale => user.language) if user
keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
end
keys.reject! {|k| k.blank?}
keys.collect! {|k| Regexp.escape(k)}
format ||= '.+'
text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
$2 && $2.strip
end
def target_project
# TODO: other ways to specify project:
# * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project)
target = Project.find_by_identifier(get_keyword(:project))
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Returns a Hash of issue attributes extracted from keywords in the email body
def issue_attributes_from_keywords(issue)
assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
attrs = {
'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id),
'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
'assigned_to_id' => assigned_to.try(:id),
'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
'estimated_hours' => get_keyword(:estimated_hours, :override => true),
'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
}.delete_if {|k, v| v.blank? }
if issue.new_record? && attrs['tracker_id'].nil?
attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
end
attrs
end
# Returns a Hash of issue custom field values extracted from keywords in the email body
def custom_field_values_from_keywords(customized)
customized.custom_field_values.inject({}) do |h, v|
if value = get_keyword(v.custom_field.name, :override => true)
h[v.custom_field.id.to_s] = value
end
h
end
end
# Returns the text/plain part of the email
# If not found (eg. HTML-only email), returns the body with tags removed
def plain_text_body
@@ -311,55 +235,5 @@ class MailHandler < ActionMailer::Base
@plain_text_body = plain_text_part.body.to_s
end
@plain_text_body.strip!
@plain_text_body
end
def cleaned_up_text_body
cleanup_body(plain_text_body)
end
def self.full_sanitizer
@full_sanitizer ||= HTML::FullSanitizer.new
end
# Creates a user account for the +email+ sender
def self.create_user_from_email(email)
addr = email.from_addrs.to_a.first
if addr && !addr.spec.blank?
user = User.new
user.mail = addr.spec
names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
user.firstname = names.shift
user.lastname = names.join(' ')
user.lastname = '-' if user.lastname.blank?
user.login = user.mail
user.password = ActiveSupport::SecureRandom.hex(5)
user.language = Setting.default_language
user.save ? user : nil
end
end
private
# Removes the email body of text after the truncation configurations.
def cleanup_body(body)
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
unless delimiters.empty?
regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
body = body.gsub(regex, '')
end
body.strip
end
def find_user_from_keyword(keyword)
user ||= User.find_by_mail(keyword)
user ||= User.find_by_login(keyword)
if user.nil? && keyword.match(/ /)
firstname, lastname = *(keyword.split) # "First Last Throwaway"
user ||= User.find_by_firstname_and_lastname(firstname, lastname)
end
user
end
end

View File

@@ -15,28 +15,13 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'ar_condition'
class Mailer < ActionMailer::Base
layout 'mailer'
helper :application
helper :issues
helper :custom_fields
include ActionController::UrlWriter
include Redmine::I18n
def self.default_url_options
h = Setting.host_name
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
{ :host => h, :protocol => Setting.protocol }
end
# Builds a tmail object used to email recipients of the added issue.
#
# Example:
# issue_add(issue) => tmail object
# Mailer.deliver_issue_add(issue) => sends an email to issue recipients
def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
@@ -48,16 +33,10 @@ class Mailer < ActionMailer::Base
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)
render_multipart('issue_add', body)
end
# Builds a tmail object used to email recipients of the edited issue.
#
# Example:
# issue_edit(journal) => tmail object
# Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
def issue_edit(journal)
issue = journal.journalized.reload
issue = journal.journalized
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
@@ -75,39 +54,25 @@ class Mailer < ActionMailer::Base
body :issue => issue,
:journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
render_multipart('issue_edit', body)
end
def reminder(user, issues, days)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_reminder, :count => issues.size, :days => days)
subject l(:mail_subject_reminder, issues.size)
body :issues => issues,
:days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
render_multipart('reminder', body)
end
# Builds a tmail object used to email users belonging to the added document's project.
#
# Example:
# document_added(document) => tmail object
# Mailer.deliver_document_added(document) => sends an email to the document's project recipients
def document_added(document)
redmine_headers 'Project' => document.project.identifier
recipients document.recipients
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)
render_multipart('document_added', body)
end
# Builds a tmail object used to email recipients of a project when an attachements are added.
#
# Example:
# attachments_added(attachments) => tmail object
# Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
def attachments_added(attachments)
container = attachments.first.container
added_to = ''
@@ -116,97 +81,41 @@ class Mailer < ActionMailer::Base
when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to = "#{l(:label_project)}: #{container}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
recipients container.recipients
end
redmine_headers 'Project' => container.project.identifier
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
render_multipart('attachments_added', body)
end
# Builds a tmail object used to email recipients of a news' project when a news item is added.
#
# Example:
# news_added(news) => tmail object
# Mailer.deliver_news_added(news) => sends an email to the news' project recipients
def news_added(news)
redmine_headers 'Project' => news.project.identifier
message_id news
recipients news.recipients
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)
render_multipart('news_added', body)
end
# Builds a tmail object used to email the recipients of the specified message that was posted.
#
# Example:
# message_posted(message) => tmail object
# Mailer.deliver_message_posted(message) => sends an email to the recipients
def message_posted(message)
def message_posted(message, recipients)
redmine_headers 'Project' => message.project.identifier,
'Topic-Id' => (message.parent_id || message.id)
message_id message
references message.parent unless message.parent.nil?
recipients(message.recipients)
cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
recipients(recipients)
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message,
:message_url => url_for(message.event_url)
render_multipart('message_posted', body)
end
# Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
#
# Example:
# wiki_content_added(wiki_content) => tmail object
# Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
def wiki_content_added(wiki_content)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title)
render_multipart('wiki_content_added', body)
end
# Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
#
# Example:
# wiki_content_updated(wiki_content) => tmail object
# Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
def wiki_content_updated(wiki_content)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title),
:wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :project_id => wiki_content.project, :id => wiki_content.page.title, :version => wiki_content.version)
render_multipart('wiki_content_updated', body)
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
end
# Builds a tmail object used to email the specified user their account information.
#
# Example:
# account_information(user, password) => tmail object
# Mailer.deliver_account_information(user, password) => sends account information to the user
def account_information(user, password)
set_language_if_valid user.language
recipients user.mail
@@ -214,35 +123,14 @@ class Mailer < ActionMailer::Base
body :user => user,
:password => password,
:login_url => url_for(:controller => 'account', :action => 'login')
render_multipart('account_information', body)
end
# Builds a tmail object used to email all active administrators of an account activation request.
#
# Example:
# account_activation_request(user) => tmail object
# Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
def account_activation_request(user)
# Send the email to all active administrators
recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
subject l(:mail_subject_account_activation_request, Setting.app_title)
body :user => user,
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
render_multipart('account_activation_request', body)
end
# Builds a tmail object used to email the specified user that their account was activated by an administrator.
#
# Example:
# account_activated(user) => tmail object
# Mailer.deliver_account_activated(user) => sends an email to the registered user
def account_activated(user)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_register, Setting.app_title)
body :user => user,
:login_url => url_for(:controller => 'account', :action => 'login')
render_multipart('account_activated', body)
end
def lost_password(token)
@@ -251,7 +139,6 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_lost_password, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
render_multipart('lost_password', body)
end
def register(token)
@@ -260,7 +147,6 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_register, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
render_multipart('register', body)
end
def test(user)
@@ -268,13 +154,11 @@ class Mailer < ActionMailer::Base
recipients user.mail
subject 'Redmine test'
body :url => url_for(:controller => 'welcome')
render_multipart('test', body)
end
# Overrides default deliver! method to prevent from sending an email
# with no recipient, cc or bcc
def deliver!(mail = @mail)
set_language_if_valid @initial_language
return false if (recipients.nil? || recipients.empty?) &&
(cc.nil? || cc.empty?) &&
(bcc.nil? || bcc.empty?)
@@ -286,21 +170,7 @@ class Mailer < ActionMailer::Base
if @references_objects
mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
end
# Log errors when raise_delivery_errors is set to false, Rails does not
raise_errors = self.class.raise_delivery_errors
self.class.raise_delivery_errors = true
begin
return super(mail)
rescue Exception => e
if raise_errors
raise e
elsif mylogger
mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/email.yml."
end
ensure
self.class.raise_delivery_errors = raise_errors
end
super(mail)
end
# Sends reminders to issue assignees
@@ -308,16 +178,13 @@ class Mailer < ActionMailer::Base
# * :days => how many days in the future to remind about (defaults to 7)
# * :tracker => id of tracker for filtering issues (defaults to all trackers)
# * :project => id or identifier of project to process (defaults to all projects)
# * :users => array of user ids who should be reminded
def self.reminders(options={})
days = options[:days] || 7
project = options[:project] ? Project.find(options[:project]) : nil
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
user_ids = options[:users]
s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
s << ["#{Issue.table_name}.assigned_to_id IN (?)", user_ids] if user_ids.present?
s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
s << "#{Issue.table_name}.project_id = #{project.id}" if project
s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
@@ -329,29 +196,23 @@ class Mailer < ActionMailer::Base
deliver_reminder(assignee, issues, days) unless assignee.nil?
end
end
# Activates/desactivates email deliveries during +block+
def self.with_deliveries(enabled = true, &block)
was_enabled = ActionMailer::Base.perform_deliveries
ActionMailer::Base.perform_deliveries = !!enabled
yield
ensure
ActionMailer::Base.perform_deliveries = was_enabled
end
private
def initialize_defaults(method_name)
super
@initial_language = current_language
set_language_if_valid Setting.default_language
from Setting.mail_from
# URL options
h = Setting.host_name
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
default_url_options[:host] = h
default_url_options[:protocol] = Setting.protocol
# Common headers
headers 'X-Mailer' => 'Redmine',
'X-Redmine-Host' => Setting.host_name,
'X-Redmine-Site' => Setting.app_title,
'Precedence' => 'bulk',
'Auto-Submitted' => 'auto-generated'
'X-Redmine-Site' => Setting.app_title
end
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
@@ -368,36 +229,35 @@ class Mailer < ActionMailer::Base
recipients.delete(@author.mail) if recipients
cc.delete(@author.mail) if cc
end
notified_users = [recipients, cc].flatten.compact.uniq
# Rails would log recipients only, not cc and bcc
mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger
# Blind carbon copy recipients
if Setting.bcc_recipients?
bcc(notified_users)
bcc([recipients, cc].flatten.compact.uniq)
recipients []
cc []
end
super
end
# Rails 2.3 has problems rendering implicit multipart messages with
# layouts so this method will wrap an multipart messages with
# explicit parts.
#
# https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
# https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
def render_multipart(method_name, body)
# Renders a message with the corresponding layout
def render_message(method_name, body)
layout = method_name.to_s.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}", :use_full_path => true)
end
# for the case of plain text only
def body(*params)
value = super(*params)
if Setting.plain_text_mail?
content_type "text/plain"
body render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
else
content_type "multipart/alternative"
part :content_type => "text/plain", :body => render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb')
part :content_type => "text/html", :body => render_message("#{method_name}.text.html.rhtml", body)
templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
unless String === @body or templates.empty?
template = File.basename(templates.first)
@body[:content_for_layout] = render(:file => template, :body => @body)
@body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
return @body
end
end
return value
end
# Makes partial rendering work with Rails 1.2 (retro-compatibility)
@@ -409,8 +269,7 @@ class Mailer < ActionMailer::Base
def self.message_id_for(object)
# id + timestamp should reduce the odds of a collision
# as far as we don't send multiple emails for the same object
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
host = "#{::Socket.gethostname}.redmine" if host.empty?
"<#{hash}@#{host}>"
@@ -426,10 +285,6 @@ class Mailer < ActionMailer::Base
@references_objects ||= []
@references_objects << object
end
def mylogger
RAILS_DEFAULT_LOGGER
end
end
# Patch TMail so that message_id is not overwritten

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# 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
@@ -17,80 +17,26 @@
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :principal, :foreign_key => 'user_id'
has_many :member_roles, :dependent => :destroy
has_many :roles, :through => :member_roles
belongs_to :role
belongs_to :project
validates_presence_of :principal, :project
validates_presence_of :role, :user, :project
validates_uniqueness_of :user_id, :scope => :project_id
after_destroy :unwatch_from_permission_change
def validate
errors.add :role_id, :activerecord_error_invalid if role && !role.member?
end
def name
self.user.name
end
alias :base_role_ids= :role_ids=
def role_ids=(arg)
ids = (arg || []).collect(&:to_i) - [0]
# Keep inherited roles
ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
new_role_ids = ids - role_ids
# Add new roles
new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
# Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
if member_roles_to_destroy.any?
member_roles_to_destroy.each(&:destroy)
unwatch_from_permission_change
end
end
def <=>(member)
a, b = roles.sort.first, member.roles.sort.first
a == b ? (principal <=> member.principal) : (a <=> b)
end
def deletable?
member_roles.detect {|mr| mr.inherited_from}.nil?
end
def include?(user)
if principal.is_a?(Group)
!user.nil? && user.groups.include?(principal)
else
self.user == user
end
role == member.role ? (user <=> member.user) : (role <=> member.role)
end
def before_destroy
if user
# remove category based auto assignments for this member
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
end
end
# Find or initilize a Member with an id, attributes, and for a Principal
def self.edit_membership(id, new_attributes, principal=nil)
@membership = id.present? ? Member.find(id) : Member.new(:principal => principal)
@membership.attributes = new_attributes
@membership
end
protected
def validate
errors.add_on_empty :role if member_roles.empty? && roles.empty?
end
private
# Unwatch things that the user is no longer allowed to view inside project
def unwatch_from_permission_change
if user
Watcher.prune(:user => user, :project => project)
end
# remove category based auto assignments for this member
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
end
end

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