Quellcode durchsuchen

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
nbc
Nicolas Chuche vor 15 Jahren
Ursprung
Commit
8c669eeb80
100 geänderte Dateien mit 1753 neuen und 900 gelöschten Zeilen
  1. 129
    41
      app/controllers/account_controller.rb
  2. 13
    14
      app/controllers/admin_controller.rb
  3. 15
    7
      app/controllers/application.rb
  4. 29
    10
      app/controllers/attachments_controller.rb
  5. 5
    3
      app/controllers/boards_controller.rb
  6. 15
    30
      app/controllers/custom_fields_controller.rb
  7. 1
    5
      app/controllers/documents_controller.rb
  8. 3
    0
      app/controllers/issue_relations_controller.rb
  9. 36
    34
      app/controllers/issues_controller.rb
  10. 1
    0
      app/controllers/journals_controller.rb
  11. 5
    3
      app/controllers/messages_controller.rb
  12. 1
    0
      app/controllers/my_controller.rb
  13. 52
    30
      app/controllers/projects_controller.rb
  14. 4
    4
      app/controllers/reports_controller.rb
  15. 21
    2
      app/controllers/repositories_controller.rb
  16. 0
    21
      app/controllers/roles_controller.rb
  17. 1
    1
      app/controllers/search_controller.rb
  18. 13
    9
      app/controllers/settings_controller.rb
  19. 27
    23
      app/controllers/sys_controller.rb
  20. 6
    1
      app/controllers/timelog_controller.rb
  21. 4
    0
      app/controllers/trackers_controller.rb
  22. 10
    6
      app/controllers/users_controller.rb
  23. 0
    6
      app/controllers/versions_controller.rb
  24. 6
    0
      app/controllers/welcome_controller.rb
  25. 24
    24
      app/controllers/wiki_controller.rb
  26. 45
    0
      app/controllers/workflows_controller.rb
  27. 9
    1
      app/helpers/admin_helper.rb
  28. 237
    107
      app/helpers/application_helper.rb
  29. 9
    4
      app/helpers/attachments_helper.rb
  30. 0
    85
      app/helpers/ifpdf_helper.rb
  31. 8
    0
      app/helpers/issues_helper.rb
  32. 35
    12
      app/helpers/projects_helper.rb
  33. 4
    2
      app/helpers/queries_helper.rb
  34. 6
    0
      app/helpers/repositories_helper.rb
  35. 1
    1
      app/helpers/search_helper.rb
  36. 1
    0
      app/helpers/settings_helper.rb
  37. 17
    6
      app/helpers/sort_helper.rb
  38. 3
    1
      app/helpers/timelog_helper.rb
  39. 3
    8
      app/helpers/users_helper.rb
  40. 0
    16
      app/helpers/wiki_helper.rb
  41. 3
    17
      app/helpers/workflows_helper.rb
  42. 18
    1
      app/models/attachment.rb
  43. 2
    1
      app/models/auth_source.rb
  44. 3
    1
      app/models/auth_source_ldap.rb
  45. 30
    17
      app/models/changeset.rb
  46. 38
    2
      app/models/custom_field.rb
  47. 12
    0
      app/models/custom_value.rb
  48. 7
    1
      app/models/document.rb
  49. 3
    1
      app/models/enumeration.rb
  50. 54
    25
      app/models/issue.rb
  51. 2
    0
      app/models/issue_relation.rb
  52. 2
    2
      app/models/issue_status.rb
  53. 1
    0
      app/models/journal.rb
  54. 110
    24
      app/models/mail_handler.rb
  55. 101
    31
      app/models/mailer.rb
  56. 12
    3
      app/models/message.rb
  57. 6
    5
      app/models/news.rb
  58. 64
    13
      app/models/project.rb
  59. 92
    64
      app/models/query.rb
  60. 44
    4
      app/models/repository.rb
  61. 1
    1
      app/models/repository/darcs.rb
  62. 20
    17
      app/models/repository/git.rb
  63. 3
    3
      app/models/repository/subversion.rb
  64. 2
    2
      app/models/role.rb
  65. 6
    2
      app/models/setting.rb
  66. 2
    2
      app/models/time_entry.rb
  67. 2
    2
      app/models/tracker.rb
  68. 40
    19
      app/models/user.rb
  69. 42
    5
      app/models/version.rb
  70. 19
    0
      app/models/wiki.rb
  71. 1
    0
      app/models/wiki_content.rb
  72. 5
    1
      app/models/wiki_page.rb
  73. 19
    0
      app/models/workflow.rb
  74. 6
    0
      app/views/account/login.rhtml
  75. 7
    2
      app/views/account/register.rhtml
  76. 47
    11
      app/views/account/show.rhtml
  77. 11
    1
      app/views/admin/index.rhtml
  78. 2
    17
      app/views/admin/info.rhtml
  79. 19
    0
      app/views/admin/plugins.rhtml
  80. 12
    14
      app/views/admin/projects.rhtml
  81. 3
    3
      app/views/attachments/_links.rhtml
  82. 6
    1
      app/views/auth_sources/list.rhtml
  83. 3
    5
      app/views/boards/index.rhtml
  84. 5
    4
      app/views/boards/show.rhtml
  85. 4
    1
      app/views/common/_diff.rhtml
  86. 1
    1
      app/views/common/feed.atom.rxml
  87. 8
    24
      app/views/custom_fields/_form.rhtml
  88. 0
    0
      app/views/custom_fields/index.rhtml
  89. 1
    1
      app/views/documents/index.rhtml
  90. 1
    1
      app/views/documents/show.rhtml
  91. 1
    1
      app/views/issues/_changesets.rhtml
  92. 1
    0
      app/views/issues/_edit.rhtml
  93. 13
    1
      app/views/issues/_form.rhtml
  94. 2
    0
      app/views/issues/_form_update.rhtml
  95. 13
    11
      app/views/issues/_history.rhtml
  96. 2
    2
      app/views/issues/_list.rhtml
  97. 7
    6
      app/views/issues/_list_simple.rhtml
  98. 3
    2
      app/views/issues/_relations.rhtml
  99. 10
    8
      app/views/issues/_sidebar.rhtml
  100. 0
    0
      app/views/issues/bulk_edit.rhtml

+ 129
- 41
app/controllers/account_controller.rb Datei anzeigen

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -24,13 +24,17 @@ class AccountController < ApplicationController

# Show user's account
def show
@user = User.find_active(params[:id])
@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
@@ -42,24 +46,10 @@ class AccountController < ApplicationController
self.logged_user = nil
else
# Authenticate user
user = User.try_to_login(params[:username], params[:password])
if user.nil?
# Invalid credentials
flash.now[:error] = l(:notice_account_invalid_creditentials)
elsif user.new_record?
# 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'
if Setting.openid? && using_open_id?
open_id_authenticate(params[:openid_url])
else
# Valid user
self.logged_user = user
# generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin?
token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end
redirect_back_or_default :controller => 'my', :action => 'page'
password_authentication
end
end
end
@@ -132,31 +122,14 @@ class AccountController < ApplicationController
else
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation]

case Setting.self_registration
when '1'
# Email activation
token = Token.new(:user => @user, :action => "register")
if @user.save and token.save
Mailer.deliver_register(token)
flash[:notice] = l(:notice_account_register_done)
redirect_to :action => 'login'
end
register_by_email_activation(@user)
when '3'
# Automatic activation
@user.status = User::STATUS_ACTIVE
if @user.save
self.logged_user = @user
flash[:notice] = l(:notice_account_activated)
redirect_to :controller => 'my', :action => 'account'
end
register_automatically(@user)
else
# Manual activation by the administrator
if @user.save
# Sends an email to the administrators
Mailer.deliver_account_activation_request(@user)
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
end
register_manually_by_administrator(@user)
end
end
end
@@ -187,4 +160,119 @@ private
session[:user_id] = nil
end
end
def password_authentication
user = User.try_to_login(params[:username], params[:password])
if user.nil?
# Invalid credentials
flash.now[:error] = l(:notice_account_invalid_creditentials)
elsif user.new_record?
# 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)
end
end

def open_id_authenticate(openid_url)
authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url) do |result, identity_url, registration|
if result.successful?
user = User.find_or_initialize_by_identity_url(identity_url)
if user.new_record?
# Self-registration off
redirect_to(home_url) && return unless Setting.self_registration?

# Create on the fly
user.login = registration['nickname'] unless registration['nickname'].nil?
user.mail = registration['email'] unless registration['email'].nil?
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
user.random_password
user.status = User::STATUS_REGISTERED

case Setting.self_registration
when '1'
register_by_email_activation(user) do
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
when '3'
register_automatically(user) do
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
else
register_manually_by_administrator(user) do
onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end
end
else
# Existing record
successful_authentication(user)
end
end
end
end
def successful_authentication(user)
# Valid user
self.logged_user = user
# generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin?
token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end
redirect_back_or_default :controller => 'my', :action => 'page'
end

# Onthefly creation failed, display the registration form to fill/fix attributes
def onthefly_creation_failed(user, auth_source_options = { })
@user = user
session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
render :action => 'register'
end

# Register a user for email activation.
#
# Pass a block for behavior when a user fails to save
def register_by_email_activation(user, &block)
token = Token.new(:user => user, :action => "register")
if user.save and token.save
Mailer.deliver_register(token)
flash[:notice] = l(:notice_account_register_done)
redirect_to :action => 'login'
else
yield if block_given?
end
end
# Automatically register a user
#
# Pass a block for behavior when a user fails to save
def register_automatically(user, &block)
# Automatic activation
user.status = User::STATUS_ACTIVE
if user.save
self.logged_user = user
flash[:notice] = l(:notice_account_activated)
redirect_to :controller => 'my', :action => 'account'
else
yield if block_given?
end
end
# Manual activation by the administrator
#
# Pass a block for behavior when a user fails to save
def register_manually_by_administrator(user, &block)
if user.save
# Sends an email to the administrators
Mailer.deliver_account_activation_request(user)
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
else
yield if block_given?
end
end
end

+ 13
- 14
app/controllers/admin_controller.rb Datei anzeigen

@@ -26,25 +26,24 @@ class AdminController < ApplicationController
end
def projects
sort_init 'name', 'asc'
sort_update
@status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
@status = params[:status] ? params[:status].to_i : 0
conditions = nil
conditions = ["status=?", @status] unless @status == 0
unless params[:name].blank?
name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
end
@project_count = Project.count(:conditions => conditions)
@project_pages = Paginator.new self, @project_count,
per_page_option,
params['page']
@projects = Project.find :all, :order => sort_clause,
:conditions => conditions,
:limit => @project_pages.items_per_page,
:offset => @project_pages.current.offset
@projects = Project.find :all, :order => 'lft',
:conditions => c.conditions

render :action => "projects", :layout => false if request.xhr?
end
def plugins
@plugins = Redmine::Plugin.all
end
# Loads the default configuration
# (roles, trackers, statuses, workflow, enumerations)
def default_configuration
@@ -78,8 +77,8 @@ class AdminController < ApplicationController
@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)
}
@plugins = Redmine::Plugin.registered_plugins
end
end

+ 15
- 7
app/controllers/application.rb Datei anzeigen

@@ -46,7 +46,7 @@ class ApplicationController < ActionController::Base
def find_current_user
if session[:user_id]
# existing session
(User.find_active(session[:user_id]) rescue nil)
(User.active.find(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature
User.find_by_autologin_key(cookies[:autologin])
@@ -82,7 +82,7 @@ class ApplicationController < ActionController::Base
def require_login
if !User.current.logged?
redirect_to :controller => "account", :action => "login", :back_url => (request.relative_url_root + request.request_uri)
redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
return false
end
true
@@ -126,10 +126,14 @@ class ApplicationController < ActionController::Base
def redirect_back_or_default(default)
back_url = CGI.unescape(params[:back_url].to_s)
if !back_url.blank?
uri = URI.parse(back_url)
# do not redirect user to another host
if uri.relative? || (uri.host == request.host)
redirect_to(back_url) and return
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
@@ -171,6 +175,7 @@ class ApplicationController < ActionController::Base
# 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']
@@ -179,7 +184,10 @@ class ApplicationController < ActionController::Base
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
attached << a unless a.new_record?
a.new_record? ? (unsaved << a) : (attached << a)
end
if unsaved.any?
flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
end
end
attached

+ 29
- 10
app/controllers/attachments_controller.rb Datei anzeigen

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -17,7 +17,11 @@

class AttachmentsController < ApplicationController
before_filter :find_project

before_filter :read_authorize, :except => :destroy
before_filter :delete_authorize, :only => :destroy
verify :method => :post, :only => :destroy
def show
if @attachment.is_diff?
@diff = File.new(@attachment.diskfile, "rb").read
@@ -25,31 +29,46 @@ class AttachmentsController < ApplicationController
elsif @attachment.is_text?
@content = File.new(@attachment.diskfile, "rb").read
render :action => 'file'
elsif
else
download
end
end
def download
@attachment.increment_download if @attachment.container.is_a?(Version)
if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
@attachment.increment_download
end
# images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
end
def destroy
# Make sure association callbacks are called
@attachment.container.attachments.delete(@attachment)
redirect_to :back
rescue ::ActionController::RedirectBackError
redirect_to :controller => 'projects', :action => 'show', :id => @project
end
private
def find_project
@attachment = Attachment.find(params[:id])
# Show 404 if the filename in the url is wrong
raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
@project = @attachment.project
permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
allowed = User.current.allowed_to?(permission, @project)
allowed ? true : (User.current.logged? ? render_403 : require_login)
rescue ActiveRecord::RecordNotFound
render_404
end
def read_authorize
@attachment.visible? ? true : deny_access
end
def delete_authorize
@attachment.deletable? ? true : deny_access
end
end

+ 5
- 3
app/controllers/boards_controller.rb Datei anzeigen

@@ -35,12 +35,14 @@ class BoardsController < ApplicationController
end

def show
sort_init "#{Message.table_name}.updated_on", "desc"
sort_update
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}",
@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

+ 15
- 30
app/controllers/custom_fields_controller.rb Datei anzeigen

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# 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
@@ -19,34 +19,22 @@ class CustomFieldsController < ApplicationController
before_filter :require_admin

def index
list
render :action => 'list' unless request.xhr?
end

def list
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name }
@tab = params[:tab] || 'IssueCustomField'
render :action => "list", :layout => false if request.xhr?
end
def new
case params[:type]
when "IssueCustomField"
@custom_field = IssueCustomField.new(params[:custom_field])
@custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids]
when "UserCustomField"
@custom_field = UserCustomField.new(params[:custom_field])
when "ProjectCustomField"
@custom_field = ProjectCustomField.new(params[:custom_field])
when "TimeEntryCustomField"
@custom_field = TimeEntryCustomField.new(params[:custom_field])
else
redirect_to :action => 'list'
return
end
@custom_field = begin
if params[:type].to_s.match(/.+CustomField$/)
params[:type].to_s.constantize.new(params[:custom_field])
end
rescue
end
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)
redirect_to :action => 'list', :tab => @custom_field.class.name
redirect_to :action => 'index', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
end
@@ -54,11 +42,8 @@ class CustomFieldsController < ApplicationController
def edit
@custom_field = CustomField.find(params[:id])
if request.post? and @custom_field.update_attributes(params[:custom_field])
if @custom_field.is_a? IssueCustomField
@custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : []
end
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list', :tab => @custom_field.class.name
redirect_to :action => 'index', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
end
@@ -75,14 +60,14 @@ class CustomFieldsController < ApplicationController
when 'lowest'
@custom_field.move_to_bottom
end if params[:position]
redirect_to :action => 'list', :tab => @custom_field.class.name
redirect_to :action => 'index', :tab => @custom_field.class.name
end
def destroy
@custom_field = CustomField.find(params[:id]).destroy
redirect_to :action => 'list', :tab => @custom_field.class.name
redirect_to :action => 'index', :tab => @custom_field.class.name
rescue
flash[:error] = "Unable to delete custom field"
redirect_to :action => 'list'
redirect_to :action => 'index'
end
end

+ 1
- 5
app/controllers/documents_controller.rb Datei anzeigen

@@ -35,6 +35,7 @@ class DocumentsController < ApplicationController
else
@grouped = documents.group_by(&:category)
end
@document = @project.documents.build
render :layout => false if request.xhr?
end
@@ -70,11 +71,6 @@ class DocumentsController < ApplicationController
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
redirect_to :action => 'show', :id => @document
end
def destroy_attachment
@document.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'show', :id => @document
end

private
def find_project

+ 3
- 0
app/controllers/issue_relations_controller.rb Datei anzeigen

@@ -21,6 +21,9 @@ class IssueRelationsController < ApplicationController
def new
@relation = IssueRelation.new(params[:relation])
@relation.issue_from = @issue
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 }

+ 36
- 34
app/controllers/issues_controller.rb Datei anzeigen

@@ -18,11 +18,11 @@
class IssuesController < ApplicationController
menu_item :new_issue, :only => :new
before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
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, :gantt, :calendar]
before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes]
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

helper :journals
@@ -30,8 +30,6 @@ class IssuesController < ApplicationController
include ProjectsHelper
helper :custom_fields
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issue_relations
include IssueRelationsHelper
helper :watchers
@@ -43,11 +41,13 @@ class IssuesController < ApplicationController
include SortHelper
include IssuesHelper
helper :timelog
include Redmine::Export::PDF

def index
sort_init "#{Issue.table_name}.id", "desc"
sort_update
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?
limit = per_page_option
respond_to do |format|
@@ -67,7 +67,7 @@ class IssuesController < ApplicationController
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
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
@@ -78,9 +78,10 @@ class IssuesController < ApplicationController
end
def changes
sort_init "#{Issue.table_name}.id", "desc"
sort_update
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,
@@ -104,7 +105,7 @@ class IssuesController < ApplicationController
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
end

@@ -121,7 +122,10 @@ class IssuesController < ApplicationController
render :nothing => true, :layout => true
return
end
@issue.attributes = params[:issue]
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
default_status = IssueStatus.default
@@ -143,7 +147,9 @@ class IssuesController < ApplicationController
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
redirect_to :controller => 'issues', :action => 'show', :id => @issue
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
@@ -176,9 +182,12 @@ class IssuesController < ApplicationController
@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 current_role.allowed_to?(:log_time)
if User.current.allowed_to?(:log_time, @project)
@time_entry.save
end
if !journal.new_record?
@@ -186,6 +195,7 @@ class IssuesController < ApplicationController
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
@@ -222,6 +232,7 @@ class IssuesController < ApplicationController
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|
@@ -233,6 +244,7 @@ class IssuesController < ApplicationController
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
@@ -253,7 +265,8 @@ class IssuesController < ApplicationController
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
: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 move
@@ -261,7 +274,7 @@ class IssuesController < ApplicationController
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
@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
@@ -273,7 +286,7 @@ class IssuesController < ApplicationController
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)
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?
@@ -310,17 +323,6 @@ class IssuesController < ApplicationController
@issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project
end

def destroy_attachment
a = @issue.attachments.find(params[:attachment_id])
a.destroy
journal = @issue.init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:old_value => a.filename)
journal.save
redirect_to :action => 'show', :id => @issue
end
def gantt
@gantt = Redmine::Helpers::Gantt.new(params)
@@ -348,8 +350,8 @@ class IssuesController < ApplicationController
respond_to do |format|
format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-gantt.pdf") }
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
@@ -450,9 +452,9 @@ private
end
def find_optional_project
return true unless params[:project_id]
@project = Project.find(params[:project_id])
authorize
@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

+ 1
- 0
app/controllers/journals_controller.rb Datei anzeigen

@@ -22,6 +22,7 @@ class JournalsController < ApplicationController
if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }

+ 5
- 3
app/controllers/messages_controller.rb Datei anzeigen

@@ -19,7 +19,7 @@ class MessagesController < ApplicationController
menu_item :boards
before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => :preview
before_filter :authorize, :except => [:preview, :edit, :destroy]

verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
verify :xhr => true, :only => :quote
@@ -30,7 +30,7 @@ class MessagesController < ApplicationController

# Show a topic and its replies
def show
@replies = @topic.children
@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?
@@ -65,7 +65,8 @@ class MessagesController < ApplicationController

# Edit a message
def edit
if params[:message] && User.current.allowed_to?(:edit_messages, @project)
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
@@ -78,6 +79,7 @@ class MessagesController < ApplicationController
# Delete a messages
def destroy
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 } :

+ 1
- 0
app/controllers/my_controller.rb Datei anzeigen

@@ -19,6 +19,7 @@ class MyController < ApplicationController
before_filter :require_login

helper :issues
helper :custom_fields

BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
'issuesreportedbyme' => :label_reported_issues,

+ 52
- 30
app/controllers/projects_controller.rb Datei anzeigen

@@ -29,12 +29,16 @@ class ProjectsController < ApplicationController
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 :ifpdf
include IfpdfHelper
helper :issues
helper IssuesHelper
helper :queries
@@ -45,17 +49,14 @@ class ProjectsController < ApplicationController
# Lists visible projects
def index
projects = Project.find :all,
:conditions => Project.visible_by(User.current),
:include => :parent
respond_to do |format|
format.html {
@project_tree = projects.group_by {|p| p.parent || p}
@project_tree.keys.each {|p| @project_tree[p] -= [p]}
@projects = Project.visible.find(:all, :order => 'lft')
}
format.atom {
render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
:title => "#{Setting.app_title}: #{l(:label_project_latest)}")
projects = Project.visible.find(:all, :order => 'created_on DESC',
:limit => Setting.feeds_limit.to_i)
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
}
end
end
@@ -64,9 +65,6 @@ class ProjectsController < ApplicationController
def add
@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')
@project = Project.new(params[:project])
if request.get?
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
@@ -76,6 +74,7 @@ class ProjectsController < ApplicationController
else
@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
@@ -84,20 +83,26 @@ class ProjectsController < ApplicationController
# Show @project
def show
if params[:jump]
# try to redirect to the requested menu item
redirect_to_project_menu_item(@project, params[:jump]) && return
end
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
@subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
@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
cond = @project.project_condition(Setting.display_subprojects_issues?)
Issue.visible_by(User.current) do
@open_issues_by_tracker = Issue.count(:group => :tracker,
@open_issues_by_tracker = Issue.visible.count(:group => :tracker,
:include => [:project, :status, :tracker],
:conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
@total_issues_by_tracker = Issue.count(:group => :tracker,
@total_issues_by_tracker = Issue.visible.count(:group => :tracker,
:include => [:project, :status, :tracker],
:conditions => cond)
end
TimeEntry.visible_by(User.current) do
@total_hours = TimeEntry.sum(:hours,
:include => :project,
@@ -107,9 +112,6 @@ class ProjectsController < ApplicationController
end

def settings
@root_projects = Project.find(:all,
:conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
:order => 'name')
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@issue_category ||= IssueCategory.new
@member ||= @project.members.new
@@ -123,6 +125,7 @@ class ProjectsController < ApplicationController
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
@@ -188,18 +191,26 @@ class ProjectsController < ApplicationController

def add_file
if request.post?
@version = @project.versions.find_by_id(params[:version_id])
attachments = attach_files(@version, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
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 "#{Attachment.table_name}.filename", "asc"
sort_update
@versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
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
@@ -221,16 +232,19 @@ class ProjectsController < ApplicationController
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date; rescue; end
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)
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.default_scope! if @activity.scope.empty?
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?

events = @activity.events(@date_from, @date_to)
@@ -240,10 +254,18 @@ class ProjectsController < ApplicationController
render :layout => false if request.xhr?
}
format.atom {
title = (@activity.scope.size == 1) ? l("label_#{@activity.scope.first.singularize}_plural") : l(:label_activity)
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

+ 4
- 4
app/controllers/reports_controller.rb Datei anzeigen

@@ -61,7 +61,7 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details"
when "subproject"
@field = "project_id"
@rows = @project.active_children
@rows = @project.descendants.active
@data = issues_by_subproject
@report_title = l(:field_subproject)
render :template => "reports/issue_report_details"
@@ -72,7 +72,7 @@ class ReportsController < ApplicationController
@categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }
@authors = @project.members.collect { |m| m.user }
@subprojects = @project.active_children
@subprojects = @project.descendants.active
issues_by_tracker
issues_by_version
issues_by_priority
@@ -229,8 +229,8 @@ private
#{Issue.table_name} i, #{IssueStatus.table_name} s
where
i.status_id=s.id
and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
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

+ 21
- 2
app/controllers/repositories_controller.rb Datei anzeigen

@@ -44,6 +44,21 @@ class RepositoriesController < ApplicationController
render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
end
def committers
@committers = @repository.committers
@users = @project.users
additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
@users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
@users.compact!
@users.sort!
if request.post? && params[:committers].is_a?(Hash)
# Build a hash with repository usernames as keys and corresponding user ids as values
@repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'committers', :id => @project
end
end
def destroy
@repository.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
@@ -73,7 +88,7 @@ class RepositoriesController < ApplicationController
def changes
@entry = @repository.entry(@path, @rev)
show_error_not_found and return unless @entry
@changesets = @repository.changesets_for_path(@path)
@changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
@properties = @repository.properties(@path, @rev)
end
@@ -84,7 +99,8 @@ class RepositoriesController < ApplicationController
params['page']
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset)
:offset => @changeset_pages.current.offset,
:include => :user)

respond_to do |format|
format.html { render :layout => false if request.xhr? }
@@ -111,6 +127,9 @@ class RepositoriesController < ApplicationController
end
def annotate
@entry = @repository.entry(@path, @rev)
show_error_not_found and return unless @entry
@annotate = @repository.scm.annotate(@path, @rev)
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
end

+ 0
- 21
app/controllers/roles_controller.rb Datei anzeigen

@@ -79,27 +79,6 @@ class RolesController < ApplicationController
redirect_to :action => 'list'
end
def workflow
@role = Role.find_by_id(params[:role_id])
@tracker = Tracker.find_by_id(params[:tracker_id])
if request.post?
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
(params[:issue_status] || []).each { |old, news|
news.each { |new|
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
}
}
if @role.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'workflow', :role_id => @role, :tracker_id => @tracker
end
end
@roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
end
def report
@roles = Role.find(:all, :order => 'builtin, position')
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }

+ 1
- 1
app/controllers/search_controller.rb Datei anzeigen

@@ -34,7 +34,7 @@ class SearchController < ApplicationController
when 'my_projects'
User.current.memberships.collect(&:project)
when 'subprojects'
@project ? ([ @project ] + @project.active_children) : nil
@project ? (@project.self_and_descendants.active) : nil
else
@project
end

+ 13
- 9
app/controllers/settings_controller.rb Datei anzeigen

@@ -5,19 +5,19 @@
# 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 SettingsController < ApplicationController
before_filter :require_admin
def index
edit
render :action => 'edit'
@@ -39,17 +39,21 @@ class SettingsController < ApplicationController
@options = {}
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
@deliveries = ActionMailer::Base.perform_deliveries

@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?
end
def plugin
plugin_id = params[:id].to_sym
@plugin = Redmine::Plugin.registered_plugins[plugin_id]
@plugin = Redmine::Plugin.find(params[:id])
if request.post?
Setting["plugin_#{plugin_id}"] = params[:settings]
Setting["plugin_#{@plugin.id}"] = params[:settings]
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => params[:id]
redirect_to :action => 'plugin', :id => @plugin.id
end
@partial = @plugin.settings[:partial]
@settings = Setting["plugin_#{plugin_id}"]
@settings = Setting["plugin_#{@plugin.id}"]
rescue Redmine::PluginNotFound
render_404
end
end

+ 27
- 23
app/controllers/sys_controller.rb Datei anzeigen

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# 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
@@ -16,31 +16,35 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class SysController < ActionController::Base
wsdl_service_name 'Sys'
web_service_api SysApi
web_service_scaffold :invoke
before_filter :check_enabled
before_invocation :check_enabled
# Returns the projects list, with their repositories
def projects_with_repository_enabled
Project.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
def projects
p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
render :xml => p.to_xml(:include => :repository)
end

# Registers a repository for the given project identifier
def repository_created(identifier, vendor, url)
project = Project.find_by_identifier(identifier)
# Do not create the repository if the project has already one
return 0 unless project && project.repository.nil?
logger.debug "Repository for #{project.name} was created"
repository = Repository.factory(vendor, :project => project, :url => url)
repository.save
repository.id || 0
def create_project_repository
project = Project.find(params[:id])
if project.repository
render :nothing => true, :status => 409
else
logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
project.repository = Repository.factory(params[:vendor], params[:repository])
if project.repository && project.repository.save
render :xml => project.repository, :status => 201
else
render :nothing => true, :status => 422
end
end
end

protected
protected

def check_enabled(name, args)
Setting.sys_api_enabled?
def check_enabled
User.current = nil
unless Setting.sys_api_enabled?
render :nothing => 'Access denied. Repository management WS is disabled.', :status => 403
return false
end
end
end

+ 6
- 1
app/controllers/timelog_controller.rb Datei anzeigen

@@ -138,7 +138,12 @@ class TimelogController < ApplicationController
def details
sort_init 'spent_on', 'desc'
sort_update
sort_update 'spent_on' => 'spent_on',
'user' => 'user_id',
'activity' => 'activity_id',
'project' => "#{Project.table_name}.name",
'issue' => 'issue_id',
'hours' => 'hours'
cond = ARCondition.new
if @project.nil?

+ 4
- 0
app/controllers/trackers_controller.rb Datei anzeigen

@@ -40,8 +40,10 @@ class TrackersController < ApplicationController
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
return
end
@trackers = Tracker.find :all, :order => 'position'
@projects = Project.find(:all)
end

def edit
@@ -49,7 +51,9 @@ class TrackersController < ApplicationController
if request.post? and @tracker.update_attributes(params[:tracker])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list'
return
end
@projects = Project.find(:all)
end

def move

+ 10
- 6
app/controllers/users_controller.rb Datei anzeigen

@@ -30,18 +30,22 @@ class UsersController < ApplicationController

def list
sort_init 'login', 'asc'
sort_update
sort_update %w(login firstname lastname mail admin created_on last_login_on)
@status = params[:status] ? params[:status].to_i : 1
conditions = "status <> 0"
conditions = ["status=?", @status] unless @status == 0
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 ?", name, name, name]
end
@user_count = User.count(:conditions => conditions)
@user_count = User.count(:conditions => c.conditions)
@user_pages = Paginator.new self, @user_count,
per_page_option,
params['page']
@users = User.find :all,:order => sort_clause,
:conditions => conditions,
:conditions => c.conditions,
:limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset

@@ -79,7 +83,7 @@ class UsersController < ApplicationController
end
@auth_sources = AuthSource.find(:all)
@roles = Role.find_all_givable
@projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
@projects = Project.active.find(:all, :order => 'lft')
@membership ||= Member.new
@memberships = @user.memberships
end

+ 0
- 6
app/controllers/versions_controller.rb Datei anzeigen

@@ -37,12 +37,6 @@ class VersionsController < ApplicationController
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def destroy_file
@version.attachments.find(params[:attachment_id]).destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
def status_by
respond_to do |format|
format.html { render :action => 'show' }

+ 6
- 0
app/controllers/welcome_controller.rb Datei anzeigen

@@ -16,9 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class WelcomeController < ApplicationController
caches_action :robots

def index
@news = News.latest User.current
@projects = Project.latest User.current
end
def robots
@projects = Project.public.active
render :layout => false, :content_type => 'text/plain'
end
end

+ 24
- 24
app/controllers/wiki_controller.rb Datei anzeigen

@@ -19,8 +19,9 @@ require 'diff'

class WikiController < ApplicationController
before_filter :find_wiki, :authorize
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }

helper :attachments
include AttachmentsHelper
@@ -44,11 +45,11 @@ class WikiController < ApplicationController
return
end
@content = @page.content_for_version(params[:version])
if params[:export] == 'html'
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[:export] == 'txt'
elsif params[:format] == 'txt'
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
return
end
@@ -63,7 +64,7 @@ class WikiController < ApplicationController
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version])
@content.text = "h1. #{@page.pretty_title}" if @content.text.blank?
@content.text = initial_page_content(@page) if @content.text.blank?
# don't keep previous comment
@content.comments = nil
if request.get?
@@ -91,8 +92,7 @@ class WikiController < ApplicationController
# rename a page
def rename
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
return render_403 unless editable?
@page.redirect_existing_links = true
# used to display the *original* title if some AR validation errors occur
@original_title = @page.pretty_title
@@ -103,15 +103,12 @@ class WikiController < ApplicationController
end
def protect
page = @wiki.find_page(params[:page])
page.update_attribute :protected, params[:protected]
redirect_to :action => 'index', :id => @project, :page => page.title
@page.update_attribute :protected, params[:protected]
redirect_to :action => 'index', :id => @project, :page => @page.title
end

# show page history
def history
@page = @wiki.find_page(params[:page])
@version_count = @page.content.versions.count
@version_pages = Paginator.new self, @version_count, per_page_option, params['p']
# don't load text
@@ -125,21 +122,19 @@ class WikiController < ApplicationController
end
def diff
@page = @wiki.find_page(params[:page])
@diff = @page.diff(params[:version], params[:version_from])
render_404 unless @diff
end
def annotate
@page = @wiki.find_page(params[:page])
@annotate = @page.annotate(params[:version])
render_404 unless @annotate
end
# remove a wiki page and its history
def destroy
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
@page.destroy if @page
return render_403 unless editable?
@page.destroy
redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end

@@ -181,19 +176,11 @@ class WikiController < ApplicationController
end

def add_attachment
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
attach_files(@page, params[:attachments])
redirect_to :action => 'index', :page => @page.title
end

def destroy_attachment
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
@page.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'index', :page => @page.title
end

private
def find_wiki
@@ -204,8 +191,21 @@ private
render_404
end
# Finds the requested page and returns a 404 error if it doesn't exist
def find_existing_page
@page = @wiki.find_page(params[:page])
render_404 if @page.nil?
end
# Returns true if the current user is allowed to edit the page, otherwise false
def editable?(page = @page)
page.editable_by?(User.current)
end

# Returns the default content of a new wiki page
def initial_page_content(page)
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page)
end
end

+ 45
- 0
app/controllers/workflows_controller.rb Datei anzeigen

@@ -0,0 +1,45 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class WorkflowsController < ApplicationController
before_filter :require_admin

def index
@workflow_counts = Workflow.count_by_tracker_and_role
end
def edit
@role = Role.find_by_id(params[:role_id])
@tracker = Tracker.find_by_id(params[:tracker_id])
if request.post?
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
(params[:issue_status] || []).each { |old, news|
news.each { |new|
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
}
}
if @role.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
end
end
@roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
end
end

+ 9
- 1
app/helpers/admin_helper.rb Datei anzeigen

@@ -17,7 +17,15 @@

module AdminHelper
def project_status_options_for_select(selected)
options_for_select([[l(:label_all), "*"],
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

+ 237
- 107
app/helpers/application_helper.rb Datei anzeigen

@@ -5,26 +5,32 @@
# 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 'coderay'
require 'coderay/helpers/file_type'
require 'forwardable'
require 'cgi'

module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
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)
@@ -34,7 +40,7 @@ module ApplicationHelper
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
# Display a link to remote if user is authorized
def link_to_remote_if_authorized(name, options = {}, html_options = nil)
url = options[:url] || {}
@@ -42,17 +48,17 @@ module ApplicationHelper
end

# Display a link to user's account page
def link_to_user(user)
user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
def link_to_user(user, options={})
(user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
end
def link_to_issue(issue, options={})
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.
# Options:
# * :text - Link text (default to attachment filename)
@@ -60,37 +66,37 @@ module ApplicationHelper
def link_to_attachment(attachment, options={})
text = options.delete(:text) || attachment.filename
action = options.delete(:download) ? 'download' : 'show'
link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
end
def toggle_link(name, id, options={})
onclick = "Element.toggle('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
onclick << "return false;"
link_to(name, "#", :onclick => onclick)
end
def image_to_function(name, function, html_options = {})
html_options.symbolize_keys!
tag(:input, html_options.merge({
:type => "image", :src => image_path(name),
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
tag(:input, html_options.merge({
:type => "image", :src => image_path(name),
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
}))
end
def prompt_to_remote(name, text, param, url, html_options = {})
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
link_to name, {}, html_options
end
def format_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)
@@ -100,43 +106,147 @@ module ApplicationHelper
@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, 100))
end
def format_activity_day(date)
date == Date.today ? l(:label_today).titleize : format_date(date)
end
def format_activity_description(text)
h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
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))
end
end

def render_page_hierarchy(pages, node=nil)
content = ''
if pages[node]
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
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"
end
content << "</ul>\n"
end
content
end
# Renders flash messages
def render_flash_messages
s = ''
flash.each do |k,v|
s << content_tag('div', v, :class => "flash #{k}")
end
s
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 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>'
s
end
end
def project_tree_options_for_select(projects, options = {})
s = ''
project_tree(projects) do |project, level|
name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
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
s
end
# Yields the given block for each project with its level in the tree
def 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)
s = ''
if projects.any?
ancestors = []
projects.sort_by(&:lft).each do |project|
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
s << "<ul>\n"
else
ancestors.pop
s << "</li>"
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
s << "</ul></li>\n"
end
end
s << "<li>"
s << yield(project).to_s
ancestors << project
end
s << ("</li></ul>\n" * ancestors.size)
end
s
end

# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
end
def html_hours(text)
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
end
def authoring(created, author)
time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))

def authoring(created, author, options={})
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(:label_added_time_by, author_tag, time_tag)
l(options[:label] || :label_added_time_by, author_tag, time_tag)
end
def l_or_humanize(s)
l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize

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
@@ -145,7 +255,7 @@ module ApplicationHelper
type = CodeRay::FileType[name]
type ? CodeRay.scan(content, type).html : h(content)
end
def to_path_param(path)
path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
end
@@ -153,53 +263,56 @@ module ApplicationHelper
def pagination_links_full(paginator, count=nil, options={})
page_param = options.delete(:page_param) || :page
url_param = params.dup
# don't reuse params if filters are present
url_param.clear if url_param.has_key?(:set_filter)
html = ''
html << link_to_remote(('&#171; ' + l(:label_previous)),
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.previous),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
# don't reuse query params if filters are present
url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)

html = ''
if paginator.current.previous
html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
end

html << (pagination_links_each(paginator, options) do |n|
link_to_remote(n.to_s,
{:url => {:params => url_param.merge(page_param => n)},
:update => 'content',
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => n))})
link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
end || '')
html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.next),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
if paginator.current.next
html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
end

unless count.nil?
html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
html << [
" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
per_page_links(paginator.items_per_page)
].compact.join(' | ')
end
html
html
end
def per_page_links(selected=nil)
url_param = params.dup
url_param.clear if url_param.has_key?(:set_filter)
links = Setting.per_page_options_array.collect do |n|
n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
n == selected ? n : link_to_remote(n, {:update => "content",
:url => params.dup.merge(:per_page => n),
:method => :get},
{:href => url_for(url_param.merge(:per_page => n))})
end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end
def breadcrumb(*args)
elements = args.flatten
elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
end
def other_formats_links(&block)
concat('<p class="other-formats">' + l(:label_export_to), block.binding)
yield Redmine::Views::OtherFormatsBuilder.new(self)
concat('</p>', block.binding)
end

def html_title(*args)
if args.empty?
title = []
@@ -234,32 +347,30 @@ module ApplicationHelper
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
only_path = options.delete(:only_path) == false ? false : true

# when using an image link, try to use an attachment, if possible
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
attachments = attachments.sort_by(&:created_on).reverse
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
style = $1
filename = $6
rf = Regexp.new(filename, Regexp::IGNORECASE)
filename = $6.downcase
# search for the picture in attachments
if found = attachments.detect { |att| att.filename =~ rf }
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(/^([^\(\)]*).*$/, "\\1")
alt = desc.blank? ? nil : "(#{desc})"
"!#{style}#{image_url}#{alt}!"
else
"!#{style}#{filename}!"
m
end
end
end
text = (Setting.text_formatting == 'textile') ?
Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
simple_format(auto_link(h(text)))

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]
@@ -272,11 +383,11 @@ module ApplicationHelper
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]]
@@ -294,7 +405,7 @@ module ApplicationHelper
page = $2
title ||= $1 if page.blank?
end
if link_project && link_project.wiki
# extract anchor
anchor = nil
@@ -307,7 +418,7 @@ module ApplicationHelper
:class => ('wiki-page' + (wiki_page ? '' : ' new')))
else
# project or wiki doesn't exist
title || page
all
end
else
all
@@ -315,7 +426,7 @@ module ApplicationHelper
end

# Redmine links
#
#
# Examples:
# Issues:
# #52 -> Link to issue #52
@@ -354,7 +465,7 @@ module ApplicationHelper
oid = oid.to_i
case prefix
when nil
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
:class => (issue.closed? ? 'issue closed' : 'issue'),
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
@@ -422,10 +533,10 @@ module ApplicationHelper
end
leading + (link || "#{prefix}#{sep}#{oid}")
end
text
end
# Same as Rails' simple_format helper without using paragraphs
def simple_format_without_paragraph(text)
text.to_s.
@@ -433,7 +544,7 @@ module ApplicationHelper
gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
end
def error_messages_for(object_name, options = {})
options = options.symbolize_keys
object = instance_variable_get("@#{object_name}")
@@ -451,14 +562,14 @@ module ApplicationHelper
end
# retrieve custom values error messages
if object.errors[:custom_values]
object.custom_values.each do |v|
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
end
content_tag("div",
content_tag(
options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
@@ -470,34 +581,35 @@ module ApplicationHelper
""
end
end
def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) +
(blank ? [["(auto)", ""]] : []) +
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 = {})
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
content_tag("label", label_text)
end
def labelled_tabular_form_for(name, object, options, &proc)
options[:html] ||= {}
options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
end
def back_url_hidden_field_tag
back_url = params[:back_url] || request.env['HTTP_REFERER']
hidden_field_tag('back_url', back_url) unless back_url.blank?
back_url = CGI.unescape(back_url.to_s)
hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
end
def check_all_links(form_name)
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
" | " +
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
end
def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts[1] = pcts[1] - pcts[0]
@@ -506,13 +618,13 @@ module ApplicationHelper
legend = options[:legend] || ''
content_tag('table',
content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
(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 context_menu_link(name, url, options={})
options[:class] ||= ''
if options.delete(:selected)
@@ -528,7 +640,7 @@ module ApplicationHelper
end
link_to name, url, options
end
def calendar_for(field_id)
include_calendar_headers_tags
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
@@ -546,26 +658,44 @@ module ApplicationHelper
end
end
end
def wikitoolbar_for(field_id)
return '' unless Setting.text_formatting == 'textile'
help_link = l(:setting_text_formatting) + ': ' +
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
:onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")

javascript_include_tag('jstoolbar/jstoolbar') +
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
end
def content_for(name, content = nil, &block)
@has_content ||= {}
@has_content[name] = true
super(name, content, &block)
end

def has_content?(name)
(@has_content && @has_content[name]) || false
end

# Returns the avatar image tag for the given +user+ if avatars are enabled
# +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?
email = nil
if user.respond_to?(:mail)
email = user.mail
elsif user.to_s =~ %r{<(.+?)>}
email = $1
end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
end
end

private

def wiki_helper
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
extend helper
return self
end
def link_to_remote_content_update(text, url_params)
link_to_remote(text,
{:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_params)}
)
end
end

+ 9
- 4
app/helpers/attachments_helper.rb Datei anzeigen

@@ -16,10 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

module AttachmentsHelper
# displays the links to a collection of attachments
def link_to_attachments(attachments, options = {})
if attachments.any?
render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
# Displays view/delete links to the attachments of the given object
# Options:
# :author -- author names are not displayed if set to false
def link_to_attachments(container, options = {})
options.assert_valid_keys(:author)
if container.attachments.any?
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
end
end

+ 0
- 85
app/helpers/ifpdf_helper.rb Datei anzeigen

@@ -1,85 +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.

require 'iconv'
require 'rfpdf/chinese'

module IfpdfHelper
class IFPDF < FPDF
include GLoc
attr_accessor :footer_date
def initialize(lang)
super()
set_language_if_valid lang
case current_language.to_s
when 'ja'
extend(PDF_Japanese)
AddSJISFont()
@font_for_content = 'SJIS'
@font_for_footer = 'SJIS'
when 'zh'
extend(PDF_Chinese)
AddGBFont()
@font_for_content = 'GB'
@font_for_footer = 'GB'
when 'zh-tw'
extend(PDF_Chinese)
AddBig5Font()
@font_for_content = 'Big5'
@font_for_footer = 'Big5'
else
@font_for_content = 'Arial'
@font_for_footer = 'Helvetica'
end
SetCreator("redMine #{Redmine::VERSION}")
SetFont(@font_for_content)
end
def SetFontStyle(style, size)
SetFont(@font_for_content, style, size)
end
def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
@ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
# these quotation marks are not correctly rendered in the pdf
txt = txt.gsub(/[“”]/, '"') if txt
txt = begin
# 0x5c char handling
txtar = txt.split('\\')
txtar << '' if txt[-1] == ?\\
txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
rescue
txt
end || ''
super w,h,txt,border,ln,align,fill,link
end
def Footer
SetFont(@font_for_footer, 'I', 8)
SetY(-15)
SetX(15)
Cell(0, 5, @footer_date, 0, 0, 'L')
SetY(-15)
SetX(-30)
Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
end
end

end

+ 8
- 0
app/helpers/issues_helper.rb Datei anzeigen

@@ -33,6 +33,14 @@ module IssuesHelper
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end
# 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
def sidebar_queries
unless @sidebar_queries
# User can see public queries and his own queries

+ 35
- 12
app/helpers/projects_helper.rb Datei anzeigen

@@ -21,18 +21,6 @@ module ProjectsHelper
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end
def format_activity_title(text)
h(truncate_single_line(text, 100))
end
def format_activity_day(date)
date == Date.today ? l(:label_today).titleize : format_date(date)
end
def format_activity_description(text)
h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
end
def project_settings_tabs
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
@@ -45,4 +33,39 @@ module ProjectsHelper
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
def parent_project_select_tag(project)
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
# The given collection may be a subset of the whole project tree
# (eg. some intermediate nodes are private and can not be seen)
def render_project_hierarchy(projects)
s = ''
if projects.any?
ancestors = []
projects.each do |project|
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
else
ancestors.pop
s << "</li>"
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
s << "</ul></li>\n"
end
end
classes = (ancestors.empty? ? 'root' : 'child')
s << "<li class='#{classes}'><div class='#{classes}'>" +
link_to(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)
end
s
end
end

+ 4
- 2
app/helpers/queries_helper.rb Datei anzeigen

@@ -22,8 +22,8 @@ module QueriesHelper
end
def column_header(column)
column.sortable ? sort_header_tag(column.sortable, :caption => column.caption,
:default_order => column.default_order) :
column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
:default_order => column.default_order) :
content_tag('th', column.caption)
end
@@ -44,6 +44,8 @@ module QueriesHelper
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
h(value)
end

+ 6
- 0
app/helpers/repositories_helper.rb Datei anzeigen

@@ -22,6 +22,12 @@ module RepositoriesHelper
txt.to_s[0,8]
end
def truncate_at_line_break(text, length = 255)
if text
text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
end
end
def render_properties(properties)
unless properties.nil? || properties.empty?
content = ''

+ 1
- 1
app/helpers/search_helper.rb Datei anzeigen

@@ -44,7 +44,7 @@ module SearchHelper
def project_select_tag
options = [[l(:label_project_all), 'all']]
options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty?
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
options << [@project.name, ''] unless @project.nil?
select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
end

+ 1
- 0
app/helpers/settings_helper.rb Datei anzeigen

@@ -18,6 +18,7 @@
module SettingsHelper
def administration_settings_tabs
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
{:name => 'display', :partial => 'settings/display', :label => :label_display},
{: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},

+ 17
- 6
app/helpers/sort_helper.rb Datei anzeigen

@@ -67,23 +67,31 @@ module SortHelper

# Updates the sort state. Call this in the controller prior to calling
# sort_clause.
#
def sort_update()
if params[:sort_key]
sort = {:key => params[:sort_key], :order => params[:sort_order]}
# 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()
session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
@sort_clause
end

# Returns a link which sorts by the named column.
@@ -112,8 +120,11 @@ module SortHelper
# 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
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},
{:update => "content", :url => url_options, :method => :get},
{:href => url_for(url_options)}) +
(icon ? nbsp(2) + image_tag(icon) : '')
end

+ 3
- 1
app/helpers/timelog_helper.rb Datei anzeigen

@@ -16,6 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

module TimelogHelper
include ApplicationHelper
def render_timelog_breadcrumb
links = []
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
@@ -81,7 +83,7 @@ module TimelogHelper
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
entries.each do |entry|
fields = [l_date(entry.spent_on),
fields = [format_date(entry.spent_on),
entry.user,
entry.activity,
entry.project,

+ 3
- 8
app/helpers/users_helper.rb Datei anzeigen

@@ -25,15 +25,10 @@ module UsersHelper
end
# Options for the new membership projects combo-box
def projects_options_for_select(projects)
def options_for_membership_project_select(user, projects)
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
projects_by_root = projects.group_by(&:root)
projects_by_root.keys.sort.each do |root|
options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root)))
projects_by_root[root].sort.each do |project|
next if project == root
options << content_tag('option', '&#187; ' + h(project.name), :value => project.id)
end
options << project_tree_options_for_select(projects) do |p|
{:disabled => (user.projects.include?(p))}
end
options
end

+ 0
- 16
app/helpers/wiki_helper.rb Datei anzeigen

@@ -16,22 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

module WikiHelper

def render_page_hierarchy(pages, node=nil)
content = ''
if pages[node]
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
content << link_to(h(page.pretty_title), {:action => 'index', :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"
end
content << "</ul>\n"
end
content
end
def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)}

app/apis/sys_api.rb → app/helpers/workflows_helper.rb Datei anzeigen

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -15,19 +15,5 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class AWSProjectWithRepository < ActionWebService::Struct
member :id, :int
member :identifier, :string
member :name, :string
member :is_public, :bool
member :repository, Repository
end

class SysApi < ActionWebService::API::Base
api_method :projects_with_repository_enabled,
:expects => [],
:returns => [[AWSProjectWithRepository]]
api_method :repository_created,
:expects => [:string, :string, :string],
:returns => [:int]
module WorkflowsHelper
end

+ 18
- 1
app/models/attachment.rb Datei anzeigen

@@ -30,12 +30,14 @@ class Attachment < ActiveRecord::Base

acts_as_activity_provider :type => 'files',
:permission => :view_files,
: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"}
acts_as_activity_provider :type => 'documents',
:permission => :view_documents,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
@@ -70,7 +72,7 @@ class Attachment < ActiveRecord::Base
File.open(diskfile, "wb") do |f|
f.write(@temp_file.read)
end
self.digest = Digest::MD5.hexdigest(File.read(diskfile))
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
@@ -96,6 +98,14 @@ class Attachment < ActiveRecord::Base
container.project
end
def visible?(user=User.current)
container.attachments_visible?(user)
end
def deletable?(user=User.current)
container.attachments_deletable?(user)
end
def image?
self.filename =~ /\.(jpe?g|gif|png)$/i
end
@@ -131,4 +141,11 @@ private
end
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
end
end

+ 2
- 1
app/models/auth_source.rb Datei anzeigen

@@ -38,7 +38,8 @@ class AuthSource < ActiveRecord::Base
begin
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
attrs = source.authenticate(login, password)
rescue
rescue => e
logger.error "Error during authentication: #{e.message}"
attrs = nil
end
return attrs if attrs

+ 3
- 1
app/models/auth_source_ldap.rb Datei anzeigen

@@ -91,6 +91,8 @@ class AuthSourceLdap < AuthSource
end
def self.get_attr(entry, attr_name)
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
if !attr_name.blank?
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
end
end
end

+ 30
- 17
app/models/changeset.rb Datei anzeigen

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -19,13 +19,13 @@ require 'iconv'

class Changeset < ActiveRecord::Base
belongs_to :repository
belongs_to :user
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues

acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
:description => :comments,
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,
:author => :committer,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments',
@@ -34,6 +34,7 @@ class Changeset < ActiveRecord::Base
:date_column => 'committed_on'
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => {:repository => :project}}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
@@ -57,6 +58,14 @@ class Changeset < ActiveRecord::Base
repository.project
end
def author
user || committer.to_s.split('<').first
end
def before_create
self.user = repository.find_committer_user(committer)
end
def after_create
scan_comment_for_issue_ids
end
@@ -96,12 +105,11 @@ class Changeset < ActiveRecord::Base
issue.reload
# don't change the status is the issue is closed
next if issue.status.is_closed?
user = committer_user || User.anonymous
csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
end
journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
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
@@ -113,15 +121,13 @@ class Changeset < ActiveRecord::Base
self.issues = referenced_issues.uniq
end

# Returns the Redmine User corresponding to the committer
def committer_user
if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3
u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank?
u
end
def short_comments
@short_comments || split_comments.first
end
def long_comments
@long_comments || split_comments.last
end
# Returns the previous changeset
@@ -140,7 +146,14 @@ class Changeset < ActiveRecord::Base
end
private

def split_comments
comments =~ /\A(.+?)\r?\n(.*)$/m
@short_comments = $1 || comments
@long_comments = $2.to_s.strip
return @short_comments, @long_comments
end

def self.to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
encoding = Setting.commit_logs_encoding.to_s.strip

+ 38
- 2
app/models/custom_field.rb Datei anzeigen

@@ -41,8 +41,6 @@ class CustomField < ActiveRecord::Base
end
def before_validation
# remove empty values
self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
# make sure these fields are not searchable
self.searchable = false if %w(int float date bool).include?(field_format)
true
@@ -59,11 +57,49 @@ class CustomField < ActiveRecord::Base
v.custom_field.is_required = false
errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
end
# Makes possible_values accept a multiline string
def possible_values=(arg)
if arg.is_a?(Array)
write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
else
self.possible_values = arg.to_s.split(/[\n\r]+/)
end
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.
def order_statement
case field_format
when 'string', 'text', 'list', 'date', 'bool'
# COALESCE is here to make sure that blank and NULL values are sorted equally
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
when 'int', 'float'
# Make the database cast values into numeric
# Postgresql will raise an error if a value can not be casted!
# CustomValue validations should ensure that it doesn't occur
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
else
nil
end
end

def <=>(field)
position <=> field.position
end
def self.customized_class
self.name =~ /^(.+)CustomField$/
begin; $1.constantize; rescue nil; end
end
# to move in project_custom_field
def self.for_all
find(:all, :conditions => ["is_for_all=?", true], :order => 'position')

+ 12
- 0
app/models/custom_value.rb Datei anzeigen

@@ -30,6 +30,18 @@ class CustomValue < ActiveRecord::Base
self.value == '1'
end
def editable?
custom_field.editable?
end
def required?
custom_field.is_required?
end
def to_s
value.to_s
end
protected
def validate
if value.blank?

+ 7
- 1
app/models/document.rb Datei anzeigen

@@ -18,7 +18,7 @@
class Document < ActiveRecord::Base
belongs_to :project
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable :delete_permission => :manage_documents

acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
@@ -28,4 +28,10 @@ class Document < ActiveRecord::Base
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
def after_initialize
if new_record?
self.category ||= Enumeration.default('DCAT')
end
end
end

+ 3
- 1
app/models/enumeration.rb Datei anzeigen

@@ -44,7 +44,9 @@ class Enumeration < ActiveRecord::Base
end

def before_save
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default?
if is_default? && is_default_changed?
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
end
end
def objects_count

+ 54
- 25
app/models/issue.rb Datei anzeigen

@@ -26,13 +26,13 @@ class Issue < ActiveRecord::Base
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'

has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
has_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_attachable :after_remove => :attachment_removed
acts_as_customizable
acts_as_watchable
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
@@ -40,15 +40,25 @@ class Issue < ActiveRecord::Base
# 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.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
: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]}
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
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

named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
# 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)
end
def after_initialize
if new_record?
# set default values for new records only
@@ -69,34 +79,43 @@ class Issue < ActiveRecord::Base
self
end
# Move an issue to a new project and tracker
def move_to(new_project, new_tracker = nil)
# Moves/copies an issue to a new project and tracker
# Returns the moved/copied issue on success, false on failure
def move_to(new_project, new_tracker = nil, options = {})
options ||= {}
issue = options[:copy] ? self.clone : self
transaction do
if new_project && project_id != new_project.id
if new_project && issue.project_id != new_project.id
# delete issue relations
unless Setting.cross_project_issue_relations?
self.relations_from.clear
self.relations_to.clear
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 = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
self.category = new_category
self.fixed_version = nil
self.project = new_project
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
self.tracker = 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 save
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
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
rollback_db_transaction
Issue.connection.rollback_db_transaction
return false
end
end
return true
return issue
end
def priority_id=(pid)
@@ -194,6 +213,11 @@ class Issue < ActiveRecord::Base
self.status.is_closed?
end
# Returns true if the issue is overdue
def overdue?
!due_date.nil? && (due_date < Date.today) && !status.is_closed?
end
# Users the issue can be assigned to
def assignable_users
project.assignable_users
@@ -251,13 +275,18 @@ class Issue < ActiveRecord::Base
@soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
end
def self.visible_by(usr)
with_scope(:find => { :conditions => Project.visible_by(usr) }) do
yield
end
end
def to_s
"#{tracker} ##{id}: #{subject}"
end
private
# Callback on attachment deletion
def attachment_removed(obj)
journal = init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => obj.id,
:old_value => obj.filename)
journal.save
end
end

+ 2
- 0
app/models/issue_relation.rb Datei anzeigen

@@ -35,6 +35,8 @@ class IssueRelation < ActiveRecord::Base
validates_numericality_of :delay, :allow_nil => true
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
attr_protected :issue_from_id, :issue_to_id
def validate
if issue_from && issue_to
errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id

+ 2
- 2
app/models/issue_status.rb Datei anzeigen

@@ -25,8 +25,8 @@ class IssueStatus < ActiveRecord::Base
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i

def before_save
IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default?
def after_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
end
# Returns the default status for new issues

+ 1
- 0
app/models/journal.rb Datei anzeigen

@@ -33,6 +33,7 @@ class Journal < ActiveRecord::Base

acts_as_activity_provider :type => 'issues',
:permission => :view_issues,
:author_key => :user_id,
:find_options => {:include => [{:issue => :project}, :details, :user],
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}

+ 110
- 24
app/models/mail_handler.rb Datei anzeigen

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

class MailHandler < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper

class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end
@@ -31,7 +32,7 @@ class MailHandler < ActionMailer::Base
@@handler_options[:allow_override] ||= []
# Project needs to be overridable if not specified
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
# Status needs to be overridable if not specified
# Status overridable by default
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
super email
end
@@ -39,7 +40,7 @@ class MailHandler < ActionMailer::Base
# Processes incoming emails
def receive(email)
@email = email
@user = User.find_active(:first, :conditions => {:mail => email.from.first})
@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
@@ -52,11 +53,24 @@ 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+)\]}
def dispatch
if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
receive_issue_update(m[1].to_i)
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.include?(method_name)
send method_name, object_id
else
# ignoring it
end
elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
receive_issue_reply(m[1].to_i)
elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
receive_message_reply(m[1].to_i)
else
receive_issue
end
@@ -78,16 +92,30 @@ class MailHandler < ActionMailer::Base
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))) || IssueStatus.default
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))

# check permission
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :status => status)
issue.subject = email.subject.chomp
issue.description = email.plain_text_body.chomp
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.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
@@ -102,7 +130,7 @@ class MailHandler < ActionMailer::Base
end
# Adds a note to an existing issue
def receive_issue_update(issue_id)
def receive_issue_reply(issue_id)
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
issue = Issue.find_by_id(issue_id)
@@ -112,15 +140,45 @@ class MailHandler < ActionMailer::Base
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)

# add the note
journal = issue.init_journal(user, email.plain_text_body.chomp)
journal = issue.init_journal(user, plain_text_body)
add_attachments(issue)
issue.status = status unless status.nil?
# 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
# Reply will be added to the issue
def receive_journal_reply(journal_id)
journal = Journal.find_by_id(journal_id)
if journal && journal.journalized_type == 'Issue'
receive_issue_reply(journal.journalized_id)
end
end
# Receives a reply to a forum message
def receive_message_reply(message_id)
message = Message.find_by_id(message_id)
if message
message = message.root
if user.allowed_to?(:add_messages, message.project) && !message.locked?
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
:content => plain_text_body)
reply.author = user
reply.board = message.board
message.children << reply
add_attachments(reply)
reply
else
raise UnauthorizedAction
end
end
end
def add_attachments(obj)
if email.has_attachments?
email.attachments.each do |attachment|
@@ -132,22 +190,50 @@ class MailHandler < ActionMailer::Base
end
end
def get_keyword(attr)
if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
$1.strip
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
# Adds To and Cc as watchers of the given object if the sender has the
# appropriate permission
def add_watchers(obj)
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
unless addresses.empty?
watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
watchers.each {|w| obj.add_watcher(w)}
end
end
end
end

class TMail::Mail
# Returns body of the first plain text part found if any
def get_keyword(attr, options={})
@keywords ||= {}
if @keywords.has_key?(attr)
@keywords[attr]
else
@keywords[attr] = begin
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
end
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
return @plain_text_body unless @plain_text_body.nil?
p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
plain = p.detect {|c| c.content_type == 'text/plain'}
@plain_text_body = plain.nil? ? self.body : plain.body
parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
if parts.empty?
parts << @email
end
plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
if plain_text_part.nil?
# no text/plain part found, assuming html-only email
# strip html tags and remove doctype directive
@plain_text_body = strip_tags(@email.body.to_s)
@plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
else
@plain_text_body = plain_text_part.body.to_s
end
@plain_text_body.strip!
end
end


+ 101
- 31
app/models/mailer.rb Datei anzeigen

@@ -5,12 +5,12 @@
# 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.
@@ -19,15 +19,17 @@ class Mailer < ActionMailer::Base
helper :application
helper :issues
helper :custom_fields
include ActionController::UrlWriter
def issue_add(issue)
def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients
message_id issue
recipients issue.recipients
cc(issue.watcher_recipients - @recipients)
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
@@ -39,6 +41,9 @@ class Mailer < ActionMailer::Base
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
message_id journal
references issue
@author = journal.user
recipients issue.recipients
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
@@ -50,16 +55,16 @@ class Mailer < ActionMailer::Base
:journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
def reminder(user, issues, days)
set_language_if_valid user.language
recipients user.mail
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 => 'issues.due_date', :sort_order => 'asc')
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
end
def document_added(document)
redmine_headers 'Project' => document.project.identifier
recipients document.project.recipients
@@ -67,12 +72,15 @@ class Mailer < ActionMailer::Base
body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
end
def attachments_added(attachments)
container = attachments.first.container
added_to = ''
added_to_url = ''
case container.class.name
when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to = "#{l(:label_project)}: #{container}"
when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}"
@@ -90,6 +98,7 @@ class Mailer < ActionMailer::Base

def news_added(news)
redmine_headers 'Project' => news.project.identifier
message_id news
recipients news.project.recipients
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
@@ -99,12 +108,14 @@ class Mailer < ActionMailer::Base
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(recipients)
subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message,
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
end
def account_information(user, password)
set_language_if_valid user.language
recipients user.mail
@@ -113,10 +124,10 @@ class Mailer < ActionMailer::Base
:password => password,
:login_url => url_for(:controller => 'account', :action => 'login')
end
def account_activation_request(user)
# Send the email to all active administrators
recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
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')
@@ -128,7 +139,7 @@ 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)
end
end

def register(token)
set_language_if_valid(token.user.language)
@@ -137,7 +148,7 @@ class Mailer < ActionMailer::Base
body :token => token,
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
end
def test(user)
set_language_if_valid(user.language)
recipients user.mail
@@ -148,12 +159,20 @@ class Mailer < ActionMailer::Base
# Overrides default deliver! method to prevent from sending an email
# with no recipient, cc or bcc
def deliver!(mail = @mail)
return false if (recipients.nil? || recipients.empty?) &&
return false if (recipients.nil? || recipients.empty?) &&
(cc.nil? || cc.empty?) &&
(bcc.nil? || bcc.empty?)
super
# Set Message-Id and References
if @message_id_object
mail.message_id = self.class.message_id_for(@message_id_object)
end
if @references_objects
mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
end
super(mail)
end
# Sends reminders to issue assignees
# Available options:
# * :days => how many days in the future to remind about (defaults to 7)
@@ -163,13 +182,13 @@ class Mailer < ActionMailer::Base
days = options[:days] || 7
project = options[:project] ? Project.find(options[:project]) : nil
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
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 << "#{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
issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
:conditions => s.conditions
).group_by(&:assigned_to)
@@ -183,45 +202,96 @@ class Mailer < ActionMailer::Base
super
set_language_if_valid Setting.default_language
from Setting.mail_from
default_url_options[:host] = Setting.host_name
# 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
end
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
def redmine_headers(h)
h.each { |k,v| headers["X-Redmine-#{k}"] = v }
end
# Overrides the create_mail method
def create_mail
# Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does
if User.current.pref[:no_self_notified]
recipients.delete(User.current.mail) if recipients
cc.delete(User.current.mail) if cc
@author ||= User.current
if @author.pref[:no_self_notified]
recipients.delete(@author.mail) if recipients
cc.delete(@author.mail) if cc
end
# Blind carbon copy recipients
if Setting.bcc_recipients?
bcc([recipients, cc].flatten.compact.uniq)
recipients []
cc []
end
end
super
end
# Renders a message with the corresponding layout
def render_message(method_name, body)
layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
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?
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)
def self.controller_path
''
end unless respond_to?('controller_path')
# Returns a predictable Message-Id for the given object
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
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}>"
end
private
def message_id(object)
@message_id_object = object
end
def references(object)
@references_objects ||= []
@references_objects << object
end
end

# Patch TMail so that message_id is not overwritten
module TMail
class Mail
def add_message_id( fqdn = nil )
self.message_id ||= ::TMail::new_message_id(fqdn)
end
end
end

+ 12
- 3
app/models/message.rb Datei anzeigen

@@ -19,11 +19,11 @@ class Message < ActiveRecord::Base
belongs_to :board
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_searchable :columns => ['subject', 'content'],
:include => {:board, :project},
:include => {:board => :project},
:project_key => 'project_id',
:date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
@@ -32,7 +32,8 @@ class Message < ActiveRecord::Base
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
{:id => o.parent_id, :anchor => "message-#{o.id}"})}

acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
:author_key => :author_id
acts_as_watchable
attr_protected :locked, :sticky
@@ -71,6 +72,14 @@ class Message < ActiveRecord::Base
def project
board.project
end

def editable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
end

def destroyable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
end
private

+ 6
- 5
app/models/news.rb Datei anzeigen

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -26,10 +26,11 @@ class News < ActiveRecord::Base

acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author]}
acts_as_activity_provider :find_options => {:include => [:project, :author]},
:author_key => :author_id
# returns latest news for projects visible by user
def self.latest(user=nil, count=5)
find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
def self.latest(user = User.current, count = 5)
find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
end
end

+ 64
- 13
app/models/project.rb Datei anzeigen

@@ -43,7 +43,9 @@ class Project < ActiveRecord::Base
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
:association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true
acts_as_nested_set :order => 'name', :dependent => :destroy
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files

acts_as_customizable
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
@@ -58,12 +60,15 @@ class Project < ActiveRecord::Base
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 3..20
validates_length_of :identifier, :in => 2..20
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
before_destroy :delete_all_members

named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
named_scope :public, { :conditions => { :is_public => true } }
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
def identifier=(identifier)
super unless identifier_frozen?
@@ -76,7 +81,7 @@ class Project < ActiveRecord::Base
def issues_with_subprojects(include_subprojects=false)
conditions = nil
if include_subprojects
ids = [id] + child_ids
ids = [id] + descendants.collect(&:id)
conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
end
conditions ||= ["#{Project.table_name}.id = ?", id]
@@ -108,9 +113,15 @@ class Project < ActiveRecord::Base
def self.allowed_to_condition(user, permission, options={})
statements = []
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if perm = Redmine::AccessControl.permission(permission)
unless perm.project_module.nil?
# If the permission belongs to a project module, make sure the module is enabled
base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
end
end
if options[:project]
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
base_statement = "(#{project_statement}) AND (#{base_statement})"
end
if user.admin?
@@ -133,7 +144,7 @@ class Project < ActiveRecord::Base
def project_condition(with_subprojects)
cond = "#{Project.table_name}.id = #{id}"
cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
cond
end
@@ -156,6 +167,7 @@ class Project < ActiveRecord::Base
self.status == STATUS_ACTIVE
end
# Archives the project and its descendants recursively
def archive
# Archive subprojects if any
children.each do |subproject|
@@ -164,21 +176,62 @@ class Project < ActiveRecord::Base
update_attribute :status, STATUS_ARCHIVED
end
# Unarchives the project
# All its ancestors must be active
def unarchive
return false if parent && !parent.active?
return false if ancestors.detect {|a| !a.active?}
update_attribute :status, STATUS_ACTIVE
end
def active_children
children.select {|child| child.active?}
# Returns an array of projects the project can be moved to
def possible_parents
@possible_parents ||= (Project.active.find(:all) - self_and_descendants)
end
# Sets the parent of the project
# Argument can be either a Project, a String, a Fixnum or nil
def set_parent!(p)
unless p.nil? || p.is_a?(Project)
if p.to_s.blank?
p = nil
else
p = Project.find_by_id(p)
return false unless p
end
end
if p == parent && !p.nil?
# Nothing to do
true
elsif p.nil? || (p.active? && move_possible?(p))
# Insert the project so that target's children or root projects stay alphabetically sorted
sibs = (p.nil? ? self.class.roots : p.children)
to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
if to_be_inserted_before
move_to_left_of(to_be_inserted_before)
elsif p.nil?
if sibs.empty?
# move_to_root adds the project in first (ie. left) position
move_to_root
else
move_to_right_of(sibs.last) unless self == sibs.last
end
else
# move_to_child_of adds the project in last (ie.right) position
move_to_child_of(p)
end
true
else
# Can not move to the given target
false
end
end
# Returns an array of the trackers used by the project and its sub projects
# Returns an array of the trackers used by the project and its active sub projects
def rolled_up_trackers
@rolled_up_trackers ||=
Tracker.find(:all, :include => :projects,
:select => "DISTINCT #{Tracker.table_name}.*",
:conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
:order => "#{Tracker.table_name}.position")
end
@@ -217,7 +270,7 @@ class Project < ActiveRecord::Base
# Returns a short description of the projects (first lines)
def short_description(length = 255)
description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
end
def allows_to?(action)
@@ -249,8 +302,6 @@ class Project < ActiveRecord::Base

protected
def validate
errors.add(parent_id, " must be a root project") if parent and parent.parent
errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
end

+ 92
- 64
app/models/query.rb Datei anzeigen

@@ -35,7 +35,7 @@ class QueryCustomFieldColumn < QueryColumn

def initialize(custom_field)
self.name = "cf_#{custom_field.id}".to_sym
self.sortable = false
self.sortable = custom_field.order_statement || false
@cf = custom_field
end
@@ -98,10 +98,10 @@ class Query < ActiveRecord::Base
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
QueryColumn.new(:author),
QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname"]),
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc'),
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
@@ -165,6 +165,10 @@ class Query < ActiveRecord::Base
end
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
if User.current.logged?
@available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
end
if project
# project specific filters
@@ -174,8 +178,8 @@ class Query < ActiveRecord::Base
unless @project.versions.empty?
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
end
unless @project.active_children.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
unless @project.descendants.active.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
end
add_custom_fields_filters(@project.all_issue_custom_fields)
else
@@ -257,7 +261,7 @@ class Query < ActiveRecord::Base
def project_statement
project_clauses = []
if project && !@project.active_children.empty?
if project && !@project.descendants.active.empty?
ids = [project.id]
if has_filter?("subproject_id")
case operator_for("subproject_id")
@@ -268,16 +272,16 @@ class Query < ActiveRecord::Base
# main project only
else
# all subprojects
ids += project.child_ids
ids += project.descendants.collect(&:id)
end
elsif Setting.display_subprojects_issues?
ids += project.child_ids
ids += project.descendants.collect(&:id)
end
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
elsif project
project_clauses << "#{Project.table_name}.id = %d" % project.id
end
project_clauses << Project.visible_by(User.current)
project_clauses << Project.allowed_to_condition(User.current, :view_issues)
project_clauses.join(' AND ')
end

@@ -288,74 +292,34 @@ class Query < ActiveRecord::Base
next if field == "subproject_id"
v = values_for(field).clone
next unless v and !v.empty?
operator = operator_for(field)
# "me" value subsitution
if %w(assigned_to_id author_id watcher_id).include?(field)
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
end
sql = ''
is_custom_filter = false
if field =~ /^cf_(\d+)$/
# custom field
db_table = CustomValue.table_name
db_field = 'value'
is_custom_filter = true
sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
elsif field == 'watcher_id'
db_table = Watcher.table_name
db_field = 'user_id'
sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
else
# regular field
db_table = Issue.table_name
db_field = field
sql << '('
end
# "me" value subsitution
if %w(assigned_to_id author_id).include?(field)
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
end
case operator_for field
when "="
sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!"
sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
sql = sql + "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
when "<="
sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
when "o"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
when "<t-"
sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
when "t-"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
when ">t+"
sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
when "<t+"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
when "t+"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
when "t"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
when "w"
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
when "!~"
sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
end
sql << ')'
filters_clauses << sql
end if filters and valid?
(filters_clauses << project_statement).join(' AND ')
@@ -363,6 +327,58 @@ class Query < ActiveRecord::Base
private
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
sql = ''
case operator
when "="
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!"
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
sql = "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
when "<="
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
when "o"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
when "<t-"
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
when "t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
when ">t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
when "<t+"
sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
when "t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
when "t"
sql = date_range_clause(db_table, db_field, 0, 0)
when "w"
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
when "!~"
sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
end
return sql
end
def add_custom_fields_filters(custom_fields)
@available_filters ||= {}
@@ -382,4 +398,16 @@ class Query < ActiveRecord::Base
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
end
end
# Returns a SQL clause for a date or datetime field.
def date_range_clause(table, field, from, to)
s = []
if from
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
end
if to
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
end
s.join(' AND ')
end
end

+ 44
- 4
app/models/repository.rb Datei anzeigen

@@ -78,11 +78,12 @@ class Repository < ActiveRecord::Base
end
# Default behaviour: we search in cached changesets
def changesets_for_path(path)
def changesets_for_path(path, options={})
path = "/#{path}" unless path.starts_with?('/')
Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
Change.find(:all, :include => {:changeset => :user},
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.id DESC",
:limit => options[:limit]).collect(&:changeset)
end
# Returns a path relative to the url of the repository
@@ -98,6 +99,45 @@ class Repository < ActiveRecord::Base
self.changesets.each(&:scan_comment_for_issue_ids)
end
# Returns an array of committers usernames and associated user_id
def committers
@committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
end
# Maps committers username to a user ids
def committer_ids=(h)
if h.is_a?(Hash)
committers.each do |committer, user_id|
new_user_id = h[committer]
if new_user_id && (new_user_id.to_i != user_id.to_i)
new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
end
end
@committers = nil
true
else
false
end
end
# Returns the Redmine User corresponding to the given +committer+
# It will return nil if the committer is not yet mapped and if no User
# with the same username or email was found
def find_committer_user(committer)
if committer
c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
if c && c.user
c.user
elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3
u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank?
u
end
end
end
# fetch new changesets for all repositories
# can be called periodically by an external script
# eg. ruby script/runner "Repository.fetch_changesets"

+ 1
- 1
app/models/repository/darcs.rb Datei anzeigen

@@ -52,7 +52,7 @@ class Repository::Darcs < Repository
end
def cat(path, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s)
scm.cat(path, patch.nil? ? nil : patch.scmid)
end

+ 20
- 17
app/models/repository/git.rb Datei anzeigen

@@ -40,10 +40,11 @@ class Repository::Git < Repository
'Git'
end

def changesets_for_path(path)
Change.find(:all, :include => :changeset,
def changesets_for_path(path, options={})
Change.find(:all, :include => {:changeset => :user},
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC",
:limit => options[:limit]).collect(&:changeset)
end

def fetch_changesets
@@ -58,20 +59,22 @@ class Repository::Git < Repository

unless changesets.find_by_scmid(scm_revision)
scm.revisions('', db_revision, nil, :reverse => true) do |revision|
transaction do
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
if changesets.find_by_scmid(revision.scmid.to_s).nil?
transaction do
changeset = Changeset.create!(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create!(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
end
end
end

+ 3
- 3
app/models/repository/subversion.rb Datei anzeigen

@@ -40,9 +40,9 @@ class Repository::Subversion < Repository
'Subversion'
end

def changesets_for_path(path)
revisions = scm.revisions(path)
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
def changesets_for_path(path, options={})
revisions = scm.revisions(path, nil, nil, :limit => options[:limit])
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
end
# Returns a path relative to the url of the repository

+ 2
- 2
app/models/role.rb Datei anzeigen

@@ -31,9 +31,9 @@ class Role < ActiveRecord::Base
raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
" FROM workflows" +
" FROM #{Workflow.table_name}" +
" WHERE role_id = #{role.id}"
end
end

+ 6
- 2
app/models/setting.rb Datei anzeigen

@@ -75,9 +75,9 @@ class Setting < ActiveRecord::Base
cattr_accessor :available_settings
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml"))
Redmine::Plugin.registered_plugins.each do |id, plugin|
Redmine::Plugin.all.each do |plugin|
next unless plugin.settings
@@available_settings["plugin_#{id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
@@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
end
validates_uniqueness_of :name
@@ -140,6 +140,10 @@ class Setting < ActiveRecord::Base
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
end
def self.openid?
Object.const_defined?(:OpenID) && self['openid'].to_s == '1'
end
# Checks if settings have changed since the values were read
# and clears the cache hash if it's the case
# Called once per request

+ 2
- 2
app/models/time_entry.rb Datei anzeigen

@@ -32,7 +32,7 @@ class TimeEntry < ActiveRecord::Base
:description => :comments
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true
validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
validates_length_of :comments, :maximum => 255, :allow_nil => true

def after_initialize
@@ -54,7 +54,7 @@ class TimeEntry < ActiveRecord::Base
end
def hours=(h)
write_attribute :hours, (h.is_a?(String) ? h.to_hours : h)
write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
end
# tyear, tmonth, tweek assigned where setting spent_on attributes

+ 2
- 2
app/models/tracker.rb Datei anzeigen

@@ -23,9 +23,9 @@ class Tracker < ActiveRecord::Base
raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker)
raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
" FROM workflows" +
" FROM #{Workflow.table_name}" +
" WHERE tracker_id = #{tracker.id}"
end
end

+ 40
- 19
app/models/user.rb Datei anzeigen

@@ -37,10 +37,14 @@ class User < ActiveRecord::Base
has_many :members, :dependent => :delete_all
has_many :projects, :through => :memberships
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_many :changesets, :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
belongs_to :auth_source
# Active non-anonymous users scope
named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
acts_as_customizable
attr_accessor :password, :password_confirmation
@@ -50,7 +54,7 @@ class User < ActiveRecord::Base
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
# Login must contain lettres, numbers, underscores only
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
validates_length_of :login, :maximum => 30
@@ -70,17 +74,19 @@ class User < ActiveRecord::Base
# update hashed_password if password was set
self.hashed_password = User.hash_password(self.password) if self.password
end

def self.active
with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
yield
end
def reload(*args)
@name = nil
super
end
def self.find_active(*args)
active do
find(*args)
def identity_url=(url)
begin
self.write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
rescue InvalidOpenId
# Invlaid url, don't save
end
self.read_attribute(:identity_url)
end
# Returns the user that matches provided login and password, or nil
@@ -119,8 +125,11 @@ class User < ActiveRecord::Base
# Return user's full name for display
def name(formatter = nil)
f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
eval '"' + f + '"'
if formatter
eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
else
@name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
end
end
def active?
@@ -138,13 +147,25 @@ class User < ActiveRecord::Base
def check_password?(clear_password)
User.hash_password(clear_password) == self.hashed_password
end

# Generate and set a random password. Useful for automated user creation
# Based on Token#generate_token_value
#
def random_password
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
password = ''
40.times { |i| password << chars[rand(chars.size-1)] }
self.password = password
self.password_confirmation = password
self
end
def pref
self.preference ||= UserPreference.new(:user => self)
end
def time_zone
@time_zone ||= (self.pref.time_zone.blank? ? nil : TimeZone[self.pref.time_zone])
@time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
end
def wants_comments_in_reverse_order?
@@ -178,15 +199,15 @@ class User < ActiveRecord::Base
token = Token.find_by_action_and_value('autologin', key)
token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
end
# Makes find_by_mail case-insensitive
def self.find_by_mail(mail)
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
end

# Sort users by their display names
def <=>(user)
if user.nil?
-1
elsif lastname.to_s.downcase == user.lastname.to_s.downcase
firstname.to_s.downcase <=> user.firstname.to_s.downcase
else
lastname.to_s.downcase <=> user.lastname.to_s.downcase
end
self.to_s.downcase <=> user.to_s.downcase
end
def to_s

+ 42
- 5
app/models/version.rb Datei anzeigen

@@ -19,7 +19,8 @@ class Version < ActiveRecord::Base
before_destroy :check_integrity
belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files

validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
@@ -50,20 +51,20 @@ class Version < ActiveRecord::Base
end
def completed_pourcent
if fixed_issues.count == 0
if issues_count == 0
0
elsif open_issues_count == 0
100
else
(closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count
issues_progress(false) + issues_progress(true)
end
end
def closed_pourcent
if fixed_issues.count == 0
if issues_count == 0
0
else
closed_issues_count * 100.0 / fixed_issues.count
issues_progress(false)
end
end
@@ -72,6 +73,11 @@ class Version < ActiveRecord::Base
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
end
# Returns assigned issues count
def issues_count
@issue_count ||= fixed_issues.count
end
def open_issues_count
@open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
end
@@ -103,4 +109,35 @@ private
def check_integrity
raise "Can't delete version" if self.fixed_issues.find(:first)
end
# Returns the average estimated time of assigned issues
# or 1 if no issue has an estimated time
# Used to weigth unestimated issues in progress calculation
def estimated_average
if @estimated_average.nil?
average = fixed_issues.average(:estimated_hours).to_f
if average == 0
average = 1
end
@estimated_average = average
end
@estimated_average
end
# Returns the total progress of open or closed issues
def issues_progress(open)
@issues_progress ||= {}
@issues_progress[open] ||= begin
progress = 0
if issues_count > 0
ratio = open ? 'done_ratio' : 100
done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
:include => :status,
:conditions => ["is_closed = ?", !open]).to_f
progress = done / (estimated_average * issues_count)
end
progress
end
end
end

+ 19
- 0
app/models/wiki.rb Datei anzeigen

@@ -43,6 +43,25 @@ class Wiki < ActiveRecord::Base
page
end
# Finds a page by title
# The given string can be of one of the forms: "title" or "project:title"
# Examples:
# Wiki.find_page("bar", project => foo)
# Wiki.find_page("foo:bar")
def self.find_page(title, options = {})
project = options[:project]
if title.to_s =~ %r{^([^\:]+)\:(.*)$}
project_identifier, title = $1, $2
project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
end
if project && project.wiki
page = project.wiki.find_page(title)
if page && page.content
page
end
end
end
# turn a string into a valid page title
def self.titleize(title)
# replace spaces with _ and remove unwanted caracters

+ 1
- 0
app/models/wiki_content.rb Datei anzeigen

@@ -37,6 +37,7 @@ class WikiContent < ActiveRecord::Base

acts_as_activity_provider :type => 'wiki_edits',
:timestamp => "#{WikiContent.versioned_table_name}.updated_on",
:author_key => "#{WikiContent.versioned_table_name}.author_id",
:permission => :view_wiki_edits,
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +

+ 5
- 1
app/models/wiki_page.rb Datei anzeigen

@@ -21,7 +21,7 @@ require 'enumerator'
class WikiPage < ActiveRecord::Base
belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
acts_as_tree :order => 'title'
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
@@ -111,6 +111,10 @@ class WikiPage < ActiveRecord::Base
def editable_by?(usr)
!protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
end
def attachments_deletable?(usr=User.current)
editable_by?(usr) && super(usr)
end
def parent_title
@parent_title || (self.parent && self.parent.pretty_title)

+ 19
- 0
app/models/workflow.rb Datei anzeigen

@@ -21,4 +21,23 @@ class Workflow < ActiveRecord::Base
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'

validates_presence_of :role, :old_status, :new_status
# Returns workflow transitions count by tracker and role
def self.count_by_tracker_and_role
counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id")
roles = Role.find(:all, :order => 'builtin, position')
trackers = Tracker.find(:all, :order => 'position')
result = []
trackers.each do |tracker|
t = []
roles.each do |role|
row = counts.detect {|c| c['role_id'] == role.id.to_s && c['tracker_id'] == tracker.id.to_s}
t << [role, (row.nil? ? 0 : row['c'].to_i)]
end
result << [tracker, t]
end
result
end
end

+ 6
- 0
app/views/account/login.rhtml Datei anzeigen

@@ -10,6 +10,12 @@
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
<td align="left"><%= password_field_tag 'password', nil, :size => 40 %></td>
</tr>
<% if Setting.openid? %>
<tr>
<td align="right"><label for="openid_url"><%=l(:field_identity_url)%></label></td>
<td align="left"><%= text_field_tag "openid_url" %></td>
</tr>
<% end %>
<tr>
<td></td>
<td align="left">

+ 7
- 2
app/views/account/register.rhtml Datei anzeigen

@@ -1,4 +1,4 @@
<h2><%=l(:label_register)%></h2>
<h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>

<% form_tag({:action => 'register'}, :class => "tabular") do %>
<%= error_messages_for 'user' %>
@@ -29,7 +29,12 @@
<p><label for="user_language"><%=l(:field_language)%></label>
<%= select("user", "language", lang_options_for_select) %></p>

<% @user.custom_field_values.each do |value| %>
<% if Setting.openid? %>
<p><label for="user_identity_url"><%=l(:field_identity_url)%></label>
<%= text_field 'user', 'identity_url' %></p>
<% end %>

<% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %>
<p><%= custom_field_tag_with_label :user, value %></p>
<% end %>
<!--[eoform:user]-->

+ 47
- 11
app/views/account/show.rhtml Datei anzeigen

@@ -2,19 +2,23 @@
<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %>
</div>

<h2><%=h @user.name %></h2>
<h2><%= avatar @user %> <%=h @user.name %></h2>

<p>
<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %>
<div class="splitcontentleft">
<ul>
<li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
<% for custom_value in @custom_values %>
<% if !custom_value.value.empty? %>
<% unless @user.pref.hide_mail %>
<li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li>
<% end %>
<% for custom_value in @custom_values %>
<% if !custom_value.value.empty? %>
<li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
<% end %>
<% end %>
<% end %>
<% end %>
<li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
<% unless @user.last_login_on.nil? %>
<li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li>
<% end %>
</ul>
</p>

<% unless @memberships.empty? %>
<h3><%=l(:label_project_plural)%></h3>
@@ -25,8 +29,40 @@
<% end %>
</ul>
<% end %>
</div>

<div class="splitcontentright">

<% unless @events_by_day.empty? %>
<h3><%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :user_id => @user, :from => @events_by_day.keys.first %></h3>

<h3><%=l(:label_activity)%></h3>
<p>
<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
</p>
</p>

<div id="activity">
<% @events_by_day.keys.sort.reverse.each do |day| %>
<h4><%= format_activity_day(day) %></h4>
<dl>
<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
<dt class="<%= e.event_type %>">
<span class="time"><%= format_time(e.event_datetime, false) %></span>
<%= content_tag('span', h(e.project), :class => 'project') %>
<%= link_to format_activity_title(e.event_title), e.event_url %></dt>
<dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd>
<% end -%>
</dl>
<% end -%>
</div>

<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :key => User.current.rss_key} %>
<% end %>

<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %>
<% end %>
<% end %>
</div>

<% html_title @user.name %>

+ 11
- 1
app/views/admin/index.rhtml Datei anzeigen

@@ -19,7 +19,7 @@
<p class="icon22 icon22-tracker">
<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> |
<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> |
<%= link_to l(:label_workflow), :controller => 'roles', :action => 'workflow' %>
<%= link_to l(:label_workflow), :controller => 'workflows', :action => 'edit' %>
</p>

<p class="icon22 icon22-workflow">
@@ -34,6 +34,16 @@
<%= link_to l(:label_settings), :controller => 'settings' %>
</p>

<% menu_items_for(:admin_menu) do |item, caption, url, selected| -%>
<%= content_tag 'p',
link_to(h(caption), item.url, item.html_options),
:class => ["icon22", "icon22-#{item.name}"].join(' ') %>
<% end -%>

<p class="icon22 icon22-plugin">
<%= link_to l(:label_plugins), :controller => 'admin', :action => 'plugins' %>
</p>

<p class="icon22 icon22-info">
<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %>
</p>

+ 2
- 17
app/views/admin/info.rhtml Datei anzeigen

@@ -4,24 +4,9 @@

<table class="list">
<tr class="odd"><td><%= l(:text_default_administrator_account_changed) %></td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_file_repository_writable) %></td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_file_repository_writable) %> (<%= Attachment.storage_path %>)</td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_plugin_assets_writable) %> (<%= Engines.public_directory %>)</td><td><%= image_tag (@flags[:plugin_assets_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="odd"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
</table>

<% if @plugins.any? %>
&nbsp;
<h3 class="icon22 icon22-plugin"><%= l(:label_plugins) %></h3>
<table class="list">
<% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %>
<tr class="<%= cycle('odd', 'even') %>">
<td><%=h @plugins[plugin].name %></td>
<td><%=h @plugins[plugin].description %></td>
<td><%=h @plugins[plugin].author %></td>
<td><%=h @plugins[plugin].version %></td>
<td><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %></td>
</tr>
<% end %>
</table>
<% end %>

<% html_title(l(:label_information_plural)) -%>

+ 19
- 0
app/views/admin/plugins.rhtml Datei anzeigen

@@ -0,0 +1,19 @@
<h2><%= l(:label_plugins) %></h2>

<% if @plugins.any? %>
<table class="list plugins">
<% @plugins.each do |plugin| %>
<tr class="<%= cycle('odd', 'even') %>">
<td><span class="name"><%=h plugin.name %></span>
<%= content_tag('span', h(plugin.description), :class => 'description') unless plugin.description.blank? %>
<%= content_tag('span', link_to(h(plugin.url), plugin.url), :class => 'url') unless plugin.url.blank? %>
</td>
<td class="author"><%= plugin.author_url.blank? ? h(plugin.author) : link_to(h(plugin.author), plugin.author_url) %></td>
<td class="version"><%=h plugin.version %></td>
<td class="configure"><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.id) if plugin.configurable? %></td>
</tr>
<% end %>
</table>
<% else %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>

+ 12
- 14
app/views/admin/projects.rhtml Datei anzeigen

@@ -4,33 +4,33 @@

<h2><%=l(:label_project_plural)%></h2>

<% form_tag() do %>
<% form_tag({}, :method => :get) do %>
<fieldset><legend><%= l(:label_filter_plural) %></legend>
<label><%= l(:field_status) %> :</label>
<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
<%= submit_tag l(:button_apply), :class => "small" %>
<label><%= l(:label_project) %>:</label>
<%= text_field_tag 'name', params[:name], :size => 30 %>
<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
</fieldset>
<% end %>
&nbsp;

<table class="list">
<thead><tr>
<%= sort_header_tag('name', :caption => l(:label_project)) %>
<th><%=l(:label_project)%></th>
<th><%=l(:field_description)%></th>
<th><%=l(:label_subproject_plural)%></th>
<%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %>
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
<th><%=l(:field_is_public)%></th>
<th><%=l(:field_created_on)%></th>
<th></th>
<th></th>
</tr></thead>
<tbody>
<% for project in @projects %>
<tr class="<%= cycle("odd", "even") %>">
<td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %>
<td><%= textilizable project.short_description, :project => project %>
<td align="center"><%= project.children.size %>
<td align="center"><%= image_tag 'true.png' if project.is_public? %>
<td align="center"><%= format_date(project.created_on) %>
<tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>">
<td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
<td><%= textilizable project.short_description, :project => project %></td>
<td align="center"><%= image_tag 'true.png' if project.is_public? %></td>
<td align="center"><%= format_date(project.created_on) %></td>
<td align="center" style="width:10%">
<small>
<%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
@@ -45,6 +45,4 @@
</tbody>
</table>

<p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p>

<% html_title(l(:label_project_plural)) -%>

+ 3
- 3
app/views/attachments/_links.rhtml Datei anzeigen

@@ -3,14 +3,14 @@
<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
<% if options[:delete_url] %>
<%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
<% if options[:deletable] %>
<%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment},
:confirm => l(:text_are_you_sure),
:method => :post,
:class => 'delete',
:title => l(:button_delete) %>
<% end %>
<% unless options[:no_author] %>
<% if options[:author] %>
<span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
<% end %>
</p>

+ 6
- 1
app/views/auth_sources/list.rhtml Datei anzeigen

@@ -9,6 +9,7 @@
<th><%=l(:field_name)%></th>
<th><%=l(:field_type)%></th>
<th><%=l(:field_host)%></th>
<th><%=l(:label_user_plural)%></th>
<th></th>
<th></th>
</tr></thead>
@@ -18,8 +19,12 @@
<td><%= link_to source.name, :action => 'edit', :id => source%></td>
<td align="center"><%= source.auth_method_name %></td>
<td align="center"><%= source.host %></td>
<td align="center"><%= source.users.count %></td>
<td align="center"><%= link_to l(:button_test), :action => 'test_connection', :id => source %></td>
<td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, :confirm => l(:text_are_you_sure), :class => "button-small" %></td>
<td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source },
:confirm => l(:text_are_you_sure),
:class => "button-small",
:disabled => source.users.any? %></td>
</tr>
<% end %>
</tbody>

+ 3
- 5
app/views/boards/index.rhtml Datei anzeigen

@@ -29,11 +29,9 @@
</tbody>
</table>

<p class="other-formats">
<%= l(:label_export_to) %>
<span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key},
:class => 'feed' %></span>
</p>
<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_messages => 1, :key => User.current.rss_key} %>
<% end %>

<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %>

+ 5
- 4
app/views/boards/show.rhtml Datei anzeigen

@@ -4,7 +4,7 @@
<%= link_to_if_authorized l(:label_message_new),
{:controller => 'messages', :action => 'new', :board_id => @board},
:class => 'icon icon-add',
:onclick => 'Element.show("add-message"); return false;' %>
:onclick => 'Element.show("add-message"); Form.Element.focus("message_subject"); return false;' %>
<%= watcher_tag(@board, User.current) %>
</div>

@@ -26,15 +26,16 @@
</div>

<h2><%=h @board.name %></h2>
<p class="subtitle"><%=h @board.description %></p>

<% if @topics.any? %>
<table class="list messages">
<thead><tr>
<th><%= l(:field_subject) %></th>
<th><%= l(:field_author) %></th>
<%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %>
<%= sort_header_tag("#{Message.table_name}.replies_count", :caption => l(:label_reply_plural)) %>
<%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %>
<%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
<%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %>
<%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %>
</tr></thead>
<tbody>
<% @topics.each do |topic| %>

+ 4
- 1
app/views/common/_diff.rhtml Datei anzeigen

@@ -1,4 +1,5 @@
<% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%>
<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%>
<% diff.each do |table_file| -%>
<div class="autoscroll">
<% if diff_type == 'sbs' -%>
<table class="filecontent CodeRay">
@@ -62,3 +63,5 @@

</div>
<% end -%>

<%= l(:text_diff_truncated) if diff.truncated? %>

+ 1
- 1
app/views/common/feed.atom.rxml Datei anzeigen

@@ -6,7 +6,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.id url_for(:controller => 'welcome', :only_path => false)
xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
xml.author { xml.name "#{Setting.app_title}" }
xml.generator(:uri => Redmine::Info.url, :version => Redmine::VERSION) { xml.text! Redmine::Info.versioned_name; }
xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
@items.each do |item|
xml.entry do
url = url_for(item.event_url(:only_path => false))

+ 8
- 24
app/views/custom_fields/_form.rhtml Datei anzeigen

@@ -49,23 +49,6 @@ function toggle_custom_field_format() {
}
}

function addValueField() {
var f = $$('p#custom_field_possible_values span');
p = document.getElementById("custom_field_possible_values");
var v = f[0].cloneNode(true);
v.childNodes[0].value = "";
p.appendChild(v);
}

function deleteValueField(e) {
var f = $$('p#custom_field_possible_values span');
if (f.length == 1) {
e.parentNode.childNodes[0].value = "";
} else {
Element.remove(e.parentNode);
}
}

//]]>
</script>

@@ -76,22 +59,22 @@ function deleteValueField(e) {
<%= f.text_field :min_length, :size => 5, :no_label => true %> -
<%= f.text_field :max_length, :size => 5, :no_label => true %><br>(<%=l(:text_min_max_length_info)%>)</p>
<p><%= f.text_field :regexp, :size => 50 %><br>(<%=l(:text_regexp_info)%>)</p>
<p id="custom_field_possible_values"><label><%= l(:field_possible_values) %> <%= image_to_function "add.png", "addValueField();return false" %></label>
<% (@custom_field.possible_values.to_a + [""]).each do |value| %>
<span><%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %><br /></span>
<% end %>
</p>
<p id="custom_field_possible_values"><%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"),
:cols => 20,
:rows => 15 %>
<br /><em><%= l(:text_custom_field_possible_values_info) %></em></p>
<p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p>
</div>

<div class="box">
<% case @custom_field.type.to_s
<% case @custom_field.class.name
when "IssueCustomField" %>
<fieldset><legend><%=l(:label_tracker_plural)%></legend>
<% for tracker in @trackers %>
<%= check_box_tag "tracker_ids[]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %>
<%= check_box_tag "custom_field[tracker_ids][]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %>
<% end %>
<%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
</fieldset>
&nbsp;
<p><%= f.check_box :is_required %></p>
@@ -101,6 +84,7 @@ when "IssueCustomField" %>
<% when "UserCustomField" %>
<p><%= f.check_box :is_required %></p>
<p><%= f.check_box :editable %></p>

<% when "ProjectCustomField" %>
<p><%= f.check_box :is_required %></p>

app/views/custom_fields/list.rhtml → app/views/custom_fields/index.rhtml Datei anzeigen


+ 1
- 1
app/views/documents/index.rhtml Datei anzeigen

@@ -2,7 +2,7 @@
<%= link_to_if_authorized l(:label_document_new),
{:controller => 'documents', :action => 'new', :project_id => @project},
:class => 'icon icon-add',
:onclick => 'Element.show("add-document"); return false;' %>
:onclick => 'Element.show("add-document"); Form.Element.focus("document_title"); return false;' %>
</div>

<div id="add-document" style="display:none;">

+ 1
- 1
app/views/documents/show.rhtml Datei anzeigen

@@ -12,7 +12,7 @@
</div>

<h3><%= l(:label_attachment_plural) %></h3>
<%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %>
<%= link_to_attachments @document %>

<% if authorize_for('documents', 'add_attachment') %>
<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",

+ 1
- 1
app/views/issues/_changesets.rhtml Datei anzeigen

@@ -2,7 +2,7 @@
<div class="changeset <%= cycle('odd', 'even') %>">
<p><%= link_to("#{l(:label_revision)} #{changeset.revision}",
:controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %><br />
<span class="author"><%= authoring(changeset.committed_on, changeset.committer) %></span></p>
<span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
<%= textilizable(changeset, :comments) %>
</div>
<% end %>

+ 1
- 0
app/views/issues/_edit.rhtml Datei anzeigen

@@ -35,6 +35,7 @@
<fieldset><legend><%= l(:field_notes) %></legend>
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
<%= wikitoolbar_for 'notes' %>
<%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
<p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
</fieldset>

+ 13
- 1
app/views/issues/_form.rhtml Datei anzeigen

@@ -8,13 +8,14 @@

<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
<p><%= f.text_field :subject, :size => 80, :required => true %></p>
<p><%= f.text_area :description, :required => true,
<p><%= f.text_area :description,
:cols => 60,
:rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
:accesskey => accesskey(:edit),
:class => 'wiki-edit' %></p>
</div>

<div class="attributes">
<div class="splitcontentleft">
<% if @issue.new_record? || @allowed_statuses.any? %>
<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
@@ -24,11 +25,13 @@

<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
<% unless @project.issue_categories.empty? %>
<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
<%= prompt_to_remote(l(:label_issue_category_new),
l(:label_issue_category_new), 'category[name]',
{:controller => 'projects', :action => 'add_issue_category', :id => @project},
:class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
<% end %>
<%= content_tag('p', f.select(:fixed_version_id,
(@project.versions.sort.collect {|v| [v.name, v.id]}),
{ :include_blank => true })) unless @project.versions.empty? %>
@@ -43,11 +46,20 @@

<div style="clear:both;"> </div>
<%= render :partial => 'form_custom_fields' %>
</div>

<% if @issue.new_record? %>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
<% end %>

<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%>
<p><label><%= l(:label_issue_watchers) %></label>
<% @issue.project.users.sort.each do |user| -%>
<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label>
<% end -%>
</p>
<% end %>

<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>

<%= wikitoolbar_for 'issue_description' %>

+ 2
- 0
app/views/issues/_form_update.rhtml Datei anzeigen

@@ -1,3 +1,4 @@
<div class="attributes">
<div class="splitcontentleft">
<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
@@ -8,3 +9,4 @@
(@project.versions.sort.collect {|v| [v.name, v.id]}),
{ :include_blank => true })) unless @project.versions.empty? %>
</div>
</div>

+ 13
- 11
app/views/issues/_history.rhtml Datei anzeigen

@@ -1,14 +1,16 @@
<% reply_links = authorize_for('issues', 'edit') -%>
<% for journal in journals %>
<div id="change-<%= journal.id %>" class="journal">
<h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
<%= content_tag('a', '', :name => "note-#{journal.indice}")%>
<%= format_time(journal.created_on) %> - <%= journal.user.name %></h4>
<ul>
<% for detail in journal.details %>
<li><%= show_detail(detail) %></li>
<% end %>
</ul>
<%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %>
</div>
<div id="change-<%= journal.id %>" class="journal">
<h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
<%= content_tag('a', '', :name => "note-#{journal.indice}")%>
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4>
<%= avatar(journal.user, :size => "32") %>
<ul>
<% for detail in journal.details %>
<li><%= show_detail(detail) %></li>
<% end %>
</ul>
<%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %>
</div>
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
<% end %>

+ 2
- 2
app/views/issues/_list.rhtml Datei anzeigen

@@ -4,14 +4,14 @@
<th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
</th>
<%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %>
<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
<% query.columns.each do |column| %>
<%= column_header(column) %>
<% end %>
</tr></thead>
<tbody>
<% issues.each do |issue| -%>
<tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>">
<td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
<% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>

+ 7
- 6
app/views/issues/_list_simple.rhtml Datei anzeigen

@@ -3,21 +3,22 @@
<table class="list issues">
<thead><tr>
<th>#</th>
<th><%=l(:field_project)%></th>
<th><%=l(:field_tracker)%></th>
<th><%=l(:field_subject)%></th>
</tr></thead>
<tbody>
<% for issue in issues %>
<tr id="issue-<%= issue.id %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>">
<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>">
<td class="id">
<%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
<%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
<%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
</td>
<td><%=h issue.project.name %> - <%= issue.tracker.name %><br />
<%= issue.status.name %> - <%= format_time(issue.updated_on) %></td>
<td class="project"><%=h issue.project %></td>
<td class="tracker"><%=h issue.tracker %></td>
<td class="subject">
<%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %>
</td>
<%= link_to h(truncate(issue.subject, 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>)
</td>
</tr>
<% end %>
</tbody>

+ 3
- 2
app/views/issues/_relations.rhtml Datei anzeigen

@@ -8,9 +8,10 @@

<% if @issue.relations.any? %>
<table style="width:100%">
<% @issue.relations.each do |relation| %>
<% @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation| %>
<tr>
<td><%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %> <%= link_to_issue relation.other_issue(@issue) %></td>
<td><%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %>
<%= h(relation.other_issue(@issue).project) + ' - ' if Setting.cross_project_issue_relations? %> <%= link_to_issue relation.other_issue(@issue) %></td>
<td><%=h relation.other_issue(@issue).subject %></td>
<td><%= relation.other_issue(@issue).status.name %></td>
<td><%= format_date(relation.other_issue(@issue).start_date) %></td>

+ 10
- 8
app/views/issues/_sidebar.rhtml Datei anzeigen

@@ -2,23 +2,25 @@
<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
<% if @project %>
<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %><br />
<% end %>
<%= call_hook(:view_issues_sidebar_issues_bottom) %>

<% planning_links = []
planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :project_id => @project)
planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :project_id => @project)
planning_links.compact!
unless planning_links.empty? %>
planning_links << link_to(l(:label_calendar), :action => 'calendar', :project_id => @project) if User.current.allowed_to?(:view_calendar, @project, :global => true)
planning_links << link_to(l(:label_gantt), :action => 'gantt', :project_id => @project) if User.current.allowed_to?(:view_gantt, @project, :global => true)
%>
<% unless planning_links.empty? %>
<h3><%= l(:label_planning) %></h3>
<p><%= planning_links.join(' | ') %></p>
<% end %>

<%= call_hook(:view_issues_sidebar_planning_bottom) %>
<% end %>

<% unless sidebar_queries.empty? -%>
<h3><%= l(:label_query_plural) %></h3>

<% sidebar_queries.each do |query| -%>
<%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
<%= link_to(h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query) %><br />
<% end -%>
<%= call_hook(:view_issues_sidebar_queries_bottom) %>
<% end -%>

+ 0
- 0
app/views/issues/bulk_edit.rhtml Datei anzeigen


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.

Laden…
Abbrechen
Speichern