Compare commits

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

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


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1791 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 13:01:40 +00:00
Nicolas Chuche 473869db6b recreate new nbc branch from trunk
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1790 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 12:22:22 +00:00
Nicolas Chuche c5d5045c83 r17045@gaspard (orig r916): nbc | 2007-11-18 19:51:48 +0100
* add Redmine.pm to authenticate with mod_perl
 * add a --test option in reposman.rb
 * change owner right to fit with apache write access to repositories
 * add a deprecated warning in reposman.pl
 


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1788 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-08 12:18:13 +00:00
Nicolas Chuche 1bb237c3e0 r17044@gaspard (orig r915): jplang | 2007-11-18 18:46:55 +0100
There's now 3 account activation strategies (available in application settings):
 * activation by email: the user receives an email containing a link to active his account
 * manual activation: an email is sent to administrators for account approval (default)
 * automatic activation: the user can log in as soon as he has registered


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1781 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-01 17:52:12 +00:00
Nicolas Chuche 77f65a81c7 r17043@gaspard (orig r914): jplang | 2007-11-18 16:53:58 +0100
'fixed version' field can now be displayed on the issue list.
 Category and fixed version fields added to the CSV export.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@1780 e93f8b46-1217-0410-a6f0-8f06a7374b81
2008-09-01 17:37:27 +00:00
Nicolas Chuche f48cf8e67d r16121@gaspard (orig r912): jplang | 2007-11-18 15:32:39 +0100
Added an alternate theme which provides issue list colorization based on issues priority.


git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@913 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 15:03:57 +00:00
Nicolas Chuche d6e8121ef1 change owner rights to work with mod_perl authentication. add a test option that just connect to WS and say what should be done
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@911 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 12:39:13 +00:00
Jean-Philippe Lang 83b6fa57f6 Added nbc branch.
git-svn-id: http://redmine.rubyforge.org/svn/branches/nbc@910 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-11-18 11:58:02 +00:00
996 changed files with 42253 additions and 65190 deletions
-19
View File
@@ -1,19 +0,0 @@
/config/additional_environment.rb
/config/database.yml
/config/email.yml
/config/initializers/session_store.rb
/coverage
/db/*.db
/db/*.sqlite3
/db/schema.rb
/files/*
/log/*.log*
/log/mongrel_debug
/public/dispatch.*
/public/plugin_assets
/tmp/*
/tmp/cache/*
/tmp/sessions/*
/tmp/sockets/*
/tmp/test/*
/vendor/rails
-5
View File
@@ -1,5 +0,0 @@
= Redmine
Redmine is a flexible project management web application written using Ruby on Rails framework.
More details can be found at http://www.redmine.org
+46 -36
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006-2008 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -20,12 +20,30 @@ class AccountController < ApplicationController
include CustomFieldsHelper include CustomFieldsHelper
# prevents login action to be filtered by check_if_login_required application scope filter # prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
# Show user's account
def show
@user = User.active.find(params[:id])
@custom_values = @user.custom_values
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project))
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
rescue ActiveRecord::RecordNotFound
render_404
end
# Login request and validation # Login request and validation
def login def login
if request.get? if request.get?
logout_user # Logout user
self.logged_user = nil
else else
# Authenticate user # Authenticate user
if Setting.openid? && using_open_id? if Setting.openid? && using_open_id?
@@ -38,7 +56,9 @@ class AccountController < ApplicationController
# Log out current user and redirect to welcome page # Log out current user and redirect to welcome page
def logout def logout
logout_user cookies.delete :autologin
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
self.logged_user = nil
redirect_to home_url redirect_to home_url
end end
@@ -64,9 +84,9 @@ class AccountController < ApplicationController
if request.post? if request.post?
user = User.find_by_mail(params[:mail]) user = User.find_by_mail(params[:mail])
# user not found in db # user not found in db
(flash.now[:error] = l(:notice_account_unknown_email); return) unless user flash.now[:error] = l(:notice_account_unknown_email) and return unless user
# user uses an external authentification # user uses an external authentification
(flash.now[:error] = l(:notice_can_t_change_password); return) if user.auth_source_id flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
# create a new token for password recovery # create a new token for password recovery
token = Token.new(:user => user, :action => "recovery") token = Token.new(:user => user, :action => "recovery")
if token.save if token.save
@@ -130,23 +150,27 @@ class AccountController < ApplicationController
redirect_to :action => 'login' redirect_to :action => 'login'
end end
private private
def logged_user=(user)
def logout_user if user && user.is_a?(User)
if User.current.logged? User.current = user
cookies.delete :autologin session[:user_id] = user.id
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) else
self.logged_user = nil User.current = User.anonymous
session[:user_id] = nil
end end
end end
def password_authentication def password_authentication
user = User.try_to_login(params[:username], params[:password]) user = User.try_to_login(params[:username], params[:password])
if user.nil? if user.nil?
invalid_credentials # Invalid credentials
flash.now[:error] = l(:notice_account_invalid_creditentials)
elsif user.new_record? elsif user.new_record?
onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id }) # Onthefly creation failed, display the registration form to fill/fix attributes
@user = user
session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
render :action => 'register'
else else
# Valid user # Valid user
successful_authentication(user) successful_authentication(user)
@@ -172,24 +196,20 @@ class AccountController < ApplicationController
case Setting.self_registration case Setting.self_registration
when '1' when '1'
register_by_email_activation(user) do register_by_email_activation(user) do
onthefly_creation_failed(user) onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end end
when '3' when '3'
register_automatically(user) do register_automatically(user) do
onthefly_creation_failed(user) onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end end
else else
register_manually_by_administrator(user) do register_manually_by_administrator(user) do
onthefly_creation_failed(user) onthefly_creation_failed(user, {:login => user.login, :identity_url => identity_url })
end end
end end
else else
# Existing record # Existing record
if user.active? successful_authentication(user)
successful_authentication(user)
else
account_pending
end
end end
end end
end end
@@ -203,7 +223,6 @@ class AccountController < ApplicationController
token = Token.create(:user => user, :action => 'autologin') token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now } cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end end
call_hook(:controller_account_success_authentication_after, {:user => user })
redirect_back_or_default :controller => 'my', :action => 'page' redirect_back_or_default :controller => 'my', :action => 'page'
end end
@@ -214,10 +233,6 @@ class AccountController < ApplicationController
render :action => 'register' render :action => 'register'
end end
def invalid_credentials
flash.now[:error] = l(:notice_account_invalid_creditentials)
end
# Register a user for email activation. # Register a user for email activation.
# #
# Pass a block for behavior when a user fails to save # Pass a block for behavior when a user fails to save
@@ -238,7 +253,6 @@ class AccountController < ApplicationController
def register_automatically(user, &block) def register_automatically(user, &block)
# Automatic activation # Automatic activation
user.status = User::STATUS_ACTIVE user.status = User::STATUS_ACTIVE
user.last_login_on = Time.now
if user.save if user.save
self.logged_user = user self.logged_user = user
flash[:notice] = l(:notice_account_activated) flash[:notice] = l(:notice_account_activated)
@@ -255,14 +269,10 @@ class AccountController < ApplicationController
if user.save if user.save
# Sends an email to the administrators # Sends an email to the administrators
Mailer.deliver_account_activation_request(user) Mailer.deliver_account_activation_request(user)
account_pending flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
else else
yield if block_given? yield if block_given?
end end
end end
def account_pending
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
end
end end
+6 -8
View File
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AdminController < ApplicationController class AdminController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
helper :sort helper :sort
@@ -76,11 +74,11 @@ class AdminController < ApplicationController
def info def info
@db_adapter_name = ActiveRecord::Base.connection.adapter_name @db_adapter_name = ActiveRecord::Base.connection.adapter_name
@checklist = [ @flags = {
[:text_default_administrator_account_changed, User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?], :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
[:text_file_repository_writable, File.writable?(Attachment.storage_path)], :file_repository_writable => File.writable?(Attachment.storage_path),
[:text_plugin_assets_writable, File.writable?(Engines.public_directory)], :plugin_assets_writable => File.writable?(Engines.public_directory),
[:text_rmagick_available, Object.const_defined?(:Magick)] :rmagick_available => Object.const_defined?(:Magick)
] }
end end
end end
@@ -19,29 +19,11 @@ require 'uri'
require 'cgi' require 'cgi'
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Redmine::I18n
layout 'base' layout 'base'
# Remove broken cookie after upgrade from 0.8.x (#4292)
# See https://rails.lighthouseapp.com/projects/8994/tickets/3360
# TODO: remove it when Rails is fixed
before_filter :delete_broken_cookies
def delete_broken_cookies
if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
cookies.delete '_redmine_session'
redirect_to home_path
return false
end
end
before_filter :user_setup, :check_if_login_required, :set_localization before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password filter_parameter_logging :password
protect_from_forgery
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
include Redmine::Search::Controller
include Redmine::MenuManager::MenuController include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper helper Redmine::MenuManager::MenuHelper
@@ -49,6 +31,10 @@ class ApplicationController < ActionController::Base
require_dependency "repository/#{scm.underscore}" require_dependency "repository/#{scm.underscore}"
end end
def current_role
@current_role ||= User.current.role_for_project(@project)
end
def user_setup def user_setup
# Check the settings cache for each request # Check the settings cache for each request
Setting.check_cache Setting.check_cache
@@ -57,40 +43,16 @@ class ApplicationController < ActionController::Base
end end
# Returns the current user or nil if no user is logged in # Returns the current user or nil if no user is logged in
# and starts a session if needed
def find_current_user def find_current_user
if session[:user_id] if session[:user_id]
# existing session # existing session
(User.active.find(session[:user_id]) rescue nil) (User.active.find(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin? elsif cookies[:autologin] && Setting.autologin?
# auto-login feature starts a new session # auto-login feature
user = User.try_to_autologin(cookies[:autologin]) User.find_by_autologin_key(cookies[:autologin])
session[:user_id] = user.id if user elsif params[:key] && accept_key_auth_actions.include?(params[:action])
user # RSS key authentication
elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication does not start a session
User.find_by_rss_key(params[:key]) User.find_by_rss_key(params[:key])
elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format]) && accept_key_auth_actions.include?(params[:action])
if params[:key].present?
# Use API key
User.find_by_api_key(params[:key])
else
# HTTP Basic, either username/password or API key/random
authenticate_with_http_basic do |username, password|
User.try_to_login(username, password) || User.find_by_api_key(username)
end
end
end
end
# Sets the logged in user
def logged_user=(user)
reset_session
if user && user.is_a?(User)
User.current = user
session[:user_id] = user.id
else
User.current = User.anonymous
end end
end end
@@ -102,35 +64,25 @@ class ApplicationController < ActionController::Base
end end
def set_localization def set_localization
lang = nil User.current.language = nil unless User.current.logged?
if User.current.logged? lang = begin
lang = find_language(User.current.language) if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
end User.current.language
if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE'] elsif request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
if !accept_lang.blank? if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
accept_lang = accept_lang.downcase User.current.language = accept_lang
lang = find_language(accept_lang) || find_language(accept_lang.split('-').first) end
end end
end rescue
lang ||= Setting.default_language nil
end || Setting.default_language
set_language_if_valid(lang) set_language_if_valid(lang)
end end
def require_login def require_login
if !User.current.logged? if !User.current.logged?
# Extract only the basic url parameters on non-GET requests redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
if request.get?
url = url_for(params)
else
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
end
respond_to do |format|
format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
format.xml { head :unauthorized }
format.json { head :unauthorized }
end
return false return false
end end
true true
@@ -150,16 +102,11 @@ class ApplicationController < ActionController::Base
end end
# Authorize the user for the requested action # Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action], global = false) def authorize(ctrl = params[:controller], action = params[:action])
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global) allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : deny_access allowed ? true : deny_access
end end
# Authorize the user for the requested action outside a project
def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
authorize(ctrl, action, global)
end
# make sure that the user is a member of the project (or admin) if project is private # make sure that the user is a member of the project (or admin) if project is private
# used as a before_filter for actions that do not require any particular permission on the project # used as a before_filter for actions that do not require any particular permission on the project
def check_project_privacy def check_project_privacy
@@ -183,8 +130,7 @@ class ApplicationController < ActionController::Base
uri = URI.parse(back_url) uri = URI.parse(back_url)
# do not redirect user to another host or to the login or register page # 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)}) if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
redirect_to(back_url) redirect_to(back_url) and return
return
end end
rescue URI::InvalidURIError rescue URI::InvalidURIError
# redirect to default # redirect to default
@@ -195,7 +141,7 @@ class ApplicationController < ActionController::Base
def render_403 def render_403
@project = nil @project = nil
render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 render :template => "common/403", :layout => !request.xhr?, :status => 403
return false return false
end end
@@ -206,11 +152,7 @@ class ApplicationController < ActionController::Base
def render_error(msg) def render_error(msg)
flash.now[:error] = msg flash.now[:error] = msg
render :text => '', :layout => !request.xhr?, :status => 500 render :nothing => true, :layout => !request.xhr?, :status => 500
end
def invalid_authenticity_token
render_error "Invalid form authenticity token."
end end
def render_feed(items, options={}) def render_feed(items, options={})
@@ -283,8 +225,6 @@ class ApplicationController < ActionController::Base
tmp.collect!{|val, q| val} tmp.collect!{|val, q| val}
end end
return tmp return tmp
rescue
nil
end end
# Returns a string that can be used as filename value in Content-Disposition header # Returns a string that can be used as filename value in Content-Disposition header
+3 -16
View File
@@ -17,7 +17,7 @@
class AttachmentsController < ApplicationController class AttachmentsController < ApplicationController
before_filter :find_project before_filter :find_project
before_filter :file_readable, :read_authorize, :except => :destroy before_filter :read_authorize, :except => :destroy
before_filter :delete_authorize, :only => :destroy before_filter :delete_authorize, :only => :destroy
verify :method => :post, :only => :destroy verify :method => :post, :only => :destroy
@@ -26,7 +26,7 @@ class AttachmentsController < ApplicationController
if @attachment.is_diff? if @attachment.is_diff?
@diff = File.new(@attachment.diskfile, "rb").read @diff = File.new(@attachment.diskfile, "rb").read
render :action => 'diff' render :action => 'diff'
elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte elsif @attachment.is_text?
@content = File.new(@attachment.diskfile, "rb").read @content = File.new(@attachment.diskfile, "rb").read
render :action => 'file' render :action => 'file'
else else
@@ -41,7 +41,7 @@ class AttachmentsController < ApplicationController
# images are sent inline # images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => detect_content_type(@attachment), :type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment') :disposition => (@attachment.image? ? 'inline' : 'attachment')
end end
@@ -64,11 +64,6 @@ private
render_404 render_404
end end
# Checks that the file exists and is readable
def file_readable
@attachment.readable? ? true : render_404
end
def read_authorize def read_authorize
@attachment.visible? ? true : deny_access @attachment.visible? ? true : deny_access
end end
@@ -76,12 +71,4 @@ private
def delete_authorize def delete_authorize
@attachment.deletable? ? true : deny_access @attachment.deletable? ? true : deny_access
end end
def detect_content_type(attachment)
content_type = attachment.content_type
if content_type.blank?
content_type = Redmine::MimeType.of(attachment.filename)
end
content_type.to_s
end
end end
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSourcesController < ApplicationController class AuthSourcesController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
def index def index
+17 -23
View File
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class BoardsController < ApplicationController class BoardsController < ApplicationController
default_search_scope :messages
before_filter :find_project, :authorize before_filter :find_project, :authorize
helper :messages helper :messages
@@ -36,29 +35,18 @@ class BoardsController < ApplicationController
end end
def show def show
respond_to do |format| sort_init 'updated_on', 'desc'
format.html { sort_update 'created_on' => "#{Message.table_name}.created_on",
sort_init 'updated_on', 'desc' 'replies' => "#{Message.table_name}.replies_count",
sort_update 'created_on' => "#{Message.table_name}.created_on", 'updated_on' => "#{Message.table_name}.updated_on"
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
@topic_count = @board.topics.count @topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page'] @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '), @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
:include => [:author, {:last_reply => :author}], :include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page, :limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset :offset => @topic_pages.current.offset
@message = Message.new render :action => 'show', :layout => !request.xhr?
render :action => 'show', :layout => !request.xhr?
}
format.atom {
@messages = @board.messages.find :all, :order => 'created_on DESC',
:include => [:author, :board],
:limit => Setting.feeds_limit.to_i
render_feed(@messages, :title => "#{@project}: #{@board}")
}
end
end end
verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index } verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
@@ -74,6 +62,12 @@ class BoardsController < ApplicationController
def edit def edit
if request.post? && @board.update_attributes(params[:board]) if request.post? && @board.update_attributes(params[:board])
case params[:position]
when 'highest'; @board.move_to_top
when 'higher'; @board.move_higher
when 'lower'; @board.move_lower
when 'lowest'; @board.move_to_bottom
end if params[:position]
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
end end
end end
+16 -5
View File
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CustomFieldsController < ApplicationController class CustomFieldsController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
def index def index
@@ -32,11 +30,10 @@ class CustomFieldsController < ApplicationController
end end
rescue rescue
end end
(redirect_to(:action => 'index'); return) unless @custom_field.is_a?(CustomField) redirect_to(:action => 'index') and return unless @custom_field.is_a?(CustomField)
if request.post? and @custom_field.save if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :tab => @custom_field.class.name redirect_to :action => 'index', :tab => @custom_field.class.name
end end
@trackers = Tracker.find(:all, :order => 'position') @trackers = Tracker.find(:all, :order => 'position')
@@ -46,12 +43,26 @@ class CustomFieldsController < ApplicationController
@custom_field = CustomField.find(params[:id]) @custom_field = CustomField.find(params[:id])
if request.post? and @custom_field.update_attributes(params[:custom_field]) if request.post? and @custom_field.update_attributes(params[:custom_field])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'index', :tab => @custom_field.class.name redirect_to :action => 'index', :tab => @custom_field.class.name
end end
@trackers = Tracker.find(:all, :order => 'position') @trackers = Tracker.find(:all, :order => 'position')
end end
def move
@custom_field = CustomField.find(params[:id])
case params[:position]
when 'highest'
@custom_field.move_to_top
when 'higher'
@custom_field.move_higher
when 'lower'
@custom_field.move_lower
when 'lowest'
@custom_field.move_to_bottom
end if params[:position]
redirect_to :action => 'index', :tab => @custom_field.class.name
end
def destroy def destroy
@custom_field = CustomField.find(params[:id]).destroy @custom_field = CustomField.find(params[:id]).destroy
redirect_to :action => 'index', :tab => @custom_field.class.name redirect_to :action => 'index', :tab => @custom_field.class.name
+3 -3
View File
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentsController < ApplicationController class DocumentsController < ApplicationController
default_search_scope :documents
before_filter :find_project, :only => [:index, :new] before_filter :find_project, :only => [:index, :new]
before_filter :find_document, :except => [:index, :new] before_filter :find_document, :except => [:index, :new]
before_filter :authorize before_filter :authorize
@@ -28,7 +27,7 @@ class DocumentsController < ApplicationController
documents = @project.documents.find :all, :include => [:attachments, :category] documents = @project.documents.find :all, :include => [:attachments, :category]
case @sort_by case @sort_by
when 'date' when 'date'
@grouped = documents.group_by {|d| d.updated_on.to_date } @grouped = documents.group_by {|d| d.created_on.to_date }
when 'title' when 'title'
@grouped = documents.group_by {|d| d.title.first.upcase} @grouped = documents.group_by {|d| d.title.first.upcase}
when 'author' when 'author'
@@ -49,12 +48,13 @@ class DocumentsController < ApplicationController
if request.post? and @document.save if request.post? and @document.save
attach_files(@document, params[:attachments]) attach_files(@document, params[:attachments])
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
redirect_to :action => 'index', :project_id => @project redirect_to :action => 'index', :project_id => @project
end end
end end
def edit def edit
@categories = DocumentCategory.all @categories = Enumeration::get_values('DCAT')
if request.post? and @document.update_attributes(params[:document]) if request.post? and @document.update_attributes(params[:document])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @document redirect_to :action => 'show', :id => @document
+20 -16
View File
@@ -16,13 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class EnumerationsController < ApplicationController class EnumerationsController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
helper :custom_fields
include CustomFieldsHelper
def index def index
list list
render :action => 'list' render :action => 'list'
@@ -36,19 +31,14 @@ class EnumerationsController < ApplicationController
end end
def new def new
begin @enumeration = Enumeration.new(:opt => params[:opt])
@enumeration = params[:type].constantize.new
rescue NameError
@enumeration = Enumeration.new
end
end end
def create def create
@enumeration = Enumeration.new(params[:enumeration]) @enumeration = Enumeration.new(params[:enumeration])
@enumeration.type = params[:enumeration][:type]
if @enumeration.save if @enumeration.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list', :type => @enumeration.type redirect_to :action => 'list', :opt => @enumeration.opt
else else
render :action => 'new' render :action => 'new'
end end
@@ -60,15 +50,29 @@ class EnumerationsController < ApplicationController
def update def update
@enumeration = Enumeration.find(params[:id]) @enumeration = Enumeration.find(params[:id])
@enumeration.type = params[:enumeration][:type] if params[:enumeration][:type]
if @enumeration.update_attributes(params[:enumeration]) if @enumeration.update_attributes(params[:enumeration])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list', :type => @enumeration.type redirect_to :action => 'list', :opt => @enumeration.opt
else else
render :action => 'edit' render :action => 'edit'
end end
end end
def move
@enumeration = Enumeration.find(params[:id])
case params[:position]
when 'highest'
@enumeration.move_to_top
when 'higher'
@enumeration.move_higher
when 'lower'
@enumeration.move_lower
when 'lowest'
@enumeration.move_to_bottom
end if params[:position]
redirect_to :action => 'index'
end
def destroy def destroy
@enumeration = Enumeration.find(params[:id]) @enumeration = Enumeration.find(params[:id])
if !@enumeration.in_use? if !@enumeration.in_use?
@@ -76,12 +80,12 @@ class EnumerationsController < ApplicationController
@enumeration.destroy @enumeration.destroy
redirect_to :action => 'index' redirect_to :action => 'index'
elsif params[:reassign_to_id] elsif params[:reassign_to_id]
if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id]) if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id])
@enumeration.destroy(reassign_to) @enumeration.destroy(reassign_to)
redirect_to :action => 'index' redirect_to :action => 'index'
end end
end end
@enumerations = @enumeration.class.find(:all) - [@enumeration] @enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration]
#rescue #rescue
# flash[:error] = 'Unable to delete enumeration' # flash[:error] = 'Unable to delete enumeration'
# redirect_to :action => 'index' # redirect_to :action => 'index'
-163
View File
@@ -1,163 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class GroupsController < ApplicationController
layout 'admin'
before_filter :require_admin
helper :custom_fields
# GET /groups
# GET /groups.xml
def index
@groups = Group.find(:all, :order => 'lastname')
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @groups }
end
end
# GET /groups/1
# GET /groups/1.xml
def show
@group = Group.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @group }
end
end
# GET /groups/new
# GET /groups/new.xml
def new
@group = Group.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @group }
end
end
# GET /groups/1/edit
def edit
@group = Group.find(params[:id])
end
# POST /groups
# POST /groups.xml
def create
@group = Group.new(params[:group])
respond_to do |format|
if @group.save
flash[:notice] = l(:notice_successful_create)
format.html { redirect_to(groups_path) }
format.xml { render :xml => @group, :status => :created, :location => @group }
else
format.html { render :action => "new" }
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
end
end
end
# PUT /groups/1
# PUT /groups/1.xml
def update
@group = Group.find(params[:id])
respond_to do |format|
if @group.update_attributes(params[:group])
flash[:notice] = l(:notice_successful_update)
format.html { redirect_to(groups_path) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /groups/1
# DELETE /groups/1.xml
def destroy
@group = Group.find(params[:id])
@group.destroy
respond_to do |format|
format.html { redirect_to(groups_url) }
format.xml { head :ok }
end
end
def add_users
@group = Group.find(params[:id])
users = User.find_all_by_id(params[:user_ids])
@group.users << users if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-users", :partial => 'groups/users'
users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
}
}
end
end
def remove_user
@group = Group.find(params[:id])
@group.users.delete(User.find(params[:user_id])) if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
end
end
def autocomplete_for_user
@group = Group.find(params[:id])
@users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
render :layout => false
end
def edit_membership
@group = Group.find(params[:id])
@membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @group)
@membership.attributes = params[:membership]
@membership.save if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
end
end
def destroy_membership
@group = Group.find(params[:id])
Member.find(params[:membership_id]).destroy if request.post?
respond_to do |format|
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
end
end
end
@@ -21,8 +21,8 @@ class IssueRelationsController < ApplicationController
def new def new
@relation = IssueRelation.new(params[:relation]) @relation = IssueRelation.new(params[:relation])
@relation.issue_from = @issue @relation.issue_from = @issue
if params[:relation] && m = params[:relation][:issue_to_id].to_s.match(/^#?(\d+)$/) if params[:relation] && !params[:relation][:issue_to_id].blank?
@relation.issue_to = Issue.visible.find_by_id(m[1].to_i) @relation.issue_to = Issue.visible.find_by_id(params[:relation][:issue_to_id])
end end
@relation.save if request.post? @relation.save if request.post?
respond_to do |format| respond_to do |format|
+16 -12
View File
@@ -16,11 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueStatusesController < ApplicationController class IssueStatusesController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move, :update_issue_done_ratio ], verify :method => :post, :only => [ :destroy, :create, :update, :move ],
:redirect_to => { :action => :list } :redirect_to => { :action => :list }
def index def index
@@ -61,6 +59,21 @@ class IssueStatusesController < ApplicationController
end end
end end
def move
@issue_status = IssueStatus.find(params[:id])
case params[:position]
when 'highest'
@issue_status.move_to_top
when 'higher'
@issue_status.move_higher
when 'lower'
@issue_status.move_lower
when 'lowest'
@issue_status.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def destroy def destroy
IssueStatus.find(params[:id]).destroy IssueStatus.find(params[:id]).destroy
redirect_to :action => 'list' redirect_to :action => 'list'
@@ -68,13 +81,4 @@ class IssueStatusesController < ApplicationController
flash[:error] = "Unable to delete issue status" flash[:error] = "Unable to delete issue status"
redirect_to :action => 'list' redirect_to :action => 'list'
end end
def update_issue_done_ratio
if IssueStatus.update_issue_done_ratios
flash[:notice] = l(:notice_issue_done_ratios_updated)
else
flash[:error] = l(:error_issue_done_ratios_not_updated)
end
redirect_to :action => 'list'
end
end end
+101 -150
View File
@@ -17,16 +17,13 @@
class IssuesController < ApplicationController class IssuesController < ApplicationController
menu_item :new_issue, :only => :new menu_item :new_issue, :only => :new
default_search_scope :issues
before_filter :find_issue, :only => [:show, :edit, :reply] before_filter :find_issue, :only => [:show, :edit, :reply]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview] before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu] before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
accept_key_auth :index, :show, :changes accept_key_auth :index, :changes
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :journals helper :journals
helper :projects helper :projects
@@ -40,45 +37,37 @@ class IssuesController < ApplicationController
helper :attachments helper :attachments
include AttachmentsHelper include AttachmentsHelper
helper :queries helper :queries
include QueriesHelper
helper :sort helper :sort
include SortHelper include SortHelper
include IssuesHelper include IssuesHelper
helper :timelog helper :timelog
include Redmine::Export::PDF include Redmine::Export::PDF
verify :method => :post,
:only => :destroy,
:render => { :nothing => true, :status => :method_not_allowed }
def index def index
retrieve_query retrieve_query
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria) sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid? if @query.valid?
limit = case params[:format] limit = per_page_option
when 'csv', 'pdf' respond_to do |format|
Setting.issues_export_limit.to_i format.html { }
when 'atom' format.atom { }
Setting.feeds_limit.to_i format.csv { limit = Setting.issues_export_limit.to_i }
else format.pdf { limit = Setting.issues_export_limit.to_i }
per_page_option
end end
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
@issue_count = @query.issue_count
@issue_pages = Paginator.new self, @issue_count, limit, params['page'] @issue_pages = Paginator.new self, @issue_count, limit, params['page']
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version], @issues = Issue.find :all, :order => sort_clause,
:order => sort_clause, :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
:offset => @issue_pages.current.offset, :conditions => @query.statement,
:limit => limit) :limit => limit,
@issue_count_by_group = @query.issue_count_by_group :offset => @issue_pages.current.offset
respond_to do |format| respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } 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.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') } format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') } format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
end end
else else
# Send html if the query is not valid # Send html if the query is not valid
@@ -91,11 +80,13 @@ class IssuesController < ApplicationController
def changes def changes
retrieve_query retrieve_query
sort_init 'id', 'desc' sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid? if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC", @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:limit => 25) :conditions => @query.statement,
:limit => 25,
:order => "#{Journal.table_name}.created_on DESC"
end end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name) @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml' render :layout => false, :content_type => 'application/atom+xml'
@@ -107,11 +98,9 @@ class IssuesController < ApplicationController
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1} @journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order? @journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project) @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@priorities = IssuePriority.all @priorities = Enumeration::get_values('IPRI')
@time_entry = TimeEntry.new @time_entry = TimeEntry.new
respond_to do |format| respond_to do |format|
format.html { render :template => 'issues/show.rhtml' } format.html { render :template => 'issues/show.rhtml' }
@@ -129,7 +118,8 @@ class IssuesController < ApplicationController
# Tracker must be set before custom field values # Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first) @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
if @issue.tracker.nil? if @issue.tracker.nil?
render_error l(:error_no_tracker_in_project) flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
render :nothing => true, :layout => true
return return
end end
if params[:issue].is_a?(Hash) if params[:issue].is_a?(Hash)
@@ -140,11 +130,12 @@ class IssuesController < ApplicationController
default_status = IssueStatus.default default_status = IssueStatus.default
unless default_status unless default_status
render_error l(:error_no_default_issue_status) flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
render :nothing => true, :layout => true
return return
end end
@issue.status = default_status @issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
if request.get? || request.xhr? if request.get? || request.xhr?
@issue.start_date ||= Date.today @issue.start_date ||= Date.today
@@ -152,17 +143,17 @@ class IssuesController < ApplicationController
requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
# Check that the user is allowed to apply the requested status # Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
if @issue.save if @issue.save
attach_files(@issue, params[:attachments]) attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } : redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
{ :action => 'show', :id => @issue }) { :action => 'show', :id => @issue })
return return
end end
end end
@priorities = IssuePriority.all @priorities = Enumeration::get_values('IPRI')
render :layout => !request.xhr? render :layout => !request.xhr?
end end
@@ -172,7 +163,7 @@ class IssuesController < ApplicationController
def edit def edit
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@priorities = IssuePriority.all @priorities = Enumeration::get_values('IPRI')
@edit_allowed = User.current.allowed_to?(:edit_issues, @project) @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new @time_entry = TimeEntry.new
@@ -189,29 +180,28 @@ class IssuesController < ApplicationController
if request.post? if request.post?
@time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry] @time_entry.attributes = params[:time_entry]
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid? attachments = attach_files(@issue, params[:attachments])
attachments = attach_files(@issue, params[:attachments]) attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
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}) call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
if @issue.save
# Log spend time if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
if User.current.allowed_to?(:log_time, @project) # Log spend time
@time_entry.save if User.current.allowed_to?(:log_time, @project)
end @time_entry.save
if !journal.new_record?
# Only send notification if something was actually changed
flash[:notice] = l(:notice_successful_update)
end
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
redirect_back_or_default({:action => 'show', :id => @issue})
end end
if !journal.new_record?
# Only send notification if something was actually changed
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end end
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
# Optimistic locking exception # Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict) flash.now[:error] = l(:notice_locking_conflict)
# Remove the previously added attachments if issue was not updated
attachments.each(&:destroy)
end end
def reply def reply
@@ -223,13 +213,10 @@ class IssuesController < ApplicationController
user = @issue.author user = @issue.author
text = @issue.description text = @issue.description
end end
# Replaces pre blocks with [...] content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]') content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page| render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";" page.<< "$('notes').value = \"#{content}\";"
page.show 'update' page.show 'update'
page << "Form.Element.focus('notes');" page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');" page << "Element.scrollTo('update');"
@@ -240,18 +227,16 @@ class IssuesController < ApplicationController
# Bulk edit a set of issues # Bulk edit a set of issues
def bulk_edit def bulk_edit
if request.post? if request.post?
tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id]) priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id]) 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]) 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.shared_versions.find_by_id(params[:fixed_version_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 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
unsaved_issue_ids = [] unsaved_issue_ids = []
@issues.each do |issue| @issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes]) journal = issue.init_journal(User.current, params[:notes])
issue.tracker = tracker if tracker
issue.priority = priority if priority issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category || params[:category_id] == 'none' issue.category = category if category || params[:category_id] == 'none'
@@ -262,7 +247,10 @@ class IssuesController < ApplicationController
issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty? 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 }) 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 # Don't save any change to the issue if the user is not authorized to apply the requested status
unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
# Send notification for each issue (if changed)
Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
else
# Keep unsaved issue ids to display them in flash error # Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id unsaved_issue_ids << issue.id
end end
@@ -270,58 +258,42 @@ class IssuesController < ApplicationController
if unsaved_issue_ids.empty? if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty? flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else else
flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size, flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
:total => @issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end end
redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project}) redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
return return
end end
@available_statuses = Workflow.available_statuses(@project) # Find potential statuses the user could be allowed to switch issues to
@custom_fields = @project.all_issue_custom_fields @available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
@custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
end end
def move def move
@copy = params[:copy_options] && params[:copy_options][:copy] @allowed_projects = []
@allowed_projects = Issue.allowed_target_projects_on_move # find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
else
User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
end
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project @target_project ||= @project
@trackers = @target_project.trackers @trackers = @target_project.trackers
@available_statuses = Workflow.available_statuses(@project)
if request.post? if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = [] unsaved_issue_ids = []
moved_issues = []
@issues.each do |issue| @issues.each do |issue|
changed_attributes = {}
[:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
unless params[valid_attribute].blank?
changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
end
end
issue.init_journal(User.current) issue.init_journal(User.current)
if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes}) unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
moved_issues << r
else
unsaved_issue_ids << issue.id
end
end end
if unsaved_issue_ids.empty? if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty? flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else else
flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size, flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
:total => @issues.size,
:ids => '#' + unsaved_issue_ids.join(', #'))
end
if params[:follow]
if @issues.size == 1 && moved_issues.size == 1
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
else
redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
end
else
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
end end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return return
end end
render :layout => false if request.xhr? render :layout => false if request.xhr?
@@ -355,31 +327,31 @@ class IssuesController < ApplicationController
def gantt def gantt
@gantt = Redmine::Helpers::Gantt.new(params) @gantt = Redmine::Helpers::Gantt.new(params)
retrieve_query retrieve_query
@query.group_by = nil
if @query.valid? if @query.valid?
events = [] events = []
# Issues that have start and due dates # Issues that have start and due dates
events += @query.issues(:include => [:tracker, :assigned_to, :priority], events += Issue.find(:all,
:order => "start_date, due_date", :order => "start_date, due_date",
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] :include => [:tracker, :status, :assigned_to, :priority, :project],
) :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date # Issues that don't have a due date but that are assigned to a version with a date
events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version], events += Issue.find(:all,
:order => "start_date, effective_date", :order => "start_date, effective_date",
:conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
) :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Versions # Versions
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to]) events += Version.find(:all, :include => :project,
:conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
@gantt.events = events @gantt.events = events
end end
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format| respond_to do |format|
format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? } format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image') format.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 => "#{basename}.pdf") } format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
end end
end end
@@ -395,13 +367,14 @@ class IssuesController < ApplicationController
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month) @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
retrieve_query retrieve_query
@query.group_by = nil
if @query.valid? if @query.valid?
events = [] events = []
events += @query.issues(:include => [:tracker, :assigned_to, :priority], events += Issue.find(:all,
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt] :include => [:tracker, :status, :assigned_to, :priority, :project],
) :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) )
events += Version.find(:all, :include => :project,
:conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events @calendar.events = events
end end
@@ -428,28 +401,18 @@ class IssuesController < ApplicationController
if @project if @project
@assignables = @project.assignable_users @assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
@trackers = @project.trackers
end end
@priorities = IssuePriority.all.reverse @priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position') @statuses = IssueStatus.find(:all, :order => 'position')
@back = params[:back_url] || request.env['HTTP_REFERER'] @back = request.env['HTTP_REFERER']
render :layout => false render :layout => false
end end
def update_form def update_form
if params[:id].blank? @issue = Issue.new(params[:issue])
@issue = Issue.new render :action => :new, :layout => false
@issue.project = @project
else
@issue = @project.issues.visible.find(params[:id])
end
@issue.attributes = params[:issue]
@allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
@priorities = IssuePriority.all
render :partial => 'attributes'
end end
def preview def preview
@@ -476,8 +439,7 @@ private
@project = projects.first @project = projects.first
else else
# TODO: let users bulk edit/move/destroy issues from different projects # TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects' render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
return false
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
@@ -505,7 +467,6 @@ private
@query = Query.find(params[:query_id], :conditions => cond) @query = Query.find(params[:query_id], :conditions => cond)
@query.project = @project @query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id} session[:query] = {:id => @query.id, :project_id => @query.project_id}
sort_clear
else else
if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil) if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid # Give it a name, required to be valid
@@ -520,22 +481,12 @@ private
@query.add_short_filter(field, params[field]) if params[field] @query.add_short_filter(field, params[field]) if params[field]
end end
end end
@query.group_by = params[:group_by] session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
@query.column_names = params[:query] && params[:query][:column_names]
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
else else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id] @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
@query.project = @project @query.project = @project
end end
end end
end end
# Rescues an invalid query statement. Just in case...
def query_statement_invalid(exception)
logger.error "Query::StatementInvalid: #{exception.message}" if logger
session.delete(:query)
sort_clear
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
end
end end
+1 -1
View File
@@ -33,7 +33,7 @@ class JournalsController < ApplicationController
private private
def find_journal def find_journal
@journal = Journal.find(params[:id]) @journal = Journal.find(params[:id])
(render_403; return false) unless @journal.editable_by?(User.current) render_403 and return false unless @journal.editable_by?(User.current)
@project = @journal.journalized.project @project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
+2 -2
View File
@@ -37,8 +37,8 @@ class MailHandlerController < ActionController::Base
def check_credential def check_credential
User.current = nil User.current = nil
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key unless Setting.mail_handler_api_enabled? && params[:key] == Setting.mail_handler_api_key
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403 render :nothing => true, :status => 403
end end
end end
end end
+8 -36
View File
@@ -16,31 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MembersController < ApplicationController class MembersController < ApplicationController
before_filter :find_member, :except => [:new, :autocomplete_for_member] before_filter :find_member, :except => :new
before_filter :find_project, :only => [:new, :autocomplete_for_member] before_filter :find_project, :only => :new
before_filter :authorize before_filter :authorize
def new def new
members = [] @project.members << Member.new(params[:member]) if request.post?
if params[:member] && request.post?
attrs = params[:member].dup
if (user_ids = attrs.delete(:user_ids))
user_ids.each do |user_id|
members << Member.new(attrs.merge(:user_id => user_id))
end
else
members << Member.new(attrs)
end
@project.members << members
end
respond_to do |format| respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project }
format.js { format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
render(:update) {|page|
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
}
}
end end
end end
@@ -48,31 +32,19 @@ class MembersController < ApplicationController
if request.post? and @member.update_attributes(params[:member]) if request.post? and @member.update_attributes(params[:member])
respond_to do |format| respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
render(:update) {|page|
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
page.visual_effect(:highlight, "member-#{@member.id}")
}
}
end end
end end
end end
def destroy def destroy
if request.post? && @member.deletable? @member.destroy
@member.destroy respond_to do |format|
end
respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end end
end end
def autocomplete_for_member
@principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals
render :layout => false
end
private private
def find_project def find_project
@project = Project.find(params[:id]) @project = Project.find(params[:id])
+3 -10
View File
@@ -17,7 +17,6 @@
class MessagesController < ApplicationController class MessagesController < ApplicationController
menu_item :boards menu_item :boards
default_search_scope :messages
before_filter :find_board, :only => [:new, :preview] before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview] before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => [:preview, :edit, :destroy] before_filter :authorize, :except => [:preview, :edit, :destroy]
@@ -47,7 +46,6 @@ class MessagesController < ApplicationController
@message.sticky = params[:message]['sticky'] @message.sticky = params[:message]['sticky']
end end
if request.post? && @message.save if request.post? && @message.save
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
attach_files(@message, params[:attachments]) attach_files(@message, params[:attachments])
redirect_to :action => 'show', :id => @message redirect_to :action => 'show', :id => @message
end end
@@ -60,7 +58,6 @@ class MessagesController < ApplicationController
@reply.board = @board @reply.board = @board
@topic.children << @reply @topic.children << @reply
if !@reply.new_record? if !@reply.new_record?
call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
attach_files(@reply, params[:attachments]) attach_files(@reply, params[:attachments])
end end
redirect_to :action => 'show', :id => @topic redirect_to :action => 'show', :id => @topic
@@ -68,7 +65,7 @@ class MessagesController < ApplicationController
# Edit a message # Edit a message
def edit def edit
(render_403; return false) unless @message.editable_by?(User.current) render_403 and return false unless @message.editable_by?(User.current)
if params[:message] if params[:message]
@message.locked = params[:message]['locked'] @message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky'] @message.sticky = params[:message]['sticky']
@@ -76,14 +73,13 @@ class MessagesController < ApplicationController
if request.post? && @message.update_attributes(params[:message]) if request.post? && @message.update_attributes(params[:message])
attach_files(@message, params[:attachments]) attach_files(@message, params[:attachments])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
@message.reload redirect_to :action => 'show', :id => @topic
redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
end end
end end
# Delete a messages # Delete a messages
def destroy def destroy
(render_403; return false) unless @message.destroyable_by?(User.current) render_403 and return false unless @message.destroyable_by?(User.current)
@message.destroy @message.destroy
redirect_to @message.parent.nil? ? redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
@@ -93,12 +89,9 @@ class MessagesController < ApplicationController
def quote def quote
user = @message.author user = @message.author
text = @message.content text = @message.content
subject = @message.subject.gsub('"', '\"')
subject = "RE: #{subject}" unless subject.starts_with?('RE:')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page| render(:update) { |page|
page << "$('reply_subject').value = \"#{subject}\";"
page.<< "$('message_content').value = \"#{content}\";" page.<< "$('message_content').value = \"#{content}\";"
page.show 'reply' page.show 'reply'
page << "Form.Element.focus('message_content');" page << "Form.Element.focus('message_content');"
+32 -54
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -28,13 +28,14 @@ class MyController < ApplicationController
'calendar' => :label_calendar, 'calendar' => :label_calendar,
'documents' => :label_document_plural, 'documents' => :label_document_plural,
'timelog' => :label_spent_time 'timelog' => :label_spent_time
}.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze }.freeze
DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
'right' => ['issuesreportedbyme'] 'right' => ['issuesreportedbyme']
}.freeze }.freeze
verify :xhr => true, verify :xhr => true,
:session => :page_layout,
:only => [:add_block, :remove_block, :order_blocks] :only => [:add_block, :remove_block, :order_blocks]
def index def index
@@ -77,11 +78,7 @@ class MyController < ApplicationController
# Manage user's password # Manage user's password
def password def password
@user = User.current @user = User.current
if @user.auth_source_id flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id
flash[:error] = l(:notice_can_t_change_password)
redirect_to :action => 'account'
return
end
if request.post? if request.post?
if @user.check_password?(params[:password]) if @user.check_password?(params[:password])
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
@@ -97,65 +94,43 @@ class MyController < ApplicationController
# Create a new feeds key # Create a new feeds key
def reset_rss_key def reset_rss_key
if request.post? if request.post? && User.current.rss_token
if User.current.rss_token User.current.rss_token.destroy
User.current.rss_token.destroy
User.current.reload
end
User.current.rss_key
flash[:notice] = l(:notice_feeds_access_key_reseted) flash[:notice] = l(:notice_feeds_access_key_reseted)
end end
redirect_to :action => 'account' redirect_to :action => 'account'
end end
# Create a new API key
def reset_api_key
if request.post?
if User.current.api_token
User.current.api_token.destroy
User.current.reload
end
User.current.api_key
flash[:notice] = l(:notice_api_access_key_reseted)
end
redirect_to :action => 'account'
end
# User's page layout configuration # User's page layout configuration
def page_layout def page_layout
@user = User.current @user = User.current
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
session[:page_layout] = @blocks
%w(top left right).each {|f| session[:page_layout][f] ||= [] }
@block_options = [] @block_options = []
BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]} BLOCKS.each {|k, v| @block_options << [l(v), k]}
end end
# Add a block to user's page # Add a block to user's page
# The block is added on top of the page # The block is added on top of the page
# params[:block] : id of the block to add # params[:block] : id of the block to add
def add_block def add_block
block = params[:block].to_s.underscore block = params[:block]
(render :nothing => true; return) unless block && (BLOCKS.keys.include? block) render(:nothing => true) and return unless block && (BLOCKS.keys.include? block)
@user = User.current @user = User.current
layout = @user.pref[:my_page_layout] || {}
# remove if already present in a group # remove if already present in a group
%w(top left right).each {|f| (layout[f] ||= []).delete block } %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
# add it on top # add it on top
layout['top'].unshift block session[:page_layout]['top'].unshift block
@user.pref[:my_page_layout] = layout
@user.pref.save
render :partial => "block", :locals => {:user => @user, :block_name => block} render :partial => "block", :locals => {:user => @user, :block_name => block}
end end
# Remove a block to user's page # Remove a block to user's page
# params[:block] : id of the block to remove # params[:block] : id of the block to remove
def remove_block def remove_block
block = params[:block].to_s.underscore block = params[:block]
@user = User.current
# remove block in all groups # remove block in all groups
layout = @user.pref[:my_page_layout] || {} %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
%w(top left right).each {|f| (layout[f] ||= []).delete block }
@user.pref[:my_page_layout] = layout
@user.pref.save
render :nothing => true render :nothing => true
end end
@@ -164,20 +139,23 @@ class MyController < ApplicationController
# params[:list-(top|left|right)] : array of block ids of the group # params[:list-(top|left|right)] : array of block ids of the group
def order_blocks def order_blocks
group = params[:group] group = params[:group]
@user = User.current group_items = params["list-#{group}"]
if group.is_a?(String) if group_items and group_items.is_a? Array
group_items = (params["list-#{group}"] || []).collect(&:underscore) # remove group blocks if they are presents in other groups
if group_items and group_items.is_a? Array %w(top left right).each {|f|
layout = @user.pref[:my_page_layout] || {} session[:page_layout][f] = (session[:page_layout][f] || []) - group_items
# remove group blocks if they are presents in other groups }
%w(top left right).each {|f| session[:page_layout][group] = group_items
layout[f] = (layout[f] || []) - group_items
}
layout[group] = group_items
@user.pref[:my_page_layout] = layout
@user.pref.save
end
end end
render :nothing => true render :nothing => true
end end
# Save user's page layout
def page_layout_save
@user = User.current
@user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout]
@user.pref.save
session[:page_layout] = nil
redirect_to :action => 'page'
end
end end
+2 -5
View File
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class NewsController < ApplicationController class NewsController < ApplicationController
default_search_scope :news
before_filter :find_news, :except => [:new, :index, :preview] before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_project, :only => [:new, :preview] before_filter :find_project, :only => [:new, :preview]
before_filter :authorize, :except => [:index, :preview] before_filter :authorize, :except => [:index, :preview]
@@ -26,13 +25,11 @@ class NewsController < ApplicationController
def index def index
@news_pages, @newss = paginate :news, @news_pages, @newss = paginate :news,
:per_page => 10, :per_page => 10,
:conditions => Project.allowed_to_condition(User.current, :view_news, :project => @project), :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
:include => [:author, :project], :include => [:author, :project],
:order => "#{News.table_name}.created_on DESC" :order => "#{News.table_name}.created_on DESC"
respond_to do |format| respond_to do |format|
format.html { render :layout => false if request.xhr? } format.html { render :layout => false if request.xhr? }
format.xml { render :xml => @newss.to_xml }
format.json { render :json => @newss.to_json }
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") } format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
end end
end end
@@ -48,6 +45,7 @@ class NewsController < ApplicationController
@news.attributes = params[:news] @news.attributes = params[:news]
if @news.save if @news.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
redirect_to :controller => 'news', :action => 'index', :project_id => @project redirect_to :controller => 'news', :action => 'index', :project_id => @project
end end
end end
@@ -67,7 +65,6 @@ class NewsController < ApplicationController
flash[:notice] = l(:label_comment_added) flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news redirect_to :action => 'show', :id => @news
else else
show
render :action => 'show' render :action => 'show'
end end
end end
+58 -179
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -21,12 +21,12 @@ class ProjectsController < ApplicationController
menu_item :roadmap, :only => :roadmap menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file] menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings menu_item :settings, :only => :settings
menu_item :issues, :only => [:changelog]
before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ] before_filter :find_project, :except => [ :index, :list, :add, :activity ]
before_filter :find_optional_project, :only => :activity before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ] before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
before_filter :authorize_global, :only => :add before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
accept_key_auth :activity accept_key_auth :activity
after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller| after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
@@ -70,56 +70,15 @@ class ProjectsController < ApplicationController
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
@project.trackers = Tracker.all @project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public? @project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Setting.default_projects_modules @project.enabled_module_names = Redmine::AccessControl.available_project_modules
else else
@project.enabled_module_names = params[:enabled_modules] @project.enabled_module_names = params[:enabled_modules]
if validate_parent_id && @project.save if @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
# Add current user as a project member if he is not admin
unless User.current.admin?
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
m = Member.new(:user => User.current, :roles => [r])
@project.members << m
end
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'projects', :action => 'settings', :id => @project
end
end
end
def copy
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@root_projects = Project.find(:all,
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
:order => 'name')
@source_project = Project.find(params[:id])
if request.get?
@project = Project.copy_from(@source_project)
if @project
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
else
redirect_to :controller => 'admin', :action => 'projects' redirect_to :controller => 'admin', :action => 'projects'
end end
else
Mailer.with_deliveries(params[:notifications] == '1') do
@project = Project.new(params[:project])
@project.enabled_module_names = params[:enabled_modules]
if validate_parent_id && @project.copy(@source_project, :only => params[:only])
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
elsif !@project.new_record?
# Project was created
# But some objects were not copied due to validation failures
# (eg. issues from disabled trackers)
# TODO: inform about that
redirect_to :controller => 'admin', :action => 'projects'
end
end
end end
rescue ActiveRecord::RecordNotFound
redirect_to :controller => 'admin', :action => 'projects'
end end
# Show @project # Show @project
@@ -129,8 +88,9 @@ class ProjectsController < ApplicationController
redirect_to_project_menu_item(@project, params[:jump]) && return redirect_to_project_menu_item(@project, params[:jump]) && return
end end
@users_by_role = @project.users_by_role @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
@subprojects = @project.children.visible @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") @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers @trackers = @project.rolled_up_trackers
@@ -164,8 +124,8 @@ class ProjectsController < ApplicationController
def edit def edit
if request.post? if request.post?
@project.attributes = params[:project] @project.attributes = params[:project]
if validate_parent_id && @project.save if @project.save
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project redirect_to :action => 'settings', :id => @project
else else
@@ -181,17 +141,13 @@ class ProjectsController < ApplicationController
end end
def archive def archive
if request.post? @project.archive if request.post? && @project.active?
unless @project.archive redirect_to :controller => 'admin', :action => 'projects'
flash[:error] = l(:error_can_not_archive_project)
end
end
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
end end
def unarchive def unarchive
@project.unarchive if request.post? && !@project.active? @project.unarchive if request.post? && !@project.active?
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status])) redirect_to :controller => 'admin', :action => 'projects'
end end
# Delete @project # Delete @project
@@ -208,26 +164,17 @@ class ProjectsController < ApplicationController
# Add a new issue category to @project # Add a new issue category to @project
def add_issue_category def add_issue_category
@category = @project.issue_categories.build(params[:category]) @category = @project.issue_categories.build(params[:category])
if request.post? if request.post? and @category.save
if @category.save respond_to do |format|
respond_to do |format| format.html do
format.html do flash[:notice] = l(:notice_successful_create)
flash[:notice] = l(:notice_successful_create) redirect_to :action => 'settings', :tab => 'categories', :id => @project
redirect_to :action => 'settings', :tab => 'categories', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_category_id",
content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
}
end
end end
else format.js do
respond_to do |format| # IE doesn't support the replace_html rjs method for select box options
format.html render(:update) {|page| page.replace "issue_category_id",
format.js do content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) } }
end
end end
end end
end end
@@ -235,34 +182,10 @@ class ProjectsController < ApplicationController
# Add a new version to @project # Add a new version to @project
def add_version def add_version
@version = @project.versions.build @version = @project.versions.build(params[:version])
if params[:version] if request.post? and @version.save
attributes = params[:version].dup flash[:notice] = l(:notice_successful_create)
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing']) redirect_to :action => 'settings', :tab => 'versions', :id => @project
@version.attributes = attributes
end
if request.post?
if @version.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'versions', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_fixed_version_id",
content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
}
end
end
else
respond_to do |format|
format.html
format.js do
render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
end
end
end
end end
end end
@@ -279,25 +202,6 @@ class ProjectsController < ApplicationController
@versions = @project.versions.sort @versions = @project.versions.sort
end end
def save_activities
if request.post? && params[:enumerations]
Project.transaction do
params[:enumerations].each do |id, activity|
@project.update_or_create_time_entry_activity(id, activity)
end
end
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def reset_activities
@project.time_entry_activities.each do |time_entry_activity|
time_entry_activity.destroy(time_entry_activity.parent)
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
end
def list_files def list_files
sort_init 'filename', 'asc' sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename", sort_update 'filename' => "#{Attachment.table_name}.filename",
@@ -310,26 +214,18 @@ class ProjectsController < ApplicationController
render :layout => !request.xhr? render :layout => !request.xhr?
end end
# Show changelog for @project
def changelog
@trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
end
def roadmap def roadmap
@trackers = @project.trackers.find(:all, :order => 'position') @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?}) retrieve_selected_tracker_ids(@trackers)
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') @versions = @project.versions.sort
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id] @versions = @versions.select {|v| !v.completed? } unless params[:completed]
@versions = @project.shared_versions.sort
@versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
@issues_by_version = {}
unless @selected_tracker_ids.empty?
@versions.each do |version|
issues = version.fixed_issues.visible.find(:all,
:include => [:project, :status, :tracker, :priority],
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version[version] = issues
end
end
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
end end
def activity def activity
@@ -352,22 +248,20 @@ class ProjectsController < ApplicationController
events = @activity.events(@date_from, @date_to) events = @activity.events(@date_from, @date_to)
if events.empty? || stale?(:etag => [events.first, User.current]) respond_to do |format|
respond_to do |format| format.html {
format.html { @events_by_day = events.group_by(&:event_date)
@events_by_day = events.group_by(&:event_date) render :layout => false if request.xhr?
render :layout => false if request.xhr? }
} format.atom {
format.atom { title = l(:label_activity)
title = l(:label_activity) if @author
if @author title = @author.name
title = @author.name elsif @activity.scope.size == 1
elsif @activity.scope.size == 1 title = l("label_#{@activity.scope.first.singularize}_plural")
title = l("label_#{@activity.scope.first.singularize}_plural") end
end render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") }
}
end
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
@@ -392,26 +286,11 @@ private
render_404 render_404
end end
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil) def retrieve_selected_tracker_ids(selectable_trackers)
if ids = params[:tracker_ids] if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s } @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
else else
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s } @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
end end
end end
# Validates parent_id param according to user's permissions
# TODO: move it to Project model in a validation that depends on User.current
def validate_parent_id
return true if User.current.admin?
parent_id = params[:project] && params[:project][:parent_id]
if parent_id || @project.new_record?
parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
unless @project.allowed_parents.include?(parent)
@project.errors.add :parent_id, :invalid
return false
end
end
true
end
end end
+3 -4
View File
@@ -24,13 +24,12 @@ class QueriesController < ApplicationController
@query = Query.new(params[:query]) @query = Query.new(params[:query])
@query.project = params[:query_is_for_all] ? nil : @project @query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current @query.user = User.current
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns] @query.column_names = nil if params[:default_columns]
params[:fields].each do |field| params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field]) @query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields] end if params[:fields]
@query.group_by ||= params[:group_by]
if request.post? && params[:confirm] && @query.save if request.post? && params[:confirm] && @query.save
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
@@ -48,7 +47,7 @@ class QueriesController < ApplicationController
end if params[:fields] end if params[:fields]
@query.attributes = params[:query] @query.attributes = params[:query]
@query.project = nil if params[:query_is_for_all] @query.project = nil if params[:query_is_for_all]
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns] @query.column_names = nil if params[:default_columns]
if @query.save if @query.save
@@ -74,7 +73,7 @@ private
def find_optional_project def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id] @project = Project.find(params[:project_id]) if params[:project_id]
render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true) User.current.allowed_to?(:save_queries, @project, :global => true)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
+47 -11
View File
@@ -31,13 +31,13 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
when "version" when "version"
@field = "fixed_version_id" @field = "fixed_version_id"
@rows = @project.shared_versions.sort @rows = @project.versions.sort
@data = issues_by_version @data = issues_by_version
@report_title = l(:field_version) @report_title = l(:field_version)
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
when "priority" when "priority"
@field = "priority_id" @field = "priority_id"
@rows = IssuePriority.all @rows = Enumeration::get_values('IPRI')
@data = issues_by_priority @data = issues_by_priority
@report_title = l(:field_priority) @report_title = l(:field_priority)
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
@@ -49,30 +49,30 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
when "assigned_to" when "assigned_to"
@field = "assigned_to_id" @field = "assigned_to_id"
@rows = @project.members.collect { |m| m.user }.sort @rows = @project.members.collect { |m| m.user }
@data = issues_by_assigned_to @data = issues_by_assigned_to
@report_title = l(:field_assigned_to) @report_title = l(:field_assigned_to)
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
when "author" when "author"
@field = "author_id" @field = "author_id"
@rows = @project.members.collect { |m| m.user }.sort @rows = @project.members.collect { |m| m.user }
@data = issues_by_author @data = issues_by_author
@report_title = l(:field_author) @report_title = l(:field_author)
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
when "subproject" when "subproject"
@field = "project_id" @field = "project_id"
@rows = @project.descendants.visible @rows = @project.descendants.active
@data = issues_by_subproject @data = issues_by_subproject
@report_title = l(:field_subproject) @report_title = l(:field_subproject)
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
else else
@trackers = @project.trackers @trackers = @project.trackers
@versions = @project.shared_versions.sort @versions = @project.versions.sort
@priorities = IssuePriority.all @priorities = Enumeration::get_values('IPRI')
@categories = @project.issue_categories @categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }.sort @assignees = @project.members.collect { |m| m.user }
@authors = @project.members.collect { |m| m.user }.sort @authors = @project.members.collect { |m| m.user }
@subprojects = @project.descendants.visible @subprojects = @project.descendants.active
issues_by_tracker issues_by_tracker
issues_by_version issues_by_version
issues_by_priority issues_by_priority
@@ -85,6 +85,42 @@ class ReportsController < ApplicationController
end end
end end
def delays
@trackers = Tracker.find(:all)
if request.get?
@selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
else
@selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
end
@selected_tracker_ids ||= []
@raw =
ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
FROM issue_histories a, issue_histories b, issues i
WHERE a.status_id =5
AND a.issue_id = b.issue_id
AND a.issue_id = i.id
AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
AND b.id = (
SELECT min( c.id )
FROM issue_histories c
WHERE b.issue_id = c.issue_id )
GROUP BY delay") unless @selected_tracker_ids.empty?
@raw ||=[]
@x_from = 0
@x_to = 0
@y_from = 0
@y_to = 0
@sum_total = 0
@sum_delay = 0
@raw.each do |r|
@x_to = [r['delay'].to_i, @x_to].max
@y_to = [r['total'].to_i, @y_to].max
@sum_total = @sum_total + r['total'].to_i
@sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
end
end
private private
# Find project of id params[:id] # Find project of id params[:id]
def find_project def find_project
@@ -130,7 +166,7 @@ private
p.id as priority_id, p.id as priority_id,
count(i.id) as total count(i.id) as total
from from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssuePriority.table_name} p #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
where where
i.status_id=s.id i.status_id=s.id
and i.priority_id=p.id and i.priority_id=p.id
+33 -26
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -24,8 +24,6 @@ class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController class RepositoriesController < ApplicationController
menu_item :repository menu_item :repository
default_search_scope :changesets
before_filter :find_repository, :except => :edit before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit before_filter :find_project, :only => :edit
before_filter :authorize before_filter :authorize
@@ -67,25 +65,30 @@ class RepositoriesController < ApplicationController
end end
def show def show
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty? # check if new revisions have been committed in the repository
@repository.fetch_changesets if Setting.autofetch_changesets?
# root entries
@entries = @repository.entries('', @rev)
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error_not_found unless @entries || @changesets.any?
end
def browse
@entries = @repository.entries(@path, @rev) @entries = @repository.entries(@path, @rev)
if request.xhr? if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true) @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else else
(show_error_not_found; return) unless @entries show_error_not_found and return unless @entries
@changesets = @repository.latest_changesets(@path, @rev)
@properties = @repository.properties(@path, @rev) @properties = @repository.properties(@path, @rev)
render :action => 'show' render :action => 'browse'
end end
end end
alias_method :browse, :show
def changes def changes
@entry = @repository.entry(@path, @rev) @entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry show_error_not_found and return unless @entry
@changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i) @changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
@properties = @repository.properties(@path, @rev) @properties = @repository.properties(@path, @rev)
end end
@@ -97,7 +100,7 @@ class RepositoriesController < ApplicationController
@changesets = @repository.changesets.find(:all, @changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page, :limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset, :offset => @changeset_pages.current.offset,
:include => [:user, :repository]) :include => :user)
respond_to do |format| respond_to do |format|
format.html { render :layout => false if request.xhr? } format.html { render :layout => false if request.xhr? }
@@ -107,15 +110,15 @@ class RepositoriesController < ApplicationController
def entry def entry
@entry = @repository.entry(@path, @rev) @entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry show_error_not_found and return unless @entry
# If the entry is a dir, show the browser # If the entry is a dir, show the browser
(show; return) if @entry.is_dir? browse and return if @entry.is_dir?
@content = @repository.cat(@path, @rev) @content = @repository.cat(@path, @rev)
(show_error_not_found; return) unless @content show_error_not_found and return unless @content
if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte) if 'raw' == params[:format] || @content.is_binary_data?
# Force the download # Force the download if it's a binary file
send_data @content, :filename => @path.split('/').last send_data @content, :filename => @path.split('/').last
else else
# Prevent empty lines when displaying a file with Windows style eol # Prevent empty lines when displaying a file with Windows style eol
@@ -125,14 +128,14 @@ class RepositoriesController < ApplicationController
def annotate def annotate
@entry = @repository.entry(@path, @rev) @entry = @repository.entry(@path, @rev)
(show_error_not_found; return) unless @entry show_error_not_found and return unless @entry
@annotate = @repository.scm.annotate(@path, @rev) @annotate = @repository.scm.annotate(@path, @rev)
(render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty? render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
end end
def revision def revision
@changeset = @repository.find_changeset_by_name(@rev) @changeset = @repository.changesets.find_by_revision(@rev)
raise ChangesetNotFound unless @changeset raise ChangesetNotFound unless @changeset
respond_to do |format| respond_to do |format|
@@ -146,7 +149,7 @@ class RepositoriesController < ApplicationController
def diff def diff
if params[:format] == 'diff' if params[:format] == 'diff'
@diff = @repository.diff(@path, @rev, @rev_to) @diff = @repository.diff(@path, @rev, @rev_to)
(show_error_not_found; return) unless @diff show_error_not_found and return unless @diff
filename = "changeset_r#{@rev}" filename = "changeset_r#{@rev}"
filename << "_r#{@rev_to}" if @rev_to filename << "_r#{@rev_to}" if @rev_to
send_data @diff.join, :filename => "#{filename}.diff", send_data @diff.join, :filename => "#{filename}.diff",
@@ -196,14 +199,17 @@ private
render_404 render_404
end end
REV_PARAM_RE = %r{^[a-f0-9]*$}
def find_repository def find_repository
@project = Project.find(params[:id]) @project = Project.find(params[:id])
@repository = @project.repository @repository = @project.repository
(render_404; return false) unless @repository render_404 and return false unless @repository
@path = params[:path].join('/') unless params[:path].nil? @path = params[:path].join('/') unless params[:path].nil?
@path ||= '' @path ||= ''
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip @rev = params[:rev]
@rev_to = params[:rev_to] @rev_to = params[:rev_to]
raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
rescue InvalidRevisionParam rescue InvalidRevisionParam
@@ -232,7 +238,8 @@ private
changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last } changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
fields = [] fields = []
12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)} month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
graph = SVG::Graph::Bar.new( graph = SVG::Graph::Bar.new(
:height => 300, :height => 300,
@@ -261,7 +268,7 @@ private
def graph_commits_per_author(repository) def graph_commits_per_author(repository)
commits_by_author = repository.changesets.count(:all, :group => :committer) commits_by_author = repository.changesets.count(:all, :group => :committer)
commits_by_author.to_a.sort! {|x, y| x.last <=> y.last} commits_by_author.sort! {|x, y| x.last <=> y.last}
changes_by_author = repository.changes.count(:all, :group => :committer) changes_by_author = repository.changes.count(:all, :group => :committer)
h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o} h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
+19 -6
View File
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class RolesController < ApplicationController class RolesController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => [ :destroy, :move ], verify :method => :post, :only => [ :destroy, :move ],
@@ -42,7 +40,7 @@ class RolesController < ApplicationController
@role.workflows.copy(copy_from) @role.workflows.copy(copy_from)
end end
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index' redirect_to :action => 'list'
end end
@permissions = @role.setable_permissions @permissions = @role.setable_permissions
@roles = Role.find :all, :order => 'builtin, position' @roles = Role.find :all, :order => 'builtin, position'
@@ -52,7 +50,7 @@ class RolesController < ApplicationController
@role = Role.find(params[:id]) @role = Role.find(params[:id])
if request.post? and @role.update_attributes(params[:role]) if request.post? and @role.update_attributes(params[:role])
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index' redirect_to :action => 'list'
end end
@permissions = @role.setable_permissions @permissions = @role.setable_permissions
end end
@@ -60,12 +58,27 @@ class RolesController < ApplicationController
def destroy def destroy
@role = Role.find(params[:id]) @role = Role.find(params[:id])
@role.destroy @role.destroy
redirect_to :action => 'index' redirect_to :action => 'list'
rescue rescue
flash[:error] = 'This role is in use and can not be deleted.' flash[:error] = 'This role is in use and can not be deleted.'
redirect_to :action => 'index' redirect_to :action => 'index'
end end
def move
@role = Role.find(params[:id])
case params[:position]
when 'highest'
@role.move_to_top
when 'higher'
@role.move_higher
when 'lower'
@role.move_lower
when 'lowest'
@role.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def report def report
@roles = Role.find(:all, :order => 'builtin, position') @roles = Role.find(:all, :order => 'builtin, position')
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
@@ -75,7 +88,7 @@ class RolesController < ApplicationController
role.save role.save
end end
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index' redirect_to :action => 'list'
end end
end end
end end
+3 -3
View File
@@ -43,7 +43,7 @@ class SearchController < ApplicationController
begin; offset = params[:offset].to_time if params[:offset]; rescue; end begin; offset = params[:offset].to_time if params[:offset]; rescue; end
# quick jump to an issue # quick jump to an issue
if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1) if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current))
redirect_to :controller => "issues", :action => "show", :id => $1 redirect_to :controller => "issues", :action => "show", :id => $1
return return
end end
@@ -62,8 +62,8 @@ class SearchController < ApplicationController
# extract tokens from the question # extract tokens from the question
# eg. hello "bye bye" => ["hello", "bye bye"] # eg. hello "bye bye" => ["hello", "bye bye"]
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
# tokens must be at least 2 characters long # tokens must be at least 3 character long
@tokens = @tokens.uniq.select {|w| w.length > 1 } @tokens = @tokens.uniq.select {|w| w.length > 2 }
if !@tokens.empty? if !@tokens.empty?
# no more than 5 tokens to search for # no more than 5 tokens to search for
+1 -3
View File
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SettingsController < ApplicationController class SettingsController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
def index def index
@@ -26,7 +24,7 @@ class SettingsController < ApplicationController
end end
def edit def edit
@notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted wiki_content_added wiki_content_updated) @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted)
if request.post? && params[:settings] && params[:settings].is_a?(Hash) if request.post? && params[:settings] && params[:settings].is_a?(Hash)
settings = (params[:settings] || {}).dup.symbolize_keys settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value| settings.each do |name, value|
+2 -19
View File
@@ -38,29 +38,12 @@ class SysController < ActionController::Base
end end
end end
def fetch_changesets
projects = []
if params[:id]
projects << Project.active.has_module(:repository).find(params[:id])
else
projects = Project.active.has_module(:repository).find(:all, :include => :repository)
end
projects.each do |project|
if project.repository
project.repository.fetch_changesets
end
end
render :nothing => true, :status => 200
rescue ActiveRecord::RecordNotFound
render :nothing => true, :status => 404
end
protected protected
def check_enabled def check_enabled
User.current = nil User.current = nil
unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key unless Setting.sys_api_enabled?
render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403 render :nothing => 'Access denied. Repository management WS is disabled.', :status => 403
return false return false
end end
end end
+10 -28
View File
@@ -46,7 +46,7 @@ class TimelogController < ApplicationController
:klass => Tracker, :klass => Tracker,
:label => :label_tracker}, :label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => TimeEntryActivity, :klass => Enumeration,
:label => :label_activity}, :label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id", 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue, :klass => Issue,
@@ -68,13 +68,6 @@ class TimelogController < ApplicationController
:label => cf.name} :label => cf.name}
end end
# Add list and boolean time entry activity custom fields
TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
:format => cf.field_format,
:label => cf.name}
end
@criterias = params[:criterias] || [] @criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq! @criterias.uniq!
@@ -87,23 +80,15 @@ class TimelogController < ApplicationController
unless @criterias.empty? unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ') sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql_condition = ''
if @project.nil?
sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
else
sql_condition = "#{TimeEntry.table_name}.issue_id = #{@issue.id}"
end
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours" sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}" sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id" sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE" sql << " WHERE"
sql << " (%s) AND" % sql_condition sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)] sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on" sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql) @hours = ActiveRecord::Base.connection.select_all(sql)
@@ -147,7 +132,7 @@ class TimelogController < ApplicationController
respond_to do |format| respond_to do |format|
format.html { render :layout => !request.xhr? } format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') } format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end end
end end
@@ -202,19 +187,16 @@ class TimelogController < ApplicationController
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions, :conditions => cond.conditions,
:order => sort_clause) :order => sort_clause)
send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv') send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
} }
end end
end end
end end
def edit def edit
(render_403; return) if @time_entry && !@time_entry.editable_by?(User.current) render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today) @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry] @time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if request.post? and @time_entry.save if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'details', :project_id => @time_entry.project redirect_back_or_default :action => 'details', :project_id => @time_entry.project
@@ -223,8 +205,8 @@ class TimelogController < ApplicationController
end end
def destroy def destroy
(render_404; return) unless @time_entry render_404 and return unless @time_entry
(render_403; return) unless @time_entry.editable_by?(User.current) render_403 and return unless @time_entry.editable_by?(User.current)
@time_entry.destroy @time_entry.destroy
flash[:notice] = l(:notice_successful_delete) flash[:notice] = l(:notice_successful_delete)
redirect_to :back redirect_to :back
+19 -5
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TrackersController < ApplicationController class TrackersController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
def index def index
@@ -25,7 +23,8 @@ class TrackersController < ApplicationController
render :action => 'list' unless request.xhr? render :action => 'list' unless request.xhr?
end end
verify :method => :post, :only => :destroy, :redirect_to => { :action => :list } # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :move ], :redirect_to => { :action => :list }
def list def list
@tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position' @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
@@ -57,6 +56,21 @@ class TrackersController < ApplicationController
@projects = Project.find(:all) @projects = Project.find(:all)
end end
def move
@tracker = Tracker.find(params[:id])
case params[:position]
when 'highest'
@tracker.move_to_top
when 'higher'
@tracker.move_higher
when 'lower'
@tracker.move_lower
when 'lowest'
@tracker.move_to_bottom
end if params[:position]
redirect_to :action => 'list'
end
def destroy def destroy
@tracker = Tracker.find(params[:id]) @tracker = Tracker.find(params[:id])
unless @tracker.issues.empty? unless @tracker.issues.empty?
+21 -63
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,9 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class UsersController < ApplicationController class UsersController < ApplicationController
layout 'admin' before_filter :require_admin
before_filter :require_admin, :except => :show
helper :sort helper :sort
include SortHelper include SortHelper
@@ -26,6 +24,11 @@ class UsersController < ApplicationController
include CustomFieldsHelper include CustomFieldsHelper
def index def index
list
render :action => 'list' unless request.xhr?
end
def list
sort_init 'login', 'asc' sort_init 'login', 'asc'
sort_update %w(login firstname lastname mail admin created_on last_login_on) sort_update %w(login firstname lastname mail admin created_on last_login_on)
@@ -34,7 +37,7 @@ class UsersController < ApplicationController
unless params[:name].blank? unless params[:name].blank?
name = "%#{params[:name].strip.downcase}%" name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name] c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
end end
@user_count = User.count(:conditions => c.conditions) @user_count = User.count(:conditions => c.conditions)
@@ -46,29 +49,7 @@ class UsersController < ApplicationController
:limit => @user_pages.items_per_page, :limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset :offset => @user_pages.current.offset
render :layout => !request.xhr? render :action => "list", :layout => false if request.xhr?
end
def show
@user = User.active.find(params[:id])
@custom_values = @user.custom_values
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project))
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
if @user != User.current && !User.current.admin? && @memberships.empty? && events.empty?
render_404
return
end
render :layout => 'base'
rescue ActiveRecord::RecordNotFound
render_404
end end
def add def add
@@ -82,9 +63,7 @@ class UsersController < ApplicationController
if @user.save if @user.save
Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
redirect_to(params[:continue] ? {:controller => 'users', :action => 'add'} : redirect_to :action => 'list'
{:controller => 'users', :action => 'edit', :id => @user})
return
end end
end end
@auth_sources = AuthSource.find(:all) @auth_sources = AuthSource.find(:all)
@@ -96,51 +75,30 @@ class UsersController < ApplicationController
@user.admin = params[:user][:admin] if params[:user][:admin] @user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login] @user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
@user.group_ids = params[:user][:group_ids] if params[:user][:group_ids] if @user.update_attributes(params[:user])
@user.attributes = params[:user]
# Was the account actived ? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
if @user.save
if was_activated
Mailer.deliver_account_activated(@user)
elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
Mailer.deliver_account_information(@user, params[:password])
end
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :back # Give a string to redirect_to otherwise it would use status param as the response code
redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
end end
end end
@auth_sources = AuthSource.find(:all) @auth_sources = AuthSource.find(:all)
@roles = Role.find_all_givable
@projects = Project.active.find(:all, :order => 'lft')
@membership ||= Member.new @membership ||= Member.new
rescue ::ActionController::RedirectBackError @memberships = @user.memberships
redirect_to :controller => 'users', :action => 'edit', :id => @user
end end
def edit_membership def edit_membership
@user = User.find(params[:id]) @user = User.find(params[:id])
@membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @user) @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
@membership.attributes = params[:membership] @membership.attributes = params[:membership]
@membership.save if request.post? @membership.save if request.post?
respond_to do |format| redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js {
render(:update) {|page|
page.replace_html "tab-content-memberships", :partial => 'users/memberships'
page.visual_effect(:highlight, "member-#{@membership.id}")
}
}
end
end end
def destroy_membership def destroy_membership
@user = User.find(params[:id]) @user = User.find(params[:id])
@membership = Member.find(params[:membership_id]) Member.find(params[:membership_id]).destroy if request.post?
if request.post? && @membership.deletable? redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
@membership.destroy
end
respond_to do |format|
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
end
end end
end end
+10 -34
View File
@@ -17,42 +17,24 @@
class VersionsController < ApplicationController class VersionsController < ApplicationController
menu_item :roadmap menu_item :roadmap
before_filter :find_version, :except => :close_completed before_filter :find_project, :authorize
before_filter :find_project, :only => :close_completed
before_filter :authorize
helper :custom_fields
helper :projects
def show def show
end end
def edit def edit
if request.post? && params[:version] if request.post? and @version.update_attributes(params[:version])
attributes = params[:version].dup flash[:notice] = l(:notice_successful_update)
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing']) redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
if @version.update_attributes(attributes)
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
end end
end end
def close_completed
if request.post?
@project.close_completed_versions
end
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def destroy def destroy
if @version.fixed_issues.empty? @version.destroy
@version.destroy redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project rescue
else flash[:error] = l(:notice_unable_delete_version)
flash[:error] = l(:notice_unable_delete_version) redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
end end
def status_by def status_by
@@ -63,16 +45,10 @@ class VersionsController < ApplicationController
end end
private private
def find_version def find_project
@version = Version.find(params[:id]) @version = Version.find(params[:id])
@project = @version.project @project = @version.project
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end end
+3 -34
View File
@@ -18,18 +18,14 @@
class WatchersController < ApplicationController class WatchersController < ApplicationController
before_filter :find_project before_filter :find_project
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch] before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
before_filter :authorize, :only => [:new, :destroy] before_filter :authorize, :only => :new
verify :method => :post, verify :method => :post,
:only => [ :watch, :unwatch ], :only => [ :watch, :unwatch ],
:render => { :nothing => true, :status => :method_not_allowed } :render => { :nothing => true, :status => :method_not_allowed }
def watch def watch
if @watched.respond_to?(:visible?) && !@watched.visible?(User.current) set_watcher(User.current, true)
render_403
else
set_watcher(User.current, true)
end
end end
def unwatch def unwatch
@@ -52,18 +48,6 @@ class WatchersController < ApplicationController
render :text => 'Watcher added.', :layout => true render :text => 'Watcher added.', :layout => true
end end
def destroy
@watched.set_watcher(User.find(params[:user_id]), false) if request.post?
respond_to do |format|
format.html { redirect_to :back }
format.js do
render :update do |page|
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
end
end
end
end
private private
def find_project def find_project
klass = Object.const_get(params[:object_type].camelcase) klass = Object.const_get(params[:object_type].camelcase)
@@ -76,24 +60,9 @@ private
def set_watcher(user, watching) def set_watcher(user, watching)
@watched.set_watcher(user, watching) @watched.set_watcher(user, watching)
if params[:replace].present?
if params[:replace].is_a? Array
replace_ids = params[:replace]
else
replace_ids = [params[:replace]]
end
else
replace_ids = 'watcher'
end
respond_to do |format| respond_to do |format|
format.html { redirect_to :back } format.html { redirect_to :back }
format.js do format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
render(:update) do |page|
replace_ids.each do |replace_id|
page.replace_html replace_id, watcher_link(@watched, user, :replace => replace_ids)
end
end
end
end end
rescue ::ActionController::RedirectBackError rescue ::ActionController::RedirectBackError
render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
+1 -1
View File
@@ -24,7 +24,7 @@ class WelcomeController < ApplicationController
end end
def robots def robots
@projects = Project.all_public.active @projects = Project.public.active
render :layout => false, :content_type => 'text/plain' render :layout => false, :content_type => 'text/plain'
end end
end end
+2 -28
View File
@@ -18,7 +18,6 @@
require 'diff' require 'diff'
class WikiController < ApplicationController class WikiController < ApplicationController
default_search_scope :wiki_pages
before_filter :find_wiki, :authorize before_filter :find_wiki, :authorize
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy] before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
@@ -26,7 +25,6 @@ class WikiController < ApplicationController
helper :attachments helper :attachments
include AttachmentsHelper include AttachmentsHelper
helper :watchers
# display a page (in editing mode if it doesn't exist) # display a page (in editing mode if it doesn't exist)
def index def index
@@ -84,7 +82,6 @@ class WikiController < ApplicationController
@content.author = User.current @content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record # if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save) if (@page.new_record? ? @page.save : @content.save)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
redirect_to :action => 'index', :id => @project, :page => @page.title redirect_to :action => 'index', :id => @project, :page => @page.title
end end
end end
@@ -134,31 +131,9 @@ class WikiController < ApplicationController
render_404 unless @annotate render_404 unless @annotate
end end
# Removes a wiki page and its history # remove a wiki page and its history
# Children can be either set as root pages, removed or reassigned to another parent page
def destroy def destroy
return render_403 unless editable? return render_403 unless editable?
@descendants_count = @page.descendants.size
if @descendants_count > 0
case params[:todo]
when 'nullify'
# Nothing to do
when 'destroy'
# Removes all its descendants
@page.descendants.each(&:destroy)
when 'reassign'
# Reassign children to another parent page
reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
return unless reassign_to
@page.children.each do |child|
child.update_attribute(:parent, reassign_to)
end
else
@reassignable_to = @wiki.pages - @page.self_and_descendants
return
end
end
@page.destroy @page.destroy
redirect_to :action => 'special', :id => @project, :page => 'Page_index' redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end end
@@ -183,8 +158,7 @@ class WikiController < ApplicationController
return return
else else
# requested special page doesn't exist, redirect to default page # requested special page doesn't exist, redirect to default page
redirect_to :action => 'index', :id => @project, :page => nil redirect_to :action => 'index', :id => @project, :page => nil and return
return
end end
render :action => "special_#{page_title}" render :action => "special_#{page_title}"
end end
+1 -39
View File
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WorkflowsController < ApplicationController class WorkflowsController < ApplicationController
layout 'admin'
before_filter :require_admin before_filter :require_admin
def index def index
@@ -42,42 +40,6 @@ class WorkflowsController < ApplicationController
end end
@roles = Role.find(:all, :order => 'builtin, position') @roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.find(:all, :order => 'position') @trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
@statuses = @tracker.issue_statuses
end
@statuses ||= IssueStatus.find(:all, :order => 'position')
end
def copy
@trackers = Tracker.find(:all, :order => 'position')
@roles = Role.find(:all, :order => 'builtin, position')
if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
@source_tracker = nil
else
@source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
end
if params[:source_role_id].blank? || params[:source_role_id] == 'any'
@source_role = nil
else
@source_role = Role.find_by_id(params[:source_role_id].to_i)
end
@target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
@target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
if request.post?
if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
flash.now[:error] = l(:error_workflow_copy_source)
elsif @target_trackers.nil? || @target_roles.nil?
flash.now[:error] = l(:error_workflow_copy_target)
else
Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
end
end
end end
end end
+114 -142
View File
@@ -22,12 +22,15 @@ require 'cgi'
module ApplicationHelper module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions include Redmine::WikiFormatting::Macros::Definitions
include Redmine::I18n
include GravatarHelper::PublicMethods include GravatarHelper::PublicMethods
extend Forwardable extend Forwardable
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter 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 # Return true if user is authorized for controller/action, otherwise false
def authorize_for(controller, action) def authorize_for(controller, action)
User.current.allowed_to?({:controller => controller, :action => action}, @project) User.current.allowed_to?({:controller => controller, :action => action}, @project)
@@ -44,45 +47,16 @@ module ApplicationHelper
link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
end end
# Displays a link to user's account page if active # Display a link to user's account page
def link_to_user(user, options={}) def link_to_user(user, options={})
if user.is_a?(User) (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
name = h(user.name(options[:format]))
if user.active?
link_to name, :controller => 'users', :action => 'show', :id => user
else
name
end
else
h(user.to_s)
end
end end
# Displays a link to +issue+ with its subject.
# Examples:
#
# link_to_issue(issue) # => Defect #6: This is the subject
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
# link_to_issue(issue, :subject => false) # => Defect #6
# link_to_issue(issue, :project => true) # => Foo - Defect #6
#
def link_to_issue(issue, options={}) def link_to_issue(issue, options={})
title = nil options[:class] ||= ''
subject = nil options[:class] << ' issue'
if options[:subject] == false options[:class] << ' closed' if issue.closed?
title = truncate(issue.subject, :length => 60) link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
else
subject = issue.subject
if options[:truncate]
subject = truncate(subject, :length => options[:truncate])
end
end
s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
:class => issue.css_classes,
:title => title
s << ": #{h subject}" if subject
s = "#{h issue.project} - " + s if options[:project]
s
end end
# Generates a link to an attachment. # Generates a link to an attachment.
@@ -96,15 +70,6 @@ module ApplicationHelper
link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options) link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
end end
# Generates a link to a SCM revision
# Options:
# * :text - Link text (default to the formatted revision)
def link_to_revision(revision, project, options={})
text = options.delete(:text) || format_revision(revision)
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
end
def toggle_link(name, id, options={}) def toggle_link(name, id, options={})
onclick = "Element.toggle('#{id}'); " onclick = "Element.toggle('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
@@ -125,8 +90,25 @@ module ApplicationHelper
link_to name, {}, html_options link_to name, {}, html_options
end end
def format_date(date)
return nil unless date
# "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
date.strftime(@date_format)
end
def format_time(time, include_date = true)
return nil unless time
time = time.to_time if time.is_a?(String)
zone = User.current.time_zone
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
@time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
end
def format_activity_title(text) def format_activity_title(text)
h(truncate_single_line(text, :length => 100)) h(truncate_single_line(text, 100))
end end
def format_activity_day(date) def format_activity_day(date)
@@ -134,15 +116,14 @@ module ApplicationHelper
end end
def format_activity_description(text) def format_activity_description(text)
h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />") h(truncate(text.to_s, 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
end end
def format_version_name(version) def distance_of_date_in_words(from_date, to_date = 0)
if version.project == @project from_date = from_date.to_date if from_date.respond_to?(:to_date)
h(version) to_date = to_date.to_date if to_date.respond_to?(:to_date)
else distance_in_days = (to_date - from_date).abs
h("#{version.project} - #{version}") lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
end
end end
def due_date_distance_in_words(date) def due_date_distance_in_words(date)
@@ -176,24 +157,15 @@ module ApplicationHelper
s s
end end
# Renders tabs and their content
def render_tabs(tabs)
if tabs.any?
render :partial => 'common/tabs', :locals => {:tabs => tabs}
else
content_tag 'p', l(:label_no_data), :class => "nodata"
end
end
# Renders the project quick-jump box # Renders the project quick-jump box
def render_project_jump_box def render_project_jump_box
# Retrieve them now to avoid a COUNT query # Retrieve them now to avoid a COUNT query
projects = User.current.projects.all projects = User.current.projects.all
if projects.any? if projects.any?
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' + s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" + "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
'<option value="" disabled="disabled">---</option>' '<option disabled="disabled">---</option>'
s << project_tree_options_for_select(projects, :selected => @project) do |p| s << project_tree_options_for_select(projects) do |p|
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) } { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
end end
s << '</select>' s << '</select>'
@@ -248,17 +220,9 @@ module ApplicationHelper
s s
end end
def principals_check_box_tags(name, principals)
s = ''
principals.sort.each do |principal|
s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
end
s
end
# Truncates and returns the string as a single line # Truncates and returns the string as a single line
def truncate_single_line(string, *args) def truncate_single_line(string, *args)
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
end end
def html_hours(text) def html_hours(text)
@@ -266,16 +230,25 @@ module ApplicationHelper
end end
def authoring(created, author, options={}) def authoring(created, author, options={})
l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)) time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
link_to(distance_of_time_in_words(Time.now, created),
{:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
:title => format_time(created))
author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
l(options[:label] || :label_added_time_by, author_tag, time_tag)
end end
def time_tag(time) def l_or_humanize(s, options={})
text = distance_of_time_in_words(Time.now, time) k = "#{options[:prefix]}#{s}".to_sym
if @project l_has_string?(k) ? l(k) : s.to_s.humanize
link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time)) end
else
content_tag('acronym', text, :title => format_time(time)) def day_name(day)
end l(:general_day_names).split(',')[day-1]
end
def month_name(month)
l(:actionview_datehelper_select_month_names).split(',')[month-1]
end end
def syntax_highlight(name, content) def syntax_highlight(name, content)
@@ -329,42 +302,15 @@ module ApplicationHelper
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end end
def reorder_links(name, url)
link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
end
def breadcrumb(*args) def breadcrumb(*args)
elements = args.flatten elements = args.flatten
elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
end end
def other_formats_links(&block) def other_formats_links(&block)
concat('<p class="other-formats">' + l(:label_export_to)) concat('<p class="other-formats">' + l(:label_export_to), block.binding)
yield Redmine::Views::OtherFormatsBuilder.new(self) yield Redmine::Views::OtherFormatsBuilder.new(self)
concat('</p>') concat('</p>', block.binding)
end
def page_header_title
if @project.nil? || @project.new_record?
h(Setting.app_title)
else
b = []
ancestors = (@project.root? ? [] : @project.ancestors.visible)
if ancestors.any?
root = ancestors.shift
b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
if ancestors.size > 2
b << '&#8230;'
ancestors = ancestors[-2, 2]
end
b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
end
b << h(@project)
b.join(' &#187; ')
end
end end
def html_title(*args) def html_title(*args)
@@ -373,7 +319,7 @@ module ApplicationHelper
title << @project.name if @project title << @project.name if @project
title += @html_title if @html_title title += @html_title if @html_title
title << Setting.app_title title << Setting.app_title
title.select {|t| !t.blank? }.join(' - ') title.compact.join(' - ')
else else
@html_title ||= [] @html_title ||= []
@html_title += args @html_title += args
@@ -505,24 +451,25 @@ module ApplicationHelper
# export:some/file -> Force the download of the file # export:some/file -> Force the download of the file
# Forum messages: # Forum messages:
# message#1218 -> Link to message with id 1218 # message#1218 -> Link to message with id 1218
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m| text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil link = nil
if esc.nil? if esc.nil?
if prefix.nil? && sep == 'r' if prefix.nil? && sep == 'r'
if project && (changeset = project.changesets.find_by_revision(identifier)) if project && (changeset = project.changesets.find_by_revision(oid))
link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
:class => 'changeset', :class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100)) :title => truncate_single_line(changeset.comments, 100))
end end
elsif sep == '#' elsif sep == '#'
oid = identifier.to_i oid = oid.to_i
case prefix case prefix
when nil when nil
if issue = Issue.visible.find_by_id(oid, :include => :status) if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
:class => issue.css_classes, :class => (issue.closed? ? 'issue closed' : 'issue'),
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
link = content_tag('del', link) if issue.closed?
end end
when 'document' when 'document'
if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current)) if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
@@ -536,7 +483,7 @@ module ApplicationHelper
end end
when 'message' when 'message'
if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current)) if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path, link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
:controller => 'messages', :controller => 'messages',
:action => 'show', :action => 'show',
:board_id => message.board, :board_id => message.board,
@@ -547,7 +494,7 @@ module ApplicationHelper
end end
elsif sep == ':' elsif sep == ':'
# removes the double quotes if any # removes the double quotes if any
name = identifier.gsub(%r{^"(.*)"$}, "\\1") name = oid.gsub(%r{^"(.*)"$}, "\\1")
case prefix case prefix
when 'document' when 'document'
if project && document = project.documents.find_by_title(name) if project && document = project.documents.find_by_title(name)
@@ -563,7 +510,7 @@ module ApplicationHelper
if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"])) if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
:class => 'changeset', :class => 'changeset',
:title => truncate_single_line(changeset.comments, :length => 100) :title => truncate_single_line(changeset.comments, 100)
end end
when 'source', 'export' when 'source', 'export'
if project && project.repository if project && project.repository
@@ -584,7 +531,7 @@ module ApplicationHelper
end end
end end
end end
leading + (link || "#{prefix}#{sep}#{identifier}") leading + (link || "#{prefix}#{sep}#{oid}")
end end
text text
@@ -598,9 +545,46 @@ module ApplicationHelper
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
end end
def error_messages_for(object_name, options = {})
options = options.symbolize_keys
object = instance_variable_get("@#{object_name}")
if object && !object.errors.empty?
# build full_messages here with controller current language
full_messages = []
object.errors.each do |attr, msg|
next if msg.nil?
msg = msg.first if msg.is_a? Array
if attr == "base"
full_messages << l(msg)
else
full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
end
end
# retrieve custom values error messages
if object.errors[:custom_values]
object.custom_values.each do |v|
v.errors.each do |attr, msg|
next if msg.nil?
msg = msg.first if msg.is_a? Array
full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
end
end
end
content_tag("div",
content_tag(
options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
) +
content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
"id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
)
else
""
end
end
def lang_options_for_select(blank=true) def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) + (blank ? [["(auto)", ""]] : []) +
valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
end end
def label_tag_for(name, option_tags = nil, options = {}) def label_tag_for(name, option_tags = nil, options = {})
@@ -628,16 +612,15 @@ module ApplicationHelper
def progress_bar(pcts, options={}) def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array) pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts = pcts.collect(&:round)
pcts[1] = pcts[1] - pcts[0] pcts[1] = pcts[1] - pcts[0]
pcts << (100 - pcts[1] - pcts[0]) pcts << (100 - pcts[1] - pcts[0])
width = options[:width] || '100px;' width = options[:width] || '100px;'
legend = options[:legend] || '' legend = options[:legend] || ''
content_tag('table', content_tag('table',
content_tag('tr', content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') + (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') + (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '') (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
), :class => 'progress', :style => "width: #{width};") + ), :class => 'progress', :style => "width: #{width};") +
content_tag('p', legend, :class => 'pourcent') content_tag('p', legend, :class => 'pourcent')
end end
@@ -668,18 +651,8 @@ module ApplicationHelper
unless @calendar_headers_tags_included unless @calendar_headers_tags_included
@calendar_headers_tags_included = true @calendar_headers_tags_included = true
content_for :header_tags do content_for :header_tags do
start_of_week = case Setting.start_of_week.to_i
when 1
'Calendar._FD = 1;' # Monday
when 7
'Calendar._FD = 0;' # Sunday
else
'' # use language
end
javascript_include_tag('calendar/calendar') + javascript_include_tag('calendar/calendar') +
javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") + javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
javascript_tag(start_of_week) +
javascript_include_tag('calendar/calendar-setup') + javascript_include_tag('calendar/calendar-setup') +
stylesheet_link_tag('calendar') stylesheet_link_tag('calendar')
end end
@@ -700,7 +673,6 @@ module ApplicationHelper
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') # +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 = { }) def avatar(user, options = { })
if Setting.gravatar_enabled? if Setting.gravatar_enabled?
options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
email = nil email = nil
if user.respond_to?(:mail) if user.respond_to?(:mail)
email = user.mail email = user.mail
+6 -31
View File
@@ -18,15 +18,10 @@
module CustomFieldsHelper module CustomFieldsHelper
def custom_fields_tabs def custom_fields_tabs
tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural}, tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time}, {:name => 'TimeEntryCustomField', :label => :label_spent_time},
{:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural}, {:name => 'ProjectCustomField', :label => :label_project_plural},
{:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural}, {:name => 'UserCustomField', :label => :label_user_plural}
{:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
{:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
] ]
end end
@@ -43,7 +38,7 @@ module CustomFieldsHelper
when "text" when "text"
text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%') text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
when "bool" when "bool"
hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id) check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0')
when "list" when "list"
blank_option = custom_field.is_required? ? blank_option = custom_field.is_required? ?
(custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') : (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
@@ -67,26 +62,6 @@ module CustomFieldsHelper
custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value) custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
end end
def custom_field_tag_for_bulk_edit(custom_field)
field_name = "custom_field_values[#{custom_field.id}]"
field_id = "custom_field_values_#{custom_field.id}"
case custom_field.field_format
when "date"
text_field_tag(field_name, '', :id => field_id, :size => 10) +
calendar_for(field_id)
when "text"
text_area_tag(field_name, '', :id => field_id, :rows => 3, :style => 'width:90%')
when "bool"
select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
[l(:general_text_yes), '1'],
[l(:general_text_no), '0']]), :id => field_id)
when "list"
select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values), :id => field_id)
else
text_field_tag(field_name, '', :id => field_id)
end
end
# Return a string used to display a custom value # Return a string used to display a custom value
def show_value(custom_value) def show_value(custom_value)
return "" unless custom_value return "" unless custom_value
@@ -100,7 +75,7 @@ module CustomFieldsHelper
when "date" when "date"
begin; format_date(value.to_date); rescue; value end begin; format_date(value.to_date); rescue; value end
when "bool" when "bool"
l(value == "1" ? :general_text_Yes : :general_text_No) l_YesNo(value == "1")
else else
value value
end end
-34
View File
@@ -1,34 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module GroupsHelper
# Options for the new membership projects combo-box
def options_for_membership_project_select(user, projects)
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
options << project_tree_options_for_select(projects) do |p|
{:disabled => (user.projects.include?(p))}
end
options
end
def group_settings_tabs
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
{:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
]
end
end
+22 -25
View File
@@ -15,6 +15,8 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'csv'
module IssuesHelper module IssuesHelper
include ApplicationHelper include ApplicationHelper
@@ -24,29 +26,18 @@ module IssuesHelper
@cached_label_assigned_to ||= l(:field_assigned_to) @cached_label_assigned_to ||= l(:field_assigned_to)
@cached_label_priority ||= l(:field_priority) @cached_label_priority ||= l(:field_priority)
link_to_issue(issue) + "<br /><br />" + link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" + "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" + "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" + "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}" "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end end
def render_custom_fields_rows(issue) # Returns a string of css classes that apply to the given issue
return if issue.custom_field_values.empty? def css_issue_classes(issue)
ordered_values = [] s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
half = (issue.custom_field_values.size / 2.0).ceil s << ' closed' if issue.closed?
half.times do |i| s << ' overdue' if issue.overdue?
ordered_values << issue.custom_field_values[i]
ordered_values << issue.custom_field_values[i + half]
end
s = "<tr>\n"
n = 0
ordered_values.compact.each do |value|
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
n += 1
end
s << "</tr>\n"
s s
end end
@@ -57,7 +48,6 @@ module IssuesHelper
# Project specific queries and global queries # Project specific queries and global queries
visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]) visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
@sidebar_queries = Query.find(:all, @sidebar_queries = Query.find(:all,
:select => 'id, name',
:order => "name ASC", :order => "name ASC",
:conditions => visible.conditions) :conditions => visible.conditions)
end end
@@ -85,8 +75,8 @@ module IssuesHelper
u = User.find_by_id(detail.value) and value = u.name if detail.value u = User.find_by_id(detail.value) and value = u.name if detail.value
u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
when 'priority_id' when 'priority_id'
e = IssuePriority.find_by_id(detail.value) and value = e.name if detail.value e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
e = IssuePriority.find_by_id(detail.old_value) and old_value = e.name if detail.old_value e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
when 'category_id' when 'category_id'
c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
@@ -129,22 +119,28 @@ module IssuesHelper
case detail.property case detail.property
when 'attr', 'cf' when 'attr', 'cf'
if !detail.old_value.blank? if !detail.old_value.blank?
l(:text_journal_changed, :label => label, :old => old_value, :new => value) label + " " + l(:text_journal_changed, old_value, value)
else else
l(:text_journal_set_to, :label => label, :value => value) label + " " + l(:text_journal_set_to, value)
end end
when 'attachment' when 'attachment'
l(:text_journal_added, :label => label, :value => value) "#{label} #{value} #{l(:label_added)}"
end end
else else
l(:text_journal_deleted, :label => label, :old => old_value) case detail.property
when 'attr', 'cf'
label + " " + l(:text_journal_deleted) + " (#{old_value})"
when 'attachment'
"#{label} #{old_value} #{l(:label_deleted)}"
end
end end
end end
def issues_to_csv(issues, project = nil) def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator) decimal_separator = l(:general_csv_decimal_separator)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields # csv header fields
headers = [ "#", headers = [ "#",
l(:field_status), l(:field_status),
@@ -194,6 +190,7 @@ module IssuesHelper
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end end
end end
export.rewind
export export
end end
end end
+1 -3
View File
@@ -30,9 +30,7 @@ module JournalsHelper
end end
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty? content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes) content << textilizable(journal, :notes)
css_classes = "wiki" content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki'))
css_classes << " editable" if editable
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
end end
def link_to_in_place_notes_editor(text, field_id, url, options={}) def link_to_in_place_notes_editor(text, field_id, url, options={})
+1 -1
View File
@@ -19,7 +19,7 @@ module MessagesHelper
def link_to_message(message) def link_to_message(message)
return '' unless message return '' unless message
link_to h(truncate(message.subject, :length => 60)), :controller => 'messages', link_to h(truncate(message.subject, 60)), :controller => 'messages',
:action => 'show', :action => 'show',
:board_id => message.board_id, :board_id => message.board_id,
:id => message.root, :id => message.root,
+3 -36
View File
@@ -18,7 +18,7 @@
module ProjectsHelper module ProjectsHelper
def link_to_version(version, options = {}) def link_to_version(version, options = {})
return '' unless version && version.is_a?(Version) return '' unless version && version.is_a?(Version)
link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end end
def project_settings_tabs def project_settings_tabs
@@ -29,23 +29,13 @@ module ProjectsHelper
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural}, {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
{:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki}, {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
{:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository}, {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}, {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}
{:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
] ]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end end
def parent_project_select_tag(project) def parent_project_select_tag(project)
selected = project.parent options = '<option></option>' + project_tree_options_for_select(project.possible_parents, :selected => project.parent)
# retrieve the requested parent project
parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
if parent_id
selected = (parent_id.blank? ? nil : Project.find(parent_id))
end
options = ''
options << "<option value=''></option>" if project.allowed_parents.include?(nil)
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
content_tag('select', options, :name => 'project[parent_id]') content_tag('select', options, :name => 'project[parent_id]')
end end
@@ -78,27 +68,4 @@ module ProjectsHelper
end end
s s
end end
# Returns a set of options for a select field, grouped by project.
def version_options_for_select(versions, selected=nil)
grouped = Hash.new {|h,k| h[k] = []}
versions.each do |version|
grouped[version.project.name] << [version.name, version.id]
end
# Add in the selected
if selected && !versions.include?(selected)
grouped[selected.project.name] << [selected.name, selected.id]
end
if grouped.keys.size > 1
grouped_options_for_select(grouped, selected && selected.id)
else
options_for_select((grouped.values.first || []), selected && selected.id)
end
end
def format_version_sharing(sharing)
sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
l("label_version_sharing_#{sharing}")
end
end end
+21 -30
View File
@@ -28,37 +28,28 @@ module QueriesHelper
end end
def column_content(column, issue) def column_content(column, issue)
value = column.value(issue) if column.is_a?(QueryCustomFieldColumn)
cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
case value.class.name show_value(cv)
when 'String'
if column.name == :subject
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
else
h(value)
end
when 'Time'
format_time(value)
when 'Date'
format_date(value)
when 'Fixnum', 'Float'
if column.name == :done_ratio
progress_bar(value, :width => '80px')
else
value.to_s
end
when 'User'
link_to_user value
when 'Project'
link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
when 'Version'
link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
when 'TrueClass'
l(:general_text_Yes)
when 'FalseClass'
l(:general_text_No)
else else
h(value) value = issue.send(column.name)
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
else
case column.name
when :subject
h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
when :done_ratio
progress_bar(value, :width => '80px')
when :fixed_version
link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
else
h(value)
end
end
end end
end end
end end
+4 -3
View File
@@ -121,7 +121,7 @@ module RepositoriesHelper
def repository_field_tags(form, repository) def repository_field_tags(form, repository)
method = repository.class.name.demodulize.underscore + "_field_tags" method = repository.class.name.demodulize.underscore + "_field_tags"
send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags' send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
end end
def scm_select_tag(repository) def scm_select_tag(repository)
@@ -147,12 +147,13 @@ module RepositoriesHelper
def subversion_field_tags(form, repository) def subversion_field_tags(form, repository)
content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) + content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') + '<br />(http://, https://, svn://, file:///)') +
content_tag('p', form.text_field(:login, :size => 30)) + content_tag('p', form.text_field(:login, :size => 30)) +
content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore', content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)), :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
:onfocus => "this.value=''; this.name='repository[password]';", :onfocus => "this.value=''; this.name='repository[password]';",
:onchange => "this.name='repository[password]';")) :onchange => "this.name='repository[password]';")) +
content_tag('p', form.check_box(:cache))
end end
def darcs_field_tags(form, repository) def darcs_field_tags(form, repository)
+1 -2
View File
@@ -27,9 +27,8 @@ module SearchHelper
result << '...' result << '...'
break break
end end
words = words.mb_chars
if i.even? if i.even?
result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words) result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words)
else else
t = (tokens.index(words.downcase) || 0) % 4 t = (tokens.index(words.downcase) || 0) % 4
result << content_tag('span', h(words), :class => "highlight token-#{t}") result << content_tag('span', h(words), :class => "highlight token-#{t}")
+4 -48
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -22,53 +22,9 @@ module SettingsHelper
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication}, {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural}, {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking}, {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification}, {:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails}, {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)},
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural} {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
] ]
end end
def setting_select(setting, choices, options={})
if blank_text = options.delete(:blank)
choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
end
setting_label(setting, options) +
select_tag("settings[#{setting}]", options_for_select(choices, Setting.send(setting).to_s), options)
end
def setting_multiselect(setting, choices, options={})
setting_values = Setting.send(setting)
setting_values = [] unless setting_values.is_a?(Array)
setting_label(setting, options) +
hidden_field_tag("settings[#{setting}][]", '') +
choices.collect do |choice|
text, value = (choice.is_a?(Array) ? choice : [choice, choice])
content_tag('label',
check_box_tag("settings[#{setting}][]", value, Setting.send(setting).include?(value)) + text.to_s,
:class => 'block'
)
end.join
end
def setting_text_field(setting, options={})
setting_label(setting, options) +
text_field_tag("settings[#{setting}]", Setting.send(setting), options)
end
def setting_text_area(setting, options={})
setting_label(setting, options) +
text_area_tag("settings[#{setting}]", Setting.send(setting), options)
end
def setting_check_box(setting, options={})
setting_label(setting, options) +
hidden_field_tag("settings[#{setting}]", 0) +
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options)
end
def setting_label(setting, options={})
label = options.delete(:label)
label != false ? content_tag("label", l(label || "setting_#{setting}")) : ''
end
end end
+71 -126
View File
@@ -1,12 +1,11 @@
# Helpers to sort tables using clickable column headers. # Helpers to sort tables using clickable column headers.
# #
# Author: Stuart Rackham <srackham@methods.co.nz>, March 2005. # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
# Jean-Philippe Lang, 2009
# License: This source code is released under the MIT license. # License: This source code is released under the MIT license.
# #
# - Consecutive clicks toggle the column's sort order. # - Consecutive clicks toggle the column's sort order.
# - Sort state is maintained by a session hash entry. # - Sort state is maintained by a session hash entry.
# - CSS classes identify sort column and state. # - Icon image identifies sort column and state.
# - Typically used in conjunction with the Pagination module. # - Typically used in conjunction with the Pagination module.
# #
# Example code snippets: # Example code snippets:
@@ -18,7 +17,7 @@
# #
# def list # def list
# sort_init 'last_name' # sort_init 'last_name'
# sort_update %w(first_name last_name) # sort_update
# @items = Contact.find_all nil, sort_clause # @items = Contact.find_all nil, sort_clause
# end # end
# #
@@ -29,7 +28,7 @@
# #
# def list # def list
# sort_init 'last_name' # sort_init 'last_name'
# sort_update %w(first_name last_name) # sort_update
# @contact_pages, @items = paginate :contacts, # @contact_pages, @items = paginate :contacts,
# :order_by => sort_clause, # :order_by => sort_clause,
# :per_page => 10 # :per_page => 10
@@ -46,161 +45,88 @@
# </tr> # </tr>
# </thead> # </thead>
# #
# - Introduces instance variables: @sort_default, @sort_criteria # - The ascending and descending sort icon images are sort_asc.png and
# - Introduces param :sort # sort_desc.png and reside in the application's images directory.
# - Introduces instance variables: @sort_name, @sort_default.
# - Introduces params :sort_key and :sort_order.
# #
module SortHelper module SortHelper
class SortCriteria
def initialize # Initializes the default sort column (default_key) and sort order
@criteria = [] # (default_order).
end
def available_criteria=(criteria)
unless criteria.is_a?(Hash)
criteria = criteria.inject({}) {|h,k| h[k] = k; h}
end
@available_criteria = criteria
end
def from_param(param)
@criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
normalize!
end
def criteria=(arg)
@criteria = arg
normalize!
end
def to_param
@criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
end
def to_sql
sql = @criteria.collect do |k,o|
if s = @available_criteria[k]
(o ? s.to_a : s.to_a.collect {|c| "#{c} DESC"}).join(', ')
end
end.compact.join(', ')
sql.blank? ? nil : sql
end
def add!(key, asc)
@criteria.delete_if {|k,o| k == key}
@criteria = [[key, asc]] + @criteria
normalize!
end
def add(*args)
r = self.class.new.from_param(to_param)
r.add!(*args)
r
end
def first_key
@criteria.first && @criteria.first.first
end
def first_asc?
@criteria.first && @criteria.first.last
end
def empty?
@criteria.empty?
end
private
def normalize!
@criteria ||= []
@criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
@criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
@criteria.slice!(3)
self
end
end
def sort_name
controller_name + '_' + action_name + '_sort'
end
# Initializes the default sort.
# Examples:
# #
# sort_init 'name' # - default_key is a column attribute name.
# sort_init 'id', 'desc' # - default_order is 'asc' or 'desc'.
# sort_init ['name', ['id', 'desc']] # - name is the name of the session hash entry that stores the sort state,
# sort_init [['name', 'desc'], ['id', 'desc']] # defaults to '<controller_name>_sort'.
# #
def sort_init(*args) def sort_init(default_key, default_order='asc', name=nil)
case args.size @sort_name = name || params[:controller] + params[:action] + '_sort'
when 1 @sort_default = {:key => default_key, :order => default_order}
@sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
when 2
@sort_default = [[args.first, args.last]]
else
raise ArgumentError
end
end end
# Updates the sort state. Call this in the controller prior to calling # Updates the sort state. Call this in the controller prior to calling
# sort_clause. # sort_clause.
# - criteria can be either an array or a hash of allowed keys # sort_keys can be either an array or a hash of allowed keys
# def sort_update(sort_keys)
def sort_update(criteria) sort_key = params[:sort_key]
@sort_criteria = SortCriteria.new sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key])
@sort_criteria.available_criteria = criteria
@sort_criteria.from_param(params[:sort] || session[sort_name])
@sort_criteria.criteria = @sort_default if @sort_criteria.empty?
session[sort_name] = @sort_criteria.to_param
end
# Clears the sort criteria session data sort_order = (params[:sort_order] == 'desc' ? 'DESC' : 'ASC')
#
def sort_clear if sort_key
session[sort_name] = nil 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 end
# Returns an SQL sort clause corresponding to the current sort state. # Returns an SQL sort clause corresponding to the current sort state.
# Use this to sort the controller's table items collection. # Use this to sort the controller's table items collection.
# #
def sort_clause() def sort_clause()
@sort_criteria.to_sql @sort_clause
end end
# Returns a link which sorts by the named column. # Returns a link which sorts by the named column.
# #
# - column is the name of an attribute in the sorted record collection. # - column is the name of an attribute in the sorted record collection.
# - the optional caption explicitly specifies the displayed link text. # - The optional caption explicitly specifies the displayed link text.
# - 2 CSS classes reflect the state of the link: sort and asc or desc # - A sort icon image is positioned to the right of the sort link.
# #
def sort_link(column, caption, default_order) def sort_link(column, caption, default_order)
css, order = nil, default_order key, order = session[@sort_name][:key], session[@sort_name][:order]
if key == column
if column.to_s == @sort_criteria.first_key if order.downcase == 'asc'
if @sort_criteria.first_asc? icon = 'sort_asc.png'
css = 'sort asc'
order = 'desc' order = 'desc'
else else
css = 'sort desc' icon = 'sort_desc.png'
order = 'asc' order = 'asc'
end end
else
icon = nil
order = default_order
end end
caption = column.to_s.humanize unless caption caption = titleize(Inflector::humanize(column)) unless caption
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param } sort_options = { :sort_key => column, :sort_order => order }
# don't reuse params if filters are present # don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options) url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
# Add project_id to url_options # Add project_id to url_options
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
link_to_remote(caption, link_to_remote(caption,
{:update => "content", :url => url_options, :method => :get}, {:update => "content", :url => url_options, :method => :get},
{:href => url_for(url_options), {:href => url_for(url_options)}) +
:class => css}) (icon ? nbsp(2) + image_tag(icon) : '')
end end
# Returns a table header <th> tag with a sort link for the named column # Returns a table header <th> tag with a sort link for the named column
@@ -216,11 +142,30 @@ module SortHelper
# #
# <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %> # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
# #
# Renders:
#
# <th title="Sort by contact ID" width="40">
# <a href="/contact/list?sort_order=desc&amp;sort_key=id">Id</a>
# &nbsp;&nbsp;<img alt="Sort_asc" src="/images/sort_asc.png" />
# </th>
#
def sort_header_tag(column, options = {}) def sort_header_tag(column, options = {})
caption = options.delete(:caption) || column.to_s.humanize caption = options.delete(:caption) || titleize(Inflector::humanize(column))
default_order = options.delete(:default_order) || 'asc' default_order = options.delete(:default_order) || 'asc'
options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title] options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
content_tag('th', sort_link(column, caption, default_order), options) content_tag('th', sort_link(column, caption, default_order), options)
end end
end
private
# Return n non-breaking spaces.
def nbsp(n)
'&nbsp;' * n
end
# Return capitalized title.
def titleize(title)
title.split.map {|w| w.capitalize }.join(' ')
end
end
+12 -42
View File
@@ -22,43 +22,20 @@ module TimelogHelper
links = [] links = []
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil}) links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
if @issue links << link_to_issue(@issue) if @issue
if @issue.visible?
links << link_to_issue(@issue, :subject => false)
else
links << "##{@issue.id}"
end
end
breadcrumb links breadcrumb links
end end
# Returns a collection of activities for a select field. time_entry def activity_collection_for_select_options
# is optional and will be used to check if the selected TimeEntryActivity activities = Enumeration::get_values('ACTI')
# is active.
def activity_collection_for_select_options(time_entry=nil, project=nil)
project ||= @project
if project.nil?
activities = TimeEntryActivity.shared.active
else
activities = project.activities
end
collection = [] collection = []
if time_entry && time_entry.activity && !time_entry.activity.active? collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
else
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
end
activities.each { |a| collection << [a.name, a.id] } activities.each { |a| collection << [a.name, a.id] }
collection collection
end end
def select_hours(data, criteria, value) def select_hours(data, criteria, value)
if value.to_s.empty? data.select {|row| row[criteria] == value}
data.select {|row| row[criteria].blank? }
else
data.select {|row| row[criteria] == value}
end
end end
def sum_hours(data) def sum_hours(data)
@@ -87,7 +64,8 @@ module TimelogHelper
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator) decimal_separator = l(:general_csv_decimal_separator)
custom_fields = TimeEntryCustomField.find(:all) custom_fields = TimeEntryCustomField.find(:all)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields # csv header fields
headers = [l(:field_spent_on), headers = [l(:field_spent_on),
l(:field_user), l(:field_user),
@@ -120,26 +98,17 @@ module TimelogHelper
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end end
end end
export.rewind
export export
end end
def format_criteria_value(criteria, value) def format_criteria_value(criteria, value)
if value.blank? value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
l(:label_none)
elsif k = @available_criterias[criteria][:klass]
obj = k.find_by_id(value.to_i)
if obj.is_a?(Issue)
obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
else
obj
end
else
format_value(value, @available_criterias[criteria][:format])
end
end end
def report_to_csv(criterias, periods, hours) def report_to_csv(criterias, periods, hours)
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# Column headers # Column headers
headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) } headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
headers += periods headers += periods
@@ -158,6 +127,7 @@ module TimelogHelper
row << "%.2f" %total row << "%.2f" %total
csv << row csv << row
end end
export.rewind
export export
end end
+1 -5
View File
@@ -34,7 +34,7 @@ module UsersHelper
end end
def change_status_link(user) def change_status_link(user)
url = {:controller => 'users', :action => 'edit', :id => user, :page => params[:page], :status => params[:status], :tab => nil} url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]}
if user.locked? if user.locked?
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock' link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
@@ -49,9 +49,5 @@ module UsersHelper
tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
{:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
] ]
if Group.all.any?
tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
end
tabs
end end
end end
+5 -31
View File
@@ -16,28 +16,17 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module WatchersHelper module WatchersHelper
def watcher_tag(object, user)
# Valid options content_tag("span", watcher_link(object, user), :id => 'watcher')
# * :id - the element id
# * :replace - a string or array of element ids that will be
# replaced
def watcher_tag(object, user, options={:replace => 'watcher'})
id = options[:id]
id ||= options[:replace] if options[:replace].is_a? String
content_tag("span", watcher_link(object, user, options), :id => id)
end end
# Valid options def watcher_link(object, user)
# * :replace - a string or array of element ids that will be
# replaced
def watcher_link(object, user, options={:replace => 'watcher'})
return '' unless user && user.logged? && object.respond_to?('watched_by?') return '' unless user && user.logged? && object.respond_to?('watched_by?')
watched = object.watched_by?(user) watched = object.watched_by?(user)
url = {:controller => 'watchers', url = {:controller => 'watchers',
:action => (watched ? 'unwatch' : 'watch'), :action => (watched ? 'unwatch' : 'watch'),
:object_type => object.class.to_s.underscore, :object_type => object.class.to_s.underscore,
:object_id => object.id, :object_id => object.id}
:replace => options[:replace]}
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)), link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
{:url => url}, {:url => url},
:href => url_for(url), :href => url_for(url),
@@ -47,21 +36,6 @@ module WatchersHelper
# Returns a comma separated list of users watching the given object # Returns a comma separated list of users watching the given object
def watchers_list(object) def watchers_list(object)
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project) object.watcher_users.collect {|u| content_tag('span', link_to_user(u), :class => 'user') }.join(",\n")
object.watcher_users.collect do |user|
s = content_tag('span', link_to_user(user), :class => 'user')
if remove_allowed
url = {:controller => 'watchers',
:action => 'destroy',
:object_type => object.class.to_s.underscore,
:object_id => object.id,
:user_id => user}
s += ' ' + link_to_remote(image_tag('delete.png'),
{:url => url},
:href => url_for(url),
:style => "vertical-align: middle")
end
s
end.join(",\n")
end end
end end
+1 -14
View File
@@ -17,19 +17,6 @@
module WikiHelper module WikiHelper
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
s = ''
pages.select {|p| p.parent == parent}.each do |page|
attrs = "value='#{page.id}'"
attrs << " selected='selected'" if selected == page
indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
s << "<option value='#{page.id}'>#{indent}#{h page.pretty_title}</option>\n" +
wiki_page_options_for_select(pages, selected, page, level + 1)
end
s
end
def html_diff(wdiff) def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)} words = wdiff.words.collect{|word| h(word)}
words_add = 0 words_add = 0
@@ -49,7 +36,7 @@ module WikiHelper
words_add += 1 words_add += 1
else else
del_at = pos unless del_at del_at = pos unless del_at
deleted << ' ' + h(change[2]) deleted << ' ' + change[2]
words_del += 1 words_del += 1
end end
end end
+12 -21
View File
@@ -33,7 +33,7 @@ class Attachment < ActiveRecord::Base
:author_key => :author_id, :author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*", :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 " + :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"} "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
acts_as_activity_provider :type => 'documents', acts_as_activity_provider :type => 'documents',
:permission => :view_documents, :permission => :view_documents,
@@ -46,9 +46,7 @@ class Attachment < ActiveRecord::Base
@@storage_path = "#{RAILS_ROOT}/files" @@storage_path = "#{RAILS_ROOT}/files"
def validate def validate
if self.filesize > Setting.attachment_max_size.to_i.kilobytes errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
end
end end
def file=(incoming_file) def file=(incoming_file)
@@ -58,9 +56,6 @@ class Attachment < ActiveRecord::Base
self.filename = sanitize_filename(@temp_file.original_filename) self.filename = sanitize_filename(@temp_file.original_filename)
self.disk_filename = Attachment.disk_filename(filename) self.disk_filename = Attachment.disk_filename(filename)
self.content_type = @temp_file.content_type.to_s.chomp self.content_type = @temp_file.content_type.to_s.chomp
if content_type.blank?
self.content_type = Redmine::MimeType.of(filename)
end
self.filesize = @temp_file.size self.filesize = @temp_file.size
end end
end end
@@ -70,20 +65,14 @@ class Attachment < ActiveRecord::Base
nil nil
end end
# Copies the temporary file to its final location # Copy temp file to its final location
# and computes its MD5 hash
def before_save def before_save
if @temp_file && (@temp_file.size > 0) if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'") logger.debug("saving '#{self.diskfile}'")
md5 = Digest::MD5.new
File.open(diskfile, "wb") do |f| File.open(diskfile, "wb") do |f|
buffer = "" f.write(@temp_file.read)
while (buffer = @temp_file.read(8192))
f.write(buffer)
md5.update(buffer)
end
end end
self.digest = md5.hexdigest self.digest = self.class.digest(diskfile)
end end
# Don't save the content type if it's longer than the authorized length # Don't save the content type if it's longer than the authorized length
if self.content_type && self.content_type.length > 255 if self.content_type && self.content_type.length > 255
@@ -129,11 +118,6 @@ class Attachment < ActiveRecord::Base
self.filename =~ /\.(patch|diff)$/i self.filename =~ /\.(patch|diff)$/i
end end
# Returns true if the file is readable
def readable?
File.readable?(diskfile)
end
private private
def sanitize_filename(value) def sanitize_filename(value)
# get only the filename, not the whole path # get only the filename, not the whole path
@@ -157,4 +141,11 @@ private
end end
df df
end 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 end
-21
View File
@@ -26,25 +26,4 @@ class Board < ActiveRecord::Base
validates_presence_of :name, :description validates_presence_of :name, :description
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
validates_length_of :description, :maximum => 255 validates_length_of :description, :maximum => 255
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_messages, project)
end
def to_s
name
end
def reset_counters!
self.class.reset_counters!(id)
end
# Updates topics_count, messages_count and last_message_id attributes for +board_id+
def self.reset_counters!(board_id)
board_id = board_id.to_i
update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
" messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
["id = ?", board_id])
end
end end
+16 -39
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang # Copyright (C) 2006-2008 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -26,7 +26,7 @@ class Changeset < ActiveRecord::Base
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments, :description => :long_comments,
:datetime => :committed_on, :datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}} :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments', acts_as_searchable :columns => 'comments',
:include => {:repository => :project}, :include => {:repository => :project},
@@ -35,15 +35,12 @@ class Changeset < ActiveRecord::Base
acts_as_activity_provider :timestamp => "#{table_name}.committed_on", acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id, :author_key => :user_id,
:find_options => {:include => [:user, {:repository => :project}]} :find_options => {:include => {:repository => :project}}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_uniqueness_of :revision, :scope => :repository_id validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
named_scope :visible, lambda {|*args| { :include => {:repository => :project},
:conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
def revision=(r) def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s) write_attribute :revision, (r.nil? ? nil : r.to_s)
end end
@@ -57,10 +54,6 @@ class Changeset < ActiveRecord::Base
super super
end end
def committer=(arg)
write_attribute(:committer, self.class.to_utf8(arg.to_s))
end
def project def project
repository.project repository.project
end end
@@ -84,6 +77,9 @@ class Changeset < ActiveRecord::Base
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
# keywords used to fix issues # keywords used to fix issues
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
# status and optional done ratio applied
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
return if kw_regexp.blank? return if kw_regexp.blank?
@@ -93,15 +89,15 @@ class Changeset < ActiveRecord::Base
if ref_keywords.delete('*') if ref_keywords.delete('*')
# find any issue ID in the comments # find any issue ID in the comments
target_issue_ids = [] target_issue_ids = []
comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] } comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
referenced_issues += find_referenced_issues_by_id(target_issue_ids) referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
end end
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match| comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
action = match[0] action = match[0]
target_issue_ids = match[1].scan(/\d+/) target_issue_ids = match[1].scan(/\d+/)
target_issues = find_referenced_issues_by_id(target_issue_ids) target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
if fix_keywords.include?(action.downcase) && fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id) if fix_status && fix_keywords.include?(action.downcase)
# update status of issues # update status of issues
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug? logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
target_issues.each do |issue| target_issues.each do |issue|
@@ -113,21 +109,17 @@ class Changeset < ActiveRecord::Base
if self.scmid && (! (csettext =~ /^r[0-9]+$/)) if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\"" csettext = "commit:\"#{self.scmid}\""
end end
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :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.status = fix_status
unless Setting.commit_fix_done_ratio.blank? issue.done_ratio = done_ratio if done_ratio
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
end
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
issue.save issue.save
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end end
end end
referenced_issues += target_issues referenced_issues += target_issues
end end
referenced_issues.uniq! self.issues = referenced_issues.uniq
self.issues = referenced_issues unless referenced_issues.empty?
end end
def short_comments def short_comments
@@ -155,15 +147,6 @@ class Changeset < ActiveRecord::Base
private private
# Finds issues that can be referenced by the commit message
# i.e. issues that belong to the repository project, a subproject or a parent project
def find_referenced_issues_by_id(ids)
return [] if ids.compact.empty?
Issue.find_all_by_id(ids, :include => :project).select {|issue|
project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
}
end
def split_comments def split_comments
comments =~ /\A(.+?)\r?\n(.*)$/m comments =~ /\A(.+?)\r?\n(.*)$/m
@short_comments = $1 || comments @short_comments = $1 || comments
@@ -176,17 +159,11 @@ class Changeset < ActiveRecord::Base
encoding = Setting.commit_logs_encoding.to_s.strip encoding = Setting.commit_logs_encoding.to_s.strip
unless encoding.blank? || encoding == 'UTF-8' unless encoding.blank? || encoding == 'UTF-8'
begin begin
str = Iconv.conv('UTF-8', encoding, str) return Iconv.conv('UTF-8', encoding, str)
rescue Iconv::Failure rescue Iconv::Failure
# do nothing here # do nothing here
end end
end end
# removes invalid UTF8 sequences str
begin
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
str
end
end end
end end
+3 -22
View File
@@ -48,14 +48,14 @@ class CustomField < ActiveRecord::Base
def validate def validate
if self.field_format == "list" if self.field_format == "list"
errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty? errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
end end
# validate default value # validate default value
v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil) v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
v.custom_field.is_required = false v.custom_field.is_required = false
errors.add(:default_value, :invalid) unless v.valid? errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
end end
# Makes possible_values accept a multiline string # Makes possible_values accept a multiline string
@@ -67,25 +67,6 @@ class CustomField < ActiveRecord::Base
end end
end end
def cast_value(value)
casted = nil
unless value.blank?
case field_format
when 'string', 'text', 'list'
casted = value
when 'date'
casted = begin; value.to_date; rescue; nil end
when 'bool'
casted = (value == '1' ? true : false)
when 'int'
casted = value.to_i
when 'float'
casted = value.to_f
end
end
casted
end
# Returns a ORDER BY clause that can used to sort customized # Returns a ORDER BY clause that can used to sort customized
# objects by their value of the custom field. # objects by their value of the custom field.
# Returns false, if the custom field can not be used for sorting. # Returns false, if the custom field can not be used for sorting.
+9 -9
View File
@@ -20,7 +20,7 @@ class CustomValue < ActiveRecord::Base
belongs_to :customized, :polymorphic => true belongs_to :customized, :polymorphic => true
def after_initialize def after_initialize
if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?)) if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
self.value ||= custom_field.default_value self.value ||= custom_field.default_value
end end
end end
@@ -45,22 +45,22 @@ class CustomValue < ActiveRecord::Base
protected protected
def validate def validate
if value.blank? if value.blank?
errors.add(:value, :blank) if custom_field.is_required? and value.blank? errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank?
else else
errors.add(:value, :invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
errors.add(:value, :too_short, :count => custom_field.min_length) if custom_field.min_length > 0 and value.length < custom_field.min_length errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
errors.add(:value, :too_long, :count => custom_field.max_length) if custom_field.max_length > 0 and value.length > custom_field.max_length errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
# Format specific validations # Format specific validations
case custom_field.field_format case custom_field.field_format
when 'int' when 'int'
errors.add(:value, :not_a_number) unless value =~ /^[+-]?\d+$/ errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/
when 'float' when 'float'
begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
when 'date' when 'date'
errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
when 'list' when 'list'
errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value) errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value)
end end
end end
end end
+2 -21
View File
@@ -17,7 +17,7 @@
class Document < ActiveRecord::Base class Document < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
acts_as_attachable :delete_permission => :manage_documents acts_as_attachable :delete_permission => :manage_documents
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
@@ -29,28 +29,9 @@ class Document < ActiveRecord::Base
validates_presence_of :project, :title, :category validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60 validates_length_of :title, :maximum => 60
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_documents, project)
end
def after_initialize def after_initialize
if new_record? if new_record?
self.category ||= DocumentCategory.default self.category ||= Enumeration.default('DCAT')
end end
end end
def updated_on
unless @updated_on
a = attachments.find(:first, :order => 'created_on DESC')
@updated_on = (a && a.created_on) || created_on
end
@updated_on
end
# Returns the mail adresses of users that should be notified
def recipients
notified = project.notified_users
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end
end end
-34
View File
@@ -1,34 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentCategory < Enumeration
has_many :documents, :foreign_key => 'category_id'
OptionName = :enumeration_doc_categories
def option_name
OptionName
end
def objects_count
documents.count
end
def transfer_relations(to)
documents.update_all("category_id = #{to.id}")
end
end
@@ -1,23 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentCategoryCustomField < CustomField
def type_name
:enumeration_doc_categories
end
end
-22
View File
@@ -1,22 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentObserver < ActiveRecord::Observer
def after_create(document)
Mailer.deliver_document_added(document) if Setting.notified_events.include?('document_added')
end
end
+2 -17
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -20,19 +20,4 @@ class EnabledModule < ActiveRecord::Base
validates_presence_of :name validates_presence_of :name
validates_uniqueness_of :name, :scope => :project_id validates_uniqueness_of :name, :scope => :project_id
after_create :module_enabled
private
# after_create callback used to do things when a module is enabled
def module_enabled
case name
when 'wiki'
# Create a wiki with a default start page
if project && project.wiki.nil?
Wiki.create(:project => project, :start_page => 'Wiki')
end
end
end
end end
+19 -72
View File
@@ -16,67 +16,54 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Enumeration < ActiveRecord::Base class Enumeration < ActiveRecord::Base
default_scope :order => "#{Enumeration.table_name}.position ASC" acts_as_list :scope => 'opt = \'#{opt}\''
belongs_to :project
acts_as_list :scope => 'type = \'#{type}\''
acts_as_customizable
acts_as_tree :order => 'position ASC'
before_destroy :check_integrity before_destroy :check_integrity
validates_presence_of :name validates_presence_of :opt, :name
validates_uniqueness_of :name, :scope => [:type, :project_id] validates_uniqueness_of :name, :scope => [:opt]
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
named_scope :shared, :conditions => { :project_id => nil } # Single table inheritance would be an option
named_scope :active, :conditions => { :active => true } OPTIONS = {
"IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id},
"DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id},
"ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id}
}.freeze
def self.default def self.get_values(option)
# Creates a fake default scope so Enumeration.default will check find(:all, :conditions => {:opt => option}, :order => 'position')
# it's type. STI subclasses will automatically add their own end
# types to the finder.
if self.descends_from_active_record? def self.default(option)
find(:first, :conditions => { :is_default => true, :type => 'Enumeration' }) find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position')
else
# STI classes are
find(:first, :conditions => { :is_default => true })
end
end end
# Overloaded on concrete classes
def option_name def option_name
nil OPTIONS[self.opt][:label]
end end
def before_save def before_save
if is_default? && is_default_changed? if is_default? && is_default_changed?
Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type}) Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
end end
end end
# Overloaded on concrete classes
def objects_count def objects_count
0 OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
end end
def in_use? def in_use?
self.objects_count != 0 self.objects_count != 0
end end
# Is this enumeration overiding a system level enumeration?
def is_override?
!self.parent.nil?
end
alias :destroy_without_reassign :destroy alias :destroy_without_reassign :destroy
# Destroy the enumeration # Destroy the enumeration
# If a enumeration is specified, objects are reassigned # If a enumeration is specified, objects are reassigned
def destroy(reassign_to = nil) def destroy(reassign_to = nil)
if reassign_to && reassign_to.is_a?(Enumeration) if reassign_to && reassign_to.is_a?(Enumeration)
self.transfer_relations(reassign_to) OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
end end
destroy_without_reassign destroy_without_reassign
end end
@@ -87,48 +74,8 @@ class Enumeration < ActiveRecord::Base
def to_s; name end def to_s; name end
# Returns the Subclasses of Enumeration. Each Subclass needs to be
# required in development mode.
#
# Note: subclasses is protected in ActiveRecord
def self.get_subclasses
@@subclasses[Enumeration]
end
# Does the +new+ Hash override the previous Enumeration?
def self.overridding_change?(new, previous)
if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
return false
else
return true
end
end
# Does the +new+ Hash have the same custom values as the previous Enumeration?
def self.same_custom_values?(new, previous)
previous.custom_field_values.each do |custom_value|
if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
return false
end
end
return true
end
# Are the new and previous fields equal?
def self.same_active_state?(new, previous)
new = (new == "1" ? true : false)
return new == previous
end
private private
def check_integrity def check_integrity
raise "Can't delete enumeration" if self.in_use? raise "Can't delete enumeration" if self.in_use?
end end
end end
# Force load the subclasses in development mode
require_dependency 'time_entry_activity'
require_dependency 'document_category'
require_dependency 'issue_priority'
-48
View File
@@ -1,48 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Group < Principal
has_and_belongs_to_many :users, :after_add => :user_added,
:after_remove => :user_removed
acts_as_customizable
validates_presence_of :lastname
validates_uniqueness_of :lastname, :case_sensitive => false
validates_length_of :lastname, :maximum => 30
def to_s
lastname.to_s
end
def user_added(user)
members.each do |member|
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
member.member_roles.each do |member_role|
user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
end
user_member.save!
end
end
def user_removed(user)
members.each do |member|
MemberRole.find(:all, :include => :member,
:conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
end
end
end
-22
View File
@@ -1,22 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class GroupCustomField < CustomField
def type_name
:label_group_plural
end
end
+45 -215
View File
@@ -22,7 +22,7 @@ class Issue < ActiveRecord::Base
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy has_many :journals, :as => :journalized, :dependent => :destroy
@@ -39,15 +39,13 @@ class Issue < ActiveRecord::Base
:include => [:project, :journals], :include => [:project, :journals],
# sort by id so that limited eager loading doesn't break with postgresql # sort by id so that limited eager loading doesn't break with postgresql
:order_column => "#{table_name}.id" :order_column => "#{table_name}.id"
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } :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 :author_key => :author_id
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
validates_presence_of :subject, :priority, :project, :tracker, :author, :status validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255 validates_length_of :subject, :maximum => 255
validates_inclusion_of :done_ratio, :in => 0..100 validates_inclusion_of :done_ratio, :in => 0..100
@@ -56,11 +54,6 @@ class Issue < ActiveRecord::Base
named_scope :visible, lambda {|*args| { :include => :project, named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } } :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
before_save :update_done_ratio_from_issue_status
after_save :create_journal
# Returns true if usr or current user is allowed to view the issue # Returns true if usr or current user is allowed to view the issue
def visible?(usr=nil) def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project) (usr || User.current).allowed_to?(:view_issues, self.project)
@@ -70,7 +63,7 @@ class Issue < ActiveRecord::Base
if new_record? if new_record?
# set default values for new records only # set default values for new records only
self.status ||= IssueStatus.default self.status ||= IssueStatus.default
self.priority ||= IssuePriority.default self.priority ||= Enumeration.default('IPRI')
end end
end end
@@ -80,10 +73,9 @@ class Issue < ActiveRecord::Base
end end
def copy_from(arg) def copy_from(arg)
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on") self.attributes = issue.attributes.dup
self.custom_values = issue.custom_values.collect {|v| v.clone} self.custom_values = issue.custom_values.collect {|v| v.clone}
self.status = issue.status
self self
end end
@@ -92,7 +84,7 @@ class Issue < ActiveRecord::Base
def move_to(new_project, new_tracker = nil, options = {}) def move_to(new_project, new_tracker = nil, options = {})
options ||= {} options ||= {}
issue = options[:copy] ? self.clone : self issue = options[:copy] ? self.clone : self
ret = Issue.transaction do transaction do
if new_project && issue.project_id != new_project.id if new_project && issue.project_id != new_project.id
# delete issue relations # delete issue relations
unless Setting.cross_project_issue_relations? unless Setting.cross_project_issue_relations?
@@ -103,10 +95,7 @@ class Issue < ActiveRecord::Base
# reassign to the category with same name if any # reassign to the category with same name if any
new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name) new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
issue.category = new_category issue.category = new_category
# Keep the fixed_version if it's still valid in the new_project issue.fixed_version = nil
unless new_project.shared_versions.include?(issue.fixed_version)
issue.fixed_version = nil
end
issue.project = new_project issue.project = new_project
end end
if new_tracker if new_tracker
@@ -114,27 +103,19 @@ class Issue < ActiveRecord::Base
end end
if options[:copy] if options[:copy]
issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
issue.status = if options[:attributes] && options[:attributes][:status_id] issue.status = self.status
IssueStatus.find_by_id(options[:attributes][:status_id])
else
self.status
end
end
# Allow bulk setting of attributes on the issue
if options[:attributes]
issue.attributes = options[:attributes]
end end
if issue.save if issue.save
unless options[:copy] unless options[:copy]
# Manually update project_id on related time entries # Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
end end
true
else else
raise ActiveRecord::Rollback Issue.connection.rollback_db_transaction
return false
end end
end end
ret ? issue : false return issue
end end
def priority_id=(pid) def priority_id=(pid)
@@ -142,73 +123,26 @@ class Issue < ActiveRecord::Base
write_attribute(:priority_id, pid) write_attribute(:priority_id, pid)
end end
def tracker_id=(tid)
self.tracker = nil
write_attribute(:tracker_id, tid)
result = write_attribute(:tracker_id, tid)
@custom_field_values = nil
result
end
# Overrides attributes= so that tracker_id gets assigned first
def attributes_with_tracker_first=(new_attributes, *args)
return if new_attributes.nil?
new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
if new_tracker_id
self.tracker_id = new_tracker_id
end
send :attributes_without_tracker_first=, new_attributes, *args
end
# Do not redefine alias chain on reload (see #4838)
alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
def estimated_hours=(h) def estimated_hours=(h)
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end end
def done_ratio
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
status.default_done_ratio
else
read_attribute(:done_ratio)
end
end
def self.use_status_for_done_ratio?
Setting.issue_done_ratio == 'issue_status'
end
def self.use_field_for_done_ratio?
Setting.issue_done_ratio == 'issue_field'
end
def validate def validate
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
errors.add :due_date, :not_a_date errors.add :due_date, :activerecord_error_not_a_date
end end
if self.due_date and self.start_date and self.due_date < self.start_date if self.due_date and self.start_date and self.due_date < self.start_date
errors.add :due_date, :greater_than_start_date errors.add :due_date, :activerecord_error_greater_than_start_date
end end
if start_date && soonest_start && start_date < soonest_start if start_date && soonest_start && start_date < soonest_start
errors.add :start_date, :invalid errors.add :start_date, :activerecord_error_invalid
end end
end
if fixed_version def validate_on_create
if !assignable_versions.include?(fixed_version) errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
errors.add :fixed_version_id, :inclusion
elsif reopened? && fixed_version.closed?
errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
end
end
# Checks that the issue can not be added/moved to a disabled tracker
if project && (tracker_id_changed? || project_id_changed?)
unless project.trackers.include?(tracker)
errors.add :tracker_id, :inclusion
end
end
end end
def before_create def before_create
@@ -218,12 +152,28 @@ class Issue < ActiveRecord::Base
end end
end end
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios def before_save
# even if the user turns off the setting later if @current_journal
def update_done_ratio_from_issue_status # attributes changes
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio? (Issue.column_names - %w(id description)).each {|c|
self.done_ratio = status.default_done_ratio @current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value)
}
@current_journal.save
end end
# Save the issue even if the journal is not saved (because empty)
true
end end
def after_save def after_save
@@ -263,18 +213,6 @@ class Issue < ActiveRecord::Base
self.status.is_closed? self.status.is_closed?
end end
# Return true if the issue is being reopened
def reopened?
if !new_record? && status_id_changed?
status_was = IssueStatus.find_by_id(status_id_was)
status_new = IssueStatus.find_by_id(status_id)
if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
return true
end
end
false
end
# Returns true if the issue is overdue # Returns true if the issue is overdue
def overdue? def overdue?
!due_date.nil? && (due_date < Date.today) && !status.is_closed? !due_date.nil? && (due_date < Date.today) && !status.is_closed?
@@ -285,41 +223,22 @@ class Issue < ActiveRecord::Base
project.assignable_users project.assignable_users
end end
# Versions that the issue can be assigned to
def assignable_versions
@assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
end
# Returns true if this issue is blocked by another issue that is still open
def blocked?
!relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
end
# Returns an array of status that user is able to apply # Returns an array of status that user is able to apply
def new_statuses_allowed_to(user) def new_statuses_allowed_to(user)
statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker) statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
statuses << status unless statuses.empty? statuses << status unless statuses.empty?
statuses = statuses.uniq.sort statuses.uniq.sort
blocked? ? statuses.reject {|s| s.is_closed?} : statuses
end end
# Returns the mail adresses of users that should be notified # Returns the mail adresses of users that should be notified for the issue
def recipients def recipients
notified = project.notified_users recipients = project.recipients
# Author and assignee are always notified unless they have been locked # Author and assignee are always notified unless they have been locked
notified << author if author && author.active? recipients << author.mail if author && author.active?
notified << assigned_to if assigned_to && assigned_to.active? recipients << assigned_to.mail if assigned_to && assigned_to.active?
notified.uniq! recipients.compact.uniq
# Remove users that can not view the issue
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end end
# Returns the total number of hours spent on this issue.
#
# Example:
# spent_hours => 0
# spent_hours => 50
def spent_hours def spent_hours
@spent_hours ||= time_entries.sum(:hours) || 0 @spent_hours ||= time_entries.sum(:hours) || 0
end end
@@ -348,11 +267,6 @@ class Issue < ActiveRecord::Base
due_date || (fixed_version ? fixed_version.effective_date : nil) due_date || (fixed_version ? fixed_version.effective_date : nil)
end end
# Returns the time scheduled for this issue.
#
# Example:
# Start Date: 2/26/09, End Date: 3/04/09
# duration => 6
def duration def duration
(start_date && due_date) ? due_date - start_date : 0 (start_date && due_date) ? due_date - start_date : 0
end end
@@ -365,68 +279,8 @@ class Issue < ActiveRecord::Base
"#{tracker} ##{id}: #{subject}" "#{tracker} ##{id}: #{subject}"
end end
# Returns a string of css classes that apply to the issue
def css_classes
s = "issue status-#{status.position} priority-#{priority.position}"
s << ' closed' if closed?
s << ' overdue' if overdue?
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
s
end
# Unassigns issues from +version+ if it's no longer shared with issue's project
def self.update_versions_from_sharing_change(version)
# Update issues assigned to the version
update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
end
# Unassigns issues from versions that are no longer shared
# after +project+ was moved
def self.update_versions_from_hierarchy_change(project)
moved_project_ids = project.self_and_descendants.reload.collect(&:id)
# Update issues of the moved projects and issues assigned to a version of a moved project
Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
end
# Returns an array of projects that current user can move issues to
def self.allowed_target_projects_on_move
projects = []
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
projects = Project.visible.all
elsif User.current.logged?
if Role.non_member.allowed_to?(:move_issues)
projects = Project.visible.all
else
User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
end
end
projects
end
private private
# Update issues so their versions are not pointing to a
# fixed_version that is not shared with the issue's project
def self.update_versions(conditions=nil)
# Only need to update issues with a fixed_version from
# a different project and that is not systemwide shared
Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
" AND #{Version.table_name}.sharing <> 'system'",
conditions),
:include => [:project, :fixed_version]
).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version)
issue.init_journal(User.current)
issue.fixed_version = nil
issue.save
end
end
end
# Callback on attachment deletion # Callback on attachment deletion
def attachment_removed(obj) def attachment_removed(obj)
journal = init_journal(User.current) journal = init_journal(User.current)
@@ -435,28 +289,4 @@ class Issue < ActiveRecord::Base
:old_value => obj.filename) :old_value => obj.filename)
journal.save journal.save
end end
# Saves the changes in a Journal
# Called after_save
def create_journal
if @current_journal
# attributes changes
(Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value)
}
@current_journal.save
end
end
end end
-22
View File
@@ -1,22 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueObserver < ActiveRecord::Observer
def after_create(issue)
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
end
end
-34
View File
@@ -1,34 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuePriority < Enumeration
has_many :issues, :foreign_key => 'priority_id'
OptionName = :enumeration_issue_priorities
def option_name
OptionName
end
def objects_count
issues.count
end
def transfer_relations(to)
issues.update_all("priority_id = #{to.id}")
end
end
-23
View File
@@ -1,23 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuePriorityCustomField < CustomField
def type_name
:enumeration_issue_priorities
end
end
+3 -18
View File
@@ -23,13 +23,11 @@ class IssueRelation < ActiveRecord::Base
TYPE_DUPLICATES = "duplicates" TYPE_DUPLICATES = "duplicates"
TYPE_BLOCKS = "blocks" TYPE_BLOCKS = "blocks"
TYPE_PRECEDES = "precedes" TYPE_PRECEDES = "precedes"
TYPE_FOLLOWS = "follows"
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 }, TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 }, TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 }, TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 5 }
}.freeze }.freeze
validates_presence_of :issue_from, :issue_to, :relation_type validates_presence_of :issue_from, :issue_to, :relation_type
@@ -41,9 +39,9 @@ class IssueRelation < ActiveRecord::Base
def validate def validate
if issue_from && issue_to if issue_from && issue_to
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
end end
end end
@@ -56,8 +54,6 @@ class IssueRelation < ActiveRecord::Base
end end
def before_save def before_save
reverse_if_needed
if TYPE_PRECEDES == relation_type if TYPE_PRECEDES == relation_type
self.delay ||= 0 self.delay ||= 0
else else
@@ -82,15 +78,4 @@ class IssueRelation < ActiveRecord::Base
def <=>(relation) def <=>(relation)
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
end end
private
def reverse_if_needed
if (TYPE_FOLLOWS == relation_type)
issue_tmp = issue_to
self.issue_to = issue_from
self.issue_from = issue_tmp
self.relation_type = TYPE_PRECEDES
end
end
end end
+11 -34
View File
@@ -24,7 +24,6 @@ class IssueStatus < ActiveRecord::Base
validates_uniqueness_of :name validates_uniqueness_of :name
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
def after_save def after_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default? IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
@@ -35,48 +34,26 @@ class IssueStatus < ActiveRecord::Base
find(:first, :conditions =>["is_default=?", true]) find(:first, :conditions =>["is_default=?", true])
end end
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
def self.update_issue_done_ratios
if Issue.use_status_for_done_ratio?
IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
Issue.update_all(["done_ratio = ?", status.default_done_ratio],
["status_id = ?", status.id])
end
end
return Issue.use_status_for_done_ratio?
end
# Returns an array of all statuses the given role can switch to # Returns an array of all statuses the given role can switch to
# Uses association cache when called more than one time # Uses association cache when called more than one time
def new_statuses_allowed_to(roles, tracker) def new_statuses_allowed_to(role, tracker)
if roles && tracker new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker
role_ids = roles.collect(&:id) new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : []
new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
else
[]
end
end end
# Same thing as above but uses a database query # Same thing as above but uses a database query
# More efficient than the previous method if called just once # More efficient than the previous method if called just once
def find_new_statuses_allowed_to(roles, tracker) def find_new_statuses_allowed_to(role, tracker)
if roles && tracker new_statuses = workflows.find(:all,
workflows.find(:all, :include => :new_status,
:include => :new_status, :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker
:conditions => { :role_id => roles.collect(&:id), new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : []
:tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
else
[]
end
end end
def new_status_allowed_to?(status, roles, tracker) def new_status_allowed_to?(status, role, tracker)
if status && roles && tracker status && role && tracker ?
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil? !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? :
else
false false
end
end end
def <=>(status) def <=>(status)
+1 -1
View File
@@ -38,7 +38,7 @@ class Journal < ActiveRecord::Base
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
def save(*args) def save
# Do not save an empty journal # Do not save an empty journal
(details.empty? && notes.blank?) ? false : super (details.empty? && notes.blank?) ? false : super
end end
-22
View File
@@ -1,22 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalObserver < ActiveRecord::Observer
def after_create(journal)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
end
+24 -101
View File
@@ -34,46 +34,19 @@ class MailHandler < ActionMailer::Base
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
# Status overridable by default # Status overridable by default
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
super email super email
end end
# Processes incoming emails # Processes incoming emails
# Returns the created object (eg. an issue, a message) or false
def receive(email) def receive(email)
@email = email @email = email
sender_email = email.from.to_a.first.to_s.strip @user = User.active.find_by_mail(email.from.first.to_s.strip)
# Ignore emails received from the application emission address to avoid hell cycles unless @user
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase # Unknown user => the email is ignored
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info # TODO: ability to create the user's account
logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
return false return false
end end
@user = User.find_by_mail(sender_email)
if @user && !@user.active?
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
return false
end
if @user.nil?
# Email was submitted by an unknown user
case @@handler_options[:unknown_user]
when 'accept'
@user = User.anonymous
when 'create'
@user = MailHandler.create_user_from_email(email)
if @user
logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
Mailer.deliver_account_information(@user, @user.password)
else
logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
return false
end
else
# Default behaviour, emails from unknown users are ignored
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
return false
end
end
User.current = @user User.current = @user
dispatch dispatch
end end
@@ -81,15 +54,15 @@ class MailHandler < ActionMailer::Base
private private
MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@} MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]} ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]} MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]+msg(\d+)\]}
def dispatch def dispatch
headers = [email.in_reply_to, email.references].flatten.compact headers = [email.in_reply_to, email.references].flatten.compact
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE} if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
klass, object_id = $1, $2.to_i klass, object_id = $1, $2.to_i
method_name = "receive_#{klass}_reply" method_name = "receive_#{klass}_reply"
if self.class.private_instance_methods.collect(&:to_s).include?(method_name) if self.class.private_instance_methods.include?(method_name)
send method_name, object_id send method_name, object_id
else else
# ignoring it # ignoring it
@@ -118,23 +91,18 @@ class MailHandler < ActionMailer::Base
project = target_project project = target_project
tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) 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))) category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority))) 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))) status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
# check permission # check permission
unless @@handler_options[:no_permission_check] raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
end
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority) issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
# check workflow # check workflow
if status && issue.new_statuses_allowed_to(user).include?(status) if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status issue.status = status
end end
issue.subject = email.subject.chomp issue.subject = email.subject.chomp.toutf8
if issue.subject.blank? issue.description = plain_text_body
issue.subject = '(no subject)'
end
# custom fields # custom fields
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c| issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
if value = get_keyword(c.name, :override => true) if value = get_keyword(c.name, :override => true)
@@ -142,12 +110,13 @@ class MailHandler < ActionMailer::Base
end end
h h
end end
issue.description = cleaned_up_text_body
# add To and Cc as watchers before saving so the watchers can reply to Redmine
add_watchers(issue)
issue.save! issue.save!
add_attachments(issue) add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && 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 issue
end end
@@ -167,13 +136,11 @@ class MailHandler < ActionMailer::Base
issue = Issue.find_by_id(issue_id) issue = Issue.find_by_id(issue_id)
return unless issue return unless issue
# check permission # check permission
unless @@handler_options[:no_permission_check] raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
end
# add the note # add the note
journal = issue.init_journal(user, cleaned_up_text_body) journal = issue.init_journal(user, plain_text_body)
add_attachments(issue) add_attachments(issue)
# check workflow # check workflow
if status && issue.new_statuses_allowed_to(user).include?(status) if status && issue.new_statuses_allowed_to(user).include?(status)
@@ -181,6 +148,7 @@ class MailHandler < ActionMailer::Base
end end
issue.save! issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info 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 journal
end end
@@ -197,21 +165,16 @@ class MailHandler < ActionMailer::Base
message = Message.find_by_id(message_id) message = Message.find_by_id(message_id)
if message if message
message = message.root message = message.root
if user.allowed_to?(:add_messages, message.project) && !message.locked?
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
end
if !message.locked?
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip, reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
:content => cleaned_up_text_body) :content => plain_text_body)
reply.author = user reply.author = user
reply.board = message.board reply.board = message.board
message.children << reply message.children << reply
add_attachments(reply) add_attachments(reply)
reply reply
else else
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info raise UnauthorizedAction
end end
end end
end end
@@ -245,7 +208,7 @@ class MailHandler < ActionMailer::Base
@keywords[attr] @keywords[attr]
else else
@keywords[attr] = begin @keywords[attr] = begin
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '') if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}:[ \t]*(.+)\s*$/i, '')
$1.strip $1.strip
elsif !@@handler_options[:issue][attr].blank? elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr] @@handler_options[:issue][attr]
@@ -272,45 +235,5 @@ class MailHandler < ActionMailer::Base
@plain_text_body = plain_text_part.body.to_s @plain_text_body = plain_text_part.body.to_s
end end
@plain_text_body.strip! @plain_text_body.strip!
@plain_text_body
end
def cleaned_up_text_body
cleanup_body(plain_text_body)
end
def self.full_sanitizer
@full_sanitizer ||= HTML::FullSanitizer.new
end
# Creates a user account for the +email+ sender
def self.create_user_from_email(email)
addr = email.from_addrs.to_a.first
if addr && !addr.spec.blank?
user = User.new
user.mail = addr.spec
names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
user.firstname = names.shift
user.lastname = names.join(' ')
user.lastname = '-' if user.lastname.blank?
user.login = user.mail
user.password = ActiveSupport::SecureRandom.hex(5)
user.language = Setting.default_language
user.save ? user : nil
end
end
private
# Removes the email body of text after the truncation configurations.
def cleanup_body(body)
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
unless delimiters.empty?
regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
body = body.gsub(regex, '')
end
body.strip
end end
end end
+31 -148
View File
@@ -16,25 +16,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Mailer < ActionMailer::Base class Mailer < ActionMailer::Base
layout 'mailer'
helper :application helper :application
helper :issues helper :issues
helper :custom_fields helper :custom_fields
include ActionController::UrlWriter include ActionController::UrlWriter
include Redmine::I18n
def self.default_url_options
h = Setting.host_name
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
{ :host => h, :protocol => Setting.protocol }
end
# Builds a tmail object used to email recipients of the added issue.
#
# Example:
# issue_add(issue) => tmail object
# Mailer.deliver_issue_add(issue) => sends an email to issue recipients
def issue_add(issue) def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier, redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id, 'Issue-Id' => issue.id,
@@ -46,16 +33,10 @@ class Mailer < ActionMailer::Base
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}" subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue, body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
render_multipart('issue_add', body)
end end
# Builds a tmail object used to email recipients of the edited issue.
#
# Example:
# issue_edit(journal) => tmail object
# Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
def issue_edit(journal) def issue_edit(journal)
issue = journal.journalized.reload issue = journal.journalized
redmine_headers 'Project' => issue.project.identifier, redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id, 'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login 'Issue-Author' => issue.author.login
@@ -73,8 +54,6 @@ class Mailer < ActionMailer::Base
body :issue => issue, body :issue => issue,
:journal => journal, :journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
render_multipart('issue_edit', body)
end end
def reminder(user, issues, days) def reminder(user, issues, days)
@@ -84,28 +63,16 @@ class Mailer < ActionMailer::Base
body :issues => issues, body :issues => issues,
:days => days, :days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc') :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
render_multipart('reminder', body)
end end
# Builds a tmail object used to email users belonging to the added document's project.
#
# Example:
# document_added(document) => tmail object
# Mailer.deliver_document_added(document) => sends an email to the document's project recipients
def document_added(document) def document_added(document)
redmine_headers 'Project' => document.project.identifier redmine_headers 'Project' => document.project.identifier
recipients document.recipients recipients document.project.recipients
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}" subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
body :document => document, body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document) :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
render_multipart('document_added', body)
end end
# Builds a tmail object used to email recipients of a project when an attachements are added.
#
# Example:
# attachments_added(attachments) => tmail object
# Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
def attachments_added(attachments) def attachments_added(attachments)
container = attachments.first.container container = attachments.first.container
added_to = '' added_to = ''
@@ -114,97 +81,41 @@ class Mailer < ActionMailer::Base
when 'Project' when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container) added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to = "#{l(:label_project)}: #{container}" added_to = "#{l(:label_project)}: #{container}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Version' when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id) added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}" added_to = "#{l(:label_version)}: #{container.name}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Document' when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}" added_to = "#{l(:label_document)}: #{container.title}"
recipients container.recipients
end end
redmine_headers 'Project' => container.project.identifier redmine_headers 'Project' => container.project.identifier
recipients container.project.recipients
subject "[#{container.project.name}] #{l(:label_attachment_new)}" subject "[#{container.project.name}] #{l(:label_attachment_new)}"
body :attachments => attachments, body :attachments => attachments,
:added_to => added_to, :added_to => added_to,
:added_to_url => added_to_url :added_to_url => added_to_url
render_multipart('attachments_added', body)
end end
# Builds a tmail object used to email recipients of a news' project when a news item is added.
#
# Example:
# news_added(news) => tmail object
# Mailer.deliver_news_added(news) => sends an email to the news' project recipients
def news_added(news) def news_added(news)
redmine_headers 'Project' => news.project.identifier redmine_headers 'Project' => news.project.identifier
message_id news message_id news
recipients news.recipients recipients news.project.recipients
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}" subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news, body :news => news,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news) :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
render_multipart('news_added', body)
end end
# Builds a tmail object used to email the recipients of the specified message that was posted. def message_posted(message, recipients)
#
# Example:
# message_posted(message) => tmail object
# Mailer.deliver_message_posted(message) => sends an email to the recipients
def message_posted(message)
redmine_headers 'Project' => message.project.identifier, redmine_headers 'Project' => message.project.identifier,
'Topic-Id' => (message.parent_id || message.id) 'Topic-Id' => (message.parent_id || message.id)
message_id message message_id message
references message.parent unless message.parent.nil? references message.parent unless message.parent.nil?
recipients(message.recipients) recipients(recipients)
cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message, body :message => message,
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root) :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
render_multipart('message_posted', body)
end end
# Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
#
# Example:
# wiki_content_added(wiki_content) => tmail object
# Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
def wiki_content_added(wiki_content)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :page => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title)
render_multipart('wiki_content_added', body)
end
# Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
#
# Example:
# wiki_content_updated(wiki_content) => tmail object
# Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
def wiki_content_updated(wiki_content)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :page => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title),
:wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :id => wiki_content.project, :page => wiki_content.page.title, :version => wiki_content.version)
render_multipart('wiki_content_updated', body)
end
# Builds a tmail object used to email the specified user their account information.
#
# Example:
# account_information(user, password) => tmail object
# Mailer.deliver_account_information(user, password) => sends account information to the user
def account_information(user, password) def account_information(user, password)
set_language_if_valid user.language set_language_if_valid user.language
recipients user.mail recipients user.mail
@@ -212,35 +123,14 @@ class Mailer < ActionMailer::Base
body :user => user, body :user => user,
:password => password, :password => password,
:login_url => url_for(:controller => 'account', :action => 'login') :login_url => url_for(:controller => 'account', :action => 'login')
render_multipart('account_information', body)
end end
# Builds a tmail object used to email all active administrators of an account activation request.
#
# Example:
# account_activation_request(user) => tmail object
# Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
def account_activation_request(user) def account_activation_request(user)
# Send the email to all active administrators # Send the email to all active administrators
recipients User.active.find(: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) subject l(:mail_subject_account_activation_request, Setting.app_title)
body :user => user, body :user => user,
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
render_multipart('account_activation_request', body)
end
# Builds a tmail object used to email the specified user that their account was activated by an administrator.
#
# Example:
# account_activated(user) => tmail object
# Mailer.deliver_account_activated(user) => sends an email to the registered user
def account_activated(user)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_register, Setting.app_title)
body :user => user,
:login_url => url_for(:controller => 'account', :action => 'login')
render_multipart('account_activated', body)
end end
def lost_password(token) def lost_password(token)
@@ -249,7 +139,6 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_lost_password, Setting.app_title) subject l(:mail_subject_lost_password, Setting.app_title)
body :token => token, body :token => token,
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value) :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
render_multipart('lost_password', body)
end end
def register(token) def register(token)
@@ -258,7 +147,6 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_register, Setting.app_title) subject l(:mail_subject_register, Setting.app_title)
body :token => token, body :token => token,
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value) :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
render_multipart('register', body)
end end
def test(user) def test(user)
@@ -266,13 +154,11 @@ class Mailer < ActionMailer::Base
recipients user.mail recipients user.mail
subject 'Redmine test' subject 'Redmine test'
body :url => url_for(:controller => 'welcome') body :url => url_for(:controller => 'welcome')
render_multipart('test', body)
end end
# Overrides default deliver! method to prevent from sending an email # Overrides default deliver! method to prevent from sending an email
# with no recipient, cc or bcc # with no recipient, cc or bcc
def deliver!(mail = @mail) def deliver!(mail = @mail)
set_language_if_valid @initial_language
return false if (recipients.nil? || recipients.empty?) && return false if (recipients.nil? || recipients.empty?) &&
(cc.nil? || cc.empty?) && (cc.nil? || cc.empty?) &&
(bcc.nil? || bcc.empty?) (bcc.nil? || bcc.empty?)
@@ -311,28 +197,22 @@ class Mailer < ActionMailer::Base
end end
end end
# Activates/desactivates email deliveries during +block+
def self.with_deliveries(enabled = true, &block)
was_enabled = ActionMailer::Base.perform_deliveries
ActionMailer::Base.perform_deliveries = !!enabled
yield
ensure
ActionMailer::Base.perform_deliveries = was_enabled
end
private private
def initialize_defaults(method_name) def initialize_defaults(method_name)
super super
@initial_language = current_language
set_language_if_valid Setting.default_language set_language_if_valid Setting.default_language
from Setting.mail_from from Setting.mail_from
# URL options
h = Setting.host_name
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
default_url_options[:host] = h
default_url_options[:protocol] = Setting.protocol
# Common headers # Common headers
headers 'X-Mailer' => 'Redmine', headers 'X-Mailer' => 'Redmine',
'X-Redmine-Host' => Setting.host_name, 'X-Redmine-Host' => Setting.host_name,
'X-Redmine-Site' => Setting.app_title, 'X-Redmine-Site' => Setting.app_title
'Precedence' => 'bulk',
'Auto-Submitted' => 'auto-generated'
end end
# Appends a Redmine header field (name is prepended with 'X-Redmine-') # Appends a Redmine header field (name is prepended with 'X-Redmine-')
@@ -358,22 +238,26 @@ class Mailer < ActionMailer::Base
super super
end end
# Rails 2.3 has problems rendering implicit multipart messages with # Renders a message with the corresponding layout
# layouts so this method will wrap an multipart messages with def render_message(method_name, body)
# explicit parts. 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)
# https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
# https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts end
def render_multipart(method_name, body) # for the case of plain text only
def body(*params)
value = super(*params)
if Setting.plain_text_mail? if Setting.plain_text_mail?
content_type "text/plain" templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
body render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb') unless String === @body or templates.empty?
else template = File.basename(templates.first)
content_type "multipart/alternative" @body[:content_for_layout] = render(:file => template, :body => @body)
part :content_type => "text/plain", :body => render(:file => "#{method_name}.text.plain.rhtml", :body => body, :layout => 'mailer.text.plain.erb') @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
part :content_type => "text/html", :body => render_message("#{method_name}.text.html.rhtml", body) return @body
end
end end
return value
end end
# Makes partial rendering work with Rails 1.2 (retro-compatibility) # Makes partial rendering work with Rails 1.2 (retro-compatibility)
@@ -385,8 +269,7 @@ class Mailer < ActionMailer::Base
def self.message_id_for(object) def self.message_id_for(object)
# id + timestamp should reduce the odds of a collision # id + timestamp should reduce the odds of a collision
# as far as we don't send multiple emails for the same object # as far as we don't send multiple emails for the same object
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on) hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '') host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
host = "#{::Socket.gethostname}.redmine" if host.empty? host = "#{::Socket.gethostname}.redmine" if host.empty?
"<#{hash}@#{host}>" "<#{hash}@#{host}>"
+10 -57
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,73 +17,26 @@
class Member < ActiveRecord::Base class Member < ActiveRecord::Base
belongs_to :user belongs_to :user
belongs_to :principal, :foreign_key => 'user_id' belongs_to :role
has_many :member_roles, :dependent => :destroy
has_many :roles, :through => :member_roles
belongs_to :project belongs_to :project
validates_presence_of :principal, :project validates_presence_of :role, :user, :project
validates_uniqueness_of :user_id, :scope => :project_id validates_uniqueness_of :user_id, :scope => :project_id
after_destroy :unwatch_from_permission_change def validate
errors.add :role_id, :activerecord_error_invalid if role && !role.member?
end
def name def name
self.user.name self.user.name
end end
alias :base_role_ids= :role_ids=
def role_ids=(arg)
ids = (arg || []).collect(&:to_i) - [0]
# Keep inherited roles
ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
new_role_ids = ids - role_ids
# Add new roles
new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
# Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
if member_roles_to_destroy.any?
member_roles_to_destroy.each(&:destroy)
unwatch_from_permission_change
end
end
def <=>(member) def <=>(member)
a, b = roles.sort.first, member.roles.sort.first role == member.role ? (user <=> member.user) : (role <=> member.role)
a == b ? (principal <=> member.principal) : (a <=> b)
end
def deletable?
member_roles.detect {|mr| mr.inherited_from}.nil?
end
def include?(user)
if principal.is_a?(Group)
!user.nil? && user.groups.include?(principal)
else
self.user == user
end
end end
def before_destroy def before_destroy
if user # remove category based auto assignments for this member
# remove category based auto assignments for this member IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
end
end
protected
def validate
errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
end
private
# Unwatch things that the user is no longer allowed to view inside project
def unwatch_from_permission_change
if user
Watcher.prune(:user => user, :project => project)
end
end end
end end
-63
View File
@@ -1,63 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MemberRole < ActiveRecord::Base
belongs_to :member
belongs_to :role
after_destroy :remove_member_if_empty
after_create :add_role_to_group_users
after_destroy :remove_role_from_group_users
validates_presence_of :role
def validate
errors.add :role_id, :invalid if role && !role.member?
end
def inherited?
!inherited_from.nil?
end
private
def remove_member_if_empty
if member.roles.empty?
member.destroy
end
end
def add_role_to_group_users
if member.principal.is_a?(Group)
member.principal.users.each do |user|
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
user_member.save!
end
end
end
def remove_role_from_group_users
MemberRole.find(:all, :conditions => { :inherited_from => id }).group_by(&:member).each do |member, member_roles|
member_roles.each(&:destroy)
if member && member.user
Watcher.prune(:user => member.user, :project => member.project)
end
end
end
end
+10 -26
View File
@@ -37,41 +37,32 @@ class Message < ActiveRecord::Base
acts_as_watchable acts_as_watchable
attr_protected :locked, :sticky attr_protected :locked, :sticky
validates_presence_of :board, :subject, :content validates_presence_of :subject, :content
validates_length_of :subject, :maximum => 255 validates_length_of :subject, :maximum => 255
after_create :add_author_as_watcher after_create :add_author_as_watcher
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_messages, project)
end
def validate_on_create def validate_on_create
# Can not reply to a locked topic # Can not reply to a locked topic
errors.add_to_base 'Topic is locked' if root.locked? && self != root errors.add_to_base 'Topic is locked' if root.locked? && self != root
end end
def after_create def after_create
board.update_attribute(:last_message_id, self.id)
board.increment! :messages_count
if parent if parent
parent.reload.update_attribute(:last_reply_id, self.id) parent.reload.update_attribute(:last_reply_id, self.id)
end else
board.reset_counters! board.increment! :topics_count
end
def after_update
if board_id_changed?
Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
Board.reset_counters!(board_id_was)
Board.reset_counters!(board_id)
end end
end end
def after_destroy def after_destroy
board.reset_counters! # The following line is required so that the previous counter
end # updates (due to children removal) are not overwritten
board.reload
def sticky=(arg) board.decrement! :messages_count
write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0) board.decrement! :topics_count unless parent
end end
def sticky? def sticky?
@@ -90,13 +81,6 @@ class Message < ActiveRecord::Base
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project))) usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
end end
# Returns the mail adresses of users that should be notified
def recipients
notified = project.notified_users
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end
private private
def add_author_as_watcher def add_author_as_watcher
+9 -1
View File
@@ -17,6 +17,14 @@
class MessageObserver < ActiveRecord::Observer class MessageObserver < ActiveRecord::Observer
def after_create(message) def after_create(message)
Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted') recipients = []
# send notification to the topic watchers
recipients += message.root.watcher_recipients
# send notification to the board watchers
recipients += message.board.watcher_recipients
# send notification to project members who want to be notified
recipients += message.board.project.recipients
recipients = recipients.compact.uniq
Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
end end
end end
+1 -12
View File
@@ -24,22 +24,11 @@ class News < ActiveRecord::Base
validates_length_of :title, :maximum => 60 validates_length_of :title, :maximum => 60
validates_length_of :summary, :maximum => 255 validates_length_of :summary, :maximum => 255
acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project 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_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 :author_key => :author_id
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_news, project)
end
# Returns the mail adresses of users that should be notified
def recipients
notified = project.notified_users
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end
# returns latest news for projects visible by user # returns latest news for projects visible by user
def self.latest(user = User.current, count = 5) 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") find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
-22
View File
@@ -1,22 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class NewsObserver < ActiveRecord::Observer
def after_create(news)
Mailer.deliver_news_added(news) if Setting.notified_events.include?('news_added')
end
end
-57
View File
@@ -1,57 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Principal < ActiveRecord::Base
set_table_name "#{table_name_prefix}users#{table_name_suffix}"
has_many :members, :foreign_key => 'user_id', :dependent => :destroy
has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
has_many :projects, :through => :memberships
# Groups and active users
named_scope :active, :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status = 1)"
named_scope :like, lambda {|q|
s = "%#{q.to_s.strip.downcase}%"
{:conditions => ["LOWER(login) LIKE :s OR LOWER(firstname) LIKE :s OR LOWER(lastname) LIKE :s OR LOWER(mail) LIKE :s", {:s => s}],
:order => 'type, login, lastname, firstname, mail'
}
}
before_create :set_default_empty_values
def <=>(principal)
if self.class.name == principal.class.name
self.to_s.downcase <=> principal.to_s.downcase
else
# groups after users
principal.class.name <=> self.class.name
end
end
protected
# Make sure we don't try to insert NULL values (see #4632)
def set_default_empty_values
self.login ||= ''
self.hashed_password ||= ''
self.firstname ||= ''
self.lastname ||= ''
self.mail ||= ''
true
end
end
+45 -422
View File
@@ -20,16 +20,8 @@ class Project < ActiveRecord::Base
STATUS_ACTIVE = 1 STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9 STATUS_ARCHIVED = 9
# Specific overidden Activities has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :time_entry_activities
has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :memberships, :class_name => 'Member'
has_many :member_principals, :class_name => 'Member',
:include => :principal,
:conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
has_many :users, :through => :members has_many :users, :through => :members
has_many :principals, :through => :member_principals, :source => :principal
has_many :enabled_modules, :dependent => :delete_all has_many :enabled_modules, :dependent => :delete_all
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
@@ -51,12 +43,12 @@ class Project < ActiveRecord::Base
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
:association_foreign_key => 'custom_field_id' :association_foreign_key => 'custom_field_id'
acts_as_nested_set :order => 'name' acts_as_nested_set :order => 'name', :dependent => :destroy
acts_as_attachable :view_permission => :view_files, acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files :delete_permission => :manage_files
acts_as_customizable acts_as_customizable
acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}, :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
:author => nil :author => nil
@@ -68,17 +60,14 @@ class Project < ActiveRecord::Base
validates_associated :repository, :wiki validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 255 validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 1..20 validates_length_of :identifier, :in => 2..20
# donwcase letters, digits, dashes but not digits only validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
# reserved words
validates_exclusion_of :identifier, :in => %w( new )
before_destroy :delete_all_members, :destroy_children 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 :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 :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
named_scope :all_public, { :conditions => { :is_public => true } } named_scope :public, { :conditions => { :is_public => true } }
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } } named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
def identifier=(identifier) def identifier=(identifier)
@@ -89,17 +78,27 @@ class Project < ActiveRecord::Base
errors[:identifier].nil? && !(new_record? || identifier.blank?) errors[:identifier].nil? && !(new_record? || identifier.blank?)
end end
def issues_with_subprojects(include_subprojects=false)
conditions = nil
if include_subprojects
ids = [id] + descendants.collect(&:id)
conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
end
conditions ||= ["#{Project.table_name}.id = ?", id]
# Quick and dirty fix for Rails 2 compatibility
Issue.send(:with_scope, :find => { :conditions => conditions }) do
Version.send(:with_scope, :find => { :conditions => conditions }) do
yield
end
end
end
# returns latest created projects # returns latest created projects
# non public projects will be returned only if user is a member of those # non public projects will be returned only if user is a member of those
def self.latest(user=nil, count=5) def self.latest(user=nil, count=5)
find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
end end
# Returns a SQL :conditions string used to find all active projects for the specified user.
#
# Examples:
# Projects.visible_by(admin) => "projects.status = 1"
# Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
def self.visible_by(user=nil) def self.visible_by(user=nil)
user ||= User.current user ||= User.current
if user && user.admin? if user && user.admin?
@@ -117,7 +116,7 @@ class Project < ActiveRecord::Base
if perm = Redmine::AccessControl.permission(permission) if perm = Redmine::AccessControl.permission(permission)
unless perm.project_module.nil? unless perm.project_module.nil?
# If the permission belongs to a project module, make sure the module is enabled # If the permission belongs to a project module, make sure the module is enabled
base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" 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
end end
if options[:project] if options[:project]
@@ -130,71 +129,19 @@ class Project < ActiveRecord::Base
else else
statements << "1=0" statements << "1=0"
if user.logged? if user.logged?
if Role.non_member.allowed_to?(permission) && !options[:member] statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
end
allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
elsif Role.anonymous.allowed_to?(permission)
# anonymous user allowed on public project
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
else else
if Role.anonymous.allowed_to?(permission) && !options[:member] # anonymous user is not authorized
# anonymous user allowed on public project
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
end
end end
end end
statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
end end
# Returns the Systemwide and project specific activities
def activities(include_inactive=false)
if include_inactive
return all_activities
else
return active_activities
end
end
# Will create a new Project specific Activity or update an existing one
#
# This will raise a ActiveRecord::Rollback if the TimeEntryActivity
# does not successfully save.
def update_or_create_time_entry_activity(id, activity_hash)
if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
self.create_time_entry_activity_if_needed(activity_hash)
else
activity = project.time_entry_activities.find_by_id(id.to_i)
activity.update_attributes(activity_hash) if activity
end
end
# Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
#
# This will raise a ActiveRecord::Rollback if the TimeEntryActivity
# does not successfully save.
def create_time_entry_activity_if_needed(activity)
if activity['parent_id']
parent_activity = TimeEntryActivity.find(activity['parent_id'])
activity['name'] = parent_activity.name
activity['position'] = parent_activity.position
if Enumeration.overridding_change?(activity, parent_activity)
project_activity = self.time_entry_activities.create(activity)
if project_activity.new_record?
raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
else
self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
end
end
end
end
# Returns a :conditions SQL string that can be used to find the issues associated with this project.
#
# Examples:
# project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
# project.project_condition(false) => "projects.id = 1"
def project_condition(with_subprojects) def project_condition(with_subprojects)
cond = "#{Project.table_name}.id = #{id}" cond = "#{Project.table_name}.id = #{id}"
cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
@@ -220,20 +167,13 @@ class Project < ActiveRecord::Base
self.status == STATUS_ACTIVE self.status == STATUS_ACTIVE
end end
# Archives the project and its descendants # Archives the project and its descendants recursively
def archive def archive
# Check that there is no issue of a non descendant project that is assigned # Archive subprojects if any
# to one of the project or descendant versions children.each do |subproject|
v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten subproject.archive
if v_ids.any? && Issue.find(:first, :include => :project,
:conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
" AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
return false
end end
Project.transaction do update_attribute :status, STATUS_ARCHIVED
archive!
end
true
end end
# Unarchives the project # Unarchives the project
@@ -244,38 +184,8 @@ class Project < ActiveRecord::Base
end end
# Returns an array of projects the project can be moved to # Returns an array of projects the project can be moved to
# by the current user def possible_parents
def allowed_parents @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
return @allowed_parents if @allowed_parents
@allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
@allowed_parents = @allowed_parents - self_and_descendants
if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
@allowed_parents << nil
end
unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
@allowed_parents << parent
end
@allowed_parents
end
# Sets the parent of the project with authorization check
def set_allowed_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.nil?
if !new_record? && allowed_parents.empty?
return false
end
elsif !allowed_parents.include?(p)
return false
end
set_parent!(p)
end end
# Sets the parent of the project # Sets the parent of the project
@@ -309,7 +219,6 @@ class Project < ActiveRecord::Base
# move_to_child_of adds the project in last (ie.right) position # move_to_child_of adds the project in last (ie.right) position
move_to_child_of(p) move_to_child_of(p)
end end
Issue.update_versions_from_hierarchy_change(self)
true true
else else
# Can not move to the given target # Can not move to the given target
@@ -326,51 +235,14 @@ class Project < ActiveRecord::Base
:order => "#{Tracker.table_name}.position") :order => "#{Tracker.table_name}.position")
end end
# Closes open and locked project versions that are completed
def close_completed_versions
Version.transaction do
versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
if version.completed?
version.update_attribute(:status, 'closed')
end
end
end
end
# Returns a scope of the Versions used by the project
def shared_versions
@shared_versions ||=
Version.scoped(:include => :project,
:conditions => "#{Project.table_name}.id = #{id}" +
" OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
" #{Version.table_name}.sharing = 'system'" +
" OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
" OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
" OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
"))")
end
# Returns a hash of project users grouped by role
def users_by_role
members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
m.roles.each do |r|
h[r] ||= []
h[r] << m.user
end
h
end
end
# Deletes all project's members # Deletes all project's members
def delete_all_members def delete_all_members
me, mr = Member.table_name, MemberRole.table_name
connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
Member.delete_all(['project_id = ?', id]) Member.delete_all(['project_id = ?', id])
end end
# Users issues can be assigned to # Users issues can be assigned to
def assignable_users def assignable_users
members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
end end
# Returns the mail adresses of users that should be always notified on project events # Returns the mail adresses of users that should be always notified on project events
@@ -378,11 +250,6 @@ class Project < ActiveRecord::Base
members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail} members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
end end
# Returns the users that should be notified on project events
def notified_users
members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
end
# Returns an array of all custom fields enabled for project issues # Returns an array of all custom fields enabled for project issues
# (explictly associated custom fields and custom fields enabled for all projects) # (explictly associated custom fields and custom fields enabled for all projects)
def all_issue_custom_fields def all_issue_custom_fields
@@ -406,10 +273,6 @@ class Project < ActiveRecord::Base
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
end end
# Return true if this project is allowed to do the specified action.
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
def allows_to?(action) def allows_to?(action)
if action.is_a? Hash if action.is_a? Hash
allowed_actions.include? "#{action[:controller]}/#{action[:action]}" allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
@@ -424,14 +287,10 @@ class Project < ActiveRecord::Base
end end
def enabled_module_names=(module_names) def enabled_module_names=(module_names)
if module_names && module_names.is_a?(Array) enabled_modules.clear
module_names = module_names.collect(&:to_s) module_names = [] unless module_names && module_names.is_a?(Array)
# remove disabled modules module_names.each do |name|
enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)} enabled_modules << EnabledModule.new(:name => name.to_s)
# add new modules
module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
else
enabled_modules.clear
end end
end end
@@ -441,202 +300,12 @@ class Project < ActiveRecord::Base
p.nil? ? nil : p.identifier.to_s.succ p.nil? ? nil : p.identifier.to_s.succ
end end
# Copies and saves the Project instance based on the +project+. protected
# Duplicates the source project's: def validate
# * Wiki errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
# * Versions
# * Categories
# * Issues
# * Members
# * Queries
#
# Accepts an +options+ argument to specify what to copy
#
# Examples:
# project.copy(1) # => copies everything
# project.copy(1, :only => 'members') # => copies members only
# project.copy(1, :only => ['members', 'versions']) # => copies members and versions
def copy(project, options={})
project = project.is_a?(Project) ? project : Project.find(project)
to_be_copied = %w(wiki versions issue_categories issues members queries boards)
to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
Project.transaction do
if save
reload
to_be_copied.each do |name|
send "copy_#{name}", project
end
Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
save
end
end
end
# Copies +project+ and returns the new instance. This will not save
# the copy
def self.copy_from(project)
begin
project = project.is_a?(Project) ? project : Project.find(project)
if project
# clear unique attributes
attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
copy = Project.new(attributes)
copy.enabled_modules = project.enabled_modules
copy.trackers = project.trackers
copy.custom_values = project.custom_values.collect {|v| v.clone}
copy.issue_custom_fields = project.issue_custom_fields
return copy
else
return nil
end
rescue ActiveRecord::RecordNotFound
return nil
end
end
private
# Destroys children before destroying self
def destroy_children
children.each do |child|
child.destroy
end
end
# Copies wiki from +project+
def copy_wiki(project)
# Check that the source project has a wiki first
unless project.wiki.nil?
self.wiki ||= Wiki.new
wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
wiki_pages_map = {}
project.wiki.pages.each do |page|
# Skip pages without content
next if page.content.nil?
new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
new_wiki_page.content = new_wiki_content
wiki.pages << new_wiki_page
wiki_pages_map[page.id] = new_wiki_page
end
wiki.save
# Reproduce page hierarchy
project.wiki.pages.each do |page|
if page.parent_id && wiki_pages_map[page.id]
wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
wiki_pages_map[page.id].save
end
end
end
end
# Copies versions from +project+
def copy_versions(project)
project.versions.each do |version|
new_version = Version.new
new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
self.versions << new_version
end
end
# Copies issue categories from +project+
def copy_issue_categories(project)
project.issue_categories.each do |issue_category|
new_issue_category = IssueCategory.new
new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
self.issue_categories << new_issue_category
end
end
# Copies issues from +project+
def copy_issues(project)
# Stores the source issue id as a key and the copied issues as the
# value. Used to map the two togeather for issue relations.
issues_map = {}
project.issues.each do |issue|
new_issue = Issue.new
new_issue.copy_from(issue)
# Reassign fixed_versions by name, since names are unique per
# project and the versions for self are not yet saved
if issue.fixed_version
new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
end
# Reassign the category by name, since names are unique per
# project and the categories for self are not yet saved
if issue.category
new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
end
self.issues << new_issue
issues_map[issue.id] = new_issue
end
# Relations after in case issues related each other
project.issues.each do |issue|
new_issue = issues_map[issue.id]
# Relations
issue.relations_from.each do |source_relation|
new_issue_relation = IssueRelation.new
new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
new_issue_relation.issue_to = source_relation.issue_to
end
new_issue.relations_from << new_issue_relation
end
issue.relations_to.each do |source_relation|
new_issue_relation = IssueRelation.new
new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
new_issue_relation.issue_from = source_relation.issue_from
end
new_issue.relations_to << new_issue_relation
end
end
end
# Copies members from +project+
def copy_members(project)
project.memberships.each do |member|
new_member = Member.new
new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
# only copy non inherited roles
# inherited roles will be added when copying the group membership
role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
next if role_ids.empty?
new_member.role_ids = role_ids
new_member.project = self
self.members << new_member
end
end
# Copies queries from +project+
def copy_queries(project)
project.queries.each do |query|
new_query = Query.new
new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
new_query.sort_criteria = query.sort_criteria if query.sort_criteria
new_query.project = self
self.queries << new_query
end
end
# Copies boards from +project+
def copy_boards(project)
project.boards.each do |board|
new_board = Board.new
new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
new_board.project = self
self.boards << new_board
end
end end
private
def allowed_permissions def allowed_permissions
@allowed_permissions ||= begin @allowed_permissions ||= begin
module_names = enabled_modules.collect {|m| m.name} module_names = enabled_modules.collect {|m| m.name}
@@ -647,50 +316,4 @@ class Project < ActiveRecord::Base
def allowed_actions def allowed_actions
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
end end
# Returns all the active Systemwide and project specific activities
def active_activities
overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
if overridden_activity_ids.empty?
return TimeEntryActivity.shared.active
else
return system_activities_and_project_overrides
end
end
# Returns all the Systemwide and project specific activities
# (inactive and active)
def all_activities
overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
if overridden_activity_ids.empty?
return TimeEntryActivity.shared
else
return system_activities_and_project_overrides(true)
end
end
# Returns the systemwide active activities merged with the project specific overrides
def system_activities_and_project_overrides(include_inactive=false)
if include_inactive
return TimeEntryActivity.shared.
find(:all,
:conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
self.time_entry_activities
else
return TimeEntryActivity.shared.active.
find(:all,
:conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
self.time_entry_activities.active
end
end
# Archives subprojects recursively
def archive!
children.each do |subproject|
subproject.send :archive!
end
update_attribute :status, STATUS_ARCHIVED
end
end end
+24 -174
View File
@@ -16,31 +16,19 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueryColumn class QueryColumn
attr_accessor :name, :sortable, :groupable, :default_order attr_accessor :name, :sortable, :default_order
include Redmine::I18n include GLoc
def initialize(name, options={}) def initialize(name, options={})
self.name = name self.name = name
self.sortable = options[:sortable] self.sortable = options[:sortable]
self.groupable = options[:groupable] || false
if groupable == true
self.groupable = name.to_s
end
self.default_order = options[:default_order] self.default_order = options[:default_order]
end end
def caption def caption
set_language_if_valid(User.current.language)
l("field_#{name}") l("field_#{name}")
end end
# Returns true if the column is sortable, otherwise false
def sortable?
!sortable.nil?
end
def value(issue)
issue.send name
end
end end
class QueryCustomFieldColumn < QueryColumn class QueryCustomFieldColumn < QueryColumn
@@ -48,10 +36,6 @@ class QueryCustomFieldColumn < QueryColumn
def initialize(custom_field) def initialize(custom_field)
self.name = "cf_#{custom_field.id}".to_sym self.name = "cf_#{custom_field.id}".to_sym
self.sortable = custom_field.order_statement || false self.sortable = custom_field.order_statement || false
if %w(list date bool int).include?(custom_field.field_format)
self.groupable = custom_field.order_statement
end
self.groupable ||= false
@cf = custom_field @cf = custom_field
end end
@@ -62,22 +46,13 @@ class QueryCustomFieldColumn < QueryColumn
def custom_field def custom_field
@cf @cf
end end
def value(issue)
cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
cv && @cf.cast_value(cv.value)
end
end end
class Query < ActiveRecord::Base class Query < ActiveRecord::Base
class StatementInvalid < ::ActiveRecord::StatementInvalid
end
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user
serialize :filters serialize :filters
serialize :column_names serialize :column_names
serialize :sort_criteria, Array
attr_protected :project_id, :user_id attr_protected :project_id, :user_id
@@ -90,8 +65,8 @@ class Query < ActiveRecord::Base
"c" => :label_closed_issues, "c" => :label_closed_issues,
"!*" => :label_none, "!*" => :label_none,
"*" => :label_all, "*" => :label_all,
">=" => :label_greater_or_equal, ">=" => '>=',
"<=" => :label_less_or_equal, "<=" => '<=',
"<t+" => :label_in_less_than, "<t+" => :label_in_less_than,
">t+" => :label_in_more_than, ">t+" => :label_in_more_than,
"t+" => :label_in, "t+" => :label_in,
@@ -118,20 +93,19 @@ class Query < ActiveRecord::Base
cattr_reader :operators_by_filter_type cattr_reader :operators_by_filter_type
@@available_columns = [ @@available_columns = [
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true), QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
QueryColumn.new(:author), QueryColumn.new(:author),
QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true), 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(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), 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(:start_date, :sortable => "#{Issue.table_name}.start_date"),
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
] ]
cattr_reader :available_columns cattr_reader :available_columns
@@ -139,6 +113,7 @@ class Query < ActiveRecord::Base
def initialize(attributes = nil) def initialize(attributes = nil)
super attributes super attributes
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
set_language_if_valid(User.current.language)
end end
def after_initialize def after_initialize
@@ -148,7 +123,7 @@ class Query < ActiveRecord::Base
def validate def validate
filters.each_key do |field| filters.each_key do |field|
errors.add label_for(field), :blank unless errors.add label_for(field), :activerecord_error_blank unless
# filter requires one or more values # filter requires one or more values
(values_for(field) and !values_for(field).first.blank?) or (values_for(field) and !values_for(field).first.blank?) or
# filter doesn't require any value # filter doesn't require any value
@@ -171,7 +146,7 @@ class Query < ActiveRecord::Base
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } }, "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
"priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } }, "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"subject" => { :type => :text, :order => 8 }, "subject" => { :type => :text, :order => 8 },
"created_on" => { :type => :date_past, :order => 9 }, "created_on" => { :type => :date_past, :order => 9 },
"updated_on" => { :type => :date_past, :order => 10 }, "updated_on" => { :type => :date_past, :order => 10 },
@@ -186,7 +161,6 @@ class Query < ActiveRecord::Base
user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
else else
# members of the user's projects # members of the user's projects
# OPTIMIZE: Is selecting from users per project (N+1)
user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
end end
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
@@ -201,8 +175,8 @@ class Query < ActiveRecord::Base
unless @project.issue_categories.empty? unless @project.issue_categories.empty?
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
end end
unless @project.shared_versions.empty? unless @project.versions.empty?
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
end end
unless @project.descendants.active.empty? 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] } } @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
@@ -210,10 +184,6 @@ class Query < ActiveRecord::Base
add_custom_fields_filters(@project.all_issue_custom_fields) add_custom_fields_filters(@project.all_issue_custom_fields)
else else
# global filters for cross project issue list # global filters for cross project issue list
system_shared_versions = Version.visible.find_all_by_sharing('system')
unless system_shared_versions.empty?
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
end
add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
end end
@available_filters @available_filters
@@ -236,7 +206,7 @@ class Query < ActiveRecord::Base
def add_short_filter(field, expression) def add_short_filter(field, expression)
return unless expression return unless expression
parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
add_filter field, (parms[0] || "="), [parms[1] || ""] add_filter field, (parms[0] || "="), [parms[1] || ""]
end end
@@ -262,21 +232,13 @@ class Query < ActiveRecord::Base
@available_columns = Query.available_columns @available_columns = Query.available_columns
@available_columns += (project ? @available_columns += (project ?
project.all_issue_custom_fields : project.all_issue_custom_fields :
IssueCustomField.find(:all) IssueCustomField.find(:all, :conditions => {:is_for_all => true})
).collect {|cf| QueryCustomFieldColumn.new(cf) } ).collect {|cf| QueryCustomFieldColumn.new(cf) }
end end
# Returns an array of columns that can be used to group the results
def groupable_columns
available_columns.select {|c| c.groupable}
end
def columns def columns
if has_default_columns? if has_default_columns?
available_columns.select do |c| available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
# Adds the project column by default for cross-project lists
Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
end
else else
# preserve the column_names order # preserve the column_names order
column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
@@ -284,14 +246,8 @@ class Query < ActiveRecord::Base
end end
def column_names=(names) def column_names=(names)
if names names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
names = names.select {|n| n.is_a?(Symbol) || !n.blank? } names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
# Set column_names to nil if default columns
if names.map(&:to_s) == Setting.issue_list_default_columns
names = nil
end
end
write_attribute(:column_names, names) write_attribute(:column_names, names)
end end
@@ -303,49 +259,6 @@ class Query < ActiveRecord::Base
column_names.nil? || column_names.empty? column_names.nil? || column_names.empty?
end end
def sort_criteria=(arg)
c = []
if arg.is_a?(Hash)
arg = arg.keys.sort.collect {|k| arg[k]}
end
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
write_attribute(:sort_criteria, c)
end
def sort_criteria
read_attribute(:sort_criteria) || []
end
def sort_criteria_key(arg)
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
end
def sort_criteria_order(arg)
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
end
# Returns the SQL sort order that should be prepended for grouping
def group_by_sort_order
if grouped? && (column = group_by_column)
column.sortable.is_a?(Array) ?
column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
"#{column.sortable} #{column.default_order}"
end
end
# Returns true if the query is a grouped query
def grouped?
!group_by.blank?
end
def group_by_column
groupable_columns.detect {|c| c.name.to_s == group_by}
end
def group_by_statement
group_by_column.groupable
end
def project_statement def project_statement
project_clauses = [] project_clauses = []
if project && !@project.descendants.active.empty? if project && !@project.descendants.active.empty?
@@ -412,69 +325,6 @@ class Query < ActiveRecord::Base
(filters_clauses << project_statement).join(' AND ') (filters_clauses << project_statement).join(' AND ')
end end
# Returns the issue count
def issue_count
Issue.count(:include => [:status, :project], :conditions => statement)
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
end
# Returns the issue count by group or nil if query is not grouped
def issue_count_by_group
r = nil
if grouped?
begin
# Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
rescue ActiveRecord::RecordNotFound
r = {nil => issue_count}
end
c = group_by_column
if c.is_a?(QueryCustomFieldColumn)
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
end
end
r
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
end
# Returns the issues
# Valid options are :order, :offset, :limit, :include, :conditions
def issues(options={})
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
order_option = nil if order_option.blank?
Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
:conditions => Query.merge_conditions(statement, options[:conditions]),
:order => order_option,
:limit => options[:limit],
:offset => options[:offset]
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
end
# Returns the journals
# Valid options are :order, :offset, :limit
def journals(options={})
Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
:conditions => statement,
:order => options[:order],
:limit => options[:limit],
:offset => options[:offset]
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
end
# Returns the versions
# Valid options are :conditions
def versions(options={})
Version.find :all, :include => :project,
:conditions => Query.merge_conditions(project_statement, options[:conditions])
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
end
private private
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
@@ -521,9 +371,9 @@ class Query < ActiveRecord::Base
Time.now.at_beginning_of_week 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)] sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~" when "~"
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
when "!~" when "!~"
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
end end
return sql return sql
+44 -55
View File
@@ -1,3 +1,4 @@
# redMine - project management software # redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
@@ -25,7 +26,7 @@ class Repository < ActiveRecord::Base
before_destroy :clear_changesets before_destroy :clear_changesets
# Checks if the SCM is enabled when creating a repository # Checks if the SCM is enabled when creating a repository
validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) } validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
# Removes leading and trailing whitespace # Removes leading and trailing whitespace
def url=(arg) def url=(arg)
@@ -38,7 +39,8 @@ class Repository < ActiveRecord::Base
end end
def scm def scm
@scm ||= self.scm_adapter.new url, root_url, login, password init_cache if cache_path.blank? and respond_to?(:init_cache)
@scm ||= self.scm_adapter.new(url, root_url, login, password, cache_path)
update_attribute(:root_url, @scm.root_url) if root_url.blank? update_attribute(:root_url, @scm.root_url) if root_url.blank?
@scm @scm
end end
@@ -63,18 +65,6 @@ class Repository < ActiveRecord::Base
scm.entries(path, identifier) scm.entries(path, identifier)
end end
def branches
scm.branches
end
def tags
scm.tags
end
def default_branch
scm.default_branch
end
def properties(path, identifier=nil) def properties(path, identifier=nil)
scm.properties(path, identifier) scm.properties(path, identifier)
end end
@@ -87,35 +77,24 @@ class Repository < ActiveRecord::Base
scm.diff(path, rev, rev_to) scm.diff(path, rev, rev_to)
end end
# Default behaviour: we search in cached changesets
def changesets_for_path(path, options={})
path = "/#{path}" unless path.starts_with?('/')
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 # Returns a path relative to the url of the repository
def relative_path(path) def relative_path(path)
path path
end end
# Finds and returns a revision with a number or the beginning of a hash
def find_changeset_by_name(name)
changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
end
def latest_changeset def latest_changeset
@latest_changeset ||= changesets.find(:first) @latest_changeset ||= changesets.find(:first)
end end
# Returns the latest changesets for +path+
# Default behaviour is to search in cached changesets
def latest_changesets(path, rev, limit=10)
if path.blank?
changesets.find(:all, :include => :user,
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
:limit => limit)
else
changes.find(:all, :include => {:changeset => :user},
:conditions => ["path = ?", path.with_leading_slash],
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
:limit => limit).collect(&:changeset)
end
end
def scan_changesets_for_issue_ids def scan_changesets_for_issue_ids
self.changesets.each(&:scan_comment_for_issue_ids) self.changesets.each(&:scan_comment_for_issue_ids)
end end
@@ -136,7 +115,6 @@ class Repository < ActiveRecord::Base
end end
end end
@committers = nil @committers = nil
@found_committer_users = nil
true true
else else
false false
@@ -147,34 +125,24 @@ class Repository < ActiveRecord::Base
# It will return nil if the committer is not yet mapped and if no User # It will return nil if the committer is not yet mapped and if no User
# with the same username or email was found # with the same username or email was found
def find_committer_user(committer) def find_committer_user(committer)
unless committer.blank? if committer
@found_committer_users ||= {}
return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
user = nil
c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user) c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
if c && c.user if c && c.user
user = c.user c.user
elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/ elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3 username, email = $1.strip, $3
u = User.find_by_login(username) u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank? u ||= User.find_by_mail(email) unless email.blank?
user = u u
end end
@found_committer_users[committer] = user
user
end end
end end
# Fetches new changesets for all repositories of active projects # fetch new changesets for all repositories
# Can be called periodically by an external script # can be called periodically by an external script
# eg. ruby script/runner "Repository.fetch_changesets" # eg. ruby script/runner "Repository.fetch_changesets"
def self.fetch_changesets def self.fetch_changesets
Project.active.has_module(:repository).find(:all, :include => :repository).each do |project| find(:all).each(&:fetch_changesets)
if project.repository
project.repository.fetch_changesets
end
end
end end
# scan changeset comments to find related and fixed issues for all repositories # scan changeset comments to find related and fixed issues for all repositories
@@ -197,8 +165,30 @@ class Repository < ActiveRecord::Base
nil nil
end end
def remove_cache
scm.remove_cache if cache
end
def create_or_sync_cache
begin
scm.create_cache
rescue => e
# clean if problem in creation
scm.remove_cache
end
scm.synchronize
end
private private
def repositories_cache_directory
unless @cache_directory
@cache_directory = Setting.repositories_cache_directory.gsub(/^([^#{File::SEPARATOR}].*)/, RAILS_ROOT + '/\1/')
Dir.mkdir(@cache_directory, File.umask(0077)) unless File.directory?(@cache_directory)
end
@cache_directory
end
def before_save def before_save
# Strips url and root_url # Strips url and root_url
url.strip! url.strip!
@@ -207,9 +197,8 @@ class Repository < ActiveRecord::Base
end end
def clear_changesets def clear_changesets
cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}" connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") connection.delete("DELETE FROM changesets_issues WHERE changesets_issues.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}")
connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
end end
end end
+45 -42
View File
@@ -21,6 +21,17 @@ class Repository::Git < Repository
attr_protected :root_url attr_protected :root_url
validates_presence_of :url validates_presence_of :url
before_destroy :remove_cache
def init_cache
return unless dir = repositories_cache_directory
# we need to use a cache only if repository isn't local and dir exists
if url[/^(rsync|https?|git|ssh):\/\//]
update_attribute(:cache_path, dir + project.identifier)
update_attribute(:cache, true)
end
end
def scm_adapter def scm_adapter
Redmine::Scm::Adapters::GitAdapter Redmine::Scm::Adapters::GitAdapter
end end
@@ -29,53 +40,45 @@ class Repository::Git < Repository
'Git' 'Git'
end end
def branches def changesets_for_path(path, options={})
scm.branches Change.find(:all, :include => {:changeset => :user},
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC",
:limit => options[:limit]).collect(&:changeset)
end end
def tags
scm.tags
end
# With SCM's that have a sequential commit numbering, redmine is able to be
# clever and only fetch changesets going forward from the most recent one
# it knows about. However, with git, you never know if people have merged
# commits into the middle of the repository history, so we should parse
# the entire log. Since it's way too slow for large repositories, we only
# parse 1 week before the last known commit.
# The repository can still be fully reloaded by calling #clear_changesets
# before fetching changesets (eg. for offline resync)
def fetch_changesets def fetch_changesets
c = changesets.find(:first, :order => 'committed_on DESC') create_or_sync_cache if cache
since = (c ? c.committed_on - 7.days : nil)
revisions = scm.revisions('', nil, nil, :all => true, :since => since) scm_info = scm.info
return if revisions.nil? || revisions.empty? if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision : nil
# latest revision in the repository
scm_revision = scm_info.lastrev.scmid
recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since]) unless changesets.find_by_scmid(scm_revision)
scm.revisions('', db_revision, nil, :reverse => true) do |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)
# Clean out revisions that are no longer in git revision.paths.each do |change|
recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }} Change.create!(:changeset => changeset,
:action => change[:action],
# Subtract revisions that redmine already knows about :path => change[:path],
recent_revisions = recent_changesets.map{|c| c.scmid} :from_path => change[:from_path],
revisions.reject!{|r| recent_revisions.include?(r.scmid)} :from_revision => change[:from_revision])
end
# Save the remaining ones to the database end
revisions.each{|r| r.save(self)} unless revisions.nil? end
end end
end
def latest_changesets(path,rev,limit=10) end
revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
return [] if revisions.nil? || revisions.empty?
changesets.find(
:all,
:conditions => [
"scmid IN (?)",
revisions.map!{|c| c.scmid}
],
:order => 'committed_on DESC'
)
end end
end end
+11
View File
@@ -21,6 +21,15 @@ class Repository::Mercurial < Repository
attr_protected :root_url attr_protected :root_url
validates_presence_of :url validates_presence_of :url
def init_cache
return unless dir = repositories_cache_directory
# we need to use a cache only if repository isn't local and dir exists
if url[/^(|https?|ssh):\/\//]
update_attribute(:cache_path, dir + project.identifier)
update_attribute(:cache, true)
end
end
def scm_adapter def scm_adapter
Redmine::Scm::Adapters::MercurialAdapter Redmine::Scm::Adapters::MercurialAdapter
end end
@@ -53,6 +62,8 @@ class Repository::Mercurial < Repository
end end
def fetch_changesets def fetch_changesets
create_or_sync_cache if cache
scm_info = scm.info scm_info = scm.info
if scm_info if scm_info
# latest revision found in database # latest revision found in database
+19 -7
View File
@@ -20,7 +20,17 @@ require 'redmine/scm/adapters/subversion_adapter'
class Repository::Subversion < Repository class Repository::Subversion < Repository
attr_protected :root_url attr_protected :root_url
validates_presence_of :url validates_presence_of :url
validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
before_destroy :remove_cache
def init_cache
return unless dir = repositories_cache_directory
# we need to use a cache only if repository isn't local and dir exists
if cache and url[/^(svn|https?|svn\+ssh):\/\//]
update_attribute(:cache_path, dir + project.identifier)
end
end
def scm_adapter def scm_adapter
Redmine::Scm::Adapters::SubversionAdapter Redmine::Scm::Adapters::SubversionAdapter
@@ -30,8 +40,8 @@ class Repository::Subversion < Repository
'Subversion' 'Subversion'
end end
def latest_changesets(path, rev, limit=10) def changesets_for_path(path, options={})
revisions = scm.revisions(path, rev, nil, :limit => limit) revisions = scm.revisions(path, nil, nil, :limit => options[:limit])
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : [] revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
end end
@@ -41,6 +51,8 @@ class Repository::Subversion < Repository
end end
def fetch_changesets def fetch_changesets
create_or_sync_cache if cache
scm_info = scm.info scm_info = scm.info
if scm_info if scm_info
# latest revision found in database # latest revision found in database
@@ -54,8 +66,8 @@ class Repository::Subversion < Repository
# loads changesets by batches of 200 # loads changesets by batches of 200
identifier_to = [identifier_from + 199, scm_revision].min identifier_to = [identifier_from + 199, scm_revision].min
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
revisions.reverse_each do |revision| transaction do
transaction do revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self, changeset = Changeset.create(:repository => self,
:revision => revision.identifier, :revision => revision.identifier,
:committer => revision.author, :committer => revision.author,
@@ -68,7 +80,7 @@ class Repository::Subversion < Repository
:path => change[:path], :path => change[:path],
:from_path => change[:from_path], :from_path => change[:from_path],
:from_revision => change[:from_revision]) :from_revision => change[:from_revision])
end unless changeset.new_record? end
end end
end unless revisions.nil? end unless revisions.nil?
identifier_from = identifier_to + 1 identifier_from = identifier_to + 1
@@ -84,6 +96,6 @@ class Repository::Subversion < Repository
# url = file:///var/svn/foo/bar # url = file:///var/svn/foo/bar
# => returns /bar # => returns /bar
def relative_url def relative_url
@relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '') @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url)}"), '')
end end
end end
+10 -10
View File
@@ -20,7 +20,6 @@ class Role < ActiveRecord::Base
BUILTIN_NON_MEMBER = 1 BUILTIN_NON_MEMBER = 1
BUILTIN_ANONYMOUS = 2 BUILTIN_ANONYMOUS = 2
named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
named_scope :builtin, lambda { |*args| named_scope :builtin, lambda { |*args|
compare = 'not' if args.first == true compare = 'not' if args.first == true
{ :conditions => "#{compare} builtin = 0" } { :conditions => "#{compare} builtin = 0" }
@@ -28,13 +27,18 @@ class Role < ActiveRecord::Base
before_destroy :check_deletable before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all do has_many :workflows, :dependent => :delete_all do
def copy(source_role) def copy(role)
Workflow.copy(nil, source_role, nil, proxy_owner) 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 #{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 #{Workflow.table_name}" +
" WHERE role_id = #{role.id}"
end end
end end
has_many :member_roles, :dependent => :destroy has_many :members
has_many :members, :through => :member_roles
acts_as_list acts_as_list
serialize :permissions, Array serialize :permissions, Array
@@ -78,11 +82,7 @@ class Role < ActiveRecord::Base
end end
def <=>(role) def <=>(role)
role ? position <=> role.position : -1 position <=> role.position
end
def to_s
name
end end
# Return true if the role is a builtin role # Return true if the role is a builtin role
+11 -1
View File
@@ -141,7 +141,7 @@ class Setting < ActiveRecord::Base
end end
def self.openid? def self.openid?
Object.const_defined?(:OpenID) && self[:openid].to_i > 0 Object.const_defined?(:OpenID) && self['openid'].to_s == '1'
end end
# Checks if settings have changed since the values were read # Checks if settings have changed since the values were read
@@ -165,4 +165,14 @@ private
setting = find_by_name(name) setting = find_by_name(name)
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
end end
protected
def validate
if self.name.to_s == "repositories_cache_directory" and not File.directory?(self.value.to_s)
logger.error("Le repertoire #{self.value.to_s} n'existe pas")
errors.add("Le repertoire #{self.value.to_s} n'existe pas")
end
end
end end
+8 -13
View File
@@ -21,30 +21,25 @@ class TimeEntry < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :issue belongs_to :issue
belongs_to :user belongs_to :user
belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id' belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
acts_as_customizable acts_as_customizable
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"}, acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"},
:url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}}, :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
:author => :user, :author => :user,
:description => :comments :description => :comments
acts_as_activity_provider :timestamp => "#{table_name}.created_on",
:author_key => :user_id,
:find_options => {:include => :project}
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true, :message => :invalid validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
validates_length_of :comments, :maximum => 255, :allow_nil => true validates_length_of :comments, :maximum => 255, :allow_nil => true
def after_initialize def after_initialize
if new_record? && self.activity.nil? if new_record? && self.activity.nil?
if default_activity = TimeEntryActivity.default if default_activity = Enumeration.default('ACTI')
self.activity_id = default_activity.id self.activity_id = default_activity.id
end end
self.hours = nil if hours == 0
end end
end end
@@ -53,9 +48,9 @@ class TimeEntry < ActiveRecord::Base
end end
def validate def validate
errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000) errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000)
errors.add :project_id, :invalid if project.nil? errors.add :project_id, :activerecord_error_invalid if project.nil?
errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project) errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
end end
def hours=(h) def hours=(h)
-34
View File
@@ -1,34 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TimeEntryActivity < Enumeration
has_many :time_entries, :foreign_key => 'activity_id'
OptionName = :enumeration_activities
def option_name
OptionName
end
def objects_count
time_entries.count
end
def transfer_relations(to)
time_entries.update_all("activity_id = #{to.id}")
end
end
@@ -1,23 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TimeEntryActivityCustomField < CustomField
def type_name
:enumeration_activities
end
end
+6 -13
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,9 +17,6 @@
class Token < ActiveRecord::Base class Token < ActiveRecord::Base
belongs_to :user belongs_to :user
validates_uniqueness_of :value
before_create :delete_previous_tokens
@@validity_time = 1.day @@validity_time = 1.day
@@ -39,13 +36,9 @@ class Token < ActiveRecord::Base
private private
def self.generate_token_value def self.generate_token_value
ActiveSupport::SecureRandom.hex(20) chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
end token_value = ''
40.times { |i| token_value << chars[rand(chars.size-1)] }
# Removes obsolete tokens (same user and action) token_value
def delete_previous_tokens
if user
Token.delete_all(['user_id = ? AND action = ?', user.id, action])
end
end end
end end
+8 -19
View File
@@ -19,8 +19,14 @@ class Tracker < ActiveRecord::Base
before_destroy :check_integrity before_destroy :check_integrity
has_many :issues has_many :issues
has_many :workflows, :dependent => :delete_all do has_many :workflows, :dependent => :delete_all do
def copy(source_tracker) def copy(tracker)
Workflow.copy(source_tracker, nil, proxy_owner, nil) 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 #{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 #{Workflow.table_name}" +
" WHERE tracker_id = #{tracker.id}"
end end
end end
@@ -43,23 +49,6 @@ class Tracker < ActiveRecord::Base
find(:all, :order => 'position') find(:all, :order => 'position')
end end
# Returns an array of IssueStatus that are used
# in the tracker's workflows
def issue_statuses
if @issue_statuses
return @issue_statuses
elsif new_record?
return []
end
ids = Workflow.
connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
flatten.
uniq
@issue_statuses = IssueStatus.find_all_by_id(ids).sort
end
private private
def check_integrity def check_integrity
raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
+31 -76
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -17,7 +17,7 @@
require "digest/sha1" require "digest/sha1"
class User < Principal class User < ActiveRecord::Base
# Account statuses # Account statuses
STATUS_ANONYMOUS = 0 STATUS_ANONYMOUS = 0
@@ -33,13 +33,13 @@ class User < Principal
:username => '#{login}' :username => '#{login}'
} }
has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
:after_remove => Proc.new {|user, group| group.user_removed(user)} has_many :members, :dependent => :delete_all
has_many :projects, :through => :memberships
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_many :changesets, :dependent => :nullify has_many :changesets, :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
belongs_to :auth_source belongs_to :auth_source
# Active non-anonymous users scope # Active non-anonymous users scope
@@ -50,7 +50,7 @@ class User < Principal
attr_accessor :password, :password_confirmation attr_accessor :password, :password_confirmation
attr_accessor :last_before_login_on attr_accessor :last_before_login_on
# Prevents unauthorized assignments # Prevents unauthorized assignments
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } 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 :login, :if => Proc.new { |user| !user.login.blank? }
@@ -62,6 +62,7 @@ class User < Principal
validates_length_of :firstname, :lastname, :maximum => 30 validates_length_of :firstname, :lastname, :maximum => 30
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
validates_length_of :mail, :maximum => 60, :allow_nil => true validates_length_of :mail, :maximum => 60, :allow_nil => true
validates_length_of :password, :minimum => 4, :allow_nil => true
validates_confirmation_of :password, :allow_nil => true validates_confirmation_of :password, :allow_nil => true
def before_create def before_create
@@ -80,14 +81,10 @@ class User < Principal
end end
def identity_url=(url) def identity_url=(url)
if url.blank? begin
write_attribute(:identity_url, '') self.write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
else rescue InvalidOpenId
begin # Invlaid url, don't save
write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
rescue OpenIdAuthentication::InvalidOpenId
# Invlaid url, don't save
end
end end
self.read_attribute(:identity_url) self.read_attribute(:identity_url)
end end
@@ -126,19 +123,6 @@ class User < Principal
raise text raise text
end end
# Returns the user who matches the given autologin +key+ or nil
def self.try_to_autologin(key)
tokens = Token.find_all_by_action_and_value('autologin', key)
# Make sure there's only 1 token that matches the key
if tokens.size == 1
token = tokens.first
if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
token.user.update_attribute(:last_login_on, Time.now)
token.user
end
end
end
# Return user's full name for display # Return user's full name for display
def name(formatter = nil) def name(formatter = nil)
if formatter if formatter
@@ -194,12 +178,6 @@ class User < Principal
token.value token.value
end end
# Return user's API key (a 40 chars long string), used to access the API
def api_key
token = self.api_token || self.create_api_token(:action => 'api')
token.value
end
# Return an array of project ids for which the user has explicitly turned mail notifications on # Return an array of project ids for which the user has explicitly turned mail notifications on
def notified_projects_ids def notified_projects_ids
@notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
@@ -217,9 +195,9 @@ class User < Principal
token && token.user.active? ? token.user : nil token && token.user.active? ? token.user : nil
end end
def self.find_by_api_key(key) def self.find_by_autologin_key(key)
token = Token.find_by_action_and_value('api', key) token = Token.find_by_action_and_value('autologin', key)
token && token.user.active? ? token.user : nil token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
end end
# Makes find_by_mail case-insensitive # Makes find_by_mail case-insensitive
@@ -227,17 +205,13 @@ class User < Principal
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase]) find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
end end
def to_s # Sort users by their display names
name def <=>(user)
self.to_s.downcase <=> user.to_s.downcase
end end
# Returns the current day according to user's time zone def to_s
def today name
if time_zone.nil?
Date.today
else
Time.now.in_time_zone(time_zone).to_date
end
end end
def logged? def logged?
@@ -248,30 +222,26 @@ class User < Principal
!logged? !logged?
end end
# Return user's roles for project # Return user's role for project
def roles_for_project(project) def role_for_project(project)
roles = []
# No role on archived projects # No role on archived projects
return roles unless project && project.active? return nil unless project && project.active?
if logged? if logged?
# Find project membership # Find project membership
membership = memberships.detect {|m| m.project_id == project.id} membership = memberships.detect {|m| m.project_id == project.id}
if membership if membership
roles = membership.roles membership.role
else else
@role_non_member ||= Role.non_member @role_non_member ||= Role.non_member
roles << @role_non_member
end end
else else
@role_anonymous ||= Role.anonymous @role_anonymous ||= Role.anonymous
roles << @role_anonymous
end end
roles
end end
# Return true if the user is a member of project # Return true if the user is a member of project
def member_of?(project) def member_of?(project)
!roles_for_project(project).detect {|role| role.member?}.nil? role_for_project(project).member?
end end
# Return true if the user is allowed to do the specified action on project # Return true if the user is allowed to do the specified action on project
@@ -287,16 +257,13 @@ class User < Principal
# Admin users are authorized for anything else # Admin users are authorized for anything else
return true if admin? return true if admin?
roles = roles_for_project(project) role = role_for_project(project)
return false unless roles return false unless role
roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)} role.allowed_to?(action) && (project.is_public? || role.member?)
elsif options[:global] elsif options[:global]
# Admin users are always authorized
return true if admin?
# authorize if user has at least one role that has this permission # authorize if user has at least one role that has this permission
roles = memberships.collect {|m| m.roles}.flatten.uniq roles = memberships.collect {|m| m.role}.uniq
roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action)) roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
else else
false false
@@ -311,8 +278,6 @@ class User < Principal
@current_user ||= User.anonymous @current_user ||= User.anonymous
end end
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
# one anonymous user per database.
def self.anonymous def self.anonymous
anonymous_user = AnonymousUser.find(:first) anonymous_user = AnonymousUser.find(:first)
if anonymous_user.nil? if anonymous_user.nil?
@@ -322,17 +287,7 @@ class User < Principal
anonymous_user anonymous_user
end end
protected private
def validate
# Password length validation based on setting
if !password.nil? && password.size < Setting.password_min_length.to_i
errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
end
end
private
# Return password digest # Return password digest
def self.hash_password(clear_password) def self.hash_password(clear_password)
Digest::SHA1.hexdigest(clear_password || "") Digest::SHA1.hexdigest(clear_password || "")
@@ -353,7 +308,7 @@ class AnonymousUser < User
# Overrides a few properties # Overrides a few properties
def logged?; false end def logged?; false end
def admin; false end def admin; false end
def name(*args); I18n.t(:label_user_anonymous) end def name; 'Anonymous' end
def mail; nil end def mail; nil end
def time_zone; nil end def time_zone; nil end
def rss_key; nil end def rss_key; nil end
+10 -72
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,31 +16,16 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Version < ActiveRecord::Base class Version < ActiveRecord::Base
after_update :update_issues_from_sharing_change before_destroy :check_integrity
belongs_to :project belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
acts_as_customizable
acts_as_attachable :view_permission => :view_files, acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files :delete_permission => :manage_files
VERSION_STATUSES = %w(open locked closed)
VERSION_SHARINGS = %w(none descendants hierarchy tree system)
validates_presence_of :name validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id] validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 60 validates_length_of :name, :maximum => 60
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
validates_inclusion_of :status, :in => VERSION_STATUSES
validates_inclusion_of :sharing, :in => VERSION_SHARINGS
named_scope :open, :conditions => {:status => 'open'}
named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
# Returns true if +user+ or current user is allowed to view the version
def visible?(user=User.current)
user.allowed_to?(:view_issues, self.project)
end
def start_date def start_date
effective_date effective_date
@@ -60,21 +45,11 @@ class Version < ActiveRecord::Base
@spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
end end
def closed?
status == 'closed'
end
def open?
status == 'open'
end
# Returns true if the version is completed: due date reached and no open issues # Returns true if the version is completed: due date reached and no open issues
def completed? def completed?
effective_date && (effective_date <= Date.today) && (open_issues_count == 0) effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
end end
# Returns the completion percentage of this version based on the amount of open/closed issues
# and the time spent on the open issues.
def completed_pourcent def completed_pourcent
if issues_count == 0 if issues_count == 0
0 0
@@ -85,7 +60,6 @@ class Version < ActiveRecord::Base
end end
end end
# Returns the percentage of issues that have been marked as 'closed'.
def closed_pourcent def closed_pourcent
if issues_count == 0 if issues_count == 0
0 0
@@ -104,12 +78,10 @@ class Version < ActiveRecord::Base
@issue_count ||= fixed_issues.count @issue_count ||= fixed_issues.count
end end
# Returns the total amount of open issues for this version.
def open_issues_count def open_issues_count
@open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status) @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
end end
# Returns the total amount of closed issues for this version.
def closed_issues_count def closed_issues_count
@closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status) @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
end end
@@ -133,38 +105,9 @@ class Version < ActiveRecord::Base
end end
end end
# Returns the sharings that +user+ can set the version to private
def allowed_sharings(user = User.current) def check_integrity
VERSION_SHARINGS.select do |s| raise "Can't delete version" if self.fixed_issues.find(:first)
if sharing == s
true
else
case s
when 'system'
# Only admin users can set a systemwide sharing
user.admin?
when 'hierarchy', 'tree'
# Only users allowed to manage versions of the root project can
# set sharing to hierarchy or tree
project.nil? || user.allowed_to?(:manage_versions, project.root)
else
true
end
end
end
end
private
# Update the issue's fixed versions. Used if a version's sharing changes.
def update_issues_from_sharing_change
if sharing_changed?
if VERSION_SHARINGS.index(sharing_was).nil? ||
VERSION_SHARINGS.index(sharing).nil? ||
VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
Issue.update_versions_from_sharing_change self
end
end
end end
# Returns the average estimated time of assigned issues # Returns the average estimated time of assigned issues
@@ -181,22 +124,17 @@ class Version < ActiveRecord::Base
@estimated_average @estimated_average
end end
# Returns the total progress of open or closed issues. The returned percentage takes into account # Returns the total progress of open or closed issues
# the amount of estimated time set for this version.
#
# Examples:
# issues_progress(true) => returns the progress percentage for open issues.
# issues_progress(false) => returns the progress percentage for closed issues.
def issues_progress(open) def issues_progress(open)
@issues_progress ||= {} @issues_progress ||= {}
@issues_progress[open] ||= begin @issues_progress[open] ||= begin
progress = 0 progress = 0
if issues_count > 0 if issues_count > 0
ratio = open ? 'done_ratio' : 100 ratio = open ? 'done_ratio' : 100
done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}", done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
:include => :status, :include => :status,
:conditions => ["is_closed = ?", !open]).to_f :conditions => ["is_closed = ?", !open]).to_f
progress = done / (estimated_average * issues_count) progress = done / (estimated_average * issues_count)
end end
progress progress
-22
View File
@@ -1,22 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class VersionCustomField < CustomField
def type_name
:label_version_plural
end
end
+1 -36
View File
@@ -22,44 +22,9 @@ class Watcher < ActiveRecord::Base
validates_presence_of :user validates_presence_of :user
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
# Unwatch things that users are no longer allowed to view
def self.prune(options={})
if options.has_key?(:user)
prune_single_user(options[:user], options)
else
pruned = 0
User.find(:all, :conditions => "id IN (SELECT DISTINCT user_id FROM #{table_name})").each do |user|
pruned += prune_single_user(user, options)
end
pruned
end
end
protected protected
def validate def validate
errors.add :user_id, :invalid unless user.nil? || user.active? errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active?
end
private
def self.prune_single_user(user, options={})
return unless user.is_a?(User)
pruned = 0
find(:all, :conditions => {:user_id => user.id}).each do |watcher|
next if watcher.watchable.nil?
if options.has_key?(:project)
next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project]
end
if watcher.watchable.respond_to?(:visible?)
unless watcher.watchable.visible?(user)
watcher.destroy
pruned += 1
end
end
end
pruned
end end
end end

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